From 44ed77b9408f69d86f332eb042b75beb520d543c Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Sat, 9 Oct 2021 09:57:17 +0200 Subject: [PATCH 01/95] python 3.9 support added --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index ddfca34..07a4b78 100644 --- a/setup.py +++ b/setup.py @@ -20,6 +20,7 @@ def readme(): 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', ], keywords='struct', author='Andrea Bonomi', From 517dfc769bb884bbda4444a78ac2afa4afdbf249 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Sat, 9 Oct 2021 09:57:32 +0200 Subject: [PATCH 02/95] Github workflows --- .github/workflows/release.yml | 91 +++++++++++++++++++++++++++++++++++ .github/workflows/tests.yml | 58 ++++++++++++++++++++++ changelog.txt | 3 +- requirements-dev.txt | 1 + requirements.txt | 0 5 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/tests.yml create mode 100644 requirements.txt diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..75addee --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,91 @@ +name: Releases + +on: + push: + tags: + - v* + +jobs: + + wait: + name: Wait for tests + runs-on: ubuntu-latest + + steps: + - uses: fountainhead/action-wait-for-check@v1.0.0 + id: wait-for-tests + with: + token: ${{ secrets.GITHUB_TOKEN }} + checkName: Tests done + ref: ${{ github.sha }} + timeoutSeconds: 3600 + + - name: Fail the Build + uses: cutenode/action-always-fail@v1 + if: steps.wait-for-tests.outputs.conclusion != 'success' + + build: + name: Build package + runs-on: ubuntu-latest + needs: [wait] + + strategy: + matrix: + python-version: ['3.9'] + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Cache pip + uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-python-${{ matrix.python }}-pip-${{ hashFiles('**/requirements*.txt') }}-git-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-python-${{ matrix.python }}-pip-${{ hashFiles('**/requirements*.txt') }} + ${{ runner.os }}-python-${{ matrix.python }}-pip- + ${{ runner.os }}-python + ${{ runner.os }}- + + - name: Upgrade pip and install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install --upgrade setuptools wheel + python -m pip install -r requirements.txt + python -m pip install -r requirements-dev.txt + + - name: Build package + run: python setup.py bdist_wheel sdist + + - name: Check package + run: twine check dist/* + + - name: Publish package + env: + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + twine upload --skip-existing dist/* + + release: + name: Release version + runs-on: ubuntu-latest + needs: [wait, build] + + steps: + - name: Create release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: ${{ github.ref }} + draft: false + prerelease: false diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..72c04d7 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,58 @@ +name: Tests + +on: + push: + pull_request: + +jobs: + tests: + name: Run tests (Python ${{matrix.python}} + + strategy: + matrix: + python: + - "3.6" + - "3.7" + - "3.8" + - "3.9" + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + + - name: Cache pip + uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-python-${{ matrix.python }}-pip-${{ hashFiles('**/requirements*.txt') }}-git-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-python-${{ matrix.python }}-pip-${{ hashFiles('**/requirements*.txt') }} + ${{ runner.os }}-python-${{ matrix.python }}-pip- + ${{ runner.os }}-python-${{ matrix.python }}- + ${{ runner.os }}-python + ${{ runner.os }}- + + - name: Upgrade pip and install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install --upgrade setuptools wheel + python -m pip install -r requirements.txt + + - name: Run tests + run: python setup.py test + + all_done: + name: Tests done + runs-on: ubuntu-latest + needs: [tests] + + steps: + - name: All done + run: echo 1 diff --git a/changelog.txt b/changelog.txt index d24f12d..76ddc4d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -103,4 +103,5 @@ ### Improved - refactoring - +- Python 3.9 support +- Github workfows diff --git a/requirements-dev.txt b/requirements-dev.txt index 5f96398..6f1aad8 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,3 +4,4 @@ mypy sphinx tox black +twine<3.4 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 From 4c05484c7675115fb7f58b3ba3b7e9c24ae29bee Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Tue, 23 Aug 2022 08:18:03 +0000 Subject: [PATCH 03/95] CODE_OF_CONDUCT.md added --- CODE_OF_CONDUCT.md | 128 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..1253724 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +andrea.bonomi@gmail.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. From 5e114ebd47cbc0308dcaacf3a5ab29d25abec88c Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Tue, 23 Aug 2022 08:25:41 +0000 Subject: [PATCH 04/95] black code style --- cstruct/__init__.py | 20 +- cstruct/abstract.py | 21 +- cstruct/base.py | 79 +++--- cstruct/c_parser.py | 93 +++---- cstruct/cstruct.py | 8 +- cstruct/mem_cstruct.py | 9 +- pyproject.toml | 2 + setup.cfg | 2 +- setup.py | 63 ++--- tests/__init__.py | 0 tests/test_alignment.py | 19 +- tests/test_cstruct.py | 555 +++++++++++++++++++++++++++++++++++++-- tests/test_define.py | 16 +- tests/test_memcstruct.py | 555 +++++++++++++++++++++++++++++++++++++-- tests/test_union.py | 62 ++++- 15 files changed, 1285 insertions(+), 219 deletions(-) create mode 100644 pyproject.toml create mode 100644 tests/__init__.py diff --git a/cstruct/__init__.py b/cstruct/__init__.py index 63e223f..5005a97 100644 --- a/cstruct/__init__.py +++ b/cstruct/__init__.py @@ -24,17 +24,13 @@ # IN THE SOFTWARE. # -__author__ = 'Andrea Bonomi ' +__author__ = 'Andrea Bonomi ' __license__ = 'MIT' __version__ = '2.1' __date__ = '15 August 2013' import struct -from typing import ( - Any, - Dict, - Type -) +from typing import Any, Dict, Type from .base import ( LITTLE_ENDIAN, BIG_ENDIAN, @@ -44,7 +40,7 @@ TYPEDEFS, C_TYPE_TO_FORMAT, CHAR_ZERO, - EMPTY_BYTES_STRING + EMPTY_BYTES_STRING, ) from .abstract import CStructMeta, AbstractCStruct from .cstruct import CStruct @@ -62,9 +58,10 @@ 'undef', 'typedef', 'sizeof', - 'parse' + 'parse', ] + def define(key: str, value: Any) -> None: """ Define a constant that can be used in the C struct @@ -74,6 +71,7 @@ def define(key: str, value: Any) -> None: """ DEFINES[key] = value + def undef(key: str) -> None: """ Undefine a symbol that was previously defined with define @@ -82,6 +80,7 @@ def undef(key: str) -> None: """ del DEFINES[key] + def typedef(type_: str, alias: str) -> None: """ Define an alias name for a data type @@ -91,6 +90,7 @@ def typedef(type_: str, alias: str) -> None: """ TYPEDEFS[alias] = type_ + def sizeof(type_: str) -> int: """ Return the size of the type. @@ -116,7 +116,8 @@ def sizeof(type_: str) -> int: else: return struct.calcsize(ttype) -def parse(__struct__: str , __cls__: Type[Any] = None, **kargs: Any) -> AbstractCStruct: + +def parse(__struct__: str, __cls__: Type[Any] = None, **kargs: Any) -> AbstractCStruct: """ Return a new class mapping a C struct/union definition. @@ -130,4 +131,3 @@ def parse(__struct__: str , __cls__: Type[Any] = None, **kargs: Any) -> Abstract if __cls__ is None: __cls__ = CStruct return __cls__.parse(__struct__, **kargs) - diff --git a/cstruct/abstract.py b/cstruct/abstract.py index 19b7511..1f8667c 100644 --- a/cstruct/abstract.py +++ b/cstruct/abstract.py @@ -28,15 +28,12 @@ from typing import cast, Any, BinaryIO, Optional, Type, Union from .base import STRUCTS import hashlib -from .c_parser import (parse_struct, parse_def, Tokens) +from .c_parser import parse_struct, parse_def, Tokens -__all__ = [ - 'CStructMeta', - 'AbstractCStruct' -] +__all__ = ['CStructMeta', 'AbstractCStruct'] -class CStructMeta(ABCMeta): +class CStructMeta(ABCMeta): def __new__(cls, name: str, bases, dct): __struct__ = dct.get("__struct__", None) dct['__cls__'] = bases[0] @@ -60,16 +57,16 @@ def __len__(cls) -> int: @property def size(cls) -> int: - """ Structure size (in bytes) """ + """Structure size (in bytes)""" return cls.__size__ + # Workaround for Python 2.x/3.x metaclass, thanks to # http://mikewatkins.ca/2008/11/29/python-2-and-3-metaclasses/#using-the-metaclass-in-python-2-x-and-3-x -_CStructParent = CStructMeta('_CStructParent', (object, ), {}) +_CStructParent = CStructMeta('_CStructParent', (object,), {}) class AbstractCStruct(_CStructParent): - def __init__(self, buffer=None, **kargs) -> None: if buffer is not None: self.unpack(buffer) @@ -135,16 +132,16 @@ def clear(self) -> None: self.unpack(None) def __len__(self) -> int: - """ Structure size (in bytes) """ + """Structure size (in bytes)""" return cast(int, self.__size__) @property def size(self) -> int: - """ Structure size (in bytes) """ + """Structure size (in bytes)""" return self.__size__ def __eq__(self, other: Any) -> bool: - return (isinstance(other, self.__class__) and self.__dict__ == other.__dict__) + return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ def __ne__(self, other: Any) -> bool: return not self.__eq__(other) diff --git a/cstruct/base.py b/cstruct/base.py index 6b52c67..e6546bb 100644 --- a/cstruct/base.py +++ b/cstruct/base.py @@ -36,7 +36,7 @@ 'TYPEDEFS', 'C_TYPE_TO_FORMAT', 'EMPTY_BYTES_STRING', - 'CHAR_ZERO' + 'CHAR_ZERO', ] # little-endian, std. size & alignment @@ -46,53 +46,50 @@ # native order, size & alignment NATIVE_ORDER = '@' -STRUCTS: Dict[str, Type[Any]] = { -} +STRUCTS: Dict[str, Type[Any]] = {} -DEFINES: Dict[str, Any] = { -} +DEFINES: Dict[str, Any] = {} TYPEDEFS: Dict[str, str] = { - 'short int': 'short', - 'unsigned short int': 'unsigned short', - 'ushort': 'unsigned short', - 'long int': 'long', - 'unsigned long int': 'unsigned long', - 'int8_t': 'int8', - 'uint8_t': 'uint8', - 'int16_t': 'int16', - 'uint16_t': 'uint16', - 'int32_t': 'int32', - 'uint32_t': 'uint32', - 'int64_t': 'int64', - 'uint64_t': 'uint64', + 'short int': 'short', + 'unsigned short int': 'unsigned short', + 'ushort': 'unsigned short', + 'long int': 'long', + 'unsigned long int': 'unsigned long', + 'int8_t': 'int8', + 'uint8_t': 'uint8', + 'int16_t': 'int16', + 'uint16_t': 'uint16', + 'int32_t': 'int32', + 'uint32_t': 'uint32', + 'int64_t': 'int64', + 'uint64_t': 'uint64', } C_TYPE_TO_FORMAT: Dict[str, str] = { - 'char': 's', - 'signed char': 'b', - 'unsigned char': 'B', - 'short': 'h', - 'unsigned short': 'H', - 'int': 'i', - 'unsigned int': 'I', - 'long': 'l', - 'unsigned long': 'L', - 'long long': 'q', - 'unsigned long long': 'Q', - 'float': 'f', - 'double': 'd', - 'void *': 'P', - 'int8': 'b', - 'uint8': 'B', - 'int16': 'h', - 'uint16': 'H', - 'int32': 'i', - 'uint32': 'I', - 'int64': 'q', - 'uint64': 'Q', + 'char': 's', + 'signed char': 'b', + 'unsigned char': 'B', + 'short': 'h', + 'unsigned short': 'H', + 'int': 'i', + 'unsigned int': 'I', + 'long': 'l', + 'unsigned long': 'L', + 'long long': 'q', + 'unsigned long long': 'Q', + 'float': 'f', + 'double': 'd', + 'void *': 'P', + 'int8': 'b', + 'uint8': 'B', + 'int16': 'h', + 'uint16': 'H', + 'int32': 'i', + 'uint32': 'I', + 'int64': 'q', + 'uint64': 'Q', } EMPTY_BYTES_STRING = bytes() CHAR_ZERO = bytes('\0', 'ascii') - diff --git a/cstruct/c_parser.py b/cstruct/c_parser.py index 81d33fd..814878f 100644 --- a/cstruct/c_parser.py +++ b/cstruct/c_parser.py @@ -26,21 +26,8 @@ import re import struct -from typing import ( - Union, - Optional, - Any, - Tuple, - Dict, - Type -) -from .base import ( - NATIVE_ORDER, - DEFINES, - TYPEDEFS, - STRUCTS, - C_TYPE_TO_FORMAT -) +from typing import Union, Optional, Any, Tuple, Dict, Type +from .base import NATIVE_ORDER, DEFINES, TYPEDEFS, STRUCTS, C_TYPE_TO_FORMAT def align(__byte_order__: Optional[str]) -> bool: @@ -48,7 +35,6 @@ def align(__byte_order__: Optional[str]) -> bool: class FieldType(object): - def __init__(self, vtype: str, vlen: int, vsize: int, fmt: str, offset: int, flexible_array: bool) -> None: """ Struct/Union field @@ -68,14 +54,14 @@ def __init__(self, vtype: str, vlen: int, vsize: int, fmt: str, offset: int, fle self.flexible_array = flexible_array def unpack_from(self, buffer: bytes, offset: int = 0) -> Any: - if self.flexible_array: # TODO - raise NotImplementedError("Flexible array member are not supported") # pragma: no cover - if isinstance(self.vtype, type): # is class - if self.vlen == 1: # single struct/union + if self.flexible_array: # TODO + raise NotImplementedError("Flexible array member are not supported") # pragma: no cover + if isinstance(self.vtype, type): # is class + if self.vlen == 1: # single struct/union result = self.vtype() result.unpack_from(buffer, self.offset + offset) return result - else: # multiple struct/union + else: # multiple struct/union result = [] for j in range(0, self.vlen): sub_struct = self.vtype() @@ -101,16 +87,12 @@ def is_array(self) -> bool: class Tokens(object): - def __init__(self, __struct__: str) -> None: # remove the comments - st = __struct__.replace("*/","*/\n") - st = " ".join(re.split("/\*.*\*/",st)) + st = __struct__.replace("*/", "*/\n") + st = " ".join(re.split("/\*.*\*/", st)) st = "\n".join([s.split("//")[0] for s in st.split("\n")]) - st = (st.replace("\n", " ") - .replace(";", " ; ") - .replace("{", " { ") - .replace("}", " } ")) + st = st.replace("\n", " ").replace(";", " ; ").replace("{", " { ").replace("}", " } ") self.tokens = st.split() def pop(self) -> str: @@ -129,9 +111,9 @@ def __str__(self) -> str: return str(self.tokens) -def parse_type(tokens: Tokens, - __cls__: Type[Any], # Type['AbstractCStruct'], - __byte_order__: Optional[str]) -> Tuple[str, int, int, str, bool, int]: +def parse_type( + tokens: Tokens, __cls__: Type[Any], __byte_order__: Optional[str] # Type['AbstractCStruct'], +) -> Tuple[str, int, int, str, bool, int]: if len(tokens) < 2: raise Exception("Parsing error") vtype = tokens.pop() @@ -173,14 +155,14 @@ def parse_type(tokens: Tokens, while vtype in TYPEDEFS: vtype = TYPEDEFS[vtype] # calculate fmt - if vtype.startswith('struct ') or vtype.startswith('union '): # struct/union + if vtype.startswith('struct ') or vtype.startswith('union '): # struct/union # __is_union__ = vtype.startswith('union ') kind, vtype = vtype.split(' ', 1) - if tokens.get() == '{': # Named nested struct + if tokens.get() == '{': # Named nested struct tokens.push(vtype) tokens.push(kind) tvtype = __cls__.parse(tokens, __name__=vtype, __byte_order__=__byte_order__) - elif vtype == '{': # Unnamed nested struct + elif vtype == '{': # Unnamed nested struct tokens.push(vtype) tokens.push(kind) tvtype = __cls__.parse(tokens, __byte_order__=__byte_order__) @@ -193,7 +175,7 @@ def parse_type(tokens: Tokens, fmt = str(vlen * tvtype.size) + ttype # alignment/ alignment = tvtype.__alignment__ - else: # other types + else: # other types try: ttype = C_TYPE_TO_FORMAT[vtype] except KeyError: @@ -207,34 +189,35 @@ def parse_type(tokens: Tokens, return tvtype, vlen, vsize, fmt, flexible_array, alignment -def parse_def(__def__: Union[str, Tokens], - __cls__: Type[Any], # Type['AbstractCStruct'], - __byte_order__: Optional[str] = None, - **kargs: Any) -> Dict[str, Any]: +def parse_def( + __def__: Union[str, Tokens], __cls__: Type[Any], __byte_order__: Optional[str] = None, **kargs: Any # Type['AbstractCStruct'], +) -> Dict[str, Any]: # naive C struct parsing if isinstance(__def__, Tokens): tokens = __def__ else: tokens = Tokens(__def__) kind = tokens.pop() - if kind not in [ 'struct', 'union' ]: + if kind not in ['struct', 'union']: raise Exception("struct or union expected - " + kind) __is_union__ = kind == 'union' vtype = tokens.pop() - if tokens.get() == '{': # Named nested struct + if tokens.get() == '{': # Named nested struct tokens.pop() return parse_struct(tokens, __cls__=__cls__, __is_union__=__is_union__, __byte_order__=__byte_order__) - elif vtype == '{': # Unnamed nested struct + elif vtype == '{': # Unnamed nested struct return parse_struct(tokens, __cls__=__cls__, __is_union__=__is_union__, __byte_order__=__byte_order__) else: raise Exception("%s definition expected" % vtype) -def parse_struct(__struct__: Union[str, Tokens], - __cls__: Type[Any], # Type['AbstractCStruct'], - __is_union__: bool = False, - __byte_order__: Optional[str] = None, - **kargs: Any) -> Dict[str, Any]: +def parse_struct( + __struct__: Union[str, Tokens], + __cls__: Type[Any], # Type['AbstractCStruct'], + __is_union__: bool = False, + __byte_order__: Optional[str] = None, + **kargs: Any +) -> Dict[str, Any]: # naive C struct parsing __is_union__ = bool(__is_union__) fields = [] @@ -261,29 +244,29 @@ def parse_struct(__struct__: Union[str, Tokens], # align stuct if byte order is native if not __is_union__ and align(__byte_order__) and vtype != 'char': modulo = offset % alignment - if modulo: # not aligned to the field size + if modulo: # not aligned to the field size delta = alignment - modulo offset = offset + delta fields_types[vname] = FieldType(vtype, vlen, vsize, fmt, offset, flexible_array) - if not __is_union__: # C struct + if not __is_union__: # C struct offset = offset + vsize t = tokens.pop() if t != ';': - raise(Exception("; expected but %s found" % t)) + raise (Exception("; expected but %s found" % t)) - if __is_union__: # C union + if __is_union__: # C union # Calculate the union size as size of its largest element size = max([struct.calcsize(x.fmt) for x in fields_types.values()]) fmt = '%ds' % size - else: # C struct + else: # C struct fmt = "".join(fmt) # add padding to stuct if byte order is native if not __is_union__ and align(__byte_order__): modulo = offset % max_alignment - if modulo: # not aligned to the max field size + if modulo: # not aligned to the max field size delta = max_alignment - modulo offset = offset + delta - size = offset # (offset is calculated as size sum) + size = offset # (offset is calculated as size sum) # Add the byte order as prefix if __byte_order__ is not None: @@ -296,7 +279,7 @@ def parse_struct(__struct__: Union[str, Tokens], '__size__': size, '__is_union__': __is_union__, '__byte_order__': __byte_order__, - '__alignment__': max_alignment + '__alignment__': max_alignment, } # # Add the missing fields to the class diff --git a/cstruct/cstruct.py b/cstruct/cstruct.py index ee2e9d7..a04a85d 100644 --- a/cstruct/cstruct.py +++ b/cstruct/cstruct.py @@ -26,7 +26,8 @@ from typing import Optional from .base import CHAR_ZERO -from .abstract import (CStructMeta, AbstractCStruct) +from .abstract import CStructMeta, AbstractCStruct + class CStruct(AbstractCStruct): """ @@ -67,11 +68,11 @@ def pack(self) -> bytes: for field in self.__fields__: field_type = self.__fields_types__[field] if isinstance(field_type.vtype, CStructMeta): - if field_type.vlen == 1: # single struct + if field_type.vlen == 1: # single struct v = getattr(self, field, field_type.vtype()) v = v.pack() result.append(v) - else: # multiple struct + else: # multiple struct values = getattr(self, field, []) for j in range(0, field_type.vlen): try: @@ -84,4 +85,3 @@ def pack(self) -> bytes: data = getattr(self, field) result.append(field_type.pack(data)) return bytes().join(result) - diff --git a/cstruct/mem_cstruct.py b/cstruct/mem_cstruct.py index f66d3af..f1f1da0 100644 --- a/cstruct/mem_cstruct.py +++ b/cstruct/mem_cstruct.py @@ -24,18 +24,13 @@ # IN THE SOFTWARE. # -from typing import ( - Any, - List, - Optional -) +from typing import Any, List, Optional import ctypes import struct -from .abstract import (CStructMeta, AbstractCStruct) +from .abstract import CStructMeta, AbstractCStruct class CStructList(List[Any]): - def __init__(self, values: List[Any], name: str, parent: Optional['MemCStruct'] = None) -> None: super().__init__(values) self.name = name diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c70aaf6 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[tool.black] +line-length = 132 diff --git a/setup.cfg b/setup.cfg index 54b2a2b..c5ee45d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,5 +2,5 @@ universal=1 [flake8] -max-line-length=120 +max-line-length=132 ignore: E401, W504, E221 diff --git a/setup.py b/setup.py index 07a4b78..1792b0d 100644 --- a/setup.py +++ b/setup.py @@ -1,41 +1,44 @@ from setuptools import setup, find_packages from cstruct import __version__ + def readme(): with open('README.md') as f: return f.read() -setup(name='cstruct', - version=__version__, - description="C-style structs for Python", - long_description="""\ + +setup( + name='cstruct', + version=__version__, + description="C-style structs for Python", + long_description="""\ Convert C struct definitions into Python classes with methods for serializing/deserializing.""", - classifiers=[ - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - ], - keywords='struct', - author='Andrea Bonomi', - author_email='andrea.bonomi@gmail.com', - url='http://github.com/andreax79/python-cstruct', - license='MIT', - packages=find_packages(exclude=['ez_setup', 'examples']), - include_package_data=True, - zip_safe=True, - install_requires=[ - # -*- Extra requirements: -*- - ], - entry_points=""" + classifiers=[ + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Software Development :: Libraries :: Python Modules', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + ], + keywords='struct', + author='Andrea Bonomi', + author_email='andrea.bonomi@gmail.com', + url='http://github.com/andreax79/python-cstruct', + license='MIT', + packages=find_packages(exclude=['ez_setup', 'examples']), + include_package_data=True, + zip_safe=True, + install_requires=[ + # -*- Extra requirements: -*- + ], + entry_points=""" # -*- Entry points: -*- """, - test_suite='nose.collector', - tests_require=['nose'], + test_suite='tests', + tests_require=['pytest'], ) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_alignment.py b/tests/test_alignment.py index 21a6ebe..73bd1dc 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -#***************************************************************************** +# ***************************************************************************** # # Copyright (c) 2013-2019 Andrea Bonomi # @@ -23,10 +23,10 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # -#***************************************************************************** +# ***************************************************************************** from unittest import TestCase, main -from cstruct import (sizeof, typedef, define, CStruct, NATIVE_ORDER) +from cstruct import sizeof, typedef, define, CStruct, NATIVE_ORDER define("UT_NAMESIZE", 32) define("UT_LINESIZE", 32) @@ -35,6 +35,7 @@ typedef("int", "pid_t") typedef("long", "time_t") + class ExitStatus(CStruct): __def__ = """ struct { @@ -42,6 +43,8 @@ class ExitStatus(CStruct): short e_exit; /* Process exit status. */ } """ + + class Timeval(CStruct): __def__ = """ struct { @@ -50,6 +53,7 @@ class Timeval(CStruct): } """ + class Utmp(CStruct): __byte_order__ = NATIVE_ORDER __def__ = """ @@ -114,6 +118,7 @@ class AllTypes(CStruct): } """ + class Foo1(CStruct): __byte_order__ = NATIVE_ORDER __def__ = """ @@ -124,6 +129,7 @@ class Foo1(CStruct): } """ + class Foo2(CStruct): __byte_order__ = NATIVE_ORDER __def__ = """ @@ -135,6 +141,7 @@ class Foo2(CStruct): } """ + class Foo3(CStruct): __byte_order__ = NATIVE_ORDER __def__ = """ @@ -144,6 +151,7 @@ class Foo3(CStruct): } """ + class Foo4(CStruct): __byte_order__ = NATIVE_ORDER __def__ = """ @@ -153,6 +161,7 @@ class Foo4(CStruct): } """ + class Foo5(CStruct): __byte_order__ = NATIVE_ORDER __def__ = """ @@ -165,6 +174,7 @@ class Foo5(CStruct): } """ + class Foo10(CStruct): __byte_order__ = NATIVE_ORDER __def__ = """ @@ -175,8 +185,8 @@ class Foo10(CStruct): } """ -class TestAlignament(TestCase): +class TestAlignament(TestCase): def test_utmp_sizeof(self): self.assertEqual(sizeof("struct Utmp"), 384) @@ -200,5 +210,6 @@ def test_foo5_sizeof(self): def test_foo10_sizeof(self): self.assertEqual(sizeof("struct Foo10"), 24) + if __name__ == '__main__': main() diff --git a/tests/test_cstruct.py b/tests/test_cstruct.py index ebab39d..b82835f 100644 --- a/tests/test_cstruct.py +++ b/tests/test_cstruct.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -#***************************************************************************** +# ***************************************************************************** # # Copyright (c) 2013-2019 Andrea Bonomi # @@ -23,16 +23,531 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # -#***************************************************************************** +# ***************************************************************************** from unittest import TestCase, main import cstruct -from cstruct import (sizeof, typedef) +from cstruct import sizeof, typedef import io import os -MBR_DATA = bytes([0xeb,0x48,0x90,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x3,0x2,0xff,0x0,0x0,0x80,0x61,0xcb,0x4,0x0,0x0,0x8,0xfa,0x80,0xca,0x80,0xea,0x53,0x7c,0x0,0x0,0x31,0xc0,0x8e,0xd8,0x8e,0xd0,0xbc,0x0,0x20,0xfb,0xa0,0x40,0x7c,0x3c,0xff,0x74,0x2,0x88,0xc2,0x52,0xbe,0x79,0x7d,0xe8,0x34,0x1,0xf6,0xc2,0x80,0x74,0x54,0xb4,0x41,0xbb,0xaa,0x55,0xcd,0x13,0x5a,0x52,0x72,0x49,0x81,0xfb,0x55,0xaa,0x75,0x43,0xa0,0x41,0x7c,0x84,0xc0,0x75,0x5,0x83,0xe1,0x1,0x74,0x37,0x66,0x8b,0x4c,0x10,0xbe,0x5,0x7c,0xc6,0x44,0xff,0x1,0x66,0x8b,0x1e,0x44,0x7c,0xc7,0x4,0x10,0x0,0xc7,0x44,0x2,0x1,0x0,0x66,0x89,0x5c,0x8,0xc7,0x44,0x6,0x0,0x70,0x66,0x31,0xc0,0x89,0x44,0x4,0x66,0x89,0x44,0xc,0xb4,0x42,0xcd,0x13,0x72,0x5,0xbb,0x0,0x70,0xeb,0x7d,0xb4,0x8,0xcd,0x13,0x73,0xa,0xf6,0xc2,0x80,0xf,0x84,0xf0,0x0,0xe9,0x8d,0x0,0xbe,0x5,0x7c,0xc6,0x44,0xff,0x0,0x66,0x31,0xc0,0x88,0xf0,0x40,0x66,0x89,0x44,0x4,0x31,0xd2,0x88,0xca,0xc1,0xe2,0x2,0x88,0xe8,0x88,0xf4,0x40,0x89,0x44,0x8,0x31,0xc0,0x88,0xd0,0xc0,0xe8,0x2,0x66,0x89,0x4,0x66,0xa1,0x44,0x7c,0x66,0x31,0xd2,0x66,0xf7,0x34,0x88,0x54,0xa,0x66,0x31,0xd2,0x66,0xf7,0x74,0x4,0x88,0x54,0xb,0x89,0x44,0xc,0x3b,0x44,0x8,0x7d,0x3c,0x8a,0x54,0xd,0xc0,0xe2,0x6,0x8a,0x4c,0xa,0xfe,0xc1,0x8,0xd1,0x8a,0x6c,0xc,0x5a,0x8a,0x74,0xb,0xbb,0x0,0x70,0x8e,0xc3,0x31,0xdb,0xb8,0x1,0x2,0xcd,0x13,0x72,0x2a,0x8c,0xc3,0x8e,0x6,0x48,0x7c,0x60,0x1e,0xb9,0x0,0x1,0x8e,0xdb,0x31,0xf6,0x31,0xff,0xfc,0xf3,0xa5,0x1f,0x61,0xff,0x26,0x42,0x7c,0xbe,0x7f,0x7d,0xe8,0x40,0x0,0xeb,0xe,0xbe,0x84,0x7d,0xe8,0x38,0x0,0xeb,0x6,0xbe,0x8e,0x7d,0xe8,0x30,0x0,0xbe,0x93,0x7d,0xe8,0x2a,0x0,0xeb,0xfe,0x47,0x52,0x55,0x42,0x20,0x0,0x47,0x65,0x6f,0x6d,0x0,0x48,0x61,0x72,0x64,0x20,0x44,0x69,0x73,0x6b,0x0,0x52,0x65,0x61,0x64,0x0,0x20,0x45,0x72,0x72,0x6f,0x72,0x0,0xbb,0x1,0x0,0xb4,0xe,0xcd,0x10,0xac,0x3c,0x0,0x75,0xf4,0xc3,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x80,0x0,0x2,0x0,0x83,0xfe,0x3f,0x86,0x1,0x0,0x0,0x0,0xc6,0x17,0x21,0x0,0x0,0x0,0x1,0x87,0x8e,0xfe,0xff,0xff,0xc7,0x17,0x21,0x0,0x4d,0xd3,0xde,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x55,0xaa]) +MBR_DATA = bytes( + [ + 0xEB, + 0x48, + 0x90, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x3, + 0x2, + 0xFF, + 0x0, + 0x0, + 0x80, + 0x61, + 0xCB, + 0x4, + 0x0, + 0x0, + 0x8, + 0xFA, + 0x80, + 0xCA, + 0x80, + 0xEA, + 0x53, + 0x7C, + 0x0, + 0x0, + 0x31, + 0xC0, + 0x8E, + 0xD8, + 0x8E, + 0xD0, + 0xBC, + 0x0, + 0x20, + 0xFB, + 0xA0, + 0x40, + 0x7C, + 0x3C, + 0xFF, + 0x74, + 0x2, + 0x88, + 0xC2, + 0x52, + 0xBE, + 0x79, + 0x7D, + 0xE8, + 0x34, + 0x1, + 0xF6, + 0xC2, + 0x80, + 0x74, + 0x54, + 0xB4, + 0x41, + 0xBB, + 0xAA, + 0x55, + 0xCD, + 0x13, + 0x5A, + 0x52, + 0x72, + 0x49, + 0x81, + 0xFB, + 0x55, + 0xAA, + 0x75, + 0x43, + 0xA0, + 0x41, + 0x7C, + 0x84, + 0xC0, + 0x75, + 0x5, + 0x83, + 0xE1, + 0x1, + 0x74, + 0x37, + 0x66, + 0x8B, + 0x4C, + 0x10, + 0xBE, + 0x5, + 0x7C, + 0xC6, + 0x44, + 0xFF, + 0x1, + 0x66, + 0x8B, + 0x1E, + 0x44, + 0x7C, + 0xC7, + 0x4, + 0x10, + 0x0, + 0xC7, + 0x44, + 0x2, + 0x1, + 0x0, + 0x66, + 0x89, + 0x5C, + 0x8, + 0xC7, + 0x44, + 0x6, + 0x0, + 0x70, + 0x66, + 0x31, + 0xC0, + 0x89, + 0x44, + 0x4, + 0x66, + 0x89, + 0x44, + 0xC, + 0xB4, + 0x42, + 0xCD, + 0x13, + 0x72, + 0x5, + 0xBB, + 0x0, + 0x70, + 0xEB, + 0x7D, + 0xB4, + 0x8, + 0xCD, + 0x13, + 0x73, + 0xA, + 0xF6, + 0xC2, + 0x80, + 0xF, + 0x84, + 0xF0, + 0x0, + 0xE9, + 0x8D, + 0x0, + 0xBE, + 0x5, + 0x7C, + 0xC6, + 0x44, + 0xFF, + 0x0, + 0x66, + 0x31, + 0xC0, + 0x88, + 0xF0, + 0x40, + 0x66, + 0x89, + 0x44, + 0x4, + 0x31, + 0xD2, + 0x88, + 0xCA, + 0xC1, + 0xE2, + 0x2, + 0x88, + 0xE8, + 0x88, + 0xF4, + 0x40, + 0x89, + 0x44, + 0x8, + 0x31, + 0xC0, + 0x88, + 0xD0, + 0xC0, + 0xE8, + 0x2, + 0x66, + 0x89, + 0x4, + 0x66, + 0xA1, + 0x44, + 0x7C, + 0x66, + 0x31, + 0xD2, + 0x66, + 0xF7, + 0x34, + 0x88, + 0x54, + 0xA, + 0x66, + 0x31, + 0xD2, + 0x66, + 0xF7, + 0x74, + 0x4, + 0x88, + 0x54, + 0xB, + 0x89, + 0x44, + 0xC, + 0x3B, + 0x44, + 0x8, + 0x7D, + 0x3C, + 0x8A, + 0x54, + 0xD, + 0xC0, + 0xE2, + 0x6, + 0x8A, + 0x4C, + 0xA, + 0xFE, + 0xC1, + 0x8, + 0xD1, + 0x8A, + 0x6C, + 0xC, + 0x5A, + 0x8A, + 0x74, + 0xB, + 0xBB, + 0x0, + 0x70, + 0x8E, + 0xC3, + 0x31, + 0xDB, + 0xB8, + 0x1, + 0x2, + 0xCD, + 0x13, + 0x72, + 0x2A, + 0x8C, + 0xC3, + 0x8E, + 0x6, + 0x48, + 0x7C, + 0x60, + 0x1E, + 0xB9, + 0x0, + 0x1, + 0x8E, + 0xDB, + 0x31, + 0xF6, + 0x31, + 0xFF, + 0xFC, + 0xF3, + 0xA5, + 0x1F, + 0x61, + 0xFF, + 0x26, + 0x42, + 0x7C, + 0xBE, + 0x7F, + 0x7D, + 0xE8, + 0x40, + 0x0, + 0xEB, + 0xE, + 0xBE, + 0x84, + 0x7D, + 0xE8, + 0x38, + 0x0, + 0xEB, + 0x6, + 0xBE, + 0x8E, + 0x7D, + 0xE8, + 0x30, + 0x0, + 0xBE, + 0x93, + 0x7D, + 0xE8, + 0x2A, + 0x0, + 0xEB, + 0xFE, + 0x47, + 0x52, + 0x55, + 0x42, + 0x20, + 0x0, + 0x47, + 0x65, + 0x6F, + 0x6D, + 0x0, + 0x48, + 0x61, + 0x72, + 0x64, + 0x20, + 0x44, + 0x69, + 0x73, + 0x6B, + 0x0, + 0x52, + 0x65, + 0x61, + 0x64, + 0x0, + 0x20, + 0x45, + 0x72, + 0x72, + 0x6F, + 0x72, + 0x0, + 0xBB, + 0x1, + 0x0, + 0xB4, + 0xE, + 0xCD, + 0x10, + 0xAC, + 0x3C, + 0x0, + 0x75, + 0xF4, + 0xC3, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x80, + 0x0, + 0x2, + 0x0, + 0x83, + 0xFE, + 0x3F, + 0x86, + 0x1, + 0x0, + 0x0, + 0x0, + 0xC6, + 0x17, + 0x21, + 0x0, + 0x0, + 0x0, + 0x1, + 0x87, + 0x8E, + 0xFE, + 0xFF, + 0xFF, + 0xC7, + 0x17, + 0x21, + 0x0, + 0x4D, + 0xD3, + 0xDE, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x55, + 0xAA, + ] +) class Position(cstruct.CStruct): @@ -45,6 +560,7 @@ class Position(cstruct.CStruct): } """ + class Partition(cstruct.CStruct): __byte_order__ = cstruct.LITTLE_ENDIAN __def__ = """ @@ -87,9 +603,11 @@ class Dummy(cstruct.CStruct): } """ -typedef('char', 'BYTE') + +typedef('char', 'BYTE') typedef('short', 'WORD') -typedef('int', 'DWORD') +typedef('int', 'DWORD') + class PartitionFlat(cstruct.CStruct): __byte_order__ = cstruct.LITTLE_ENDIAN @@ -106,6 +624,7 @@ class PartitionFlat(cstruct.CStruct): } """ + class PartitionNested(cstruct.CStruct): __byte_order__ = cstruct.LITTLE_ENDIAN __def__ = """ @@ -127,7 +646,6 @@ class PartitionNested(cstruct.CStruct): class TestCStruct(TestCase): - def test_len(self): mbr = MBR() self.assertEqual(len(mbr), 512) @@ -142,10 +660,10 @@ def test_unpack(self): f = io.BytesIO(MBR_DATA) mbr.unpack(f) self.assertEqual(mbr.signature[0], 0x55) - self.assertEqual(mbr.signature[1], 0xaa) + self.assertEqual(mbr.signature[1], 0xAA) self.assertEqual(mbr.partitions[0].start.head, 0) - self.assertEqual(mbr.partitions[0].end.head, 0xfe) - self.assertEqual(mbr.partitions[1].start_sect, 0x2117c7) + self.assertEqual(mbr.partitions[0].end.head, 0xFE) + self.assertEqual(mbr.partitions[1].start_sect, 0x2117C7) def test_pack(self): mbr = MBR(MBR_DATA) @@ -172,12 +690,15 @@ def test_none(self): def test_clear(self): mbr = MBR() mbr.unpack(MBR_DATA) - self.assertEqual(mbr.partitions[0].end.head, 0xfe) + self.assertEqual(mbr.partitions[0].end.head, 0xFE) mbr.clear() self.assertEqual(mbr.partitions[0].end.head, 0x00) def test_inline(self): - TestStruct = cstruct.parse('struct TestStruct { unsigned char head; unsigned char sector; unsigned char cyl; }',__byte_order__=cstruct.LITTLE_ENDIAN) + TestStruct = cstruct.parse( + 'struct TestStruct { unsigned char head; unsigned char sector; unsigned char cyl; }', + __byte_order__=cstruct.LITTLE_ENDIAN, + ) s = TestStruct(head=254, sector=63, cyl=134) p = Position(head=254, sector=63, cyl=134) self.assertEqual(s.pack(), p.pack()) @@ -191,8 +712,8 @@ def test_dummy(self): dummy.vi[i] = i * 10 dummy.f = 123.456 for i in range(0, 10): - dummy.vf[i] = 10.0 / (i+1) - dummy.vl = list(range(0, 10)); + dummy.vf[i] = 10.0 / (i + 1) + dummy.vl = list(range(0, 10)) data = dummy.pack() dummy1 = Dummy(data) for i in range(0, 10): @@ -202,11 +723,11 @@ def test_dummy(self): dummy2.vf[2] = 79 self.assertNotEqual(dummy.pack(), dummy2.pack()) dummy3 = Dummy(data) - dummy3.vl = list(range(1, 11)); + dummy3.vl = list(range(1, 11)) self.assertNotEqual(dummy.pack(), dummy3.pack()) def test_nested(self): - data = os.urandom(sizeof("struct PartitionFlat") ) + data = os.urandom(sizeof("struct PartitionFlat")) flat = PartitionFlat(data) flat.unpack(data) nested = PartitionNested(data) @@ -220,6 +741,6 @@ def test_nested(self): self.assertEqual(flat.startLBA, nested.startLBA) self.assertEqual(flat.endLBA, nested.endLBA) + if __name__ == '__main__': main() - diff --git a/tests/test_define.py b/tests/test_define.py index a6eb63a..4fc0d59 100644 --- a/tests/test_define.py +++ b/tests/test_define.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -#***************************************************************************** +# ***************************************************************************** # # Copyright (c) 2013-2019 Andrea Bonomi # @@ -23,11 +23,12 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # -#***************************************************************************** +# ***************************************************************************** from unittest import TestCase, main import cstruct -from cstruct import (define, undef, sizeof, typedef) +from cstruct import define, undef, sizeof, typedef + class Position(cstruct.CStruct): __byte_order__ = cstruct.LITTLE_ENDIAN @@ -41,7 +42,6 @@ class Position(cstruct.CStruct): class TestCaseDefine(TestCase): - def test_sizeof(self): self.assertEqual(sizeof('int'), 4) define('INIT_THREAD_SIZE', 2048 * sizeof('long')) @@ -49,19 +49,19 @@ def test_sizeof(self): self.assertEqual(sizeof('struct Position'), 3) self.assertEqual(sizeof('struct Position'), len(Position)) self.assertEqual(sizeof(Position), 3) - self.assertRaises(KeyError, lambda : sizeof('bla')) - self.assertRaises(KeyError, lambda : sizeof('struct Bla')) + self.assertRaises(KeyError, lambda: sizeof('bla')) + self.assertRaises(KeyError, lambda: sizeof('struct Bla')) def test_define(self): define('A', 10) self.assertEqual(cstruct.DEFINES['A'], 10) undef('A') - self.assertRaises(KeyError, lambda : cstruct.DEFINES['A']) + self.assertRaises(KeyError, lambda: cstruct.DEFINES['A']) def test_typedef(self): typedef('int', 'integer') self.assertEqual(sizeof('integer'), 4) + if __name__ == '__main__': main() - diff --git a/tests/test_memcstruct.py b/tests/test_memcstruct.py index 466c896..5706943 100644 --- a/tests/test_memcstruct.py +++ b/tests/test_memcstruct.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -#***************************************************************************** +# ***************************************************************************** # # Copyright (c) 2013-2019 Andrea Bonomi # @@ -23,15 +23,530 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # -#***************************************************************************** +# ***************************************************************************** from unittest import TestCase, main import cstruct -from cstruct import (sizeof, typedef) +from cstruct import sizeof, typedef import os -MBR_DATA = bytes([0xeb,0x48,0x90,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x3,0x2,0xff,0x0,0x0,0x80,0x61,0xcb,0x4,0x0,0x0,0x8,0xfa,0x80,0xca,0x80,0xea,0x53,0x7c,0x0,0x0,0x31,0xc0,0x8e,0xd8,0x8e,0xd0,0xbc,0x0,0x20,0xfb,0xa0,0x40,0x7c,0x3c,0xff,0x74,0x2,0x88,0xc2,0x52,0xbe,0x79,0x7d,0xe8,0x34,0x1,0xf6,0xc2,0x80,0x74,0x54,0xb4,0x41,0xbb,0xaa,0x55,0xcd,0x13,0x5a,0x52,0x72,0x49,0x81,0xfb,0x55,0xaa,0x75,0x43,0xa0,0x41,0x7c,0x84,0xc0,0x75,0x5,0x83,0xe1,0x1,0x74,0x37,0x66,0x8b,0x4c,0x10,0xbe,0x5,0x7c,0xc6,0x44,0xff,0x1,0x66,0x8b,0x1e,0x44,0x7c,0xc7,0x4,0x10,0x0,0xc7,0x44,0x2,0x1,0x0,0x66,0x89,0x5c,0x8,0xc7,0x44,0x6,0x0,0x70,0x66,0x31,0xc0,0x89,0x44,0x4,0x66,0x89,0x44,0xc,0xb4,0x42,0xcd,0x13,0x72,0x5,0xbb,0x0,0x70,0xeb,0x7d,0xb4,0x8,0xcd,0x13,0x73,0xa,0xf6,0xc2,0x80,0xf,0x84,0xf0,0x0,0xe9,0x8d,0x0,0xbe,0x5,0x7c,0xc6,0x44,0xff,0x0,0x66,0x31,0xc0,0x88,0xf0,0x40,0x66,0x89,0x44,0x4,0x31,0xd2,0x88,0xca,0xc1,0xe2,0x2,0x88,0xe8,0x88,0xf4,0x40,0x89,0x44,0x8,0x31,0xc0,0x88,0xd0,0xc0,0xe8,0x2,0x66,0x89,0x4,0x66,0xa1,0x44,0x7c,0x66,0x31,0xd2,0x66,0xf7,0x34,0x88,0x54,0xa,0x66,0x31,0xd2,0x66,0xf7,0x74,0x4,0x88,0x54,0xb,0x89,0x44,0xc,0x3b,0x44,0x8,0x7d,0x3c,0x8a,0x54,0xd,0xc0,0xe2,0x6,0x8a,0x4c,0xa,0xfe,0xc1,0x8,0xd1,0x8a,0x6c,0xc,0x5a,0x8a,0x74,0xb,0xbb,0x0,0x70,0x8e,0xc3,0x31,0xdb,0xb8,0x1,0x2,0xcd,0x13,0x72,0x2a,0x8c,0xc3,0x8e,0x6,0x48,0x7c,0x60,0x1e,0xb9,0x0,0x1,0x8e,0xdb,0x31,0xf6,0x31,0xff,0xfc,0xf3,0xa5,0x1f,0x61,0xff,0x26,0x42,0x7c,0xbe,0x7f,0x7d,0xe8,0x40,0x0,0xeb,0xe,0xbe,0x84,0x7d,0xe8,0x38,0x0,0xeb,0x6,0xbe,0x8e,0x7d,0xe8,0x30,0x0,0xbe,0x93,0x7d,0xe8,0x2a,0x0,0xeb,0xfe,0x47,0x52,0x55,0x42,0x20,0x0,0x47,0x65,0x6f,0x6d,0x0,0x48,0x61,0x72,0x64,0x20,0x44,0x69,0x73,0x6b,0x0,0x52,0x65,0x61,0x64,0x0,0x20,0x45,0x72,0x72,0x6f,0x72,0x0,0xbb,0x1,0x0,0xb4,0xe,0xcd,0x10,0xac,0x3c,0x0,0x75,0xf4,0xc3,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x80,0x0,0x2,0x0,0x83,0xfe,0x3f,0x86,0x1,0x0,0x0,0x0,0xc6,0x17,0x21,0x0,0x0,0x0,0x1,0x87,0x8e,0xfe,0xff,0xff,0xc7,0x17,0x21,0x0,0x4d,0xd3,0xde,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x55,0xaa]) +MBR_DATA = bytes( + [ + 0xEB, + 0x48, + 0x90, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x3, + 0x2, + 0xFF, + 0x0, + 0x0, + 0x80, + 0x61, + 0xCB, + 0x4, + 0x0, + 0x0, + 0x8, + 0xFA, + 0x80, + 0xCA, + 0x80, + 0xEA, + 0x53, + 0x7C, + 0x0, + 0x0, + 0x31, + 0xC0, + 0x8E, + 0xD8, + 0x8E, + 0xD0, + 0xBC, + 0x0, + 0x20, + 0xFB, + 0xA0, + 0x40, + 0x7C, + 0x3C, + 0xFF, + 0x74, + 0x2, + 0x88, + 0xC2, + 0x52, + 0xBE, + 0x79, + 0x7D, + 0xE8, + 0x34, + 0x1, + 0xF6, + 0xC2, + 0x80, + 0x74, + 0x54, + 0xB4, + 0x41, + 0xBB, + 0xAA, + 0x55, + 0xCD, + 0x13, + 0x5A, + 0x52, + 0x72, + 0x49, + 0x81, + 0xFB, + 0x55, + 0xAA, + 0x75, + 0x43, + 0xA0, + 0x41, + 0x7C, + 0x84, + 0xC0, + 0x75, + 0x5, + 0x83, + 0xE1, + 0x1, + 0x74, + 0x37, + 0x66, + 0x8B, + 0x4C, + 0x10, + 0xBE, + 0x5, + 0x7C, + 0xC6, + 0x44, + 0xFF, + 0x1, + 0x66, + 0x8B, + 0x1E, + 0x44, + 0x7C, + 0xC7, + 0x4, + 0x10, + 0x0, + 0xC7, + 0x44, + 0x2, + 0x1, + 0x0, + 0x66, + 0x89, + 0x5C, + 0x8, + 0xC7, + 0x44, + 0x6, + 0x0, + 0x70, + 0x66, + 0x31, + 0xC0, + 0x89, + 0x44, + 0x4, + 0x66, + 0x89, + 0x44, + 0xC, + 0xB4, + 0x42, + 0xCD, + 0x13, + 0x72, + 0x5, + 0xBB, + 0x0, + 0x70, + 0xEB, + 0x7D, + 0xB4, + 0x8, + 0xCD, + 0x13, + 0x73, + 0xA, + 0xF6, + 0xC2, + 0x80, + 0xF, + 0x84, + 0xF0, + 0x0, + 0xE9, + 0x8D, + 0x0, + 0xBE, + 0x5, + 0x7C, + 0xC6, + 0x44, + 0xFF, + 0x0, + 0x66, + 0x31, + 0xC0, + 0x88, + 0xF0, + 0x40, + 0x66, + 0x89, + 0x44, + 0x4, + 0x31, + 0xD2, + 0x88, + 0xCA, + 0xC1, + 0xE2, + 0x2, + 0x88, + 0xE8, + 0x88, + 0xF4, + 0x40, + 0x89, + 0x44, + 0x8, + 0x31, + 0xC0, + 0x88, + 0xD0, + 0xC0, + 0xE8, + 0x2, + 0x66, + 0x89, + 0x4, + 0x66, + 0xA1, + 0x44, + 0x7C, + 0x66, + 0x31, + 0xD2, + 0x66, + 0xF7, + 0x34, + 0x88, + 0x54, + 0xA, + 0x66, + 0x31, + 0xD2, + 0x66, + 0xF7, + 0x74, + 0x4, + 0x88, + 0x54, + 0xB, + 0x89, + 0x44, + 0xC, + 0x3B, + 0x44, + 0x8, + 0x7D, + 0x3C, + 0x8A, + 0x54, + 0xD, + 0xC0, + 0xE2, + 0x6, + 0x8A, + 0x4C, + 0xA, + 0xFE, + 0xC1, + 0x8, + 0xD1, + 0x8A, + 0x6C, + 0xC, + 0x5A, + 0x8A, + 0x74, + 0xB, + 0xBB, + 0x0, + 0x70, + 0x8E, + 0xC3, + 0x31, + 0xDB, + 0xB8, + 0x1, + 0x2, + 0xCD, + 0x13, + 0x72, + 0x2A, + 0x8C, + 0xC3, + 0x8E, + 0x6, + 0x48, + 0x7C, + 0x60, + 0x1E, + 0xB9, + 0x0, + 0x1, + 0x8E, + 0xDB, + 0x31, + 0xF6, + 0x31, + 0xFF, + 0xFC, + 0xF3, + 0xA5, + 0x1F, + 0x61, + 0xFF, + 0x26, + 0x42, + 0x7C, + 0xBE, + 0x7F, + 0x7D, + 0xE8, + 0x40, + 0x0, + 0xEB, + 0xE, + 0xBE, + 0x84, + 0x7D, + 0xE8, + 0x38, + 0x0, + 0xEB, + 0x6, + 0xBE, + 0x8E, + 0x7D, + 0xE8, + 0x30, + 0x0, + 0xBE, + 0x93, + 0x7D, + 0xE8, + 0x2A, + 0x0, + 0xEB, + 0xFE, + 0x47, + 0x52, + 0x55, + 0x42, + 0x20, + 0x0, + 0x47, + 0x65, + 0x6F, + 0x6D, + 0x0, + 0x48, + 0x61, + 0x72, + 0x64, + 0x20, + 0x44, + 0x69, + 0x73, + 0x6B, + 0x0, + 0x52, + 0x65, + 0x61, + 0x64, + 0x0, + 0x20, + 0x45, + 0x72, + 0x72, + 0x6F, + 0x72, + 0x0, + 0xBB, + 0x1, + 0x0, + 0xB4, + 0xE, + 0xCD, + 0x10, + 0xAC, + 0x3C, + 0x0, + 0x75, + 0xF4, + 0xC3, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x80, + 0x0, + 0x2, + 0x0, + 0x83, + 0xFE, + 0x3F, + 0x86, + 0x1, + 0x0, + 0x0, + 0x0, + 0xC6, + 0x17, + 0x21, + 0x0, + 0x0, + 0x0, + 0x1, + 0x87, + 0x8E, + 0xFE, + 0xFF, + 0xFF, + 0xC7, + 0x17, + 0x21, + 0x0, + 0x4D, + 0xD3, + 0xDE, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x0, + 0x55, + 0xAA, + ] +) class Position(cstruct.MemCStruct): @@ -44,6 +559,7 @@ class Position(cstruct.MemCStruct): } """ + class Partition(cstruct.MemCStruct): __byte_order__ = cstruct.LITTLE_ENDIAN __def__ = """ @@ -86,9 +602,11 @@ class Dummy(cstruct.MemCStruct): } """ -typedef('char', 'BYTE') + +typedef('char', 'BYTE') typedef('short', 'WORD') -typedef('int', 'DWORD') +typedef('int', 'DWORD') + class PartitionFlat(cstruct.MemCStruct): __byte_order__ = cstruct.LITTLE_ENDIAN @@ -105,6 +623,7 @@ class PartitionFlat(cstruct.MemCStruct): } """ + class PartitionNested(cstruct.MemCStruct): __byte_order__ = cstruct.LITTLE_ENDIAN __def__ = """ @@ -124,8 +643,8 @@ class PartitionNested(cstruct.MemCStruct): } """ -class TestMemCStruct(TestCase): +class TestMemCStruct(TestCase): def test_len(self): mbr = MBR() self.assertEqual(len(mbr), 512) @@ -139,10 +658,10 @@ def test_unpack(self): mbr = MBR() mbr.unpack(MBR_DATA) self.assertEqual(mbr.signature[0], 0x55) - self.assertEqual(mbr.signature[1], 0xaa) + self.assertEqual(mbr.signature[1], 0xAA) self.assertEqual(mbr.partitions[0].start.head, 0) - self.assertEqual(mbr.partitions[0].end.head, 0xfe) - self.assertEqual(mbr.partitions[1].start_sect, 0x2117c7) + self.assertEqual(mbr.partitions[0].end.head, 0xFE) + self.assertEqual(mbr.partitions[1].start_sect, 0x2117C7) def test_pack(self): mbr = MBR(MBR_DATA) @@ -169,12 +688,14 @@ def test_none(self): def test_clear(self): mbr = MBR() mbr.unpack(MBR_DATA) - self.assertEqual(mbr.partitions[0].end.head, 0xfe) + self.assertEqual(mbr.partitions[0].end.head, 0xFE) mbr.clear() self.assertEqual(mbr.partitions[0].end.head, 0x00) def test_inline(self): - TestStruct = cstruct.MemCStruct.parse('struct { unsigned char head; unsigned char sector; unsigned char cyl; }',__byte_order__=cstruct.LITTLE_ENDIAN) + TestStruct = cstruct.MemCStruct.parse( + 'struct { unsigned char head; unsigned char sector; unsigned char cyl; }', __byte_order__=cstruct.LITTLE_ENDIAN + ) s = TestStruct(head=254, sector=63, cyl=134) p = Position(head=254, sector=63, cyl=134) self.assertEqual(s.pack(), p.pack()) @@ -188,8 +709,8 @@ def test_dummy(self): dummy.vi[i] = i * 10 dummy.f = 123.456 for i in range(0, 10): - dummy.vf[i] = 10.0 / (i+1) - dummy.vl = list(range(0, 10)); + dummy.vf[i] = 10.0 / (i + 1) + dummy.vl = list(range(0, 10)) data = dummy.pack() dummy1 = Dummy(data) for i in range(0, 10): @@ -199,11 +720,11 @@ def test_dummy(self): dummy2.vf[2] = 79 self.assertNotEqual(dummy.pack(), dummy2.pack()) dummy3 = Dummy(data) - dummy3.vl = list(range(1, 11)); + dummy3.vl = list(range(1, 11)) self.assertNotEqual(dummy.pack(), dummy3.pack()) def test_nested(self): - data = os.urandom(sizeof("struct PartitionFlat") ) + data = os.urandom(sizeof("struct PartitionFlat")) flat = PartitionFlat(data) flat.unpack(data) nested = PartitionNested(data) @@ -217,6 +738,6 @@ def test_nested(self): self.assertEqual(flat.startLBA, nested.startLBA) self.assertEqual(flat.endLBA, nested.endLBA) + if __name__ == '__main__': main() - diff --git a/tests/test_union.py b/tests/test_union.py index c67d19c..b625d1b 100644 --- a/tests/test_union.py +++ b/tests/test_union.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -#***************************************************************************** +# ***************************************************************************** # # Copyright (c) 2013-2019 Andrea Bonomi # @@ -23,13 +23,39 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # -#***************************************************************************** +# ***************************************************************************** from unittest import TestCase, main import cstruct from cstruct import define, sizeof import struct + +class Position(cstruct.CStruct): + __byte_order__ = cstruct.LITTLE_ENDIAN + __def__ = """ + struct { + unsigned char head; + unsigned char sector; + unsigned char cyl; + } + """ + + +class Partition(cstruct.CStruct): + __byte_order__ = cstruct.LITTLE_ENDIAN + __def__ = """ + struct { + unsigned char status; /* 0x80 - active */ + struct Position start; + unsigned char partition_type; + struct Position end; + unsigned int start_sect; /* starting sector counting from 0 */ + unsigned int sectors; // nr of sectors in partition + } + """ + + class TestUnion(cstruct.CStruct): __byte_order__ = cstruct.LITTLE_ENDIAN __def__ = """ @@ -43,15 +69,23 @@ class TestUnion(cstruct.CStruct): } """ -define('INIT_THREAD_SIZE', 2048 * sizeof('long')) -# class TaskUnion(cstruct.CStruct): -# __def__ = """ -# union { -# struct task_struct task; -# unsigned long stack[INIT_TASK_SIZE/sizeof(long)]; -# } -# """ +class TestStruct(cstruct.CStruct): + __def__ = """ + struct test_union { + char magic[4]; + union { + struct { + uint32 a; + uint32 b; + } a; + struct { + char b[8]; + } b; + } c; + } + """ + class TestCaseUnion(TestCase): @@ -60,8 +94,10 @@ class TestCaseUnion(TestCase): # self.assertEqual(sizeof('struct TestUnion'), 64) # def test_union(self): - pass - #raise Exception() + s = TestStruct() + self.assertEqual(len(s), 12) + print(len(s)) + raise Exception() def test_union_unpack(self): union = TestUnion() @@ -81,6 +117,6 @@ def test_union_unpack(self): self.assertEqual(union.b, 1979) self.assertEqual(union.c, 1979) + if __name__ == '__main__': main() - From 6bc8a331c01006d640b4f9dc7f8a86136c233ce4 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Tue, 23 Aug 2022 09:05:58 +0000 Subject: [PATCH 05/95] pytest --- coverage.sh | 1 - pytest.ini | 2 + requirements-dev.txt | 1 + setup.py | 1 + test.sh | 1 - tests/test_alignment.py | 39 ++++---- tests/test_cstruct.py | 203 ++++++++++++++++++++------------------- tests/test_define.py | 42 ++++---- tests/test_memcstruct.py | 199 +++++++++++++++++++------------------- tests/test_union.py | 79 +++++++-------- 10 files changed, 283 insertions(+), 285 deletions(-) delete mode 100755 coverage.sh create mode 100644 pytest.ini delete mode 100755 test.sh diff --git a/coverage.sh b/coverage.sh deleted file mode 100755 index 29db92c..0000000 --- a/coverage.sh +++ /dev/null @@ -1 +0,0 @@ -python3 -m coverage run --source=cstruct --omit 'cstruct/examples/*.py' setup.py test && python3 -m coverage report -m diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..5ee6477 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +testpaths = tests diff --git a/requirements-dev.txt b/requirements-dev.txt index 6f1aad8..abd417e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,3 +5,4 @@ sphinx tox black twine<3.4 +pytest diff --git a/setup.py b/setup.py index 1792b0d..23b1348 100644 --- a/setup.py +++ b/setup.py @@ -24,6 +24,7 @@ def readme(): 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', ], keywords='struct', author='Andrea Bonomi', diff --git a/test.sh b/test.sh deleted file mode 100755 index 0e04138..0000000 --- a/test.sh +++ /dev/null @@ -1 +0,0 @@ -python3 setup.py test diff --git a/tests/test_alignment.py b/tests/test_alignment.py index 73bd1dc..c26f93d 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -25,7 +25,6 @@ # # ***************************************************************************** -from unittest import TestCase, main from cstruct import sizeof, typedef, define, CStruct, NATIVE_ORDER define("UT_NAMESIZE", 32) @@ -186,30 +185,32 @@ class Foo10(CStruct): """ -class TestAlignament(TestCase): - def test_utmp_sizeof(self): - self.assertEqual(sizeof("struct Utmp"), 384) +def test_utmp_sizeof(): + assert sizeof("struct Utmp") == 384 - # http://www.catb.org/esr/structure-packing/ - def test_foo1_sizeof(self): - self.assertEqual(sizeof("struct Foo1"), 24) +# http://www.catb.org/esr/structure-packing/ - def test_foo2_sizeof(self): - self.assertEqual(sizeof("struct Foo2"), 24) - def test_foo3_sizeof(self): - self.assertEqual(sizeof("struct Foo3"), 16) +def test_foo1_sizeof(): + assert sizeof("struct Foo1") == 24 - def test_foo4_sizeof(self): - self.assertEqual(sizeof("struct Foo4"), 4) - def test_foo5_sizeof(self): - self.assertEqual(sizeof("struct Foo5"), 24) +def test_foo2_sizeof(): + assert sizeof("struct Foo2") == 24 - def test_foo10_sizeof(self): - self.assertEqual(sizeof("struct Foo10"), 24) +def test_foo3_sizeof(): + assert sizeof("struct Foo3") == 16 -if __name__ == '__main__': - main() + +def test_foo4_sizeof(): + assert sizeof("struct Foo4") == 4 + + +def test_foo5_sizeof(): + assert sizeof("struct Foo5") == 24 + + +def test_foo10_sizeof(): + assert sizeof("struct Foo10") == 24 diff --git a/tests/test_cstruct.py b/tests/test_cstruct.py index b82835f..4e28795 100644 --- a/tests/test_cstruct.py +++ b/tests/test_cstruct.py @@ -25,7 +25,6 @@ # # ***************************************************************************** -from unittest import TestCase, main import cstruct from cstruct import sizeof, typedef import io @@ -645,102 +644,106 @@ class PartitionNested(cstruct.CStruct): """ -class TestCStruct(TestCase): - def test_len(self): - mbr = MBR() - self.assertEqual(len(mbr), 512) - self.assertEqual(mbr.size, 512) - - def test_sizeof(self): - self.assertEqual(sizeof("struct Partition"), sizeof("struct PartitionFlat")) - self.assertEqual(sizeof("struct Partition"), sizeof("struct PartitionNested")) - - def test_unpack(self): - mbr = MBR() - f = io.BytesIO(MBR_DATA) - mbr.unpack(f) - self.assertEqual(mbr.signature[0], 0x55) - self.assertEqual(mbr.signature[1], 0xAA) - self.assertEqual(mbr.partitions[0].start.head, 0) - self.assertEqual(mbr.partitions[0].end.head, 0xFE) - self.assertEqual(mbr.partitions[1].start_sect, 0x2117C7) - - def test_pack(self): - mbr = MBR(MBR_DATA) - d = mbr.pack() - self.assertEqual(MBR_DATA, d) - mbr.partitions[3].start.head = 123 - d1 = mbr.pack() - mbr1 = MBR(d1) - self.assertEqual(mbr1.partitions[3].start.head, 123) - - def test_init(self): - p = Position(head=254, sector=63, cyl=134) - mbr = MBR(MBR_DATA) - self.assertEqual(mbr.partitions[0].end.head, p.head) - self.assertEqual(mbr.partitions[0].end.sector, p.sector) - self.assertEqual(mbr.partitions[0].end.cyl, p.cyl) - - def test_none(self): - mbr = MBR() - self.assertEqual(mbr.partitions[0].end.sector, 0) - mbr.unpack(None) - self.assertEqual(mbr.partitions[0].end.head, 0) - - def test_clear(self): - mbr = MBR() - mbr.unpack(MBR_DATA) - self.assertEqual(mbr.partitions[0].end.head, 0xFE) - mbr.clear() - self.assertEqual(mbr.partitions[0].end.head, 0x00) - - def test_inline(self): - TestStruct = cstruct.parse( - 'struct TestStruct { unsigned char head; unsigned char sector; unsigned char cyl; }', - __byte_order__=cstruct.LITTLE_ENDIAN, - ) - s = TestStruct(head=254, sector=63, cyl=134) - p = Position(head=254, sector=63, cyl=134) - self.assertEqual(s.pack(), p.pack()) - - def test_dummy(self): - dummy = Dummy() - dummy.c = b'A' - dummy.vc = b'ABCDEFGHIJ' - dummy.i = 123456 - for i in range(0, 10): - dummy.vi[i] = i * 10 - dummy.f = 123.456 - for i in range(0, 10): - dummy.vf[i] = 10.0 / (i + 1) - dummy.vl = list(range(0, 10)) - data = dummy.pack() - dummy1 = Dummy(data) - for i in range(0, 10): - dummy1.vl[i] = dummy.vl[i] - self.assertEqual(dummy.pack(), dummy1.pack()) - dummy2 = Dummy(data) - dummy2.vf[2] = 79 - self.assertNotEqual(dummy.pack(), dummy2.pack()) - dummy3 = Dummy(data) - dummy3.vl = list(range(1, 11)) - self.assertNotEqual(dummy.pack(), dummy3.pack()) - - def test_nested(self): - data = os.urandom(sizeof("struct PartitionFlat")) - flat = PartitionFlat(data) - flat.unpack(data) - nested = PartitionNested(data) - nested.unpack(data) - self.assertEqual(flat.status, nested.status) - self.assertEqual(flat.startAddrHead, nested.start.addrHead) - self.assertEqual(flat.startAddrCylSec, nested.start.addrCylSec) - self.assertEqual(flat.partType, nested.partType) - self.assertEqual(flat.endAddrHead, nested.end.addrHead) - self.assertEqual(flat.endAddrCylSec, nested.end.addrCylSec) - self.assertEqual(flat.startLBA, nested.startLBA) - self.assertEqual(flat.endLBA, nested.endLBA) - - -if __name__ == '__main__': - main() +def test_len(): + mbr = MBR() + assert len(mbr) == 512 + assert mbr.size == 512 + + +def test_sizeof(): + assert sizeof("struct Partition") == sizeof("struct PartitionFlat") + assert sizeof("struct Partition") == sizeof("struct PartitionNested") + + +def test_unpack(): + mbr = MBR() + f = io.BytesIO(MBR_DATA) + mbr.unpack(f) + assert mbr.signature[0] == 0x55 + assert mbr.signature[1] == 0xAA + assert mbr.partitions[0].start.head == 0 + assert mbr.partitions[0].end.head == 0xFE + assert mbr.partitions[1].start_sect == 0x2117C7 + + +def test_pack(): + mbr = MBR(MBR_DATA) + d = mbr.pack() + assert MBR_DATA == d + mbr.partitions[3].start.head = 123 + d1 = mbr.pack() + mbr1 = MBR(d1) + assert mbr1.partitions[3].start.head == 123 + + +def test_init(): + p = Position(head=254, sector=63, cyl=134) + mbr = MBR(MBR_DATA) + assert mbr.partitions[0].end.head == p.head + assert mbr.partitions[0].end.sector == p.sector + assert mbr.partitions[0].end.cyl == p.cyl + + +def test_none(): + mbr = MBR() + assert mbr.partitions[0].end.sector == 0 + mbr.unpack(None) + assert mbr.partitions[0].end.head == 0 + + +def test_clear(): + mbr = MBR() + mbr.unpack(MBR_DATA) + assert mbr.partitions[0].end.head == 0xFE + mbr.clear() + assert mbr.partitions[0].end.head == 0x00 + + +def test_inline(): + StructT1 = cstruct.parse( + 'struct StructT1 { unsigned char head; unsigned char sector; unsigned char cyl; }', + __byte_order__=cstruct.LITTLE_ENDIAN, + ) + s = StructT1(head=254, sector=63, cyl=134) + p = Position(head=254, sector=63, cyl=134) + assert s.pack() == p.pack() + + +def test_dummy(): + dummy = Dummy() + dummy.c = b'A' + dummy.vc = b'ABCDEFGHIJ' + dummy.i = 123456 + for i in range(0, 10): + dummy.vi[i] = i * 10 + dummy.f = 123.456 + for i in range(0, 10): + dummy.vf[i] = 10.0 / (i + 1) + dummy.vl = list(range(0, 10)) + data = dummy.pack() + dummy1 = Dummy(data) + for i in range(0, 10): + dummy1.vl[i] = dummy.vl[i] + assert dummy.pack() == dummy1.pack() + dummy2 = Dummy(data) + dummy2.vf[2] = 79 + assert dummy.pack() != dummy2.pack() + dummy3 = Dummy(data) + dummy3.vl = list(range(1, 11)) + assert dummy.pack() != dummy3.pack() + + +def test_nested(): + data = os.urandom(sizeof("struct PartitionFlat")) + flat = PartitionFlat(data) + flat.unpack(data) + nested = PartitionNested(data) + nested.unpack(data) + assert flat.status == nested.status + assert flat.startAddrHead == nested.start.addrHead + assert flat.startAddrCylSec == nested.start.addrCylSec + assert flat.partType == nested.partType + assert flat.endAddrHead == nested.end.addrHead + assert flat.endAddrCylSec == nested.end.addrCylSec + assert flat.startLBA == nested.startLBA + assert flat.endLBA == nested.endLBA diff --git a/tests/test_define.py b/tests/test_define.py index 4fc0d59..a98ec42 100644 --- a/tests/test_define.py +++ b/tests/test_define.py @@ -25,7 +25,7 @@ # # ***************************************************************************** -from unittest import TestCase, main +import pytest import cstruct from cstruct import define, undef, sizeof, typedef @@ -41,27 +41,27 @@ class Position(cstruct.CStruct): """ -class TestCaseDefine(TestCase): - def test_sizeof(self): - self.assertEqual(sizeof('int'), 4) - define('INIT_THREAD_SIZE', 2048 * sizeof('long')) - self.assertEqual(cstruct.DEFINES['INIT_THREAD_SIZE'], 16384) - self.assertEqual(sizeof('struct Position'), 3) - self.assertEqual(sizeof('struct Position'), len(Position)) - self.assertEqual(sizeof(Position), 3) - self.assertRaises(KeyError, lambda: sizeof('bla')) - self.assertRaises(KeyError, lambda: sizeof('struct Bla')) +def test_sizeof(): + assert sizeof('int') == 4 + define('INIT_THREAD_SIZE', 2048 * sizeof('long')) + assert cstruct.DEFINES['INIT_THREAD_SIZE'] == 16384 + assert sizeof('struct Position') == 3 + assert sizeof('struct Position') == len(Position) + assert sizeof(Position) == 3 + with pytest.raises(KeyError): + sizeof('bla') + with pytest.raises(KeyError): + sizeof('struct Bla') - def test_define(self): - define('A', 10) - self.assertEqual(cstruct.DEFINES['A'], 10) - undef('A') - self.assertRaises(KeyError, lambda: cstruct.DEFINES['A']) - def test_typedef(self): - typedef('int', 'integer') - self.assertEqual(sizeof('integer'), 4) +def test_define(): + define('A', 10) + assert cstruct.DEFINES['A'] == 10 + undef('A') + with pytest.raises(KeyError): + cstruct.DEFINES['A'] -if __name__ == '__main__': - main() +def test_typedef(): + typedef('int', 'integer') + assert sizeof('integer') == 4 diff --git a/tests/test_memcstruct.py b/tests/test_memcstruct.py index 5706943..c444f0f 100644 --- a/tests/test_memcstruct.py +++ b/tests/test_memcstruct.py @@ -25,7 +25,6 @@ # # ***************************************************************************** -from unittest import TestCase, main import cstruct from cstruct import sizeof, typedef import os @@ -644,100 +643,104 @@ class PartitionNested(cstruct.MemCStruct): """ -class TestMemCStruct(TestCase): - def test_len(self): - mbr = MBR() - self.assertEqual(len(mbr), 512) - self.assertEqual(mbr.size, 512) - - def test_sizeof(self): - self.assertEqual(sizeof("struct Partition"), sizeof("struct PartitionFlat")) - self.assertEqual(sizeof("struct Partition"), sizeof("struct PartitionNested")) - - def test_unpack(self): - mbr = MBR() - mbr.unpack(MBR_DATA) - self.assertEqual(mbr.signature[0], 0x55) - self.assertEqual(mbr.signature[1], 0xAA) - self.assertEqual(mbr.partitions[0].start.head, 0) - self.assertEqual(mbr.partitions[0].end.head, 0xFE) - self.assertEqual(mbr.partitions[1].start_sect, 0x2117C7) - - def test_pack(self): - mbr = MBR(MBR_DATA) - d = mbr.pack() - self.assertEqual(MBR_DATA, d) - mbr.partitions[3].start.head = 123 - d1 = mbr.pack() - mbr1 = MBR(d1) - self.assertEqual(mbr1.partitions[3].start.head, 123) - - def test_init(self): - p = Position(head=254, sector=63, cyl=134) - mbr = MBR(MBR_DATA) - self.assertEqual(mbr.partitions[0].end.head, p.head) - self.assertEqual(mbr.partitions[0].end.sector, p.sector) - self.assertEqual(mbr.partitions[0].end.cyl, p.cyl) - - def test_none(self): - mbr = MBR() - self.assertEqual(mbr.partitions[0].end.sector, 0) - mbr.unpack(None) - self.assertEqual(mbr.partitions[0].end.head, 0) - - def test_clear(self): - mbr = MBR() - mbr.unpack(MBR_DATA) - self.assertEqual(mbr.partitions[0].end.head, 0xFE) - mbr.clear() - self.assertEqual(mbr.partitions[0].end.head, 0x00) - - def test_inline(self): - TestStruct = cstruct.MemCStruct.parse( - 'struct { unsigned char head; unsigned char sector; unsigned char cyl; }', __byte_order__=cstruct.LITTLE_ENDIAN - ) - s = TestStruct(head=254, sector=63, cyl=134) - p = Position(head=254, sector=63, cyl=134) - self.assertEqual(s.pack(), p.pack()) - - def test_dummy(self): - dummy = Dummy() - dummy.c = b'A' - dummy.vc = b'ABCDEFGHIJ' - dummy.i = 123456 - for i in range(0, 10): - dummy.vi[i] = i * 10 - dummy.f = 123.456 - for i in range(0, 10): - dummy.vf[i] = 10.0 / (i + 1) - dummy.vl = list(range(0, 10)) - data = dummy.pack() - dummy1 = Dummy(data) - for i in range(0, 10): - dummy1.vl[i] = dummy.vl[i] - self.assertEqual(dummy.pack(), dummy1.pack()) - dummy2 = Dummy(data) - dummy2.vf[2] = 79 - self.assertNotEqual(dummy.pack(), dummy2.pack()) - dummy3 = Dummy(data) - dummy3.vl = list(range(1, 11)) - self.assertNotEqual(dummy.pack(), dummy3.pack()) - - def test_nested(self): - data = os.urandom(sizeof("struct PartitionFlat")) - flat = PartitionFlat(data) - flat.unpack(data) - nested = PartitionNested(data) - nested.unpack(data) - self.assertEqual(flat.status, nested.status) - self.assertEqual(flat.startAddrHead, nested.start.addrHead) - self.assertEqual(flat.startAddrCylSec, nested.start.addrCylSec) - self.assertEqual(flat.partType, nested.partType) - self.assertEqual(flat.endAddrHead, nested.end.addrHead) - self.assertEqual(flat.endAddrCylSec, nested.end.addrCylSec) - self.assertEqual(flat.startLBA, nested.startLBA) - self.assertEqual(flat.endLBA, nested.endLBA) - - -if __name__ == '__main__': - main() +def test_len(): + mbr = MBR() + assert len(mbr) == 512 + assert mbr.size == 512 + + +def test_sizeof(): + assert sizeof("struct Partition") == sizeof("struct PartitionFlat") + assert sizeof("struct Partition") == sizeof("struct PartitionNested") + + +def test_unpack(): + mbr = MBR() + mbr.unpack(MBR_DATA) + assert mbr.signature[0] == 0x55 + assert mbr.signature[1] == 0xAA + assert mbr.partitions[0].start.head == 0 + assert mbr.partitions[0].end.head == 0xFE + assert mbr.partitions[1].start_sect == 0x2117C7 + + +def test_pack(): + mbr = MBR(MBR_DATA) + d = mbr.pack() + assert MBR_DATA == d + mbr.partitions[3].start.head = 123 + d1 = mbr.pack() + mbr1 = MBR(d1) + assert mbr1.partitions[3].start.head == 123 + + +def test_init(): + p = Position(head=254, sector=63, cyl=134) + mbr = MBR(MBR_DATA) + assert mbr.partitions[0].end.head == p.head + assert mbr.partitions[0].end.sector == p.sector + assert mbr.partitions[0].end.cyl == p.cyl + + +def test_none(): + mbr = MBR() + assert mbr.partitions[0].end.sector == 0 + mbr.unpack(None) + assert mbr.partitions[0].end.head == 0 + + +def test_clear(): + mbr = MBR() + mbr.unpack(MBR_DATA) + assert mbr.partitions[0].end.head == 0xFE + mbr.clear() + assert mbr.partitions[0].end.head == 0x00 + + +def test_inline(): + TestStruct = cstruct.MemCStruct.parse( + 'struct { unsigned char head; unsigned char sector; unsigned char cyl; }', __byte_order__=cstruct.LITTLE_ENDIAN + ) + s = TestStruct(head=254, sector=63, cyl=134) + p = Position(head=254, sector=63, cyl=134) + assert s.pack() == p.pack() + + +def test_dummy(): + dummy = Dummy() + dummy.c = b'A' + dummy.vc = b'ABCDEFGHIJ' + dummy.i = 123456 + for i in range(0, 10): + dummy.vi[i] = i * 10 + dummy.f = 123.456 + for i in range(0, 10): + dummy.vf[i] = 10.0 / (i + 1) + dummy.vl = list(range(0, 10)) + data = dummy.pack() + dummy1 = Dummy(data) + for i in range(0, 10): + dummy1.vl[i] = dummy.vl[i] + assert dummy.pack() == dummy1.pack() + dummy2 = Dummy(data) + dummy2.vf[2] = 79 + assert dummy.pack() != dummy2.pack() + dummy3 = Dummy(data) + dummy3.vl = list(range(1, 11)) + assert dummy.pack() != dummy3.pack() + + +def test_nested(): + data = os.urandom(sizeof("struct PartitionFlat")) + flat = PartitionFlat(data) + flat.unpack(data) + nested = PartitionNested(data) + nested.unpack(data) + assert flat.status == nested.status + assert flat.startAddrHead == nested.start.addrHead + assert flat.startAddrCylSec == nested.start.addrCylSec + assert flat.partType == nested.partType + assert flat.endAddrHead == nested.end.addrHead + assert flat.endAddrCylSec == nested.end.addrCylSec + assert flat.startLBA == nested.startLBA + assert flat.endLBA == nested.endLBA diff --git a/tests/test_union.py b/tests/test_union.py index b625d1b..07287b7 100644 --- a/tests/test_union.py +++ b/tests/test_union.py @@ -25,9 +25,8 @@ # # ***************************************************************************** -from unittest import TestCase, main import cstruct -from cstruct import define, sizeof +from cstruct import sizeof import struct @@ -56,7 +55,7 @@ class Partition(cstruct.CStruct): """ -class TestUnion(cstruct.CStruct): +class UnionT1(cstruct.CStruct): __byte_order__ = cstruct.LITTLE_ENDIAN __def__ = """ union { @@ -70,53 +69,43 @@ class TestUnion(cstruct.CStruct): """ -class TestStruct(cstruct.CStruct): +class StructT1(cstruct.CStruct): __def__ = """ struct test_union { - char magic[4]; - union { - struct { - uint32 a; - uint32 b; - } a; - struct { - char b[8]; - } b; - } c; + char magic[4]; + union { + struct { + uint32 a; + uint32 b; + } a; + struct { + char b[8]; + } b; + } c; } """ -class TestCaseUnion(TestCase): - - # def test_sizeof(self): - # self.assertEqual(cstruct.DEFINES['INIT_THREAD_SIZE'], 16384) - # self.assertEqual(sizeof('struct TestUnion'), 64) - # - def test_union(self): - s = TestStruct() - self.assertEqual(len(s), 12) - print(len(s)) - raise Exception() - - def test_union_unpack(self): - union = TestUnion() - union.unpack(None) - self.assertEqual(union.a, 0) - self.assertEqual(union.a1, 0) - self.assertEqual(union.b, 0) - self.assertEqual(union.c, 0) - union.unpack(struct.pack('b', 10) + cstruct.CHAR_ZERO * union.size) - self.assertEqual(union.a, 10) - self.assertEqual(union.a1, 10) - self.assertEqual(union.b, 10) - self.assertEqual(union.c, 10) - union.unpack(struct.pack('h', 1979) + cstruct.CHAR_ZERO * union.size) - self.assertEqual(union.a, 187) - self.assertEqual(union.a1, 187) - self.assertEqual(union.b, 1979) - self.assertEqual(union.c, 1979) +def test_sizeof(): + assert sizeof('struct UnionT1') == 64 + s = UnionT1() + assert len(s) == 64 -if __name__ == '__main__': - main() +def test_union_unpack(): + union = UnionT1() + union.unpack(None) + assert union.a == 0 + assert union.a1 == 0 + assert union.b == 0 + assert union.c == 0 + union.unpack(struct.pack('b', 10) + cstruct.CHAR_ZERO * union.size) + assert union.a == 10 + assert union.a1 == 10 + assert union.b == 10 + assert union.c == 10 + union.unpack(struct.pack('h', 1979) + cstruct.CHAR_ZERO * union.size) + assert union.a == 187 + assert union.a1 == 187 + assert union.b == 1979 + assert union.c == 1979 From c0158ed4088179d516a27bdc00f26c77c454c11c Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Tue, 23 Aug 2022 09:31:15 +0000 Subject: [PATCH 06/95] Fix empty MemCStruct size --- cstruct/mem_cstruct.py | 3 ++- tests/test_cstruct.py | 14 ++++++++++++++ tests/test_memcstruct.py | 14 ++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/cstruct/mem_cstruct.py b/cstruct/mem_cstruct.py index f1f1da0..bcd0b4e 100644 --- a/cstruct/mem_cstruct.py +++ b/cstruct/mem_cstruct.py @@ -72,7 +72,8 @@ def unpack_from(self, buffer: Optional[bytes], offset: int = 0) -> bool: """ self.__base__ = offset # Base offset if buffer is None: - self.__mem__ = ctypes.create_string_buffer(self.__size__) + # the buffer is one item larger than its size and the last element is NUL + self.__mem__ = ctypes.create_string_buffer(self.__size__ + 1) elif isinstance(buffer, ctypes.Array): self.__mem__ = buffer else: diff --git a/tests/test_cstruct.py b/tests/test_cstruct.py index 4e28795..94c181d 100644 --- a/tests/test_cstruct.py +++ b/tests/test_cstruct.py @@ -650,6 +650,20 @@ def test_len(): assert mbr.size == 512 +def test_pack_len(): + buffer = b'\x00' * 512 + mbr = MBR(buffer) + d = mbr.pack() + assert len(d) == 512 + mbr = MBR() + mbr.unpack(MBR_DATA) + d = mbr.pack() + assert len(d) == 512 + mbr = MBR() + d = mbr.pack() + assert len(d) == 512 + + def test_sizeof(): assert sizeof("struct Partition") == sizeof("struct PartitionFlat") assert sizeof("struct Partition") == sizeof("struct PartitionNested") diff --git a/tests/test_memcstruct.py b/tests/test_memcstruct.py index c444f0f..833300a 100644 --- a/tests/test_memcstruct.py +++ b/tests/test_memcstruct.py @@ -649,6 +649,20 @@ def test_len(): assert mbr.size == 512 +def test_pack_len(): + buffer = b'\x00' * 512 + mbr = MBR(buffer) + d = mbr.pack() + assert len(d) == 512 + mbr = MBR() + mbr.unpack(MBR_DATA) + d = mbr.pack() + assert len(d) == 512 + mbr = MBR() + d = mbr.pack() + assert len(d) == 512 + + def test_sizeof(): assert sizeof("struct Partition") == sizeof("struct PartitionFlat") assert sizeof("struct Partition") == sizeof("struct PartitionNested") From 357c3f7d97178e2e94d9aa957546d46fd9871d6d Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Tue, 23 Aug 2022 09:42:16 +0000 Subject: [PATCH 07/95] Makefile updated --- .gitignore | 1 + Makefile | 44 +++++++++++++++++++++++++++----------------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index e035e66..fc1cb1f 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,4 @@ nosetests.xml /share /include .mypy_cache +pyvenv.cfg diff --git a/Makefile b/Makefile index 68a55d2..6a9f9e9 100644 --- a/Makefile +++ b/Makefile @@ -1,30 +1,40 @@ SHELL=/bin/bash -e help: - @echo - make coverage - @echo - make test - @echo - make typecheck - @echo - make lint - @echo - make release - @echo - make clean + @echo - make black ------ Format code + @echo - make clean ------ Clean virtual environment + @echo - make coverage --- Run tests coverage + @echo - make docs ------- Make docs + @echo - make lint ------- Run lint + @echo - make test ------- Run test + @echo - make typecheck -- Typecheck + @echo - make venv ------- Create virtual environment + +black: + black -S cstruct tests setup.py + +clean: + -rm -rf build dist + -rm -rf *.egg-info + -rm -rf bin lib share pyvenv.cfg coverage: python3 -m coverage run --source=cstruct setup.py test && python3 -m coverage report -m +.PHONY: docs +docs: + cd docs; $(MAKE) html + +lint: + flake8 cstruct tests + test: python3 setup.py test typecheck: mypy --strict --no-warn-unused-ignores cstruct -lint: - python setup.py flake8 - -release: - python ./setup.py bdist_wheel - cd docs; $(MAKE) html - -clean: - -rm -rf build dist - -rm -rf *.egg-info - +venv: + python3 -m virtualenv . + . bin/activate; pip install -Ur requirements.txt + . bin/activate; pip install -Ur requirements-dev.txt From 805fac4af151155ddba3d5c213e75e101f567606 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Tue, 23 Aug 2022 10:12:02 +0000 Subject: [PATCH 08/95] README.md update --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index c62e093..cdc2bb9 100644 --- a/README.md +++ b/README.md @@ -3,14 +3,14 @@ Python-CStruct C-style structs for Python -Convert C struct definitions into Python classes with methods for +Convert C struct/union definitions into Python classes with methods for serializing/deserializing. -The usage is very simple: create a class subclassing cstruct.CStruct -and add a C struct definition as a string in the __struct__ field. -The C struct definition is parsed at runtime and the struct format string +The usage is very simple: create a class subclassing cstruct.MemCStruct +and add a C struct/union definition as a string in the __struct__ field. +The C struct/union definition is parsed at runtime and the struct format string is generated. The class offers the method "unpack" for deserializing -a string of bytes into a Python object and the method "pack" for -serializing the values into a string. +an array of bytes into a Python object and the method "pack" for +serializing the values into an array of bytes. Example ------- @@ -21,7 +21,7 @@ The following program reads the DOS partition information from a disk. #!/usr/bin/env python import cstruct -class Position(cstruct.CStruct): +class Position(cstruct.MemCStruct): __byte_order__ = cstruct.LITTLE_ENDIAN __def__ = """ struct { @@ -31,7 +31,7 @@ class Position(cstruct.CStruct): } """ -class Partition(cstruct.CStruct): +class Partition(cstruct.MemCStruct): __byte_order__ = cstruct.LITTLE_ENDIAN __def__ = """ struct { @@ -52,7 +52,7 @@ class Partition(cstruct.CStruct): print("starting sector: %08X" % self.start_sect) print("size MB: %s" % (self.sectors / 2 / 1024)) -class MBR(cstruct.CStruct): +class MBR(cstruct.MemCStruct): __byte_order__ = cstruct.LITTLE_ENDIAN __def__ = """ struct { From 31b0b500becb929516aeeac66be5842b131e2443 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Tue, 23 Aug 2022 10:12:27 +0000 Subject: [PATCH 09/95] tests added --- tests/test_cstruct.py | 519 +-------------------------------------- tests/test_memcstruct.py | 519 +-------------------------------------- tests/test_union.py | 31 +-- 3 files changed, 15 insertions(+), 1054 deletions(-) diff --git a/tests/test_cstruct.py b/tests/test_cstruct.py index 94c181d..785dea9 100644 --- a/tests/test_cstruct.py +++ b/tests/test_cstruct.py @@ -29,524 +29,9 @@ from cstruct import sizeof, typedef import io import os +from pathlib import Path - -MBR_DATA = bytes( - [ - 0xEB, - 0x48, - 0x90, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x3, - 0x2, - 0xFF, - 0x0, - 0x0, - 0x80, - 0x61, - 0xCB, - 0x4, - 0x0, - 0x0, - 0x8, - 0xFA, - 0x80, - 0xCA, - 0x80, - 0xEA, - 0x53, - 0x7C, - 0x0, - 0x0, - 0x31, - 0xC0, - 0x8E, - 0xD8, - 0x8E, - 0xD0, - 0xBC, - 0x0, - 0x20, - 0xFB, - 0xA0, - 0x40, - 0x7C, - 0x3C, - 0xFF, - 0x74, - 0x2, - 0x88, - 0xC2, - 0x52, - 0xBE, - 0x79, - 0x7D, - 0xE8, - 0x34, - 0x1, - 0xF6, - 0xC2, - 0x80, - 0x74, - 0x54, - 0xB4, - 0x41, - 0xBB, - 0xAA, - 0x55, - 0xCD, - 0x13, - 0x5A, - 0x52, - 0x72, - 0x49, - 0x81, - 0xFB, - 0x55, - 0xAA, - 0x75, - 0x43, - 0xA0, - 0x41, - 0x7C, - 0x84, - 0xC0, - 0x75, - 0x5, - 0x83, - 0xE1, - 0x1, - 0x74, - 0x37, - 0x66, - 0x8B, - 0x4C, - 0x10, - 0xBE, - 0x5, - 0x7C, - 0xC6, - 0x44, - 0xFF, - 0x1, - 0x66, - 0x8B, - 0x1E, - 0x44, - 0x7C, - 0xC7, - 0x4, - 0x10, - 0x0, - 0xC7, - 0x44, - 0x2, - 0x1, - 0x0, - 0x66, - 0x89, - 0x5C, - 0x8, - 0xC7, - 0x44, - 0x6, - 0x0, - 0x70, - 0x66, - 0x31, - 0xC0, - 0x89, - 0x44, - 0x4, - 0x66, - 0x89, - 0x44, - 0xC, - 0xB4, - 0x42, - 0xCD, - 0x13, - 0x72, - 0x5, - 0xBB, - 0x0, - 0x70, - 0xEB, - 0x7D, - 0xB4, - 0x8, - 0xCD, - 0x13, - 0x73, - 0xA, - 0xF6, - 0xC2, - 0x80, - 0xF, - 0x84, - 0xF0, - 0x0, - 0xE9, - 0x8D, - 0x0, - 0xBE, - 0x5, - 0x7C, - 0xC6, - 0x44, - 0xFF, - 0x0, - 0x66, - 0x31, - 0xC0, - 0x88, - 0xF0, - 0x40, - 0x66, - 0x89, - 0x44, - 0x4, - 0x31, - 0xD2, - 0x88, - 0xCA, - 0xC1, - 0xE2, - 0x2, - 0x88, - 0xE8, - 0x88, - 0xF4, - 0x40, - 0x89, - 0x44, - 0x8, - 0x31, - 0xC0, - 0x88, - 0xD0, - 0xC0, - 0xE8, - 0x2, - 0x66, - 0x89, - 0x4, - 0x66, - 0xA1, - 0x44, - 0x7C, - 0x66, - 0x31, - 0xD2, - 0x66, - 0xF7, - 0x34, - 0x88, - 0x54, - 0xA, - 0x66, - 0x31, - 0xD2, - 0x66, - 0xF7, - 0x74, - 0x4, - 0x88, - 0x54, - 0xB, - 0x89, - 0x44, - 0xC, - 0x3B, - 0x44, - 0x8, - 0x7D, - 0x3C, - 0x8A, - 0x54, - 0xD, - 0xC0, - 0xE2, - 0x6, - 0x8A, - 0x4C, - 0xA, - 0xFE, - 0xC1, - 0x8, - 0xD1, - 0x8A, - 0x6C, - 0xC, - 0x5A, - 0x8A, - 0x74, - 0xB, - 0xBB, - 0x0, - 0x70, - 0x8E, - 0xC3, - 0x31, - 0xDB, - 0xB8, - 0x1, - 0x2, - 0xCD, - 0x13, - 0x72, - 0x2A, - 0x8C, - 0xC3, - 0x8E, - 0x6, - 0x48, - 0x7C, - 0x60, - 0x1E, - 0xB9, - 0x0, - 0x1, - 0x8E, - 0xDB, - 0x31, - 0xF6, - 0x31, - 0xFF, - 0xFC, - 0xF3, - 0xA5, - 0x1F, - 0x61, - 0xFF, - 0x26, - 0x42, - 0x7C, - 0xBE, - 0x7F, - 0x7D, - 0xE8, - 0x40, - 0x0, - 0xEB, - 0xE, - 0xBE, - 0x84, - 0x7D, - 0xE8, - 0x38, - 0x0, - 0xEB, - 0x6, - 0xBE, - 0x8E, - 0x7D, - 0xE8, - 0x30, - 0x0, - 0xBE, - 0x93, - 0x7D, - 0xE8, - 0x2A, - 0x0, - 0xEB, - 0xFE, - 0x47, - 0x52, - 0x55, - 0x42, - 0x20, - 0x0, - 0x47, - 0x65, - 0x6F, - 0x6D, - 0x0, - 0x48, - 0x61, - 0x72, - 0x64, - 0x20, - 0x44, - 0x69, - 0x73, - 0x6B, - 0x0, - 0x52, - 0x65, - 0x61, - 0x64, - 0x0, - 0x20, - 0x45, - 0x72, - 0x72, - 0x6F, - 0x72, - 0x0, - 0xBB, - 0x1, - 0x0, - 0xB4, - 0xE, - 0xCD, - 0x10, - 0xAC, - 0x3C, - 0x0, - 0x75, - 0xF4, - 0xC3, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x80, - 0x0, - 0x2, - 0x0, - 0x83, - 0xFE, - 0x3F, - 0x86, - 0x1, - 0x0, - 0x0, - 0x0, - 0xC6, - 0x17, - 0x21, - 0x0, - 0x0, - 0x0, - 0x1, - 0x87, - 0x8E, - 0xFE, - 0xFF, - 0xFF, - 0xC7, - 0x17, - 0x21, - 0x0, - 0x4D, - 0xD3, - 0xDE, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x55, - 0xAA, - ] -) +MBR_DATA = (Path(__file__).parent.parent / 'mbr').read_bytes() class Position(cstruct.CStruct): diff --git a/tests/test_memcstruct.py b/tests/test_memcstruct.py index 833300a..f61da32 100644 --- a/tests/test_memcstruct.py +++ b/tests/test_memcstruct.py @@ -28,524 +28,9 @@ import cstruct from cstruct import sizeof, typedef import os +from pathlib import Path - -MBR_DATA = bytes( - [ - 0xEB, - 0x48, - 0x90, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x3, - 0x2, - 0xFF, - 0x0, - 0x0, - 0x80, - 0x61, - 0xCB, - 0x4, - 0x0, - 0x0, - 0x8, - 0xFA, - 0x80, - 0xCA, - 0x80, - 0xEA, - 0x53, - 0x7C, - 0x0, - 0x0, - 0x31, - 0xC0, - 0x8E, - 0xD8, - 0x8E, - 0xD0, - 0xBC, - 0x0, - 0x20, - 0xFB, - 0xA0, - 0x40, - 0x7C, - 0x3C, - 0xFF, - 0x74, - 0x2, - 0x88, - 0xC2, - 0x52, - 0xBE, - 0x79, - 0x7D, - 0xE8, - 0x34, - 0x1, - 0xF6, - 0xC2, - 0x80, - 0x74, - 0x54, - 0xB4, - 0x41, - 0xBB, - 0xAA, - 0x55, - 0xCD, - 0x13, - 0x5A, - 0x52, - 0x72, - 0x49, - 0x81, - 0xFB, - 0x55, - 0xAA, - 0x75, - 0x43, - 0xA0, - 0x41, - 0x7C, - 0x84, - 0xC0, - 0x75, - 0x5, - 0x83, - 0xE1, - 0x1, - 0x74, - 0x37, - 0x66, - 0x8B, - 0x4C, - 0x10, - 0xBE, - 0x5, - 0x7C, - 0xC6, - 0x44, - 0xFF, - 0x1, - 0x66, - 0x8B, - 0x1E, - 0x44, - 0x7C, - 0xC7, - 0x4, - 0x10, - 0x0, - 0xC7, - 0x44, - 0x2, - 0x1, - 0x0, - 0x66, - 0x89, - 0x5C, - 0x8, - 0xC7, - 0x44, - 0x6, - 0x0, - 0x70, - 0x66, - 0x31, - 0xC0, - 0x89, - 0x44, - 0x4, - 0x66, - 0x89, - 0x44, - 0xC, - 0xB4, - 0x42, - 0xCD, - 0x13, - 0x72, - 0x5, - 0xBB, - 0x0, - 0x70, - 0xEB, - 0x7D, - 0xB4, - 0x8, - 0xCD, - 0x13, - 0x73, - 0xA, - 0xF6, - 0xC2, - 0x80, - 0xF, - 0x84, - 0xF0, - 0x0, - 0xE9, - 0x8D, - 0x0, - 0xBE, - 0x5, - 0x7C, - 0xC6, - 0x44, - 0xFF, - 0x0, - 0x66, - 0x31, - 0xC0, - 0x88, - 0xF0, - 0x40, - 0x66, - 0x89, - 0x44, - 0x4, - 0x31, - 0xD2, - 0x88, - 0xCA, - 0xC1, - 0xE2, - 0x2, - 0x88, - 0xE8, - 0x88, - 0xF4, - 0x40, - 0x89, - 0x44, - 0x8, - 0x31, - 0xC0, - 0x88, - 0xD0, - 0xC0, - 0xE8, - 0x2, - 0x66, - 0x89, - 0x4, - 0x66, - 0xA1, - 0x44, - 0x7C, - 0x66, - 0x31, - 0xD2, - 0x66, - 0xF7, - 0x34, - 0x88, - 0x54, - 0xA, - 0x66, - 0x31, - 0xD2, - 0x66, - 0xF7, - 0x74, - 0x4, - 0x88, - 0x54, - 0xB, - 0x89, - 0x44, - 0xC, - 0x3B, - 0x44, - 0x8, - 0x7D, - 0x3C, - 0x8A, - 0x54, - 0xD, - 0xC0, - 0xE2, - 0x6, - 0x8A, - 0x4C, - 0xA, - 0xFE, - 0xC1, - 0x8, - 0xD1, - 0x8A, - 0x6C, - 0xC, - 0x5A, - 0x8A, - 0x74, - 0xB, - 0xBB, - 0x0, - 0x70, - 0x8E, - 0xC3, - 0x31, - 0xDB, - 0xB8, - 0x1, - 0x2, - 0xCD, - 0x13, - 0x72, - 0x2A, - 0x8C, - 0xC3, - 0x8E, - 0x6, - 0x48, - 0x7C, - 0x60, - 0x1E, - 0xB9, - 0x0, - 0x1, - 0x8E, - 0xDB, - 0x31, - 0xF6, - 0x31, - 0xFF, - 0xFC, - 0xF3, - 0xA5, - 0x1F, - 0x61, - 0xFF, - 0x26, - 0x42, - 0x7C, - 0xBE, - 0x7F, - 0x7D, - 0xE8, - 0x40, - 0x0, - 0xEB, - 0xE, - 0xBE, - 0x84, - 0x7D, - 0xE8, - 0x38, - 0x0, - 0xEB, - 0x6, - 0xBE, - 0x8E, - 0x7D, - 0xE8, - 0x30, - 0x0, - 0xBE, - 0x93, - 0x7D, - 0xE8, - 0x2A, - 0x0, - 0xEB, - 0xFE, - 0x47, - 0x52, - 0x55, - 0x42, - 0x20, - 0x0, - 0x47, - 0x65, - 0x6F, - 0x6D, - 0x0, - 0x48, - 0x61, - 0x72, - 0x64, - 0x20, - 0x44, - 0x69, - 0x73, - 0x6B, - 0x0, - 0x52, - 0x65, - 0x61, - 0x64, - 0x0, - 0x20, - 0x45, - 0x72, - 0x72, - 0x6F, - 0x72, - 0x0, - 0xBB, - 0x1, - 0x0, - 0xB4, - 0xE, - 0xCD, - 0x10, - 0xAC, - 0x3C, - 0x0, - 0x75, - 0xF4, - 0xC3, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x80, - 0x0, - 0x2, - 0x0, - 0x83, - 0xFE, - 0x3F, - 0x86, - 0x1, - 0x0, - 0x0, - 0x0, - 0xC6, - 0x17, - 0x21, - 0x0, - 0x0, - 0x0, - 0x1, - 0x87, - 0x8E, - 0xFE, - 0xFF, - 0xFF, - 0xC7, - 0x17, - 0x21, - 0x0, - 0x4D, - 0xD3, - 0xDE, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x0, - 0x55, - 0xAA, - ] -) +MBR_DATA = (Path(__file__).parent.parent / 'mbr').read_bytes() class Position(cstruct.MemCStruct): diff --git a/tests/test_union.py b/tests/test_union.py index 07287b7..ad90713 100644 --- a/tests/test_union.py +++ b/tests/test_union.py @@ -30,7 +30,7 @@ import struct -class Position(cstruct.CStruct): +class Position(cstruct.MemCStruct): __byte_order__ = cstruct.LITTLE_ENDIAN __def__ = """ struct { @@ -41,7 +41,7 @@ class Position(cstruct.CStruct): """ -class Partition(cstruct.CStruct): +class Partition(cstruct.MemCStruct): __byte_order__ = cstruct.LITTLE_ENDIAN __def__ = """ struct { @@ -55,7 +55,7 @@ class Partition(cstruct.CStruct): """ -class UnionT1(cstruct.CStruct): +class UnionT1(cstruct.MemCStruct): __byte_order__ = cstruct.LITTLE_ENDIAN __def__ = """ union { @@ -69,23 +69,6 @@ class UnionT1(cstruct.CStruct): """ -class StructT1(cstruct.CStruct): - __def__ = """ - struct test_union { - char magic[4]; - union { - struct { - uint32 a; - uint32 b; - } a; - struct { - char b[8]; - } b; - } c; - } - """ - - def test_sizeof(): assert sizeof('struct UnionT1') == 64 s = UnionT1() @@ -109,3 +92,11 @@ def test_union_unpack(): assert union.a1 == 187 assert union.b == 1979 assert union.c == 1979 + print(union) + union2 = UnionT1(union.pack()) + print(union2) + assert len(union) == len(union2) + assert union2.a == 187 + assert union2.a1 == 187 + assert union2.b == 1979 + assert union2.c == 1979 From e0932538bc2075c91fb7c462faf4cfb1282f4a09 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Tue, 23 Aug 2022 10:13:06 +0000 Subject: [PATCH 10/95] black code style --- Makefile | 2 +- examples/fdisk.py | 18 +++++++++++------- examples/who.py | 30 ++++++++++++++++++++++-------- 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/Makefile b/Makefile index 6a9f9e9..d5c4e2c 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ help: @echo - make venv ------- Create virtual environment black: - black -S cstruct tests setup.py + black -S cstruct tests examples setup.py clean: -rm -rf build dist diff --git a/examples/fdisk.py b/examples/fdisk.py index 724c61f..b77ae21 100644 --- a/examples/fdisk.py +++ b/examples/fdisk.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -#***************************************************************************** +# ***************************************************************************** # # Copyright (c) 2013 Andrea Bonomi # @@ -24,12 +24,13 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # -#***************************************************************************** +# ***************************************************************************** import cstruct import sys -class Position(cstruct.CStruct): + +class Position(cstruct.MemCStruct): __byte_order__ = cstruct.LITTLE_ENDIAN __struct__ = """ unsigned char head; @@ -37,7 +38,8 @@ class Position(cstruct.CStruct): unsigned char cyl; """ -class Partition(cstruct.CStruct): + +class Partition(cstruct.MemCStruct): __byte_order__ = cstruct.LITTLE_ENDIAN __struct__ = """ unsigned char status; /* 0x80 - active */ @@ -56,7 +58,8 @@ def print_info(self): print("starting sector: %08X" % self.start_sect) print("size MB: %s" % (self.sectors / 2 / 1024)) -class MBR(cstruct.CStruct): + +class MBR(cstruct.MemCStruct): __byte_order__ = cstruct.LITTLE_ENDIAN __struct__ = """ char unused[440]; @@ -74,6 +77,7 @@ def print_info(self): print("partition: %s" % i) partition.print_info() + def main(): if len(sys.argv) != 2: print("usage: %s disk" % sys.argv[0]) @@ -84,6 +88,6 @@ def main(): mbr.unpack(data) mbr.print_info() -if __name__ == "__main__": - main() +if __name__ == "__main__": + main() diff --git a/examples/who.py b/examples/who.py index fbc755b..882fdf4 100644 --- a/examples/who.py +++ b/examples/who.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -#***************************************************************************** +# ***************************************************************************** # # Copyright (c) 2013 Andrea Bonomi # @@ -24,7 +24,7 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # -#***************************************************************************** +# ***************************************************************************** # pts/1 2013-06-06 18:09 23120 id=ts/1 term=0 exit=0 # system boot 2013-05-20 21:27 @@ -45,7 +45,7 @@ # pts/30 2013-07-24 14:40 19054 id=s/30 term=0 exit=0 # pts/28 2013-07-30 20:49 24942 id=s/28 term=0 exit=0 # pts/27 2013-08-02 17:59 31326 id=s/27 term=0 exit=0 -#012345678901234567890123456789012345678901234567890123456789012345678901234567890 +# 012345678901234567890123456789012345678901234567890123456789012345678901234567890 from cstruct import define, typedef, MemCStruct, NATIVE_ORDER import sys import time @@ -57,21 +57,26 @@ typedef("int", "pid_t") typedef("long", "time_t") + class ExitStatus(MemCStruct): __struct__ = """ short e_termination; /* Process termination status. */ short e_exit; /* Process exit status. */ """ + + class Timeval(MemCStruct): __struct__ = """ int32_t tv_sec; /* Seconds. */ int32_t tv_usec; /* Microseconds. */ """ + def str_from_c(string): - #return str(string.split("\0")[0]) + # return str(string.split("\0")[0]) return string.decode().split("\0")[0] + class Utmp(MemCStruct): __byte_order__ = NATIVE_ORDER __struct__ = """ @@ -94,13 +99,22 @@ class Utmp(MemCStruct): def print_info(self, all_): "andreax + pts/0 2013-08-21 08:58 . 32341 (l26.box)" " pts/34 2013-06-12 15:04 26396 id=s/34 term=0 exit=0" - if all_ or self.ut_type in [6,7]: - print("%-10s %-12s %15s %15s %-8s" % ( + if all_ or self.ut_type in [6, 7]: + print( + "%-10s %-12s %15s %15s %-8s" + % ( str_from_c(self.ut_user), str_from_c(self.ut_line), time.strftime("%Y-%m-%d %H:%M", time.gmtime(self.ut_tv.tv_sec)), self.ut_pid, - str_from_c(self.ut_host) and "(%s)" % str_from_c(self.ut_host) or str_from_c(self.ut_id) and "id=%s" % str_from_c(self.ut_id) or "")) + str_from_c(self.ut_host) + and "(%s)" % str_from_c(self.ut_host) + or str_from_c(self.ut_id) + and "id=%s" % str_from_c(self.ut_id) + or "", + ) + ) + def main(): utmp = len(sys.argv) > 1 and sys.argv[1] or "/var/run/utmp" @@ -110,6 +124,6 @@ def main(): while utmp.unpack(f): utmp.print_info(all_) + if __name__ == "__main__": main() - From 9cf08de61e0c1c41bfcf4d5fd421e11e72a83c01 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Tue, 23 Aug 2022 10:15:42 +0000 Subject: [PATCH 11/95] version 2.2 --- changelog.txt | 14 ++++++++++++++ cstruct/__init__.py | 4 ++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index 76ddc4d..5da72e5 100644 --- a/changelog.txt +++ b/changelog.txt @@ -105,3 +105,17 @@ - refactoring - Python 3.9 support - Github workfows + +### 2.2 + +2022-08-23 + +### Fix + +- Fix empty MemCStruct size + +### Improved + +- Python 3.10 support +- pytest +- black code style diff --git a/cstruct/__init__.py b/cstruct/__init__.py index 5005a97..db4d3b1 100644 --- a/cstruct/__init__.py +++ b/cstruct/__init__.py @@ -26,11 +26,11 @@ __author__ = 'Andrea Bonomi ' __license__ = 'MIT' -__version__ = '2.1' +__version__ = '2.2' __date__ = '15 August 2013' import struct -from typing import Any, Dict, Type +from typing import Any, Type from .base import ( LITTLE_ENDIAN, BIG_ENDIAN, From 64e1c661e713e28ac0e1b0e0b4ef71bf9029df15 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Tue, 23 Aug 2022 12:21:35 +0200 Subject: [PATCH 12/95] Update README.md --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index cdc2bb9..1edb855 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,13 @@ Python-CStruct C-style structs for Python +[![Build Status](https://github.com/andreax79/python-cstruct/workflows/Tests/badge.svg)](https://github.com/andreax79/python-cstruct/actions) +[![PyPI version](https://badge.fury.io/py/cstruct.svg)](https://badge.fury.io/py/cstruct) +[![PyPI](https://img.shields.io/pypi/pyversions/cstruct.svg)](https://pypi.org/project/cstruct) +[![Downloads](https://pepy.tech/badge/cstruct/month)](https://pepy.tech/project/cstruct) +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) + + Convert C struct/union definitions into Python classes with methods for serializing/deserializing. The usage is very simple: create a class subclassing cstruct.MemCStruct From cedb0c9b4ae769b65de7c6cf8405e98361cc7445 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Tue, 23 Aug 2022 19:00:13 +0000 Subject: [PATCH 13/95] pytest fix --- .github/workflows/tests.yml | 4 +++- Makefile | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 72c04d7..45e5185 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,6 +15,7 @@ jobs: - "3.7" - "3.8" - "3.9" + - "3.10" runs-on: ubuntu-latest @@ -44,9 +45,10 @@ jobs: python -m pip install --upgrade pip python -m pip install --upgrade setuptools wheel python -m pip install -r requirements.txt + python -m pip install pytest - name: Run tests - run: python setup.py test + run: pytest all_done: name: Tests done diff --git a/Makefile b/Makefile index d5c4e2c..720db9f 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ lint: flake8 cstruct tests test: - python3 setup.py test + pytest typecheck: mypy --strict --no-warn-unused-ignores cstruct From 27686732fb6df937d26e28011e19ba56f787d104 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Thu, 1 Sep 2022 07:08:32 +0000 Subject: [PATCH 14/95] Fix compare with None --- changelog.txt | 8 ++++++++ cstruct/__init__.py | 2 +- cstruct/abstract.py | 2 +- tests/test_cstruct.py | 6 ++++++ tests/test_memcstruct.py | 6 ++++++ 5 files changed, 22 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index 5da72e5..778507e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -119,3 +119,11 @@ - Python 3.10 support - pytest - black code style + +### 2.3 + +2022-09-01 + +### Fix + +- Fix compare with None diff --git a/cstruct/__init__.py b/cstruct/__init__.py index db4d3b1..96e5321 100644 --- a/cstruct/__init__.py +++ b/cstruct/__init__.py @@ -26,7 +26,7 @@ __author__ = 'Andrea Bonomi ' __license__ = 'MIT' -__version__ = '2.2' +__version__ = '2.3' __date__ = '15 August 2013' import struct diff --git a/cstruct/abstract.py b/cstruct/abstract.py index 1f8667c..2405544 100644 --- a/cstruct/abstract.py +++ b/cstruct/abstract.py @@ -141,7 +141,7 @@ def size(self) -> int: return self.__size__ def __eq__(self, other: Any) -> bool: - return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ + return other is not None and isinstance(other, self.__class__) and self.__dict__ == other.__dict__ def __ne__(self, other: Any) -> bool: return not self.__eq__(other) diff --git a/tests/test_cstruct.py b/tests/test_cstruct.py index 785dea9..cd7350b 100644 --- a/tests/test_cstruct.py +++ b/tests/test_cstruct.py @@ -246,3 +246,9 @@ def test_nested(): assert flat.endAddrCylSec == nested.end.addrCylSec assert flat.startLBA == nested.startLBA assert flat.endLBA == nested.endLBA + + +def test_null_compare(): + c = Dummy() + assert c is not None + assert c != None diff --git a/tests/test_memcstruct.py b/tests/test_memcstruct.py index f61da32..7ea92c3 100644 --- a/tests/test_memcstruct.py +++ b/tests/test_memcstruct.py @@ -243,3 +243,9 @@ def test_nested(): assert flat.endAddrCylSec == nested.end.addrCylSec assert flat.startLBA == nested.startLBA assert flat.endLBA == nested.endLBA + + +def test_null_compare(): + c = Dummy() + assert c is not None + assert c != None From 2789d12134aadddf6acce49cb7232026930114a8 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Tue, 23 Aug 2022 16:53:52 +0000 Subject: [PATCH 15/95] flexible array support --- README.md | 2 +- changelog.txt | 8 ++ cstruct/__init__.py | 26 +++- cstruct/abstract.py | 127 +++++++++++++------- cstruct/base.py | 7 +- cstruct/c_expr.py | 131 ++++++++++++++++++++ cstruct/c_parser.py | 225 ++++++++++++----------------------- cstruct/cstruct.py | 27 ++--- cstruct/field.py | 180 ++++++++++++++++++++++++++++ cstruct/mem_cstruct.py | 47 +++++--- examples/fdisk.py | 133 +++++++++++++++++---- examples/flexible_array.py | 64 ++++++++++ examples/who.py | 100 +++++++++++----- mbr | Bin 512 -> 512 bytes tests/test_alignment.py | 17 +++ tests/test_c_expr.py | 73 ++++++++++++ tests/test_flexible_array.py | 168 ++++++++++++++++++++++++++ 17 files changed, 1058 insertions(+), 277 deletions(-) create mode 100644 cstruct/c_expr.py create mode 100644 cstruct/field.py create mode 100644 examples/flexible_array.py create mode 100644 tests/test_c_expr.py create mode 100644 tests/test_flexible_array.py diff --git a/README.md b/README.md index 1edb855..20d12ef 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ C-style structs for Python [![PyPI](https://img.shields.io/pypi/pyversions/cstruct.svg)](https://pypi.org/project/cstruct) [![Downloads](https://pepy.tech/badge/cstruct/month)](https://pepy.tech/project/cstruct) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) - +[![Known Vulnerabilities](https://snyk.io/test/github/andreax79/python-cstruct/badge.svg)](https://snyk.io/test/github/andreax79/python-cstruct) Convert C struct/union definitions into Python classes with methods for serializing/deserializing. diff --git a/changelog.txt b/changelog.txt index 778507e..6f0e3e2 100644 --- a/changelog.txt +++ b/changelog.txt @@ -127,3 +127,11 @@ ### Fix - Fix compare with None + +### 2.4 + +2022-XX-XX + +### Added + +- flexible array support diff --git a/cstruct/__init__.py b/cstruct/__init__.py index 96e5321..f90d4e2 100644 --- a/cstruct/__init__.py +++ b/cstruct/__init__.py @@ -30,7 +30,7 @@ __date__ = '15 August 2013' import struct -from typing import Any, Type +from typing import Any, Dict, Optional, Type from .base import ( LITTLE_ENDIAN, BIG_ENDIAN, @@ -44,6 +44,7 @@ ) from .abstract import CStructMeta, AbstractCStruct from .cstruct import CStruct +from .c_parser import parse_def from .mem_cstruct import MemCStruct __all__ = [ @@ -56,6 +57,7 @@ 'MemCStruct', 'define', 'undef', + 'getdef', 'typedef', 'sizeof', 'parse', @@ -81,6 +83,15 @@ def undef(key: str) -> None: del DEFINES[key] +def getdef(key: str) -> Any: + """ + Return the value for a constant + + :param key: identifier + """ + return DEFINES[key] + + def typedef(type_: str, alias: str) -> None: """ Define an alias name for a data type @@ -108,7 +119,7 @@ def sizeof(type_: str) -> int: if t is None: raise KeyError("Unknow %s \"%s\"" % (kind, type_)) else: - return t.size + return t.sizeof() else: ttype = C_TYPE_TO_FORMAT.get(type_, None) if ttype is None: @@ -117,9 +128,12 @@ def sizeof(type_: str) -> int: return struct.calcsize(ttype) -def parse(__struct__: str, __cls__: Type[Any] = None, **kargs: Any) -> AbstractCStruct: +def parse( + __struct__: str, __cls__: Optional[Type[AbstractCStruct]] = None, __name__: Optional[str] = None, **kargs: Dict[str, Any] +) -> Optional[Type[AbstractCStruct]]: """ Return a new class mapping a C struct/union definition. + If the string does not contains any definition, return None. :param __struct__: definition of the struct (or union) in C syntax :param __cls__: (optional) super class - CStruct(default) or MemCStruct @@ -130,4 +144,8 @@ def parse(__struct__: str, __cls__: Type[Any] = None, **kargs: Any) -> AbstractC """ if __cls__ is None: __cls__ = CStruct - return __cls__.parse(__struct__, **kargs) + cls_def = parse_def(__struct__, __cls__=__cls__, **kargs) + if cls_def is None: + return None + else: + return __cls__.parse(cls_def, __name__, **kargs) diff --git a/cstruct/abstract.py b/cstruct/abstract.py index 2405544..f3e6f3e 100644 --- a/cstruct/abstract.py +++ b/cstruct/abstract.py @@ -25,30 +25,35 @@ # from abc import ABCMeta -from typing import cast, Any, BinaryIO, Optional, Type, Union +from collections import OrderedDict +from typing import Any, BinaryIO, List, Dict, Optional, Type, Tuple, Union from .base import STRUCTS import hashlib from .c_parser import parse_struct, parse_def, Tokens +from .field import calculate_padding, FieldType __all__ = ['CStructMeta', 'AbstractCStruct'] class CStructMeta(ABCMeta): - def __new__(cls, name: str, bases, dct): - __struct__ = dct.get("__struct__", None) - dct['__cls__'] = bases[0] + __size__: int = 0 + + # def __new__(cls: Type[type], name: str, bases: tuple, classdict: dict) -> MetaClass: + def __new__(metacls: Type[type], name: str, bases: Tuple[str], namespace: Dict[str, Any]) -> Type[Any]: + __struct__ = namespace.get('__struct__', None) + namespace['__cls__'] = bases[0] if bases else None # Parse the struct - if '__struct__' in dct: - if isinstance(dct['__struct__'], ("".__class__, u"".__class__, Tokens)): - dct.update(parse_struct(**dct)) + if '__struct__' in namespace: + if isinstance(namespace['__struct__'], (str, Tokens)): + namespace.update(parse_struct(**namespace)) __struct__ = True - if '__def__' in dct: - dct.update(parse_def(**dct)) + if '__def__' in namespace: + namespace.update(parse_def(**namespace)) __struct__ = True # Create the new class - new_class = type.__new__(cls, name, bases, dct) + new_class: Type[Any] = super().__new__(metacls, name, bases, namespace) # Register the class - if __struct__ is not None and not dct.get('__anonymous__'): + if __struct__ is not None and not namespace.get('__anonymous__'): STRUCTS[name] = new_class return new_class @@ -57,29 +62,38 @@ def __len__(cls) -> int: @property def size(cls) -> int: - """Structure size (in bytes)""" + "Structure size (in bytes)" return cls.__size__ -# Workaround for Python 2.x/3.x metaclass, thanks to -# http://mikewatkins.ca/2008/11/29/python-2-and-3-metaclasses/#using-the-metaclass-in-python-2-x-and-3-x -_CStructParent = CStructMeta('_CStructParent', (object,), {}) - - -class AbstractCStruct(_CStructParent): - def __init__(self, buffer=None, **kargs) -> None: +class AbstractCStruct(metaclass=CStructMeta): + __size__: int = 0 + __fields__: List[str] = [] + __fields_types__: Dict[str, FieldType] + __byte_order__: Optional[str] = None + __alignment__: int = 0 + __is_union__: bool = False + + def __init__( + self, buffer: Optional[Union[bytes, BinaryIO]] = None, flexible_array_length: Optional[int] = None, **kargs: Dict[str, Any] + ) -> None: + self.set_flexible_array_length(flexible_array_length) + self.__fields__ = [x for x in self.__fields__] # Create a copy + self.__fields_types__ = OrderedDict({k: v.copy() for k, v in self.__fields_types__.items()}) # Create a copy if buffer is not None: self.unpack(buffer) else: try: self.unpack(buffer) - except: + except Exception: pass for key, value in kargs.items(): setattr(self, key, value) @classmethod - def parse(cls, __struct__, __name__: Optional[str] = None, **kargs) -> Type[Any]: + def parse( + cls, __struct__: Union[str, Tokens, Dict[str, Any]], __name__: Optional[str] = None, **kargs: Dict[str, Any] + ) -> Type["AbstractCStruct"]: """ Return a new class mapping a C struct/union definition. @@ -89,31 +103,51 @@ def parse(cls, __struct__, __name__: Optional[str] = None, **kargs) -> Type[Any] :param __is_union__: (optional) True for union, False for struct (default) :returns: cls subclass """ - kargs = dict(kargs) - kargs['__struct__'] = __struct__ - if isinstance(__struct__, ("".__class__, u"".__class__, Tokens)): - del kargs['__struct__'] - kargs.update(parse_def(__struct__, __cls__=cls, **kargs)) - kargs['__struct__'] = None + cls_kargs: Dict[str, Any] = dict(kargs) + cls_kargs['__struct__'] = __struct__ + if isinstance(__struct__, (str, Tokens)): + del cls_kargs['__struct__'] + cls_kargs.update(parse_def(__struct__, __cls__=cls, **cls_kargs)) + cls_kargs['__struct__'] = None + elif isinstance(__struct__, dict): + del cls_kargs['__struct__'] + cls_kargs.update(__struct__) + cls_kargs['__struct__'] = None if __name__ is None: # Anonymous struct __name__ = cls.__name__ + '_' + hashlib.sha1(str(__struct__).encode('utf-8')).hexdigest() - kargs['__anonymous__'] = True - kargs['__name__'] = __name__ - return type(__name__, (cls,), kargs) + cls_kargs['__anonymous__'] = True + cls_kargs['__name__'] = __name__ + return type(__name__, (cls,), cls_kargs) - def unpack(self, buffer: Optional[Union[bytes, BinaryIO]]) -> bool: + def set_flexible_array_length(self, flexible_array_length: Optional[int]) -> None: + """ + Set flexible array length (i.e. number of elements) + + :flexible_array_length: flexible array length + """ + if flexible_array_length is not None: + # Search for the flexible array + flexible_array: Optional[FieldType] = [x for x in self.__fields_types__.values() if x.flexible_array][0] + if flexible_array is None: + raise ValueError("Flexible array not found in struct") + flexible_array.vlen = flexible_array_length + + def unpack(self, buffer: Optional[Union[bytes, BinaryIO]], flexible_array_length: Optional[int] = None) -> bool: """ Unpack bytes containing packed C structure data :param buffer: bytes or binary stream to be unpacked """ + self.set_flexible_array_length(flexible_array_length) if hasattr(buffer, 'read'): - buffer = buffer.read(self.__size__) + buffer = buffer.read(self.size) # type: ignore if not buffer: return False return self.unpack_from(buffer) - def unpack_from(self, buffer: Optional[bytes], offset: int = 0) -> bool: # pragma: no cover + def unpack_from( + self, buffer: Optional[bytes], offset: int = 0, flexible_array_length: Optional[int] = None + ) -> bool: # pragma: no cover """ Unpack bytes containing packed C structure data @@ -123,22 +157,35 @@ def unpack_from(self, buffer: Optional[bytes], offset: int = 0) -> bool: # prag raise NotImplementedError def pack(self) -> bytes: # pragma: no cover - """ - Pack the structure data into bytes - """ + "Pack the structure data into bytes" raise NotImplementedError def clear(self) -> None: self.unpack(None) def __len__(self) -> int: - """Structure size (in bytes)""" - return cast(int, self.__size__) + "Actual structure size (in bytes)" + return self.size @property def size(self) -> int: - """Structure size (in bytes)""" - return self.__size__ + "Actual structure size (in bytes)" + if not self.__fields_types__: # no fields + return 0 + elif self.__is_union__: # C union + # Calculate the sizeof union as size of its largest element + return max(x.vsize for x in self.__fields_types__.values()) + else: # C struct + # Calculate the sizeof struct as last item's offset + size + padding + last_field_type = list(self.__fields_types__.values())[-1] + size = last_field_type.offset + last_field_type.vsize + padding = calculate_padding(self.__byte_order__, self.__alignment__, size) + return size + padding + + @classmethod + def sizeof(cls) -> int: + "Structure size in bytes (flexible array member size is omitted)" + return cls.__size__ def __eq__(self, other: Any) -> bool: return other is not None and isinstance(other, self.__class__) and self.__dict__ == other.__dict__ diff --git a/cstruct/base.py b/cstruct/base.py index e6546bb..9f0b261 100644 --- a/cstruct/base.py +++ b/cstruct/base.py @@ -24,7 +24,10 @@ # IN THE SOFTWARE. # -from typing import Any, Dict, Type +from typing import Any, Dict, Type, TYPE_CHECKING + +if TYPE_CHECKING: + from .abstract import AbstractCStruct __all__ = [ 'LITTLE_ENDIAN', @@ -46,7 +49,7 @@ # native order, size & alignment NATIVE_ORDER = '@' -STRUCTS: Dict[str, Type[Any]] = {} +STRUCTS: Dict[str, Type["AbstractCStruct"]] = {} DEFINES: Dict[str, Any] = {} diff --git a/cstruct/c_expr.py b/cstruct/c_expr.py new file mode 100644 index 0000000..e12e439 --- /dev/null +++ b/cstruct/c_expr.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2013-2019 Andrea Bonomi +# +# Published under the terms of the MIT license. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. +# + +import ast +import operator +from typing import Any, Callable, Dict, Union, Type, TYPE_CHECKING +from .base import DEFINES, STRUCTS + +if TYPE_CHECKING: + from .abstract import AbstractCStruct + +__all__ = ['c_eval'] + + +def c_eval(expr: str) -> Union[int, float]: + "Evaluate a C arithmetic/logic expression and return the result" + expr = expr.replace("!", " not ").replace("&&", " and ").replace("||", " or ") + return eval_node(ast.parse(expr.strip()).body[0]) + + +def eval_node(node: ast.stmt) -> Union[int, float]: + handler = OPS[type(node)] + result = handler(node) + if isinstance(result, bool): # convert bool to int + return 1 if result else 0 + return result + + +def eval_get(node) -> Union[int, float, Type["AbstractCStruct"]]: + "Get definition/struct by name" + try: + return DEFINES[node.id] + except KeyError: + return STRUCTS[node.id] + + +def eval_compare(node) -> bool: + "Evaluate a compare node" + right = eval_node(node.left) + for operation, comp in zip(node.ops, node.comparators): + left = right + right = eval_node(comp) + if not OPS[type(operation)](left, right): + return False + return True + + +def eval_div(node) -> Union[int, float]: + "Evaluate div node (integer/float)" + left = eval_node(node.left) + right = eval_node(node.right) + if isinstance(left, float) or isinstance(right, float): + return operator.truediv(left, right) + else: + return operator.floordiv(left, right) + + +def eval_call(node) -> Union[int, float]: + if node.func.id == "sizeof": + from . import sizeof + + args = [eval_node(x) for x in node.args] + return sizeof(*args) + raise KeyError(node.func.id) + + +try: + Constant = ast.Constant +except AttributeError: # python < 3.8 + Constant = ast.NameConstant + +OPS: Dict[Type[ast.AST], Callable[[Any], Any]] = { + ast.Expr: lambda node: eval_node(node.value), + ast.Num: lambda node: node.n, + ast.Name: eval_get, + ast.Call: eval_call, + Constant: lambda node: node.value, + # and/or + ast.BoolOp: lambda node: OPS[type(node.op)](node), # and/or operator + ast.And: lambda node: all(eval_node(x) for x in node.values), # && operator + ast.Or: lambda node: any(eval_node(x) for x in node.values), # || operator + # binary + ast.BinOp: lambda node: OPS[type(node.op)](node), # binary operators + ast.Add: lambda node: operator.add(eval_node(node.left), eval_node(node.right)), # + operator + ast.Sub: lambda node: operator.sub(eval_node(node.left), eval_node(node.right)), # - operator + ast.Mult: lambda node: operator.mul(eval_node(node.left), eval_node(node.right)), # * operator + ast.Div: eval_div, + ast.Mod: lambda node: operator.mod(eval_node(node.left), eval_node(node.right)), # % operator + ast.LShift: lambda node: operator.lshift(eval_node(node.left), eval_node(node.right)), # << operator + ast.RShift: lambda node: operator.rshift(eval_node(node.left), eval_node(node.right)), # >> operator + ast.BitOr: lambda node: operator.or_(eval_node(node.left), eval_node(node.right)), # | operator + ast.BitXor: lambda node: operator.xor(eval_node(node.left), eval_node(node.right)), # ^ operator + ast.BitAnd: lambda node: operator.and_(eval_node(node.left), eval_node(node.right)), # & operator + # unary + ast.UnaryOp: lambda node: OPS[type(node.op)](node), # unary operators + ast.UAdd: lambda node: operator.pos(eval_node(node.operand)), # unary + operator + ast.USub: lambda node: operator.neg(eval_node(node.operand)), # unary - operator + ast.Not: lambda node: operator.not_(eval_node(node.operand)), # ! operator + ast.Invert: lambda node: operator.invert(eval_node(node.operand)), # ~ operator + # Compare + ast.Compare: eval_compare, + ast.Eq: operator.eq, + ast.NotEq: operator.ne, + ast.Gt: operator.gt, + ast.Lt: operator.lt, + ast.GtE: operator.ge, + ast.LtE: operator.le, +} diff --git a/cstruct/c_parser.py b/cstruct/c_parser.py index 814878f..02bd9ad 100644 --- a/cstruct/c_parser.py +++ b/cstruct/c_parser.py @@ -25,75 +25,36 @@ # import re -import struct -from typing import Union, Optional, Any, Tuple, Dict, Type -from .base import NATIVE_ORDER, DEFINES, TYPEDEFS, STRUCTS, C_TYPE_TO_FORMAT +from collections import OrderedDict +from typing import Union, Optional, Any, Dict, Type, TYPE_CHECKING +from .base import DEFINES, TYPEDEFS, STRUCTS +from .field import calculate_padding, Kind, FieldType +from .c_expr import c_eval +if TYPE_CHECKING: + from .abstract import AbstractCStruct -def align(__byte_order__: Optional[str]) -> bool: - return __byte_order__ is None or __byte_order__ == NATIVE_ORDER - - -class FieldType(object): - def __init__(self, vtype: str, vlen: int, vsize: int, fmt: str, offset: int, flexible_array: bool) -> None: - """ - Struct/Union field - - :param vtype: field type - :param vlen: number of elements - :param vsize: size in bytes - :param fmt: struct format prefixed by byte order - :param offset: relative memory position of the field (relative to the struct) - :param flexible_array: True for flexible arrays - """ - self.vtype = vtype - self.vlen = vlen - self.vsize = vsize - self.fmt = fmt - self.offset = offset - self.flexible_array = flexible_array - - def unpack_from(self, buffer: bytes, offset: int = 0) -> Any: - if self.flexible_array: # TODO - raise NotImplementedError("Flexible array member are not supported") # pragma: no cover - if isinstance(self.vtype, type): # is class - if self.vlen == 1: # single struct/union - result = self.vtype() - result.unpack_from(buffer, self.offset + offset) - return result - else: # multiple struct/union - result = [] - for j in range(0, self.vlen): - sub_struct = self.vtype() - sub_struct.unpack_from(buffer, self.offset + offset + j * sub_struct.size) - result.append(sub_struct) - return result - else: - result = struct.unpack_from(self.fmt, buffer, self.offset + offset) - if self.is_array: - return list(result) - else: - return result[0] - - def pack(self, data: Any) -> bytes: - if self.is_array: - return struct.pack(self.fmt, *data) - else: - return struct.pack(self.fmt, data) - - @property - def is_array(self) -> bool: - return not (self.vlen == 1 or self.vtype == 'char') +__all__ = ['parse_struct', 'parse_def', 'Tokens'] class Tokens(object): - def __init__(self, __struct__: str) -> None: + def __init__(self, text: str) -> None: # remove the comments - st = __struct__.replace("*/", "*/\n") - st = " ".join(re.split("/\*.*\*/", st)) - st = "\n".join([s.split("//")[0] for s in st.split("\n")]) - st = st.replace("\n", " ").replace(";", " ; ").replace("{", " { ").replace("}", " } ") - self.tokens = st.split() + text = re.sub(r"//.*?$|/\*.*?\*/", "", text, flags=re.S | re.MULTILINE) + # c preprocessor + lines = [] + for line in text.split("\n"): + if re.match(r"^\s*#define", line): + try: + _, name, value = line.strip().split(maxsplit=2) + DEFINES[name] = c_eval(value) + except Exception: + raise Exception("Parsing line {}".format(line)) + else: + lines.append(line) + text = " ".join(lines) + text = text.replace(";", " ; ").replace("{", " { ").replace("}", " } ") + self.tokens = text.split() def pop(self) -> str: return self.tokens.pop(0) @@ -111,24 +72,22 @@ def __str__(self) -> str: return str(self.tokens) -def parse_type( - tokens: Tokens, __cls__: Type[Any], __byte_order__: Optional[str] # Type['AbstractCStruct'], -) -> Tuple[str, int, int, str, bool, int]: +def parse_type(tokens: Tokens, __cls__: Type['AbstractCStruct'], byte_order: Optional[str], offset: int) -> "FieldType": if len(tokens) < 2: raise Exception("Parsing error") - vtype = tokens.pop() + c_type = tokens.pop() # signed/unsigned/struct - if vtype in ['signed', 'unsigned', 'struct', 'union'] and len(tokens) > 1: - vtype = vtype + " " + tokens.pop() + if c_type in ['signed', 'unsigned', 'struct', 'union'] and len(tokens) > 1: + c_type = c_type + " " + tokens.pop() next_token = tokens.pop() # short int, long int, or long long if next_token in ['int', 'long']: - vtype = vtype + " " + next_token + c_type = c_type + " " + next_token next_token = tokens.pop() # void * if next_token.startswith("*"): next_token = next_token[1:] - vtype = 'void *' + c_type = 'void *' # parse length vlen = 1 flexible_array = False @@ -137,69 +96,65 @@ def parse_type( if len(t) != 2: raise Exception("Error parsing: " + next_token) next_token = t[0].strip() - t_vlen = t[1].split("]")[0].strip() + vlen_part = t[1] + vlen_expr = [] + while not vlen_part.endswith("]"): + vlen_expr.append(vlen_part.split("]")[0].strip()) + vlen_part = tokens.pop() + t_vlen = vlen_part.split("]")[0].strip() + vlen_expr.append(vlen_part.split("]")[0].strip()) + t_vlen = " ".join(vlen_expr) if not t_vlen: flexible_array = True vlen = 0 else: try: + vlen = c_eval(t_vlen) + except (ValueError, TypeError): vlen = int(t_vlen) - except: - t_vlen = DEFINES.get(t_vlen, None) - if t_vlen is None: - raise - else: - vlen = int(t_vlen) tokens.push(next_token) # resolve typedefs - while vtype in TYPEDEFS: - vtype = TYPEDEFS[vtype] + while c_type in TYPEDEFS: + c_type = TYPEDEFS[c_type] # calculate fmt - if vtype.startswith('struct ') or vtype.startswith('union '): # struct/union - # __is_union__ = vtype.startswith('union ') - kind, vtype = vtype.split(' ', 1) + if c_type.startswith('struct ') or c_type.startswith('union '): # struct/union + c_type, tail = c_type.split(' ', 1) + kind = Kind.STRUCT if c_type == 'struct' else Kind.UNION if tokens.get() == '{': # Named nested struct - tokens.push(vtype) - tokens.push(kind) - tvtype = __cls__.parse(tokens, __name__=vtype, __byte_order__=__byte_order__) - elif vtype == '{': # Unnamed nested struct - tokens.push(vtype) - tokens.push(kind) - tvtype = __cls__.parse(tokens, __byte_order__=__byte_order__) + tokens.push(tail) + tokens.push(c_type) + ref = __cls__.parse(tokens, __name__=tail, __byte_order__=byte_order) + elif tail == '{': # Unnamed nested struct + tokens.push(tail) + tokens.push(c_type) + ref = __cls__.parse(tokens, __byte_order__=byte_order) else: try: - tvtype = STRUCTS[vtype] + ref = STRUCTS[tail] except KeyError: - raise Exception("Unknow %s \"%s\"" % (kind, vtype)) - ttype = "c" - fmt = str(vlen * tvtype.size) + ttype - # alignment/ - alignment = tvtype.__alignment__ + raise Exception("Unknow %s \"%s\"" % (c_type, tail)) else: # other types - try: - ttype = C_TYPE_TO_FORMAT[vtype] - except KeyError: - raise Exception("Unknow type \"" + vtype + "\"") - fmt = (str(vlen) if vlen > 1 or flexible_array else '') + ttype - # alignment - alignment = struct.calcsize((__byte_order__ + ttype) if __byte_order__ is not None else ttype) - tvtype = vtype - fmt = (__byte_order__ + fmt) if __byte_order__ is not None else fmt - vsize = struct.calcsize(fmt) - return tvtype, vlen, vsize, fmt, flexible_array, alignment + kind = Kind.NATIVE + ref = None + return FieldType(kind, c_type, ref, vlen, flexible_array, byte_order, offset) def parse_def( - __def__: Union[str, Tokens], __cls__: Type[Any], __byte_order__: Optional[str] = None, **kargs: Any # Type['AbstractCStruct'], -) -> Dict[str, Any]: + __def__: Union[str, Tokens], + __cls__: Type['AbstractCStruct'], + __byte_order__: Optional[str] = None, + **kargs: Any # Type['AbstractCStruct'], +) -> Optional[Dict[str, Any]]: # naive C struct parsing if isinstance(__def__, Tokens): tokens = __def__ else: tokens = Tokens(__def__) + if not tokens: + return None kind = tokens.pop() if kind not in ['struct', 'union']: - raise Exception("struct or union expected - " + kind) + raise Exception("struct or union expected - {}".format(kind)) __is_union__ = kind == 'union' vtype = tokens.pop() if tokens.get() == '{': # Named nested struct @@ -208,20 +163,19 @@ def parse_def( elif vtype == '{': # Unnamed nested struct return parse_struct(tokens, __cls__=__cls__, __is_union__=__is_union__, __byte_order__=__byte_order__) else: - raise Exception("%s definition expected" % vtype) + raise Exception("{} definition expected".format(vtype)) def parse_struct( __struct__: Union[str, Tokens], - __cls__: Type[Any], # Type['AbstractCStruct'], + __cls__: Type['AbstractCStruct'], __is_union__: bool = False, __byte_order__: Optional[str] = None, **kargs: Any ) -> Dict[str, Any]: # naive C struct parsing __is_union__ = bool(__is_union__) - fields = [] - fields_types = {} + fields_types: Dict[str, FieldType] = OrderedDict() flexible_array: bool = False offset: int = 0 max_alignment: int = 0 @@ -236,54 +190,33 @@ def parse_struct( # flexible array member must be the last member of such a struct if flexible_array: raise Exception("Flexible array member must be the last member of such a struct") - vtype, vlen, vsize, fmt, flexible_array, alignment = parse_type(tokens, __cls__, __byte_order__) + field_type = parse_type(tokens, __cls__, __byte_order__, offset) vname = tokens.pop() - fields.append(vname) + fields_types[vname] = field_type # calculate the max field size (for the alignment) - max_alignment = max(max_alignment, alignment) - # align stuct if byte order is native - if not __is_union__ and align(__byte_order__) and vtype != 'char': - modulo = offset % alignment - if modulo: # not aligned to the field size - delta = alignment - modulo - offset = offset + delta - fields_types[vname] = FieldType(vtype, vlen, vsize, fmt, offset, flexible_array) + max_alignment = max(max_alignment, field_type.alignment) + # align struct if byte order is native if not __is_union__: # C struct - offset = offset + vsize + field_type.align_filed_offset() + offset = field_type.offset + field_type.vsize t = tokens.pop() if t != ';': raise (Exception("; expected but %s found" % t)) if __is_union__: # C union - # Calculate the union size as size of its largest element - size = max([struct.calcsize(x.fmt) for x in fields_types.values()]) - fmt = '%ds' % size + # Calculate the sizeof union as size of its largest element + size = max([x.vsize for x in fields_types.values()]) else: # C struct - fmt = "".join(fmt) - # add padding to stuct if byte order is native - if not __is_union__ and align(__byte_order__): - modulo = offset % max_alignment - if modulo: # not aligned to the max field size - delta = max_alignment - modulo - offset = offset + delta - size = offset # (offset is calculated as size sum) - - # Add the byte order as prefix - if __byte_order__ is not None: - fmt = __byte_order__ + fmt + # add padding to struct if byte order is native + size = offset + calculate_padding(__byte_order__, max_alignment, offset) # Prepare the result result = { - '__fields__': fields, + '__fields__': list(fields_types.keys()), '__fields_types__': fields_types, '__size__': size, '__is_union__': __is_union__, '__byte_order__': __byte_order__, '__alignment__': max_alignment, } - - # # Add the missing fields to the class - # for field in __fields__ or []: - # if field not in dict: - # result[field] = None return result diff --git a/cstruct/cstruct.py b/cstruct/cstruct.py index a04a85d..88ca322 100644 --- a/cstruct/cstruct.py +++ b/cstruct/cstruct.py @@ -24,9 +24,9 @@ # IN THE SOFTWARE. # -from typing import Optional +from typing import List, Optional from .base import CHAR_ZERO -from .abstract import CStructMeta, AbstractCStruct +from .abstract import AbstractCStruct class CStruct(AbstractCStruct): @@ -38,24 +38,24 @@ class CStruct(AbstractCStruct): __is_union__ = (optional) True for union definitions, False for struct definitions (default) The following fields are generated from the C struct definition - __size__ = lenght of the structure in bytes + __size__ = size of the structure in bytes (flexible array member size is omitted) __fields__ = list of structure fields __fields_types__ = dictionary mapping field names to types Every fields defined in the structure is added to the class """ - __size__: int = 0 - - def unpack_from(self, buffer: Optional[bytes], offset: int = 0) -> bool: + def unpack_from(self, buffer: Optional[bytes], offset: int = 0, flexible_array_length: Optional[int] = None) -> bool: """ Unpack bytes containing packed C structure data :param buffer: bytes to be unpacked :param offset: optional buffer offset + :param flexible_array_length: optional flexible array lenght (number of elements) """ + self.set_flexible_array_length(flexible_array_length) if buffer is None: - buffer = CHAR_ZERO * self.__size__ + buffer = CHAR_ZERO * self.size for field, field_type in self.__fields_types__.items(): setattr(self, field, field_type.unpack_from(buffer, offset)) return True @@ -64,12 +64,11 @@ def pack(self) -> bytes: """ Pack the structure data into bytes """ - result = [] - for field in self.__fields__: - field_type = self.__fields_types__[field] - if isinstance(field_type.vtype, CStructMeta): + result: List[bytes] = [] + for field, field_type in self.__fields_types__.items(): + if field_type.is_struct or field_type.is_union: if field_type.vlen == 1: # single struct - v = getattr(self, field, field_type.vtype()) + v = getattr(self, field, field_type.ref()) v = v.pack() result.append(v) else: # multiple struct @@ -77,8 +76,8 @@ def pack(self) -> bytes: for j in range(0, field_type.vlen): try: v = values[j] - except: - v = field_type.vtype() + except KeyError: + v = field_type.ref() v = v.pack() result.append(v) else: diff --git a/cstruct/field.py b/cstruct/field.py new file mode 100644 index 0000000..00b2fb5 --- /dev/null +++ b/cstruct/field.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2013-2019 Andrea Bonomi +# +# Published under the terms of the MIT license. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. +# + +import copy +import struct +from enum import Enum +from typing import Optional, Any, List, Type, TYPE_CHECKING +from .base import NATIVE_ORDER, C_TYPE_TO_FORMAT + +if TYPE_CHECKING: + from .abstract import AbstractCStruct + +__all__ = ['align', 'calculate_padding', 'Kind', 'FieldType'] + + +def align(byte_order: Optional[str]) -> bool: + return byte_order is None or byte_order == NATIVE_ORDER + + +def calculate_padding(byte_order: Optional[str], alignment: int, pos: int) -> int: + if align(byte_order): # calculate the padding + modulo = pos % alignment + if modulo: # not aligned + return alignment - modulo + return 0 + + +class Kind(Enum): + NATIVE = 0 + STRUCT = 1 + UNION = 2 + + +class FieldType(object): + def __init__( + self, + kind: Kind, + c_type: str, + ref: Optional[Type["AbstractCStruct"]], + vlen: int, + flexible_array: bool, + byte_order: Optional[str], + offset: int, + ) -> None: + """ + Struct/Union field + + :param kind: struct/union/native + :param c_type: field type + :param ref: struct/union class ref + :param vlen: number of elements + :param flexible_array: True for flexible arrays + :param offset: relative memory position of the field (relative to the struct) + """ + self.kind = kind + self.c_type = c_type + self.ref = ref + self.vlen = vlen + self.flexible_array = flexible_array + self.byte_order = byte_order + self.offset = self.base_offset = offset + self.padding = 0 + + def unpack_from(self, buffer: bytes, offset: int = 0) -> Any: + if self.is_native: + result = struct.unpack_from(self.fmt, buffer, self.offset + offset) + if self.is_array: + return list(result) + else: + return result[0] + else: # struct/union + if self.vlen == 1: # single struct/union + instance: AbstractCStruct = self.ref() # type: ignore + instance.unpack_from(buffer, self.offset + offset) + return instance + else: # multiple struct/union + instances: List[AbstractCStruct] = [] + for j in range(0, self.vlen): + instance: AbstractCStruct = self.ref() # type: ignore + instance.unpack_from(buffer, self.offset + offset + j * instance.size) + instances.append(instance) + return instances + + def pack(self, data: Any) -> bytes: + if self.flexible_array: + self.vlen = len(data) # set flexible array size + return struct.pack(self.fmt, *data) + elif self.is_array: + return struct.pack(self.fmt, *data) + else: + return struct.pack(self.fmt, data) + + @property + def is_array(self) -> bool: + "True if field is an array/flexible array" + return self.flexible_array or (not (self.vlen == 1 or self.c_type == 'char')) + + @property + def is_native(self) -> bool: + return self.kind == Kind.NATIVE + + @property + def is_struct(self) -> bool: + return self.kind == Kind.STRUCT + + @property + def is_union(self) -> bool: + return self.kind == Kind.UNION + + @property + def native_format(self) -> str: + "Field format (struct library format)" + if self.is_native: + try: + return C_TYPE_TO_FORMAT[self.c_type] + except KeyError: + raise Exception("Unknow type \"" + self.c_type + "\"") + else: + return 'c' + + @property + def fmt(self) -> str: + "Field format prefixed by byte order (struct library format)" + if self.is_native: + fmt = (str(self.vlen) if self.vlen > 1 or self.flexible_array else '') + self.native_format + else: # Struct/Union + fmt = str(self.vlen * self.ref.sizeof()) + self.native_format + if self.byte_order: + return self.byte_order + fmt + else: + return fmt + + @property + def vsize(self) -> int: + "Field size in bytes" + return struct.calcsize(self.fmt) + + @property + def alignment(self) -> int: + "Alignment" + if self.is_native: + if self.byte_order is not None: + return struct.calcsize(self.byte_order + self.native_format) + else: + return struct.calcsize(self.native_format) + else: # struct/union + return self.ref.__alignment__ + + def align_filed_offset(self) -> None: + "Align file if byte order is native" + if align(self.byte_order) and self.c_type != 'char': + self.padding = calculate_padding(self.byte_order, self.alignment, self.base_offset) + self.offset = self.base_offset + self.padding + + def copy(self) -> "FieldType": + "Return a shallow copy of this FieldType" + return copy.copy(self) diff --git a/cstruct/mem_cstruct.py b/cstruct/mem_cstruct.py index bcd0b4e..de7cf4d 100644 --- a/cstruct/mem_cstruct.py +++ b/cstruct/mem_cstruct.py @@ -27,7 +27,7 @@ from typing import Any, List, Optional import ctypes import struct -from .abstract import CStructMeta, AbstractCStruct +from .abstract import AbstractCStruct class CStructList(List[Any]): @@ -36,7 +36,7 @@ def __init__(self, values: List[Any], name: str, parent: Optional['MemCStruct'] self.name = name self.parent = parent - def __setitem__(self, key: int, value: List[Any]) -> None: + def __setitem__(self, key: int, value: List[Any]) -> None: # noqa: F811 super().__setitem__(key, value) # Notify the parent when a value is changed if self.parent is not None: @@ -53,35 +53,34 @@ class MemCStruct(AbstractCStruct): The following fields are generated from the C struct definition __mem_ = mutable character buffer - __size__ = lenght of the structure in bytes + __size__ = size of the structure in bytes (flexible array member size is omitted) __fields__ = list of structure fields __fields_types__ = dictionary mapping field names to types Every fields defined in the structure is added to the class """ - __size__: int = 0 - __mem__: ctypes.Array + __mem__ = None - def unpack_from(self, buffer: Optional[bytes], offset: int = 0) -> bool: + def unpack_from(self, buffer: Optional[bytes], offset: int = 0, flexible_array_length: Optional[int] = None) -> bool: """ Unpack bytes containing packed C structure data :param buffer: bytes to be unpacked :param offset: optional buffer offset + :param flexible_array_length: optional flexible array lenght (number of elements) """ + self.set_flexible_array_length(flexible_array_length) self.__base__ = offset # Base offset if buffer is None: # the buffer is one item larger than its size and the last element is NUL - self.__mem__ = ctypes.create_string_buffer(self.__size__ + 1) + self.__mem__ = ctypes.create_string_buffer(self.size + 1) elif isinstance(buffer, ctypes.Array): self.__mem__ = buffer else: self.__mem__ = ctypes.create_string_buffer(buffer) for field, field_type in self.__fields_types__.items(): - if field_type.flexible_array: # TODO - raise NotImplementedError("Flexible array member are not supported") # pragma: no cover - if isinstance(field_type.vtype, CStructMeta): + if field_type.is_struct or field_type.is_union: setattr(self, field, field_type.unpack_from(self.__mem__, offset)) return True @@ -101,6 +100,19 @@ def pack(self) -> bytes: """ return self.__mem__.raw[:-1] # the buffer is one item larger than its size and the last element is NUL + def set_flexible_array_length(self, flexible_array_length: Optional[int]) -> None: + """ + Set flexible array length (i.e. number of elements) + + :flexible_array_length: flexible array length + """ + super().set_flexible_array_length(flexible_array_length) + if self.__mem__ is not None: + try: + ctypes.resize(self.__mem__, self.size + 1) + except ValueError: + pass + def __getattr__(self, attr: str) -> Any: field_type = self.__fields_types__[attr] result = field_type.unpack_from(self.__mem__, self.__base__) @@ -113,12 +125,15 @@ def __setattr__(self, attr: str, value: Any) -> None: field_type = self.__fields_types__.get(attr) if field_type is None: object.__setattr__(self, attr, value) - else: - if isinstance(field_type.vtype, CStructMeta): - object.__setattr__(self, attr, value) - else: - addr = field_type.offset + self.__base__ - self.memcpy(addr, field_type.pack(value), field_type.vsize) + elif field_type.is_struct or field_type.is_union: + object.__setattr__(self, attr, value) + else: # native + if field_type.flexible_array and len(value) != field_type.vlen: + # flexible array size changed, resize the buffer + field_type.vlen = len(value) + ctypes.resize(self.__mem__, self.size + 1) + addr = field_type.offset + self.__base__ + self.memcpy(addr, field_type.pack(value), field_type.vsize) def on_change_list(self, attr: str, key: int, value: Any) -> None: field_type = self.__fields_types__[attr] diff --git a/examples/fdisk.py b/examples/fdisk.py index b77ae21..c7b1a75 100644 --- a/examples/fdisk.py +++ b/examples/fdisk.py @@ -26,9 +26,52 @@ # # ***************************************************************************** +from pathlib import Path +import argparse import cstruct import sys +UNITS = ['B', 'K', 'M', 'G', 'T'] +SECTOR_SIZE = 512 +TYPES = { + 0x00: "Empty", + 0x01: "FAT12", + 0x05: "Extended", + 0x06: "FAT16", + 0x07: "HPFS/NTFS/exFAT", + 0x0B: "W95 FAT32", + 0x0C: "W95 FAT32 (LBA)", + 0x0E: "W95 FAT16 (LBA)", + 0x0F: "W95 extended (LBA)", + 0x11: "Hidden FAT12", + 0x14: "Hidden FAT16 <32M", + 0x16: "Hidden FAT16", + 0x17: "Hidden HPFS/NTFS", + 0x1B: "Hidden W95 FAT32", + 0x1C: "Hidden W95 FAT32 (LBA)", + 0x1E: "Hidden W95 FAT16 (LBA)", + 0x27: "Hidden NTFS WinRE", + 0x81: "Minix / old Linux", + 0x82: "Linux swap / Solaris", + 0x83: "Linux", + 0x85: "Linux extended", + 0x86: "NTFS volume set", + 0x87: "NTFS volume set", + 0x88: "Linux plaintext", + 0x8E: "Linux LVM", + 0x9F: "BSD/OS", + 0xA5: "FreeBSD", + 0xA6: "OpenBSD", + 0xAF: "HFS / HFS+", + 0xEA: "Linux extended boot", + 0xEE: "GPT", + 0xEF: "EFI (FAT-12/16/32)", + 0xF2: "DOS secondary", + 0xFB: "VMware VMFS", + 0xFC: "VMware VMKCORE", + 0xFD: "Linux raid autodetect", +} + class Position(cstruct.MemCStruct): __byte_order__ = cstruct.LITTLE_ENDIAN @@ -42,6 +85,8 @@ class Position(cstruct.MemCStruct): class Partition(cstruct.MemCStruct): __byte_order__ = cstruct.LITTLE_ENDIAN __struct__ = """ + #define ACTIVE_FLAG 0x80 + unsigned char status; /* 0x80 - active */ struct Position start; unsigned char partition_type; @@ -50,43 +95,81 @@ class Partition(cstruct.MemCStruct): unsigned int sectors; /* nr of sectors in partition */ """ - def print_info(self): - print("bootable: %s" % ((self.status & 0x80) and "Y" or "N")) - print("partition_type: %02X" % self.partition_type) - print("start: head: %X sectory: %X cyl: %X" % (self.start.head, self.start.sector, self.start.cyl)) - print("end: head: %X sectory: %X cyl: %X" % (self.end.head, self.end.sector, self.end.cyl)) - print("starting sector: %08X" % self.start_sect) - print("size MB: %s" % (self.sectors / 2 / 1024)) + @property + def bootable_str(self): + return "*" if (self.status & cstruct.getdef("ACTIVE_FLAG")) else " " + + @property + def end_sect(self): + return self.start_sect + self.sectors - 1 + + @property + def part_size_str(self): + val = self.sectors * SECTOR_SIZE + for unit in UNITS: + if val < 1000: + break + val = int(val / 1000) + return f"{val}{unit}" + + @property + def part_type_str(self): + return TYPES.get(self.partition_type, "") + + def __str__(self): + return f"{self.bootable_str} {self.start_sect:>10} {self.end_sect:>8} {self.sectors:>8} {self.part_size_str:>4} {self.partition_type:02x} {self.part_type_str}" class MBR(cstruct.MemCStruct): __byte_order__ = cstruct.LITTLE_ENDIAN __struct__ = """ - char unused[440]; - unsigned char disk_signature[4]; - unsigned char usualy_nulls[2]; - struct Partition partitions[4]; - char signature[2]; + #define MBR_SIZE 512 + #define MBR_DISK_SIGNATURE_SIZE 4 + #define MBR_USUALY_NULLS_SIZE 2 + #define MBR_SIGNATURE_SIZE 2 + #define MBR_BOOT_SIGNATURE 0xaa55 + #define MBR_PARTITIONS_NUM 4 + #define MBR_PARTITIONS_SIZE (sizeof(Partition) * MBR_PARTITIONS_NUM) + #define MBR_UNUSED_SIZE (MBR_SIZE - MBR_DISK_SIGNATURE_SIZE - MBR_USUALY_NULLS_SIZE - MBR_PARTITIONS_SIZE - MBR_SIGNATURE_SIZE) + + char unused[MBR_UNUSED_SIZE]; + unsigned char disk_signature[MBR_DISK_SIGNATURE_SIZE]; + unsigned char usualy_nulls[MBR_USUALY_NULLS_SIZE]; + struct Partition partitions[MBR_PARTITIONS_NUM]; + uint16 signature; """ + @property + def disk_signature_str(self): + return "".join(reversed([f"{x:02x}" for x in self.disk_signature])) + def print_info(self): - print("disk signature: %s" % "".join(["%02X" % x for x in self.disk_signature])) - print("usualy nulls: %s" % "".join(["%02X" % x for x in self.usualy_nulls])) + print(f"Sector size: {cstruct.getdef('MBR_SIZE')}") + if self.signature != cstruct.getdef('MBR_BOOT_SIGNATURE'): + print("Invalid MBR signature") + + print(f"Disk identifier: 0x{self.disk_signature_str}") + print() + print("Device Boot Start End Sectors Size Id Type") for i, partition in enumerate(self.partitions): - print("") - print("partition: %s" % i) - partition.print_info() + if partition.sectors: + print(f"part{i:<2} {partition}") def main(): - if len(sys.argv) != 2: - print("usage: %s disk" % sys.argv[0]) - sys.exit(2) - with open(sys.argv[1], "rb") as f: - mbr = MBR() - data = f.read(len(mbr)) - mbr.unpack(data) - mbr.print_info() + parser = argparse.ArgumentParser(description="Display or manipulate a disk partition table.") + parser.add_argument("disk") + args = parser.parse_args() + + try: + with Path(args.disk).open("rb") as f: + mbr = MBR() + data = f.read(len(mbr)) + mbr.unpack(data) + mbr.print_info() + except (IOError, OSError) as ex: + print(ex) + sys.exit(1) if __name__ == "__main__": diff --git a/examples/flexible_array.py b/examples/flexible_array.py new file mode 100644 index 0000000..ddb5047 --- /dev/null +++ b/examples/flexible_array.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import random +from cstruct import MemCStruct +from pathlib import Path + + +class FlexArray(MemCStruct): + __struct__ = """ + int length; + uint32 checksum; + long data[]; + """ + + def set_length(self, length): + self.length = length + self.set_flexible_array_length(length) + + +def write(filename, length): + print("---write---") + flex = FlexArray() + flex.set_length(length) + # Generate random data + flex.data = [random.randint(0, 2**63) for _ in range(0, length)] + # Calculate the checksum + flex.checksum = 0 + for num in flex.data: + flex.checksum = (flex.checksum + num) % 2**32 + print(f"checksum: {flex.checksum}") + # Write data + with Path(filename).open("wb") as f: + f.write(flex.pack()) + + +def read(filename): + print("---read---") + with Path(filename).open("rb") as f: + # Read the header + flex = FlexArray(f) + print(f"length: {flex.length}, checksum: {flex.checksum}") + # Read header and data + f.seek(0, 0) + flex.unpack(f, flexible_array_length=flex.length) + if len(flex.data) == flex.length: + print("length ok") + # Check the checksum + checksum = 0 + for num in flex.data: + checksum = (checksum + num) % 2**32 + if flex.checksum == checksum: + print("checksum ok") + + +def main(): + filename = "tempfile" + random.seed(5) + write(filename, 1000) + read(filename) + + +if __name__ == "__main__": + main() diff --git a/examples/who.py b/examples/who.py index 882fdf4..88cd8f3 100644 --- a/examples/who.py +++ b/examples/who.py @@ -46,13 +46,38 @@ # pts/28 2013-07-30 20:49 24942 id=s/28 term=0 exit=0 # pts/27 2013-08-02 17:59 31326 id=s/27 term=0 exit=0 # 012345678901234567890123456789012345678901234567890123456789012345678901234567890 -from cstruct import define, typedef, MemCStruct, NATIVE_ORDER +from cstruct import parse, getdef, typedef, MemCStruct, NATIVE_ORDER +from pathlib import Path +import argparse import sys import time -define("UT_NAMESIZE", 32) -define("UT_LINESIZE", 32) -define("UT_HOSTSIZE", 256) +DEFAULT_FILENAME = "/var/run/utmp" + +parse( + """ +/* Values for ut_type field, below */ + +#define EMPTY 0 /* Record does not contain valid info + (formerly known as UT_UNKNOWN on Linux) */ +#define RUN_LVL 1 /* Change in system run-level (see + init(1)) */ +#define BOOT_TIME 2 /* Time of system boot (in ut_tv) */ +#define NEW_TIME 3 /* Time after system clock change + (in ut_tv) */ +#define OLD_TIME 4 /* Time before system clock change + (in ut_tv) */ +#define INIT_PROCESS 5 /* Process spawned by init(1) */ +#define LOGIN_PROCESS 6 /* Session leader process for user login */ +#define USER_PROCESS 7 /* Normal process */ +#define DEAD_PROCESS 8 /* Terminated process */ +#define ACCOUNTING 9 /* Not implemented */ + +#define UT_LINESIZE 32 +#define UT_NAMESIZE 32 +#define UT_HOSTSIZE 256 +""" +) typedef("int", "pid_t") typedef("long", "time_t") @@ -73,7 +98,6 @@ class Timeval(MemCStruct): def str_from_c(string): - # return str(string.split("\0")[0]) return string.decode().split("\0")[0] @@ -96,33 +120,51 @@ class Utmp(MemCStruct): char __unused[20]; /* Reserved for future use */ """ - def print_info(self, all_): - "andreax + pts/0 2013-08-21 08:58 . 32341 (l26.box)" - " pts/34 2013-06-12 15:04 26396 id=s/34 term=0 exit=0" - if all_ or self.ut_type in [6, 7]: - print( - "%-10s %-12s %15s %15s %-8s" - % ( - str_from_c(self.ut_user), - str_from_c(self.ut_line), - time.strftime("%Y-%m-%d %H:%M", time.gmtime(self.ut_tv.tv_sec)), - self.ut_pid, - str_from_c(self.ut_host) - and "(%s)" % str_from_c(self.ut_host) - or str_from_c(self.ut_id) - and "id=%s" % str_from_c(self.ut_id) - or "", - ) - ) + @property + def user(self): + return str_from_c(self.ut_user) + + @property + def line(self): + return str_from_c(self.ut_line) + + @property + def time(self): + return time.strftime("%Y-%m-%d %H:%M", time.gmtime(self.ut_tv.tv_sec)) + + @property + def host(self): + if str_from_c(self.ut_host): + host = str_from_c(self.ut_host) + return f"({host})" + elif self.ut_id: + ut_id = str_from_c(self.ut_id) + return f"id={ut_id}" + else: + return "" + + def __str__(self): + return f"{self.user:<10s} {self.line:<12s} {self.time:<15s} {self.ut_pid:>15} {self.host:<8s}" + + def print_info(self, show_all): + if show_all or self.ut_type in (getdef('LOGIN_PROCESS'), getdef('USER_PROCESS')): + print(self) def main(): - utmp = len(sys.argv) > 1 and sys.argv[1] or "/var/run/utmp" - all_ = '-a' in sys.argv - with open(utmp, "rb") as f: - utmp = Utmp() - while utmp.unpack(f): - utmp.print_info(all_) + parser = argparse.ArgumentParser(description="Print information about users who are currently logged in.") + parser.add_argument("-a", "--all", action="store_true", dest="show_all", help="show all enties") + parser.add_argument("file", nargs="?", help="if FILE is not specified use /var/run/utmp", default=DEFAULT_FILENAME) + args = parser.parse_args() + + utmp = Utmp() + try: + with Path(args.file).open("rb") as f: + while utmp.unpack(f): + utmp.print_info(args.show_all) + except (IOError, OSError) as ex: + print(ex) + sys.exit(1) if __name__ == "__main__": diff --git a/mbr b/mbr index 16458fa5a44dda4afaaea5266914d9f6e6604058..d312e194289bb8874dd9617e8da714c19ffeac47 100644 GIT binary patch delta 15 WcmZo*X<*r~gOSlr1ib(N diff --git a/tests/test_alignment.py b/tests/test_alignment.py index c26f93d..252c353 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -186,31 +186,48 @@ class Foo10(CStruct): def test_utmp_sizeof(): + assert Utmp.__fields_types__['ut_type'].padding == 0 + assert Utmp.__fields_types__['ut_pid'].padding == 2 assert sizeof("struct Utmp") == 384 + assert Utmp().size == 384 # http://www.catb.org/esr/structure-packing/ def test_foo1_sizeof(): + assert Foo1.__fields_types__['p'].padding == 0 + assert Foo1.__fields_types__['c'].padding == 0 + assert Foo1.__fields_types__['x'].padding == 7 assert sizeof("struct Foo1") == 24 + assert Foo1().size == 24 def test_foo2_sizeof(): assert sizeof("struct Foo2") == 24 + assert Foo2().size == 24 def test_foo3_sizeof(): assert sizeof("struct Foo3") == 16 + assert Foo3().size == 16 def test_foo4_sizeof(): assert sizeof("struct Foo4") == 4 + assert Foo4().size == 4 def test_foo5_sizeof(): + assert Foo5.__fields_types__['c'].padding == 0 + assert Foo5.__fields_types__['inner'].padding == 7 assert sizeof("struct Foo5") == 24 + assert Foo5().size == 24 def test_foo10_sizeof(): + assert Foo10.__fields_types__['c'].padding == 0 + assert Foo10.__fields_types__['p'].padding == 7 + assert Foo10.__fields_types__['s'].padding == 0 assert sizeof("struct Foo10") == 24 + assert Foo10().size == 24 diff --git a/tests/test_c_expr.py b/tests/test_c_expr.py new file mode 100644 index 0000000..d46c012 --- /dev/null +++ b/tests/test_c_expr.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python +# ***************************************************************************** +# +# Copyright (c) 2013-2019 Andrea Bonomi +# +# Published under the terms of the MIT license. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. +# +# ***************************************************************************** + +from cstruct import parse, getdef, define +from cstruct.c_expr import c_eval + +def test_c_expr_def(): + parse(""" + #define A1 10 /* test */ + #define A2 10 + A1 /* comment */ + #define A3 30 + """) + assert getdef("A1") == 10 + assert getdef('A2') == 20 # TODO + assert c_eval("A1 / 10") == 1 + +def test_c_expr_binary(): + assert c_eval("6*2/( 2+1 * 2/3 +6) +8 * (8/4)") == 17 + assert c_eval("6*2/(2+2/3 + 6) + 8 * (8/4)") == 17 + assert c_eval("6*2/(2+0+6) + 8 * (8/4)") == 17 + assert c_eval("((1 + 2) + (3 - 4)) * 10 / 5") == 4 + assert c_eval("12 % 2") == 0 + assert c_eval("64 >> 2") == 16 + assert c_eval("3 & 2") == 2 + assert c_eval("3 | 2") == 3 + +def test_c_expr_bool(): + assert c_eval("3 && 2") == 1 + assert c_eval("3 && 2 && 1") == 1 + assert c_eval("3 || 2") == 1 + +def test_c_expr_unary(): + assert c_eval("16 << 2") == 64 + assert c_eval("+123") == 123 + assert c_eval("-123") == -123 + assert c_eval("!100") == -0 + assert c_eval("!0") == 1 + assert c_eval("~0") == -1 + assert c_eval("~1") == -2 + +def test_c_expr_compare(): + assert c_eval("1 == 2") == 0 + assert c_eval("5 == 5") == 1 + assert c_eval("1 < 2 <= 3") == 1 + assert c_eval("3 > 2 > 1") == 1 + assert c_eval("3 >= 30") == 0 + assert c_eval("3 <= 30") == 1 + define('A10', 10) + assert c_eval("((A10 < 6) || (A10>10))") == 0 diff --git a/tests/test_flexible_array.py b/tests/test_flexible_array.py new file mode 100644 index 0000000..a8a33df --- /dev/null +++ b/tests/test_flexible_array.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python +# ***************************************************************************** +# +# Copyright (c) 2013-2019 Andrea Bonomi +# +# Published under the terms of the MIT license. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. +# +# ***************************************************************************** + +import cstruct +from cstruct import sizeof + + +class Pkg(cstruct.CStruct): + __byte_order__ = cstruct.LITTLE_ENDIAN + __def__ = """ + struct { + uint16_t cmd; + uint16_t length; + uint8_t data[]; + } + """ + + +class MemPkg(cstruct.MemCStruct): + __byte_order__ = cstruct.LITTLE_ENDIAN + __def__ = """ + struct { + uint16_t cmd; + uint16_t length; + uint8_t data[]; + } + """ + + +def test_len(): + pkg = Pkg() + assert len(pkg) == sizeof('uint16_t') * 2 + assert len(pkg.pack()) + assert len(pkg) == sizeof('uint16_t') * 2 + assert pkg.sizeof() == sizeof('uint16_t') * 2 + assert pkg.__size__ == sizeof('uint16_t') * 2 + + pkg.length = 10 + pkg.data = list(range(pkg.length)) + assert len(pkg.pack()) == (sizeof('uint16_t') * 2) + (sizeof('uint8_t') * pkg.length) + assert len(pkg) == (sizeof('uint16_t') * 2) + (sizeof('uint8_t') * pkg.length) + assert pkg.sizeof() == sizeof('uint16_t') * 2 + assert pkg.__size__ == sizeof('uint16_t') * 2 + + pkg2 = Pkg() + pkg2.length = 20 + pkg2.data = list(range(pkg2.length)) + assert len(pkg2.pack()) == (sizeof('uint16_t') * 2) + (sizeof('uint8_t') * pkg2.length) + assert len(pkg2) == (sizeof('uint16_t') * 2) + (sizeof('uint8_t') * pkg2.length) + assert pkg2.sizeof() == sizeof('uint16_t') * 2 + assert pkg2.__size__ == sizeof('uint16_t') * 2 + assert len(pkg) != len(pkg2) + + +def test_pack_unpack(): + pkg = Pkg() + pkg.cmd = 5 + pkg.length = 10 + assert pkg.__fields_types__['data'].vlen == 0 + assert pkg.__fields_types__['data'].vsize == 0 + assert len(pkg) == sizeof('uint16_t') * 2 + pkg.data = list(range(pkg.length)) + assert len(pkg.pack()) == (sizeof('uint16_t') * 2) + (sizeof('uint8_t') * pkg.length) + assert pkg.__fields_types__['data'].vlen == pkg.length + assert pkg.__fields_types__['data'].vsize == (sizeof('uint8_t') * pkg.length) + assert len(pkg.data) == pkg.length + data = pkg.pack() + + pkg2 = Pkg() + assert pkg2.__fields_types__['data'].vlen == 0 + pkg2.unpack(data, flexible_array_length=pkg.length) + assert pkg2.__fields_types__['data'].vlen == pkg2.length + assert pkg2.cmd == pkg.cmd + assert pkg2.length == pkg.length + assert pkg2.data == pkg.data + assert len(pkg2.data) == len(pkg.data) + + pkg3 = Pkg(data, flexible_array_length=pkg.length) + assert pkg3.cmd == pkg.cmd + assert pkg3.length == pkg.length + assert len(pkg3.data) == len(pkg.data) + assert pkg3.data == pkg.data + + +def test_mem_len(): + pkg = MemPkg() + assert len(pkg) == sizeof('uint16_t') * 2 + assert len(pkg.pack()) + assert len(pkg) == sizeof('uint16_t') * 2 + assert pkg.sizeof() == sizeof('uint16_t') * 2 + assert pkg.__size__ == sizeof('uint16_t') * 2 + + pkg.length = 10 + pkg.data = list(range(pkg.length)) + assert len(pkg.pack()) == (sizeof('uint16_t') * 2) + (sizeof('uint8_t') * pkg.length) + assert len(pkg) == (sizeof('uint16_t') * 2) + (sizeof('uint8_t') * pkg.length) + assert pkg.sizeof() == sizeof('uint16_t') * 2 + assert pkg.__size__ == sizeof('uint16_t') * 2 + + pkg.length = 5 + pkg.data = list(range(pkg.length)) + assert len(pkg.pack()) == (sizeof('uint16_t') * 2) + (sizeof('uint8_t') * pkg.length) + assert len(pkg) == (sizeof('uint16_t') * 2) + (sizeof('uint8_t') * pkg.length) + assert pkg.sizeof() == sizeof('uint16_t') * 2 + assert pkg.__size__ == sizeof('uint16_t') * 2 + + pkg2 = MemPkg() + pkg2.length = 20 + pkg2.data = list(range(pkg2.length)) + assert len(pkg2.pack()) == (sizeof('uint16_t') * 2) + (sizeof('uint8_t') * pkg2.length) + assert len(pkg2) == (sizeof('uint16_t') * 2) + (sizeof('uint8_t') * pkg2.length) + assert pkg2.sizeof() == sizeof('uint16_t') * 2 + assert pkg2.__size__ == sizeof('uint16_t') * 2 + assert len(pkg) != len(pkg2) + + +def test_mem_pack_unpack(): + pkg = MemPkg() + pkg.cmd = 5 + pkg.length = 10 + assert pkg.__fields_types__['data'].vlen == 0 + assert pkg.__fields_types__['data'].vsize == 0 + assert len(pkg) == sizeof('uint16_t') * 2 + pkg.data = list(range(pkg.length)) + assert len(pkg.pack()) == (sizeof('uint16_t') * 2) + (sizeof('uint8_t') * pkg.length) + assert pkg.__fields_types__['data'].vlen == pkg.length + assert pkg.__fields_types__['data'].vsize == (sizeof('uint8_t') * pkg.length) + assert len(pkg.data) == pkg.length + data = pkg.pack() + + pkg2 = MemPkg() + assert pkg2.__fields_types__['data'].vlen == 0 + pkg2.unpack(data, flexible_array_length=pkg.length) + assert pkg2.__fields_types__['data'].vlen == pkg2.length + assert pkg2.cmd == pkg.cmd + assert pkg2.length == pkg.length + assert pkg2.data == pkg.data + assert len(pkg2.data) == len(pkg.data) + + pkg3 = MemPkg(data, flexible_array_length=pkg.length) + assert pkg3.cmd == pkg.cmd + assert pkg3.length == pkg.length + assert len(pkg3.data) == len(pkg.data) + assert pkg3.data == pkg.data From 3a241741f4253d839bac1a5b54ce74a275ffd4ab Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Mon, 5 Sep 2022 14:03:07 +0000 Subject: [PATCH 16/95] documentation --- .gitignore | 1 + .readthedocs.yaml | 7 ++++ LICENSE | 2 +- Makefile | 3 +- cstruct/__init__.py | 47 +++++++++++++---------- cstruct/abstract.py | 54 ++++++++++++++++++++------- cstruct/base.py | 10 ++--- cstruct/c_expr.py | 35 +++++++++++++---- cstruct/c_parser.py | 19 +++++----- cstruct/cstruct.py | 29 +++++++-------- cstruct/exceptions.py | 41 ++++++++++++++++++++ cstruct/field.py | 66 +++++++++++++++++++++++++++------ cstruct/mem_cstruct.py | 41 ++++++++++---------- docs/api/abstract.md | 1 + docs/api/base.md | 1 + docs/api/c_expr.md | 1 + docs/api/cstruct.md | 1 + docs/api/field.md | 1 + docs/api/mem_cstruct.md | 1 + docs/api/module.md | 1 + docs/examples/fdisk.md | 3 ++ docs/examples/flexible_array.md | 3 ++ docs/examples/who.md | 3 ++ docs/index.md | 12 ++++++ docs/license.md | 1 + mkdocs.yml | 34 +++++++++++++++++ requirements-dev.txt | 5 ++- tests/test_c_expr.py | 11 +++++- 28 files changed, 326 insertions(+), 108 deletions(-) create mode 100644 .readthedocs.yaml create mode 100644 cstruct/exceptions.py create mode 100644 docs/api/abstract.md create mode 100644 docs/api/base.md create mode 100644 docs/api/c_expr.md create mode 100644 docs/api/cstruct.md create mode 100644 docs/api/field.md create mode 100644 docs/api/mem_cstruct.md create mode 100644 docs/api/module.md create mode 100644 docs/examples/fdisk.md create mode 100644 docs/examples/flexible_array.md create mode 100644 docs/examples/who.md create mode 100644 docs/index.md create mode 100644 docs/license.md create mode 100644 mkdocs.yml diff --git a/.gitignore b/.gitignore index fc1cb1f..ee62e3d 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ nosetests.xml /include .mypy_cache pyvenv.cfg +site/ diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..8774cd0 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,7 @@ +# .readthedocs.yaml +version: 2 +mkdocs: + configuration: mkdocs.yml +python: + install: + - requirements: requirements-dev.txt diff --git a/LICENSE b/LICENSE index e7a1f91..d0b2f46 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2013-2017 Andrea Bonomi +Copyright (c) 2013-2022 Andrea Bonomi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/Makefile b/Makefile index 720db9f..02d0e21 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,8 @@ coverage: .PHONY: docs docs: - cd docs; $(MAKE) html + @mkdocs build + @mkdocs gh-deploy lint: flake8 cstruct tests diff --git a/cstruct/__init__.py b/cstruct/__init__.py index f90d4e2..f9179c2 100644 --- a/cstruct/__init__.py +++ b/cstruct/__init__.py @@ -1,6 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -# # Copyright (c) 2013-2019 Andrea Bonomi # # Published under the terms of the MIT license. @@ -40,7 +38,6 @@ TYPEDEFS, C_TYPE_TO_FORMAT, CHAR_ZERO, - EMPTY_BYTES_STRING, ) from .abstract import CStructMeta, AbstractCStruct from .cstruct import CStruct @@ -52,7 +49,6 @@ 'BIG_ENDIAN', 'NATIVE_ORDER', 'CHAR_ZERO', - 'EMPTY_BYTES_STRING', 'CStruct', 'MemCStruct', 'define', @@ -68,8 +64,9 @@ def define(key: str, value: Any) -> None: """ Define a constant that can be used in the C struct - :param key: identifier - :param value: value of the constant + Args: + key: identifier + value: value of the constant """ DEFINES[key] = value @@ -78,7 +75,8 @@ def undef(key: str) -> None: """ Undefine a symbol that was previously defined with define - :param key: identifier + Args: + key: identifier """ del DEFINES[key] @@ -87,7 +85,8 @@ def getdef(key: str) -> Any: """ Return the value for a constant - :param key: identifier + Args: + key: identifier """ return DEFINES[key] @@ -96,8 +95,9 @@ def typedef(type_: str, alias: str) -> None: """ Define an alias name for a data type - :param type_: data type - :param alias: new alias name + Args: + type_: data type + alias: new alias name """ TYPEDEFS[alias] = type_ @@ -106,8 +106,11 @@ def sizeof(type_: str) -> int: """ Return the size of the type. - :param type_: C type, struct or union (e.g. 'short int' or 'struct ZYZ') - :return: size in bytes + Args: + type_: C type, struct or union (e.g. 'short int' or 'struct ZYZ') + + Returns: + size: size in bytes """ while type_ in TYPEDEFS: type_ = TYPEDEFS[type_] @@ -129,18 +132,24 @@ def sizeof(type_: str) -> int: def parse( - __struct__: str, __cls__: Optional[Type[AbstractCStruct]] = None, __name__: Optional[str] = None, **kargs: Dict[str, Any] + __struct__: str, + __cls__: Optional[Type[AbstractCStruct]] = None, + __name__: Optional[str] = None, + **kargs: Dict[str, Any] ) -> Optional[Type[AbstractCStruct]]: """ Return a new class mapping a C struct/union definition. If the string does not contains any definition, return None. - :param __struct__: definition of the struct (or union) in C syntax - :param __cls__: (optional) super class - CStruct(default) or MemCStruct - :param __name__: (optional) name of the new class. If empty, a name based on the __struct__ hash is generated - :param __byte_order__: (optional) byte order, valid values are LITTLE_ENDIAN, BIG_ENDIAN, NATIVE_ORDER - :param __is_union__: (optional) True for union, False for struct (default) - :returns: __cls__ subclass + Args: + __struct__ (str): definition of the struct (or union) in C syntax + __cls__ (type): super class - CStruct(default) or MemCStruct + __name__ (str): name of the new class. If empty, a name based on the __struct__ hash is generated + __byte_order__ (str): byte order, valid values are LITTLE_ENDIAN, BIG_ENDIAN, NATIVE_ORDER + __is_union__ (bool): True for union, False for struct (default) + + Returns: + cls: __cls__ subclass """ if __cls__ is None: __cls__ = CStruct diff --git a/cstruct/abstract.py b/cstruct/abstract.py index f3e6f3e..50bf024 100644 --- a/cstruct/abstract.py +++ b/cstruct/abstract.py @@ -27,10 +27,11 @@ from abc import ABCMeta from collections import OrderedDict from typing import Any, BinaryIO, List, Dict, Optional, Type, Tuple, Union -from .base import STRUCTS import hashlib +from .base import STRUCTS from .c_parser import parse_struct, parse_def, Tokens from .field import calculate_padding, FieldType +from .exceptions import CStructException __all__ = ['CStructMeta', 'AbstractCStruct'] @@ -38,7 +39,6 @@ class CStructMeta(ABCMeta): __size__: int = 0 - # def __new__(cls: Type[type], name: str, bases: tuple, classdict: dict) -> MetaClass: def __new__(metacls: Type[type], name: str, bases: Tuple[str], namespace: Dict[str, Any]) -> Type[Any]: __struct__ = namespace.get('__struct__', None) namespace['__cls__'] = bases[0] if bases else None @@ -58,6 +58,7 @@ def __new__(metacls: Type[type], name: str, bases: Tuple[str], namespace: Dict[s return new_class def __len__(cls) -> int: + "Structure size (in bytes)" return cls.__size__ @property @@ -67,12 +68,22 @@ def size(cls) -> int: class AbstractCStruct(metaclass=CStructMeta): + """ + Abstract C struct to Python class + """ + __size__: int = 0 + " Size in bytes " __fields__: List[str] = [] + " Struct/union fileds " __fields_types__: Dict[str, FieldType] + " Dictionary mapping field names to types " __byte_order__: Optional[str] = None + " Byte order " __alignment__: int = 0 + " Alignament " __is_union__: bool = False + " True if the class is an union, False if it is a struct " def __init__( self, buffer: Optional[Union[bytes, BinaryIO]] = None, flexible_array_length: Optional[int] = None, **kargs: Dict[str, Any] @@ -92,18 +103,30 @@ def __init__( @classmethod def parse( - cls, __struct__: Union[str, Tokens, Dict[str, Any]], __name__: Optional[str] = None, **kargs: Dict[str, Any] + cls, + __struct__: Union[str, Tokens, Dict[str, Any]], + __name__: Optional[str] = None, + __byte_order__: Optional[str] = None, + __is_union__: Optional[bool] = False, + **kargs: Dict[str, Any] ) -> Type["AbstractCStruct"]: """ Return a new class mapping a C struct/union definition. - :param __struct__: definition of the struct (or union) in C syntax - :param __name__: (optional) name of the new class. If empty, a name based on the __struct__ hash is generated - :param __byte_order__: (optional) byte order, valid values are LITTLE_ENDIAN, BIG_ENDIAN, NATIVE_ORDER - :param __is_union__: (optional) True for union, False for struct (default) - :returns: cls subclass + Args: + __struct__: definition of the struct (or union) in C syntax + __name__: name of the new class. If empty, a name based on the __struct__ hash is generated + __byte_order__: byte order, valid values are LITTLE_ENDIAN, BIG_ENDIAN, NATIVE_ORDER + __is_union__: True for union, False for struct + + Returns: + cls: a new class mapping the defintion """ cls_kargs: Dict[str, Any] = dict(kargs) + if __byte_order__ is not None: + cls_kargs['__byte_order__'] = __byte_order__ + if __is_union__ is not None: + cls_kargs['__is_union__'] = __is_union__ cls_kargs['__struct__'] = __struct__ if isinstance(__struct__, (str, Tokens)): del cls_kargs['__struct__'] @@ -123,20 +146,23 @@ def set_flexible_array_length(self, flexible_array_length: Optional[int]) -> Non """ Set flexible array length (i.e. number of elements) - :flexible_array_length: flexible array length + Args: + flexible_array_length: flexible array length """ if flexible_array_length is not None: # Search for the flexible array flexible_array: Optional[FieldType] = [x for x in self.__fields_types__.values() if x.flexible_array][0] if flexible_array is None: - raise ValueError("Flexible array not found in struct") + raise CStructException("Flexible array not found in struct") flexible_array.vlen = flexible_array_length def unpack(self, buffer: Optional[Union[bytes, BinaryIO]], flexible_array_length: Optional[int] = None) -> bool: """ Unpack bytes containing packed C structure data - :param buffer: bytes or binary stream to be unpacked + Args: + buffer: bytes or binary stream to be unpacked + flexible_array_length: flexible array length """ self.set_flexible_array_length(flexible_array_length) if hasattr(buffer, 'read'): @@ -151,8 +177,10 @@ def unpack_from( """ Unpack bytes containing packed C structure data - :param buffer: bytes to be unpacked - :param offset: optional buffer offset + Args: + buffer: bytes to be unpacked + offset: optional buffer offset + flexible_array_length: flexible array length """ raise NotImplementedError diff --git a/cstruct/base.py b/cstruct/base.py index 9f0b261..b350a67 100644 --- a/cstruct/base.py +++ b/cstruct/base.py @@ -1,6 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -# # Copyright (c) 2013-2019 Andrea Bonomi # # Published under the terms of the MIT license. @@ -38,16 +36,15 @@ 'DEFINES', 'TYPEDEFS', 'C_TYPE_TO_FORMAT', - 'EMPTY_BYTES_STRING', 'CHAR_ZERO', ] -# little-endian, std. size & alignment LITTLE_ENDIAN = '<' -# big-endian, std. size & alignment +"Little-endian, std. size & alignment" BIG_ENDIAN = '>' -# native order, size & alignment +"Big-endian, std. size & alignment" NATIVE_ORDER = '@' +"Native order, size & alignment" STRUCTS: Dict[str, Type["AbstractCStruct"]] = {} @@ -94,5 +91,4 @@ 'uint64': 'Q', } -EMPTY_BYTES_STRING = bytes() CHAR_ZERO = bytes('\0', 'ascii') diff --git a/cstruct/c_expr.py b/cstruct/c_expr.py index e12e439..a74123d 100644 --- a/cstruct/c_expr.py +++ b/cstruct/c_expr.py @@ -1,6 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -# # Copyright (c) 2013-2019 Andrea Bonomi # # Published under the terms of the MIT license. @@ -28,6 +26,7 @@ import operator from typing import Any, Callable, Dict, Union, Type, TYPE_CHECKING from .base import DEFINES, STRUCTS +from .exceptions import EvalError if TYPE_CHECKING: from .abstract import AbstractCStruct @@ -36,9 +35,31 @@ def c_eval(expr: str) -> Union[int, float]: - "Evaluate a C arithmetic/logic expression and return the result" - expr = expr.replace("!", " not ").replace("&&", " and ").replace("||", " or ") - return eval_node(ast.parse(expr.strip()).body[0]) + """ + Evaluate a C arithmetic/logic expression and return the result + + Examples: + >>> c_eval('10 + (5 / 3)') + 11 + >>> c_eval('!0') + 1 + >>> c_eval('sizeof(x)') + 128 + + Args: + expr: C expression + + Returns: + result: the expression evaluation result + + Raises: + EvalError: expression evaluation error + """ + try: + expr = expr.replace("!", " not ").replace("&&", " and ").replace("||", " or ") + return eval_node(ast.parse(expr.strip()).body[0]) + except Exception: + raise EvalError def eval_node(node: ast.stmt) -> Union[int, float]: @@ -79,9 +100,9 @@ def eval_div(node) -> Union[int, float]: def eval_call(node) -> Union[int, float]: - if node.func.id == "sizeof": - from . import sizeof + from . import sizeof + if node.func.id == "sizeof": args = [eval_node(x) for x in node.args] return sizeof(*args) raise KeyError(node.func.id) diff --git a/cstruct/c_parser.py b/cstruct/c_parser.py index 02bd9ad..d1a947c 100644 --- a/cstruct/c_parser.py +++ b/cstruct/c_parser.py @@ -1,6 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -# # Copyright (c) 2013-2019 Andrea Bonomi # # Published under the terms of the MIT license. @@ -30,6 +28,7 @@ from .base import DEFINES, TYPEDEFS, STRUCTS from .field import calculate_padding, Kind, FieldType from .c_expr import c_eval +from .exceptions import CStructException, ParserError if TYPE_CHECKING: from .abstract import AbstractCStruct @@ -49,7 +48,7 @@ def __init__(self, text: str) -> None: _, name, value = line.strip().split(maxsplit=2) DEFINES[name] = c_eval(value) except Exception: - raise Exception("Parsing line {}".format(line)) + raise ParserError("Parsing line {}".format(line)) else: lines.append(line) text = " ".join(lines) @@ -74,7 +73,7 @@ def __str__(self) -> str: def parse_type(tokens: Tokens, __cls__: Type['AbstractCStruct'], byte_order: Optional[str], offset: int) -> "FieldType": if len(tokens) < 2: - raise Exception("Parsing error") + raise ParserError("Parsing error") c_type = tokens.pop() # signed/unsigned/struct if c_type in ['signed', 'unsigned', 'struct', 'union'] and len(tokens) > 1: @@ -94,7 +93,7 @@ def parse_type(tokens: Tokens, __cls__: Type['AbstractCStruct'], byte_order: Opt if "[" in next_token: t = next_token.split("[") if len(t) != 2: - raise Exception("Error parsing: " + next_token) + raise ParserError("Error parsing: " + next_token) next_token = t[0].strip() vlen_part = t[1] vlen_expr = [] @@ -132,7 +131,7 @@ def parse_type(tokens: Tokens, __cls__: Type['AbstractCStruct'], byte_order: Opt try: ref = STRUCTS[tail] except KeyError: - raise Exception("Unknow %s \"%s\"" % (c_type, tail)) + raise ParserError("Unknow {} {}".format(c_type, tail)) else: # other types kind = Kind.NATIVE ref = None @@ -154,7 +153,7 @@ def parse_def( return None kind = tokens.pop() if kind not in ['struct', 'union']: - raise Exception("struct or union expected - {}".format(kind)) + raise ParserError("struct or union expected - {}".format(kind)) __is_union__ = kind == 'union' vtype = tokens.pop() if tokens.get() == '{': # Named nested struct @@ -163,7 +162,7 @@ def parse_def( elif vtype == '{': # Unnamed nested struct return parse_struct(tokens, __cls__=__cls__, __is_union__=__is_union__, __byte_order__=__byte_order__) else: - raise Exception("{} definition expected".format(vtype)) + raise ParserError("{} definition expected".format(vtype)) def parse_struct( @@ -189,7 +188,7 @@ def parse_struct( break # flexible array member must be the last member of such a struct if flexible_array: - raise Exception("Flexible array member must be the last member of such a struct") + raise CStructException("Flexible array member must be the last member of such a struct") field_type = parse_type(tokens, __cls__, __byte_order__, offset) vname = tokens.pop() fields_types[vname] = field_type @@ -201,7 +200,7 @@ def parse_struct( offset = field_type.offset + field_type.vsize t = tokens.pop() if t != ';': - raise (Exception("; expected but %s found" % t)) + raise ParserError("; expected but %s found" % t) if __is_union__: # C union # Calculate the sizeof union as size of its largest element diff --git a/cstruct/cstruct.py b/cstruct/cstruct.py index 88ca322..78dd801 100644 --- a/cstruct/cstruct.py +++ b/cstruct/cstruct.py @@ -1,6 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -# # Copyright (c) 2013-2019 Andrea Bonomi # # Published under the terms of the MIT license. @@ -33,25 +31,23 @@ class CStruct(AbstractCStruct): """ Convert C struct definitions into Python classes. - __struct__ = definition of the struct (or union) in C syntax - __byte_order__ = (optional) byte order, valid values are LITTLE_ENDIAN, BIG_ENDIAN, NATIVE_ORDER - __is_union__ = (optional) True for union definitions, False for struct definitions (default) - - The following fields are generated from the C struct definition - __size__ = size of the structure in bytes (flexible array member size is omitted) - __fields__ = list of structure fields - __fields_types__ = dictionary mapping field names to types - Every fields defined in the structure is added to the class - + Attributes: + __struct__ (str): definition of the struct (or union) in C syntax + __byte_order__ (str): byte order, valid values are LITTLE_ENDIAN, BIG_ENDIAN, NATIVE_ORDER + __is_union__ (bool): True for union definitions, False for struct definitions + __size__ (int): size of the structure in bytes (flexible array member size is omitted) + __fields__ (list): list of structure fields + __fields_types__ (dict): dictionary mapping field names to types """ def unpack_from(self, buffer: Optional[bytes], offset: int = 0, flexible_array_length: Optional[int] = None) -> bool: """ Unpack bytes containing packed C structure data - :param buffer: bytes to be unpacked - :param offset: optional buffer offset - :param flexible_array_length: optional flexible array lenght (number of elements) + Args: + buffer: bytes to be unpacked + offset: optional buffer offset + flexible_array_length: optional flexible array lenght (number of elements) """ self.set_flexible_array_length(flexible_array_length) if buffer is None: @@ -63,6 +59,9 @@ def unpack_from(self, buffer: Optional[bytes], offset: int = 0, flexible_array_l def pack(self) -> bytes: """ Pack the structure data into bytes + + Returns: + bytes: The packed structure """ result: List[bytes] = [] for field, field_type in self.__fields_types__.items(): diff --git a/cstruct/exceptions.py b/cstruct/exceptions.py new file mode 100644 index 0000000..65cd668 --- /dev/null +++ b/cstruct/exceptions.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +# Copyright (c) 2013-2019 Andrea Bonomi +# +# Published under the terms of the MIT license. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. +# + +__all__ = [ + "CStructException", + "ParserError", + "EvalError", +] + + +class CStructException(Exception): + pass + + +class ParserError(CStructException): + pass + + +class EvalError(CStructException): + pass diff --git a/cstruct/field.py b/cstruct/field.py index 00b2fb5..2597b83 100644 --- a/cstruct/field.py +++ b/cstruct/field.py @@ -1,6 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -# # Copyright (c) 2013-2019 Andrea Bonomi # # Published under the terms of the MIT license. @@ -29,6 +27,7 @@ from enum import Enum from typing import Optional, Any, List, Type, TYPE_CHECKING from .base import NATIVE_ORDER, C_TYPE_TO_FORMAT +from .exceptions import ParserError if TYPE_CHECKING: from .abstract import AbstractCStruct @@ -49,12 +48,32 @@ def calculate_padding(byte_order: Optional[str], alignment: int, pos: int) -> in class Kind(Enum): + """ + Field type + """ + NATIVE = 0 + "Native type (e.g. int, char)" STRUCT = 1 + "Struct type" UNION = 2 + "Union type" class FieldType(object): + """ + Struct/Union field + + Attributes: + kind (Kind): struct/union/native + c_type (str): field type + ref (AbstractCStruct): struct/union class ref + vlen (int): number of elements + flexible_array (bool): True for flexible arrays + offset (int): relative memory position of the field (relative to the struct) + padding (int): padding + """ + def __init__( self, kind: Kind, @@ -66,14 +85,15 @@ def __init__( offset: int, ) -> None: """ - Struct/Union field - - :param kind: struct/union/native - :param c_type: field type - :param ref: struct/union class ref - :param vlen: number of elements - :param flexible_array: True for flexible arrays - :param offset: relative memory position of the field (relative to the struct) + Initialize a Struct/Union field + + Args: + kind: struct/union/native + c_type: field type + ref: struct/union class ref + vlen: number of elements + flexible_array: True for flexible arrays + offset: relative memory position of the field (relative to the struct) """ self.kind = kind self.c_type = c_type @@ -85,6 +105,16 @@ def __init__( self.padding = 0 def unpack_from(self, buffer: bytes, offset: int = 0) -> Any: + """ + Unpack bytes containing packed C structure data + + Args: + buffer: bytes to be unpacked + offset: optional buffer offset + + Returns: + data: The unpacked data + """ if self.is_native: result = struct.unpack_from(self.fmt, buffer, self.offset + offset) if self.is_array: @@ -105,6 +135,15 @@ def unpack_from(self, buffer: bytes, offset: int = 0) -> Any: return instances def pack(self, data: Any) -> bytes: + """ + Pack the field into bytes + + Args: + data: data to be packed + + Returns: + bytes: The packed structure + """ if self.flexible_array: self.vlen = len(data) # set flexible array size return struct.pack(self.fmt, *data) @@ -120,14 +159,17 @@ def is_array(self) -> bool: @property def is_native(self) -> bool: + "True if the field is a native type (e.g. int, char)" return self.kind == Kind.NATIVE @property def is_struct(self) -> bool: + "True if the field is a struct" return self.kind == Kind.STRUCT @property def is_union(self) -> bool: + "True if the field is an union" return self.kind == Kind.UNION @property @@ -137,7 +179,7 @@ def native_format(self) -> str: try: return C_TYPE_TO_FORMAT[self.c_type] except KeyError: - raise Exception("Unknow type \"" + self.c_type + "\"") + raise ParserError("Unknow type {}".format(self.c_type)) else: return 'c' @@ -170,7 +212,7 @@ def alignment(self) -> int: return self.ref.__alignment__ def align_filed_offset(self) -> None: - "Align file if byte order is native" + "If the byte order is native, align the field" if align(self.byte_order) and self.c_type != 'char': self.padding = calculate_padding(self.byte_order, self.alignment, self.base_offset) self.offset = self.base_offset + self.padding diff --git a/cstruct/mem_cstruct.py b/cstruct/mem_cstruct.py index de7cf4d..61f2c6a 100644 --- a/cstruct/mem_cstruct.py +++ b/cstruct/mem_cstruct.py @@ -1,6 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -# # Copyright (c) 2013-2019 Andrea Bonomi # # Published under the terms of the MIT license. @@ -47,17 +45,14 @@ class MemCStruct(AbstractCStruct): """ Convert C struct definitions into Python classes. - __struct__ = definition of the struct (or union) in C syntax - __byte_order__ = (optional) byte order, valid values are LITTLE_ENDIAN, BIG_ENDIAN, NATIVE_ORDER - __is_union__ = (optional) True for union definitions, False for struct definitions (default) - - The following fields are generated from the C struct definition - __mem_ = mutable character buffer - __size__ = size of the structure in bytes (flexible array member size is omitted) - __fields__ = list of structure fields - __fields_types__ = dictionary mapping field names to types - Every fields defined in the structure is added to the class - + Attributes: + __struct__ (str): definition of the struct (or union) in C syntax + __byte_order__ (str): byte order, valid values are LITTLE_ENDIAN, BIG_ENDIAN, NATIVE_ORDER + __is_union__ (bool): True for union definitions, False for struct definitions + __mem__: mutable character buffer + __size__ (int): size of the structure in bytes (flexible array member size is omitted) + __fields__ (list): list of structure fields + __fields_types__ (dict): dictionary mapping field names to types """ __mem__ = None @@ -66,9 +61,10 @@ def unpack_from(self, buffer: Optional[bytes], offset: int = 0, flexible_array_l """ Unpack bytes containing packed C structure data - :param buffer: bytes to be unpacked - :param offset: optional buffer offset - :param flexible_array_length: optional flexible array lenght (number of elements) + Args: + buffer: bytes to be unpacked + offset: optional buffer offset + flexible_array_length: optional flexible array lenght (number of elements) """ self.set_flexible_array_length(flexible_array_length) self.__base__ = offset # Base offset @@ -88,15 +84,19 @@ def memcpy(self, destination: int, source: bytes, num: int) -> None: """ Copies the values of num bytes from source to the struct memory - :param destination: destination address - :param source: source data to be copied - :param num: number of bytes to copy + Args: + destination: destination address + source: source data to be copied + num: number of bytes to copy """ ctypes.memmove(ctypes.byref(self.__mem__, destination), source, num) def pack(self) -> bytes: """ Pack the structure data into bytes + + Returns: + bytes: The packed structure """ return self.__mem__.raw[:-1] # the buffer is one item larger than its size and the last element is NUL @@ -104,7 +104,8 @@ def set_flexible_array_length(self, flexible_array_length: Optional[int]) -> Non """ Set flexible array length (i.e. number of elements) - :flexible_array_length: flexible array length + Args: + flexible_array_length: flexible array length """ super().set_flexible_array_length(flexible_array_length) if self.__mem__ is not None: diff --git a/docs/api/abstract.md b/docs/api/abstract.md new file mode 100644 index 0000000..1ccb5ce --- /dev/null +++ b/docs/api/abstract.md @@ -0,0 +1 @@ +::: cstruct.abstract diff --git a/docs/api/base.md b/docs/api/base.md new file mode 100644 index 0000000..45f824e --- /dev/null +++ b/docs/api/base.md @@ -0,0 +1 @@ +::: cstruct.base diff --git a/docs/api/c_expr.md b/docs/api/c_expr.md new file mode 100644 index 0000000..0602709 --- /dev/null +++ b/docs/api/c_expr.md @@ -0,0 +1 @@ +::: cstruct.c_expr diff --git a/docs/api/cstruct.md b/docs/api/cstruct.md new file mode 100644 index 0000000..a3274cf --- /dev/null +++ b/docs/api/cstruct.md @@ -0,0 +1 @@ +::: cstruct.cstruct diff --git a/docs/api/field.md b/docs/api/field.md new file mode 100644 index 0000000..29dfe8a --- /dev/null +++ b/docs/api/field.md @@ -0,0 +1 @@ +::: cstruct.field diff --git a/docs/api/mem_cstruct.md b/docs/api/mem_cstruct.md new file mode 100644 index 0000000..d08bd26 --- /dev/null +++ b/docs/api/mem_cstruct.md @@ -0,0 +1 @@ +::: cstruct.mem_cstruct diff --git a/docs/api/module.md b/docs/api/module.md new file mode 100644 index 0000000..8f87977 --- /dev/null +++ b/docs/api/module.md @@ -0,0 +1 @@ +::: cstruct diff --git a/docs/examples/fdisk.md b/docs/examples/fdisk.md new file mode 100644 index 0000000..1680788 --- /dev/null +++ b/docs/examples/fdisk.md @@ -0,0 +1,3 @@ +``` +{!examples/fdisk.py!} +``` diff --git a/docs/examples/flexible_array.md b/docs/examples/flexible_array.md new file mode 100644 index 0000000..ea76b6e --- /dev/null +++ b/docs/examples/flexible_array.md @@ -0,0 +1,3 @@ +``` +{!examples/flexible_array.py!} +``` diff --git a/docs/examples/who.md b/docs/examples/who.md new file mode 100644 index 0000000..b4521ab --- /dev/null +++ b/docs/examples/who.md @@ -0,0 +1,3 @@ +``` +{!examples/who.py!} +``` diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..9850dfb --- /dev/null +++ b/docs/index.md @@ -0,0 +1,12 @@ +# Python-CStruct + +Convert C struct/union definitions into Python classes with methods for +serializing/deserializing. + +The usage is very simple: create a class subclassing cstruct.MemCStruct +and add a C struct/union definition as a string in the `__struct__` field. + +The C struct/union definition is parsed at runtime and the struct format string +is generated. The class offers the method `unpack` for deserializing +an array of bytes into a Python object and the method `pack` for +serializing the values into an array of bytes. diff --git a/docs/license.md b/docs/license.md new file mode 100644 index 0000000..84f33f7 --- /dev/null +++ b/docs/license.md @@ -0,0 +1 @@ +{!LICENSE!} diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..01fb098 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,34 @@ +site_name: Python CStruct documentation +docs_dir: docs + +theme: + name: readthedocs + highlightjs: true + +plugins: +- search +- autorefs +- mkdocstrings: + watch: + - cstruct + - examples + +nav: + - CStruct Docs: index.md + - Examples: + - "fdisk.py": examples/fdisk.md + - "flexible_array.py": examples/flexible_array.md + - "who.py": examples/who.md + - API: + - "cstruct": api/module.md + - "cstruct.abstract": api/abstract.md + - "cstruct.base": api/base.md + - "cstruct.c_expr": api/c_expr.md + - "cstruct.cstruct": api/cstruct.md + - "cstruct.field": api/field.md + - "cstruct.mem_cstruct": api/mem_cstruct.md + - License: license.md + +markdown_extensions: + - markdown_include.include: + base_path: . diff --git a/requirements-dev.txt b/requirements-dev.txt index abd417e..8bf236b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,8 +1,11 @@ coverage flake8 mypy -sphinx tox black twine<3.4 pytest +mkdocs +mkdocstrings[python] +mkdocs-material +markdown_include diff --git a/tests/test_c_expr.py b/tests/test_c_expr.py index d46c012..8230bd6 100644 --- a/tests/test_c_expr.py +++ b/tests/test_c_expr.py @@ -28,16 +28,20 @@ from cstruct import parse, getdef, define from cstruct.c_expr import c_eval + def test_c_expr_def(): - parse(""" + parse( + """ #define A1 10 /* test */ #define A2 10 + A1 /* comment */ #define A3 30 - """) + """ + ) assert getdef("A1") == 10 assert getdef('A2') == 20 # TODO assert c_eval("A1 / 10") == 1 + def test_c_expr_binary(): assert c_eval("6*2/( 2+1 * 2/3 +6) +8 * (8/4)") == 17 assert c_eval("6*2/(2+2/3 + 6) + 8 * (8/4)") == 17 @@ -48,11 +52,13 @@ def test_c_expr_binary(): assert c_eval("3 & 2") == 2 assert c_eval("3 | 2") == 3 + def test_c_expr_bool(): assert c_eval("3 && 2") == 1 assert c_eval("3 && 2 && 1") == 1 assert c_eval("3 || 2") == 1 + def test_c_expr_unary(): assert c_eval("16 << 2") == 64 assert c_eval("+123") == 123 @@ -62,6 +68,7 @@ def test_c_expr_unary(): assert c_eval("~0") == -1 assert c_eval("~1") == -2 + def test_c_expr_compare(): assert c_eval("1 == 2") == 0 assert c_eval("5 == 5") == 1 From 4bd11910ec56b89eb80c2fae45d11677e9228e92 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Mon, 5 Sep 2022 14:03:07 +0000 Subject: [PATCH 17/95] documentation --- .gitignore | 1 + .readthedocs.yaml | 7 ++++ LICENSE | 2 +- Makefile | 3 +- cstruct/__init__.py | 47 +++++++++++++---------- cstruct/abstract.py | 54 ++++++++++++++++++++------- cstruct/base.py | 10 ++--- cstruct/c_expr.py | 35 +++++++++++++---- cstruct/c_parser.py | 19 +++++----- cstruct/cstruct.py | 29 +++++++-------- cstruct/exceptions.py | 41 ++++++++++++++++++++ cstruct/field.py | 66 +++++++++++++++++++++++++++------ cstruct/mem_cstruct.py | 41 ++++++++++---------- docs/api/abstract.md | 1 + docs/api/base.md | 1 + docs/api/c_expr.md | 1 + docs/api/cstruct.md | 1 + docs/api/field.md | 1 + docs/api/mem_cstruct.md | 1 + docs/api/module.md | 1 + docs/examples/fdisk.md | 3 ++ docs/examples/flexible_array.md | 3 ++ docs/examples/who.md | 3 ++ docs/index.md | 12 ++++++ docs/license.md | 1 + examples/fdisk.py | 26 ------------- examples/flexible_array.py | 1 - examples/who.py | 48 +----------------------- mkdocs.yml | 34 +++++++++++++++++ requirements-dev.txt | 5 ++- tests/test_c_expr.py | 11 +++++- 31 files changed, 327 insertions(+), 182 deletions(-) create mode 100644 .readthedocs.yaml create mode 100644 cstruct/exceptions.py create mode 100644 docs/api/abstract.md create mode 100644 docs/api/base.md create mode 100644 docs/api/c_expr.md create mode 100644 docs/api/cstruct.md create mode 100644 docs/api/field.md create mode 100644 docs/api/mem_cstruct.md create mode 100644 docs/api/module.md create mode 100644 docs/examples/fdisk.md create mode 100644 docs/examples/flexible_array.md create mode 100644 docs/examples/who.md create mode 100644 docs/index.md create mode 100644 docs/license.md create mode 100644 mkdocs.yml diff --git a/.gitignore b/.gitignore index fc1cb1f..ee62e3d 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ nosetests.xml /include .mypy_cache pyvenv.cfg +site/ diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..8774cd0 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,7 @@ +# .readthedocs.yaml +version: 2 +mkdocs: + configuration: mkdocs.yml +python: + install: + - requirements: requirements-dev.txt diff --git a/LICENSE b/LICENSE index e7a1f91..d0b2f46 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2013-2017 Andrea Bonomi +Copyright (c) 2013-2022 Andrea Bonomi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/Makefile b/Makefile index 720db9f..02d0e21 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,8 @@ coverage: .PHONY: docs docs: - cd docs; $(MAKE) html + @mkdocs build + @mkdocs gh-deploy lint: flake8 cstruct tests diff --git a/cstruct/__init__.py b/cstruct/__init__.py index f90d4e2..f9179c2 100644 --- a/cstruct/__init__.py +++ b/cstruct/__init__.py @@ -1,6 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -# # Copyright (c) 2013-2019 Andrea Bonomi # # Published under the terms of the MIT license. @@ -40,7 +38,6 @@ TYPEDEFS, C_TYPE_TO_FORMAT, CHAR_ZERO, - EMPTY_BYTES_STRING, ) from .abstract import CStructMeta, AbstractCStruct from .cstruct import CStruct @@ -52,7 +49,6 @@ 'BIG_ENDIAN', 'NATIVE_ORDER', 'CHAR_ZERO', - 'EMPTY_BYTES_STRING', 'CStruct', 'MemCStruct', 'define', @@ -68,8 +64,9 @@ def define(key: str, value: Any) -> None: """ Define a constant that can be used in the C struct - :param key: identifier - :param value: value of the constant + Args: + key: identifier + value: value of the constant """ DEFINES[key] = value @@ -78,7 +75,8 @@ def undef(key: str) -> None: """ Undefine a symbol that was previously defined with define - :param key: identifier + Args: + key: identifier """ del DEFINES[key] @@ -87,7 +85,8 @@ def getdef(key: str) -> Any: """ Return the value for a constant - :param key: identifier + Args: + key: identifier """ return DEFINES[key] @@ -96,8 +95,9 @@ def typedef(type_: str, alias: str) -> None: """ Define an alias name for a data type - :param type_: data type - :param alias: new alias name + Args: + type_: data type + alias: new alias name """ TYPEDEFS[alias] = type_ @@ -106,8 +106,11 @@ def sizeof(type_: str) -> int: """ Return the size of the type. - :param type_: C type, struct or union (e.g. 'short int' or 'struct ZYZ') - :return: size in bytes + Args: + type_: C type, struct or union (e.g. 'short int' or 'struct ZYZ') + + Returns: + size: size in bytes """ while type_ in TYPEDEFS: type_ = TYPEDEFS[type_] @@ -129,18 +132,24 @@ def sizeof(type_: str) -> int: def parse( - __struct__: str, __cls__: Optional[Type[AbstractCStruct]] = None, __name__: Optional[str] = None, **kargs: Dict[str, Any] + __struct__: str, + __cls__: Optional[Type[AbstractCStruct]] = None, + __name__: Optional[str] = None, + **kargs: Dict[str, Any] ) -> Optional[Type[AbstractCStruct]]: """ Return a new class mapping a C struct/union definition. If the string does not contains any definition, return None. - :param __struct__: definition of the struct (or union) in C syntax - :param __cls__: (optional) super class - CStruct(default) or MemCStruct - :param __name__: (optional) name of the new class. If empty, a name based on the __struct__ hash is generated - :param __byte_order__: (optional) byte order, valid values are LITTLE_ENDIAN, BIG_ENDIAN, NATIVE_ORDER - :param __is_union__: (optional) True for union, False for struct (default) - :returns: __cls__ subclass + Args: + __struct__ (str): definition of the struct (or union) in C syntax + __cls__ (type): super class - CStruct(default) or MemCStruct + __name__ (str): name of the new class. If empty, a name based on the __struct__ hash is generated + __byte_order__ (str): byte order, valid values are LITTLE_ENDIAN, BIG_ENDIAN, NATIVE_ORDER + __is_union__ (bool): True for union, False for struct (default) + + Returns: + cls: __cls__ subclass """ if __cls__ is None: __cls__ = CStruct diff --git a/cstruct/abstract.py b/cstruct/abstract.py index f3e6f3e..50bf024 100644 --- a/cstruct/abstract.py +++ b/cstruct/abstract.py @@ -27,10 +27,11 @@ from abc import ABCMeta from collections import OrderedDict from typing import Any, BinaryIO, List, Dict, Optional, Type, Tuple, Union -from .base import STRUCTS import hashlib +from .base import STRUCTS from .c_parser import parse_struct, parse_def, Tokens from .field import calculate_padding, FieldType +from .exceptions import CStructException __all__ = ['CStructMeta', 'AbstractCStruct'] @@ -38,7 +39,6 @@ class CStructMeta(ABCMeta): __size__: int = 0 - # def __new__(cls: Type[type], name: str, bases: tuple, classdict: dict) -> MetaClass: def __new__(metacls: Type[type], name: str, bases: Tuple[str], namespace: Dict[str, Any]) -> Type[Any]: __struct__ = namespace.get('__struct__', None) namespace['__cls__'] = bases[0] if bases else None @@ -58,6 +58,7 @@ def __new__(metacls: Type[type], name: str, bases: Tuple[str], namespace: Dict[s return new_class def __len__(cls) -> int: + "Structure size (in bytes)" return cls.__size__ @property @@ -67,12 +68,22 @@ def size(cls) -> int: class AbstractCStruct(metaclass=CStructMeta): + """ + Abstract C struct to Python class + """ + __size__: int = 0 + " Size in bytes " __fields__: List[str] = [] + " Struct/union fileds " __fields_types__: Dict[str, FieldType] + " Dictionary mapping field names to types " __byte_order__: Optional[str] = None + " Byte order " __alignment__: int = 0 + " Alignament " __is_union__: bool = False + " True if the class is an union, False if it is a struct " def __init__( self, buffer: Optional[Union[bytes, BinaryIO]] = None, flexible_array_length: Optional[int] = None, **kargs: Dict[str, Any] @@ -92,18 +103,30 @@ def __init__( @classmethod def parse( - cls, __struct__: Union[str, Tokens, Dict[str, Any]], __name__: Optional[str] = None, **kargs: Dict[str, Any] + cls, + __struct__: Union[str, Tokens, Dict[str, Any]], + __name__: Optional[str] = None, + __byte_order__: Optional[str] = None, + __is_union__: Optional[bool] = False, + **kargs: Dict[str, Any] ) -> Type["AbstractCStruct"]: """ Return a new class mapping a C struct/union definition. - :param __struct__: definition of the struct (or union) in C syntax - :param __name__: (optional) name of the new class. If empty, a name based on the __struct__ hash is generated - :param __byte_order__: (optional) byte order, valid values are LITTLE_ENDIAN, BIG_ENDIAN, NATIVE_ORDER - :param __is_union__: (optional) True for union, False for struct (default) - :returns: cls subclass + Args: + __struct__: definition of the struct (or union) in C syntax + __name__: name of the new class. If empty, a name based on the __struct__ hash is generated + __byte_order__: byte order, valid values are LITTLE_ENDIAN, BIG_ENDIAN, NATIVE_ORDER + __is_union__: True for union, False for struct + + Returns: + cls: a new class mapping the defintion """ cls_kargs: Dict[str, Any] = dict(kargs) + if __byte_order__ is not None: + cls_kargs['__byte_order__'] = __byte_order__ + if __is_union__ is not None: + cls_kargs['__is_union__'] = __is_union__ cls_kargs['__struct__'] = __struct__ if isinstance(__struct__, (str, Tokens)): del cls_kargs['__struct__'] @@ -123,20 +146,23 @@ def set_flexible_array_length(self, flexible_array_length: Optional[int]) -> Non """ Set flexible array length (i.e. number of elements) - :flexible_array_length: flexible array length + Args: + flexible_array_length: flexible array length """ if flexible_array_length is not None: # Search for the flexible array flexible_array: Optional[FieldType] = [x for x in self.__fields_types__.values() if x.flexible_array][0] if flexible_array is None: - raise ValueError("Flexible array not found in struct") + raise CStructException("Flexible array not found in struct") flexible_array.vlen = flexible_array_length def unpack(self, buffer: Optional[Union[bytes, BinaryIO]], flexible_array_length: Optional[int] = None) -> bool: """ Unpack bytes containing packed C structure data - :param buffer: bytes or binary stream to be unpacked + Args: + buffer: bytes or binary stream to be unpacked + flexible_array_length: flexible array length """ self.set_flexible_array_length(flexible_array_length) if hasattr(buffer, 'read'): @@ -151,8 +177,10 @@ def unpack_from( """ Unpack bytes containing packed C structure data - :param buffer: bytes to be unpacked - :param offset: optional buffer offset + Args: + buffer: bytes to be unpacked + offset: optional buffer offset + flexible_array_length: flexible array length """ raise NotImplementedError diff --git a/cstruct/base.py b/cstruct/base.py index 9f0b261..b350a67 100644 --- a/cstruct/base.py +++ b/cstruct/base.py @@ -1,6 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -# # Copyright (c) 2013-2019 Andrea Bonomi # # Published under the terms of the MIT license. @@ -38,16 +36,15 @@ 'DEFINES', 'TYPEDEFS', 'C_TYPE_TO_FORMAT', - 'EMPTY_BYTES_STRING', 'CHAR_ZERO', ] -# little-endian, std. size & alignment LITTLE_ENDIAN = '<' -# big-endian, std. size & alignment +"Little-endian, std. size & alignment" BIG_ENDIAN = '>' -# native order, size & alignment +"Big-endian, std. size & alignment" NATIVE_ORDER = '@' +"Native order, size & alignment" STRUCTS: Dict[str, Type["AbstractCStruct"]] = {} @@ -94,5 +91,4 @@ 'uint64': 'Q', } -EMPTY_BYTES_STRING = bytes() CHAR_ZERO = bytes('\0', 'ascii') diff --git a/cstruct/c_expr.py b/cstruct/c_expr.py index e12e439..a74123d 100644 --- a/cstruct/c_expr.py +++ b/cstruct/c_expr.py @@ -1,6 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -# # Copyright (c) 2013-2019 Andrea Bonomi # # Published under the terms of the MIT license. @@ -28,6 +26,7 @@ import operator from typing import Any, Callable, Dict, Union, Type, TYPE_CHECKING from .base import DEFINES, STRUCTS +from .exceptions import EvalError if TYPE_CHECKING: from .abstract import AbstractCStruct @@ -36,9 +35,31 @@ def c_eval(expr: str) -> Union[int, float]: - "Evaluate a C arithmetic/logic expression and return the result" - expr = expr.replace("!", " not ").replace("&&", " and ").replace("||", " or ") - return eval_node(ast.parse(expr.strip()).body[0]) + """ + Evaluate a C arithmetic/logic expression and return the result + + Examples: + >>> c_eval('10 + (5 / 3)') + 11 + >>> c_eval('!0') + 1 + >>> c_eval('sizeof(x)') + 128 + + Args: + expr: C expression + + Returns: + result: the expression evaluation result + + Raises: + EvalError: expression evaluation error + """ + try: + expr = expr.replace("!", " not ").replace("&&", " and ").replace("||", " or ") + return eval_node(ast.parse(expr.strip()).body[0]) + except Exception: + raise EvalError def eval_node(node: ast.stmt) -> Union[int, float]: @@ -79,9 +100,9 @@ def eval_div(node) -> Union[int, float]: def eval_call(node) -> Union[int, float]: - if node.func.id == "sizeof": - from . import sizeof + from . import sizeof + if node.func.id == "sizeof": args = [eval_node(x) for x in node.args] return sizeof(*args) raise KeyError(node.func.id) diff --git a/cstruct/c_parser.py b/cstruct/c_parser.py index 02bd9ad..d1a947c 100644 --- a/cstruct/c_parser.py +++ b/cstruct/c_parser.py @@ -1,6 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -# # Copyright (c) 2013-2019 Andrea Bonomi # # Published under the terms of the MIT license. @@ -30,6 +28,7 @@ from .base import DEFINES, TYPEDEFS, STRUCTS from .field import calculate_padding, Kind, FieldType from .c_expr import c_eval +from .exceptions import CStructException, ParserError if TYPE_CHECKING: from .abstract import AbstractCStruct @@ -49,7 +48,7 @@ def __init__(self, text: str) -> None: _, name, value = line.strip().split(maxsplit=2) DEFINES[name] = c_eval(value) except Exception: - raise Exception("Parsing line {}".format(line)) + raise ParserError("Parsing line {}".format(line)) else: lines.append(line) text = " ".join(lines) @@ -74,7 +73,7 @@ def __str__(self) -> str: def parse_type(tokens: Tokens, __cls__: Type['AbstractCStruct'], byte_order: Optional[str], offset: int) -> "FieldType": if len(tokens) < 2: - raise Exception("Parsing error") + raise ParserError("Parsing error") c_type = tokens.pop() # signed/unsigned/struct if c_type in ['signed', 'unsigned', 'struct', 'union'] and len(tokens) > 1: @@ -94,7 +93,7 @@ def parse_type(tokens: Tokens, __cls__: Type['AbstractCStruct'], byte_order: Opt if "[" in next_token: t = next_token.split("[") if len(t) != 2: - raise Exception("Error parsing: " + next_token) + raise ParserError("Error parsing: " + next_token) next_token = t[0].strip() vlen_part = t[1] vlen_expr = [] @@ -132,7 +131,7 @@ def parse_type(tokens: Tokens, __cls__: Type['AbstractCStruct'], byte_order: Opt try: ref = STRUCTS[tail] except KeyError: - raise Exception("Unknow %s \"%s\"" % (c_type, tail)) + raise ParserError("Unknow {} {}".format(c_type, tail)) else: # other types kind = Kind.NATIVE ref = None @@ -154,7 +153,7 @@ def parse_def( return None kind = tokens.pop() if kind not in ['struct', 'union']: - raise Exception("struct or union expected - {}".format(kind)) + raise ParserError("struct or union expected - {}".format(kind)) __is_union__ = kind == 'union' vtype = tokens.pop() if tokens.get() == '{': # Named nested struct @@ -163,7 +162,7 @@ def parse_def( elif vtype == '{': # Unnamed nested struct return parse_struct(tokens, __cls__=__cls__, __is_union__=__is_union__, __byte_order__=__byte_order__) else: - raise Exception("{} definition expected".format(vtype)) + raise ParserError("{} definition expected".format(vtype)) def parse_struct( @@ -189,7 +188,7 @@ def parse_struct( break # flexible array member must be the last member of such a struct if flexible_array: - raise Exception("Flexible array member must be the last member of such a struct") + raise CStructException("Flexible array member must be the last member of such a struct") field_type = parse_type(tokens, __cls__, __byte_order__, offset) vname = tokens.pop() fields_types[vname] = field_type @@ -201,7 +200,7 @@ def parse_struct( offset = field_type.offset + field_type.vsize t = tokens.pop() if t != ';': - raise (Exception("; expected but %s found" % t)) + raise ParserError("; expected but %s found" % t) if __is_union__: # C union # Calculate the sizeof union as size of its largest element diff --git a/cstruct/cstruct.py b/cstruct/cstruct.py index 88ca322..78dd801 100644 --- a/cstruct/cstruct.py +++ b/cstruct/cstruct.py @@ -1,6 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -# # Copyright (c) 2013-2019 Andrea Bonomi # # Published under the terms of the MIT license. @@ -33,25 +31,23 @@ class CStruct(AbstractCStruct): """ Convert C struct definitions into Python classes. - __struct__ = definition of the struct (or union) in C syntax - __byte_order__ = (optional) byte order, valid values are LITTLE_ENDIAN, BIG_ENDIAN, NATIVE_ORDER - __is_union__ = (optional) True for union definitions, False for struct definitions (default) - - The following fields are generated from the C struct definition - __size__ = size of the structure in bytes (flexible array member size is omitted) - __fields__ = list of structure fields - __fields_types__ = dictionary mapping field names to types - Every fields defined in the structure is added to the class - + Attributes: + __struct__ (str): definition of the struct (or union) in C syntax + __byte_order__ (str): byte order, valid values are LITTLE_ENDIAN, BIG_ENDIAN, NATIVE_ORDER + __is_union__ (bool): True for union definitions, False for struct definitions + __size__ (int): size of the structure in bytes (flexible array member size is omitted) + __fields__ (list): list of structure fields + __fields_types__ (dict): dictionary mapping field names to types """ def unpack_from(self, buffer: Optional[bytes], offset: int = 0, flexible_array_length: Optional[int] = None) -> bool: """ Unpack bytes containing packed C structure data - :param buffer: bytes to be unpacked - :param offset: optional buffer offset - :param flexible_array_length: optional flexible array lenght (number of elements) + Args: + buffer: bytes to be unpacked + offset: optional buffer offset + flexible_array_length: optional flexible array lenght (number of elements) """ self.set_flexible_array_length(flexible_array_length) if buffer is None: @@ -63,6 +59,9 @@ def unpack_from(self, buffer: Optional[bytes], offset: int = 0, flexible_array_l def pack(self) -> bytes: """ Pack the structure data into bytes + + Returns: + bytes: The packed structure """ result: List[bytes] = [] for field, field_type in self.__fields_types__.items(): diff --git a/cstruct/exceptions.py b/cstruct/exceptions.py new file mode 100644 index 0000000..65cd668 --- /dev/null +++ b/cstruct/exceptions.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +# Copyright (c) 2013-2019 Andrea Bonomi +# +# Published under the terms of the MIT license. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. +# + +__all__ = [ + "CStructException", + "ParserError", + "EvalError", +] + + +class CStructException(Exception): + pass + + +class ParserError(CStructException): + pass + + +class EvalError(CStructException): + pass diff --git a/cstruct/field.py b/cstruct/field.py index 00b2fb5..2597b83 100644 --- a/cstruct/field.py +++ b/cstruct/field.py @@ -1,6 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -# # Copyright (c) 2013-2019 Andrea Bonomi # # Published under the terms of the MIT license. @@ -29,6 +27,7 @@ from enum import Enum from typing import Optional, Any, List, Type, TYPE_CHECKING from .base import NATIVE_ORDER, C_TYPE_TO_FORMAT +from .exceptions import ParserError if TYPE_CHECKING: from .abstract import AbstractCStruct @@ -49,12 +48,32 @@ def calculate_padding(byte_order: Optional[str], alignment: int, pos: int) -> in class Kind(Enum): + """ + Field type + """ + NATIVE = 0 + "Native type (e.g. int, char)" STRUCT = 1 + "Struct type" UNION = 2 + "Union type" class FieldType(object): + """ + Struct/Union field + + Attributes: + kind (Kind): struct/union/native + c_type (str): field type + ref (AbstractCStruct): struct/union class ref + vlen (int): number of elements + flexible_array (bool): True for flexible arrays + offset (int): relative memory position of the field (relative to the struct) + padding (int): padding + """ + def __init__( self, kind: Kind, @@ -66,14 +85,15 @@ def __init__( offset: int, ) -> None: """ - Struct/Union field - - :param kind: struct/union/native - :param c_type: field type - :param ref: struct/union class ref - :param vlen: number of elements - :param flexible_array: True for flexible arrays - :param offset: relative memory position of the field (relative to the struct) + Initialize a Struct/Union field + + Args: + kind: struct/union/native + c_type: field type + ref: struct/union class ref + vlen: number of elements + flexible_array: True for flexible arrays + offset: relative memory position of the field (relative to the struct) """ self.kind = kind self.c_type = c_type @@ -85,6 +105,16 @@ def __init__( self.padding = 0 def unpack_from(self, buffer: bytes, offset: int = 0) -> Any: + """ + Unpack bytes containing packed C structure data + + Args: + buffer: bytes to be unpacked + offset: optional buffer offset + + Returns: + data: The unpacked data + """ if self.is_native: result = struct.unpack_from(self.fmt, buffer, self.offset + offset) if self.is_array: @@ -105,6 +135,15 @@ def unpack_from(self, buffer: bytes, offset: int = 0) -> Any: return instances def pack(self, data: Any) -> bytes: + """ + Pack the field into bytes + + Args: + data: data to be packed + + Returns: + bytes: The packed structure + """ if self.flexible_array: self.vlen = len(data) # set flexible array size return struct.pack(self.fmt, *data) @@ -120,14 +159,17 @@ def is_array(self) -> bool: @property def is_native(self) -> bool: + "True if the field is a native type (e.g. int, char)" return self.kind == Kind.NATIVE @property def is_struct(self) -> bool: + "True if the field is a struct" return self.kind == Kind.STRUCT @property def is_union(self) -> bool: + "True if the field is an union" return self.kind == Kind.UNION @property @@ -137,7 +179,7 @@ def native_format(self) -> str: try: return C_TYPE_TO_FORMAT[self.c_type] except KeyError: - raise Exception("Unknow type \"" + self.c_type + "\"") + raise ParserError("Unknow type {}".format(self.c_type)) else: return 'c' @@ -170,7 +212,7 @@ def alignment(self) -> int: return self.ref.__alignment__ def align_filed_offset(self) -> None: - "Align file if byte order is native" + "If the byte order is native, align the field" if align(self.byte_order) and self.c_type != 'char': self.padding = calculate_padding(self.byte_order, self.alignment, self.base_offset) self.offset = self.base_offset + self.padding diff --git a/cstruct/mem_cstruct.py b/cstruct/mem_cstruct.py index de7cf4d..61f2c6a 100644 --- a/cstruct/mem_cstruct.py +++ b/cstruct/mem_cstruct.py @@ -1,6 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -# # Copyright (c) 2013-2019 Andrea Bonomi # # Published under the terms of the MIT license. @@ -47,17 +45,14 @@ class MemCStruct(AbstractCStruct): """ Convert C struct definitions into Python classes. - __struct__ = definition of the struct (or union) in C syntax - __byte_order__ = (optional) byte order, valid values are LITTLE_ENDIAN, BIG_ENDIAN, NATIVE_ORDER - __is_union__ = (optional) True for union definitions, False for struct definitions (default) - - The following fields are generated from the C struct definition - __mem_ = mutable character buffer - __size__ = size of the structure in bytes (flexible array member size is omitted) - __fields__ = list of structure fields - __fields_types__ = dictionary mapping field names to types - Every fields defined in the structure is added to the class - + Attributes: + __struct__ (str): definition of the struct (or union) in C syntax + __byte_order__ (str): byte order, valid values are LITTLE_ENDIAN, BIG_ENDIAN, NATIVE_ORDER + __is_union__ (bool): True for union definitions, False for struct definitions + __mem__: mutable character buffer + __size__ (int): size of the structure in bytes (flexible array member size is omitted) + __fields__ (list): list of structure fields + __fields_types__ (dict): dictionary mapping field names to types """ __mem__ = None @@ -66,9 +61,10 @@ def unpack_from(self, buffer: Optional[bytes], offset: int = 0, flexible_array_l """ Unpack bytes containing packed C structure data - :param buffer: bytes to be unpacked - :param offset: optional buffer offset - :param flexible_array_length: optional flexible array lenght (number of elements) + Args: + buffer: bytes to be unpacked + offset: optional buffer offset + flexible_array_length: optional flexible array lenght (number of elements) """ self.set_flexible_array_length(flexible_array_length) self.__base__ = offset # Base offset @@ -88,15 +84,19 @@ def memcpy(self, destination: int, source: bytes, num: int) -> None: """ Copies the values of num bytes from source to the struct memory - :param destination: destination address - :param source: source data to be copied - :param num: number of bytes to copy + Args: + destination: destination address + source: source data to be copied + num: number of bytes to copy """ ctypes.memmove(ctypes.byref(self.__mem__, destination), source, num) def pack(self) -> bytes: """ Pack the structure data into bytes + + Returns: + bytes: The packed structure """ return self.__mem__.raw[:-1] # the buffer is one item larger than its size and the last element is NUL @@ -104,7 +104,8 @@ def set_flexible_array_length(self, flexible_array_length: Optional[int]) -> Non """ Set flexible array length (i.e. number of elements) - :flexible_array_length: flexible array length + Args: + flexible_array_length: flexible array length """ super().set_flexible_array_length(flexible_array_length) if self.__mem__ is not None: diff --git a/docs/api/abstract.md b/docs/api/abstract.md new file mode 100644 index 0000000..1ccb5ce --- /dev/null +++ b/docs/api/abstract.md @@ -0,0 +1 @@ +::: cstruct.abstract diff --git a/docs/api/base.md b/docs/api/base.md new file mode 100644 index 0000000..45f824e --- /dev/null +++ b/docs/api/base.md @@ -0,0 +1 @@ +::: cstruct.base diff --git a/docs/api/c_expr.md b/docs/api/c_expr.md new file mode 100644 index 0000000..0602709 --- /dev/null +++ b/docs/api/c_expr.md @@ -0,0 +1 @@ +::: cstruct.c_expr diff --git a/docs/api/cstruct.md b/docs/api/cstruct.md new file mode 100644 index 0000000..a3274cf --- /dev/null +++ b/docs/api/cstruct.md @@ -0,0 +1 @@ +::: cstruct.cstruct diff --git a/docs/api/field.md b/docs/api/field.md new file mode 100644 index 0000000..29dfe8a --- /dev/null +++ b/docs/api/field.md @@ -0,0 +1 @@ +::: cstruct.field diff --git a/docs/api/mem_cstruct.md b/docs/api/mem_cstruct.md new file mode 100644 index 0000000..d08bd26 --- /dev/null +++ b/docs/api/mem_cstruct.md @@ -0,0 +1 @@ +::: cstruct.mem_cstruct diff --git a/docs/api/module.md b/docs/api/module.md new file mode 100644 index 0000000..8f87977 --- /dev/null +++ b/docs/api/module.md @@ -0,0 +1 @@ +::: cstruct diff --git a/docs/examples/fdisk.md b/docs/examples/fdisk.md new file mode 100644 index 0000000..1680788 --- /dev/null +++ b/docs/examples/fdisk.md @@ -0,0 +1,3 @@ +``` +{!examples/fdisk.py!} +``` diff --git a/docs/examples/flexible_array.md b/docs/examples/flexible_array.md new file mode 100644 index 0000000..ea76b6e --- /dev/null +++ b/docs/examples/flexible_array.md @@ -0,0 +1,3 @@ +``` +{!examples/flexible_array.py!} +``` diff --git a/docs/examples/who.md b/docs/examples/who.md new file mode 100644 index 0000000..b4521ab --- /dev/null +++ b/docs/examples/who.md @@ -0,0 +1,3 @@ +``` +{!examples/who.py!} +``` diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..9850dfb --- /dev/null +++ b/docs/index.md @@ -0,0 +1,12 @@ +# Python-CStruct + +Convert C struct/union definitions into Python classes with methods for +serializing/deserializing. + +The usage is very simple: create a class subclassing cstruct.MemCStruct +and add a C struct/union definition as a string in the `__struct__` field. + +The C struct/union definition is parsed at runtime and the struct format string +is generated. The class offers the method `unpack` for deserializing +an array of bytes into a Python object and the method `pack` for +serializing the values into an array of bytes. diff --git a/docs/license.md b/docs/license.md new file mode 100644 index 0000000..84f33f7 --- /dev/null +++ b/docs/license.md @@ -0,0 +1 @@ +{!LICENSE!} diff --git a/examples/fdisk.py b/examples/fdisk.py index c7b1a75..9aadf46 100644 --- a/examples/fdisk.py +++ b/examples/fdisk.py @@ -1,30 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -# ***************************************************************************** -# -# Copyright (c) 2013 Andrea Bonomi -# -# Published under the terms of the MIT license. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. -# -# ***************************************************************************** from pathlib import Path import argparse diff --git a/examples/flexible_array.py b/examples/flexible_array.py index ddb5047..7a46764 100644 --- a/examples/flexible_array.py +++ b/examples/flexible_array.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- import random from cstruct import MemCStruct diff --git a/examples/who.py b/examples/who.py index 88cd8f3..c18d2b4 100644 --- a/examples/who.py +++ b/examples/who.py @@ -1,51 +1,5 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -# ***************************************************************************** -# -# Copyright (c) 2013 Andrea Bonomi -# -# Published under the terms of the MIT license. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. -# -# ***************************************************************************** - -# pts/1 2013-06-06 18:09 23120 id=ts/1 term=0 exit=0 -# system boot 2013-05-20 21:27 -# run-level 2 2013-05-20 21:27 -# LOGIN tty4 2013-05-20 21:27 1631 id=4 -# LOGIN tty5 2013-05-20 21:27 1645 id=5 -# LOGIN tty2 2013-05-20 21:27 1657 id=2 -# LOGIN tty3 2013-05-20 21:27 1658 id=3 -# LOGIN tty6 2013-05-20 21:27 1661 id=6 -# LOGIN tty1 2013-05-20 21:27 2879 id=1 -# pts/22 2013-06-06 07:43 972 id=s/22 term=0 exit=0 -# andreax + pts/0 2013-08-22 09:04 . 15682 (l26.box) -# pts/34 2013-06-12 15:04 26396 id=s/34 term=0 exit=0 -# pts/21 2013-06-25 11:12 32321 id=s/21 term=0 exit=0 -# pts/24 2013-07-02 22:04 29473 id=/24 term=0 exit=0 -# pts/27 2013-07-03 12:04 8492 id=/27 term=0 exit=0 -# pts/31 2013-07-18 18:49 27215 id=s/31 term=0 exit=0 -# pts/30 2013-07-24 14:40 19054 id=s/30 term=0 exit=0 -# pts/28 2013-07-30 20:49 24942 id=s/28 term=0 exit=0 -# pts/27 2013-08-02 17:59 31326 id=s/27 term=0 exit=0 -# 012345678901234567890123456789012345678901234567890123456789012345678901234567890 + from cstruct import parse, getdef, typedef, MemCStruct, NATIVE_ORDER from pathlib import Path import argparse diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..01fb098 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,34 @@ +site_name: Python CStruct documentation +docs_dir: docs + +theme: + name: readthedocs + highlightjs: true + +plugins: +- search +- autorefs +- mkdocstrings: + watch: + - cstruct + - examples + +nav: + - CStruct Docs: index.md + - Examples: + - "fdisk.py": examples/fdisk.md + - "flexible_array.py": examples/flexible_array.md + - "who.py": examples/who.md + - API: + - "cstruct": api/module.md + - "cstruct.abstract": api/abstract.md + - "cstruct.base": api/base.md + - "cstruct.c_expr": api/c_expr.md + - "cstruct.cstruct": api/cstruct.md + - "cstruct.field": api/field.md + - "cstruct.mem_cstruct": api/mem_cstruct.md + - License: license.md + +markdown_extensions: + - markdown_include.include: + base_path: . diff --git a/requirements-dev.txt b/requirements-dev.txt index abd417e..8bf236b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,8 +1,11 @@ coverage flake8 mypy -sphinx tox black twine<3.4 pytest +mkdocs +mkdocstrings[python] +mkdocs-material +markdown_include diff --git a/tests/test_c_expr.py b/tests/test_c_expr.py index d46c012..8230bd6 100644 --- a/tests/test_c_expr.py +++ b/tests/test_c_expr.py @@ -28,16 +28,20 @@ from cstruct import parse, getdef, define from cstruct.c_expr import c_eval + def test_c_expr_def(): - parse(""" + parse( + """ #define A1 10 /* test */ #define A2 10 + A1 /* comment */ #define A3 30 - """) + """ + ) assert getdef("A1") == 10 assert getdef('A2') == 20 # TODO assert c_eval("A1 / 10") == 1 + def test_c_expr_binary(): assert c_eval("6*2/( 2+1 * 2/3 +6) +8 * (8/4)") == 17 assert c_eval("6*2/(2+2/3 + 6) + 8 * (8/4)") == 17 @@ -48,11 +52,13 @@ def test_c_expr_binary(): assert c_eval("3 & 2") == 2 assert c_eval("3 | 2") == 3 + def test_c_expr_bool(): assert c_eval("3 && 2") == 1 assert c_eval("3 && 2 && 1") == 1 assert c_eval("3 || 2") == 1 + def test_c_expr_unary(): assert c_eval("16 << 2") == 64 assert c_eval("+123") == 123 @@ -62,6 +68,7 @@ def test_c_expr_unary(): assert c_eval("~0") == -1 assert c_eval("~1") == -2 + def test_c_expr_compare(): assert c_eval("1 == 2") == 0 assert c_eval("5 == 5") == 1 From 943d2f815ae2ef2281cb2aa2e92f33cf7101cf8d Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Mon, 5 Sep 2022 14:09:19 +0000 Subject: [PATCH 18/95] version 3.0 --- changelog.txt | 6 +++--- cstruct/__init__.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/changelog.txt b/changelog.txt index 6f0e3e2..760c88f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -128,10 +128,10 @@ - Fix compare with None -### 2.4 +### 3.0 -2022-XX-XX +2022-09-05 ### Added -- flexible array support +- Flexible array support diff --git a/cstruct/__init__.py b/cstruct/__init__.py index f9179c2..8e57f3e 100644 --- a/cstruct/__init__.py +++ b/cstruct/__init__.py @@ -24,7 +24,7 @@ __author__ = 'Andrea Bonomi ' __license__ = 'MIT' -__version__ = '2.3' +__version__ = '3.0' __date__ = '15 August 2013' import struct From 400c88edaabe60ee1b7a269df8395a8f8bc745a3 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Mon, 5 Sep 2022 16:19:14 +0200 Subject: [PATCH 19/95] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 20d12ef..2cac70d 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ C-style structs for Python [![Downloads](https://pepy.tech/badge/cstruct/month)](https://pepy.tech/project/cstruct) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Known Vulnerabilities](https://snyk.io/test/github/andreax79/python-cstruct/badge.svg)](https://snyk.io/test/github/andreax79/python-cstruct) +[![Documentation](https://readthedocs.org/projects/python-cstruct/badge/?version=latest)](https://python-cstruct.readthedocs.io/en/latest/) Convert C struct/union definitions into Python classes with methods for serializing/deserializing. From 2e64ffdc989ff98735672907f5151d5267f5ef6c Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Tue, 13 Sep 2022 06:08:10 +0000 Subject: [PATCH 20/95] Make CStruct/MemCStruct Pickle Friendly --- cstruct/abstract.py | 20 +++++++++++++++++ tests/test_pickle.py | 52 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 tests/test_pickle.py diff --git a/cstruct/abstract.py b/cstruct/abstract.py index 50bf024..45be23f 100644 --- a/cstruct/abstract.py +++ b/cstruct/abstract.py @@ -229,3 +229,23 @@ def __str__(self) -> str: def __repr__(self) -> str: # pragma: no cover return self.__str__() + + def __getstate__(self) -> bytes: + """ + This method is called and the returned object is pickled + as the contents for the instance, instead of the contents of + the instance’s dictionary + + Returns: + bytes: The packed structure + """ + return self.pack() + + def __setstate__(self, state: bytes) -> bool: + """ + This method it is called with the unpickled state + + Args: + state: bytes to be unpacked + """ + return self.unpack(state) diff --git a/tests/test_pickle.py b/tests/test_pickle.py new file mode 100644 index 0000000..14789a0 --- /dev/null +++ b/tests/test_pickle.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python + +import pickle +import cstruct + + +class Position(cstruct.CStruct): + __byte_order__ = cstruct.LITTLE_ENDIAN + __def__ = """ + struct { + unsigned char head; + unsigned char sector; + unsigned char cyl; + } + """ + + +class MemPosition(cstruct.MemCStruct): + __byte_order__ = cstruct.LITTLE_ENDIAN + __def__ = """ + struct { + unsigned char head; + unsigned char sector; + unsigned char cyl; + } + """ + + +def test_pickle(): + # pickle an object + original_pos = Position(head=254, sector=63, cyl=134) + pickled_bytes = pickle.dumps(original_pos) + + # reconstitute a pickled object + reconstituted_pos = pickle.loads(pickled_bytes) + + assert reconstituted_pos.head == original_pos.head + assert reconstituted_pos.sector == original_pos.sector + assert reconstituted_pos.cyl == original_pos.cyl + + +def test_mem_pickle(): + # pickle an object + original_pos = MemPosition(head=254, sector=63, cyl=134) + pickled_bytes = pickle.dumps(original_pos) + + # reconstitute a pickled object + reconstituted_pos = pickle.loads(pickled_bytes) + + assert reconstituted_pos.head == original_pos.head + assert reconstituted_pos.sector == original_pos.sector + assert reconstituted_pos.cyl == original_pos.cyl From 26c15be27a0bccf899100e4ce485d482649d66ef Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Tue, 13 Sep 2022 06:18:59 +0000 Subject: [PATCH 21/95] Add coverage configuration --- Makefile | 2 +- pyproject.toml | 6 ++++++ requirements-dev.txt | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 02d0e21..aa727c8 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ clean: -rm -rf bin lib share pyvenv.cfg coverage: - python3 -m coverage run --source=cstruct setup.py test && python3 -m coverage report -m + python3 -m coverage run setup.py test && python3 -m coverage report -m .PHONY: docs docs: diff --git a/pyproject.toml b/pyproject.toml index c70aaf6..70b86b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,8 @@ [tool.black] line-length = 132 + +[tool.coverage.run] +source = ["cstruct"] + +[tool.coverage.report] +exclude_lines = [ "# pragma: no cover", "if TYPE_CHECKING:" ] diff --git a/requirements-dev.txt b/requirements-dev.txt index 8bf236b..c9eff59 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,4 @@ -coverage +coverage[toml] flake8 mypy tox From 6236ff6d96e4202a2068c1846b87bc7ac7de27b4 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Tue, 13 Sep 2022 06:36:52 +0000 Subject: [PATCH 22/95] Update README.md --- README.md | 76 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 44 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 2cac70d..37b5b98 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ is generated. The class offers the method "unpack" for deserializing an array of bytes into a Python object and the method "pack" for serializing the values into an array of bytes. +[Api Documentation](https://readthedocs.org/projects/python-cstruct/badge/?version=latest) + Example ------- @@ -31,53 +33,63 @@ import cstruct class Position(cstruct.MemCStruct): __byte_order__ = cstruct.LITTLE_ENDIAN - __def__ = """ - struct { - unsigned char head; - unsigned char sector; - unsigned char cyl; - } + __struct__ = """ + unsigned char head; + unsigned char sector; + unsigned char cyl; """ + class Partition(cstruct.MemCStruct): __byte_order__ = cstruct.LITTLE_ENDIAN - __def__ = """ - struct { - unsigned char status; /* 0x80 - active */ - struct Position start; - unsigned char partition_type; - struct Position end; - unsigned int start_sect; /* starting sector counting from 0 */ - unsigned int sectors; /* nr of sectors in partition */ - } + __struct__ = """ + #define ACTIVE_FLAG 0x80 + + unsigned char status; /* 0x80 - active */ + struct Position start; + unsigned char partition_type; + struct Position end; + unsigned int start_sect; /* starting sector counting from 0 */ + unsigned int sectors; /* nr of sectors in partition */ """ def print_info(self): - print("bootable: %s" % ((self.status & 0x80) and "Y" or "N")) - print("partition_type: %02X" % self.partition_type) - print("start: head: %X sectory: %X cyl: %X" % (self.start.head, self.start.sector, self.start.cyl)) - print("end: head: %X sectory: %X cyl: %X" % (self.end.head, self.end.sector, self.end.cyl)) - print("starting sector: %08X" % self.start_sect) - print("size MB: %s" % (self.sectors / 2 / 1024)) + print(f"bootable: {'Y' if self.status & cstruct.getdef('ACTIVE_FLAG') else 'N'}") + print(f"partition_type: {self.partition_type:02X}") + print(f"start: head: {self.start.head:X} sectory: {self.start.sector:X} cyl: {self.start.cyl:X}") + print(f"end: head: {self.end.head:X} sectory: {self.end.sector:X} cyl: {self.end.cyl:X}") + print(f"starting sector: {self.start_sect:08x}") + print(f"size MB: {self.sectors / 2 / 1024}") + class MBR(cstruct.MemCStruct): __byte_order__ = cstruct.LITTLE_ENDIAN - __def__ = """ - struct { - char unused[440]; - unsigned char disk_signature[4]; - unsigned char usualy_nulls[2]; - struct Partition partitions[4]; - char signature[2]; - } + __struct__ = """ + #define MBR_SIZE 512 + #define MBR_DISK_SIGNATURE_SIZE 4 + #define MBR_USUALY_NULLS_SIZE 2 + #define MBR_SIGNATURE_SIZE 2 + #define MBR_BOOT_SIGNATURE 0xaa55 + #define MBR_PARTITIONS_NUM 4 + #define MBR_PARTITIONS_SIZE (sizeof(Partition) * MBR_PARTITIONS_NUM) + #define MBR_UNUSED_SIZE (MBR_SIZE - MBR_DISK_SIGNATURE_SIZE - MBR_USUALY_NULLS_SIZE - MBR_PARTITIONS_SIZE - MBR_SIGNATURE_SIZE) + + char unused[MBR_UNUSED_SIZE]; + unsigned char disk_signature[MBR_DISK_SIGNATURE_SIZE]; + unsigned char usualy_nulls[MBR_USUALY_NULLS_SIZE]; + struct Partition partitions[MBR_PARTITIONS_NUM]; + uint16 signature; """ + @property + def disk_signature_str(self): + return "".join(reversed([f"{x:02x}" for x in self.disk_signature])) + def print_info(self): - print("disk signature: %s" % "".join(["%02X" % x for x in self.disk_signature])) - print("usualy nulls: %s" % "".join(["%02X" % x for x in self.usualy_nulls])) + print(f"disk signature: {self.disk_signature_str}") for i, partition in enumerate(self.partitions): print("") - print("partition: %s" % i) + print(f"partition: {i}") partition.print_info() disk = "mbr" From 492bfc37eb6a4114e39b872262ef448cd770da87 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Tue, 13 Sep 2022 06:42:00 +0000 Subject: [PATCH 23/95] Fix doc strings --- cstruct/abstract.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/cstruct/abstract.py b/cstruct/abstract.py index 45be23f..f5aeca9 100644 --- a/cstruct/abstract.py +++ b/cstruct/abstract.py @@ -148,6 +148,9 @@ def set_flexible_array_length(self, flexible_array_length: Optional[int]) -> Non Args: flexible_array_length: flexible array length + + Raises: + CStructException: If flexible array is not present in the structure """ if flexible_array_length is not None: # Search for the flexible array @@ -185,7 +188,12 @@ def unpack_from( raise NotImplementedError def pack(self) -> bytes: # pragma: no cover - "Pack the structure data into bytes" + """ + Pack the structure data into bytes + + Returns: + bytes: The packed structure + """ raise NotImplementedError def clear(self) -> None: From 9790a914d17a802144969501a68d95497d5a469e Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Tue, 13 Sep 2022 06:42:29 +0000 Subject: [PATCH 24/95] version 3.1 --- README.md | 2 +- changelog.txt | 8 ++++++++ cstruct/__init__.py | 7 ++----- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 37b5b98..44e9095 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ is generated. The class offers the method "unpack" for deserializing an array of bytes into a Python object and the method "pack" for serializing the values into an array of bytes. -[Api Documentation](https://readthedocs.org/projects/python-cstruct/badge/?version=latest) +[Api Documentation](https://python-cstruct.readthedocs.io/en/latest/) Example ------- diff --git a/changelog.txt b/changelog.txt index 760c88f..becb010 100644 --- a/changelog.txt +++ b/changelog.txt @@ -135,3 +135,11 @@ ### Added - Flexible array support + +### 3.1 + +2022-09-13 + +### Added + +- Make CStruct/MemCStruct Pickle Friendly diff --git a/cstruct/__init__.py b/cstruct/__init__.py index 8e57f3e..331f9d1 100644 --- a/cstruct/__init__.py +++ b/cstruct/__init__.py @@ -24,7 +24,7 @@ __author__ = 'Andrea Bonomi ' __license__ = 'MIT' -__version__ = '3.0' +__version__ = '3.1' __date__ = '15 August 2013' import struct @@ -132,10 +132,7 @@ def sizeof(type_: str) -> int: def parse( - __struct__: str, - __cls__: Optional[Type[AbstractCStruct]] = None, - __name__: Optional[str] = None, - **kargs: Dict[str, Any] + __struct__: str, __cls__: Optional[Type[AbstractCStruct]] = None, __name__: Optional[str] = None, **kargs: Dict[str, Any] ) -> Optional[Type[AbstractCStruct]]: """ Return a new class mapping a C struct/union definition. From 165d12de7a2b28457959f8b3b26976c2df55c207 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Tue, 13 Sep 2022 14:04:50 +0000 Subject: [PATCH 25/95] Tests added --- Makefile | 2 +- requirements-dev.txt | 1 + tests/test_define.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index aa727c8..8048837 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ clean: -rm -rf bin lib share pyvenv.cfg coverage: - python3 -m coverage run setup.py test && python3 -m coverage report -m + @pytest --cov --cov-report=term-missing .PHONY: docs docs: diff --git a/requirements-dev.txt b/requirements-dev.txt index c9eff59..ff893de 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,5 @@ coverage[toml] +pytest-cov flake8 mypy tox diff --git a/tests/test_define.py b/tests/test_define.py index a98ec42..f840ab5 100644 --- a/tests/test_define.py +++ b/tests/test_define.py @@ -28,6 +28,7 @@ import pytest import cstruct from cstruct import define, undef, sizeof, typedef +from cstruct.exceptions import ParserError class Position(cstruct.CStruct): @@ -65,3 +66,31 @@ def test_define(): def test_typedef(): typedef('int', 'integer') assert sizeof('integer') == 4 + + +def test_invalid_type(): + with pytest.raises(ParserError): + + class Invalid(cstruct.CStruct): + __def__ = """ + struct { + unsigned xxx yyy; + } + """ + + +def test_invalid_define(): + with pytest.raises(ParserError): + cstruct.parse(""" + #define xxx yyy zzz + """) + + +def test_invalid_struct(): + with pytest.raises(ParserError): + cstruct.parse(""" + struct { + int a; + int; + } + """) From ceb71ced3c2d1099b5851ef77be7251e1a09180e Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Sun, 23 Oct 2022 07:49:48 +0000 Subject: [PATCH 26/95] Fix padding tests on 32bit architectures --- changelog.txt | 8 +++++ cstruct/__init__.py | 2 +- setup.py | 3 -- tests/test_alignment.py | 67 ++++++++++++++++++++++++++++++----------- 4 files changed, 58 insertions(+), 22 deletions(-) diff --git a/changelog.txt b/changelog.txt index becb010..8797a5a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -143,3 +143,11 @@ ### Added - Make CStruct/MemCStruct Pickle Friendly + +### 3.2 + +2022-10-23 + +### Fix + +- Fix padding tests on 32bit architectures diff --git a/cstruct/__init__.py b/cstruct/__init__.py index 331f9d1..52f213f 100644 --- a/cstruct/__init__.py +++ b/cstruct/__init__.py @@ -24,7 +24,7 @@ __author__ = 'Andrea Bonomi ' __license__ = 'MIT' -__version__ = '3.1' +__version__ = '3.2' __date__ = '15 August 2013' import struct diff --git a/setup.py b/setup.py index 23b1348..f8814ff 100644 --- a/setup.py +++ b/setup.py @@ -17,9 +17,6 @@ def readme(): 'Operating System :: OS Independent', 'Programming Language :: Python', 'Topic :: Software Development :: Libraries :: Python Modules', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', diff --git a/tests/test_alignment.py b/tests/test_alignment.py index 252c353..b7cc3e1 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -25,8 +25,11 @@ # # ***************************************************************************** +import sys from cstruct import sizeof, typedef, define, CStruct, NATIVE_ORDER +IS_64BITS = sys.maxsize > 2**32 + define("UT_NAMESIZE", 32) define("UT_LINESIZE", 32) define("UT_HOSTSIZE", 256) @@ -196,21 +199,36 @@ def test_utmp_sizeof(): def test_foo1_sizeof(): - assert Foo1.__fields_types__['p'].padding == 0 - assert Foo1.__fields_types__['c'].padding == 0 - assert Foo1.__fields_types__['x'].padding == 7 - assert sizeof("struct Foo1") == 24 - assert Foo1().size == 24 + if IS_64BITS: + assert Foo1.__fields_types__['p'].padding == 0 + assert Foo1.__fields_types__['c'].padding == 0 + assert Foo1.__fields_types__['x'].padding == 7 + assert sizeof("struct Foo1") == 24 + assert Foo1().size == 24 + else: + assert Foo1.__fields_types__['p'].padding == 0 + assert Foo1.__fields_types__['c'].padding == 0 + assert Foo1.__fields_types__['x'].padding == 3 + assert sizeof("struct Foo1") == 12 + assert Foo1().size == 12 def test_foo2_sizeof(): - assert sizeof("struct Foo2") == 24 - assert Foo2().size == 24 + if IS_64BITS: + assert sizeof("struct Foo2") == 24 + assert Foo2().size == 24 + else: + assert sizeof("struct Foo2") == 16 + assert Foo2().size == 16 def test_foo3_sizeof(): - assert sizeof("struct Foo3") == 16 - assert Foo3().size == 16 + if IS_64BITS: + assert sizeof("struct Foo3") == 16 + assert Foo3().size == 16 + else: + assert sizeof("struct Foo3") == 8 + assert Foo3().size == 8 def test_foo4_sizeof(): @@ -219,15 +237,28 @@ def test_foo4_sizeof(): def test_foo5_sizeof(): - assert Foo5.__fields_types__['c'].padding == 0 - assert Foo5.__fields_types__['inner'].padding == 7 - assert sizeof("struct Foo5") == 24 - assert Foo5().size == 24 + if IS_64BITS: + assert Foo5.__fields_types__['c'].padding == 0 + assert Foo5.__fields_types__['inner'].padding == 7 + assert sizeof("struct Foo5") == 24 + assert Foo5().size == 24 + else: + assert Foo5.__fields_types__['c'].padding == 0 + assert Foo5.__fields_types__['inner'].padding == 3 + assert sizeof("struct Foo5") == 12 + assert Foo5().size == 12 def test_foo10_sizeof(): - assert Foo10.__fields_types__['c'].padding == 0 - assert Foo10.__fields_types__['p'].padding == 7 - assert Foo10.__fields_types__['s'].padding == 0 - assert sizeof("struct Foo10") == 24 - assert Foo10().size == 24 + if IS_64BITS: + assert Foo10.__fields_types__['c'].padding == 0 + assert Foo10.__fields_types__['p'].padding == 7 + assert Foo10.__fields_types__['s'].padding == 0 + assert sizeof("struct Foo10") == 24 + assert Foo10().size == 24 + else: + assert Foo10.__fields_types__['c'].padding == 0 + assert Foo10.__fields_types__['p'].padding == 3 + assert Foo10.__fields_types__['s'].padding == 0 + assert sizeof("struct Foo10") == 12 + assert Foo10().size == 12 From 3af7f3917c6ab6fbdfd8009ddb8a5b0f88104b97 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Mon, 24 Oct 2022 08:15:36 +0000 Subject: [PATCH 27/95] 32bit architecture test environment --- Makefile | 4 ++++ docker/i386/Dockerfile | 14 ++++++++++++++ docker/i386/Makefile | 31 +++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+) create mode 100644 docker/i386/Dockerfile create mode 100644 docker/i386/Makefile diff --git a/Makefile b/Makefile index 8048837..6b9e616 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ help: @echo - make docs ------- Make docs @echo - make lint ------- Run lint @echo - make test ------- Run test + @echo - make test-32bit - Run test on 32bit architecture @echo - make typecheck -- Typecheck @echo - make venv ------- Create virtual environment @@ -32,6 +33,9 @@ lint: test: pytest +test-32bit: + @make -C docker/i386 test + typecheck: mypy --strict --no-warn-unused-ignores cstruct diff --git a/docker/i386/Dockerfile b/docker/i386/Dockerfile new file mode 100644 index 0000000..9037dda --- /dev/null +++ b/docker/i386/Dockerfile @@ -0,0 +1,14 @@ +FROM i386/ubuntu + +RUN apt-get update && \ + apt-get -y install \ + python3.6 \ + python3.6-dev \ + python3.6-distutils \ + curl \ + make && \ + rm -rf /var/lib/apt/lists/* +RUN curl -sSL https://bootstrap.pypa.io/pip/3.6/get-pip.py -o get-pip.py && \ + python3.6 get-pip.py +RUN pip install pytest +WORKDIR /app diff --git a/docker/i386/Makefile b/docker/i386/Makefile new file mode 100644 index 0000000..9431f7e --- /dev/null +++ b/docker/i386/Makefile @@ -0,0 +1,31 @@ +PROJECT=cstruct +BASENAME=test-i386 +IMAGE_NAME=${PROJECT}-${BASENAME} + +.PHONY: help build push all + +help: + @echo "- make build Build docker image" + @echo "- make test Build and run tests" + @echo "- make shell Run interactive shell" + +.DEFAULT_GOAL := help + +build: + @DOCKER_BUILDKIT=1 docker build --tag ${IMAGE_NAME}:latest . + +test: build + @docker run --rm -it \ + --mount type=bind,source=$$PWD/../..,target=/app \ + --hostname=$(BASENAME) \ + ${IMAGE_NAME} \ + pytest + +shell: + @docker run --rm -it \ + --mount type=bind,source=$$PWD/../..,target=/app \ + --hostname=$(BASENAME) \ + ${IMAGE_NAME} \ + bash -i + +all: build From ecc321bf06fb47d967142c2bc2bb18496e53c0a3 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Mon, 24 Oct 2022 08:20:02 +0000 Subject: [PATCH 28/95] Fix size test on 32bit architectures --- changelog.txt | 12 ++++++++++++ cstruct/__init__.py | 2 +- tests/test_define.py | 8 +++++++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index 8797a5a..aaba19e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -151,3 +151,15 @@ ### Fix - Fix padding tests on 32bit architectures + +### 3.3 + +2022-10-24 + +### Added + +- Add 32bit test environment + +### Fix + +- Fix padding tests on 32bit architectures diff --git a/cstruct/__init__.py b/cstruct/__init__.py index 52f213f..d20062e 100644 --- a/cstruct/__init__.py +++ b/cstruct/__init__.py @@ -24,7 +24,7 @@ __author__ = 'Andrea Bonomi ' __license__ = 'MIT' -__version__ = '3.2' +__version__ = '3.3' __date__ = '15 August 2013' import struct diff --git a/tests/test_define.py b/tests/test_define.py index f840ab5..a47e3a8 100644 --- a/tests/test_define.py +++ b/tests/test_define.py @@ -25,11 +25,14 @@ # # ***************************************************************************** +import sys import pytest import cstruct from cstruct import define, undef, sizeof, typedef from cstruct.exceptions import ParserError +IS_64BITS = sys.maxsize > 2**32 + class Position(cstruct.CStruct): __byte_order__ = cstruct.LITTLE_ENDIAN @@ -45,7 +48,10 @@ class Position(cstruct.CStruct): def test_sizeof(): assert sizeof('int') == 4 define('INIT_THREAD_SIZE', 2048 * sizeof('long')) - assert cstruct.DEFINES['INIT_THREAD_SIZE'] == 16384 + if IS_64BITS: + assert cstruct.DEFINES['INIT_THREAD_SIZE'] == 16384 + else: + assert cstruct.DEFINES['INIT_THREAD_SIZE'] == 8192 assert sizeof('struct Position') == 3 assert sizeof('struct Position') == len(Position) assert sizeof(Position) == 3 From 6acd68264cbed8d70896016fda7568dd42be78c2 Mon Sep 17 00:00:00 2001 From: Sophie Tyalie Date: Mon, 31 Oct 2022 17:08:13 +0100 Subject: [PATCH 29/95] Fix: c_parser ignored scopes with nameless inline struct The `parse_type` would continue looking for tokens even when the c_type ended with a `{`. This would mean that the following resulted in an exception: ``` __struct__ = """ struct { int i; } s; """ ``` `parse_type` now ignores any following tokens if `c_type` ends with an `{` - meaning the tokens afterwards are not element of the current type scope. Signed-off-by: Sophie Tyalie --- cstruct/c_parser.py | 72 +++++++++++++++++++++++-------------------- tests/test_cstruct.py | 5 ++- 2 files changed, 42 insertions(+), 35 deletions(-) diff --git a/cstruct/c_parser.py b/cstruct/c_parser.py index d1a947c..476067a 100644 --- a/cstruct/c_parser.py +++ b/cstruct/c_parser.py @@ -78,43 +78,47 @@ def parse_type(tokens: Tokens, __cls__: Type['AbstractCStruct'], byte_order: Opt # signed/unsigned/struct if c_type in ['signed', 'unsigned', 'struct', 'union'] and len(tokens) > 1: c_type = c_type + " " + tokens.pop() - next_token = tokens.pop() - # short int, long int, or long long - if next_token in ['int', 'long']: - c_type = c_type + " " + next_token - next_token = tokens.pop() - # void * - if next_token.startswith("*"): - next_token = next_token[1:] - c_type = 'void *' - # parse length + vlen = 1 flexible_array = False - if "[" in next_token: - t = next_token.split("[") - if len(t) != 2: - raise ParserError("Error parsing: " + next_token) - next_token = t[0].strip() - vlen_part = t[1] - vlen_expr = [] - while not vlen_part.endswith("]"): + + if not c_type.endswith("{"): + next_token = tokens.pop() + # short int, long int, or long long + if next_token in ['int', 'long']: + c_type = c_type + " " + next_token + next_token = tokens.pop() + # void * + if next_token.startswith("*"): + next_token = next_token[1:] + c_type = 'void *' + # parse length + if "[" in next_token: + t = next_token.split("[") + if len(t) != 2: + raise ParserError("Error parsing: " + next_token) + next_token = t[0].strip() + vlen_part = t[1] + vlen_expr = [] + while not vlen_part.endswith("]"): + vlen_expr.append(vlen_part.split("]")[0].strip()) + vlen_part = tokens.pop() + t_vlen = vlen_part.split("]")[0].strip() vlen_expr.append(vlen_part.split("]")[0].strip()) - vlen_part = tokens.pop() - t_vlen = vlen_part.split("]")[0].strip() - vlen_expr.append(vlen_part.split("]")[0].strip()) - t_vlen = " ".join(vlen_expr) - if not t_vlen: - flexible_array = True - vlen = 0 - else: - try: - vlen = c_eval(t_vlen) - except (ValueError, TypeError): - vlen = int(t_vlen) - tokens.push(next_token) - # resolve typedefs - while c_type in TYPEDEFS: - c_type = TYPEDEFS[c_type] + t_vlen = " ".join(vlen_expr) + if not t_vlen: + flexible_array = True + vlen = 0 + else: + try: + vlen = c_eval(t_vlen) + except (ValueError, TypeError): + vlen = int(t_vlen) + tokens.push(next_token) + # resolve typedefs + while c_type in TYPEDEFS: + c_type = TYPEDEFS[c_type] + # calculate fmt if c_type.startswith('struct ') or c_type.startswith('union '): # struct/union c_type, tail = c_type.split(' ', 1) diff --git a/tests/test_cstruct.py b/tests/test_cstruct.py index cd7350b..a3653a1 100644 --- a/tests/test_cstruct.py +++ b/tests/test_cstruct.py @@ -75,10 +75,12 @@ class MBR(cstruct.CStruct): class Dummy(cstruct.CStruct): __byte_order__ = cstruct.LITTLE_ENDIAN __def__ = """ + struct { struct { + int i; + } s; char c; char vc[10]; - int i; int vi[10]; long long l; long vl[10]; @@ -210,6 +212,7 @@ def test_inline(): def test_dummy(): dummy = Dummy() + dummy.c = b'A' dummy.vc = b'ABCDEFGHIJ' dummy.i = 123456 From a4fa6fb0222aff30f2f6bf5f8d883e6256b6a86b Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Mon, 31 Oct 2022 16:49:45 +0000 Subject: [PATCH 30/95] add __repr__ method to FieldType --- cstruct/field.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cstruct/field.py b/cstruct/field.py index 2597b83..287a027 100644 --- a/cstruct/field.py +++ b/cstruct/field.py @@ -220,3 +220,6 @@ def align_filed_offset(self) -> None: def copy(self) -> "FieldType": "Return a shallow copy of this FieldType" return copy.copy(self) + + def __repr__(self) -> str: # pragma: no cover + return repr(self.__dict__) From f337b0d2815696d645f9c90336cf5a66cd5e9a5d Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Tue, 1 Nov 2022 08:00:40 +0000 Subject: [PATCH 31/95] Add check for duplicate members --- cstruct/c_parser.py | 6 ++++-- tests/test_cstruct.py | 10 ++++++++++ tests/test_memcstruct.py | 10 ++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/cstruct/c_parser.py b/cstruct/c_parser.py index 476067a..e4cfbb2 100644 --- a/cstruct/c_parser.py +++ b/cstruct/c_parser.py @@ -78,7 +78,7 @@ def parse_type(tokens: Tokens, __cls__: Type['AbstractCStruct'], byte_order: Opt # signed/unsigned/struct if c_type in ['signed', 'unsigned', 'struct', 'union'] and len(tokens) > 1: c_type = c_type + " " + tokens.pop() - + vlen = 1 flexible_array = False @@ -195,6 +195,8 @@ def parse_struct( raise CStructException("Flexible array member must be the last member of such a struct") field_type = parse_type(tokens, __cls__, __byte_order__, offset) vname = tokens.pop() + if vname in fields_types: + raise ParserError("Duplicate member '{}'".format(vname)) fields_types[vname] = field_type # calculate the max field size (for the alignment) max_alignment = max(max_alignment, field_type.alignment) @@ -204,7 +206,7 @@ def parse_struct( offset = field_type.offset + field_type.vsize t = tokens.pop() if t != ';': - raise ParserError("; expected but %s found" % t) + raise ParserError("; expected but {} found".format(t)) if __is_union__: # C union # Calculate the sizeof union as size of its largest element diff --git a/tests/test_cstruct.py b/tests/test_cstruct.py index a3653a1..b5eab9f 100644 --- a/tests/test_cstruct.py +++ b/tests/test_cstruct.py @@ -25,11 +25,13 @@ # # ***************************************************************************** +import pytest import cstruct from cstruct import sizeof, typedef import io import os from pathlib import Path +from cstruct.exceptions import ParserError MBR_DATA = (Path(__file__).parent.parent / 'mbr').read_bytes() @@ -255,3 +257,11 @@ def test_null_compare(): c = Dummy() assert c is not None assert c != None + + +def test_invalid_inline(): + with pytest.raises(ParserError): + cstruct.MemCStruct.parse( + 'struct { unsigned char head; unsigned char head; }', __byte_order__=cstruct.LITTLE_ENDIAN + ) + assert False diff --git a/tests/test_memcstruct.py b/tests/test_memcstruct.py index 7ea92c3..b312d43 100644 --- a/tests/test_memcstruct.py +++ b/tests/test_memcstruct.py @@ -25,10 +25,12 @@ # # ***************************************************************************** +import pytest import cstruct from cstruct import sizeof, typedef import os from pathlib import Path +from cstruct.exceptions import ParserError MBR_DATA = (Path(__file__).parent.parent / 'mbr').read_bytes() @@ -249,3 +251,11 @@ def test_null_compare(): c = Dummy() assert c is not None assert c != None + + +def test_invalid_inline(): + with pytest.raises(ParserError): + cstruct.MemCStruct.parse( + 'struct { unsigned char head; unsigned char head; }', __byte_order__=cstruct.LITTLE_ENDIAN + ) + assert False From 2ebb795cafcc37bc91e5aba9e511807780e621bb Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Tue, 1 Nov 2022 13:16:02 +0000 Subject: [PATCH 32/95] Add anonymous nested union --- cstruct/c_parser.py | 11 +++ tests/test_nested.py | 212 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 tests/test_nested.py diff --git a/cstruct/c_parser.py b/cstruct/c_parser.py index e4cfbb2..34229bc 100644 --- a/cstruct/c_parser.py +++ b/cstruct/c_parser.py @@ -182,6 +182,7 @@ def parse_struct( flexible_array: bool = False offset: int = 0 max_alignment: int = 0 + anonymous: int = 0 if isinstance(__struct__, Tokens): tokens = __struct__ else: @@ -197,6 +198,16 @@ def parse_struct( vname = tokens.pop() if vname in fields_types: raise ParserError("Duplicate member '{}'".format(vname)) + # anonymous nested union + if vname == ';' and field_type.ref is not None and (__is_union__ or field_type.ref.__is_union__): + # add the anonymous struct fields to the parent + for nested_field_name, nested_field_type in field_type.ref.__fields_types__.items(): + if nested_field_name in fields_types: + raise ParserError("Duplicate member '{}'".format(nested_field_name)) + fields_types[nested_field_name] = nested_field_type + vname = "__anonymous{}".format(anonymous) + anonymous += 1 + tokens.push(';') fields_types[vname] = field_type # calculate the max field size (for the alignment) max_alignment = max(max_alignment, field_type.alignment) diff --git a/tests/test_nested.py b/tests/test_nested.py new file mode 100644 index 0000000..4220ca5 --- /dev/null +++ b/tests/test_nested.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python +# ***************************************************************************** +# +# Copyright (c) 2013-2019 Andrea Bonomi +# +# Published under the terms of the MIT license. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. +# +# ***************************************************************************** + +import pytest +import cstruct +from cstruct import sizeof +from cstruct.exceptions import ParserError + + +INVALID_ANONYMOUS = """ + struct NestedStruct { + struct { + int a; + int b; + }; + int a; + int b; + }; +""" + + +class NestedStruct(cstruct.MemCStruct): + __byte_order__ = cstruct.LITTLE_ENDIAN + __def__ = """ + struct NestedStruct { + struct { + int a; + int b; + } s; + int a; + int b; + }; + """ + + +class NestedUnion(cstruct.MemCStruct): + __byte_order__ = cstruct.LITTLE_ENDIAN + __def__ = """ + union NestedUnion { + struct { + int a; + int b; + } s; + int a; + }; + """ + + +class NestedAnonymousUnion(cstruct.MemCStruct): + __byte_order__ = cstruct.LITTLE_ENDIAN + __def__ = """ + union NestedAnonymousUnion { + struct { + int a; + int b; + }; + int c; + }; + """ + + +class Packet(cstruct.MemCStruct): + __byte_order__ = cstruct.LITTLE_ENDIAN + __def__ = """ + #define MaxPacket 20 + + struct Packet { + uint8_t packetLength; + union { + uint8_t bytes[MaxPacket]; + struct { + uint16_t field1; + uint16_t field2; + uint16_t field3; + } format1; + struct { + double value1; + double value2; + } format2; + }; + }; + """ + + +def test_invalid_anonymous(): + with pytest.raises(ParserError): + cstruct.parse(INVALID_ANONYMOUS) + assert True + + +def test_sizeof_nested_struct(): + assert sizeof('struct NestedStruct') == 16 + o = NestedStruct() + assert len(o) == 16 + + +def test_pack_unpack_nested_struct(): + o = NestedStruct() + o.s.a = 1 + o.s.b = 2 + o.a = 10 + o.b = 20 + b = o.pack() + o1 = NestedStruct() + + o1.unpack(b) + assert o1.s.a == 1 + assert o1.s.b == 2 + assert o1.a == 10 + assert o1.b == 20 + + +def test_sizeof_nested_union(): + assert sizeof('struct NestedUnion') == 8 + o = NestedUnion() + assert len(o) == 8 + + +def test_pack_unpack_nested_union(): + o = NestedUnion() + o.s.a = 1 + o.s.b = 2 + b = o.pack() + + o1 = NestedUnion() + o1.unpack(b) + assert o1.s.a == 1 + assert o1.s.b == 2 + + o = NestedUnion() + o.a = 1979 + b = o.pack() + + o1 = NestedUnion() + o1.unpack(b) + assert o1.a == 1979 + o1.s.b = 0 + assert o1.a == 1979 + o1.s.a = 0 + assert o1.a == 0 + + +def test_sizeof_nested_anonymous_union(): + assert sizeof('struct NestedAnonymousUnion') == 8 + o = NestedAnonymousUnion() + assert len(o) == 8 + + +def test_pack_unpack_nested_anonymous_union(): + o = NestedAnonymousUnion() + o.a = 1 + o.b = 2 + b = o.pack() + + o1 = NestedAnonymousUnion() + o1.unpack(b) + assert o1.a == 1 + assert o1.b == 2 + + o = NestedAnonymousUnion() + o.c = 1979 + b = o.pack() + + o1 = NestedAnonymousUnion() + o1.unpack(b) + assert o1.c == 1979 + o1.b = 0 + assert o1.c == 1979 + o1.a = 0 + assert o1.c == 0 + + +def test_nested_anonymous_union_struct(): + o = Packet() + assert sizeof("struct Packet") == len(o) + + o = Packet() + o.packetLength = 10 + o.format1.field1 = 11 + o.format1.field2 = 12 + o.format1.field3 = 13 + b = o.pack() + + o1 = Packet() + o1.unpack(b) + assert o1.format1.field1 == 11 + assert o1.format1.field2 == 12 + assert o1.format1.field3 == 13 From 221238967a49133463ef11b2cb79928af510421f Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Tue, 1 Nov 2022 13:19:44 +0000 Subject: [PATCH 33/95] version 4.0 --- changelog.txt | 12 ++++++++++++ cstruct/__init__.py | 2 +- setup.py | 1 + 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index aaba19e..65b3659 100644 --- a/changelog.txt +++ b/changelog.txt @@ -163,3 +163,15 @@ ### Fix - Fix padding tests on 32bit architectures + +### 4.0 + +2022-11-01 + +### Added + +- Add support for nameless inline struct + +### Improved + +- Python 3.11 support diff --git a/cstruct/__init__.py b/cstruct/__init__.py index d20062e..e1ad18f 100644 --- a/cstruct/__init__.py +++ b/cstruct/__init__.py @@ -24,7 +24,7 @@ __author__ = 'Andrea Bonomi ' __license__ = 'MIT' -__version__ = '3.3' +__version__ = '4.0' __date__ = '15 August 2013' import struct diff --git a/setup.py b/setup.py index f8814ff..33bf4e5 100644 --- a/setup.py +++ b/setup.py @@ -22,6 +22,7 @@ def readme(): 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', ], keywords='struct', author='Andrea Bonomi', From eec2cd35d1334f10110b10d61da67a990a953435 Mon Sep 17 00:00:00 2001 From: Sophie Tyalie Date: Mon, 31 Oct 2022 19:54:34 +0100 Subject: [PATCH 34/95] Feature: Introduce C enum parsing using the CEnum class one create Python Enums easily using a C style syntax. ``` class Test(CEnum): __enum__ = """ A = 1, B = 2, C """ ``` Signed-off-by: Sophie Tyalie --- cstruct/__init__.py | 1 + cstruct/abstract.py | 76 ++++++++++++++++++++++++++++++++++++++++++++- cstruct/c_parser.py | 55 ++++++++++++++++++++++++++++++-- cstruct/cenum.py | 4 +++ tests/test_cenum.py | 17 ++++++++++ 5 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 cstruct/cenum.py create mode 100644 tests/test_cenum.py diff --git a/cstruct/__init__.py b/cstruct/__init__.py index e1ad18f..d2331ce 100644 --- a/cstruct/__init__.py +++ b/cstruct/__init__.py @@ -43,6 +43,7 @@ from .cstruct import CStruct from .c_parser import parse_def from .mem_cstruct import MemCStruct +from .cenum import CEnum __all__ = [ 'LITTLE_ENDIAN', diff --git a/cstruct/abstract.py b/cstruct/abstract.py index f5aeca9..0843bd3 100644 --- a/cstruct/abstract.py +++ b/cstruct/abstract.py @@ -28,8 +28,10 @@ from collections import OrderedDict from typing import Any, BinaryIO, List, Dict, Optional, Type, Tuple, Union import hashlib +from enum import IntEnum, EnumMeta, _EnumDict +from unicodedata import name from .base import STRUCTS -from .c_parser import parse_struct, parse_def, Tokens +from .c_parser import parse_struct, parse_def, parse_enum, Tokens from .field import calculate_padding, FieldType from .exceptions import CStructException @@ -257,3 +259,75 @@ def __setstate__(self, state: bytes) -> bool: state: bytes to be unpacked """ return self.unpack(state) + + +class CEnumMeta(EnumMeta): + __size__: int = 0 + + class WrapperDict(_EnumDict): + def __setitem__(self, key: str, value: Any) -> None: + if key == "__enum__": + env = parse_enum(value, self["__qualname__"]) + for k, v in env["__constants__"].items(): + super().__setitem__(k, v) + else: + return super().__setitem__(key, value) + + @classmethod + def __prepare__(metacls, cls, bases, **kwds): + namespace = EnumMeta.__prepare__(cls, bases, **kwds) + namespace.__class__ = metacls.WrapperDict + return namespace + + def __len__(cls) -> int: + "Enum size (in bytes)" + return cls.__size__ + + @property + def size(cls) -> int: + "Enum size (in bytes)" + return cls.__size__ + +class AbstractCEnum(IntEnum, metaclass=CEnumMeta): + """ + Abstract C enum to Python class + """ + + @classmethod + def parse( + cls, + __enum__: Union[str, Tokens, Dict[str, Any]], + __name__: Optional[str] = None, + __size__: Optional[int] = None, + **kargs: Dict[str, Any] + ) -> Type["AbstractCEnum"]: + """ + Return a new Python Enum class mapping a C enum definition + + Args: + __enum__: Definition of the enum in C syntax + __name__: Name of the new Enum. If empty, a name based on the __enum__ hash is generated + + Returns: + cls: A new class mapping the definition + """ + + cls_kargs: Dict[str, Any] = dict(kargs) + if __size__ is not None: + cls_kargs['__size__'] = __size__ + + cls_kargs['__enum__'] = __enum__ + if isinstance(__enum__, (str, Tokens)): + del cls_kargs["__enum__"] + cls_kargs.update(parse_def(__enum__, __cls__=cls, **cls_kargs)) + cls_kargs["__enum__"] = None + elif isinstance(__enum__, dict): + del cls_kargs["__enum__"] + cls_kargs.update(__enum__) + cls_kargs["__enum__"] = None + if __name__ is None: + __name__ = cls.__name__ + "_" + hashlib.sha1(str(__enum__).encode("utf-8")).hexdigest() + cls_kargs["__anonymous__"] = True + cls_kargs["__name__"] = __name__ + return IntEnum(__name__, cls_kargs["__constants__"]) + return type(__name__, (cls,), cls_kargs) \ No newline at end of file diff --git a/cstruct/c_parser.py b/cstruct/c_parser.py index 34229bc..cfc6521 100644 --- a/cstruct/c_parser.py +++ b/cstruct/c_parser.py @@ -31,7 +31,7 @@ from .exceptions import CStructException, ParserError if TYPE_CHECKING: - from .abstract import AbstractCStruct + from .abstract import AbstractCStruct, AbstractCEnum __all__ = ['parse_struct', 'parse_def', 'Tokens'] @@ -52,7 +52,7 @@ def __init__(self, text: str) -> None: else: lines.append(line) text = " ".join(lines) - text = text.replace(";", " ; ").replace("{", " { ").replace("}", " } ") + text = text.replace(";", " ; ").replace("{", " { ").replace("}", " } ").replace(",", " , ").replace("=", " = ") self.tokens = text.split() def pop(self) -> str: @@ -168,6 +168,57 @@ def parse_def( else: raise ParserError("{} definition expected".format(vtype)) +def parse_enum( + __enum__: Union[str, Tokens], + __cls__: Type['AbstractCEnum'], + **kargs: Any +) -> Optional[Dict[str, Any]]: + constants: Dict[str, int] = OrderedDict() + + if isinstance(__enum__, Tokens): + tokens = __enum__ + else: + tokens = Tokens(__enum__) + + while len(tokens): + name = tokens.pop() + + next_token = tokens.pop() + if next_token == ",": # enum-constant without explicit value + if len(constants) == 0: + value = 0 + else: + value = next(reversed(constants.values())) + 1 + elif next_token == "=": + exp_elems = [] + next_token = tokens.pop() + while not next_token.endswith(","): + exp_elems.append(next_token) + if len(tokens) > 0: + next_token = tokens.pop() + else: + break + + if len(exp_elems) == 0: + raise ParserError(f"enum is missing value expression") + + int_expr = " ".join(exp_elems) + try: + value = c_eval(int_expr) + except (ValueError, TypeError): + value = int(int_expr) + else: + raise ParserError(f"{__enum__} is not a valid enum expression") + + if name in constants: + raise ParserError(f"duplicate enum name {name}") + constants[name] = value + + result = { + '__constants__': constants + } + return result + def parse_struct( __struct__: Union[str, Tokens], diff --git a/cstruct/cenum.py b/cstruct/cenum.py new file mode 100644 index 0000000..0fc1654 --- /dev/null +++ b/cstruct/cenum.py @@ -0,0 +1,4 @@ +from .abstract import AbstractCEnum + +class CEnum(AbstractCEnum): + ... \ No newline at end of file diff --git a/tests/test_cenum.py b/tests/test_cenum.py new file mode 100644 index 0000000..f137f8f --- /dev/null +++ b/tests/test_cenum.py @@ -0,0 +1,17 @@ +from cstruct import CEnum + +class Dummy(CEnum): + __enum__ = """ + A, + B, + C = 2, + D = 5 + 7, + E = 2 + """ + +def test_dummy(): + assert Dummy.A == 0 + assert Dummy.B == 1 + assert Dummy.C == 2 + assert Dummy.D == 12 + assert Dummy.E == 2 \ No newline at end of file From c53ac8186d3a6000d4844e12c0f0e025d9f524e9 Mon Sep 17 00:00:00 2001 From: Sophie Tyalie Date: Mon, 31 Oct 2022 20:13:44 +0100 Subject: [PATCH 35/95] Feature: Allow CEnum to be initialized using __def__ - renamed `parse_def` to `parse_struct_def` to allow for `parse_enum_def` function Signed-off-by: Sophie Tyalie --- cstruct/__init__.py | 4 ++-- cstruct/abstract.py | 16 ++++++++++------ cstruct/c_parser.py | 40 +++++++++++++++++++++++++++++++++++++--- 3 files changed, 49 insertions(+), 11 deletions(-) diff --git a/cstruct/__init__.py b/cstruct/__init__.py index d2331ce..c7ccb12 100644 --- a/cstruct/__init__.py +++ b/cstruct/__init__.py @@ -41,7 +41,7 @@ ) from .abstract import CStructMeta, AbstractCStruct from .cstruct import CStruct -from .c_parser import parse_def +from .c_parser import parse_struct_def from .mem_cstruct import MemCStruct from .cenum import CEnum @@ -151,7 +151,7 @@ def parse( """ if __cls__ is None: __cls__ = CStruct - cls_def = parse_def(__struct__, __cls__=__cls__, **kargs) + cls_def = parse_struct_def(__struct__, __cls__=__cls__, **kargs) if cls_def is None: return None else: diff --git a/cstruct/abstract.py b/cstruct/abstract.py index 0843bd3..b997f9c 100644 --- a/cstruct/abstract.py +++ b/cstruct/abstract.py @@ -31,7 +31,7 @@ from enum import IntEnum, EnumMeta, _EnumDict from unicodedata import name from .base import STRUCTS -from .c_parser import parse_struct, parse_def, parse_enum, Tokens +from .c_parser import parse_struct, parse_struct_def, parse_enum_def,parse_enum, Tokens from .field import calculate_padding, FieldType from .exceptions import CStructException @@ -50,7 +50,7 @@ def __new__(metacls: Type[type], name: str, bases: Tuple[str], namespace: Dict[s namespace.update(parse_struct(**namespace)) __struct__ = True if '__def__' in namespace: - namespace.update(parse_def(**namespace)) + namespace.update(parse_struct_def(**namespace)) __struct__ = True # Create the new class new_class: Type[Any] = super().__new__(metacls, name, bases, namespace) @@ -132,7 +132,7 @@ def parse( cls_kargs['__struct__'] = __struct__ if isinstance(__struct__, (str, Tokens)): del cls_kargs['__struct__'] - cls_kargs.update(parse_def(__struct__, __cls__=cls, **cls_kargs)) + cls_kargs.update(parse_struct_def(__struct__, __cls__=cls, **cls_kargs)) cls_kargs['__struct__'] = None elif isinstance(__struct__, dict): del cls_kargs['__struct__'] @@ -266,8 +266,13 @@ class CEnumMeta(EnumMeta): class WrapperDict(_EnumDict): def __setitem__(self, key: str, value: Any) -> None: + env = None if key == "__enum__": env = parse_enum(value, self["__qualname__"]) + elif key == "__def__": + env = parse_enum_def(value, self["__qualname__"]) + + if env is not None: for k, v in env["__constants__"].items(): super().__setitem__(k, v) else: @@ -319,7 +324,7 @@ def parse( cls_kargs['__enum__'] = __enum__ if isinstance(__enum__, (str, Tokens)): del cls_kargs["__enum__"] - cls_kargs.update(parse_def(__enum__, __cls__=cls, **cls_kargs)) + cls_kargs.update(parse_enum_def(__enum__, __cls__=cls, **cls_kargs)) cls_kargs["__enum__"] = None elif isinstance(__enum__, dict): del cls_kargs["__enum__"] @@ -329,5 +334,4 @@ def parse( __name__ = cls.__name__ + "_" + hashlib.sha1(str(__enum__).encode("utf-8")).hexdigest() cls_kargs["__anonymous__"] = True cls_kargs["__name__"] = __name__ - return IntEnum(__name__, cls_kargs["__constants__"]) - return type(__name__, (cls,), cls_kargs) \ No newline at end of file + return IntEnum(__name__, cls_kargs["__constants__"]) \ No newline at end of file diff --git a/cstruct/c_parser.py b/cstruct/c_parser.py index cfc6521..dfb74d1 100644 --- a/cstruct/c_parser.py +++ b/cstruct/c_parser.py @@ -33,7 +33,7 @@ if TYPE_CHECKING: from .abstract import AbstractCStruct, AbstractCEnum -__all__ = ['parse_struct', 'parse_def', 'Tokens'] +__all__ = ['parse_struct', 'parse_struct_def', 'Tokens'] class Tokens(object): @@ -142,7 +142,7 @@ def parse_type(tokens: Tokens, __cls__: Type['AbstractCStruct'], byte_order: Opt return FieldType(kind, c_type, ref, vlen, flexible_array, byte_order, offset) -def parse_def( +def parse_struct_def( __def__: Union[str, Tokens], __cls__: Type['AbstractCStruct'], __byte_order__: Optional[str] = None, @@ -168,6 +168,33 @@ def parse_def( else: raise ParserError("{} definition expected".format(vtype)) + +def parse_enum_def( + __def__: Union[str, Tokens], + __cls__: Type["AbstractCEnum"], + **kargs: Any +) -> Optional[Dict[str, Any]]: + # naive C enum parsing + if isinstance(__def__, Tokens): + tokens = __def__ + else: + tokens = Tokens(__def__) + if not tokens: + return None + kind = tokens.pop() + if kind not in ['enum']: + raise ParserError(f"enum expected - {kind}") + + vtype = tokens.pop() + if tokens.get() == '{': # named enum + tokens.pop() + return parse_enum(tokens, __cls__=__cls__) + elif vtype == '{': + return parse_enum(tokens, __cls__=__cls__) + else: + raise ParserError(f"{vtype} definition expected") + + def parse_enum( __enum__: Union[str, Tokens], __cls__: Type['AbstractCEnum'], @@ -181,6 +208,10 @@ def parse_enum( tokens = Tokens(__enum__) while len(tokens): + if tokens.get() == '}': + tokens.pop() + break + name = tokens.pop() next_token = tokens.pop() @@ -192,7 +223,7 @@ def parse_enum( elif next_token == "=": exp_elems = [] next_token = tokens.pop() - while not next_token.endswith(","): + while next_token not in {",", "}"}: exp_elems.append(next_token) if len(tokens) > 0: next_token = tokens.pop() @@ -207,6 +238,9 @@ def parse_enum( value = c_eval(int_expr) except (ValueError, TypeError): value = int(int_expr) + + if next_token == "}": + break else: raise ParserError(f"{__enum__} is not a valid enum expression") From 2aa934e13ab08458a2e94a0e1a6fb391e7d035f1 Mon Sep 17 00:00:00 2001 From: Sophie Tyalie Date: Tue, 1 Nov 2022 11:18:31 +0100 Subject: [PATCH 36/95] enum: Register enums in base.ENUMS Using Pythons metaclass new function, we look for valid C Enum classes (__enum__ = True) and register them there in the `base.ENUMS` variable - `__size__` is a required parameter for Enums - introduced `CEnumException` Signed-off-by: Sophie Tyalie --- cstruct/abstract.py | 21 +++++++++++++++++---- cstruct/base.py | 4 +++- cstruct/exceptions.py | 3 +++ 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/cstruct/abstract.py b/cstruct/abstract.py index b997f9c..bf88060 100644 --- a/cstruct/abstract.py +++ b/cstruct/abstract.py @@ -28,12 +28,13 @@ from collections import OrderedDict from typing import Any, BinaryIO, List, Dict, Optional, Type, Tuple, Union import hashlib +import sys from enum import IntEnum, EnumMeta, _EnumDict from unicodedata import name -from .base import STRUCTS +from .base import STRUCTS, ENUMS from .c_parser import parse_struct, parse_struct_def, parse_enum_def,parse_enum, Tokens from .field import calculate_padding, FieldType -from .exceptions import CStructException +from .exceptions import CStructException, CEnumException __all__ = ['CStructMeta', 'AbstractCStruct'] @@ -262,8 +263,6 @@ def __setstate__(self, state: bytes) -> bool: class CEnumMeta(EnumMeta): - __size__: int = 0 - class WrapperDict(_EnumDict): def __setitem__(self, key: str, value: Any) -> None: env = None @@ -273,8 +272,13 @@ def __setitem__(self, key: str, value: Any) -> None: env = parse_enum_def(value, self["__qualname__"]) if env is not None: + # register the enum constants in the object namespace, + # using the Python Enum class Namespace dict that does the + # heavy lifting for k, v in env["__constants__"].items(): super().__setitem__(k, v) + + super().__setitem__("__enum__", True) else: return super().__setitem__(key, value) @@ -284,6 +288,15 @@ def __prepare__(metacls, cls, bases, **kwds): namespace.__class__ = metacls.WrapperDict return namespace + def __new__(metacls: type["CEnumMeta"], cls: str, bases: tuple[type, ...], classdict: _EnumDict, **kwds: Any) -> "CEnumMeta": + inst = super().__new__(metacls, cls, bases, classdict, **kwds) + + if classdict.get("__enum__", False): + if "__size__" not in classdict: + raise CEnumException("__size__ not specified. Cannot derive size as it is architecture dependent") + ENUMS[cls] = inst + return inst + def __len__(cls) -> int: "Enum size (in bytes)" return cls.__size__ diff --git a/cstruct/base.py b/cstruct/base.py index b350a67..f9ca0f1 100644 --- a/cstruct/base.py +++ b/cstruct/base.py @@ -25,7 +25,7 @@ from typing import Any, Dict, Type, TYPE_CHECKING if TYPE_CHECKING: - from .abstract import AbstractCStruct + from .abstract import AbstractCStruct, AbstractCEnum __all__ = [ 'LITTLE_ENDIAN', @@ -48,6 +48,8 @@ STRUCTS: Dict[str, Type["AbstractCStruct"]] = {} +ENUMS: Dict[str, Type["AbstractCEnum"]] = {} + DEFINES: Dict[str, Any] = {} TYPEDEFS: Dict[str, str] = { diff --git a/cstruct/exceptions.py b/cstruct/exceptions.py index 65cd668..3d66fb3 100644 --- a/cstruct/exceptions.py +++ b/cstruct/exceptions.py @@ -23,11 +23,14 @@ # __all__ = [ + "CEnumException", "CStructException", "ParserError", "EvalError", ] +class CEnumException(Exception): + pass class CStructException(Exception): pass From 4a584122fb4435deb4bc51dfc4e9fb6a41139fc0 Mon Sep 17 00:00:00 2001 From: Sophie Tyalie Date: Tue, 1 Nov 2022 11:24:15 +0100 Subject: [PATCH 37/95] Removed __cls__ parameter in enum parsing It wasn't used so far and I'm not sure if that will change. Enum parsing is far more simple than struct parsing in this regard Signed-off-by: Sophie Tyalie --- cstruct/abstract.py | 4 ++-- cstruct/c_parser.py | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/cstruct/abstract.py b/cstruct/abstract.py index bf88060..8c17523 100644 --- a/cstruct/abstract.py +++ b/cstruct/abstract.py @@ -267,9 +267,9 @@ class WrapperDict(_EnumDict): def __setitem__(self, key: str, value: Any) -> None: env = None if key == "__enum__": - env = parse_enum(value, self["__qualname__"]) + env = parse_enum(value) elif key == "__def__": - env = parse_enum_def(value, self["__qualname__"]) + env = parse_enum_def(value) if env is not None: # register the enum constants in the object namespace, diff --git a/cstruct/c_parser.py b/cstruct/c_parser.py index dfb74d1..bd63859 100644 --- a/cstruct/c_parser.py +++ b/cstruct/c_parser.py @@ -171,7 +171,6 @@ def parse_struct_def( def parse_enum_def( __def__: Union[str, Tokens], - __cls__: Type["AbstractCEnum"], **kargs: Any ) -> Optional[Dict[str, Any]]: # naive C enum parsing @@ -188,16 +187,15 @@ def parse_enum_def( vtype = tokens.pop() if tokens.get() == '{': # named enum tokens.pop() - return parse_enum(tokens, __cls__=__cls__) + return parse_enum(tokens) elif vtype == '{': - return parse_enum(tokens, __cls__=__cls__) + return parse_enum(tokens) else: raise ParserError(f"{vtype} definition expected") def parse_enum( __enum__: Union[str, Tokens], - __cls__: Type['AbstractCEnum'], **kargs: Any ) -> Optional[Dict[str, Any]]: constants: Dict[str, int] = OrderedDict() From 2c050517bf7d17cdac3a8d85115c74eae2257b92 Mon Sep 17 00:00:00 2001 From: Sophie Tyalie Date: Tue, 1 Nov 2022 11:46:17 +0100 Subject: [PATCH 38/95] Enum: Allow parsing of enums in struct definitions Previously defined named CEnums are correctly parsed when used in a struct definition now. It is unclear whether unnamed and `AbstractCEnum.parse` enums can also be used. Presumably not. Signed-off-by: Sophie Tyalie --- cstruct/base.py | 7 +++++++ cstruct/c_parser.py | 11 +++++++++-- cstruct/field.py | 24 ++++++++++++++++++++---- 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/cstruct/base.py b/cstruct/base.py index f9ca0f1..ab21666 100644 --- a/cstruct/base.py +++ b/cstruct/base.py @@ -93,4 +93,11 @@ 'uint64': 'Q', } +ENUM_SIZE_TO_C_TYPE: Dict[int, str] = { + 1: 'uint8', + 2: 'uint16', + 4: 'uint32', + 8: 'uint64' +} + CHAR_ZERO = bytes('\0', 'ascii') diff --git a/cstruct/c_parser.py b/cstruct/c_parser.py index bd63859..bd28e3c 100644 --- a/cstruct/c_parser.py +++ b/cstruct/c_parser.py @@ -25,7 +25,7 @@ import re from collections import OrderedDict from typing import Union, Optional, Any, Dict, Type, TYPE_CHECKING -from .base import DEFINES, TYPEDEFS, STRUCTS +from .base import DEFINES, ENUMS, TYPEDEFS, STRUCTS from .field import calculate_padding, Kind, FieldType from .c_expr import c_eval from .exceptions import CStructException, ParserError @@ -76,7 +76,7 @@ def parse_type(tokens: Tokens, __cls__: Type['AbstractCStruct'], byte_order: Opt raise ParserError("Parsing error") c_type = tokens.pop() # signed/unsigned/struct - if c_type in ['signed', 'unsigned', 'struct', 'union'] and len(tokens) > 1: + if c_type in ['signed', 'unsigned', 'struct', 'union', 'enum'] and len(tokens) > 1: c_type = c_type + " " + tokens.pop() vlen = 1 @@ -136,6 +136,13 @@ def parse_type(tokens: Tokens, __cls__: Type['AbstractCStruct'], byte_order: Opt ref = STRUCTS[tail] except KeyError: raise ParserError("Unknow {} {}".format(c_type, tail)) + elif c_type.startswith('enum'): + c_type, tail = c_type.split(' ', 1) + kind = Kind.ENUM + try: + ref = ENUMS[tail] + except KeyError: + raise ParserError(f"Unknown '{c_type}' '{tail}'") else: # other types kind = Kind.NATIVE ref = None diff --git a/cstruct/field.py b/cstruct/field.py index 287a027..7e0e3e8 100644 --- a/cstruct/field.py +++ b/cstruct/field.py @@ -26,7 +26,7 @@ import struct from enum import Enum from typing import Optional, Any, List, Type, TYPE_CHECKING -from .base import NATIVE_ORDER, C_TYPE_TO_FORMAT +from .base import NATIVE_ORDER, C_TYPE_TO_FORMAT, ENUM_SIZE_TO_C_TYPE from .exceptions import ParserError if TYPE_CHECKING: @@ -58,6 +58,8 @@ class Kind(Enum): "Struct type" UNION = 2 "Union type" + ENUM = 3 + "Enum type" class FieldType(object): @@ -115,8 +117,12 @@ def unpack_from(self, buffer: bytes, offset: int = 0) -> Any: Returns: data: The unpacked data """ - if self.is_native: + if self.is_native or self.is_enum: result = struct.unpack_from(self.fmt, buffer, self.offset + offset) + + if self.is_enum: + result = tuple(map(self.ref, result)) + if self.is_array: return list(result) else: @@ -162,6 +168,11 @@ def is_native(self) -> bool: "True if the field is a native type (e.g. int, char)" return self.kind == Kind.NATIVE + @property + def is_enum(self) -> bool: + "True if the field is an enum" + return self.kind == Kind.ENUM + @property def is_struct(self) -> bool: "True if the field is a struct" @@ -180,13 +191,18 @@ def native_format(self) -> str: return C_TYPE_TO_FORMAT[self.c_type] except KeyError: raise ParserError("Unknow type {}".format(self.c_type)) + elif self.is_enum: + try: + return C_TYPE_TO_FORMAT[ENUM_SIZE_TO_C_TYPE[len(self.ref)]] + except KeyError: + raise ParserError(f"Enum has invalid size. Needs to be in {ENUM_SIZE_TO_C_TYPE.keys()}") else: return 'c' @property def fmt(self) -> str: "Field format prefixed by byte order (struct library format)" - if self.is_native: + if self.is_native or self.is_enum: fmt = (str(self.vlen) if self.vlen > 1 or self.flexible_array else '') + self.native_format else: # Struct/Union fmt = str(self.vlen * self.ref.sizeof()) + self.native_format @@ -203,7 +219,7 @@ def vsize(self) -> int: @property def alignment(self) -> int: "Alignment" - if self.is_native: + if self.is_native or self.is_enum: if self.byte_order is not None: return struct.calcsize(self.byte_order + self.native_format) else: From 64b0523495707939337d09ccd5c91893a8aff9ef Mon Sep 17 00:00:00 2001 From: Sophie Tyalie Date: Tue, 1 Nov 2022 11:57:48 +0100 Subject: [PATCH 39/95] Enum: Fix parser Parser couldn't handle the case that an enum-constant without explicit value and no following `,` was the last thing in the enum definition. Example error case ``` enum { A } ``` Signed-off-by: Sophie Tyalie --- cstruct/c_parser.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cstruct/c_parser.py b/cstruct/c_parser.py index bd28e3c..7001b32 100644 --- a/cstruct/c_parser.py +++ b/cstruct/c_parser.py @@ -220,7 +220,8 @@ def parse_enum( name = tokens.pop() next_token = tokens.pop() - if next_token == ",": # enum-constant without explicit value + print(name, next_token) + if next_token in {",", "}"}: # enum-constant without explicit value if len(constants) == 0: value = 0 else: From b3bc43c5cb1d6ac638d25d23b2963f6b29814bbd Mon Sep 17 00:00:00 2001 From: Sophie Tyalie Date: Tue, 1 Nov 2022 12:23:14 +0100 Subject: [PATCH 40/95] Enum: Fix `AbstractCEnum.parse` This function was not in a working state previously. While making the function work a few things needed to be changed too. For example should `len(enum)` return the number of elements instead of the size for CEnums and such. Introduced default enum size of 4 bytes Signed-off-by: Sophie Tyalie --- cstruct/abstract.py | 29 ++++++++++------------------- cstruct/base.py | 1 + cstruct/field.py | 2 +- 3 files changed, 12 insertions(+), 20 deletions(-) diff --git a/cstruct/abstract.py b/cstruct/abstract.py index 8c17523..616acb5 100644 --- a/cstruct/abstract.py +++ b/cstruct/abstract.py @@ -31,12 +31,12 @@ import sys from enum import IntEnum, EnumMeta, _EnumDict from unicodedata import name -from .base import STRUCTS, ENUMS +from .base import STRUCTS, ENUMS, DEFAULT_ENUM_SIZE from .c_parser import parse_struct, parse_struct_def, parse_enum_def,parse_enum, Tokens from .field import calculate_padding, FieldType from .exceptions import CStructException, CEnumException -__all__ = ['CStructMeta', 'AbstractCStruct'] +__all__ = ['CStructMeta', 'AbstractCStruct', "CEnumMeta", 'AbstractCEnum'] class CStructMeta(ABCMeta): @@ -277,8 +277,6 @@ def __setitem__(self, key: str, value: Any) -> None: # heavy lifting for k, v in env["__constants__"].items(): super().__setitem__(k, v) - - super().__setitem__("__enum__", True) else: return super().__setitem__(key, value) @@ -291,16 +289,13 @@ def __prepare__(metacls, cls, bases, **kwds): def __new__(metacls: type["CEnumMeta"], cls: str, bases: tuple[type, ...], classdict: _EnumDict, **kwds: Any) -> "CEnumMeta": inst = super().__new__(metacls, cls, bases, classdict, **kwds) - if classdict.get("__enum__", False): + if len(inst) > 0: if "__size__" not in classdict: raise CEnumException("__size__ not specified. Cannot derive size as it is architecture dependent") - ENUMS[cls] = inst + if not classdict.get("__anonymous__", False): + ENUMS[cls] = inst return inst - def __len__(cls) -> int: - "Enum size (in bytes)" - return cls.__size__ - @property def size(cls) -> int: "Enum size (in bytes)" @@ -331,20 +326,16 @@ def parse( """ cls_kargs: Dict[str, Any] = dict(kargs) - if __size__ is not None: - cls_kargs['__size__'] = __size__ + cls_kargs['__size__'] = DEFAULT_ENUM_SIZE if __size__ is None else __size__ - cls_kargs['__enum__'] = __enum__ if isinstance(__enum__, (str, Tokens)): - del cls_kargs["__enum__"] cls_kargs.update(parse_enum_def(__enum__, __cls__=cls, **cls_kargs)) - cls_kargs["__enum__"] = None elif isinstance(__enum__, dict): - del cls_kargs["__enum__"] cls_kargs.update(__enum__) - cls_kargs["__enum__"] = None + if __name__ is None: __name__ = cls.__name__ + "_" + hashlib.sha1(str(__enum__).encode("utf-8")).hexdigest() cls_kargs["__anonymous__"] = True - cls_kargs["__name__"] = __name__ - return IntEnum(__name__, cls_kargs["__constants__"]) \ No newline at end of file + + cls_kargs.update(cls_kargs["__constants__"]) + return cls(__name__, cls_kargs) \ No newline at end of file diff --git a/cstruct/base.py b/cstruct/base.py index ab21666..74e569f 100644 --- a/cstruct/base.py +++ b/cstruct/base.py @@ -101,3 +101,4 @@ } CHAR_ZERO = bytes('\0', 'ascii') +DEFAULT_ENUM_SIZE = 4 \ No newline at end of file diff --git a/cstruct/field.py b/cstruct/field.py index 7e0e3e8..08c5bc0 100644 --- a/cstruct/field.py +++ b/cstruct/field.py @@ -193,7 +193,7 @@ def native_format(self) -> str: raise ParserError("Unknow type {}".format(self.c_type)) elif self.is_enum: try: - return C_TYPE_TO_FORMAT[ENUM_SIZE_TO_C_TYPE[len(self.ref)]] + return C_TYPE_TO_FORMAT[ENUM_SIZE_TO_C_TYPE[self.ref.size]] except KeyError: raise ParserError(f"Enum has invalid size. Needs to be in {ENUM_SIZE_TO_C_TYPE.keys()}") else: From 4974d951e3408aec5ab9c410a60021ca9266ad04 Mon Sep 17 00:00:00 2001 From: Sophie Tyalie Date: Tue, 1 Nov 2022 12:26:56 +0100 Subject: [PATCH 41/95] Enum: Remove hard size enforcement A warning will now be displayed and the enum size will be set to `base.DEFAULT_ENUM_SIZE` instead Signed-off-by: Sophie Tyalie --- cstruct/abstract.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cstruct/abstract.py b/cstruct/abstract.py index 616acb5..f8ccaff 100644 --- a/cstruct/abstract.py +++ b/cstruct/abstract.py @@ -291,7 +291,8 @@ def __new__(metacls: type["CEnumMeta"], cls: str, bases: tuple[type, ...], class if len(inst) > 0: if "__size__" not in classdict: - raise CEnumException("__size__ not specified. Cannot derive size as it is architecture dependent") + inst.__size__ = DEFAULT_ENUM_SIZE + print(f"Warning: __size__ not specified for enum {cls}. Will default to {DEFAULT_ENUM_SIZE} bytes") if not classdict.get("__anonymous__", False): ENUMS[cls] = inst return inst @@ -320,13 +321,15 @@ def parse( Args: __enum__: Definition of the enum in C syntax __name__: Name of the new Enum. If empty, a name based on the __enum__ hash is generated + __size__: Number of bytes that the enum should be read as Returns: cls: A new class mapping the definition """ cls_kargs: Dict[str, Any] = dict(kargs) - cls_kargs['__size__'] = DEFAULT_ENUM_SIZE if __size__ is None else __size__ + if __size__ is not None: + cls_kargs['__size__'] = __size__ if isinstance(__enum__, (str, Tokens)): cls_kargs.update(parse_enum_def(__enum__, __cls__=cls, **cls_kargs)) From f44f7109cf746ad033d912b4995382dc034b1094 Mon Sep 17 00:00:00 2001 From: Sophie Tyalie Date: Tue, 1 Nov 2022 12:42:35 +0100 Subject: [PATCH 42/95] Enum: Allow inline definition of enums This includes named and unnamed enums that are defined in a struct, similar to this: ``` struct { enum {A, B, C} v; } ``` Signed-off-by: Sophie Tyalie --- cstruct/c_parser.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/cstruct/c_parser.py b/cstruct/c_parser.py index 7001b32..3e4eaab 100644 --- a/cstruct/c_parser.py +++ b/cstruct/c_parser.py @@ -137,12 +137,24 @@ def parse_type(tokens: Tokens, __cls__: Type['AbstractCStruct'], byte_order: Opt except KeyError: raise ParserError("Unknow {} {}".format(c_type, tail)) elif c_type.startswith('enum'): + #from .abstract import AbstractCEnum + from .cenum import CEnum + c_type, tail = c_type.split(' ', 1) kind = Kind.ENUM - try: - ref = ENUMS[tail] - except KeyError: - raise ParserError(f"Unknown '{c_type}' '{tail}'") + if tokens.get() == '{': # Named nested struct + tokens.push(tail) + tokens.push(c_type) + ref = CEnum.parse(tokens, __name__=tail) + elif tail == '{': # unnamed nested struct + tokens.push(tail) + tokens.push(c_type) + ref = CEnum.parse(tokens) + else: + try: + ref = ENUMS[tail] + except KeyError: + raise ParserError(f"Unknown '{c_type}' '{tail}'") else: # other types kind = Kind.NATIVE ref = None @@ -254,6 +266,9 @@ def parse_enum( raise ParserError(f"duplicate enum name {name}") constants[name] = value + if next_token == "}": + break + result = { '__constants__': constants } From 2e9836c0ba516bbeb183a9356dd48c4eff60ea8b Mon Sep 17 00:00:00 2001 From: Sophie Tyalie Date: Tue, 1 Nov 2022 12:47:01 +0100 Subject: [PATCH 43/95] Enum: Fix enum parser I'm not fully sure what the test case was that would have resulted in an error, but looking over the code there was a break at the wrong position that could have resulted in an unparsed enum constant. Signed-off-by: Sophie Tyalie --- cstruct/c_parser.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/cstruct/c_parser.py b/cstruct/c_parser.py index 3e4eaab..1ead448 100644 --- a/cstruct/c_parser.py +++ b/cstruct/c_parser.py @@ -232,13 +232,12 @@ def parse_enum( name = tokens.pop() next_token = tokens.pop() - print(name, next_token) if next_token in {",", "}"}: # enum-constant without explicit value if len(constants) == 0: value = 0 else: value = next(reversed(constants.values())) + 1 - elif next_token == "=": + elif next_token == "=": # enum-constant with explicit value exp_elems = [] next_token = tokens.pop() while next_token not in {",", "}"}: @@ -256,9 +255,6 @@ def parse_enum( value = c_eval(int_expr) except (ValueError, TypeError): value = int(int_expr) - - if next_token == "}": - break else: raise ParserError(f"{__enum__} is not a valid enum expression") From 942b62cdf6a54361595b78a9a3ce898e1368d918 Mon Sep 17 00:00:00 2001 From: Sophie Tyalie Date: Tue, 1 Nov 2022 13:09:21 +0100 Subject: [PATCH 44/95] Enum: use signed instead of unsigned int for enum representation This hits upon a larger issue. It doesn't seem fully clear what type a compiler will use to represent an enum, but signed integer seems to be a good enough solution as enum-constants (not the enum itself) are defined as int signed. See following patch note https://gcc.gnu.org/legacy-ml/gcc-patches/2000-07/msg00993.html Signed-off-by: Sophie Tyalie --- cstruct/base.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cstruct/base.py b/cstruct/base.py index 74e569f..6b427ae 100644 --- a/cstruct/base.py +++ b/cstruct/base.py @@ -94,10 +94,10 @@ } ENUM_SIZE_TO_C_TYPE: Dict[int, str] = { - 1: 'uint8', - 2: 'uint16', - 4: 'uint32', - 8: 'uint64' + 1: 'int8', + 2: 'int16', + 4: 'int32', + 8: 'int64' } CHAR_ZERO = bytes('\0', 'ascii') From 3b5bc778b682503397d187f4aa9f31517a53144d Mon Sep 17 00:00:00 2001 From: Sophie Tyalie Date: Tue, 1 Nov 2022 13:14:22 +0100 Subject: [PATCH 45/95] Enum: Fix pytest errors with older python versions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Python 3.10 supports using `type[…]` or `tuple[…]` as type hints. This is relatively new and as such not support in older pythons Signed-off-by: Sophie Tyalie --- cstruct/abstract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cstruct/abstract.py b/cstruct/abstract.py index f8ccaff..88e8493 100644 --- a/cstruct/abstract.py +++ b/cstruct/abstract.py @@ -286,7 +286,7 @@ def __prepare__(metacls, cls, bases, **kwds): namespace.__class__ = metacls.WrapperDict return namespace - def __new__(metacls: type["CEnumMeta"], cls: str, bases: tuple[type, ...], classdict: _EnumDict, **kwds: Any) -> "CEnumMeta": + def __new__(metacls: Type["CEnumMeta"], cls: str, bases: Tuple[Type, ...], classdict: _EnumDict, **kwds: Any) -> "CEnumMeta": inst = super().__new__(metacls, cls, bases, classdict, **kwds) if len(inst) > 0: From e0a1c93cb9b06dd15bf1f2ef8bef7ad12e52ef68 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Tue, 1 Nov 2022 13:31:39 +0000 Subject: [PATCH 46/95] black code style --- cstruct/abstract.py | 9 +++++---- cstruct/base.py | 9 ++------- cstruct/c_parser.py | 20 ++++++-------------- cstruct/cenum.py | 3 ++- cstruct/exceptions.py | 2 ++ tests/test_cenum.py | 14 ++++++++------ tests/test_cstruct.py | 4 +--- tests/test_define.py | 12 ++++++++---- tests/test_memcstruct.py | 4 +--- 9 files changed, 35 insertions(+), 42 deletions(-) diff --git a/cstruct/abstract.py b/cstruct/abstract.py index 88e8493..a7d9903 100644 --- a/cstruct/abstract.py +++ b/cstruct/abstract.py @@ -32,7 +32,7 @@ from enum import IntEnum, EnumMeta, _EnumDict from unicodedata import name from .base import STRUCTS, ENUMS, DEFAULT_ENUM_SIZE -from .c_parser import parse_struct, parse_struct_def, parse_enum_def,parse_enum, Tokens +from .c_parser import parse_struct, parse_struct_def, parse_enum_def, parse_enum, Tokens from .field import calculate_padding, FieldType from .exceptions import CStructException, CEnumException @@ -111,7 +111,7 @@ def parse( __name__: Optional[str] = None, __byte_order__: Optional[str] = None, __is_union__: Optional[bool] = False, - **kargs: Dict[str, Any] + **kargs: Dict[str, Any], ) -> Type["AbstractCStruct"]: """ Return a new class mapping a C struct/union definition. @@ -302,6 +302,7 @@ def size(cls) -> int: "Enum size (in bytes)" return cls.__size__ + class AbstractCEnum(IntEnum, metaclass=CEnumMeta): """ Abstract C enum to Python class @@ -313,7 +314,7 @@ def parse( __enum__: Union[str, Tokens, Dict[str, Any]], __name__: Optional[str] = None, __size__: Optional[int] = None, - **kargs: Dict[str, Any] + **kargs: Dict[str, Any], ) -> Type["AbstractCEnum"]: """ Return a new Python Enum class mapping a C enum definition @@ -341,4 +342,4 @@ def parse( cls_kargs["__anonymous__"] = True cls_kargs.update(cls_kargs["__constants__"]) - return cls(__name__, cls_kargs) \ No newline at end of file + return cls(__name__, cls_kargs) diff --git a/cstruct/base.py b/cstruct/base.py index 6b427ae..96f2496 100644 --- a/cstruct/base.py +++ b/cstruct/base.py @@ -93,12 +93,7 @@ 'uint64': 'Q', } -ENUM_SIZE_TO_C_TYPE: Dict[int, str] = { - 1: 'int8', - 2: 'int16', - 4: 'int32', - 8: 'int64' -} +ENUM_SIZE_TO_C_TYPE: Dict[int, str] = {1: 'int8', 2: 'int16', 4: 'int32', 8: 'int64'} CHAR_ZERO = bytes('\0', 'ascii') -DEFAULT_ENUM_SIZE = 4 \ No newline at end of file +DEFAULT_ENUM_SIZE = 4 diff --git a/cstruct/c_parser.py b/cstruct/c_parser.py index 1ead448..3eb574b 100644 --- a/cstruct/c_parser.py +++ b/cstruct/c_parser.py @@ -137,7 +137,7 @@ def parse_type(tokens: Tokens, __cls__: Type['AbstractCStruct'], byte_order: Opt except KeyError: raise ParserError("Unknow {} {}".format(c_type, tail)) elif c_type.startswith('enum'): - #from .abstract import AbstractCEnum + # from .abstract import AbstractCEnum from .cenum import CEnum c_type, tail = c_type.split(' ', 1) @@ -165,7 +165,7 @@ def parse_struct_def( __def__: Union[str, Tokens], __cls__: Type['AbstractCStruct'], __byte_order__: Optional[str] = None, - **kargs: Any # Type['AbstractCStruct'], + **kargs: Any, # Type['AbstractCStruct'], ) -> Optional[Dict[str, Any]]: # naive C struct parsing if isinstance(__def__, Tokens): @@ -188,10 +188,7 @@ def parse_struct_def( raise ParserError("{} definition expected".format(vtype)) -def parse_enum_def( - __def__: Union[str, Tokens], - **kargs: Any -) -> Optional[Dict[str, Any]]: +def parse_enum_def(__def__: Union[str, Tokens], **kargs: Any) -> Optional[Dict[str, Any]]: # naive C enum parsing if isinstance(__def__, Tokens): tokens = __def__ @@ -213,10 +210,7 @@ def parse_enum_def( raise ParserError(f"{vtype} definition expected") -def parse_enum( - __enum__: Union[str, Tokens], - **kargs: Any -) -> Optional[Dict[str, Any]]: +def parse_enum(__enum__: Union[str, Tokens], **kargs: Any) -> Optional[Dict[str, Any]]: constants: Dict[str, int] = OrderedDict() if isinstance(__enum__, Tokens): @@ -265,9 +259,7 @@ def parse_enum( if next_token == "}": break - result = { - '__constants__': constants - } + result = {'__constants__': constants} return result @@ -276,7 +268,7 @@ def parse_struct( __cls__: Type['AbstractCStruct'], __is_union__: bool = False, __byte_order__: Optional[str] = None, - **kargs: Any + **kargs: Any, ) -> Dict[str, Any]: # naive C struct parsing __is_union__ = bool(__is_union__) diff --git a/cstruct/cenum.py b/cstruct/cenum.py index 0fc1654..7929f03 100644 --- a/cstruct/cenum.py +++ b/cstruct/cenum.py @@ -1,4 +1,5 @@ from .abstract import AbstractCEnum + class CEnum(AbstractCEnum): - ... \ No newline at end of file + ... diff --git a/cstruct/exceptions.py b/cstruct/exceptions.py index 3d66fb3..b5546b4 100644 --- a/cstruct/exceptions.py +++ b/cstruct/exceptions.py @@ -29,9 +29,11 @@ "EvalError", ] + class CEnumException(Exception): pass + class CStructException(Exception): pass diff --git a/tests/test_cenum.py b/tests/test_cenum.py index f137f8f..c9184b9 100644 --- a/tests/test_cenum.py +++ b/tests/test_cenum.py @@ -1,7 +1,8 @@ from cstruct import CEnum + class Dummy(CEnum): - __enum__ = """ + __enum__ = """ A, B, C = 2, @@ -9,9 +10,10 @@ class Dummy(CEnum): E = 2 """ + def test_dummy(): - assert Dummy.A == 0 - assert Dummy.B == 1 - assert Dummy.C == 2 - assert Dummy.D == 12 - assert Dummy.E == 2 \ No newline at end of file + assert Dummy.A == 0 + assert Dummy.B == 1 + assert Dummy.C == 2 + assert Dummy.D == 12 + assert Dummy.E == 2 diff --git a/tests/test_cstruct.py b/tests/test_cstruct.py index b5eab9f..e591273 100644 --- a/tests/test_cstruct.py +++ b/tests/test_cstruct.py @@ -261,7 +261,5 @@ def test_null_compare(): def test_invalid_inline(): with pytest.raises(ParserError): - cstruct.MemCStruct.parse( - 'struct { unsigned char head; unsigned char head; }', __byte_order__=cstruct.LITTLE_ENDIAN - ) + cstruct.MemCStruct.parse('struct { unsigned char head; unsigned char head; }', __byte_order__=cstruct.LITTLE_ENDIAN) assert False diff --git a/tests/test_define.py b/tests/test_define.py index a47e3a8..eda2947 100644 --- a/tests/test_define.py +++ b/tests/test_define.py @@ -87,16 +87,20 @@ class Invalid(cstruct.CStruct): def test_invalid_define(): with pytest.raises(ParserError): - cstruct.parse(""" + cstruct.parse( + """ #define xxx yyy zzz - """) + """ + ) def test_invalid_struct(): with pytest.raises(ParserError): - cstruct.parse(""" + cstruct.parse( + """ struct { int a; int; } - """) + """ + ) diff --git a/tests/test_memcstruct.py b/tests/test_memcstruct.py index b312d43..0ff95f8 100644 --- a/tests/test_memcstruct.py +++ b/tests/test_memcstruct.py @@ -255,7 +255,5 @@ def test_null_compare(): def test_invalid_inline(): with pytest.raises(ParserError): - cstruct.MemCStruct.parse( - 'struct { unsigned char head; unsigned char head; }', __byte_order__=cstruct.LITTLE_ENDIAN - ) + cstruct.MemCStruct.parse('struct { unsigned char head; unsigned char head; }', __byte_order__=cstruct.LITTLE_ENDIAN) assert False From c58f158e75cc1b74ca4daf0e7d5199e8ca1ebdb2 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Tue, 1 Nov 2022 18:09:52 +0000 Subject: [PATCH 47/95] Add enum support to cstruct.parse --- cstruct/__init__.py | 13 ++++++++----- cstruct/c_parser.py | 26 +++++++++++++++++--------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/cstruct/__init__.py b/cstruct/__init__.py index c7ccb12..7015ae3 100644 --- a/cstruct/__init__.py +++ b/cstruct/__init__.py @@ -28,7 +28,7 @@ __date__ = '15 August 2013' import struct -from typing import Any, Dict, Optional, Type +from typing import Any, Dict, Optional, Type, Union from .base import ( LITTLE_ENDIAN, BIG_ENDIAN, @@ -39,7 +39,7 @@ C_TYPE_TO_FORMAT, CHAR_ZERO, ) -from .abstract import CStructMeta, AbstractCStruct +from .abstract import CStructMeta, AbstractCStruct, AbstractCEnum from .cstruct import CStruct from .c_parser import parse_struct_def from .mem_cstruct import MemCStruct @@ -52,6 +52,7 @@ 'CHAR_ZERO', 'CStruct', 'MemCStruct', + 'CEnum', 'define', 'undef', 'getdef', @@ -134,13 +135,13 @@ def sizeof(type_: str) -> int: def parse( __struct__: str, __cls__: Optional[Type[AbstractCStruct]] = None, __name__: Optional[str] = None, **kargs: Dict[str, Any] -) -> Optional[Type[AbstractCStruct]]: +) -> Union[Type[AbstractCStruct], Type[AbstractCEnum], None]: """ - Return a new class mapping a C struct/union definition. + Return a new class mapping a C struct/union/enum definition. If the string does not contains any definition, return None. Args: - __struct__ (str): definition of the struct (or union) in C syntax + __struct__ (str): definition of the struct (or union/enum) in C syntax __cls__ (type): super class - CStruct(default) or MemCStruct __name__ (str): name of the new class. If empty, a name based on the __struct__ hash is generated __byte_order__ (str): byte order, valid values are LITTLE_ENDIAN, BIG_ENDIAN, NATIVE_ORDER @@ -154,5 +155,7 @@ def parse( cls_def = parse_struct_def(__struct__, __cls__=__cls__, **kargs) if cls_def is None: return None + elif cls_def['__is_enum__']: + return AbstractCEnum.parse(cls_def, __name__, **kargs) else: return __cls__.parse(cls_def, __name__, **kargs) diff --git a/cstruct/c_parser.py b/cstruct/c_parser.py index 3eb574b..f7a8ba8 100644 --- a/cstruct/c_parser.py +++ b/cstruct/c_parser.py @@ -31,9 +31,9 @@ from .exceptions import CStructException, ParserError if TYPE_CHECKING: - from .abstract import AbstractCStruct, AbstractCEnum + from .abstract import AbstractCStruct -__all__ = ['parse_struct', 'parse_struct_def', 'Tokens'] +__all__ = ['parse_struct', 'parse_struct_def', 'parse_enum_def', 'Tokens'] class Tokens(object): @@ -48,7 +48,7 @@ def __init__(self, text: str) -> None: _, name, value = line.strip().split(maxsplit=2) DEFINES[name] = c_eval(value) except Exception: - raise ParserError("Parsing line {}".format(line)) + raise ParserError(f"Parsing line {line}") else: lines.append(line) text = " ".join(lines) @@ -135,9 +135,8 @@ def parse_type(tokens: Tokens, __cls__: Type['AbstractCStruct'], byte_order: Opt try: ref = STRUCTS[tail] except KeyError: - raise ParserError("Unknow {} {}".format(c_type, tail)) + raise ParserError(f"Unknown '{c_type}' '{tail}'") elif c_type.startswith('enum'): - # from .abstract import AbstractCEnum from .cenum import CEnum c_type, tail = c_type.split(' ', 1) @@ -175,8 +174,10 @@ def parse_struct_def( if not tokens: return None kind = tokens.pop() + if kind == 'enum': + return parse_enum_def(__def__, **kargs) if kind not in ['struct', 'union']: - raise ParserError("struct or union expected - {}".format(kind)) + raise ParserError("struct, union, or enum expected - {kind}") __is_union__ = kind == 'union' vtype = tokens.pop() if tokens.get() == '{': # Named nested struct @@ -185,7 +186,7 @@ def parse_struct_def( elif vtype == '{': # Unnamed nested struct return parse_struct(tokens, __cls__=__cls__, __is_union__=__is_union__, __byte_order__=__byte_order__) else: - raise ParserError("{} definition expected".format(vtype)) + raise ParserError(f"{vtype} definition expected") def parse_enum_def(__def__: Union[str, Tokens], **kargs: Any) -> Optional[Dict[str, Any]]: @@ -242,7 +243,7 @@ def parse_enum(__enum__: Union[str, Tokens], **kargs: Any) -> Optional[Dict[str, break if len(exp_elems) == 0: - raise ParserError(f"enum is missing value expression") + raise ParserError("enum is missing value expression") int_expr = " ".join(exp_elems) try: @@ -259,7 +260,12 @@ def parse_enum(__enum__: Union[str, Tokens], **kargs: Any) -> Optional[Dict[str, if next_token == "}": break - result = {'__constants__': constants} + result = { + '__constants__': constants, + '__is_struct__': False, + '__is_union__': False, + '__is_enum__': True, + } return result @@ -325,7 +331,9 @@ def parse_struct( '__fields__': list(fields_types.keys()), '__fields_types__': fields_types, '__size__': size, + '__is_struct__': not __is_union__, '__is_union__': __is_union__, + '__is_enum__': False, '__byte_order__': __byte_order__, '__alignment__': max_alignment, } From 87ce6d3e4f7273d27230f59f591846e7ed5261f1 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Tue, 1 Nov 2022 18:10:14 +0000 Subject: [PATCH 48/95] Add some enum tests --- tests/test_cenum.py | 83 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 77 insertions(+), 6 deletions(-) diff --git a/tests/test_cenum.py b/tests/test_cenum.py index c9184b9..7e84c7a 100644 --- a/tests/test_cenum.py +++ b/tests/test_cenum.py @@ -1,14 +1,42 @@ +import pytest +import cstruct from cstruct import CEnum +from enum import Enum class Dummy(CEnum): __enum__ = """ - A, - B, - C = 2, - D = 5 + 7, - E = 2 - """ + #define SOME_DEFINE 7 + + A, + B, + C = 2, + D = 5 + SOME_DEFINE, + E = 2 + """ + + +class HtmlFont(CEnum): + __size__ = 2 + __def__ = """ + #define NONE 0 + + enum htmlfont { + HTMLFONT_NONE = NONE, + HTMLFONT_BOLD, + HTMLFONT_ITALIC, + }; + """ + + +class StructWithEnum(cstruct.MemCStruct): + __byte_order__ = cstruct.LITTLE_ENDIAN + __def__ = """ + struct StructWithEnum { + enum HtmlFont font; + unsigned int font_size; + } + """ def test_dummy(): @@ -17,3 +45,46 @@ def test_dummy(): assert Dummy.C == 2 assert Dummy.D == 12 assert Dummy.E == 2 + + +def test_missing_attribute(): + with pytest.raises(AttributeError): + assert Dummy.F + + +def test_set_attribute(): + with pytest.raises(AttributeError): + Dummy.A = 100 + + +def test_html_font(): + assert HtmlFont.HTMLFONT_NONE == 0 + assert HtmlFont.HTMLFONT_BOLD == 1 + assert HtmlFont.HTMLFONT_ITALIC == 2 + + +def test_parse_enum(): + reply_stat = cstruct.parse("enum reply_stat { MSG_ACCEPTED=0, MSG_DENIED=1 }") + assert isinstance(reply_stat.MSG_ACCEPTED, Enum) + assert isinstance(reply_stat.MSG_DENIED, Enum) + assert reply_stat.MSG_ACCEPTED == 0 + assert reply_stat.MSG_DENIED == 1 + + +def test_struct_with_enum(): + s = StructWithEnum() + s.font = HtmlFont.HTMLFONT_BOLD + s.font_size = 11 + assert s.font == HtmlFont.HTMLFONT_BOLD + assert s.font_size == 11 + + s.font = HtmlFont.HTMLFONT_NONE + s.font_size = 20 + assert s.font == HtmlFont.HTMLFONT_NONE + assert s.font_size == 20 + packed = s.pack() + + s1 = StructWithEnum() + s1.unpack(packed) + assert s1.font == HtmlFont.HTMLFONT_NONE + assert s1.font_size == 20 From d9e7c68aeebd3e9296c793e92dc8576953af8492 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Tue, 1 Nov 2022 18:14:01 +0000 Subject: [PATCH 49/95] Update README.md --- README.md | 268 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 268 insertions(+) diff --git a/README.md b/README.md index 44e9095..2a9f272 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,274 @@ serializing the values into an array of bytes. [Api Documentation](https://python-cstruct.readthedocs.io/en/latest/) +Install +------- + +``` +pip install cstruct +``` + +Features +-------- + +### Structs + +Struct definition subclassing `cstruct.MemCStruct` + +```python +class Position(cstruct.MemCStruct): + __def__ = """ + struct { + unsigned char head; + unsigned char sector; + unsigned char cyl; + } + """ + +pos = Position(head=10, sector=20, cyl=30) +print(f"head: {pos.head} sector: {pos.sector} cyl: {pos.cyl}") +``` + +Struct definition using `cstruct.parse` + +```python +Partition = cstruct.parse(""" + struct { + #define ACTIVE_FLAG 0x80 + + unsigned char status; /* 0x80 - active */ + struct Position start; + unsigned char partition_type; + struct Position end; + unsigned int start_sect; /* starting sector counting from 0 */ + unsigned int sectors; /* nr of sectors in partition */ + } +""") + +part = Partition() +part.status = cstruct.getdef('ACTIVE_FLAG') +``` + +### Unions + +Union definition subclassing `cstruct.MemCStruct` + +```python +class Data(cstruct.MemCStruct): + __def__ = """ + union { + int integer; + float real; + } + """ + +data = Data() +data.integer = 2 +data.real = 3 +assert data.integer != 2 +``` + +### Enums + +Enum definition subclassing `cstruct.CEnum` + +```python +class HtmlFont(cstruct.CEnum): + __size__ = 2 + __def__ = """ + #define NONE 0 + + enum htmlfont { + HTMLFONT_NONE = NONE, + HTMLFONT_BOLD, + HTMLFONT_ITALIC + } + """ + +assert HtmlFont.HTMLFONT_NONE == 0 +assert HtmlFont.HTMLFONT_BOLD == 1 +assert HtmlFont.HTMLFONT_ITALIC == 2 +``` + +Different supported `__def__` styles: + +```c +enum Type_A a; // externally defined using CEnum +enum Type_B {A, B, C} b; +enum {A, B, C} c; +``` + +```python +class Type_A(cstruct.CEnum): + __size__ = 2 + __enum__ = """ + #define SOME_DEFINE 7 + + A, + B, + C = 5, + D, + E = 7 + SOME_DEFINE + """ + +# this is a nice gimmick that works, but wasn't really planned to be supported +class Type_C(cstruct.CEnum): + A = 0, + B = 1, + C = 2, + D = 3 +``` + +### Nested structures (named/anonymous) + +```python +class Packet(cstruct.MemCStruct): + __def__ = """ + struct Packet { + uint8_t packetLength; + union { + struct { + uint16_t field1; + uint16_t field2; + uint16_t field3; + } format1; + struct { + double value1; + double value2; + } format2; + }; + }; + """ +``` + +### Byte Order, Size, and Padding + +Suported byte orders: + +* `cstruct.LITTLE_ENDIAN` - Little endian byte order, standard size, no padding +* `cstruct.BIG_ENDIAN` - Big endian byte order, standard size, no padding +* `cstruct.NATIVE_ORDER` - Native byte order, native size, padding + +```python +class Native(cstruct.MemCStruct): + __byte_order__ = cstruct.NATIVE_ORDER + __def__ = """ + struct { + long p; + char c; + long x; + } + """ +``` + +### Flexible Array Member + +```python +class Pkg(cstruct.MemCStruct): + __byte_order__ = cstruct.LITTLE_ENDIAN + __def__ = """ + struct { + uint16_t cmd; + uint16_t length; + uint8_t data[]; + } + """ + +pkg = Pkg() +pkg.length = 4 +pkg.data = [10, 20, 30, 40] +``` + +### Pack and Unpack + +```python +class StructWithEnum(cstruct.MemCStruct): + __byte_order__ = cstruct.LITTLE_ENDIAN + __def__ = """ + struct StructWithEnum { + enum HtmlFont font; + unsigned int font_size; + } + """ + +# Pack +s.font = HtmlFont.HTMLFONT_NONE +s.font_size = 20 +assert s.font == HtmlFont.HTMLFONT_NONE +assert s.font_size == 20 +packed = s.pack() + +# Unpack +s1 = StructWithEnum() +s1.unpack(packed) +assert s1.font == HtmlFont.HTMLFONT_NONE +assert s1.font_size == 20 +``` + +### Define, Sizeof, and Eval + +Definitions in Struct declaration: + +```python +class Packet(cstruct.MemCStruct): + __byte_order__ = cstruct.LITTLE_ENDIAN + __def__ = """ + #define MaxPacket 20 + + struct Packet { + uint8_t bytes[MaxPacket]; + } + """ +``` + +Parse C definitions: + +```python +cstruct.parse(""" + #define A1 10 + #define A2 10 + A1 + #define A3 30 +""") +assert cstruct.getdef("A1") == 10 +assert cstruct.getdef('A2') == 20 +``` + +Get structure size: + +```python +cstruct.sizeof(Partition) +``` + +Evaluate C expression: + +```python +cstruct.c_eval("A1 / 10") +cstruct.c_eval("((A10 < 6) || (A10>10))") +``` + +C expressions are automatically evaluated during structure definitions: + +```python +class MBR(cstruct.MemCStruct): + __byte_order__ = cstruct.LITTLE_ENDIAN + __struct__ = """ + #define MBR_SIZE 512 + #define MBR_DISK_SIGNATURE_SIZE 4 + #define MBR_USUALY_NULLS_SIZE 2 + #define MBR_SIGNATURE_SIZE 2 + #define MBR_BOOT_SIGNATURE 0xaa55 + #define MBR_PARTITIONS_NUM 4 + #define MBR_PARTITIONS_SIZE (sizeof(Partition) * MBR_PARTITIONS_NUM) + #define MBR_UNUSED_SIZE (MBR_SIZE - MBR_DISK_SIGNATURE_SIZE - MBR_USUALY_NULLS_SIZE - MBR_PARTITIONS_SIZE - MBR_SIGNATURE_SIZE) + + char unused[MBR_UNUSED_SIZE]; + unsigned char disk_signature[MBR_DISK_SIGNATURE_SIZE]; + unsigned char usualy_nulls[MBR_USUALY_NULLS_SIZE]; + struct Partition partitions[MBR_PARTITIONS_NUM]; + uint16 signature; + """ +``` + Example ------- From 275f57d8d1de9e9d5789a7aa2cb556c94551aa8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Wed, 2 Nov 2022 08:22:01 +0100 Subject: [PATCH 50/95] exclude tests from being installed Do not install `tests` as a top-level package -- such a package name is bound to collide with other packages, and for this reason no package should be installing files there. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 33bf4e5..bfeaed7 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ def readme(): author_email='andrea.bonomi@gmail.com', url='http://github.com/andreax79/python-cstruct', license='MIT', - packages=find_packages(exclude=['ez_setup', 'examples']), + packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), include_package_data=True, zip_safe=True, install_requires=[ From d69ee4a6fd2c27691c9c0ad3362dc9d6353e89e1 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Wed, 2 Nov 2022 11:17:47 +0000 Subject: [PATCH 51/95] Update README.md --- README.md | 66 +++++++++++++++++++++++++++++++-------------------- docs/index.md | 13 +--------- 2 files changed, 41 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 2a9f272..28934a5 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,13 @@ C-style structs for Python Convert C struct/union definitions into Python classes with methods for serializing/deserializing. + The usage is very simple: create a class subclassing cstruct.MemCStruct -and add a C struct/union definition as a string in the __struct__ field. +and add a C struct/union definition as a string in the `__def__` field. + The C struct/union definition is parsed at runtime and the struct format string -is generated. The class offers the method "unpack" for deserializing -an array of bytes into a Python object and the method "pack" for +is generated. The class offers the method `unpack` for deserializing +an array of bytes into a Python object and the method `pack` for serializing the values into an array of bytes. [Api Documentation](https://python-cstruct.readthedocs.io/en/latest/) @@ -272,7 +274,7 @@ C expressions are automatically evaluated during structure definitions: ```python class MBR(cstruct.MemCStruct): __byte_order__ = cstruct.LITTLE_ENDIAN - __struct__ = """ + __def__ = """ #define MBR_SIZE 512 #define MBR_DISK_SIGNATURE_SIZE 4 #define MBR_USUALY_NULLS_SIZE 2 @@ -282,11 +284,13 @@ class MBR(cstruct.MemCStruct): #define MBR_PARTITIONS_SIZE (sizeof(Partition) * MBR_PARTITIONS_NUM) #define MBR_UNUSED_SIZE (MBR_SIZE - MBR_DISK_SIGNATURE_SIZE - MBR_USUALY_NULLS_SIZE - MBR_PARTITIONS_SIZE - MBR_SIGNATURE_SIZE) - char unused[MBR_UNUSED_SIZE]; - unsigned char disk_signature[MBR_DISK_SIGNATURE_SIZE]; - unsigned char usualy_nulls[MBR_USUALY_NULLS_SIZE]; - struct Partition partitions[MBR_PARTITIONS_NUM]; - uint16 signature; + struct { + char unused[MBR_UNUSED_SIZE]; + unsigned char disk_signature[MBR_DISK_SIGNATURE_SIZE]; + unsigned char usualy_nulls[MBR_USUALY_NULLS_SIZE]; + struct Partition partitions[MBR_PARTITIONS_NUM]; + uint16 signature; + } """ ``` @@ -301,24 +305,30 @@ import cstruct class Position(cstruct.MemCStruct): __byte_order__ = cstruct.LITTLE_ENDIAN - __struct__ = """ - unsigned char head; - unsigned char sector; - unsigned char cyl; + __def__ = """ + struct { + unsigned char head; + unsigned char sector; + unsigned char cyl; + } """ class Partition(cstruct.MemCStruct): __byte_order__ = cstruct.LITTLE_ENDIAN - __struct__ = """ + __def__ = """ #define ACTIVE_FLAG 0x80 - unsigned char status; /* 0x80 - active */ - struct Position start; - unsigned char partition_type; - struct Position end; - unsigned int start_sect; /* starting sector counting from 0 */ - unsigned int sectors; /* nr of sectors in partition */ + typedef struct Position Position; + + struct { + unsigned char status; /* 0x80 - active */ + Position start; + unsigned char partition_type; + Position end; + unsigned int start_sect; /* starting sector counting from 0 */ + unsigned int sectors; /* nr of sectors in partition */ + } """ def print_info(self): @@ -332,7 +342,7 @@ class Partition(cstruct.MemCStruct): class MBR(cstruct.MemCStruct): __byte_order__ = cstruct.LITTLE_ENDIAN - __struct__ = """ + __def__ = """ #define MBR_SIZE 512 #define MBR_DISK_SIGNATURE_SIZE 4 #define MBR_USUALY_NULLS_SIZE 2 @@ -342,11 +352,15 @@ class MBR(cstruct.MemCStruct): #define MBR_PARTITIONS_SIZE (sizeof(Partition) * MBR_PARTITIONS_NUM) #define MBR_UNUSED_SIZE (MBR_SIZE - MBR_DISK_SIGNATURE_SIZE - MBR_USUALY_NULLS_SIZE - MBR_PARTITIONS_SIZE - MBR_SIGNATURE_SIZE) - char unused[MBR_UNUSED_SIZE]; - unsigned char disk_signature[MBR_DISK_SIGNATURE_SIZE]; - unsigned char usualy_nulls[MBR_USUALY_NULLS_SIZE]; - struct Partition partitions[MBR_PARTITIONS_NUM]; - uint16 signature; + typedef struct Partition Partition; + + struct { + char unused[MBR_UNUSED_SIZE]; + unsigned char disk_signature[MBR_DISK_SIGNATURE_SIZE]; + unsigned char usualy_nulls[MBR_USUALY_NULLS_SIZE]; + Partition partitions[MBR_PARTITIONS_NUM]; + uint16 signature; + } """ @property diff --git a/docs/index.md b/docs/index.md index 9850dfb..563ed56 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,12 +1 @@ -# Python-CStruct - -Convert C struct/union definitions into Python classes with methods for -serializing/deserializing. - -The usage is very simple: create a class subclassing cstruct.MemCStruct -and add a C struct/union definition as a string in the `__struct__` field. - -The C struct/union definition is parsed at runtime and the struct format string -is generated. The class offers the method `unpack` for deserializing -an array of bytes into a Python object and the method `pack` for -serializing the values into an array of bytes. +{!README.md!} From de1168763326e034295e7b9f2339041bb2bf4c03 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Wed, 2 Nov 2022 11:18:47 +0000 Subject: [PATCH 52/95] Add typedef parsing --- cstruct/c_parser.py | 21 ++++++++++++- tests/test_typdef.py | 73 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 tests/test_typdef.py diff --git a/cstruct/c_parser.py b/cstruct/c_parser.py index f7a8ba8..60b3e99 100644 --- a/cstruct/c_parser.py +++ b/cstruct/c_parser.py @@ -160,6 +160,22 @@ def parse_type(tokens: Tokens, __cls__: Type['AbstractCStruct'], byte_order: Opt return FieldType(kind, c_type, ref, vlen, flexible_array, byte_order, offset) +def parse_typedef(tokens: Tokens, __cls__: Type['AbstractCStruct'], byte_order: Optional[str]) -> None: + field_type = parse_type(tokens, __cls__, byte_order, 0) + vname = tokens.pop() + if field_type.ref is None: + TYPEDEFS[vname] = field_type.c_type + elif field_type.ref.__is_enum__: + TYPEDEFS[vname] = f"enum {field_type.ref.__name__}" + elif field_type.ref.__is_union__: + TYPEDEFS[vname] = f"union {field_type.ref.__name__}" + else: + TYPEDEFS[vname] = f"struct {field_type.ref.__name__}" + t = tokens.pop() + if t != ';': + raise ParserError(f"; expected but {t} found") + + def parse_struct_def( __def__: Union[str, Tokens], __cls__: Type['AbstractCStruct'], @@ -174,10 +190,13 @@ def parse_struct_def( if not tokens: return None kind = tokens.pop() + if kind == 'typedef': + parse_typedef(tokens, __cls__, __byte_order__) + return parse_struct_def(tokens, __cls__, __byte_order__, **kargs) if kind == 'enum': return parse_enum_def(__def__, **kargs) if kind not in ['struct', 'union']: - raise ParserError("struct, union, or enum expected - {kind}") + raise ParserError(f"struct, union, or enum expected - {kind}") __is_union__ = kind == 'union' vtype = tokens.pop() if tokens.get() == '{': # Named nested struct diff --git a/tests/test_typdef.py b/tests/test_typdef.py new file mode 100644 index 0000000..d02eaa3 --- /dev/null +++ b/tests/test_typdef.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python +# ***************************************************************************** +# +# Copyright (c) 2013-2019 Andrea Bonomi +# +# Published under the terms of the MIT license. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. +# +# ***************************************************************************** + +from cstruct import MemCStruct, NATIVE_ORDER +from cstruct.base import TYPEDEFS + +class ExitStatus(MemCStruct): + __def__ = """ + struct ExitStatus { + short e_termination; /* Process termination status. */ + short e_exit; /* Process exit status. */ + } + """ + +class Utmp(MemCStruct): + __byte_order__ = NATIVE_ORDER + __def__ = """ + #define UT_NAMESIZE 32 + #define UT_LINESIZE 32 + #define UT_HOSTSIZE 256 + + typedef int pid_t; + typedef long time_t; + typedef unsigned long int ulong; + typedef struct ExitStatus ExitStatus; + + struct { + short ut_type; /* Type of record */ + pid_t ut_pid; /* PID of login process */ + char ut_line[UT_LINESIZE]; /* Device name of tty - "/dev/" */ + char ut_id[4]; /* Terminal name suffix, or inittab(5) ID */ + char ut_user[UT_NAMESIZE]; /* Username */ + char ut_host[UT_HOSTSIZE]; /* Hostname for remote login, or kernel version for run-level messages */ + ExitStatus ut_exit; /* Exit status of a process marked as DEAD_PROCESS; not used by Linux init (1 */ + int32_t ut_session; /* Session ID (getsid(2)), used for windowing */ + struct { + int32_t tv_sec; /* Seconds */ + int32_t tv_usec; /* Microseconds */ + } ut_tv; /* Time entry was made */ + int32_t ut_addr_v6[4]; /* Internet address of remote host; IPv4 address uses just ut_addr_v6[0] */ + char __unused[20]; /* Reserved for future use */ + } + """ + +def test_typedef(): + assert TYPEDEFS['pid_t'] == 'int' + assert TYPEDEFS['time_t'] == 'long' + assert TYPEDEFS['ulong'] == 'unsigned long' + assert TYPEDEFS['ExitStatus'] == 'struct ExitStatus' From 5df786d4b857e68ba643f8df70177f92c7ffccdf Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Wed, 2 Nov 2022 11:19:09 +0000 Subject: [PATCH 53/95] Update examples --- examples/fdisk.py | 45 ++++++++++++++++++------------ examples/fdisk.sh | 3 ++ examples/flexible_array.py | 10 ++++--- examples/flexible_array.sh | 3 ++ examples/who.py | 56 ++++++++++++++++++++++---------------- examples/who.sh | 3 ++ 6 files changed, 74 insertions(+), 46 deletions(-) create mode 100755 examples/fdisk.sh create mode 100755 examples/flexible_array.sh create mode 100755 examples/who.sh diff --git a/examples/fdisk.py b/examples/fdisk.py index 9aadf46..fdb352c 100644 --- a/examples/fdisk.py +++ b/examples/fdisk.py @@ -49,24 +49,29 @@ class Position(cstruct.MemCStruct): __byte_order__ = cstruct.LITTLE_ENDIAN - __struct__ = """ - unsigned char head; - unsigned char sector; - unsigned char cyl; + __def__ = """ + struct { + unsigned char head; + unsigned char sector; + unsigned char cyl; + } """ class Partition(cstruct.MemCStruct): __byte_order__ = cstruct.LITTLE_ENDIAN - __struct__ = """ + __def__ = """ #define ACTIVE_FLAG 0x80 - - unsigned char status; /* 0x80 - active */ - struct Position start; - unsigned char partition_type; - struct Position end; - unsigned int start_sect; /* starting sector counting from 0 */ - unsigned int sectors; /* nr of sectors in partition */ + typedef struct Position Position; + + struct { + unsigned char status; /* 0x80 - active */ + Position start; + unsigned char partition_type; + Position end; + unsigned int start_sect; /* starting sector counting from 0 */ + unsigned int sectors; /* nr of sectors in partition */ + } """ @property @@ -96,7 +101,7 @@ def __str__(self): class MBR(cstruct.MemCStruct): __byte_order__ = cstruct.LITTLE_ENDIAN - __struct__ = """ + __def__ = """ #define MBR_SIZE 512 #define MBR_DISK_SIGNATURE_SIZE 4 #define MBR_USUALY_NULLS_SIZE 2 @@ -106,11 +111,15 @@ class MBR(cstruct.MemCStruct): #define MBR_PARTITIONS_SIZE (sizeof(Partition) * MBR_PARTITIONS_NUM) #define MBR_UNUSED_SIZE (MBR_SIZE - MBR_DISK_SIGNATURE_SIZE - MBR_USUALY_NULLS_SIZE - MBR_PARTITIONS_SIZE - MBR_SIGNATURE_SIZE) - char unused[MBR_UNUSED_SIZE]; - unsigned char disk_signature[MBR_DISK_SIGNATURE_SIZE]; - unsigned char usualy_nulls[MBR_USUALY_NULLS_SIZE]; - struct Partition partitions[MBR_PARTITIONS_NUM]; - uint16 signature; + typedef struct Partition Partition; + + struct { + char unused[MBR_UNUSED_SIZE]; + unsigned char disk_signature[MBR_DISK_SIGNATURE_SIZE]; + unsigned char usualy_nulls[MBR_USUALY_NULLS_SIZE]; + Partition partitions[MBR_PARTITIONS_NUM]; + uint16 signature; + } """ @property diff --git a/examples/fdisk.sh b/examples/fdisk.sh new file mode 100755 index 0000000..d81478d --- /dev/null +++ b/examples/fdisk.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env sh +cd "$(dirname "$0")/.." || exit +python -m examples.fdisk $* diff --git a/examples/flexible_array.py b/examples/flexible_array.py index 7a46764..4f8c28a 100644 --- a/examples/flexible_array.py +++ b/examples/flexible_array.py @@ -6,10 +6,12 @@ class FlexArray(MemCStruct): - __struct__ = """ - int length; - uint32 checksum; - long data[]; + __def__ = """ + struct { + int length; + uint32 checksum; + long data[]; + } """ def set_length(self, length): diff --git a/examples/flexible_array.sh b/examples/flexible_array.sh new file mode 100755 index 0000000..73d2cc2 --- /dev/null +++ b/examples/flexible_array.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env sh +cd "$(dirname "$0")/.." || exit +python -m examples.flexible_array $* diff --git a/examples/who.py b/examples/who.py index c18d2b4..0569cd1 100644 --- a/examples/who.py +++ b/examples/who.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -from cstruct import parse, getdef, typedef, MemCStruct, NATIVE_ORDER +from cstruct import parse, getdef, MemCStruct, NATIVE_ORDER from pathlib import Path import argparse import sys @@ -30,24 +30,28 @@ #define UT_LINESIZE 32 #define UT_NAMESIZE 32 #define UT_HOSTSIZE 256 + +typedef int pid_t; +typedef long time_t; """ ) -typedef("int", "pid_t") -typedef("long", "time_t") - class ExitStatus(MemCStruct): - __struct__ = """ - short e_termination; /* Process termination status. */ - short e_exit; /* Process exit status. */ + __def__ = """ + struct ExitStatus { + short e_termination; /* Process termination status. */ + short e_exit; /* Process exit status. */ + } """ class Timeval(MemCStruct): - __struct__ = """ - int32_t tv_sec; /* Seconds. */ - int32_t tv_usec; /* Microseconds. */ + __def__ = """ + struct { + int32_t tv_sec; /* Seconds. */ + int32_t tv_usec; /* Microseconds. */ + } """ @@ -57,21 +61,25 @@ def str_from_c(string): class Utmp(MemCStruct): __byte_order__ = NATIVE_ORDER - __struct__ = """ - short ut_type; /* Type of record */ - pid_t ut_pid; /* PID of login process */ - char ut_line[UT_LINESIZE]; /* Device name of tty - "/dev/" */ - char ut_id[4]; /* Terminal name suffix, or inittab(5) ID */ - char ut_user[UT_NAMESIZE]; /* Username */ - char ut_host[UT_HOSTSIZE]; /* Hostname for remote login, or kernel version for run-level messages */ - struct ExitStatus ut_exit; /* Exit status of a process marked as DEAD_PROCESS; not used by Linux init (1 */ - int32_t ut_session; /* Session ID (getsid(2)), used for windowing */ + __def__ = """ + typedef struct ExitStatus ExitStatus; + struct { - int32_t tv_sec; /* Seconds */ - int32_t tv_usec; /* Microseconds */ - } ut_tv; /* Time entry was made */ - int32_t ut_addr_v6[4]; /* Internet address of remote host; IPv4 address uses just ut_addr_v6[0] */ - char __unused[20]; /* Reserved for future use */ + short ut_type; /* Type of record */ + pid_t ut_pid; /* PID of login process */ + char ut_line[UT_LINESIZE]; /* Device name of tty - "/dev/" */ + char ut_id[4]; /* Terminal name suffix, or inittab(5) ID */ + char ut_user[UT_NAMESIZE]; /* Username */ + char ut_host[UT_HOSTSIZE]; /* Hostname for remote login, or kernel version for run-level messages */ + ExitStatus ut_exit; /* Exit status of a process marked as DEAD_PROCESS; not used by Linux init (1 */ + int32_t ut_session; /* Session ID (getsid(2)), used for windowing */ + struct { + int32_t tv_sec; /* Seconds */ + int32_t tv_usec; /* Microseconds */ + } ut_tv; /* Time entry was made */ + int32_t ut_addr_v6[4]; /* Internet address of remote host; IPv4 address uses just ut_addr_v6[0] */ + char __unused[20]; /* Reserved for future use */ + } """ @property diff --git a/examples/who.sh b/examples/who.sh new file mode 100755 index 0000000..467de69 --- /dev/null +++ b/examples/who.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env sh +cd "$(dirname "$0")/.." || exit +python -m examples.who $* From f3fedb5af150c280ad8568a8d52be2d18cf1cd7b Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Wed, 2 Nov 2022 22:12:22 +0000 Subject: [PATCH 54/95] Add check for reserved fields names --- cstruct/c_parser.py | 10 ++++++---- tests/test_cstruct.py | 5 ++++- tests/test_memcstruct.py | 5 ++++- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/cstruct/c_parser.py b/cstruct/c_parser.py index 60b3e99..e578acc 100644 --- a/cstruct/c_parser.py +++ b/cstruct/c_parser.py @@ -316,15 +316,17 @@ def parse_struct( field_type = parse_type(tokens, __cls__, __byte_order__, offset) vname = tokens.pop() if vname in fields_types: - raise ParserError("Duplicate member '{}'".format(vname)) + raise ParserError(f"Duplicate member '{vname}'") + if vname in dir(__cls__): + raise ParserError(f"Invalid reserved member name '{vname}'") # anonymous nested union if vname == ';' and field_type.ref is not None and (__is_union__ or field_type.ref.__is_union__): # add the anonymous struct fields to the parent for nested_field_name, nested_field_type in field_type.ref.__fields_types__.items(): if nested_field_name in fields_types: - raise ParserError("Duplicate member '{}'".format(nested_field_name)) + raise ParserError(f"Duplicate member '{nested_field_name}'") fields_types[nested_field_name] = nested_field_type - vname = "__anonymous{}".format(anonymous) + vname = f"__anonymous{anonymous}" anonymous += 1 tokens.push(';') fields_types[vname] = field_type @@ -336,7 +338,7 @@ def parse_struct( offset = field_type.offset + field_type.vsize t = tokens.pop() if t != ';': - raise ParserError("; expected but {} found".format(t)) + raise ParserError(f"; expected but {t} found") if __is_union__: # C union # Calculate the sizeof union as size of its largest element diff --git a/tests/test_cstruct.py b/tests/test_cstruct.py index e591273..66e6bca 100644 --- a/tests/test_cstruct.py +++ b/tests/test_cstruct.py @@ -262,4 +262,7 @@ def test_null_compare(): def test_invalid_inline(): with pytest.raises(ParserError): cstruct.MemCStruct.parse('struct { unsigned char head; unsigned char head; }', __byte_order__=cstruct.LITTLE_ENDIAN) - assert False + +def test_invalid_inline_reserved(): + with pytest.raises(ParserError): + cstruct.CStruct.parse('struct { int size; }') diff --git a/tests/test_memcstruct.py b/tests/test_memcstruct.py index 0ff95f8..047b9cb 100644 --- a/tests/test_memcstruct.py +++ b/tests/test_memcstruct.py @@ -256,4 +256,7 @@ def test_null_compare(): def test_invalid_inline(): with pytest.raises(ParserError): cstruct.MemCStruct.parse('struct { unsigned char head; unsigned char head; }', __byte_order__=cstruct.LITTLE_ENDIAN) - assert False + +def test_invalid_inline_reserved(): + with pytest.raises(ParserError): + cstruct.MemCStruct.parse('struct { int size; }') From 1dbf640dd960864deec090f4239890f5fa45f61e Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Wed, 2 Nov 2022 22:43:36 +0000 Subject: [PATCH 55/95] Add inspect method --- cstruct/abstract.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/cstruct/abstract.py b/cstruct/abstract.py index a7d9903..d06e08b 100644 --- a/cstruct/abstract.py +++ b/cstruct/abstract.py @@ -29,6 +29,7 @@ from typing import Any, BinaryIO, List, Dict, Optional, Type, Tuple, Union import hashlib import sys +from io import StringIO from enum import IntEnum, EnumMeta, _EnumDict from unicodedata import name from .base import STRUCTS, ENUMS, DEFAULT_ENUM_SIZE @@ -226,6 +227,33 @@ def sizeof(cls) -> int: "Structure size in bytes (flexible array member size is omitted)" return cls.__size__ + def inspect(self, start_addr: Optional[int] = None, end_addr: Optional[int] = None) -> str: + """ + Return memory content in hexadecimal + + Args: + start_addr: start address + end_addr: end address + """ + buffer = StringIO() + if hasattr(self, '__mem__'): + mem = self.__mem__ + else: + mem = self.pack() + for i in range(start_addr or 0, end_addr or self.size, 16): + row = mem[i : i + 16] + buffer.write(f"{i:08x} ") + for j, c in enumerate(row): + separator = ' ' if j == 7 else '' + buffer.write(f" {c:02x}{separator}") + buffer.write(" |") + for c in row: + buffer.write(chr(c) if c >= 32 and c < 127 else '.') + buffer.write("|") + buffer.write("\n") + buffer.seek(0, 0) + return buffer.read() + def __eq__(self, other: Any) -> bool: return other is not None and isinstance(other, self.__class__) and self.__dict__ == other.__dict__ From 0ad6fbc15d5f34c96a6445c35590baa7298d904a Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Wed, 2 Nov 2022 22:44:57 +0000 Subject: [PATCH 56/95] Fix imports --- cstruct/abstract.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cstruct/abstract.py b/cstruct/abstract.py index d06e08b..9667ffe 100644 --- a/cstruct/abstract.py +++ b/cstruct/abstract.py @@ -28,14 +28,12 @@ from collections import OrderedDict from typing import Any, BinaryIO, List, Dict, Optional, Type, Tuple, Union import hashlib -import sys from io import StringIO from enum import IntEnum, EnumMeta, _EnumDict -from unicodedata import name from .base import STRUCTS, ENUMS, DEFAULT_ENUM_SIZE from .c_parser import parse_struct, parse_struct_def, parse_enum_def, parse_enum, Tokens from .field import calculate_padding, FieldType -from .exceptions import CStructException, CEnumException +from .exceptions import CStructException __all__ = ['CStructMeta', 'AbstractCStruct', "CEnumMeta", 'AbstractCEnum'] From e23fc1a660f44b4e894d48c9165263f9f2eba9c5 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Tue, 8 Nov 2022 07:49:24 +0000 Subject: [PATCH 57/95] documentation --- README.md | 215 +++++++++++++------------------- changelog.txt | 26 ++-- cstruct/c_parser.py | 25 +++- docs/CODE_OF_CONDUCT.md | 1 + docs/api/c_parser.md | 1 + docs/changelog.md | 1 + docs/examples/fdisk.md | 2 + docs/examples/flexible_array.md | 2 + docs/examples/who.md | 2 + mkdocs.yml | 8 +- 10 files changed, 135 insertions(+), 148 deletions(-) create mode 100644 docs/CODE_OF_CONDUCT.md create mode 100644 docs/api/c_parser.md create mode 100644 docs/changelog.md diff --git a/README.md b/README.md index 28934a5..cb6dee4 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,8 @@ C-style structs for Python Convert C struct/union definitions into Python classes with methods for serializing/deserializing. -The usage is very simple: create a class subclassing cstruct.MemCStruct +The usage is very simple: create a class subclassing +[`cstruct.MemCStruct`](https://python-cstruct.readthedocs.io/en/latest/api/mem_cstruct/) and add a C struct/union definition as a string in the `__def__` field. The C struct/union definition is parsed at runtime and the struct format string @@ -22,8 +23,6 @@ is generated. The class offers the method `unpack` for deserializing an array of bytes into a Python object and the method `pack` for serializing the values into an array of bytes. -[Api Documentation](https://python-cstruct.readthedocs.io/en/latest/) - Install ------- @@ -31,12 +30,20 @@ Install pip install cstruct ``` +Examples +-------- + +* [Read the DOS-type (MBR) partition table](https://python-cstruct.readthedocs.io/en/latest/examples/fdisk/) +* [Print information about logged uses](https://python-cstruct.readthedocs.io/en/latest/examples/who/) +* [Flexible Array Member (FAM)](https://python-cstruct.readthedocs.io/en/latest/examples/flexible_array/) + + Features -------- ### Structs -Struct definition subclassing `cstruct.MemCStruct` +Struct definition subclassing `cstruct.MemCStruct`. Methods can access stuct values as instance variables. ```python class Position(cstruct.MemCStruct): @@ -47,18 +54,21 @@ class Position(cstruct.MemCStruct): unsigned char cyl; } """ + @property + def lba(self): + return (self.cyl * 16 + self.head) * 63 + (self.sector - 1) -pos = Position(head=10, sector=20, cyl=30) -print(f"head: {pos.head} sector: {pos.sector} cyl: {pos.cyl}") +pos = Position(cyl=15, head=15, sector=63) +print(f"head: {pos.head} sector: {pos.sector} cyl: {pos.cyl} lba: {pos.lba}") ``` -Struct definition using `cstruct.parse` +Struct definition using `cstruct.parse`. ```python Partition = cstruct.parse(""" - struct { - #define ACTIVE_FLAG 0x80 + #define ACTIVE_FLAG 0x80 + struct Partition { unsigned char status; /* 0x80 - active */ struct Position start; unsigned char partition_type; @@ -74,7 +84,7 @@ part.status = cstruct.getdef('ACTIVE_FLAG') ### Unions -Union definition subclassing `cstruct.MemCStruct` +Union definition subclassing `cstruct.MemCStruct`. ```python class Data(cstruct.MemCStruct): @@ -93,7 +103,7 @@ assert data.integer != 2 ### Enums -Enum definition subclassing `cstruct.CEnum` +Enum definition subclassing `cstruct.CEnum`. ```python class HtmlFont(cstruct.CEnum): @@ -113,7 +123,7 @@ assert HtmlFont.HTMLFONT_BOLD == 1 assert HtmlFont.HTMLFONT_ITALIC == 2 ``` -Different supported `__def__` styles: +Different enum styles are supported in struct/union definitions. ```c enum Type_A a; // externally defined using CEnum @@ -121,28 +131,9 @@ enum Type_B {A, B, C} b; enum {A, B, C} c; ``` -```python -class Type_A(cstruct.CEnum): - __size__ = 2 - __enum__ = """ - #define SOME_DEFINE 7 - - A, - B, - C = 5, - D, - E = 7 + SOME_DEFINE - """ - -# this is a nice gimmick that works, but wasn't really planned to be supported -class Type_C(cstruct.CEnum): - A = 0, - B = 1, - C = 2, - D = 3 -``` +### Nested structs/unions -### Nested structures (named/anonymous) +Nested stucts and unions are supported, both named and anonymous. ```python class Packet(cstruct.MemCStruct): @@ -204,28 +195,33 @@ pkg.data = [10, 20, 30, 40] ### Pack and Unpack +A code example illustrating how to use +[`pack`](https://python-cstruct.readthedocs.io/en/latest/api/abstract/#cstruct.abstract.AbstractCStruct.pack) to pack a structure into binary form. + ```python -class StructWithEnum(cstruct.MemCStruct): +class Position(cstruct.MemCStruct): __byte_order__ = cstruct.LITTLE_ENDIAN __def__ = """ - struct StructWithEnum { - enum HtmlFont font; - unsigned int font_size; + struct { + unsigned char head; + unsigned char sector; + unsigned char cyl; } """ -# Pack -s.font = HtmlFont.HTMLFONT_NONE -s.font_size = 20 -assert s.font == HtmlFont.HTMLFONT_NONE -assert s.font_size == 20 -packed = s.pack() - -# Unpack -s1 = StructWithEnum() -s1.unpack(packed) -assert s1.font == HtmlFont.HTMLFONT_NONE -assert s1.font_size == 20 +pos = Position(head=10, sector=20, cyl=3) +packed = pos.pack() +``` + +Binary representation can be converted into structure using +[`unpack`](https://python-cstruct.readthedocs.io/en/latest/api/abstract/#cstruct.abstract.AbstractCStruct.unpack). + +``` +pos1 = Position() +pos1.unpack(packed) +assert pos1.head == 10 +assert pos1.sector == 20 +assert pos1.cyl == 3 ``` ### Define, Sizeof, and Eval @@ -262,7 +258,7 @@ Get structure size: cstruct.sizeof(Partition) ``` -Evaluate C expression: +Evaluate C expression using [`c_eval`](https://python-cstruct.readthedocs.io/en/latest/api/c_expr/): ```python cstruct.c_eval("A1 / 10") @@ -294,90 +290,47 @@ class MBR(cstruct.MemCStruct): """ ``` -Example -------- +### Ispect memory -The following program reads the DOS partition information from a disk. +The [`inspect`](https://python-cstruct.readthedocs.io/en/latest/api/abstract/#cstruct.abstract.AbstractCStruct.inspect) methods displays memory contents in hexadecimal. ```python -#!/usr/bin/env python -import cstruct - -class Position(cstruct.MemCStruct): - __byte_order__ = cstruct.LITTLE_ENDIAN - __def__ = """ - struct { - unsigned char head; - unsigned char sector; - unsigned char cyl; - } - """ - - -class Partition(cstruct.MemCStruct): - __byte_order__ = cstruct.LITTLE_ENDIAN - __def__ = """ - #define ACTIVE_FLAG 0x80 - - typedef struct Position Position; - - struct { - unsigned char status; /* 0x80 - active */ - Position start; - unsigned char partition_type; - Position end; - unsigned int start_sect; /* starting sector counting from 0 */ - unsigned int sectors; /* nr of sectors in partition */ - } - """ - - def print_info(self): - print(f"bootable: {'Y' if self.status & cstruct.getdef('ACTIVE_FLAG') else 'N'}") - print(f"partition_type: {self.partition_type:02X}") - print(f"start: head: {self.start.head:X} sectory: {self.start.sector:X} cyl: {self.start.cyl:X}") - print(f"end: head: {self.end.head:X} sectory: {self.end.sector:X} cyl: {self.end.cyl:X}") - print(f"starting sector: {self.start_sect:08x}") - print(f"size MB: {self.sectors / 2 / 1024}") - - -class MBR(cstruct.MemCStruct): - __byte_order__ = cstruct.LITTLE_ENDIAN - __def__ = """ - #define MBR_SIZE 512 - #define MBR_DISK_SIGNATURE_SIZE 4 - #define MBR_USUALY_NULLS_SIZE 2 - #define MBR_SIGNATURE_SIZE 2 - #define MBR_BOOT_SIGNATURE 0xaa55 - #define MBR_PARTITIONS_NUM 4 - #define MBR_PARTITIONS_SIZE (sizeof(Partition) * MBR_PARTITIONS_NUM) - #define MBR_UNUSED_SIZE (MBR_SIZE - MBR_DISK_SIGNATURE_SIZE - MBR_USUALY_NULLS_SIZE - MBR_PARTITIONS_SIZE - MBR_SIGNATURE_SIZE) - - typedef struct Partition Partition; - - struct { - char unused[MBR_UNUSED_SIZE]; - unsigned char disk_signature[MBR_DISK_SIGNATURE_SIZE]; - unsigned char usualy_nulls[MBR_USUALY_NULLS_SIZE]; - Partition partitions[MBR_PARTITIONS_NUM]; - uint16 signature; - } - """ +print(mbr.inspect()) +``` - @property - def disk_signature_str(self): - return "".join(reversed([f"{x:02x}" for x in self.disk_signature])) - - def print_info(self): - print(f"disk signature: {self.disk_signature_str}") - for i, partition in enumerate(self.partitions): - print("") - print(f"partition: {i}") - partition.print_info() - -disk = "mbr" -with open(disk, "rb") as f: - mbr = MBR() - mbr.unpack(f) - mbr.print_info() +Output example: +``` +00000000 eb 48 90 00 00 00 00 00 00 00 00 00 00 00 00 00 |.H..............| +00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03 02 |................| +00000040 ff 00 00 80 61 cb 04 00 00 08 fa 80 ca 80 ea 53 |....a..........S| +00000050 7c 00 00 31 c0 8e d8 8e d0 bc 00 20 fb a0 40 7c ||..1....... ..@|| +00000060 3c ff 74 02 88 c2 52 be 79 7d e8 34 01 f6 c2 80 |<.t...R.y}.4....| +00000070 74 54 b4 41 bb aa 55 cd 13 5a 52 72 49 81 fb 55 |tT.A..U..ZRrI..U| +00000080 aa 75 43 a0 41 7c 84 c0 75 05 83 e1 01 74 37 66 |.uC.A|..u....t7f| +00000090 8b 4c 10 be 05 7c c6 44 ff 01 66 8b 1e 44 7c c7 |.L...|.D..f..D|.| +000000a0 04 10 00 c7 44 02 01 00 66 89 5c 08 c7 44 06 00 |....D...f.\..D..| +000000b0 70 66 31 c0 89 44 04 66 89 44 0c b4 42 cd 13 72 |pf1..D.f.D..B..r| +000000c0 05 bb 00 70 eb 7d b4 08 cd 13 73 0a f6 c2 80 0f |...p.}....s.....| +000000d0 84 f0 00 e9 8d 00 be 05 7c c6 44 ff 00 66 31 c0 |........|.D..f1.| +000000e0 88 f0 40 66 89 44 04 31 d2 88 ca c1 e2 02 88 e8 |..@f.D.1........| +000000f0 88 f4 40 89 44 08 31 c0 88 d0 c0 e8 02 66 89 04 |..@.D.1......f..| +00000100 66 a1 44 7c 66 31 d2 66 f7 34 88 54 0a 66 31 d2 |f.D|f1.f.4.T.f1.| +00000110 66 f7 74 04 88 54 0b 89 44 0c 3b 44 08 7d 3c 8a |f.t..T..D.;D.}<.| +00000120 54 0d c0 e2 06 8a 4c 0a fe c1 08 d1 8a 6c 0c 5a |T.....L......l.Z| +00000130 8a 74 0b bb 00 70 8e c3 31 db b8 01 02 cd 13 72 |.t...p..1......r| +00000140 2a 8c c3 8e 06 48 7c 60 1e b9 00 01 8e db 31 f6 |*....H|`......1.| +00000150 31 ff fc f3 a5 1f 61 ff 26 42 7c be 7f 7d e8 40 |1.....a.&B|..}.@| +00000160 00 eb 0e be 84 7d e8 38 00 eb 06 be 8e 7d e8 30 |.....}.8.....}.0| +00000170 00 be 93 7d e8 2a 00 eb fe 47 52 55 42 20 00 47 |...}.*...GRUB .G| +00000180 65 6f 6d 00 48 61 72 64 20 44 69 73 6b 00 52 65 |eom.Hard Disk.Re| +00000190 61 64 00 20 45 72 72 6f 72 00 bb 01 00 b4 0e cd |ad. Error.......| +000001a0 10 ac 3c 00 75 f4 c3 00 00 00 00 00 00 00 00 00 |..<.u...........| +000001b0 00 00 00 00 00 00 00 00 40 e2 01 00 00 00 80 00 |........@.......| +000001c0 02 00 83 fe 3f 86 01 00 00 00 c6 17 21 00 00 00 |....?.......!...| +000001d0 01 87 8e fe ff ff c7 17 21 00 4d d3 de 00 00 00 |........!.M.....| +000001e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +000001f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa |..............U.| ``` diff --git a/changelog.txt b/changelog.txt index 65b3659..5a46494 100644 --- a/changelog.txt +++ b/changelog.txt @@ -46,7 +46,7 @@ - compatibiliy fix -### 1.6 +## 1.6 2017-12-12 @@ -54,7 +54,7 @@ - fixed size of 64-bit integers, they now have 64 bits, not 32 -### 1.7 +## 1.7 2018-03-14 @@ -62,7 +62,7 @@ - add support for // comments -### 1.8 +## 1.8 2018-10-30 @@ -75,7 +75,7 @@ - fix Python 2.5 support in main module - examples fix -### 1.9 +## 1.9 2019-07-09 @@ -88,7 +88,7 @@ - flexible array parsing - union initial support -### 2.0 +## 2.0 2020-04-11 @@ -96,7 +96,7 @@ - drop Python 2 support -### 2.1 +## 2.1 2020-10-09 @@ -106,7 +106,7 @@ - Python 3.9 support - Github workfows -### 2.2 +## 2.2 2022-08-23 @@ -120,7 +120,7 @@ - pytest - black code style -### 2.3 +## 2.3 2022-09-01 @@ -128,7 +128,7 @@ - Fix compare with None -### 3.0 +## 3.0 2022-09-05 @@ -136,7 +136,7 @@ - Flexible array support -### 3.1 +## 3.1 2022-09-13 @@ -144,7 +144,7 @@ - Make CStruct/MemCStruct Pickle Friendly -### 3.2 +## 3.2 2022-10-23 @@ -152,7 +152,7 @@ - Fix padding tests on 32bit architectures -### 3.3 +## 3.3 2022-10-24 @@ -164,7 +164,7 @@ - Fix padding tests on 32bit architectures -### 4.0 +## 4.0 2022-11-01 diff --git a/cstruct/c_parser.py b/cstruct/c_parser.py index e578acc..aaa453a 100644 --- a/cstruct/c_parser.py +++ b/cstruct/c_parser.py @@ -31,7 +31,7 @@ from .exceptions import CStructException, ParserError if TYPE_CHECKING: - from .abstract import AbstractCStruct + from .abstract import AbstractCStruct, AbstractCEnum __all__ = ['parse_struct', 'parse_struct_def', 'parse_enum_def', 'Tokens'] @@ -126,7 +126,7 @@ def parse_type(tokens: Tokens, __cls__: Type['AbstractCStruct'], byte_order: Opt if tokens.get() == '{': # Named nested struct tokens.push(tail) tokens.push(c_type) - ref = __cls__.parse(tokens, __name__=tail, __byte_order__=byte_order) + ref: Union[Type[AbstractCEnum], Type[AbstractCStruct]] = __cls__.parse(tokens, __name__=tail, __byte_order__=byte_order) elif tail == '{': # Unnamed nested struct tokens.push(tail) tokens.push(c_type) @@ -231,6 +231,15 @@ def parse_enum_def(__def__: Union[str, Tokens], **kargs: Any) -> Optional[Dict[s def parse_enum(__enum__: Union[str, Tokens], **kargs: Any) -> Optional[Dict[str, Any]]: + """ + Parser for C-like enum syntax. + + Args: + __enum__: definition of the enum in C syntax + + Returns: + dict: the parsed definition + """ constants: Dict[str, int] = OrderedDict() if isinstance(__enum__, Tokens): @@ -295,6 +304,18 @@ def parse_struct( __byte_order__: Optional[str] = None, **kargs: Any, ) -> Dict[str, Any]: + """ + Parser for C-like struct syntax. + + Args: + __struct__: definition of the struct/union in C syntax + __cls__: base class (MemCStruct or CStruct) + __is_union__: True for union, False for struct + __byte_order__: byte order, valid values are LITTLE_ENDIAN, BIG_ENDIAN, NATIVE_ORDER + + Returns: + dict: the parsed definition + """ # naive C struct parsing __is_union__ = bool(__is_union__) fields_types: Dict[str, FieldType] = OrderedDict() diff --git a/docs/CODE_OF_CONDUCT.md b/docs/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..97b4f23 --- /dev/null +++ b/docs/CODE_OF_CONDUCT.md @@ -0,0 +1 @@ +{!CODE_OF_CONDUCT.md!} diff --git a/docs/api/c_parser.md b/docs/api/c_parser.md new file mode 100644 index 0000000..1428994 --- /dev/null +++ b/docs/api/c_parser.md @@ -0,0 +1 @@ +::: cstruct.c_parser diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 0000000..85c3845 --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1 @@ +{!changelog.txt!} diff --git a/docs/examples/fdisk.md b/docs/examples/fdisk.md index 1680788..5682f42 100644 --- a/docs/examples/fdisk.md +++ b/docs/examples/fdisk.md @@ -1,3 +1,5 @@ +The following program reads the DOS-type (MBR) partition table from a disk. + ``` {!examples/fdisk.py!} ``` diff --git a/docs/examples/flexible_array.md b/docs/examples/flexible_array.md index ea76b6e..041e1cd 100644 --- a/docs/examples/flexible_array.md +++ b/docs/examples/flexible_array.md @@ -1,3 +1,5 @@ +[Flexible Array Member (FAM)](https://en.wikipedia.org/wiki/Flexible_array_member) example. + ``` {!examples/flexible_array.py!} ``` diff --git a/docs/examples/who.md b/docs/examples/who.md index b4521ab..a2289e8 100644 --- a/docs/examples/who.md +++ b/docs/examples/who.md @@ -1,3 +1,5 @@ +The following program prints information about users who are currently logged in. + ``` {!examples/who.py!} ``` diff --git a/mkdocs.yml b/mkdocs.yml index 01fb098..45441d3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -14,7 +14,11 @@ plugins: - examples nav: - - CStruct Docs: index.md + - Introduction: index.md + - Changelog: changelog.md + - License: license.md + - Code of Conduct: CODE_OF_CONDUCT.md + - Source Code Repository: "https://github.com/andreax79/python-cstruct" - Examples: - "fdisk.py": examples/fdisk.md - "flexible_array.py": examples/flexible_array.md @@ -24,10 +28,10 @@ nav: - "cstruct.abstract": api/abstract.md - "cstruct.base": api/base.md - "cstruct.c_expr": api/c_expr.md + - "cstruct.c_parser": api/c_parser.md - "cstruct.cstruct": api/cstruct.md - "cstruct.field": api/field.md - "cstruct.mem_cstruct": api/mem_cstruct.md - - License: license.md markdown_extensions: - markdown_include.include: From 2d27fd3030fb88cc3cb44cfa30a8eb6f5a72dbaf Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Thu, 10 Nov 2022 07:19:19 +0000 Subject: [PATCH 58/95] Add support for multiple definition to cstruct.parse --- cstruct/__init__.py | 14 +++---- cstruct/abstract.py | 4 +- cstruct/c_parser.py | 91 ++++++++++++++++++++++++++++++++------------- 3 files changed, 74 insertions(+), 35 deletions(-) diff --git a/cstruct/__init__.py b/cstruct/__init__.py index 7015ae3..94ffac8 100644 --- a/cstruct/__init__.py +++ b/cstruct/__init__.py @@ -134,28 +134,24 @@ def sizeof(type_: str) -> int: def parse( - __struct__: str, __cls__: Optional[Type[AbstractCStruct]] = None, __name__: Optional[str] = None, **kargs: Dict[str, Any] + __struct__: str, __cls__: Optional[Type[AbstractCStruct]] = None, **kargs: Dict[str, Any] ) -> Union[Type[AbstractCStruct], Type[AbstractCEnum], None]: """ Return a new class mapping a C struct/union/enum definition. If the string does not contains any definition, return None. + Args: __struct__ (str): definition of the struct (or union/enum) in C syntax __cls__ (type): super class - CStruct(default) or MemCStruct - __name__ (str): name of the new class. If empty, a name based on the __struct__ hash is generated __byte_order__ (str): byte order, valid values are LITTLE_ENDIAN, BIG_ENDIAN, NATIVE_ORDER - __is_union__ (bool): True for union, False for struct (default) Returns: cls: __cls__ subclass """ if __cls__ is None: - __cls__ = CStruct - cls_def = parse_struct_def(__struct__, __cls__=__cls__, **kargs) + __cls__ = MemCStruct + cls_def = parse_struct_def(__struct__, __cls__=__cls__, process_muliple_definition=True, **kargs) if cls_def is None: return None - elif cls_def['__is_enum__']: - return AbstractCEnum.parse(cls_def, __name__, **kargs) - else: - return __cls__.parse(cls_def, __name__, **kargs) + return cls_def['__cls__'].parse(cls_def, **kargs) diff --git a/cstruct/abstract.py b/cstruct/abstract.py index 9667ffe..b1f4d6e 100644 --- a/cstruct/abstract.py +++ b/cstruct/abstract.py @@ -35,7 +35,7 @@ from .field import calculate_padding, FieldType from .exceptions import CStructException -__all__ = ['CStructMeta', 'AbstractCStruct', "CEnumMeta", 'AbstractCEnum'] +__all__ = ['CStructMeta', 'AbstractCStruct', 'CEnumMeta', 'AbstractCEnum'] class CStructMeta(ABCMeta): @@ -138,6 +138,7 @@ def parse( del cls_kargs['__struct__'] cls_kargs.update(__struct__) cls_kargs['__struct__'] = None + __name__ = cls_kargs.get('__name__') or __name__ if __name__ is None: # Anonymous struct __name__ = cls.__name__ + '_' + hashlib.sha1(str(__struct__).encode('utf-8')).hexdigest() cls_kargs['__anonymous__'] = True @@ -363,6 +364,7 @@ def parse( elif isinstance(__enum__, dict): cls_kargs.update(__enum__) + __name__ = cls_kargs.get('__name__') or __name__ if __name__ is None: __name__ = cls.__name__ + "_" + hashlib.sha1(str(__enum__).encode("utf-8")).hexdigest() cls_kargs["__anonymous__"] = True diff --git a/cstruct/c_parser.py b/cstruct/c_parser.py index aaa453a..e9b4591 100644 --- a/cstruct/c_parser.py +++ b/cstruct/c_parser.py @@ -180,6 +180,7 @@ def parse_struct_def( __def__: Union[str, Tokens], __cls__: Type['AbstractCStruct'], __byte_order__: Optional[str] = None, + process_muliple_definition: bool = False, **kargs: Any, # Type['AbstractCStruct'], ) -> Optional[Dict[str, Any]]: # naive C struct parsing @@ -187,25 +188,47 @@ def parse_struct_def( tokens = __def__ else: tokens = Tokens(__def__) - if not tokens: - return None - kind = tokens.pop() - if kind == 'typedef': - parse_typedef(tokens, __cls__, __byte_order__) - return parse_struct_def(tokens, __cls__, __byte_order__, **kargs) - if kind == 'enum': - return parse_enum_def(__def__, **kargs) - if kind not in ['struct', 'union']: - raise ParserError(f"struct, union, or enum expected - {kind}") - __is_union__ = kind == 'union' - vtype = tokens.pop() - if tokens.get() == '{': # Named nested struct - tokens.pop() - return parse_struct(tokens, __cls__=__cls__, __is_union__=__is_union__, __byte_order__=__byte_order__) - elif vtype == '{': # Unnamed nested struct - return parse_struct(tokens, __cls__=__cls__, __is_union__=__is_union__, __byte_order__=__byte_order__) - else: - raise ParserError(f"{vtype} definition expected") + result = None + while tokens and (process_muliple_definition or not result): + kind = tokens.pop() + if kind == ';': + pass + + elif kind == 'typedef': + if result: + result['__cls__'].parse(result, **kargs) + parse_typedef(tokens, __cls__, __byte_order__) + + elif kind == 'enum': + if result: + result['__cls__'].parse(result, **kargs) + name = tokens.pop() + if tokens.get() == '{': # named enum + tokens.pop() # pop "{" + result = parse_enum(tokens, __name__=name) + elif name == '{': # unnamed enum + result = parse_enum(tokens) + else: + raise ParserError(f"{name} definition expected") + + elif kind in ['struct', 'union']: + if result: + result['__cls__'].parse(result, **kargs) + __is_union__ = kind == 'union' + name = tokens.pop() + if name == '{': # unnamed nested struct + result = parse_struct(tokens, __cls__=__cls__, __is_union__=__is_union__, __byte_order__=__byte_order__) + elif tokens.get() == '{': # Named nested struct + tokens.pop() # pop "{" + result = parse_struct( + tokens, __cls__=__cls__, __is_union__=__is_union__, __byte_order__=__byte_order__, __name__=name + ) + else: + raise ParserError(f"{name} definition expected") + + else: + raise ParserError(f"struct, union, or enum expected - {kind}") + return result def parse_enum_def(__def__: Union[str, Tokens], **kargs: Any) -> Optional[Dict[str, Any]]: @@ -220,26 +243,33 @@ def parse_enum_def(__def__: Union[str, Tokens], **kargs: Any) -> Optional[Dict[s if kind not in ['enum']: raise ParserError(f"enum expected - {kind}") - vtype = tokens.pop() + name = tokens.pop() if tokens.get() == '{': # named enum - tokens.pop() - return parse_enum(tokens) - elif vtype == '{': + tokens.pop() # pop "{" + return parse_enum(tokens, __name__=name) + elif name == '{': # unnamed enum return parse_enum(tokens) else: - raise ParserError(f"{vtype} definition expected") + raise ParserError(f"{name} definition expected") -def parse_enum(__enum__: Union[str, Tokens], **kargs: Any) -> Optional[Dict[str, Any]]: +def parse_enum( + __enum__: Union[str, Tokens], + __name__: Optional[str] = None, + **kargs: Any, +) -> Optional[Dict[str, Any]]: """ Parser for C-like enum syntax. Args: __enum__: definition of the enum in C syntax + __name__: enum name Returns: dict: the parsed definition """ + from .cenum import CEnum + constants: Dict[str, int] = OrderedDict() if isinstance(__enum__, Tokens): @@ -293,6 +323,8 @@ def parse_enum(__enum__: Union[str, Tokens], **kargs: Any) -> Optional[Dict[str, '__is_struct__': False, '__is_union__': False, '__is_enum__': True, + '__name__': __name__, + '__cls__': CEnum, } return result @@ -302,6 +334,7 @@ def parse_struct( __cls__: Type['AbstractCStruct'], __is_union__: bool = False, __byte_order__: Optional[str] = None, + __name__: Optional[str] = None, **kargs: Any, ) -> Dict[str, Any]: """ @@ -312,11 +345,17 @@ def parse_struct( __cls__: base class (MemCStruct or CStruct) __is_union__: True for union, False for struct __byte_order__: byte order, valid values are LITTLE_ENDIAN, BIG_ENDIAN, NATIVE_ORDER + __name__: struct/union name Returns: dict: the parsed definition """ # naive C struct parsing + from .abstract import AbstractCStruct + from .mem_cstruct import MemCStruct + + if __cls__ is None or __cls__ == AbstractCStruct: + __cls__ = MemCStruct __is_union__ = bool(__is_union__) fields_types: Dict[str, FieldType] = OrderedDict() flexible_array: bool = False @@ -378,5 +417,7 @@ def parse_struct( '__is_enum__': False, '__byte_order__': __byte_order__, '__alignment__': max_alignment, + '__name__': __name__, + '__cls__': __cls__, } return result From b2e37ed4776047cea5614c2bde8d41d990ef08fc Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Fri, 11 Nov 2022 07:03:41 +0000 Subject: [PATCH 59/95] cstruct.native_types and get_type added --- cstruct/__init__.py | 64 ++++++++--- cstruct/base.py | 28 +---- cstruct/field.py | 9 +- cstruct/native_types.py | 224 +++++++++++++++++++++++++++++++++++++++ docs/api/native_types.md | 1 + mkdocs.yml | 1 + tests/test_cstruct.py | 1 + tests/test_get_type.py | 82 ++++++++++++++ tests/test_memcstruct.py | 1 + tests/test_typdef.py | 3 + 10 files changed, 367 insertions(+), 47 deletions(-) create mode 100644 cstruct/native_types.py create mode 100644 docs/api/native_types.md create mode 100644 tests/test_get_type.py diff --git a/cstruct/__init__.py b/cstruct/__init__.py index 94ffac8..8af6fcc 100644 --- a/cstruct/__init__.py +++ b/cstruct/__init__.py @@ -27,16 +27,15 @@ __version__ = '4.0' __date__ = '15 August 2013' -import struct from typing import Any, Dict, Optional, Type, Union from .base import ( LITTLE_ENDIAN, BIG_ENDIAN, NATIVE_ORDER, STRUCTS, + ENUMS, DEFINES, TYPEDEFS, - C_TYPE_TO_FORMAT, CHAR_ZERO, ) from .abstract import CStructMeta, AbstractCStruct, AbstractCEnum @@ -44,6 +43,7 @@ from .c_parser import parse_struct_def from .mem_cstruct import MemCStruct from .cenum import CEnum +from .native_types import get_native_type __all__ = [ 'LITTLE_ENDIAN', @@ -57,6 +57,7 @@ 'undef', 'getdef', 'typedef', + 'get_type', 'sizeof', 'parse', ] @@ -104,33 +105,62 @@ def typedef(type_: str, alias: str) -> None: TYPEDEFS[alias] = type_ -def sizeof(type_: str) -> int: +def get_type(type_: str) -> Any: """ - Return the size of the type. + Get a data type (struct, union, enum) by name + + Examples: + >>> get_type("struct Position") + + >>> get_type("enum htmlfont") + Args: - type_: C type, struct or union (e.g. 'short int' or 'struct ZYZ') + type_: C type, struct or union (e.g. 'short int' or 'struct ZYZ'), enum or native type Returns: - size: size in bytes + class: data type class """ while type_ in TYPEDEFS: type_ = TYPEDEFS[type_] if isinstance(type_, CStructMeta): - return len(type_) + return type_ elif type_.startswith('struct ') or type_.startswith('union '): kind, type_ = type_.split(' ', 1) - t = STRUCTS.get(type_, None) - if t is None: - raise KeyError("Unknow %s \"%s\"" % (kind, type_)) - else: - return t.sizeof() + try: + return STRUCTS[type_] + except KeyError: + raise KeyError(f"Unknown {kind} `{type_}`") + elif type_.startswith('enum '): + kind, type_ = type_.split(' ', 1) + try: + return ENUMS[type_] + except KeyError: + raise KeyError(f"Unknown {kind} `{type_}`") else: - ttype = C_TYPE_TO_FORMAT.get(type_, None) - if ttype is None: - raise KeyError("Unknow type \"" + type_ + "\"") - else: - return struct.calcsize(ttype) + return get_native_type(type_) + + +def sizeof(type_: str) -> int: + """ + Return the size of the type. + + Examples: + >>> sizeof("struct Position") + 16 + >>> sizeof("int") + 4 + + Args: + type_: C type, struct or union (e.g. 'short int' or 'struct ZYZ'), enum or native type + + Returns: + size: size in bytes + """ + while type_ in TYPEDEFS: + type_ = TYPEDEFS[type_] + data_type = get_type(type_) + return data_type.sizeof() def parse( diff --git a/cstruct/base.py b/cstruct/base.py index 96f2496..d764c85 100644 --- a/cstruct/base.py +++ b/cstruct/base.py @@ -35,8 +35,8 @@ 'STRUCTS', 'DEFINES', 'TYPEDEFS', - 'C_TYPE_TO_FORMAT', 'CHAR_ZERO', + 'DEFAULT_ENUM_SIZE', ] LITTLE_ENDIAN = '<' @@ -68,32 +68,8 @@ 'uint64_t': 'uint64', } -C_TYPE_TO_FORMAT: Dict[str, str] = { - 'char': 's', - 'signed char': 'b', - 'unsigned char': 'B', - 'short': 'h', - 'unsigned short': 'H', - 'int': 'i', - 'unsigned int': 'I', - 'long': 'l', - 'unsigned long': 'L', - 'long long': 'q', - 'unsigned long long': 'Q', - 'float': 'f', - 'double': 'd', - 'void *': 'P', - 'int8': 'b', - 'uint8': 'B', - 'int16': 'h', - 'uint16': 'H', - 'int32': 'i', - 'uint32': 'I', - 'int64': 'q', - 'uint64': 'Q', -} - ENUM_SIZE_TO_C_TYPE: Dict[int, str] = {1: 'int8', 2: 'int16', 4: 'int32', 8: 'int64'} CHAR_ZERO = bytes('\0', 'ascii') + DEFAULT_ENUM_SIZE = 4 diff --git a/cstruct/field.py b/cstruct/field.py index 08c5bc0..1533a2a 100644 --- a/cstruct/field.py +++ b/cstruct/field.py @@ -26,7 +26,8 @@ import struct from enum import Enum from typing import Optional, Any, List, Type, TYPE_CHECKING -from .base import NATIVE_ORDER, C_TYPE_TO_FORMAT, ENUM_SIZE_TO_C_TYPE +from .base import NATIVE_ORDER, ENUM_SIZE_TO_C_TYPE +from .native_types import get_native_type from .exceptions import ParserError if TYPE_CHECKING: @@ -188,12 +189,12 @@ def native_format(self) -> str: "Field format (struct library format)" if self.is_native: try: - return C_TYPE_TO_FORMAT[self.c_type] + return get_native_type(self.c_type).native_format except KeyError: - raise ParserError("Unknow type {}".format(self.c_type)) + raise ParserError(f"Unknow type `{self.c_type}`") elif self.is_enum: try: - return C_TYPE_TO_FORMAT[ENUM_SIZE_TO_C_TYPE[self.ref.size]] + return get_native_type(ENUM_SIZE_TO_C_TYPE[self.ref.size]).native_format except KeyError: raise ParserError(f"Enum has invalid size. Needs to be in {ENUM_SIZE_TO_C_TYPE.keys()}") else: diff --git a/cstruct/native_types.py b/cstruct/native_types.py new file mode 100644 index 0000000..517930d --- /dev/null +++ b/cstruct/native_types.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python +# Copyright (c) 2013-2019 Andrea Bonomi +# +# Published under the terms of the MIT license. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. +# + +import struct +from abc import ABCMeta +from typing import Any, Dict, Type, Tuple + +__all__ = [ + "get_native_type", + "AbstractNativeType", + "Char", + "SignedChar", + "UnsignedChar", + "Short", + "UnsignedShort", + "Int", + "UnsignedInt", + "Long", + "UnsignedLong", + "LongLong", + "UnsignedLongLong", + "Float", + "Double", + "Pointer", + "Int8", + "UnsignedInt8", + "Int16", + "UnsignedInt16", + "Int32", + "UnsignedInt32", + "Int64", + "UnsignedInt64", +] + + +NATIVE_TYPES: Dict[str, "AbstractNativeType"] = {} + + +def get_native_type(type_: str) -> "AbstractNativeType": + """ + Get a base data type by name + + Args: + type_: data type + + Returns: + class: data type class + """ + try: + return NATIVE_TYPES[type_] + except KeyError: + raise KeyError(f"Unknown type `{type_}`") + + +class NativeTypeMeta(ABCMeta): + __size__: int = 0 + " Size in bytes " + type_name: str = "" + " Type name " + native_format: str = "" + " Type format " + + def __new__(metacls: Type[type], name: str, bases: Tuple[str], namespace: Dict[str, Any]) -> Type[Any]: + if namespace.get('native_format'): + native_format = namespace['native_format'] + namespace['__size__'] = struct.calcsize(native_format) + else: + native_format = None + namespace['native_format'] = None + namespace['__size__'] = None + new_class: Type[Any] = super().__new__(metacls, name, bases, namespace) + if namespace.get('type_name'): + NATIVE_TYPES[namespace['type_name']] = new_class + return new_class + + def __len__(cls) -> int: + "Type size (in bytes)" + return cls.__size__ + + @property + def size(cls) -> int: + "Type size (in bytes)" + return cls.__size__ + + +class AbstractNativeType(metaclass=NativeTypeMeta): + def __str__(self) -> str: + return self.type_name + + @classmethod + def sizeof(cls) -> int: + "Type size (in bytes)" + return cls.__size__ + + +class Char(AbstractNativeType): + type_name = "char" + native_format = "s" + + +class SignedChar(AbstractNativeType): + type_name = "signed char" + native_format = "b" + + +class UnsignedChar(AbstractNativeType): + type_name = "unsigned char" + native_format = "B" + + +class Short(AbstractNativeType): + type_name = "short" + native_format = "h" + + +class UnsignedShort(AbstractNativeType): + type_name = "unsigned short" + native_format = "H" + + +class Int(AbstractNativeType): + type_name = "int" + native_format = "i" + + +class UnsignedInt(AbstractNativeType): + type_name = "unsigned int" + native_format = "I" + + +class Long(AbstractNativeType): + type_name = "long" + native_format = "l" + + +class UnsignedLong(AbstractNativeType): + type_name = "unsigned long" + native_format = "L" + + +class LongLong(AbstractNativeType): + type_name = "long long" + native_format = "q" + + +class UnsignedLongLong(AbstractNativeType): + type_name = "unsigned long long" + native_format = "Q" + + +class Float(AbstractNativeType): + type_name = "float" + native_format = "f" + + +class Double(AbstractNativeType): + type_name = "double" + native_format = "d" + + +class Pointer(AbstractNativeType): + type_name = "void *" + native_format = "P" + + +class Int8(AbstractNativeType): + type_name = "int8" + native_format = "b" + + +class UnsignedInt8(AbstractNativeType): + type_name = "uint8" + native_format = "B" + + +class Int16(AbstractNativeType): + type_name = "int16" + native_format = "h" + + +class UnsignedInt16(AbstractNativeType): + type_name = "uint16" + native_format = "H" + + +class Int32(AbstractNativeType): + type_name = "int32" + native_format = "i" + + +class UnsignedInt32(AbstractNativeType): + type_name = "uint32" + native_format = "I" + + +class Int64(AbstractNativeType): + type_name = "int64" + native_format = "q" + + +class UnsignedInt64(AbstractNativeType): + type_name = "uint64" + native_format = "Q" diff --git a/docs/api/native_types.md b/docs/api/native_types.md new file mode 100644 index 0000000..d405a28 --- /dev/null +++ b/docs/api/native_types.md @@ -0,0 +1 @@ +::: cstruct.native_types diff --git a/mkdocs.yml b/mkdocs.yml index 45441d3..435ebbe 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -32,6 +32,7 @@ nav: - "cstruct.cstruct": api/cstruct.md - "cstruct.field": api/field.md - "cstruct.mem_cstruct": api/mem_cstruct.md + - "cstruct.native_types": api/native_types.md markdown_extensions: - markdown_include.include: diff --git a/tests/test_cstruct.py b/tests/test_cstruct.py index 66e6bca..57e9557 100644 --- a/tests/test_cstruct.py +++ b/tests/test_cstruct.py @@ -263,6 +263,7 @@ def test_invalid_inline(): with pytest.raises(ParserError): cstruct.MemCStruct.parse('struct { unsigned char head; unsigned char head; }', __byte_order__=cstruct.LITTLE_ENDIAN) + def test_invalid_inline_reserved(): with pytest.raises(ParserError): cstruct.CStruct.parse('struct { int size; }') diff --git a/tests/test_get_type.py b/tests/test_get_type.py new file mode 100644 index 0000000..72cb159 --- /dev/null +++ b/tests/test_get_type.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python +# ***************************************************************************** +# +# Copyright (c) 2013-2019 Andrea Bonomi +# +# Published under the terms of the MIT license. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. +# +# ***************************************************************************** + +import pytest +import cstruct + + +def test_get_type(): + S2 = cstruct.parse( + """ + #define ACTIVE_FLAG 0x80 + + struct S1 { + unsigned char head; + unsigned char sector; + unsigned char cyl; + }; + typedef struct S1 S1; + + union U1 { + unsigned int a; + float b; + }; + typedef union U1 U1type; + + #define NONE 0 + + enum htmlfont { + HTMLFONT_NONE = NONE, + HTMLFONT_BOLD, + HTMLFONT_ITALIC, + }; + + struct S2 { + unsigned char status; /* 0x80 - active */ + struct S1 start; + unsigned char partition_type; + S1 end; + }; + + typedef struct S2 S2type; + """ + ) + assert S2 + assert cstruct.get_type("struct S1") + assert cstruct.get_type("S1") + assert cstruct.get_type("union U1") + assert cstruct.get_type("U1type") + assert cstruct.get_type("enum htmlfont") + assert cstruct.get_type("struct S2") + assert cstruct.get_type("int") + assert cstruct.get_type("unsigned int") + assert cstruct.get_type("long long") + with pytest.raises(KeyError): + cstruct.get_type("struct X") + with pytest.raises(KeyError): + cstruct.get_type("U1") + assert cstruct.sizeof("union U1") == max(cstruct.sizeof("unsigned int"), cstruct.sizeof("float")) diff --git a/tests/test_memcstruct.py b/tests/test_memcstruct.py index 047b9cb..e5c1dca 100644 --- a/tests/test_memcstruct.py +++ b/tests/test_memcstruct.py @@ -257,6 +257,7 @@ def test_invalid_inline(): with pytest.raises(ParserError): cstruct.MemCStruct.parse('struct { unsigned char head; unsigned char head; }', __byte_order__=cstruct.LITTLE_ENDIAN) + def test_invalid_inline_reserved(): with pytest.raises(ParserError): cstruct.MemCStruct.parse('struct { int size; }') diff --git a/tests/test_typdef.py b/tests/test_typdef.py index d02eaa3..9990f8b 100644 --- a/tests/test_typdef.py +++ b/tests/test_typdef.py @@ -28,6 +28,7 @@ from cstruct import MemCStruct, NATIVE_ORDER from cstruct.base import TYPEDEFS + class ExitStatus(MemCStruct): __def__ = """ struct ExitStatus { @@ -36,6 +37,7 @@ class ExitStatus(MemCStruct): } """ + class Utmp(MemCStruct): __byte_order__ = NATIVE_ORDER __def__ = """ @@ -66,6 +68,7 @@ class Utmp(MemCStruct): } """ + def test_typedef(): assert TYPEDEFS['pid_t'] == 'int' assert TYPEDEFS['time_t'] == 'long' From c68c9a4eb8daca5321c8bf94b51a89a7830a6b30 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Fri, 11 Nov 2022 08:31:36 +0000 Subject: [PATCH 60/95] sizeof enum --- cstruct/cenum.py | 5 ++++- tests/test_cenum.py | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/cstruct/cenum.py b/cstruct/cenum.py index 7929f03..83bfedc 100644 --- a/cstruct/cenum.py +++ b/cstruct/cenum.py @@ -2,4 +2,7 @@ class CEnum(AbstractCEnum): - ... + @classmethod + def sizeof(cls) -> int: + "Type size (in bytes)" + return cls.__size__ diff --git a/tests/test_cenum.py b/tests/test_cenum.py index 7e84c7a..37b09bb 100644 --- a/tests/test_cenum.py +++ b/tests/test_cenum.py @@ -21,7 +21,7 @@ class HtmlFont(CEnum): __def__ = """ #define NONE 0 - enum htmlfont { + enum HtmlFont { HTMLFONT_NONE = NONE, HTMLFONT_BOLD, HTMLFONT_ITALIC, @@ -87,4 +87,14 @@ def test_struct_with_enum(): s1 = StructWithEnum() s1.unpack(packed) assert s1.font == HtmlFont.HTMLFONT_NONE - assert s1.font_size == 20 + + +def test_sizeof(): + assert cstruct.sizeof("enum Dummy") == 4 + assert cstruct.sizeof("enum HtmlFont") == 2 + + +# def test_type(): +# color = cstruct.parse("enum Color : short { red, green, blue };") +# assert color.__size__ == 2 +# assert cstruct.sizeof("enum Color") == 2 From 7963a3ba56fdfc312d30640cea71f889680f1898 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Fri, 11 Nov 2022 17:58:44 +0000 Subject: [PATCH 61/95] support enum with explicit type (C++11) --- cstruct/__init__.py | 1 + cstruct/abstract.py | 23 ++++++++++++++++++----- cstruct/c_parser.py | 31 +++++++++++++++++++++++++++---- cstruct/field.py | 7 ++----- tests/test_cenum.py | 14 ++++++++++---- 5 files changed, 58 insertions(+), 18 deletions(-) diff --git a/cstruct/__init__.py b/cstruct/__init__.py index 8af6fcc..2b21464 100644 --- a/cstruct/__init__.py +++ b/cstruct/__init__.py @@ -182,6 +182,7 @@ def parse( if __cls__ is None: __cls__ = MemCStruct cls_def = parse_struct_def(__struct__, __cls__=__cls__, process_muliple_definition=True, **kargs) + print('!!!!!', cls_def, kargs) if cls_def is None: return None return cls_def['__cls__'].parse(cls_def, **kargs) diff --git a/cstruct/abstract.py b/cstruct/abstract.py index b1f4d6e..6bdd5b0 100644 --- a/cstruct/abstract.py +++ b/cstruct/abstract.py @@ -30,10 +30,12 @@ import hashlib from io import StringIO from enum import IntEnum, EnumMeta, _EnumDict -from .base import STRUCTS, ENUMS, DEFAULT_ENUM_SIZE +import struct +from .base import STRUCTS, ENUMS, DEFAULT_ENUM_SIZE, ENUM_SIZE_TO_C_TYPE from .c_parser import parse_struct, parse_struct_def, parse_enum_def, parse_enum, Tokens from .field import calculate_padding, FieldType -from .exceptions import CStructException +from .native_types import get_native_type +from .exceptions import CStructException, ParserError __all__ = ['CStructMeta', 'AbstractCStruct', 'CEnumMeta', 'AbstractCEnum'] @@ -315,11 +317,19 @@ def __prepare__(metacls, cls, bases, **kwds): def __new__(metacls: Type["CEnumMeta"], cls: str, bases: Tuple[Type, ...], classdict: _EnumDict, **kwds: Any) -> "CEnumMeta": inst = super().__new__(metacls, cls, bases, classdict, **kwds) - if len(inst) > 0: - if "__size__" not in classdict: + if classdict.get("__native_format__"): # data type specified + inst.__size__ = struct.calcsize(classdict["__native_format__"]) + elif "__size__" in classdict: # size specified + try: + inst.__native_format__ = get_native_type(ENUM_SIZE_TO_C_TYPE[inst.__size__]).native_format + except KeyError: + raise ParserError(f"Enum has invalid size. Needs to be in {ENUM_SIZE_TO_C_TYPE.keys()}") + else: # default inst.__size__ = DEFAULT_ENUM_SIZE + inst.__native_format__ = get_native_type(ENUM_SIZE_TO_C_TYPE[inst.__size__]).native_format print(f"Warning: __size__ not specified for enum {cls}. Will default to {DEFAULT_ENUM_SIZE} bytes") + if not classdict.get("__anonymous__", False): ENUMS[cls] = inst return inst @@ -341,6 +351,7 @@ def parse( __enum__: Union[str, Tokens, Dict[str, Any]], __name__: Optional[str] = None, __size__: Optional[int] = None, + __native_format__: Optional[str] = None, **kargs: Dict[str, Any], ) -> Type["AbstractCEnum"]: """ @@ -350,14 +361,16 @@ def parse( __enum__: Definition of the enum in C syntax __name__: Name of the new Enum. If empty, a name based on the __enum__ hash is generated __size__: Number of bytes that the enum should be read as + __native_format__: struct module format Returns: cls: A new class mapping the definition """ - cls_kargs: Dict[str, Any] = dict(kargs) if __size__ is not None: cls_kargs['__size__'] = __size__ + if __native_format__ is not None: + cls_kargs['__native_format__'] = __native_format__ if isinstance(__enum__, (str, Tokens)): cls_kargs.update(parse_enum_def(__enum__, __cls__=cls, **cls_kargs)) diff --git a/cstruct/c_parser.py b/cstruct/c_parser.py index e9b4591..1f98b89 100644 --- a/cstruct/c_parser.py +++ b/cstruct/c_parser.py @@ -29,6 +29,7 @@ from .field import calculate_padding, Kind, FieldType from .c_expr import c_eval from .exceptions import CStructException, ParserError +from .native_types import get_native_type if TYPE_CHECKING: from .abstract import AbstractCStruct, AbstractCEnum @@ -58,6 +59,16 @@ def __init__(self, text: str) -> None: def pop(self) -> str: return self.tokens.pop(0) + def pop_c_type(self) -> str: + c_type = self.pop() + if c_type in ['signed', 'unsigned'] and len(self) > 1: + # short int, long int, or long long + c_type = c_type + " " + self.pop() + elif c_type in ['short', 'long'] and len(self) > 1 and self.get() in ['int', 'long']: + # short int, long int, or long long + c_type = c_type + " " + self.pop() + return c_type + def get(self) -> str: return self.tokens[0] @@ -203,11 +214,16 @@ def parse_struct_def( if result: result['__cls__'].parse(result, **kargs) name = tokens.pop() + native_format = None + if tokens.get() == ':': # enumeration type declaration + tokens.pop() # pop ":" + type_ = get_native_type(tokens.pop_c_type()) + native_format = type_.native_format if tokens.get() == '{': # named enum tokens.pop() # pop "{" - result = parse_enum(tokens, __name__=name) + result = parse_enum(tokens, __name__=name, native_format=native_format) elif name == '{': # unnamed enum - result = parse_enum(tokens) + result = parse_enum(tokens, native_format=native_format) else: raise ParserError(f"{name} definition expected") @@ -244,9 +260,14 @@ def parse_enum_def(__def__: Union[str, Tokens], **kargs: Any) -> Optional[Dict[s raise ParserError(f"enum expected - {kind}") name = tokens.pop() + native_format = None + if tokens.get() == ':': # enumeration type declaration + tokens.pop() # pop ":" + type_ = get_native_type(tokens.pop_c_type()) + native_format = type_.native_format if tokens.get() == '{': # named enum tokens.pop() # pop "{" - return parse_enum(tokens, __name__=name) + return parse_enum(tokens, __name__=name, native_format=native_format) elif name == '{': # unnamed enum return parse_enum(tokens) else: @@ -256,6 +277,7 @@ def parse_enum_def(__def__: Union[str, Tokens], **kargs: Any) -> Optional[Dict[s def parse_enum( __enum__: Union[str, Tokens], __name__: Optional[str] = None, + native_format: Optional[str] = None, **kargs: Any, ) -> Optional[Dict[str, Any]]: """ @@ -264,6 +286,7 @@ def parse_enum( Args: __enum__: definition of the enum in C syntax __name__: enum name + native_format: struct module format Returns: dict: the parsed definition @@ -283,7 +306,6 @@ def parse_enum( break name = tokens.pop() - next_token = tokens.pop() if next_token in {",", "}"}: # enum-constant without explicit value if len(constants) == 0: @@ -324,6 +346,7 @@ def parse_enum( '__is_union__': False, '__is_enum__': True, '__name__': __name__, + '__native_format__': native_format, '__cls__': CEnum, } return result diff --git a/cstruct/field.py b/cstruct/field.py index 1533a2a..957b1d3 100644 --- a/cstruct/field.py +++ b/cstruct/field.py @@ -26,7 +26,7 @@ import struct from enum import Enum from typing import Optional, Any, List, Type, TYPE_CHECKING -from .base import NATIVE_ORDER, ENUM_SIZE_TO_C_TYPE +from .base import NATIVE_ORDER from .native_types import get_native_type from .exceptions import ParserError @@ -193,10 +193,7 @@ def native_format(self) -> str: except KeyError: raise ParserError(f"Unknow type `{self.c_type}`") elif self.is_enum: - try: - return get_native_type(ENUM_SIZE_TO_C_TYPE[self.ref.size]).native_format - except KeyError: - raise ParserError(f"Enum has invalid size. Needs to be in {ENUM_SIZE_TO_C_TYPE.keys()}") + return self.ref.__native_format__ else: return 'c' diff --git a/tests/test_cenum.py b/tests/test_cenum.py index 37b09bb..5823a12 100644 --- a/tests/test_cenum.py +++ b/tests/test_cenum.py @@ -29,6 +29,12 @@ class HtmlFont(CEnum): """ +class EnumWithType(CEnum): + __def__ = """ + enum EnumWithType : int { a, b, c, d}; + """ + + class StructWithEnum(cstruct.MemCStruct): __byte_order__ = cstruct.LITTLE_ENDIAN __def__ = """ @@ -94,7 +100,7 @@ def test_sizeof(): assert cstruct.sizeof("enum HtmlFont") == 2 -# def test_type(): -# color = cstruct.parse("enum Color : short { red, green, blue };") -# assert color.__size__ == 2 -# assert cstruct.sizeof("enum Color") == 2 +def test_type(): + color = cstruct.parse("enum Color : unsigned short { red, green, blue };") + assert color.__size__ == 2 + assert cstruct.sizeof("enum Color") == 2 From d99e99ae803fabdda6dfd4a270ae15116179ed53 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Fri, 11 Nov 2022 18:39:20 +0000 Subject: [PATCH 62/95] tokenizer --- cstruct/c_parser.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/cstruct/c_parser.py b/cstruct/c_parser.py index 1f98b89..eb00b34 100644 --- a/cstruct/c_parser.py +++ b/cstruct/c_parser.py @@ -24,7 +24,7 @@ import re from collections import OrderedDict -from typing import Union, Optional, Any, Dict, Type, TYPE_CHECKING +from typing import Union, Optional, Any, Dict, List, Type, TYPE_CHECKING from .base import DEFINES, ENUMS, TYPEDEFS, STRUCTS from .field import calculate_padding, Kind, FieldType from .c_expr import c_eval @@ -36,6 +36,8 @@ __all__ = ['parse_struct', 'parse_struct_def', 'parse_enum_def', 'Tokens'] +SEPARATORS = [" ", "\t", "\n", ";", "{", "}", ":", ",", "="] +SPACES = [" ", "\t", "\n"] class Tokens(object): def __init__(self, text: str) -> None: @@ -52,9 +54,24 @@ def __init__(self, text: str) -> None: raise ParserError(f"Parsing line {line}") else: lines.append(line) - text = " ".join(lines) - text = text.replace(";", " ; ").replace("{", " { ").replace("}", " } ").replace(",", " , ").replace("=", " = ") - self.tokens = text.split() + text = "\n".join(lines) + self.tokens = self.tokenize(text) + + def tokenize(self, text) -> List[str]: + tokens: List[str] = [] + t: List[str] = [] + for c in text: + if c in SEPARATORS: + if t: + tokens.append("".join(t)) + t.clear() + if c not in SPACES: + tokens.append(c) + else: + t.append(c) + if t: + tokens.append(t.getvalue()) + return tokens def pop(self) -> str: return self.tokens.pop(0) From f7b037272b0f49800eb1e6f7900d57ecfefb3c3b Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Sat, 12 Nov 2022 07:52:35 +0000 Subject: [PATCH 63/95] examples --- cstruct/__init__.py | 39 ++++++++++++++++++++++++++++++++++++++- cstruct/c_parser.py | 32 ++++++++++++++++---------------- cstruct/native_types.py | 3 +++ 3 files changed, 57 insertions(+), 17 deletions(-) diff --git a/cstruct/__init__.py b/cstruct/__init__.py index 2b21464..68f168e 100644 --- a/cstruct/__init__.py +++ b/cstruct/__init__.py @@ -67,6 +67,9 @@ def define(key: str, value: Any) -> None: """ Define a constant that can be used in the C struct + Examples: + >>> define("INIT_THREAD_SIZE", 16384) + Args: key: identifier value: value of the constant @@ -78,8 +81,15 @@ def undef(key: str) -> None: """ Undefine a symbol that was previously defined with define + Examples: + >>> define("INIT_THREAD_SIZE", 16384) + >>> undef("INIT_THREAD_SIZE") + Args: key: identifier + + Raises: + KeyError: If key is not defined """ del DEFINES[key] @@ -88,8 +98,15 @@ def getdef(key: str) -> Any: """ Return the value for a constant + Examples: + >>> define("INIT_THREAD_SIZE", 16384) + >>> getdef("INIT_THREAD_SIZE") + Args: key: identifier + + Raises: + KeyError: If key is not defined """ return DEFINES[key] @@ -98,6 +115,11 @@ def typedef(type_: str, alias: str) -> None: """ Define an alias name for a data type + Examples: + >>> typedef("int", "status") + >>> sizeof("status") + 4 + Args: type_: data type alias: new alias name @@ -120,6 +142,9 @@ def get_type(type_: str) -> Any: Returns: class: data type class + + Raises: + KeyError: If type is not defined """ while type_ in TYPEDEFS: type_ = TYPEDEFS[type_] @@ -148,6 +173,8 @@ def sizeof(type_: str) -> int: Examples: >>> sizeof("struct Position") 16 + >>> sizeof('enum htmlfont') + 4 >>> sizeof("int") 4 @@ -156,6 +183,9 @@ def sizeof(type_: str) -> int: Returns: size: size in bytes + + Raises: + KeyError: If type is not defined """ while type_ in TYPEDEFS: type_ = TYPEDEFS[type_] @@ -169,7 +199,11 @@ def parse( """ Return a new class mapping a C struct/union/enum definition. If the string does not contains any definition, return None. + If the string contains multiple struct/union/enum definitions, returns the last definition. + Examples: + >>> cstruct.parse('struct Pair { unsigned char a; unsigned char b; };') + Args: __struct__ (str): definition of the struct (or union/enum) in C syntax @@ -178,11 +212,14 @@ def parse( Returns: cls: __cls__ subclass + + Raises: + cstruct.exceptions.ParserError: Parsing exception + """ if __cls__ is None: __cls__ = MemCStruct cls_def = parse_struct_def(__struct__, __cls__=__cls__, process_muliple_definition=True, **kargs) - print('!!!!!', cls_def, kargs) if cls_def is None: return None return cls_def['__cls__'].parse(cls_def, **kargs) diff --git a/cstruct/c_parser.py b/cstruct/c_parser.py index eb00b34..3f6d0a5 100644 --- a/cstruct/c_parser.py +++ b/cstruct/c_parser.py @@ -51,7 +51,7 @@ def __init__(self, text: str) -> None: _, name, value = line.strip().split(maxsplit=2) DEFINES[name] = c_eval(value) except Exception: - raise ParserError(f"Parsing line {line}") + raise ParserError(f"Parsing line `{line}`") else: lines.append(line) text = "\n".join(lines) @@ -124,7 +124,7 @@ def parse_type(tokens: Tokens, __cls__: Type['AbstractCStruct'], byte_order: Opt if "[" in next_token: t = next_token.split("[") if len(t) != 2: - raise ParserError("Error parsing: " + next_token) + raise ParserError(f"Error parsing: `{next_token}`") next_token = t[0].strip() vlen_part = t[1] vlen_expr = [] @@ -163,7 +163,7 @@ def parse_type(tokens: Tokens, __cls__: Type['AbstractCStruct'], byte_order: Opt try: ref = STRUCTS[tail] except KeyError: - raise ParserError(f"Unknown '{c_type}' '{tail}'") + raise ParserError(f"Unknown `{c_type} {tail}`") elif c_type.startswith('enum'): from .cenum import CEnum @@ -181,7 +181,7 @@ def parse_type(tokens: Tokens, __cls__: Type['AbstractCStruct'], byte_order: Opt try: ref = ENUMS[tail] except KeyError: - raise ParserError(f"Unknown '{c_type}' '{tail}'") + raise ParserError(f"Unknown `{c_type} {tail}`") else: # other types kind = Kind.NATIVE ref = None @@ -201,7 +201,7 @@ def parse_typedef(tokens: Tokens, __cls__: Type['AbstractCStruct'], byte_order: TYPEDEFS[vname] = f"struct {field_type.ref.__name__}" t = tokens.pop() if t != ';': - raise ParserError(f"; expected but {t} found") + raise ParserError(f"`;` expected but `{t}` found") def parse_struct_def( @@ -242,7 +242,7 @@ def parse_struct_def( elif name == '{': # unnamed enum result = parse_enum(tokens, native_format=native_format) else: - raise ParserError(f"{name} definition expected") + raise ParserError(f"`{name}` definition expected") elif kind in ['struct', 'union']: if result: @@ -257,10 +257,10 @@ def parse_struct_def( tokens, __cls__=__cls__, __is_union__=__is_union__, __byte_order__=__byte_order__, __name__=name ) else: - raise ParserError(f"{name} definition expected") + raise ParserError(f"`{name}` definition expected") else: - raise ParserError(f"struct, union, or enum expected - {kind}") + raise ParserError(f"struct, union, or enum expected - `{kind}` found") return result @@ -274,7 +274,7 @@ def parse_enum_def(__def__: Union[str, Tokens], **kargs: Any) -> Optional[Dict[s return None kind = tokens.pop() if kind not in ['enum']: - raise ParserError(f"enum expected - {kind}") + raise ParserError(f"enum expected - `{kind}` found") name = tokens.pop() native_format = None @@ -288,7 +288,7 @@ def parse_enum_def(__def__: Union[str, Tokens], **kargs: Any) -> Optional[Dict[s elif name == '{': # unnamed enum return parse_enum(tokens) else: - raise ParserError(f"{name} definition expected") + raise ParserError(f"`{name}` definition expected") def parse_enum( @@ -348,10 +348,10 @@ def parse_enum( except (ValueError, TypeError): value = int(int_expr) else: - raise ParserError(f"{__enum__} is not a valid enum expression") + raise ParserError(f"`{__enum__}` is not a valid enum expression") if name in constants: - raise ParserError(f"duplicate enum name {name}") + raise ParserError(f"duplicate enum name `{name}`") constants[name] = value if next_token == "}": @@ -416,15 +416,15 @@ def parse_struct( field_type = parse_type(tokens, __cls__, __byte_order__, offset) vname = tokens.pop() if vname in fields_types: - raise ParserError(f"Duplicate member '{vname}'") + raise ParserError(f"Duplicate member `{vname}`") if vname in dir(__cls__): - raise ParserError(f"Invalid reserved member name '{vname}'") + raise ParserError(f"Invalid reserved member name `{vname}`") # anonymous nested union if vname == ';' and field_type.ref is not None and (__is_union__ or field_type.ref.__is_union__): # add the anonymous struct fields to the parent for nested_field_name, nested_field_type in field_type.ref.__fields_types__.items(): if nested_field_name in fields_types: - raise ParserError(f"Duplicate member '{nested_field_name}'") + raise ParserError(f"Duplicate member `{nested_field_name}`") fields_types[nested_field_name] = nested_field_type vname = f"__anonymous{anonymous}" anonymous += 1 @@ -438,7 +438,7 @@ def parse_struct( offset = field_type.offset + field_type.vsize t = tokens.pop() if t != ';': - raise ParserError(f"; expected but {t} found") + raise ParserError(f"`;` expected but `{t}` found") if __is_union__: # C union # Calculate the sizeof union as size of its largest element diff --git a/cstruct/native_types.py b/cstruct/native_types.py index 517930d..46cf011 100644 --- a/cstruct/native_types.py +++ b/cstruct/native_types.py @@ -66,6 +66,9 @@ def get_native_type(type_: str) -> "AbstractNativeType": Returns: class: data type class + + Raises: + KeyError: If type is not defined """ try: return NATIVE_TYPES[type_] From 8d522678d60c5d1765e174e0da254e1748bc769b Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Sat, 12 Nov 2022 07:57:25 +0000 Subject: [PATCH 64/95] Black code style --- cstruct/__init__.py | 46 +++++++------- cstruct/abstract.py | 50 +++++++-------- cstruct/base.py | 54 ++++++++-------- cstruct/c_expr.py | 2 +- cstruct/c_parser.py | 119 ++++++++++++++++++----------------- cstruct/field.py | 10 +-- cstruct/mem_cstruct.py | 2 +- cstruct/native_types.py | 14 ++--- tests/test_alignment.py | 36 +++++------ tests/test_c_expr.py | 4 +- tests/test_cstruct.py | 20 +++--- tests/test_define.py | 28 ++++----- tests/test_flexible_array.py | 88 +++++++++++++------------- tests/test_memcstruct.py | 20 +++--- tests/test_nested.py | 6 +- tests/test_typdef.py | 8 +-- tests/test_union.py | 6 +- 17 files changed, 257 insertions(+), 256 deletions(-) diff --git a/cstruct/__init__.py b/cstruct/__init__.py index 68f168e..a20e843 100644 --- a/cstruct/__init__.py +++ b/cstruct/__init__.py @@ -22,10 +22,10 @@ # IN THE SOFTWARE. # -__author__ = 'Andrea Bonomi ' -__license__ = 'MIT' -__version__ = '4.0' -__date__ = '15 August 2013' +__author__ = "Andrea Bonomi " +__license__ = "MIT" +__version__ = "4.0" +__date__ = "15 August 2013" from typing import Any, Dict, Optional, Type, Union from .base import ( @@ -46,20 +46,20 @@ from .native_types import get_native_type __all__ = [ - 'LITTLE_ENDIAN', - 'BIG_ENDIAN', - 'NATIVE_ORDER', - 'CHAR_ZERO', - 'CStruct', - 'MemCStruct', - 'CEnum', - 'define', - 'undef', - 'getdef', - 'typedef', - 'get_type', - 'sizeof', - 'parse', + "LITTLE_ENDIAN", + "BIG_ENDIAN", + "NATIVE_ORDER", + "CHAR_ZERO", + "CStruct", + "MemCStruct", + "CEnum", + "define", + "undef", + "getdef", + "typedef", + "get_type", + "sizeof", + "parse", ] @@ -150,14 +150,14 @@ def get_type(type_: str) -> Any: type_ = TYPEDEFS[type_] if isinstance(type_, CStructMeta): return type_ - elif type_.startswith('struct ') or type_.startswith('union '): - kind, type_ = type_.split(' ', 1) + elif type_.startswith("struct ") or type_.startswith("union "): + kind, type_ = type_.split(" ", 1) try: return STRUCTS[type_] except KeyError: raise KeyError(f"Unknown {kind} `{type_}`") - elif type_.startswith('enum '): - kind, type_ = type_.split(' ', 1) + elif type_.startswith("enum "): + kind, type_ = type_.split(" ", 1) try: return ENUMS[type_] except KeyError: @@ -222,4 +222,4 @@ def parse( cls_def = parse_struct_def(__struct__, __cls__=__cls__, process_muliple_definition=True, **kargs) if cls_def is None: return None - return cls_def['__cls__'].parse(cls_def, **kargs) + return cls_def["__cls__"].parse(cls_def, **kargs) diff --git a/cstruct/abstract.py b/cstruct/abstract.py index 6bdd5b0..d97bca5 100644 --- a/cstruct/abstract.py +++ b/cstruct/abstract.py @@ -37,27 +37,27 @@ from .native_types import get_native_type from .exceptions import CStructException, ParserError -__all__ = ['CStructMeta', 'AbstractCStruct', 'CEnumMeta', 'AbstractCEnum'] +__all__ = ["CStructMeta", "AbstractCStruct", "CEnumMeta", "AbstractCEnum"] class CStructMeta(ABCMeta): __size__: int = 0 def __new__(metacls: Type[type], name: str, bases: Tuple[str], namespace: Dict[str, Any]) -> Type[Any]: - __struct__ = namespace.get('__struct__', None) - namespace['__cls__'] = bases[0] if bases else None + __struct__ = namespace.get("__struct__", None) + namespace["__cls__"] = bases[0] if bases else None # Parse the struct - if '__struct__' in namespace: - if isinstance(namespace['__struct__'], (str, Tokens)): + if "__struct__" in namespace: + if isinstance(namespace["__struct__"], (str, Tokens)): namespace.update(parse_struct(**namespace)) __struct__ = True - if '__def__' in namespace: + if "__def__" in namespace: namespace.update(parse_struct_def(**namespace)) __struct__ = True # Create the new class new_class: Type[Any] = super().__new__(metacls, name, bases, namespace) # Register the class - if __struct__ is not None and not namespace.get('__anonymous__'): + if __struct__ is not None and not namespace.get("__anonymous__"): STRUCTS[name] = new_class return new_class @@ -128,23 +128,23 @@ def parse( """ cls_kargs: Dict[str, Any] = dict(kargs) if __byte_order__ is not None: - cls_kargs['__byte_order__'] = __byte_order__ + cls_kargs["__byte_order__"] = __byte_order__ if __is_union__ is not None: - cls_kargs['__is_union__'] = __is_union__ - cls_kargs['__struct__'] = __struct__ + cls_kargs["__is_union__"] = __is_union__ + cls_kargs["__struct__"] = __struct__ if isinstance(__struct__, (str, Tokens)): - del cls_kargs['__struct__'] + del cls_kargs["__struct__"] cls_kargs.update(parse_struct_def(__struct__, __cls__=cls, **cls_kargs)) - cls_kargs['__struct__'] = None + cls_kargs["__struct__"] = None elif isinstance(__struct__, dict): - del cls_kargs['__struct__'] + del cls_kargs["__struct__"] cls_kargs.update(__struct__) - cls_kargs['__struct__'] = None - __name__ = cls_kargs.get('__name__') or __name__ + cls_kargs["__struct__"] = None + __name__ = cls_kargs.get("__name__") or __name__ if __name__ is None: # Anonymous struct - __name__ = cls.__name__ + '_' + hashlib.sha1(str(__struct__).encode('utf-8')).hexdigest() - cls_kargs['__anonymous__'] = True - cls_kargs['__name__'] = __name__ + __name__ = cls.__name__ + "_" + hashlib.sha1(str(__struct__).encode("utf-8")).hexdigest() + cls_kargs["__anonymous__"] = True + cls_kargs["__name__"] = __name__ return type(__name__, (cls,), cls_kargs) def set_flexible_array_length(self, flexible_array_length: Optional[int]) -> None: @@ -173,7 +173,7 @@ def unpack(self, buffer: Optional[Union[bytes, BinaryIO]], flexible_array_length flexible_array_length: flexible array length """ self.set_flexible_array_length(flexible_array_length) - if hasattr(buffer, 'read'): + if hasattr(buffer, "read"): buffer = buffer.read(self.size) # type: ignore if not buffer: return False @@ -237,7 +237,7 @@ def inspect(self, start_addr: Optional[int] = None, end_addr: Optional[int] = No end_addr: end address """ buffer = StringIO() - if hasattr(self, '__mem__'): + if hasattr(self, "__mem__"): mem = self.__mem__ else: mem = self.pack() @@ -245,11 +245,11 @@ def inspect(self, start_addr: Optional[int] = None, end_addr: Optional[int] = No row = mem[i : i + 16] buffer.write(f"{i:08x} ") for j, c in enumerate(row): - separator = ' ' if j == 7 else '' + separator = " " if j == 7 else "" buffer.write(f" {c:02x}{separator}") buffer.write(" |") for c in row: - buffer.write(chr(c) if c >= 32 and c < 127 else '.') + buffer.write(chr(c) if c >= 32 and c < 127 else ".") buffer.write("|") buffer.write("\n") buffer.seek(0, 0) @@ -368,16 +368,16 @@ def parse( """ cls_kargs: Dict[str, Any] = dict(kargs) if __size__ is not None: - cls_kargs['__size__'] = __size__ + cls_kargs["__size__"] = __size__ if __native_format__ is not None: - cls_kargs['__native_format__'] = __native_format__ + cls_kargs["__native_format__"] = __native_format__ if isinstance(__enum__, (str, Tokens)): cls_kargs.update(parse_enum_def(__enum__, __cls__=cls, **cls_kargs)) elif isinstance(__enum__, dict): cls_kargs.update(__enum__) - __name__ = cls_kargs.get('__name__') or __name__ + __name__ = cls_kargs.get("__name__") or __name__ if __name__ is None: __name__ = cls.__name__ + "_" + hashlib.sha1(str(__enum__).encode("utf-8")).hexdigest() cls_kargs["__anonymous__"] = True diff --git a/cstruct/base.py b/cstruct/base.py index d764c85..1ee83b3 100644 --- a/cstruct/base.py +++ b/cstruct/base.py @@ -28,22 +28,22 @@ from .abstract import AbstractCStruct, AbstractCEnum __all__ = [ - 'LITTLE_ENDIAN', - 'BIG_ENDIAN', - 'NATIVE_ORDER', - 'CHAR_ZERO', - 'STRUCTS', - 'DEFINES', - 'TYPEDEFS', - 'CHAR_ZERO', - 'DEFAULT_ENUM_SIZE', + "LITTLE_ENDIAN", + "BIG_ENDIAN", + "NATIVE_ORDER", + "CHAR_ZERO", + "STRUCTS", + "DEFINES", + "TYPEDEFS", + "CHAR_ZERO", + "DEFAULT_ENUM_SIZE", ] -LITTLE_ENDIAN = '<' +LITTLE_ENDIAN = "<" "Little-endian, std. size & alignment" -BIG_ENDIAN = '>' +BIG_ENDIAN = ">" "Big-endian, std. size & alignment" -NATIVE_ORDER = '@' +NATIVE_ORDER = "@" "Native order, size & alignment" STRUCTS: Dict[str, Type["AbstractCStruct"]] = {} @@ -53,23 +53,23 @@ DEFINES: Dict[str, Any] = {} TYPEDEFS: Dict[str, str] = { - 'short int': 'short', - 'unsigned short int': 'unsigned short', - 'ushort': 'unsigned short', - 'long int': 'long', - 'unsigned long int': 'unsigned long', - 'int8_t': 'int8', - 'uint8_t': 'uint8', - 'int16_t': 'int16', - 'uint16_t': 'uint16', - 'int32_t': 'int32', - 'uint32_t': 'uint32', - 'int64_t': 'int64', - 'uint64_t': 'uint64', + "short int": "short", + "unsigned short int": "unsigned short", + "ushort": "unsigned short", + "long int": "long", + "unsigned long int": "unsigned long", + "int8_t": "int8", + "uint8_t": "uint8", + "int16_t": "int16", + "uint16_t": "uint16", + "int32_t": "int32", + "uint32_t": "uint32", + "int64_t": "int64", + "uint64_t": "uint64", } -ENUM_SIZE_TO_C_TYPE: Dict[int, str] = {1: 'int8', 2: 'int16', 4: 'int32', 8: 'int64'} +ENUM_SIZE_TO_C_TYPE: Dict[int, str] = {1: "int8", 2: "int16", 4: "int32", 8: "int64"} -CHAR_ZERO = bytes('\0', 'ascii') +CHAR_ZERO = bytes("\0", "ascii") DEFAULT_ENUM_SIZE = 4 diff --git a/cstruct/c_expr.py b/cstruct/c_expr.py index a74123d..d46406f 100644 --- a/cstruct/c_expr.py +++ b/cstruct/c_expr.py @@ -31,7 +31,7 @@ if TYPE_CHECKING: from .abstract import AbstractCStruct -__all__ = ['c_eval'] +__all__ = ["c_eval"] def c_eval(expr: str) -> Union[int, float]: diff --git a/cstruct/c_parser.py b/cstruct/c_parser.py index 3f6d0a5..16cedb5 100644 --- a/cstruct/c_parser.py +++ b/cstruct/c_parser.py @@ -34,11 +34,12 @@ if TYPE_CHECKING: from .abstract import AbstractCStruct, AbstractCEnum -__all__ = ['parse_struct', 'parse_struct_def', 'parse_enum_def', 'Tokens'] +__all__ = ["parse_struct", "parse_struct_def", "parse_enum_def", "Tokens"] SEPARATORS = [" ", "\t", "\n", ";", "{", "}", ":", ",", "="] SPACES = [" ", "\t", "\n"] + class Tokens(object): def __init__(self, text: str) -> None: # remove the comments @@ -78,10 +79,10 @@ def pop(self) -> str: def pop_c_type(self) -> str: c_type = self.pop() - if c_type in ['signed', 'unsigned'] and len(self) > 1: + if c_type in ["signed", "unsigned"] and len(self) > 1: # short int, long int, or long long c_type = c_type + " " + self.pop() - elif c_type in ['short', 'long'] and len(self) > 1 and self.get() in ['int', 'long']: + elif c_type in ["short", "long"] and len(self) > 1 and self.get() in ["int", "long"]: # short int, long int, or long long c_type = c_type + " " + self.pop() return c_type @@ -99,12 +100,12 @@ def __str__(self) -> str: return str(self.tokens) -def parse_type(tokens: Tokens, __cls__: Type['AbstractCStruct'], byte_order: Optional[str], offset: int) -> "FieldType": +def parse_type(tokens: Tokens, __cls__: Type["AbstractCStruct"], byte_order: Optional[str], offset: int) -> "FieldType": if len(tokens) < 2: raise ParserError("Parsing error") c_type = tokens.pop() # signed/unsigned/struct - if c_type in ['signed', 'unsigned', 'struct', 'union', 'enum'] and len(tokens) > 1: + if c_type in ["signed", "unsigned", "struct", "union", "enum"] and len(tokens) > 1: c_type = c_type + " " + tokens.pop() vlen = 1 @@ -113,13 +114,13 @@ def parse_type(tokens: Tokens, __cls__: Type['AbstractCStruct'], byte_order: Opt if not c_type.endswith("{"): next_token = tokens.pop() # short int, long int, or long long - if next_token in ['int', 'long']: + if next_token in ["int", "long"]: c_type = c_type + " " + next_token next_token = tokens.pop() # void * if next_token.startswith("*"): next_token = next_token[1:] - c_type = 'void *' + c_type = "void *" # parse length if "[" in next_token: t = next_token.split("[") @@ -148,14 +149,14 @@ def parse_type(tokens: Tokens, __cls__: Type['AbstractCStruct'], byte_order: Opt c_type = TYPEDEFS[c_type] # calculate fmt - if c_type.startswith('struct ') or c_type.startswith('union '): # struct/union - c_type, tail = c_type.split(' ', 1) - kind = Kind.STRUCT if c_type == 'struct' else Kind.UNION - if tokens.get() == '{': # Named nested struct + if c_type.startswith("struct ") or c_type.startswith("union "): # struct/union + c_type, tail = c_type.split(" ", 1) + kind = Kind.STRUCT if c_type == "struct" else Kind.UNION + if tokens.get() == "{": # Named nested struct tokens.push(tail) tokens.push(c_type) ref: Union[Type[AbstractCEnum], Type[AbstractCStruct]] = __cls__.parse(tokens, __name__=tail, __byte_order__=byte_order) - elif tail == '{': # Unnamed nested struct + elif tail == "{": # Unnamed nested struct tokens.push(tail) tokens.push(c_type) ref = __cls__.parse(tokens, __byte_order__=byte_order) @@ -164,16 +165,16 @@ def parse_type(tokens: Tokens, __cls__: Type['AbstractCStruct'], byte_order: Opt ref = STRUCTS[tail] except KeyError: raise ParserError(f"Unknown `{c_type} {tail}`") - elif c_type.startswith('enum'): + elif c_type.startswith("enum"): from .cenum import CEnum - c_type, tail = c_type.split(' ', 1) + c_type, tail = c_type.split(" ", 1) kind = Kind.ENUM - if tokens.get() == '{': # Named nested struct + if tokens.get() == "{": # Named nested struct tokens.push(tail) tokens.push(c_type) ref = CEnum.parse(tokens, __name__=tail) - elif tail == '{': # unnamed nested struct + elif tail == "{": # unnamed nested struct tokens.push(tail) tokens.push(c_type) ref = CEnum.parse(tokens) @@ -188,7 +189,7 @@ def parse_type(tokens: Tokens, __cls__: Type['AbstractCStruct'], byte_order: Opt return FieldType(kind, c_type, ref, vlen, flexible_array, byte_order, offset) -def parse_typedef(tokens: Tokens, __cls__: Type['AbstractCStruct'], byte_order: Optional[str]) -> None: +def parse_typedef(tokens: Tokens, __cls__: Type["AbstractCStruct"], byte_order: Optional[str]) -> None: field_type = parse_type(tokens, __cls__, byte_order, 0) vname = tokens.pop() if field_type.ref is None: @@ -200,13 +201,13 @@ def parse_typedef(tokens: Tokens, __cls__: Type['AbstractCStruct'], byte_order: else: TYPEDEFS[vname] = f"struct {field_type.ref.__name__}" t = tokens.pop() - if t != ';': + if t != ";": raise ParserError(f"`;` expected but `{t}` found") def parse_struct_def( __def__: Union[str, Tokens], - __cls__: Type['AbstractCStruct'], + __cls__: Type["AbstractCStruct"], __byte_order__: Optional[str] = None, process_muliple_definition: bool = False, **kargs: Any, # Type['AbstractCStruct'], @@ -219,39 +220,39 @@ def parse_struct_def( result = None while tokens and (process_muliple_definition or not result): kind = tokens.pop() - if kind == ';': + if kind == ";": pass - elif kind == 'typedef': + elif kind == "typedef": if result: - result['__cls__'].parse(result, **kargs) + result["__cls__"].parse(result, **kargs) parse_typedef(tokens, __cls__, __byte_order__) - elif kind == 'enum': + elif kind == "enum": if result: - result['__cls__'].parse(result, **kargs) + result["__cls__"].parse(result, **kargs) name = tokens.pop() native_format = None - if tokens.get() == ':': # enumeration type declaration + if tokens.get() == ":": # enumeration type declaration tokens.pop() # pop ":" type_ = get_native_type(tokens.pop_c_type()) native_format = type_.native_format - if tokens.get() == '{': # named enum + if tokens.get() == "{": # named enum tokens.pop() # pop "{" result = parse_enum(tokens, __name__=name, native_format=native_format) - elif name == '{': # unnamed enum + elif name == "{": # unnamed enum result = parse_enum(tokens, native_format=native_format) else: raise ParserError(f"`{name}` definition expected") - elif kind in ['struct', 'union']: + elif kind in ["struct", "union"]: if result: - result['__cls__'].parse(result, **kargs) - __is_union__ = kind == 'union' + result["__cls__"].parse(result, **kargs) + __is_union__ = kind == "union" name = tokens.pop() - if name == '{': # unnamed nested struct + if name == "{": # unnamed nested struct result = parse_struct(tokens, __cls__=__cls__, __is_union__=__is_union__, __byte_order__=__byte_order__) - elif tokens.get() == '{': # Named nested struct + elif tokens.get() == "{": # Named nested struct tokens.pop() # pop "{" result = parse_struct( tokens, __cls__=__cls__, __is_union__=__is_union__, __byte_order__=__byte_order__, __name__=name @@ -273,19 +274,19 @@ def parse_enum_def(__def__: Union[str, Tokens], **kargs: Any) -> Optional[Dict[s if not tokens: return None kind = tokens.pop() - if kind not in ['enum']: + if kind not in ["enum"]: raise ParserError(f"enum expected - `{kind}` found") name = tokens.pop() native_format = None - if tokens.get() == ':': # enumeration type declaration + if tokens.get() == ":": # enumeration type declaration tokens.pop() # pop ":" type_ = get_native_type(tokens.pop_c_type()) native_format = type_.native_format - if tokens.get() == '{': # named enum + if tokens.get() == "{": # named enum tokens.pop() # pop "{" return parse_enum(tokens, __name__=name, native_format=native_format) - elif name == '{': # unnamed enum + elif name == "{": # unnamed enum return parse_enum(tokens) else: raise ParserError(f"`{name}` definition expected") @@ -318,7 +319,7 @@ def parse_enum( tokens = Tokens(__enum__) while len(tokens): - if tokens.get() == '}': + if tokens.get() == "}": tokens.pop() break @@ -358,20 +359,20 @@ def parse_enum( break result = { - '__constants__': constants, - '__is_struct__': False, - '__is_union__': False, - '__is_enum__': True, - '__name__': __name__, - '__native_format__': native_format, - '__cls__': CEnum, + "__constants__": constants, + "__is_struct__": False, + "__is_union__": False, + "__is_enum__": True, + "__name__": __name__, + "__native_format__": native_format, + "__cls__": CEnum, } return result def parse_struct( __struct__: Union[str, Tokens], - __cls__: Type['AbstractCStruct'], + __cls__: Type["AbstractCStruct"], __is_union__: bool = False, __byte_order__: Optional[str] = None, __name__: Optional[str] = None, @@ -407,7 +408,7 @@ def parse_struct( else: tokens = Tokens(__struct__) while len(tokens): - if tokens.get() == '}': + if tokens.get() == "}": tokens.pop() break # flexible array member must be the last member of such a struct @@ -420,7 +421,7 @@ def parse_struct( if vname in dir(__cls__): raise ParserError(f"Invalid reserved member name `{vname}`") # anonymous nested union - if vname == ';' and field_type.ref is not None and (__is_union__ or field_type.ref.__is_union__): + if vname == ";" and field_type.ref is not None and (__is_union__ or field_type.ref.__is_union__): # add the anonymous struct fields to the parent for nested_field_name, nested_field_type in field_type.ref.__fields_types__.items(): if nested_field_name in fields_types: @@ -428,7 +429,7 @@ def parse_struct( fields_types[nested_field_name] = nested_field_type vname = f"__anonymous{anonymous}" anonymous += 1 - tokens.push(';') + tokens.push(";") fields_types[vname] = field_type # calculate the max field size (for the alignment) max_alignment = max(max_alignment, field_type.alignment) @@ -437,7 +438,7 @@ def parse_struct( field_type.align_filed_offset() offset = field_type.offset + field_type.vsize t = tokens.pop() - if t != ';': + if t != ";": raise ParserError(f"`;` expected but `{t}` found") if __is_union__: # C union @@ -449,15 +450,15 @@ def parse_struct( # Prepare the result result = { - '__fields__': list(fields_types.keys()), - '__fields_types__': fields_types, - '__size__': size, - '__is_struct__': not __is_union__, - '__is_union__': __is_union__, - '__is_enum__': False, - '__byte_order__': __byte_order__, - '__alignment__': max_alignment, - '__name__': __name__, - '__cls__': __cls__, + "__fields__": list(fields_types.keys()), + "__fields_types__": fields_types, + "__size__": size, + "__is_struct__": not __is_union__, + "__is_union__": __is_union__, + "__is_enum__": False, + "__byte_order__": __byte_order__, + "__alignment__": max_alignment, + "__name__": __name__, + "__cls__": __cls__, } return result diff --git a/cstruct/field.py b/cstruct/field.py index 957b1d3..bd9b3e3 100644 --- a/cstruct/field.py +++ b/cstruct/field.py @@ -33,7 +33,7 @@ if TYPE_CHECKING: from .abstract import AbstractCStruct -__all__ = ['align', 'calculate_padding', 'Kind', 'FieldType'] +__all__ = ["align", "calculate_padding", "Kind", "FieldType"] def align(byte_order: Optional[str]) -> bool: @@ -162,7 +162,7 @@ def pack(self, data: Any) -> bytes: @property def is_array(self) -> bool: "True if field is an array/flexible array" - return self.flexible_array or (not (self.vlen == 1 or self.c_type == 'char')) + return self.flexible_array or (not (self.vlen == 1 or self.c_type == "char")) @property def is_native(self) -> bool: @@ -195,13 +195,13 @@ def native_format(self) -> str: elif self.is_enum: return self.ref.__native_format__ else: - return 'c' + return "c" @property def fmt(self) -> str: "Field format prefixed by byte order (struct library format)" if self.is_native or self.is_enum: - fmt = (str(self.vlen) if self.vlen > 1 or self.flexible_array else '') + self.native_format + fmt = (str(self.vlen) if self.vlen > 1 or self.flexible_array else "") + self.native_format else: # Struct/Union fmt = str(self.vlen * self.ref.sizeof()) + self.native_format if self.byte_order: @@ -227,7 +227,7 @@ def alignment(self) -> int: def align_filed_offset(self) -> None: "If the byte order is native, align the field" - if align(self.byte_order) and self.c_type != 'char': + if align(self.byte_order) and self.c_type != "char": self.padding = calculate_padding(self.byte_order, self.alignment, self.base_offset) self.offset = self.base_offset + self.padding diff --git a/cstruct/mem_cstruct.py b/cstruct/mem_cstruct.py index 61f2c6a..dc2f9c3 100644 --- a/cstruct/mem_cstruct.py +++ b/cstruct/mem_cstruct.py @@ -29,7 +29,7 @@ class CStructList(List[Any]): - def __init__(self, values: List[Any], name: str, parent: Optional['MemCStruct'] = None) -> None: + def __init__(self, values: List[Any], name: str, parent: Optional["MemCStruct"] = None) -> None: super().__init__(values) self.name = name self.parent = parent diff --git a/cstruct/native_types.py b/cstruct/native_types.py index 46cf011..6a60f6c 100644 --- a/cstruct/native_types.py +++ b/cstruct/native_types.py @@ -85,16 +85,16 @@ class NativeTypeMeta(ABCMeta): " Type format " def __new__(metacls: Type[type], name: str, bases: Tuple[str], namespace: Dict[str, Any]) -> Type[Any]: - if namespace.get('native_format'): - native_format = namespace['native_format'] - namespace['__size__'] = struct.calcsize(native_format) + if namespace.get("native_format"): + native_format = namespace["native_format"] + namespace["__size__"] = struct.calcsize(native_format) else: native_format = None - namespace['native_format'] = None - namespace['__size__'] = None + namespace["native_format"] = None + namespace["__size__"] = None new_class: Type[Any] = super().__new__(metacls, name, bases, namespace) - if namespace.get('type_name'): - NATIVE_TYPES[namespace['type_name']] = new_class + if namespace.get("type_name"): + NATIVE_TYPES[namespace["type_name"]] = new_class return new_class def __len__(cls) -> int: diff --git a/tests/test_alignment.py b/tests/test_alignment.py index b7cc3e1..609abf5 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -189,8 +189,8 @@ class Foo10(CStruct): def test_utmp_sizeof(): - assert Utmp.__fields_types__['ut_type'].padding == 0 - assert Utmp.__fields_types__['ut_pid'].padding == 2 + assert Utmp.__fields_types__["ut_type"].padding == 0 + assert Utmp.__fields_types__["ut_pid"].padding == 2 assert sizeof("struct Utmp") == 384 assert Utmp().size == 384 @@ -200,15 +200,15 @@ def test_utmp_sizeof(): def test_foo1_sizeof(): if IS_64BITS: - assert Foo1.__fields_types__['p'].padding == 0 - assert Foo1.__fields_types__['c'].padding == 0 - assert Foo1.__fields_types__['x'].padding == 7 + assert Foo1.__fields_types__["p"].padding == 0 + assert Foo1.__fields_types__["c"].padding == 0 + assert Foo1.__fields_types__["x"].padding == 7 assert sizeof("struct Foo1") == 24 assert Foo1().size == 24 else: - assert Foo1.__fields_types__['p'].padding == 0 - assert Foo1.__fields_types__['c'].padding == 0 - assert Foo1.__fields_types__['x'].padding == 3 + assert Foo1.__fields_types__["p"].padding == 0 + assert Foo1.__fields_types__["c"].padding == 0 + assert Foo1.__fields_types__["x"].padding == 3 assert sizeof("struct Foo1") == 12 assert Foo1().size == 12 @@ -238,27 +238,27 @@ def test_foo4_sizeof(): def test_foo5_sizeof(): if IS_64BITS: - assert Foo5.__fields_types__['c'].padding == 0 - assert Foo5.__fields_types__['inner'].padding == 7 + assert Foo5.__fields_types__["c"].padding == 0 + assert Foo5.__fields_types__["inner"].padding == 7 assert sizeof("struct Foo5") == 24 assert Foo5().size == 24 else: - assert Foo5.__fields_types__['c'].padding == 0 - assert Foo5.__fields_types__['inner'].padding == 3 + assert Foo5.__fields_types__["c"].padding == 0 + assert Foo5.__fields_types__["inner"].padding == 3 assert sizeof("struct Foo5") == 12 assert Foo5().size == 12 def test_foo10_sizeof(): if IS_64BITS: - assert Foo10.__fields_types__['c'].padding == 0 - assert Foo10.__fields_types__['p'].padding == 7 - assert Foo10.__fields_types__['s'].padding == 0 + assert Foo10.__fields_types__["c"].padding == 0 + assert Foo10.__fields_types__["p"].padding == 7 + assert Foo10.__fields_types__["s"].padding == 0 assert sizeof("struct Foo10") == 24 assert Foo10().size == 24 else: - assert Foo10.__fields_types__['c'].padding == 0 - assert Foo10.__fields_types__['p'].padding == 3 - assert Foo10.__fields_types__['s'].padding == 0 + assert Foo10.__fields_types__["c"].padding == 0 + assert Foo10.__fields_types__["p"].padding == 3 + assert Foo10.__fields_types__["s"].padding == 0 assert sizeof("struct Foo10") == 12 assert Foo10().size == 12 diff --git a/tests/test_c_expr.py b/tests/test_c_expr.py index 8230bd6..294fa24 100644 --- a/tests/test_c_expr.py +++ b/tests/test_c_expr.py @@ -38,7 +38,7 @@ def test_c_expr_def(): """ ) assert getdef("A1") == 10 - assert getdef('A2') == 20 # TODO + assert getdef("A2") == 20 # TODO assert c_eval("A1 / 10") == 1 @@ -76,5 +76,5 @@ def test_c_expr_compare(): assert c_eval("3 > 2 > 1") == 1 assert c_eval("3 >= 30") == 0 assert c_eval("3 <= 30") == 1 - define('A10', 10) + define("A10", 10) assert c_eval("((A10 < 6) || (A10>10))") == 0 diff --git a/tests/test_cstruct.py b/tests/test_cstruct.py index 57e9557..ae1d060 100644 --- a/tests/test_cstruct.py +++ b/tests/test_cstruct.py @@ -33,7 +33,7 @@ from pathlib import Path from cstruct.exceptions import ParserError -MBR_DATA = (Path(__file__).parent.parent / 'mbr').read_bytes() +MBR_DATA = (Path(__file__).parent.parent / "mbr").read_bytes() class Position(cstruct.CStruct): @@ -92,9 +92,9 @@ class Dummy(cstruct.CStruct): """ -typedef('char', 'BYTE') -typedef('short', 'WORD') -typedef('int', 'DWORD') +typedef("char", "BYTE") +typedef("short", "WORD") +typedef("int", "DWORD") class PartitionFlat(cstruct.CStruct): @@ -140,7 +140,7 @@ def test_len(): def test_pack_len(): - buffer = b'\x00' * 512 + buffer = b"\x00" * 512 mbr = MBR(buffer) d = mbr.pack() assert len(d) == 512 @@ -204,7 +204,7 @@ def test_clear(): def test_inline(): StructT1 = cstruct.parse( - 'struct StructT1 { unsigned char head; unsigned char sector; unsigned char cyl; }', + "struct StructT1 { unsigned char head; unsigned char sector; unsigned char cyl; }", __byte_order__=cstruct.LITTLE_ENDIAN, ) s = StructT1(head=254, sector=63, cyl=134) @@ -215,8 +215,8 @@ def test_inline(): def test_dummy(): dummy = Dummy() - dummy.c = b'A' - dummy.vc = b'ABCDEFGHIJ' + dummy.c = b"A" + dummy.vc = b"ABCDEFGHIJ" dummy.i = 123456 for i in range(0, 10): dummy.vi[i] = i * 10 @@ -261,9 +261,9 @@ def test_null_compare(): def test_invalid_inline(): with pytest.raises(ParserError): - cstruct.MemCStruct.parse('struct { unsigned char head; unsigned char head; }', __byte_order__=cstruct.LITTLE_ENDIAN) + cstruct.MemCStruct.parse("struct { unsigned char head; unsigned char head; }", __byte_order__=cstruct.LITTLE_ENDIAN) def test_invalid_inline_reserved(): with pytest.raises(ParserError): - cstruct.CStruct.parse('struct { int size; }') + cstruct.CStruct.parse("struct { int size; }") diff --git a/tests/test_define.py b/tests/test_define.py index eda2947..6a5a74e 100644 --- a/tests/test_define.py +++ b/tests/test_define.py @@ -46,32 +46,32 @@ class Position(cstruct.CStruct): def test_sizeof(): - assert sizeof('int') == 4 - define('INIT_THREAD_SIZE', 2048 * sizeof('long')) + assert sizeof("int") == 4 + define("INIT_THREAD_SIZE", 2048 * sizeof("long")) if IS_64BITS: - assert cstruct.DEFINES['INIT_THREAD_SIZE'] == 16384 + assert cstruct.DEFINES["INIT_THREAD_SIZE"] == 16384 else: - assert cstruct.DEFINES['INIT_THREAD_SIZE'] == 8192 - assert sizeof('struct Position') == 3 - assert sizeof('struct Position') == len(Position) + assert cstruct.DEFINES["INIT_THREAD_SIZE"] == 8192 + assert sizeof("struct Position") == 3 + assert sizeof("struct Position") == len(Position) assert sizeof(Position) == 3 with pytest.raises(KeyError): - sizeof('bla') + sizeof("bla") with pytest.raises(KeyError): - sizeof('struct Bla') + sizeof("struct Bla") def test_define(): - define('A', 10) - assert cstruct.DEFINES['A'] == 10 - undef('A') + define("A", 10) + assert cstruct.DEFINES["A"] == 10 + undef("A") with pytest.raises(KeyError): - cstruct.DEFINES['A'] + cstruct.DEFINES["A"] def test_typedef(): - typedef('int', 'integer') - assert sizeof('integer') == 4 + typedef("int", "integer") + assert sizeof("integer") == 4 def test_invalid_type(): diff --git a/tests/test_flexible_array.py b/tests/test_flexible_array.py index a8a33df..5a3aef6 100644 --- a/tests/test_flexible_array.py +++ b/tests/test_flexible_array.py @@ -53,26 +53,26 @@ class MemPkg(cstruct.MemCStruct): def test_len(): pkg = Pkg() - assert len(pkg) == sizeof('uint16_t') * 2 + assert len(pkg) == sizeof("uint16_t") * 2 assert len(pkg.pack()) - assert len(pkg) == sizeof('uint16_t') * 2 - assert pkg.sizeof() == sizeof('uint16_t') * 2 - assert pkg.__size__ == sizeof('uint16_t') * 2 + assert len(pkg) == sizeof("uint16_t") * 2 + assert pkg.sizeof() == sizeof("uint16_t") * 2 + assert pkg.__size__ == sizeof("uint16_t") * 2 pkg.length = 10 pkg.data = list(range(pkg.length)) - assert len(pkg.pack()) == (sizeof('uint16_t') * 2) + (sizeof('uint8_t') * pkg.length) - assert len(pkg) == (sizeof('uint16_t') * 2) + (sizeof('uint8_t') * pkg.length) - assert pkg.sizeof() == sizeof('uint16_t') * 2 - assert pkg.__size__ == sizeof('uint16_t') * 2 + assert len(pkg.pack()) == (sizeof("uint16_t") * 2) + (sizeof("uint8_t") * pkg.length) + assert len(pkg) == (sizeof("uint16_t") * 2) + (sizeof("uint8_t") * pkg.length) + assert pkg.sizeof() == sizeof("uint16_t") * 2 + assert pkg.__size__ == sizeof("uint16_t") * 2 pkg2 = Pkg() pkg2.length = 20 pkg2.data = list(range(pkg2.length)) - assert len(pkg2.pack()) == (sizeof('uint16_t') * 2) + (sizeof('uint8_t') * pkg2.length) - assert len(pkg2) == (sizeof('uint16_t') * 2) + (sizeof('uint8_t') * pkg2.length) - assert pkg2.sizeof() == sizeof('uint16_t') * 2 - assert pkg2.__size__ == sizeof('uint16_t') * 2 + assert len(pkg2.pack()) == (sizeof("uint16_t") * 2) + (sizeof("uint8_t") * pkg2.length) + assert len(pkg2) == (sizeof("uint16_t") * 2) + (sizeof("uint8_t") * pkg2.length) + assert pkg2.sizeof() == sizeof("uint16_t") * 2 + assert pkg2.__size__ == sizeof("uint16_t") * 2 assert len(pkg) != len(pkg2) @@ -80,20 +80,20 @@ def test_pack_unpack(): pkg = Pkg() pkg.cmd = 5 pkg.length = 10 - assert pkg.__fields_types__['data'].vlen == 0 - assert pkg.__fields_types__['data'].vsize == 0 - assert len(pkg) == sizeof('uint16_t') * 2 + assert pkg.__fields_types__["data"].vlen == 0 + assert pkg.__fields_types__["data"].vsize == 0 + assert len(pkg) == sizeof("uint16_t") * 2 pkg.data = list(range(pkg.length)) - assert len(pkg.pack()) == (sizeof('uint16_t') * 2) + (sizeof('uint8_t') * pkg.length) - assert pkg.__fields_types__['data'].vlen == pkg.length - assert pkg.__fields_types__['data'].vsize == (sizeof('uint8_t') * pkg.length) + assert len(pkg.pack()) == (sizeof("uint16_t") * 2) + (sizeof("uint8_t") * pkg.length) + assert pkg.__fields_types__["data"].vlen == pkg.length + assert pkg.__fields_types__["data"].vsize == (sizeof("uint8_t") * pkg.length) assert len(pkg.data) == pkg.length data = pkg.pack() pkg2 = Pkg() - assert pkg2.__fields_types__['data'].vlen == 0 + assert pkg2.__fields_types__["data"].vlen == 0 pkg2.unpack(data, flexible_array_length=pkg.length) - assert pkg2.__fields_types__['data'].vlen == pkg2.length + assert pkg2.__fields_types__["data"].vlen == pkg2.length assert pkg2.cmd == pkg.cmd assert pkg2.length == pkg.length assert pkg2.data == pkg.data @@ -108,33 +108,33 @@ def test_pack_unpack(): def test_mem_len(): pkg = MemPkg() - assert len(pkg) == sizeof('uint16_t') * 2 + assert len(pkg) == sizeof("uint16_t") * 2 assert len(pkg.pack()) - assert len(pkg) == sizeof('uint16_t') * 2 - assert pkg.sizeof() == sizeof('uint16_t') * 2 - assert pkg.__size__ == sizeof('uint16_t') * 2 + assert len(pkg) == sizeof("uint16_t") * 2 + assert pkg.sizeof() == sizeof("uint16_t") * 2 + assert pkg.__size__ == sizeof("uint16_t") * 2 pkg.length = 10 pkg.data = list(range(pkg.length)) - assert len(pkg.pack()) == (sizeof('uint16_t') * 2) + (sizeof('uint8_t') * pkg.length) - assert len(pkg) == (sizeof('uint16_t') * 2) + (sizeof('uint8_t') * pkg.length) - assert pkg.sizeof() == sizeof('uint16_t') * 2 - assert pkg.__size__ == sizeof('uint16_t') * 2 + assert len(pkg.pack()) == (sizeof("uint16_t") * 2) + (sizeof("uint8_t") * pkg.length) + assert len(pkg) == (sizeof("uint16_t") * 2) + (sizeof("uint8_t") * pkg.length) + assert pkg.sizeof() == sizeof("uint16_t") * 2 + assert pkg.__size__ == sizeof("uint16_t") * 2 pkg.length = 5 pkg.data = list(range(pkg.length)) - assert len(pkg.pack()) == (sizeof('uint16_t') * 2) + (sizeof('uint8_t') * pkg.length) - assert len(pkg) == (sizeof('uint16_t') * 2) + (sizeof('uint8_t') * pkg.length) - assert pkg.sizeof() == sizeof('uint16_t') * 2 - assert pkg.__size__ == sizeof('uint16_t') * 2 + assert len(pkg.pack()) == (sizeof("uint16_t") * 2) + (sizeof("uint8_t") * pkg.length) + assert len(pkg) == (sizeof("uint16_t") * 2) + (sizeof("uint8_t") * pkg.length) + assert pkg.sizeof() == sizeof("uint16_t") * 2 + assert pkg.__size__ == sizeof("uint16_t") * 2 pkg2 = MemPkg() pkg2.length = 20 pkg2.data = list(range(pkg2.length)) - assert len(pkg2.pack()) == (sizeof('uint16_t') * 2) + (sizeof('uint8_t') * pkg2.length) - assert len(pkg2) == (sizeof('uint16_t') * 2) + (sizeof('uint8_t') * pkg2.length) - assert pkg2.sizeof() == sizeof('uint16_t') * 2 - assert pkg2.__size__ == sizeof('uint16_t') * 2 + assert len(pkg2.pack()) == (sizeof("uint16_t") * 2) + (sizeof("uint8_t") * pkg2.length) + assert len(pkg2) == (sizeof("uint16_t") * 2) + (sizeof("uint8_t") * pkg2.length) + assert pkg2.sizeof() == sizeof("uint16_t") * 2 + assert pkg2.__size__ == sizeof("uint16_t") * 2 assert len(pkg) != len(pkg2) @@ -142,20 +142,20 @@ def test_mem_pack_unpack(): pkg = MemPkg() pkg.cmd = 5 pkg.length = 10 - assert pkg.__fields_types__['data'].vlen == 0 - assert pkg.__fields_types__['data'].vsize == 0 - assert len(pkg) == sizeof('uint16_t') * 2 + assert pkg.__fields_types__["data"].vlen == 0 + assert pkg.__fields_types__["data"].vsize == 0 + assert len(pkg) == sizeof("uint16_t") * 2 pkg.data = list(range(pkg.length)) - assert len(pkg.pack()) == (sizeof('uint16_t') * 2) + (sizeof('uint8_t') * pkg.length) - assert pkg.__fields_types__['data'].vlen == pkg.length - assert pkg.__fields_types__['data'].vsize == (sizeof('uint8_t') * pkg.length) + assert len(pkg.pack()) == (sizeof("uint16_t") * 2) + (sizeof("uint8_t") * pkg.length) + assert pkg.__fields_types__["data"].vlen == pkg.length + assert pkg.__fields_types__["data"].vsize == (sizeof("uint8_t") * pkg.length) assert len(pkg.data) == pkg.length data = pkg.pack() pkg2 = MemPkg() - assert pkg2.__fields_types__['data'].vlen == 0 + assert pkg2.__fields_types__["data"].vlen == 0 pkg2.unpack(data, flexible_array_length=pkg.length) - assert pkg2.__fields_types__['data'].vlen == pkg2.length + assert pkg2.__fields_types__["data"].vlen == pkg2.length assert pkg2.cmd == pkg.cmd assert pkg2.length == pkg.length assert pkg2.data == pkg.data diff --git a/tests/test_memcstruct.py b/tests/test_memcstruct.py index e5c1dca..bac47c1 100644 --- a/tests/test_memcstruct.py +++ b/tests/test_memcstruct.py @@ -32,7 +32,7 @@ from pathlib import Path from cstruct.exceptions import ParserError -MBR_DATA = (Path(__file__).parent.parent / 'mbr').read_bytes() +MBR_DATA = (Path(__file__).parent.parent / "mbr").read_bytes() class Position(cstruct.MemCStruct): @@ -89,9 +89,9 @@ class Dummy(cstruct.MemCStruct): """ -typedef('char', 'BYTE') -typedef('short', 'WORD') -typedef('int', 'DWORD') +typedef("char", "BYTE") +typedef("short", "WORD") +typedef("int", "DWORD") class PartitionFlat(cstruct.MemCStruct): @@ -137,7 +137,7 @@ def test_len(): def test_pack_len(): - buffer = b'\x00' * 512 + buffer = b"\x00" * 512 mbr = MBR(buffer) d = mbr.pack() assert len(d) == 512 @@ -200,7 +200,7 @@ def test_clear(): def test_inline(): TestStruct = cstruct.MemCStruct.parse( - 'struct { unsigned char head; unsigned char sector; unsigned char cyl; }', __byte_order__=cstruct.LITTLE_ENDIAN + "struct { unsigned char head; unsigned char sector; unsigned char cyl; }", __byte_order__=cstruct.LITTLE_ENDIAN ) s = TestStruct(head=254, sector=63, cyl=134) p = Position(head=254, sector=63, cyl=134) @@ -209,8 +209,8 @@ def test_inline(): def test_dummy(): dummy = Dummy() - dummy.c = b'A' - dummy.vc = b'ABCDEFGHIJ' + dummy.c = b"A" + dummy.vc = b"ABCDEFGHIJ" dummy.i = 123456 for i in range(0, 10): dummy.vi[i] = i * 10 @@ -255,9 +255,9 @@ def test_null_compare(): def test_invalid_inline(): with pytest.raises(ParserError): - cstruct.MemCStruct.parse('struct { unsigned char head; unsigned char head; }', __byte_order__=cstruct.LITTLE_ENDIAN) + cstruct.MemCStruct.parse("struct { unsigned char head; unsigned char head; }", __byte_order__=cstruct.LITTLE_ENDIAN) def test_invalid_inline_reserved(): with pytest.raises(ParserError): - cstruct.MemCStruct.parse('struct { int size; }') + cstruct.MemCStruct.parse("struct { int size; }") diff --git a/tests/test_nested.py b/tests/test_nested.py index 4220ca5..b6b0bbf 100644 --- a/tests/test_nested.py +++ b/tests/test_nested.py @@ -113,7 +113,7 @@ def test_invalid_anonymous(): def test_sizeof_nested_struct(): - assert sizeof('struct NestedStruct') == 16 + assert sizeof("struct NestedStruct") == 16 o = NestedStruct() assert len(o) == 16 @@ -135,7 +135,7 @@ def test_pack_unpack_nested_struct(): def test_sizeof_nested_union(): - assert sizeof('struct NestedUnion') == 8 + assert sizeof("struct NestedUnion") == 8 o = NestedUnion() assert len(o) == 8 @@ -165,7 +165,7 @@ def test_pack_unpack_nested_union(): def test_sizeof_nested_anonymous_union(): - assert sizeof('struct NestedAnonymousUnion') == 8 + assert sizeof("struct NestedAnonymousUnion") == 8 o = NestedAnonymousUnion() assert len(o) == 8 diff --git a/tests/test_typdef.py b/tests/test_typdef.py index 9990f8b..ca8a564 100644 --- a/tests/test_typdef.py +++ b/tests/test_typdef.py @@ -70,7 +70,7 @@ class Utmp(MemCStruct): def test_typedef(): - assert TYPEDEFS['pid_t'] == 'int' - assert TYPEDEFS['time_t'] == 'long' - assert TYPEDEFS['ulong'] == 'unsigned long' - assert TYPEDEFS['ExitStatus'] == 'struct ExitStatus' + assert TYPEDEFS["pid_t"] == "int" + assert TYPEDEFS["time_t"] == "long" + assert TYPEDEFS["ulong"] == "unsigned long" + assert TYPEDEFS["ExitStatus"] == "struct ExitStatus" diff --git a/tests/test_union.py b/tests/test_union.py index ad90713..a13e97f 100644 --- a/tests/test_union.py +++ b/tests/test_union.py @@ -70,7 +70,7 @@ class UnionT1(cstruct.MemCStruct): def test_sizeof(): - assert sizeof('struct UnionT1') == 64 + assert sizeof("struct UnionT1") == 64 s = UnionT1() assert len(s) == 64 @@ -82,12 +82,12 @@ def test_union_unpack(): assert union.a1 == 0 assert union.b == 0 assert union.c == 0 - union.unpack(struct.pack('b', 10) + cstruct.CHAR_ZERO * union.size) + union.unpack(struct.pack("b", 10) + cstruct.CHAR_ZERO * union.size) assert union.a == 10 assert union.a1 == 10 assert union.b == 10 assert union.c == 10 - union.unpack(struct.pack('h', 1979) + cstruct.CHAR_ZERO * union.size) + union.unpack(struct.pack("h", 1979) + cstruct.CHAR_ZERO * union.size) assert union.a == 187 assert union.a1 == 187 assert union.b == 1979 From 3a0ea6efe513171b197713d78591e227e82eca30 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Sat, 12 Nov 2022 07:58:19 +0000 Subject: [PATCH 65/95] setup --- .github/workflows/release.yml | 2 +- pyproject.toml | 4 +++ requirements-dev.txt | 1 + setup.cfg | 61 +++++++++++++++++++++++++++++++++-- setup.py | 46 +++----------------------- 5 files changed, 68 insertions(+), 46 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 75addee..c1e0e3b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -61,7 +61,7 @@ jobs: python -m pip install -r requirements-dev.txt - name: Build package - run: python setup.py bdist_wheel sdist + run: python -m build - name: Check package run: twine check dist/* diff --git a/pyproject.toml b/pyproject.toml index 70b86b1..074200f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,7 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + [tool.black] line-length = 132 diff --git a/requirements-dev.txt b/requirements-dev.txt index ff893de..7584936 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,3 +10,4 @@ mkdocs mkdocstrings[python] mkdocs-material markdown_include +build diff --git a/setup.cfg b/setup.cfg index c5ee45d..2b497a6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,61 @@ +[metadata] +name = cstruct +version = attr: cstruct.__version__ +keywords = struct, cstruct, enum, binary, pack, unpack +description = C-style structs for Python +author = Andrea Bonomi +author_email = andrea.bonomi@gmail.com +url = http://github.com/andreax79/python-cstruct +long_description = file: README.md +long_description_content_type = text/markdown +license = MIT +license_files = LICENSE +platforms = any +classifiers = + Development Status :: 5 - Production/Stable + Environment :: Console + Intended Audience :: Developers + License :: OSI Approved :: MIT License + Operating System :: OS Independent + Programming Language :: Python + Topic :: Software Development :: Libraries :: Python Modules + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 +project_urls = + Bug Tracker = http://github.com/andreax79/python-cstruct/issues + Documentation = https://python-cstruct.readthedocs.io/en/latest/ + Source Code = http://github.com/andreax79/python-cstruct + +[options] +zip_safe = True +include_package_data = True +python_requires = >=3.6 +packages = find: + +[options.packages.find] +include = cstruct* +exclude = + ez_setup + examples + tests + +[options.extras_require] +test = pytest + +[aliases] +test = pytest + [bdist_wheel] -universal=1 +universal = 1 [flake8] -max-line-length=132 -ignore: E401, W504, E221 +max-line-length = 132 +extend-ignore = + E203 + E401 + W504 + E221 diff --git a/setup.py b/setup.py index bfeaed7..dbe9716 100644 --- a/setup.py +++ b/setup.py @@ -1,43 +1,5 @@ -from setuptools import setup, find_packages -from cstruct import __version__ +#!/usr/bin/env python +import setuptools - -def readme(): - with open('README.md') as f: - return f.read() - - -setup( - name='cstruct', - version=__version__, - description="C-style structs for Python", - long_description="""\ -Convert C struct definitions into Python classes with methods for serializing/deserializing.""", - classifiers=[ - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - ], - keywords='struct', - author='Andrea Bonomi', - author_email='andrea.bonomi@gmail.com', - url='http://github.com/andreax79/python-cstruct', - license='MIT', - packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), - include_package_data=True, - zip_safe=True, - install_requires=[ - # -*- Extra requirements: -*- - ], - entry_points=""" - # -*- Entry points: -*- - """, - test_suite='tests', - tests_require=['pytest'], -) +if __name__ == "__main__": + setuptools.setup() From 14f0e93634728a1bb625da2f8bd58519d57e4362 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Sat, 12 Nov 2022 08:02:29 +0000 Subject: [PATCH 66/95] version 5.0 --- changelog.txt | 15 +++++++++++++++ cstruct/__init__.py | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 5a46494..a7a35c2 100644 --- a/changelog.txt +++ b/changelog.txt @@ -175,3 +175,18 @@ ### Improved - Python 3.11 support + +## 5.0 + +2022-11-12 + +### Added + +- Add support for enums +- Add support for multiple definition to cstruct.parse +- Add inspect method + +### Improved + +- Documentation and examples +- Restructure setup diff --git a/cstruct/__init__.py b/cstruct/__init__.py index a20e843..13af539 100644 --- a/cstruct/__init__.py +++ b/cstruct/__init__.py @@ -24,7 +24,7 @@ __author__ = "Andrea Bonomi " __license__ = "MIT" -__version__ = "4.0" +__version__ = "5.0" __date__ = "15 August 2013" from typing import Any, Dict, Optional, Type, Union From 7149384cd04ab6c9c087384b3ccdc4bf356fcf9a Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Sun, 20 Nov 2022 07:26:05 +0000 Subject: [PATCH 67/95] add support for char constants --- README.md | 5 +++++ cstruct/c_expr.py | 7 +++++++ tests/test_c_expr.py | 5 +++++ tests/test_cenum.py | 6 ++++++ 4 files changed, 23 insertions(+) diff --git a/README.md b/README.md index cb6dee4..71e547b 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,8 @@ Different enum styles are supported in struct/union definitions. enum Type_A a; // externally defined using CEnum enum Type_B {A, B, C} b; enum {A, B, C} c; +enum Type_D : short {A, B, C} d; // specify the underlying type +enum Direction { left = 'l', right = 'r' }; ``` ### Nested structs/unions @@ -334,3 +336,6 @@ Output example: 000001f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa |..............U.| ``` +Links +----- +* [C/C++ reference](https://en.cppreference.com/) diff --git a/cstruct/c_expr.py b/cstruct/c_expr.py index d46406f..58fc1e3 100644 --- a/cstruct/c_expr.py +++ b/cstruct/c_expr.py @@ -58,6 +58,8 @@ def c_eval(expr: str) -> Union[int, float]: try: expr = expr.replace("!", " not ").replace("&&", " and ").replace("||", " or ") return eval_node(ast.parse(expr.strip()).body[0]) + except EvalError: + raise except Exception: raise EvalError @@ -67,6 +69,11 @@ def eval_node(node: ast.stmt) -> Union[int, float]: result = handler(node) if isinstance(result, bool): # convert bool to int return 1 if result else 0 + elif isinstance(result, str): # convert char to int + if len(result) != 1: + raise EvalError("Multi-character constant") + else: + return ord(result) return result diff --git a/tests/test_c_expr.py b/tests/test_c_expr.py index 294fa24..da8c89b 100644 --- a/tests/test_c_expr.py +++ b/tests/test_c_expr.py @@ -78,3 +78,8 @@ def test_c_expr_compare(): assert c_eval("3 <= 30") == 1 define("A10", 10) assert c_eval("((A10 < 6) || (A10>10))") == 0 + + +def test_c_expr_char(): + assert c_eval("'A'") == 65 + assert c_eval("'B'") == 66 diff --git a/tests/test_cenum.py b/tests/test_cenum.py index 5823a12..bea6aa2 100644 --- a/tests/test_cenum.py +++ b/tests/test_cenum.py @@ -104,3 +104,9 @@ def test_type(): color = cstruct.parse("enum Color : unsigned short { red, green, blue };") assert color.__size__ == 2 assert cstruct.sizeof("enum Color") == 2 + + +def test_char(): + direction = cstruct.parse("enum Direction { left = 'l', right = 'r' };") + assert direction.__size__ == 4 + assert cstruct.sizeof("enum Direction") == 4 From 45a5ea480da47f2e5e190ed11b28d83a841cd3cc Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Sun, 20 Nov 2022 16:20:02 +0000 Subject: [PATCH 68/95] native type test --- tests/test_native_types.py | 59 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 tests/test_native_types.py diff --git a/tests/test_native_types.py b/tests/test_native_types.py new file mode 100644 index 0000000..5a46710 --- /dev/null +++ b/tests/test_native_types.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python +# ***************************************************************************** +# +# Copyright (c) 2013-2019 Andrea Bonomi +# +# Published under the terms of the MIT license. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. +# +# ***************************************************************************** + +from cstruct import MemCStruct, sizeof +from cstruct.base import TYPEDEFS +from cstruct.native_types import AbstractNativeType + + +class SizeT(AbstractNativeType): + type_name = "size_t" + native_format = "Q" + + +class StructWithSizeT(MemCStruct): + __def__ = """ + typedef size_t size; + typedef unsigned long long ull; + + struct StructWithSizeT { + ull a; + size_t b; + size c; + } + """ + + +def test_typedef(): + assert TYPEDEFS["size"] == "size_t" + assert TYPEDEFS["ull"] == "unsigned long long" + + +def test_sizeof_custom_native_type(): + assert sizeof("size_t") == 8 + assert sizeof("ull") == 8 + assert sizeof("struct StructWithSizeT") == 8 * 3 From 66041fa76d30847a89c8e5c6d0f6f55c356696bf Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Sun, 20 Nov 2022 16:37:59 +0000 Subject: [PATCH 69/95] example of integration with libc via ctypes --- README.md | 1 + docs/examples/dir.md | 6 +++ examples/dir.c | 22 +++++++++++ examples/dir.py | 94 ++++++++++++++++++++++++++++++++++++++++++++ examples/dir.sh | 3 ++ mkdocs.yml | 1 + 6 files changed, 127 insertions(+) create mode 100644 docs/examples/dir.md create mode 100644 examples/dir.c create mode 100644 examples/dir.py create mode 100755 examples/dir.sh diff --git a/README.md b/README.md index 71e547b..161c36e 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ Examples * [Read the DOS-type (MBR) partition table](https://python-cstruct.readthedocs.io/en/latest/examples/fdisk/) * [Print information about logged uses](https://python-cstruct.readthedocs.io/en/latest/examples/who/) * [Flexible Array Member (FAM)](https://python-cstruct.readthedocs.io/en/latest/examples/flexible_array/) +* [libc integration (using ctypes)](https://python-cstruct.readthedocs.io/en/latest/examples/dir/) Features diff --git a/docs/examples/dir.md b/docs/examples/dir.md new file mode 100644 index 0000000..3710ad1 --- /dev/null +++ b/docs/examples/dir.md @@ -0,0 +1,6 @@ +The following program prints the names of the files in a directory, +calling the libc functions `getcwd`, `opendir`, `readdir`, and `closedir`: + +``` +{!examples/dir.py!} +``` diff --git a/examples/dir.c b/examples/dir.c new file mode 100644 index 0000000..912a7f7 --- /dev/null +++ b/examples/dir.c @@ -0,0 +1,22 @@ +/* https://www.gnu.org/software/libc/manual/html_mono/libc.html#Simple-Directory-Lister */ + +#include +#include +#include + +int main (void) { + DIR *dp; + struct dirent *ep; + + dp = opendir("."); + if (dp != NULL) { + while (ep = readdir (dp)) { + puts(ep->d_name); + } + closedir(dp); + } else { + perror("Couldn't open the directory"); + } + + return 0; +} diff --git a/examples/dir.py b/examples/dir.py new file mode 100644 index 0000000..0cde462 --- /dev/null +++ b/examples/dir.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python +import cstruct +import ctypes +import sys + +libc = ctypes.cdll.LoadLibrary("libc.so.6") +# opendir +libc.opendir.argtypes = [ctypes.c_char_p] +libc.opendir.restype = ctypes.c_void_p +# readdir +libc.readdir.argtypes = [ctypes.c_void_p] +libc.readdir.restype = ctypes.c_void_p +# closedir +libc.closedir.argtypes = [ctypes.c_void_p] +libc.closedir.restype = ctypes.c_int + + +class DType(cstruct.CEnum): + __size__ = 1 + __def__ = """ + enum d_type { + DT_UNKNOWN = 0x0, + DT_FIFO = 0x1, + DT_CHR = 0x2, + DT_DIR = 0x4, + DT_BLK = 0x6, + DT_REG = 0x8, + DT_LNK = 0xa, + DT_SOCK = 0xc + }; + """ + + def __str__(self): + return { + DType.DT_UNKNOWN: "", + DType.DT_FIFO: "", + DType.DT_CHR: "", + DType.DT_DIR: "", + DType.DT_BLK: "", + DType.DT_REG: "", + DType.DT_LNK: "", + DType.DT_SOCK: "", + }[self] + + +class Dirent(cstruct.MemCStruct): + __def__ = """ + #define PATH_MAX 4096 + + typedef long ino_t; + typedef long off_t; + + struct dirent { + ino_t d_ino; /* Inode number */ + off_t d_off; /* Not an offset */ + unsigned short d_reclen; /* Length of this record */ + unsigned char d_type; /* Type of file; not supported + by all filesystem types */ + char d_name[256]; /* Null-terminated filename */ + }; + """ + + @property + def name(self): + return ctypes.c_char_p(self.d_name).value.decode("ascii") + + @property + def type(self): + return DType(self.d_type) + + +def main(): + if len(sys.argv) > 1: + cwd = ctypes.create_string_buffer(sys.argv[1].encode("ascii")) + else: + # Get current dir + cwd = ctypes.create_string_buffer(cstruct.getdef("PATH_MAX") + 1) + assert libc.getcwd(cwd, ctypes.sizeof(cwd)) != 0 + # Open dir + dp = libc.opendir(cwd) + assert dp != 0 + # Read dir entries + ep = libc.readdir(dp) + while ep: + contents = ctypes.cast(ep, ctypes.POINTER(ctypes.c_char * Dirent.size)).contents + dirent = Dirent(contents) + print(f"{dirent.d_ino:8} {dirent.type:10} {dirent.name}") + ep = libc.readdir(dp) + # Close dir + libc.closedir(dp) + + +if __name__ == "__main__": + main() diff --git a/examples/dir.sh b/examples/dir.sh new file mode 100755 index 0000000..8fc463c --- /dev/null +++ b/examples/dir.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env sh +cd "$(dirname "$0")/.." || exit +python -m examples.dir $* diff --git a/mkdocs.yml b/mkdocs.yml index 435ebbe..29c11cf 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -20,6 +20,7 @@ nav: - Code of Conduct: CODE_OF_CONDUCT.md - Source Code Repository: "https://github.com/andreax79/python-cstruct" - Examples: + - "dir.py": examples/dir.md - "fdisk.py": examples/fdisk.md - "flexible_array.py": examples/flexible_array.md - "who.py": examples/who.md From 84be7faee3a8edd7295874f5d6821ca979586845 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Sun, 20 Nov 2022 16:54:06 +0000 Subject: [PATCH 70/95] support unpack from ctype pointers --- cstruct/mem_cstruct.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cstruct/mem_cstruct.py b/cstruct/mem_cstruct.py index dc2f9c3..e078f62 100644 --- a/cstruct/mem_cstruct.py +++ b/cstruct/mem_cstruct.py @@ -73,6 +73,9 @@ def unpack_from(self, buffer: Optional[bytes], offset: int = 0, flexible_array_l self.__mem__ = ctypes.create_string_buffer(self.size + 1) elif isinstance(buffer, ctypes.Array): self.__mem__ = buffer + elif isinstance(buffer, int): + # buffer is a pointer + self.__mem__ = ctypes.cast(buffer, ctypes.POINTER(ctypes.c_char * self.size)).contents else: self.__mem__ = ctypes.create_string_buffer(buffer) for field, field_type in self.__fields_types__.items(): From 1dafc6b1c03a952133f409ae65c1bb94757f2469 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Sun, 20 Nov 2022 20:33:52 +0000 Subject: [PATCH 71/95] add support for char constants in python < 3.8 --- cstruct/c_expr.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cstruct/c_expr.py b/cstruct/c_expr.py index 58fc1e3..b7d0796 100644 --- a/cstruct/c_expr.py +++ b/cstruct/c_expr.py @@ -126,6 +126,7 @@ def eval_call(node) -> Union[int, float]: ast.Name: eval_get, ast.Call: eval_call, Constant: lambda node: node.value, + ast.Str: lambda node: node.s, # python < 3.8 # and/or ast.BoolOp: lambda node: OPS[type(node.op)](node), # and/or operator ast.And: lambda node: all(eval_node(x) for x in node.values), # && operator From cc86c656445b11a3d2e46a596c7e99e856e2dd39 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Sun, 20 Nov 2022 20:37:55 +0000 Subject: [PATCH 72/95] version 5.1 --- changelog.txt | 14 ++++++++++++++ cstruct/__init__.py | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index a7a35c2..d9b4924 100644 --- a/changelog.txt +++ b/changelog.txt @@ -190,3 +190,17 @@ - Documentation and examples - Restructure setup + +## 5.1 + +2022-11-20 + +### Improved + +- Support unpack from ctype pointers + +### Added + +- Add support for char constants +- Add native type test +- dir.py example diff --git a/cstruct/__init__.py b/cstruct/__init__.py index 13af539..cbea067 100644 --- a/cstruct/__init__.py +++ b/cstruct/__init__.py @@ -24,7 +24,7 @@ __author__ = "Andrea Bonomi " __license__ = "MIT" -__version__ = "5.0" +__version__ = "5.1" __date__ = "15 August 2013" from typing import Any, Dict, Optional, Type, Union From bd3ff53e2175c5c134da4bd6745ed6d36d0fdcc4 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Wed, 23 Nov 2022 09:50:34 +0000 Subject: [PATCH 73/95] fix nested struct unpack --- cstruct/mem_cstruct.py | 3 ++- tests/test_nested.py | 49 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/cstruct/mem_cstruct.py b/cstruct/mem_cstruct.py index e078f62..fcbf41f 100644 --- a/cstruct/mem_cstruct.py +++ b/cstruct/mem_cstruct.py @@ -56,6 +56,7 @@ class MemCStruct(AbstractCStruct): """ __mem__ = None + __base__ = 0 def unpack_from(self, buffer: Optional[bytes], offset: int = 0, flexible_array_length: Optional[int] = None) -> bool: """ @@ -101,7 +102,7 @@ def pack(self) -> bytes: Returns: bytes: The packed structure """ - return self.__mem__.raw[:-1] # the buffer is one item larger than its size and the last element is NUL + return self.__mem__.raw[self.__base__ : self.__base__ + self.size] def set_flexible_array_length(self, flexible_array_length: Optional[int]) -> None: """ diff --git a/tests/test_nested.py b/tests/test_nested.py index b6b0bbf..6acf9d8 100644 --- a/tests/test_nested.py +++ b/tests/test_nested.py @@ -210,3 +210,52 @@ def test_nested_anonymous_union_struct(): assert o1.format1.field1 == 11 assert o1.format1.field2 == 12 assert o1.format1.field3 == 13 + + +def test_nested_struct_offset(): + cstruct.parse( + """ + struct op_a { + int a; + }; + + struct op_b { + char a; + char b; + char c; + }; + """, + __byte_order__=cstruct.LITTLE_ENDIAN, + ) + + Op = cstruct.parse( + """ + struct op { + char preamble[10]; + uint64_t magic; + union { + struct op_a a_op; + struct op_b b_op; + } u1; + struct op_a aaa; + }; + """, + __byte_order__=cstruct.LITTLE_ENDIAN, + ) + + o = Op() + o.preamble = b'ciao_ciao' + o.magic = 3771778641802345472 + o.u1.a_op.a = 2022 + o.aaa.a = 0x33333333 + assert o.u1.b_op.a == b'\xe6' + assert o.u1.b_op.b == b'\x07' + assert o.u1.b_op.c == b'\x00' + assert o.__base__ == 0 + assert o.u1.__base__ >= 10 + assert o.u1.__base__ == o.u1.a_op.__base__ + assert o.u1.__base__ == o.u1.b_op.__base__ + assert o.aaa.__base__ > o.u1.__base__ + assert o.pack() == b'ciao_ciao\x00\x00\xbc\x08\xe4\xb0\x0cX4\xe6\x07\x00\x003333' + assert o.u1.pack() == b'\xe6\x07\x00\x00' + assert o.aaa.pack() == b'3333' From 364a13f0742f7c762ea2f7502241bb10aff68927 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Wed, 23 Nov 2022 11:02:20 +0000 Subject: [PATCH 74/95] fix nested anonymous union offset --- cstruct/c_parser.py | 4 +++ tests/test_nested.py | 66 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/cstruct/c_parser.py b/cstruct/c_parser.py index 16cedb5..a6865bf 100644 --- a/cstruct/c_parser.py +++ b/cstruct/c_parser.py @@ -426,6 +426,10 @@ def parse_struct( for nested_field_name, nested_field_type in field_type.ref.__fields_types__.items(): if nested_field_name in fields_types: raise ParserError(f"Duplicate member `{nested_field_name}`") + # set the corret offset + nested_field_type = nested_field_type.copy() + nested_field_type.base_offset = offset + nested_field_type.base_offset + nested_field_type.offset = offset + nested_field_type.offset fields_types[nested_field_name] = nested_field_type vname = f"__anonymous{anonymous}" anonymous += 1 diff --git a/tests/test_nested.py b/tests/test_nested.py index 6acf9d8..3b64a71 100644 --- a/tests/test_nested.py +++ b/tests/test_nested.py @@ -259,3 +259,69 @@ def test_nested_struct_offset(): assert o.pack() == b'ciao_ciao\x00\x00\xbc\x08\xe4\xb0\x0cX4\xe6\x07\x00\x003333' assert o.u1.pack() == b'\xe6\x07\x00\x00' assert o.aaa.pack() == b'3333' + + +def test_nested_anonymous_struct_offset(): + cstruct.parse( + """ + struct op_a1 { + int a; + }; + + struct op_b1 { + char a; + char b; + char c; + }; + """, + __byte_order__=cstruct.LITTLE_ENDIAN, + ) + + Opu = cstruct.parse( + """ + struct opu { + char preamble[10]; + uint64_t magic; + union { + struct op_a1 a_op; + struct op_b1 b_op; + }; + struct op_a aaa; + }; + """, + __byte_order__=cstruct.LITTLE_ENDIAN, + ) + + o = Opu() + o.preamble = b'ciao_ciao' + o.magic = 3771778641802345472 + o.__anonymous0.a_op.a = 2022 + o.aaa.a = 0x33333333 + assert o.__anonymous0.b_op.a == b'\xe6' + assert o.__anonymous0.b_op.b == b'\x07' + assert o.__anonymous0.b_op.c == b'\x00' + assert o.__base__ == 0 + assert o.__anonymous0.__base__ >= 10 + assert o.__anonymous0.__base__ == o.__anonymous0.a_op.__base__ + assert o.__anonymous0.__base__ == o.__anonymous0.b_op.__base__ + assert o.aaa.__base__ > o.__anonymous0.__base__ + assert o.pack() == b'ciao_ciao\x00\x00\xbc\x08\xe4\xb0\x0cX4\xe6\x07\x00\x003333' + assert o.__anonymous0.pack() == b'\xe6\x07\x00\x00' + assert o.aaa.pack() == b'3333' + + o = Opu() + o.preamble = b'ciao_ciao' + o.magic = 3771778641802345472 + o.a_op.a = 2022 + o.aaa.a = 0x33333333 + assert o.b_op.a == b'\xe6' + assert o.b_op.b == b'\x07' + assert o.b_op.c == b'\x00' + assert o.__base__ == 0 + assert o.__anonymous0.__base__ >= 10 + assert o.__anonymous0.__base__ == o.a_op.__base__ + assert o.__anonymous0.__base__ == o.b_op.__base__ + assert o.aaa.__base__ > o.__base__ + assert o.pack() == b'ciao_ciao\x00\x00\xbc\x08\xe4\xb0\x0cX4\xe6\x07\x00\x003333' + assert o.a_op.pack() == b'\xe6\x07\x00\x00' + assert o.aaa.pack() == b'3333' From e3c44497eef61978709ae9ada537125ab92a5014 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Wed, 23 Nov 2022 11:07:07 +0000 Subject: [PATCH 75/95] fix inspect offset for nested struct/union --- cstruct/abstract.py | 13 ++++++++++--- tests/test_nested.py | 7 +++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/cstruct/abstract.py b/cstruct/abstract.py index d97bca5..b1f6e35 100644 --- a/cstruct/abstract.py +++ b/cstruct/abstract.py @@ -238,18 +238,25 @@ def inspect(self, start_addr: Optional[int] = None, end_addr: Optional[int] = No """ buffer = StringIO() if hasattr(self, "__mem__"): - mem = self.__mem__ + mem = self.__mem__[self.__base__ :] else: mem = self.pack() - for i in range(start_addr or 0, end_addr or self.size, 16): - row = mem[i : i + 16] + if end_addr is None: + end_addr = self.size + for i in range(start_addr or 0, end_addr, 16): + row = mem[i : min(i + 16, end_addr)] buffer.write(f"{i:08x} ") for j, c in enumerate(row): separator = " " if j == 7 else "" buffer.write(f" {c:02x}{separator}") + for j in range(len(row) - 1, 15): + separator = " " if j == 7 else "" + buffer.write(f" {separator}") buffer.write(" |") for c in row: buffer.write(chr(c) if c >= 32 and c < 127 else ".") + for j in range(len(row) - 1, 15): + buffer.write(" ") buffer.write("|") buffer.write("\n") buffer.seek(0, 0) diff --git a/tests/test_nested.py b/tests/test_nested.py index 3b64a71..c5a877b 100644 --- a/tests/test_nested.py +++ b/tests/test_nested.py @@ -259,6 +259,8 @@ def test_nested_struct_offset(): assert o.pack() == b'ciao_ciao\x00\x00\xbc\x08\xe4\xb0\x0cX4\xe6\x07\x00\x003333' assert o.u1.pack() == b'\xe6\x07\x00\x00' assert o.aaa.pack() == b'3333' + assert o.u1.a_op.inspect() == "00000000 e6 07 00 00 |.... |\n" + assert o.u1.b_op.inspect() == "00000000 e6 07 00 |... |\n" def test_nested_anonymous_struct_offset(): @@ -308,6 +310,9 @@ def test_nested_anonymous_struct_offset(): assert o.pack() == b'ciao_ciao\x00\x00\xbc\x08\xe4\xb0\x0cX4\xe6\x07\x00\x003333' assert o.__anonymous0.pack() == b'\xe6\x07\x00\x00' assert o.aaa.pack() == b'3333' + assert o.__anonymous0.inspect() == "00000000 e6 07 00 00 |.... |\n" + assert o.__anonymous0.a_op.inspect() == "00000000 e6 07 00 00 |.... |\n" + assert o.__anonymous0.b_op.inspect() == "00000000 e6 07 00 |... |\n" o = Opu() o.preamble = b'ciao_ciao' @@ -325,3 +330,5 @@ def test_nested_anonymous_struct_offset(): assert o.pack() == b'ciao_ciao\x00\x00\xbc\x08\xe4\xb0\x0cX4\xe6\x07\x00\x003333' assert o.a_op.pack() == b'\xe6\x07\x00\x00' assert o.aaa.pack() == b'3333' + assert o.a_op.inspect() == "00000000 e6 07 00 00 |.... |\n" + assert o.b_op.inspect() == "00000000 e6 07 00 |... |\n" From d1223b542ae4d890659b11a09d8d07d3028c7786 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Wed, 23 Nov 2022 11:08:50 +0000 Subject: [PATCH 76/95] version 5.2 --- changelog.txt | 10 ++++++++++ cstruct/__init__.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index d9b4924..6aece75 100644 --- a/changelog.txt +++ b/changelog.txt @@ -204,3 +204,13 @@ - Add support for char constants - Add native type test - dir.py example + +## 5.2 + +2022-11-23 + +### Fix + +- nested struct unpack fix +- nested anonymous union offset fix +- inspect offset for nested struct/union fix diff --git a/cstruct/__init__.py b/cstruct/__init__.py index cbea067..b549276 100644 --- a/cstruct/__init__.py +++ b/cstruct/__init__.py @@ -24,7 +24,7 @@ __author__ = "Andrea Bonomi " __license__ = "MIT" -__version__ = "5.1" +__version__ = "5.2" __date__ = "15 August 2013" from typing import Any, Dict, Optional, Type, Union From 65e266e4c41d69af309fe54f6d340b8280ae3102 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Wed, 14 Dec 2022 10:26:35 +0100 Subject: [PATCH 77/95] Upgrade setuptools to 65.5.1 --- requirements-dev.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements-dev.txt b/requirements-dev.txt index 7584936..eaeedf2 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,3 +11,4 @@ mkdocstrings[python] mkdocs-material markdown_include build +setuptools>=65.5.1 # not directly required, pinned by Snyk to avoid a vulnerability From 1d818a39f759c512e8e6a83ebe0255e2a58463d3 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Wed, 14 Dec 2022 10:36:06 +0100 Subject: [PATCH 78/95] Update test version matrix --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 45e5185..28f196a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,11 +11,11 @@ jobs: strategy: matrix: python: - - "3.6" - "3.7" - "3.8" - "3.9" - "3.10" + - "3.11" runs-on: ubuntu-latest From 854b2e32bdd8d98239fc06dd6b951260a944bb9a Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Thu, 28 Dec 2023 13:39:01 +0100 Subject: [PATCH 79/95] Update github workflows --- .github/workflows/release.yml | 8 ++++---- .github/workflows/tests.yml | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c1e0e3b..6b8f5e0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,19 +31,19 @@ jobs: strategy: matrix: - python-version: ['3.9'] + python-version: ['3.12'] steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Cache pip - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ~/.cache/pip key: ${{ runner.os }}-python-${{ matrix.python }}-pip-${{ hashFiles('**/requirements*.txt') }}-git-${{ github.sha }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 28f196a..41d40e3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,25 +11,25 @@ jobs: strategy: matrix: python: - - "3.7" - "3.8" - "3.9" - "3.10" - "3.11" + - "3.12" runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} - name: Cache pip - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ~/.cache/pip key: ${{ runner.os }}-python-${{ matrix.python }}-pip-${{ hashFiles('**/requirements*.txt') }}-git-${{ github.sha }} From c1ac3047f9f26ab16baf9eb761b4566f20d03778 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Thu, 28 Dec 2023 13:40:43 +0100 Subject: [PATCH 80/95] Python 3.12 --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 2b497a6..59d4b3f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,6 +25,7 @@ classifiers = Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 project_urls = Bug Tracker = http://github.com/andreax79/python-cstruct/issues Documentation = https://python-cstruct.readthedocs.io/en/latest/ From 1b980819c00c58b23b0d29c4ccab070cbddfba30 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Thu, 28 Dec 2023 13:47:25 +0100 Subject: [PATCH 81/95] isort --- Makefile | 10 ++++++++-- README.md | 1 + cstruct/__init__.py | 15 ++++++++------- cstruct/abstract.py | 17 +++++++++-------- cstruct/base.py | 4 ++-- cstruct/c_expr.py | 3 ++- cstruct/c_parser.py | 9 +++++---- cstruct/cstruct.py | 3 ++- cstruct/field.py | 5 +++-- cstruct/mem_cstruct.py | 3 ++- cstruct/native_types.py | 2 +- examples/dir.py | 3 ++- examples/fdisk.py | 5 +++-- examples/flexible_array.py | 3 ++- examples/who.py | 5 +++-- pyproject.toml | 3 +++ requirements-dev.txt | 19 ++++++++++--------- tests/test_alignment.py | 3 ++- tests/test_c_expr.py | 2 +- tests/test_cenum.py | 4 +++- tests/test_cstruct.py | 8 +++++--- tests/test_define.py | 4 +++- tests/test_get_type.py | 1 + tests/test_memcstruct.py | 6 ++++-- tests/test_nested.py | 2 +- tests/test_pickle.py | 1 + tests/test_typdef.py | 2 +- tests/test_union.py | 3 ++- 28 files changed, 90 insertions(+), 56 deletions(-) diff --git a/Makefile b/Makefile index 6b9e616..8986a88 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ SHELL=/bin/bash -e help: @echo - make black ------ Format code + @echo - make isort ------ Sort imports @echo - make clean ------ Clean virtual environment @echo - make coverage --- Run tests coverage @echo - make docs ------- Make docs @@ -11,8 +12,13 @@ help: @echo - make typecheck -- Typecheck @echo - make venv ------- Create virtual environment -black: - black -S cstruct tests examples setup.py +.PHONY: isort +isort: + @isort --profile black cstruct tests examples setup.py + +.PHONY: black +black: isort + @black -S cstruct tests examples setup.py clean: -rm -rf build dist diff --git a/README.md b/README.md index 161c36e..de0a08d 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ C-style structs for Python [![PyPI](https://img.shields.io/pypi/pyversions/cstruct.svg)](https://pypi.org/project/cstruct) [![Downloads](https://pepy.tech/badge/cstruct/month)](https://pepy.tech/project/cstruct) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/) [![Known Vulnerabilities](https://snyk.io/test/github/andreax79/python-cstruct/badge.svg)](https://snyk.io/test/github/andreax79/python-cstruct) [![Documentation](https://readthedocs.org/projects/python-cstruct/badge/?version=latest)](https://python-cstruct.readthedocs.io/en/latest/) diff --git a/cstruct/__init__.py b/cstruct/__init__.py index b549276..1cf8e4f 100644 --- a/cstruct/__init__.py +++ b/cstruct/__init__.py @@ -28,21 +28,22 @@ __date__ = "15 August 2013" from typing import Any, Dict, Optional, Type, Union + +from .abstract import AbstractCEnum, AbstractCStruct, CStructMeta from .base import ( - LITTLE_ENDIAN, BIG_ENDIAN, + CHAR_ZERO, + DEFINES, + ENUMS, + LITTLE_ENDIAN, NATIVE_ORDER, STRUCTS, - ENUMS, - DEFINES, TYPEDEFS, - CHAR_ZERO, ) -from .abstract import CStructMeta, AbstractCStruct, AbstractCEnum -from .cstruct import CStruct from .c_parser import parse_struct_def -from .mem_cstruct import MemCStruct from .cenum import CEnum +from .cstruct import CStruct +from .mem_cstruct import MemCStruct from .native_types import get_native_type __all__ = [ diff --git a/cstruct/abstract.py b/cstruct/abstract.py index b1f6e35..896bb50 100644 --- a/cstruct/abstract.py +++ b/cstruct/abstract.py @@ -24,18 +24,19 @@ # IN THE SOFTWARE. # +import hashlib +import struct from abc import ABCMeta from collections import OrderedDict -from typing import Any, BinaryIO, List, Dict, Optional, Type, Tuple, Union -import hashlib +from enum import EnumMeta, IntEnum, _EnumDict from io import StringIO -from enum import IntEnum, EnumMeta, _EnumDict -import struct -from .base import STRUCTS, ENUMS, DEFAULT_ENUM_SIZE, ENUM_SIZE_TO_C_TYPE -from .c_parser import parse_struct, parse_struct_def, parse_enum_def, parse_enum, Tokens -from .field import calculate_padding, FieldType -from .native_types import get_native_type +from typing import Any, BinaryIO, Dict, List, Optional, Tuple, Type, Union + +from .base import DEFAULT_ENUM_SIZE, ENUM_SIZE_TO_C_TYPE, ENUMS, STRUCTS +from .c_parser import Tokens, parse_enum, parse_enum_def, parse_struct, parse_struct_def from .exceptions import CStructException, ParserError +from .field import FieldType, calculate_padding +from .native_types import get_native_type __all__ = ["CStructMeta", "AbstractCStruct", "CEnumMeta", "AbstractCEnum"] diff --git a/cstruct/base.py b/cstruct/base.py index 1ee83b3..fc3a29c 100644 --- a/cstruct/base.py +++ b/cstruct/base.py @@ -22,10 +22,10 @@ # IN THE SOFTWARE. # -from typing import Any, Dict, Type, TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Dict, Type if TYPE_CHECKING: - from .abstract import AbstractCStruct, AbstractCEnum + from .abstract import AbstractCEnum, AbstractCStruct __all__ = [ "LITTLE_ENDIAN", diff --git a/cstruct/c_expr.py b/cstruct/c_expr.py index b7d0796..b2d16c6 100644 --- a/cstruct/c_expr.py +++ b/cstruct/c_expr.py @@ -24,7 +24,8 @@ import ast import operator -from typing import Any, Callable, Dict, Union, Type, TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Callable, Dict, Type, Union + from .base import DEFINES, STRUCTS from .exceptions import EvalError diff --git a/cstruct/c_parser.py b/cstruct/c_parser.py index a6865bf..5b15014 100644 --- a/cstruct/c_parser.py +++ b/cstruct/c_parser.py @@ -24,15 +24,16 @@ import re from collections import OrderedDict -from typing import Union, Optional, Any, Dict, List, Type, TYPE_CHECKING -from .base import DEFINES, ENUMS, TYPEDEFS, STRUCTS -from .field import calculate_padding, Kind, FieldType +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union + +from .base import DEFINES, ENUMS, STRUCTS, TYPEDEFS from .c_expr import c_eval from .exceptions import CStructException, ParserError +from .field import FieldType, Kind, calculate_padding from .native_types import get_native_type if TYPE_CHECKING: - from .abstract import AbstractCStruct, AbstractCEnum + from .abstract import AbstractCEnum, AbstractCStruct __all__ = ["parse_struct", "parse_struct_def", "parse_enum_def", "Tokens"] diff --git a/cstruct/cstruct.py b/cstruct/cstruct.py index 78dd801..31c76fa 100644 --- a/cstruct/cstruct.py +++ b/cstruct/cstruct.py @@ -23,8 +23,9 @@ # from typing import List, Optional -from .base import CHAR_ZERO + from .abstract import AbstractCStruct +from .base import CHAR_ZERO class CStruct(AbstractCStruct): diff --git a/cstruct/field.py b/cstruct/field.py index bd9b3e3..1c30a35 100644 --- a/cstruct/field.py +++ b/cstruct/field.py @@ -25,10 +25,11 @@ import copy import struct from enum import Enum -from typing import Optional, Any, List, Type, TYPE_CHECKING +from typing import TYPE_CHECKING, Any, List, Optional, Type + from .base import NATIVE_ORDER -from .native_types import get_native_type from .exceptions import ParserError +from .native_types import get_native_type if TYPE_CHECKING: from .abstract import AbstractCStruct diff --git a/cstruct/mem_cstruct.py b/cstruct/mem_cstruct.py index fcbf41f..66578f9 100644 --- a/cstruct/mem_cstruct.py +++ b/cstruct/mem_cstruct.py @@ -22,9 +22,10 @@ # IN THE SOFTWARE. # -from typing import Any, List, Optional import ctypes import struct +from typing import Any, List, Optional + from .abstract import AbstractCStruct diff --git a/cstruct/native_types.py b/cstruct/native_types.py index 6a60f6c..f1e205c 100644 --- a/cstruct/native_types.py +++ b/cstruct/native_types.py @@ -24,7 +24,7 @@ import struct from abc import ABCMeta -from typing import Any, Dict, Type, Tuple +from typing import Any, Dict, Tuple, Type __all__ = [ "get_native_type", diff --git a/examples/dir.py b/examples/dir.py index 0cde462..8002067 100644 --- a/examples/dir.py +++ b/examples/dir.py @@ -1,8 +1,9 @@ #!/usr/bin/env python -import cstruct import ctypes import sys +import cstruct + libc = ctypes.cdll.LoadLibrary("libc.so.6") # opendir libc.opendir.argtypes = [ctypes.c_char_p] diff --git a/examples/fdisk.py b/examples/fdisk.py index fdb352c..2054f3e 100644 --- a/examples/fdisk.py +++ b/examples/fdisk.py @@ -1,9 +1,10 @@ #!/usr/bin/env python -from pathlib import Path import argparse -import cstruct import sys +from pathlib import Path + +import cstruct UNITS = ['B', 'K', 'M', 'G', 'T'] SECTOR_SIZE = 512 diff --git a/examples/flexible_array.py b/examples/flexible_array.py index 4f8c28a..2664683 100644 --- a/examples/flexible_array.py +++ b/examples/flexible_array.py @@ -1,9 +1,10 @@ #!/usr/bin/env python import random -from cstruct import MemCStruct from pathlib import Path +from cstruct import MemCStruct + class FlexArray(MemCStruct): __def__ = """ diff --git a/examples/who.py b/examples/who.py index 0569cd1..7737de7 100644 --- a/examples/who.py +++ b/examples/who.py @@ -1,10 +1,11 @@ #!/usr/bin/env python -from cstruct import parse, getdef, MemCStruct, NATIVE_ORDER -from pathlib import Path import argparse import sys import time +from pathlib import Path + +from cstruct import NATIVE_ORDER, MemCStruct, getdef, parse DEFAULT_FILENAME = "/var/run/utmp" diff --git a/pyproject.toml b/pyproject.toml index 074200f..ccb95b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,3 +10,6 @@ source = ["cstruct"] [tool.coverage.report] exclude_lines = [ "# pragma: no cover", "if TYPE_CHECKING:" ] + +[tool.isort] +profile = "black" diff --git a/requirements-dev.txt b/requirements-dev.txt index eaeedf2..ea2ede6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,14 +1,15 @@ +black +build coverage[toml] -pytest-cov flake8 -mypy -tox -black -twine<3.4 -pytest +isort +markdown_include mkdocs -mkdocstrings[python] mkdocs-material -markdown_include -build +mkdocstrings[python] +mypy +pytest +pytest-cov setuptools>=65.5.1 # not directly required, pinned by Snyk to avoid a vulnerability +tox +twine<3.4 diff --git a/tests/test_alignment.py b/tests/test_alignment.py index 609abf5..3895a2f 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -26,7 +26,8 @@ # ***************************************************************************** import sys -from cstruct import sizeof, typedef, define, CStruct, NATIVE_ORDER + +from cstruct import NATIVE_ORDER, CStruct, define, sizeof, typedef IS_64BITS = sys.maxsize > 2**32 diff --git a/tests/test_c_expr.py b/tests/test_c_expr.py index da8c89b..fe89dcb 100644 --- a/tests/test_c_expr.py +++ b/tests/test_c_expr.py @@ -25,7 +25,7 @@ # # ***************************************************************************** -from cstruct import parse, getdef, define +from cstruct import define, getdef, parse from cstruct.c_expr import c_eval diff --git a/tests/test_cenum.py b/tests/test_cenum.py index bea6aa2..4e9432c 100644 --- a/tests/test_cenum.py +++ b/tests/test_cenum.py @@ -1,7 +1,9 @@ +from enum import Enum + import pytest + import cstruct from cstruct import CEnum -from enum import Enum class Dummy(CEnum): diff --git a/tests/test_cstruct.py b/tests/test_cstruct.py index ae1d060..47594bf 100644 --- a/tests/test_cstruct.py +++ b/tests/test_cstruct.py @@ -25,12 +25,14 @@ # # ***************************************************************************** -import pytest -import cstruct -from cstruct import sizeof, typedef import io import os from pathlib import Path + +import pytest + +import cstruct +from cstruct import sizeof, typedef from cstruct.exceptions import ParserError MBR_DATA = (Path(__file__).parent.parent / "mbr").read_bytes() diff --git a/tests/test_define.py b/tests/test_define.py index 6a5a74e..e5d6c8f 100644 --- a/tests/test_define.py +++ b/tests/test_define.py @@ -26,9 +26,11 @@ # ***************************************************************************** import sys + import pytest + import cstruct -from cstruct import define, undef, sizeof, typedef +from cstruct import define, sizeof, typedef, undef from cstruct.exceptions import ParserError IS_64BITS = sys.maxsize > 2**32 diff --git a/tests/test_get_type.py b/tests/test_get_type.py index 72cb159..dd4b886 100644 --- a/tests/test_get_type.py +++ b/tests/test_get_type.py @@ -26,6 +26,7 @@ # ***************************************************************************** import pytest + import cstruct diff --git a/tests/test_memcstruct.py b/tests/test_memcstruct.py index bac47c1..603083b 100644 --- a/tests/test_memcstruct.py +++ b/tests/test_memcstruct.py @@ -25,11 +25,13 @@ # # ***************************************************************************** +import os +from pathlib import Path + import pytest + import cstruct from cstruct import sizeof, typedef -import os -from pathlib import Path from cstruct.exceptions import ParserError MBR_DATA = (Path(__file__).parent.parent / "mbr").read_bytes() diff --git a/tests/test_nested.py b/tests/test_nested.py index c5a877b..74b2b31 100644 --- a/tests/test_nested.py +++ b/tests/test_nested.py @@ -26,11 +26,11 @@ # ***************************************************************************** import pytest + import cstruct from cstruct import sizeof from cstruct.exceptions import ParserError - INVALID_ANONYMOUS = """ struct NestedStruct { struct { diff --git a/tests/test_pickle.py b/tests/test_pickle.py index 14789a0..29425c8 100644 --- a/tests/test_pickle.py +++ b/tests/test_pickle.py @@ -1,6 +1,7 @@ #!/usr/bin/env python import pickle + import cstruct diff --git a/tests/test_typdef.py b/tests/test_typdef.py index ca8a564..e0bb408 100644 --- a/tests/test_typdef.py +++ b/tests/test_typdef.py @@ -25,7 +25,7 @@ # # ***************************************************************************** -from cstruct import MemCStruct, NATIVE_ORDER +from cstruct import NATIVE_ORDER, MemCStruct from cstruct.base import TYPEDEFS diff --git a/tests/test_union.py b/tests/test_union.py index a13e97f..aa4b543 100644 --- a/tests/test_union.py +++ b/tests/test_union.py @@ -25,9 +25,10 @@ # # ***************************************************************************** +import struct + import cstruct from cstruct import sizeof -import struct class Position(cstruct.MemCStruct): From 1994571985bb5c93899ee75664e884133b054162 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Mon, 8 Jan 2024 15:22:35 +0100 Subject: [PATCH 82/95] fix struct in struct array parsing --- cstruct/c_parser.py | 51 ++++++++++++++++++++++---------------- tests/test_nested.py | 59 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 21 deletions(-) diff --git a/cstruct/c_parser.py b/cstruct/c_parser.py index 5b15014..21a6488 100644 --- a/cstruct/c_parser.py +++ b/cstruct/c_parser.py @@ -24,7 +24,7 @@ import re from collections import OrderedDict -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, Union from .base import DEFINES, ENUMS, STRUCTS, TYPEDEFS from .c_expr import c_eval @@ -101,6 +101,30 @@ def __str__(self) -> str: return str(self.tokens) +def parse_length(tokens: Tokens, next_token: str, vlen: int, flexible_array: bool) -> Tuple[str, int, bool]: + t = next_token.split("[") + if len(t) != 2: + raise ParserError(f"Error parsing: `{next_token}`") + next_token = t[0].strip() + vlen_part = t[1] + vlen_expr = [] + while not vlen_part.endswith("]"): + vlen_expr.append(vlen_part.split("]")[0].strip()) + vlen_part = tokens.pop() + t_vlen = vlen_part.split("]")[0].strip() + vlen_expr.append(vlen_part.split("]")[0].strip()) + t_vlen = " ".join(vlen_expr) + if not t_vlen: + flexible_array = True + vlen = 0 + else: + try: + vlen = c_eval(t_vlen) + except (ValueError, TypeError): + vlen = int(t_vlen) + return next_token, vlen, flexible_array + + def parse_type(tokens: Tokens, __cls__: Type["AbstractCStruct"], byte_order: Optional[str], offset: int) -> "FieldType": if len(tokens) < 2: raise ParserError("Parsing error") @@ -124,26 +148,7 @@ def parse_type(tokens: Tokens, __cls__: Type["AbstractCStruct"], byte_order: Opt c_type = "void *" # parse length if "[" in next_token: - t = next_token.split("[") - if len(t) != 2: - raise ParserError(f"Error parsing: `{next_token}`") - next_token = t[0].strip() - vlen_part = t[1] - vlen_expr = [] - while not vlen_part.endswith("]"): - vlen_expr.append(vlen_part.split("]")[0].strip()) - vlen_part = tokens.pop() - t_vlen = vlen_part.split("]")[0].strip() - vlen_expr.append(vlen_part.split("]")[0].strip()) - t_vlen = " ".join(vlen_expr) - if not t_vlen: - flexible_array = True - vlen = 0 - else: - try: - vlen = c_eval(t_vlen) - except (ValueError, TypeError): - vlen = int(t_vlen) + next_token, vlen, flexible_array = parse_length(tokens, next_token, vlen, flexible_array) tokens.push(next_token) # resolve typedefs while c_type in TYPEDEFS: @@ -421,6 +426,10 @@ def parse_struct( raise ParserError(f"Duplicate member `{vname}`") if vname in dir(__cls__): raise ParserError(f"Invalid reserved member name `{vname}`") + # parse length + if "[" in vname: + vname, field_type.vlen, field_type.flexible_array = parse_length(tokens, vname, 1, flexible_array) + flexible_array = flexible_array or field_type.flexible_array # anonymous nested union if vname == ";" and field_type.ref is not None and (__is_union__ or field_type.ref.__is_union__): # add the anonymous struct fields to the parent diff --git a/tests/test_nested.py b/tests/test_nested.py index 74b2b31..e527180 100644 --- a/tests/test_nested.py +++ b/tests/test_nested.py @@ -70,6 +70,17 @@ class NestedUnion(cstruct.MemCStruct): """ +class NestedStructArr(cstruct.MemCStruct): + __byte_order__ = cstruct.LITTLE_ENDIAN + __def__ = """ + struct NestedStructArr { + struct b { + int c; + } bval[10]; + }; + """ + + class NestedAnonymousUnion(cstruct.MemCStruct): __byte_order__ = cstruct.LITTLE_ENDIAN __def__ = """ @@ -332,3 +343,51 @@ def test_nested_anonymous_struct_offset(): assert o.aaa.pack() == b'3333' assert o.a_op.inspect() == "00000000 e6 07 00 00 |.... |\n" assert o.b_op.inspect() == "00000000 e6 07 00 |... |\n" + + +def test_nested_struct_array(): + Nested = cstruct.parse( + """ + struct Nested { + struct b { + int c; + } bval; + int a; + }; + """, + __byte_order__=cstruct.LITTLE_ENDIAN, + ) + assert len(Nested) == 8 + t = Nested() + assert "a" in t.__fields_types__ + assert "bval" in t.__fields_types__ + + NestedArray = cstruct.parse( + """ + struct NestedArray { + struct b { + int c; + } bval[2]; + int a; + }; + """, + __byte_order__=cstruct.LITTLE_ENDIAN, + ) + t = NestedArray() + assert "a" in t.__fields_types__ + assert "bval" in t.__fields_types__ + assert len(NestedArray) > len(Nested) + t.bval[0].c = 10 + t.bval[1].c = 11 + assert t.bval[0].c == 10 + assert t.bval[1].c == 11 + assert len(t.bval) == 2 + + assert len(NestedStructArr) == 40 + t = NestedStructArr() + assert "bval" in t.__fields_types__ + t.bval[0].c = 10 + t.bval[1].c = 11 + assert t.bval[0].c == 10 + assert t.bval[1].c == 11 + assert len(t.bval) == 10 From 19bbf493999e2ad9b4d7b094ddbdef5e79f6f60d Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Mon, 8 Jan 2024 15:40:59 +0100 Subject: [PATCH 83/95] version 5.3 --- .gitignore | 1 + changelog.txt | 96 +++++++++++++++------------------------------ cstruct/__init__.py | 2 +- 3 files changed, 34 insertions(+), 65 deletions(-) diff --git a/.gitignore b/.gitignore index ee62e3d..553e4ea 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,4 @@ nosetests.xml .mypy_cache pyvenv.cfg site/ +__pycache__ diff --git a/changelog.txt b/changelog.txt index 6aece75..8a7f723 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,16 +1,12 @@ -# python-cstruct changelog +# Changelog -## 1.0 - -2013-08-19 +## [1.0] - 2013-08-19 ### Added - initial version -## 1.2 - -2017-05-18 +## [1.2] - 2017-05-18 ### Improved @@ -22,49 +18,37 @@ - who.py example - a changelog :) -## 1.3 - -2017-05-21 +## [1.3] - 2017-05-21 ### Fix - default value fix -## 1.4 - -2017-06-02 +## [1.4] - 2017-06-02 ### Fix - default value fix -## 1.5 - -2017-07-22 +## [1.5] - 2017-07-22 ### Fix - compatibiliy fix -## 1.6 - -2017-12-12 +## [1.6] - 2017-12-12 ### Fix - fixed size of 64-bit integers, they now have 64 bits, not 32 -## 1.7 - -2018-03-14 +## [1.7] - 2018-03-14 ### Improved - add support for // comments -## 1.8 - -2018-10-30 +## [1.8] - 2018-10-30 ### Improved @@ -75,9 +59,7 @@ - fix Python 2.5 support in main module - examples fix -## 1.9 - -2019-07-09 +## [1.9] - 2019-07-09 ### Improved @@ -88,17 +70,13 @@ - flexible array parsing - union initial support -## 2.0 - -2020-04-11 +## [2.0] - 2020-04-11 ### Improved - drop Python 2 support -## 2.1 - -2020-10-09 +## [2.1] - 2020-10-09 ### Improved @@ -106,9 +84,7 @@ - Python 3.9 support - Github workfows -## 2.2 - -2022-08-23 +## [2.2] - 2022-08-23 ### Fix @@ -120,41 +96,31 @@ - pytest - black code style -## 2.3 - -2022-09-01 +## [2.3] - 2022-09-01 ### Fix - Fix compare with None -## 3.0 - -2022-09-05 +## [3.0] - 2022-09-05 ### Added - Flexible array support -## 3.1 - -2022-09-13 +## [3.1] - 2022-09-13 ### Added - Make CStruct/MemCStruct Pickle Friendly -## 3.2 - -2022-10-23 +## [3.2] - 2022-10-23 ### Fix - Fix padding tests on 32bit architectures -## 3.3 - -2022-10-24 +## [3.3] - 2022-10-24 ### Added @@ -164,9 +130,7 @@ - Fix padding tests on 32bit architectures -## 4.0 - -2022-11-01 +## [4.0] - 2022-11-01 ### Added @@ -176,9 +140,7 @@ - Python 3.11 support -## 5.0 - -2022-11-12 +## [5.0] - 2022-11-12 ### Added @@ -191,9 +153,7 @@ - Documentation and examples - Restructure setup -## 5.1 - -2022-11-20 +## [5.1] - 2022-11-20 ### Improved @@ -205,12 +165,20 @@ - Add native type test - dir.py example -## 5.2 - -2022-11-23 +## [5.2] - 2022-11-23 ### Fix - nested struct unpack fix - nested anonymous union offset fix - inspect offset for nested struct/union fix + +## [5.3] - 2024-01-08 + +### Fix + +- fix struct in struct array parsing + +### Improved + +- Python 3.12 support diff --git a/cstruct/__init__.py b/cstruct/__init__.py index 1cf8e4f..580b400 100644 --- a/cstruct/__init__.py +++ b/cstruct/__init__.py @@ -24,7 +24,7 @@ __author__ = "Andrea Bonomi " __license__ = "MIT" -__version__ = "5.2" +__version__ = "5.3" __date__ = "15 August 2013" from typing import Any, Dict, Optional, Type, Union From 1236e1e3be91177ed80538cb0a2508ff05fcff0d Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Fri, 27 Sep 2024 13:00:50 +0200 Subject: [PATCH 84/95] codespell --- Makefile | 6 +++++- cstruct/abstract.py | 6 +++--- cstruct/c_parser.py | 2 +- cstruct/cstruct.py | 2 +- cstruct/field.py | 2 +- cstruct/mem_cstruct.py | 2 +- requirements-dev.txt | 1 + 7 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 8986a88..5a2e117 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,10 @@ help: @echo - make typecheck -- Typecheck @echo - make venv ------- Create virtual environment +.PHONY: isort +codespell: + @codespell -w cstruct tests examples setup.py + .PHONY: isort isort: @isort --profile black cstruct tests examples setup.py @@ -46,6 +50,6 @@ typecheck: mypy --strict --no-warn-unused-ignores cstruct venv: - python3 -m virtualenv . + python3 -m venv . || python3 -m virtualenv . . bin/activate; pip install -Ur requirements.txt . bin/activate; pip install -Ur requirements-dev.txt diff --git a/cstruct/abstract.py b/cstruct/abstract.py index 896bb50..31a9ffb 100644 --- a/cstruct/abstract.py +++ b/cstruct/abstract.py @@ -80,13 +80,13 @@ class AbstractCStruct(metaclass=CStructMeta): __size__: int = 0 " Size in bytes " __fields__: List[str] = [] - " Struct/union fileds " + " Struct/union fields " __fields_types__: Dict[str, FieldType] " Dictionary mapping field names to types " __byte_order__: Optional[str] = None " Byte order " __alignment__: int = 0 - " Alignament " + " Alignment " __is_union__: bool = False " True if the class is an union, False if it is a struct " @@ -125,7 +125,7 @@ def parse( __is_union__: True for union, False for struct Returns: - cls: a new class mapping the defintion + cls: a new class mapping the definition """ cls_kargs: Dict[str, Any] = dict(kargs) if __byte_order__ is not None: diff --git a/cstruct/c_parser.py b/cstruct/c_parser.py index 21a6488..8c24e56 100644 --- a/cstruct/c_parser.py +++ b/cstruct/c_parser.py @@ -436,7 +436,7 @@ def parse_struct( for nested_field_name, nested_field_type in field_type.ref.__fields_types__.items(): if nested_field_name in fields_types: raise ParserError(f"Duplicate member `{nested_field_name}`") - # set the corret offset + # set the correct offset nested_field_type = nested_field_type.copy() nested_field_type.base_offset = offset + nested_field_type.base_offset nested_field_type.offset = offset + nested_field_type.offset diff --git a/cstruct/cstruct.py b/cstruct/cstruct.py index 31c76fa..9e899d9 100644 --- a/cstruct/cstruct.py +++ b/cstruct/cstruct.py @@ -48,7 +48,7 @@ def unpack_from(self, buffer: Optional[bytes], offset: int = 0, flexible_array_l Args: buffer: bytes to be unpacked offset: optional buffer offset - flexible_array_length: optional flexible array lenght (number of elements) + flexible_array_length: optional flexible array length (number of elements) """ self.set_flexible_array_length(flexible_array_length) if buffer is None: diff --git a/cstruct/field.py b/cstruct/field.py index 1c30a35..3568b36 100644 --- a/cstruct/field.py +++ b/cstruct/field.py @@ -192,7 +192,7 @@ def native_format(self) -> str: try: return get_native_type(self.c_type).native_format except KeyError: - raise ParserError(f"Unknow type `{self.c_type}`") + raise ParserError(f"Unknown type `{self.c_type}`") elif self.is_enum: return self.ref.__native_format__ else: diff --git a/cstruct/mem_cstruct.py b/cstruct/mem_cstruct.py index 66578f9..a0aaf1d 100644 --- a/cstruct/mem_cstruct.py +++ b/cstruct/mem_cstruct.py @@ -66,7 +66,7 @@ def unpack_from(self, buffer: Optional[bytes], offset: int = 0, flexible_array_l Args: buffer: bytes to be unpacked offset: optional buffer offset - flexible_array_length: optional flexible array lenght (number of elements) + flexible_array_length: optional flexible array length (number of elements) """ self.set_flexible_array_length(flexible_array_length) self.__base__ = offset # Base offset diff --git a/requirements-dev.txt b/requirements-dev.txt index ea2ede6..ea86fe9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,6 @@ black build +codespell coverage[toml] flake8 isort From 5bca4a1f756d9421fdb017d7fd8ffc0399c07c64 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Wed, 15 Jan 2025 22:42:54 +0100 Subject: [PATCH 85/95] Access to Python class attributes in struct definition --- .readthedocs.yaml | 9 ++++ README.md | 27 ++++++++++ changelog.txt | 10 ++++ cstruct/__init__.py | 4 +- cstruct/abstract.py | 20 +++++-- cstruct/base.py | 2 +- cstruct/c_expr.py | 50 ++++++++++++++++-- cstruct/c_parser.py | 45 +++++++++++----- cstruct/cstruct.py | 2 +- cstruct/exceptions.py | 7 ++- cstruct/field.py | 63 +++++++++++++++++----- cstruct/mem_cstruct.py | 6 +-- cstruct/native_types.py | 17 +++--- setup.cfg | 1 + tests/test_cstruct_var.py | 107 ++++++++++++++++++++++++++++++++++++++ 15 files changed, 321 insertions(+), 49 deletions(-) create mode 100644 tests/test_cstruct_var.py diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 8774cd0..b7168c0 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,7 +1,16 @@ # .readthedocs.yaml version: 2 + +# Set the OS and Python version +build: + os: ubuntu-22.04 + tools: + python: "3.12" + mkdocs: configuration: mkdocs.yml + +# Declare the Python requirements required to build the documentation python: install: - requirements: requirements-dev.txt diff --git a/README.md b/README.md index de0a08d..771adbd 100644 --- a/README.md +++ b/README.md @@ -197,6 +197,33 @@ pkg.length = 4 pkg.data = [10, 20, 30, 40] ``` +### Python object attributes + +In struct definition, you can access Python object attributes using `self`. +The value of expression accessing class attributes is evaluated at runtime. + +```python +class RT11DirectoryEntry(cstruct.CStruct): + + __byte_order__ = cstruct.LITTLE_ENDIAN + __def__ = """ + struct RT11DirectoryEntry { + uint8_t type; + uint8_t clazz; + uint16_t raw_filename1; + uint16_t raw_filename2; + uint16_t raw_extension; + uint16_t length; + uint8_t job; + uint8_t channel; + uint16_t raw_creation_date; + uint16_t extra_bytes[self.extra_bytes_len]; /* The size of the array is determined at runtime */ + }; + """ + + extra_bytes_len: int = 0 +``` + ### Pack and Unpack A code example illustrating how to use diff --git a/changelog.txt b/changelog.txt index 8a7f723..e0ff73f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -182,3 +182,13 @@ ### Improved - Python 3.12 support + +## [6.0] - 2025-01-16 + +### Added + +- access to Python class attributes in struct definition + +### Improved + +- Python 3.13 support diff --git a/cstruct/__init__.py b/cstruct/__init__.py index 580b400..c86d726 100644 --- a/cstruct/__init__.py +++ b/cstruct/__init__.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (c) 2013-2019 Andrea Bonomi +# Copyright (c) 2013-2025 Andrea Bonomi # # Published under the terms of the MIT license. # @@ -24,7 +24,7 @@ __author__ = "Andrea Bonomi " __license__ = "MIT" -__version__ = "5.3" +__version__ = "6.0" __date__ = "15 August 2013" from typing import Any, Dict, Optional, Type, Union diff --git a/cstruct/abstract.py b/cstruct/abstract.py index 31a9ffb..378d3ce 100644 --- a/cstruct/abstract.py +++ b/cstruct/abstract.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -# -# Copyright (c) 2013-2019 Andrea Bonomi +# Copyright (c) 2013-2025 Andrea Bonomi # # Published under the terms of the MIT license. # @@ -163,7 +161,7 @@ def set_flexible_array_length(self, flexible_array_length: Optional[int]) -> Non flexible_array: Optional[FieldType] = [x for x in self.__fields_types__.values() if x.flexible_array][0] if flexible_array is None: raise CStructException("Flexible array not found in struct") - flexible_array.vlen = flexible_array_length + flexible_array.vlen_ex = flexible_array_length def unpack(self, buffer: Optional[Union[bytes, BinaryIO]], flexible_array_length: Optional[int] = None) -> bool: """ @@ -202,6 +200,17 @@ def pack(self) -> bytes: # pragma: no cover """ raise NotImplementedError + def pack_into(self, buffer: bytearray, offset: int = 0) -> None: + """ + Pack the structure data into a buffer + + Args: + buffer: target buffer (must be large enough to contain the packed structure) + offset: optional buffer offset + """ + tmp = self.pack() + buffer[offset : offset + len(tmp)] = tmp + def clear(self) -> None: self.unpack(None) @@ -300,6 +309,9 @@ def __setstate__(self, state: bytes) -> bool: class CEnumMeta(EnumMeta): + __size__: int + __native_format__: str + class WrapperDict(_EnumDict): def __setitem__(self, key: str, value: Any) -> None: env = None diff --git a/cstruct/base.py b/cstruct/base.py index fc3a29c..ea5c223 100644 --- a/cstruct/base.py +++ b/cstruct/base.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (c) 2013-2019 Andrea Bonomi +# Copyright (c) 2013-2025 Andrea Bonomi # # Published under the terms of the MIT license. # diff --git a/cstruct/c_expr.py b/cstruct/c_expr.py index b2d16c6..70edf46 100644 --- a/cstruct/c_expr.py +++ b/cstruct/c_expr.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (c) 2013-2019 Andrea Bonomi +# Copyright (c) 2013-2025 Andrea Bonomi # # Published under the terms of the MIT license. # @@ -23,11 +23,12 @@ # import ast +import inspect import operator -from typing import TYPE_CHECKING, Any, Callable, Dict, Type, Union +from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Type, Union from .base import DEFINES, STRUCTS -from .exceptions import EvalError +from .exceptions import ContextNotFound, EvalError if TYPE_CHECKING: from .abstract import AbstractCStruct @@ -65,7 +66,36 @@ def c_eval(expr: str) -> Union[int, float]: raise EvalError +def eval_attribute_node(node: ast.Attribute) -> Union[int, float]: + """ + Evaluate node attribute, e.g. 'self.x' + Only 'self' is allowed. The attribute must be a number. + + Args: + node: attribute node + + Returns: + result: the attribute value + + Raises: + EvalError: expression result is not a number, or not self attribute + ContextNotFound: context is not defined + """ + if not node.value or node.value.id != "self": # type: ignore + raise EvalError("only self is allowed") + context = get_cstruct_context() + if context is None: + raise ContextNotFound("context is not defined") + result = getattr(context, node.attr) + if not isinstance(result, (int, float)): + raise EvalError("expression result is not a number") + return result + + def eval_node(node: ast.stmt) -> Union[int, float]: + if isinstance(node, ast.Attribute): + return eval_attribute_node(node) + handler = OPS[type(node)] result = handler(node) if isinstance(result, bool): # convert bool to int @@ -116,6 +146,20 @@ def eval_call(node) -> Union[int, float]: raise KeyError(node.func.id) +def get_cstruct_context() -> Optional["AbstractCStruct"]: + """ + Get the calling CStruct instance from the stack (if any) + """ + from .abstract import AbstractCStruct + + stack = inspect.stack() + for frame in stack: + caller_self = frame.frame.f_locals.get("self") + if isinstance(caller_self, AbstractCStruct): + return caller_self + return None + + try: Constant = ast.Constant except AttributeError: # python < 3.8 diff --git a/cstruct/c_parser.py b/cstruct/c_parser.py index 8c24e56..2a95705 100644 --- a/cstruct/c_parser.py +++ b/cstruct/c_parser.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (c) 2013-2019 Andrea Bonomi +# Copyright (c) 2013-2025 Andrea Bonomi # # Published under the terms of the MIT license. # @@ -24,11 +24,21 @@ import re from collections import OrderedDict -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, Union +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + List, + Optional, + Tuple, + Type, + Union, +) from .base import DEFINES, ENUMS, STRUCTS, TYPEDEFS from .c_expr import c_eval -from .exceptions import CStructException, ParserError +from .exceptions import CStructException, EvalError, ParserError from .field import FieldType, Kind, calculate_padding from .native_types import get_native_type @@ -41,7 +51,7 @@ SPACES = [" ", "\t", "\n"] -class Tokens(object): +class Tokens: def __init__(self, text: str) -> None: # remove the comments text = re.sub(r"//.*?$|/\*.*?\*/", "", text, flags=re.S | re.MULTILINE) @@ -59,7 +69,7 @@ def __init__(self, text: str) -> None: text = "\n".join(lines) self.tokens = self.tokenize(text) - def tokenize(self, text) -> List[str]: + def tokenize(self, text: str) -> List[str]: tokens: List[str] = [] t: List[str] = [] for c in text: @@ -72,7 +82,7 @@ def tokenize(self, text) -> List[str]: else: t.append(c) if t: - tokens.append(t.getvalue()) + tokens.append("".join(t)) return tokens def pop(self) -> str: @@ -101,7 +111,8 @@ def __str__(self) -> str: return str(self.tokens) -def parse_length(tokens: Tokens, next_token: str, vlen: int, flexible_array: bool) -> Tuple[str, int, bool]: +def parse_length(tokens: Tokens, next_token: str, flexible_array: bool) -> Tuple[str, Union[int, Callable[[], int]], bool]: + # Extract t_vlen t = next_token.split("[") if len(t) != 2: raise ParserError(f"Error parsing: `{next_token}`") @@ -114,14 +125,19 @@ def parse_length(tokens: Tokens, next_token: str, vlen: int, flexible_array: boo t_vlen = vlen_part.split("]")[0].strip() vlen_expr.append(vlen_part.split("]")[0].strip()) t_vlen = " ".join(vlen_expr) + # Evaluate t_vlen + vlen: Union[int, Callable[[], int]] if not t_vlen: + # If the length expression is empty, this is a flex array flexible_array = True vlen = 0 else: + # Evaluate the length expression + # If the length expression is not a constant, it is evaluated at runtime try: - vlen = c_eval(t_vlen) - except (ValueError, TypeError): - vlen = int(t_vlen) + vlen = int(c_eval(t_vlen)) + except EvalError: + vlen = lambda: int(c_eval(t_vlen)) return next_token, vlen, flexible_array @@ -133,7 +149,7 @@ def parse_type(tokens: Tokens, __cls__: Type["AbstractCStruct"], byte_order: Opt if c_type in ["signed", "unsigned", "struct", "union", "enum"] and len(tokens) > 1: c_type = c_type + " " + tokens.pop() - vlen = 1 + vlen: Union[int, Callable[[], int]] = 1 flexible_array = False if not c_type.endswith("{"): @@ -148,20 +164,21 @@ def parse_type(tokens: Tokens, __cls__: Type["AbstractCStruct"], byte_order: Opt c_type = "void *" # parse length if "[" in next_token: - next_token, vlen, flexible_array = parse_length(tokens, next_token, vlen, flexible_array) + next_token, vlen, flexible_array = parse_length(tokens, next_token, flexible_array) tokens.push(next_token) # resolve typedefs while c_type in TYPEDEFS: c_type = TYPEDEFS[c_type] # calculate fmt + ref: Union[None, Type[AbstractCEnum], Type[AbstractCStruct]] if c_type.startswith("struct ") or c_type.startswith("union "): # struct/union c_type, tail = c_type.split(" ", 1) kind = Kind.STRUCT if c_type == "struct" else Kind.UNION if tokens.get() == "{": # Named nested struct tokens.push(tail) tokens.push(c_type) - ref: Union[Type[AbstractCEnum], Type[AbstractCStruct]] = __cls__.parse(tokens, __name__=tail, __byte_order__=byte_order) + ref = __cls__.parse(tokens, __name__=tail, __byte_order__=byte_order) elif tail == "{": # Unnamed nested struct tokens.push(tail) tokens.push(c_type) @@ -428,7 +445,7 @@ def parse_struct( raise ParserError(f"Invalid reserved member name `{vname}`") # parse length if "[" in vname: - vname, field_type.vlen, field_type.flexible_array = parse_length(tokens, vname, 1, flexible_array) + vname, field_type.vlen_ex, field_type.flexible_array = parse_length(tokens, vname, flexible_array) flexible_array = flexible_array or field_type.flexible_array # anonymous nested union if vname == ";" and field_type.ref is not None and (__is_union__ or field_type.ref.__is_union__): diff --git a/cstruct/cstruct.py b/cstruct/cstruct.py index 9e899d9..66be23f 100644 --- a/cstruct/cstruct.py +++ b/cstruct/cstruct.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (c) 2013-2019 Andrea Bonomi +# Copyright (c) 2013-2025 Andrea Bonomi # # Published under the terms of the MIT license. # diff --git a/cstruct/exceptions.py b/cstruct/exceptions.py index b5546b4..cf496ae 100644 --- a/cstruct/exceptions.py +++ b/cstruct/exceptions.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (c) 2013-2019 Andrea Bonomi +# Copyright (c) 2013-2025 Andrea Bonomi # # Published under the terms of the MIT license. # @@ -27,6 +27,7 @@ "CStructException", "ParserError", "EvalError", + "ContextNotFound", ] @@ -44,3 +45,7 @@ class ParserError(CStructException): class EvalError(CStructException): pass + + +class ContextNotFound(EvalError): + pass diff --git a/cstruct/field.py b/cstruct/field.py index 3568b36..2d0bb16 100644 --- a/cstruct/field.py +++ b/cstruct/field.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (c) 2013-2019 Andrea Bonomi +# Copyright (c) 2013-2025 Andrea Bonomi # # Published under the terms of the MIT license. # @@ -23,12 +23,13 @@ # import copy +import inspect import struct from enum import Enum -from typing import TYPE_CHECKING, Any, List, Optional, Type +from typing import TYPE_CHECKING, Any, Callable, List, Optional, Type, Union from .base import NATIVE_ORDER -from .exceptions import ParserError +from .exceptions import ContextNotFound, ParserError from .native_types import get_native_type if TYPE_CHECKING: @@ -49,6 +50,20 @@ def calculate_padding(byte_order: Optional[str], alignment: int, pos: int) -> in return 0 +def get_cstruct_context() -> Optional["AbstractCStruct"]: + """ + Get the current CStruct context (instance) from the stack + """ + from .abstract import AbstractCStruct + + stack = inspect.stack() + for frame in stack: + caller_self = frame.frame.f_locals.get("self") + if isinstance(caller_self, AbstractCStruct): + return caller_self + return None + + class Kind(Enum): """ Field type @@ -64,7 +79,7 @@ class Kind(Enum): "Enum type" -class FieldType(object): +class FieldType: """ Struct/Union field @@ -72,18 +87,27 @@ class FieldType(object): kind (Kind): struct/union/native c_type (str): field type ref (AbstractCStruct): struct/union class ref - vlen (int): number of elements + vlen_ex (int|callable int): number of elements flexible_array (bool): True for flexible arrays offset (int): relative memory position of the field (relative to the struct) padding (int): padding """ + kind: Kind + c_type: str + ref: Optional[Type["AbstractCStruct"]] + vlen_ex: Union[int, Callable[[], int]] + flexible_array: bool + byte_order: Optional[str] + offset: int + padding: int + def __init__( self, kind: Kind, c_type: str, ref: Optional[Type["AbstractCStruct"]], - vlen: int, + vlen_ex: Union[int, Callable[[], int]], flexible_array: bool, byte_order: Optional[str], offset: int, @@ -95,26 +119,27 @@ def __init__( kind: struct/union/native c_type: field type ref: struct/union class ref - vlen: number of elements + vlen_ex: number of elements flexible_array: True for flexible arrays offset: relative memory position of the field (relative to the struct) """ self.kind = kind self.c_type = c_type self.ref = ref - self.vlen = vlen + self.vlen_ex = vlen_ex self.flexible_array = flexible_array self.byte_order = byte_order self.offset = self.base_offset = offset self.padding = 0 - def unpack_from(self, buffer: bytes, offset: int = 0) -> Any: + def unpack_from(self, buffer: bytes, offset: int = 0, context: Optional["AbstractCStruct"] = None) -> Any: """ Unpack bytes containing packed C structure data Args: buffer: bytes to be unpacked offset: optional buffer offset + context: context (cstruct instance) Returns: data: The unpacked data @@ -153,13 +178,24 @@ def pack(self, data: Any) -> bytes: bytes: The packed structure """ if self.flexible_array: - self.vlen = len(data) # set flexible array size + self.vlen_ex = len(data) # set flexible array size return struct.pack(self.fmt, *data) elif self.is_array: - return struct.pack(self.fmt, *data) + if self.vlen == 0: # empty array + return bytes() + else: + return struct.pack(self.fmt, *data) else: return struct.pack(self.fmt, data) + @property + def vlen(self) -> int: + "Number of elements" + try: + return self.vlen_ex() if callable(self.vlen_ex) else self.vlen_ex + except ContextNotFound: + return 0 + @property def is_array(self) -> bool: "True if field is an array/flexible array" @@ -202,7 +238,10 @@ def native_format(self) -> str: def fmt(self) -> str: "Field format prefixed by byte order (struct library format)" if self.is_native or self.is_enum: - fmt = (str(self.vlen) if self.vlen > 1 or self.flexible_array else "") + self.native_format + if self.vlen == 0: + fmt = "" + else: + fmt = (str(self.vlen) if self.vlen > 1 or self.flexible_array else "") + self.native_format else: # Struct/Union fmt = str(self.vlen * self.ref.sizeof()) + self.native_format if self.byte_order: diff --git a/cstruct/mem_cstruct.py b/cstruct/mem_cstruct.py index a0aaf1d..e457de6 100644 --- a/cstruct/mem_cstruct.py +++ b/cstruct/mem_cstruct.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (c) 2013-2019 Andrea Bonomi +# Copyright (c) 2013-2025 Andrea Bonomi # # Published under the terms of the MIT license. # @@ -35,7 +35,7 @@ def __init__(self, values: List[Any], name: str, parent: Optional["MemCStruct"] self.name = name self.parent = parent - def __setitem__(self, key: int, value: List[Any]) -> None: # noqa: F811 + def __setitem__(self, key: int, value: List[Any]) -> None: # type: ignore super().__setitem__(key, value) # Notify the parent when a value is changed if self.parent is not None: @@ -136,7 +136,7 @@ def __setattr__(self, attr: str, value: Any) -> None: else: # native if field_type.flexible_array and len(value) != field_type.vlen: # flexible array size changed, resize the buffer - field_type.vlen = len(value) + field_type.vlen_ex = len(value) ctypes.resize(self.__mem__, self.size + 1) addr = field_type.offset + self.__base__ self.memcpy(addr, field_type.pack(value), field_type.vsize) diff --git a/cstruct/native_types.py b/cstruct/native_types.py index f1e205c..fd20f47 100644 --- a/cstruct/native_types.py +++ b/cstruct/native_types.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (c) 2013-2019 Andrea Bonomi +# Copyright (c) 2013-2025 Andrea Bonomi # # Published under the terms of the MIT license. # @@ -54,10 +54,10 @@ ] -NATIVE_TYPES: Dict[str, "AbstractNativeType"] = {} +NATIVE_TYPES: Dict[str, Type["AbstractNativeType"]] = {} -def get_native_type(type_: str) -> "AbstractNativeType": +def get_native_type(type_: str) -> Type["AbstractNativeType"]: """ Get a base data type by name @@ -79,10 +79,6 @@ def get_native_type(type_: str) -> "AbstractNativeType": class NativeTypeMeta(ABCMeta): __size__: int = 0 " Size in bytes " - type_name: str = "" - " Type name " - native_format: str = "" - " Type format " def __new__(metacls: Type[type], name: str, bases: Tuple[str], namespace: Dict[str, Any]) -> Type[Any]: if namespace.get("native_format"): @@ -92,7 +88,7 @@ def __new__(metacls: Type[type], name: str, bases: Tuple[str], namespace: Dict[s native_format = None namespace["native_format"] = None namespace["__size__"] = None - new_class: Type[Any] = super().__new__(metacls, name, bases, namespace) + new_class: Type[AbstractNativeType] = super().__new__(metacls, name, bases, namespace) # type: ignore if namespace.get("type_name"): NATIVE_TYPES[namespace["type_name"]] = new_class return new_class @@ -108,6 +104,11 @@ def size(cls) -> int: class AbstractNativeType(metaclass=NativeTypeMeta): + type_name: str = "" + " Type name " + native_format: str = "" + " Type format " + def __str__(self) -> str: return self.type_name diff --git a/setup.cfg b/setup.cfg index 59d4b3f..f737c08 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,6 +26,7 @@ classifiers = Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 + Programming Language :: Python :: 3.13 project_urls = Bug Tracker = http://github.com/andreax79/python-cstruct/issues Documentation = https://python-cstruct.readthedocs.io/en/latest/ diff --git a/tests/test_cstruct_var.py b/tests/test_cstruct_var.py new file mode 100644 index 0000000..4ad9c07 --- /dev/null +++ b/tests/test_cstruct_var.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python +# ***************************************************************************** +# +# Copyright (c) 2013-2025 Andrea Bonomi +# +# Published under the terms of the MIT license. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. +# +# ***************************************************************************** + +import cstruct +from cstruct import sizeof +from cstruct.c_expr import c_eval + + +class Struct0(cstruct.CStruct): + __byte_order__ = cstruct.LITTLE_ENDIAN + __def__ = """ + struct { + uint16_t a; + uint16_t b; + char c[6]; + } + """ + + +class Struct1(cstruct.CStruct): + __byte_order__ = cstruct.LITTLE_ENDIAN + __def__ = """ + #define X1 6 + struct { + uint16_t a; + uint16_t b; + char c[X1]; + } + """ + + +class Struct2(cstruct.CStruct): + c_len: int = 6 + + __byte_order__ = cstruct.LITTLE_ENDIAN + __def__ = """ + struct { + uint16_t a; + uint16_t b; + char c[self.c_len]; + } + """ + + +class Struct3(cstruct.MemCStruct): + c_len: int = 0 + + __byte_order__ = cstruct.LITTLE_ENDIAN + __def__ = """ + struct { + uint16_t a; + uint16_t b; + uint16_t c[self.c_len]; + } + """ + + +def test_v(): + assert c_eval('10') == 10 + + s0 = Struct0() + assert len(s0) == 10 + s1 = Struct1() + assert len(s1) == 10 + + assert sizeof(Struct2) == 4 + + s2 = Struct2() + assert len(s2) == 10 + s2.c_len = 10 + assert len(s2) == 14 + + for i in range(10): + s2.c_len = i + assert len(s2) == 4 + i + + assert sizeof(Struct3) == 4 + s3 = Struct3() + assert len(s3) == 4 + + for i in range(10): + s3.c_len = i + assert len(s3) == 4 + i * 2 From 5a8a7c6d6def9a03d72fb058f357676b2c38340d Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Tue, 18 Feb 2025 06:53:49 +0100 Subject: [PATCH 86/95] Update README.md --- README.md | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 771adbd..cd12111 100644 --- a/README.md +++ b/README.md @@ -161,11 +161,16 @@ class Packet(cstruct.MemCStruct): ### Byte Order, Size, and Padding -Suported byte orders: +Python-CStruct supports big-endian, little-endian, and native byte orders, which you can specify using: -* `cstruct.LITTLE_ENDIAN` - Little endian byte order, standard size, no padding -* `cstruct.BIG_ENDIAN` - Big endian byte order, standard size, no padding -* `cstruct.NATIVE_ORDER` - Native byte order, native size, padding +* `cstruct.LITTLE_ENDIAN` - Little endian byte order, standard size, no padding. +* `cstruct.BIG_ENDIAN` - Big endian byte order, standard size, no padding. +* `cstruct.NATIVE_ORDER` - Native byte order, native size, padding. Native byte order is big-endian or little-endian, depending on the host system. + +Standard size depends only on the format character while native size depends on the host system. +For native order, padding is automatically added to align the structure members. + +For more information, see the [struct - Byte Order, Size, and Padding](https://docs.python.org/3/library/struct.html#byte-order-size-and-alignment) section. ```python class Native(cstruct.MemCStruct): @@ -321,6 +326,24 @@ class MBR(cstruct.MemCStruct): """ ``` +### Accessing Field Definitions + +Python-CStruct provides the `__fields_types__` attribute at the class level, which allows you to inspect the metadata of each field. +The dictionary contains each field's name as a key and its metadata as a value. +Each field has the following attributes: + +| Attribute | Description | +|------------------|------------| +| `kind` | Indicates whether the field is a primitive type or a nested struct. | +| `c_type` | The corresponding C type. | +| `ref` | If the field is a nested struct, this contains a reference to the class representing that struct. | +| `vlen_ex` | The number of elements in the field. | +| `flexible_array` | Indicates whether the field is a flexible array. | +| `byte_order` | The byte order of the field. | +| `offset` | The relative position of the field within the struct. | +| `padding` | The number of bytes added before this field for alignment. | + + ### Ispect memory The [`inspect`](https://python-cstruct.readthedocs.io/en/latest/api/abstract/#cstruct.abstract.AbstractCStruct.inspect) methods displays memory contents in hexadecimal. From 322424e541f2b1de5eb30de5fca6c7df9570781f Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Fri, 21 Mar 2025 23:00:28 +0100 Subject: [PATCH 87/95] version 6.1 - fix CStruct.pack() padding --- Makefile | 2 +- changelog.txt | 6 ++++ cstruct/__init__.py | 2 +- cstruct/cstruct.py | 4 +++ tests/test_padding.py | 71 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 tests/test_padding.py diff --git a/Makefile b/Makefile index 5a2e117..4cfe394 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ black: isort clean: -rm -rf build dist -rm -rf *.egg-info - -rm -rf bin lib share pyvenv.cfg + -rm -rf bin lib lib64 share include pyvenv.cfg coverage: @pytest --cov --cov-report=term-missing diff --git a/changelog.txt b/changelog.txt index e0ff73f..8d03ecf 100644 --- a/changelog.txt +++ b/changelog.txt @@ -192,3 +192,9 @@ ### Improved - Python 3.13 support + +## [6.1] - 2025-03-21 + +### Fix + +- fix CStruct.pack() padding diff --git a/cstruct/__init__.py b/cstruct/__init__.py index c86d726..149b919 100644 --- a/cstruct/__init__.py +++ b/cstruct/__init__.py @@ -24,7 +24,7 @@ __author__ = "Andrea Bonomi " __license__ = "MIT" -__version__ = "6.0" +__version__ = "6.1" __date__ = "15 August 2013" from typing import Any, Dict, Optional, Type, Union diff --git a/cstruct/cstruct.py b/cstruct/cstruct.py index 66be23f..e0fee15 100644 --- a/cstruct/cstruct.py +++ b/cstruct/cstruct.py @@ -66,6 +66,10 @@ def pack(self) -> bytes: """ result: List[bytes] = [] for field, field_type in self.__fields_types__.items(): + # Add padding if needed + if field_type.padding: + result.append(CHAR_ZERO * field_type.padding) + if field_type.is_struct or field_type.is_union: if field_type.vlen == 1: # single struct v = getattr(self, field, field_type.ref()) diff --git a/tests/test_padding.py b/tests/test_padding.py new file mode 100644 index 0000000..da0a7cf --- /dev/null +++ b/tests/test_padding.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python + +import cstruct +from cstruct import LITTLE_ENDIAN, NATIVE_ORDER, sizeof + +DEFINITION = """ + struct struct1 { + char a[2]; + uint32_t b; + } +""" + + +class CStructNative(cstruct.CStruct): + __byte_order__ = NATIVE_ORDER + __def__ = DEFINITION + + +class MemCStructNative(cstruct.MemCStruct): + __byte_order__ = NATIVE_ORDER + __def__ = DEFINITION + + +class CStructLittleEndian(cstruct.CStruct): + __byte_order__ = LITTLE_ENDIAN + __def__ = DEFINITION + + +class MemCStructLittleEndian(cstruct.MemCStruct): + __byte_order__ = LITTLE_ENDIAN + __def__ = DEFINITION + + +def test_memcstruct_padding(): + for struct in (CStructNative, MemCStructNative): + data = b"\x41\x42\x00\x00\x01\x00\x00\x00" + assert sizeof(struct) == 8 + t = struct() + t.unpack(data) + assert t.a == b"AB" + assert t.b == 1 + buf = t.pack() + assert len(buf) == sizeof(struct) + assert buf == data + + t2 = struct() + t2.unpack(buf) + assert t2.a == b"AB" + assert t2.b == 1 + buf = t.pack() + assert len(buf) == sizeof(struct) + + +def test_no_padding(): + for struct in (CStructLittleEndian, MemCStructLittleEndian): + data = b"\x41\x42\x01\x00\x00\x00" + assert sizeof(struct) == 6 + t = struct() + t.unpack(data) + assert t.a == b"AB" + assert t.b == 1 + buf = t.pack() + assert len(buf) == sizeof(struct) + assert buf == data + + t2 = struct() + t2.unpack(buf) + assert t2.a == b"AB" + assert t2.b == 1 + buf = t.pack() + assert len(buf) == sizeof(struct) From cffe68912061bb945bc9a97a08a5babf0c4ebde4 Mon Sep 17 00:00:00 2001 From: Philipp Bartsch Date: Fri, 9 Jan 2026 19:27:59 +0100 Subject: [PATCH 88/95] make ast module usage compatible to python 3.14 Python 3.8 deprecated some AST nodes, which got removed in Python 3.14. See https://docs.python.org/3.14/whatsnew/3.14.html#id9 --- cstruct/c_expr.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/cstruct/c_expr.py b/cstruct/c_expr.py index 70edf46..03e0f47 100644 --- a/cstruct/c_expr.py +++ b/cstruct/c_expr.py @@ -25,6 +25,7 @@ import ast import inspect import operator +import sys from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Type, Union from .base import DEFINES, STRUCTS @@ -160,18 +161,11 @@ def get_cstruct_context() -> Optional["AbstractCStruct"]: return None -try: - Constant = ast.Constant -except AttributeError: # python < 3.8 - Constant = ast.NameConstant - OPS: Dict[Type[ast.AST], Callable[[Any], Any]] = { ast.Expr: lambda node: eval_node(node.value), - ast.Num: lambda node: node.n, ast.Name: eval_get, ast.Call: eval_call, - Constant: lambda node: node.value, - ast.Str: lambda node: node.s, # python < 3.8 + ast.Constant: lambda node: node.value, # and/or ast.BoolOp: lambda node: OPS[type(node.op)](node), # and/or operator ast.And: lambda node: all(eval_node(x) for x in node.values), # && operator @@ -203,3 +197,7 @@ def get_cstruct_context() -> Optional["AbstractCStruct"]: ast.GtE: operator.ge, ast.LtE: operator.le, } + +if sys.version_info.major == 3 and sys.version_info.minor < 8: + OPS[ast.Num] = lambda node: node.n + OPS[ast.Str] = lambda node: node.s From 7044350dfbc860d6018aad449d530ef322fe121d Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Sat, 10 Jan 2026 09:33:16 +0100 Subject: [PATCH 89/95] github tests with python 3.13 and 3.14 --- .github/workflows/tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 41d40e3..20a2332 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,6 +16,8 @@ jobs: - "3.10" - "3.11" - "3.12" + - "3.13" + - "3.14" runs-on: ubuntu-latest From 8f71a0c5670e43acc88debaba3f9d7158d09f704 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Sat, 10 Jan 2026 14:16:02 +0100 Subject: [PATCH 90/95] add llms.txt --- llms.txt | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 llms.txt diff --git a/llms.txt b/llms.txt new file mode 100644 index 0000000..ed5ea1d --- /dev/null +++ b/llms.txt @@ -0,0 +1,63 @@ +# llms.txt - Guidance for Large Language Models +# Project: Python-CStruct + +python-cstruct is a Python module for defining, parsing, and serializing +C-style binary structures using C syntax (`struct`, `union`, `enum`). +It allows users to describe structured binary layouts and convert +between raw bytes and Python objects. + +python-cstruct is a Python library for defining and parsing binary data using +C-style syntax (`struct`, `union`, `enum`). It converts C definitions into +Python classes that can pack and unpack raw bytes deterministically. + +## What This Project Is +- A binary parsing and serialization library +- Based on C syntax embedded as strings +- Produces Python objects with `.pack()` / `.unpack()` +- Suitable for files, protocols, and low-level data formats + +## What This Project Is NOT +- NOT a general Python–C FFI or ABI binding +- NOT a replacement for `ctypes.Structure` +- NOT an automatic structure inference tool + +## Core APIs & Concepts +- `MemCStruct`: base class for C `struct` / `union` definitions +- `CEnum`: base class for C `enum` definitions +- `cstruct.parse()`: parse C definitions at runtime +- Fields are accessed as Python attributes +- Layout is deterministic and explicit + +## Endianness +- Explicitly controlled via: + - `LITTLE_ENDIAN` + - `BIG_ENDIAN` + - `NATIVE_ORDER` +- Do NOT assume platform defaults + +## Typical Usage +1. Define a C struct/union/enum as a string +2. Bind it to a Python class or parse it dynamically +3. Pack Python objects into bytes or unpack bytes into objects +4. Access fields as attributes + +## Examples (Authoritative) +- MBR partition parsing: + https://python-cstruct.readthedocs.io/en/latest/examples/fdisk/ +- Logged users (`who`) example: + https://python-cstruct.readthedocs.io/en/latest/examples/who/ +- Flexible Array Member (FAM): + https://python-cstruct.readthedocs.io/en/latest/examples/flexible_array/ +- libc integration (ctypes): + https://python-cstruct.readthedocs.io/en/latest/examples/dir/ + +## API Reference (Source of Truth) +https://python-cstruct.readthedocs.io/en/latest/api/module/ + +## Guidance for Code Generation +- Use real API names (`MemCStruct`, `CEnum`, `cstruct.parse`) +- Always specify byte order when relevant +- Follow the official documentation if uncertain + +If there is any conflict between assumptions and the documentation or source +code, the documentation and source code take precedence. From ecd1d2a1e36b86ab751fe455c43e96a9037babd7 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Sat, 10 Jan 2026 15:27:21 +0100 Subject: [PATCH 91/95] format with ruff --- README.md | 3 +-- examples/fdisk.py | 4 ++-- examples/who.py | 2 +- tests/test_cstruct_var.py | 2 +- tests/test_nested.py | 42 +++++++++++++++++++-------------------- 5 files changed, 26 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index cd12111..0a6b276 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,7 @@ C-style structs for Python [![PyPI version](https://badge.fury.io/py/cstruct.svg)](https://badge.fury.io/py/cstruct) [![PyPI](https://img.shields.io/pypi/pyversions/cstruct.svg)](https://pypi.org/project/cstruct) [![Downloads](https://pepy.tech/badge/cstruct/month)](https://pepy.tech/project/cstruct) -[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) -[![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/) +[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) [![Known Vulnerabilities](https://snyk.io/test/github/andreax79/python-cstruct/badge.svg)](https://snyk.io/test/github/andreax79/python-cstruct) [![Documentation](https://readthedocs.org/projects/python-cstruct/badge/?version=latest)](https://python-cstruct.readthedocs.io/en/latest/) diff --git a/examples/fdisk.py b/examples/fdisk.py index 2054f3e..b18c970 100644 --- a/examples/fdisk.py +++ b/examples/fdisk.py @@ -6,7 +6,7 @@ import cstruct -UNITS = ['B', 'K', 'M', 'G', 'T'] +UNITS = ["B", "K", "M", "G", "T"] SECTOR_SIZE = 512 TYPES = { 0x00: "Empty", @@ -129,7 +129,7 @@ def disk_signature_str(self): def print_info(self): print(f"Sector size: {cstruct.getdef('MBR_SIZE')}") - if self.signature != cstruct.getdef('MBR_BOOT_SIGNATURE'): + if self.signature != cstruct.getdef("MBR_BOOT_SIGNATURE"): print("Invalid MBR signature") print(f"Disk identifier: 0x{self.disk_signature_str}") diff --git a/examples/who.py b/examples/who.py index 7737de7..a8741b0 100644 --- a/examples/who.py +++ b/examples/who.py @@ -110,7 +110,7 @@ def __str__(self): return f"{self.user:<10s} {self.line:<12s} {self.time:<15s} {self.ut_pid:>15} {self.host:<8s}" def print_info(self, show_all): - if show_all or self.ut_type in (getdef('LOGIN_PROCESS'), getdef('USER_PROCESS')): + if show_all or self.ut_type in (getdef("LOGIN_PROCESS"), getdef("USER_PROCESS")): print(self) diff --git a/tests/test_cstruct_var.py b/tests/test_cstruct_var.py index 4ad9c07..95e24ef 100644 --- a/tests/test_cstruct_var.py +++ b/tests/test_cstruct_var.py @@ -80,7 +80,7 @@ class Struct3(cstruct.MemCStruct): def test_v(): - assert c_eval('10') == 10 + assert c_eval("10") == 10 s0 = Struct0() assert len(s0) == 10 diff --git a/tests/test_nested.py b/tests/test_nested.py index e527180..a95b0b8 100644 --- a/tests/test_nested.py +++ b/tests/test_nested.py @@ -255,21 +255,21 @@ def test_nested_struct_offset(): ) o = Op() - o.preamble = b'ciao_ciao' + o.preamble = b"ciao_ciao" o.magic = 3771778641802345472 o.u1.a_op.a = 2022 o.aaa.a = 0x33333333 - assert o.u1.b_op.a == b'\xe6' - assert o.u1.b_op.b == b'\x07' - assert o.u1.b_op.c == b'\x00' + assert o.u1.b_op.a == b"\xe6" + assert o.u1.b_op.b == b"\x07" + assert o.u1.b_op.c == b"\x00" assert o.__base__ == 0 assert o.u1.__base__ >= 10 assert o.u1.__base__ == o.u1.a_op.__base__ assert o.u1.__base__ == o.u1.b_op.__base__ assert o.aaa.__base__ > o.u1.__base__ - assert o.pack() == b'ciao_ciao\x00\x00\xbc\x08\xe4\xb0\x0cX4\xe6\x07\x00\x003333' - assert o.u1.pack() == b'\xe6\x07\x00\x00' - assert o.aaa.pack() == b'3333' + assert o.pack() == b"ciao_ciao\x00\x00\xbc\x08\xe4\xb0\x0cX4\xe6\x07\x00\x003333" + assert o.u1.pack() == b"\xe6\x07\x00\x00" + assert o.aaa.pack() == b"3333" assert o.u1.a_op.inspect() == "00000000 e6 07 00 00 |.... |\n" assert o.u1.b_op.inspect() == "00000000 e6 07 00 |... |\n" @@ -306,41 +306,41 @@ def test_nested_anonymous_struct_offset(): ) o = Opu() - o.preamble = b'ciao_ciao' + o.preamble = b"ciao_ciao" o.magic = 3771778641802345472 o.__anonymous0.a_op.a = 2022 o.aaa.a = 0x33333333 - assert o.__anonymous0.b_op.a == b'\xe6' - assert o.__anonymous0.b_op.b == b'\x07' - assert o.__anonymous0.b_op.c == b'\x00' + assert o.__anonymous0.b_op.a == b"\xe6" + assert o.__anonymous0.b_op.b == b"\x07" + assert o.__anonymous0.b_op.c == b"\x00" assert o.__base__ == 0 assert o.__anonymous0.__base__ >= 10 assert o.__anonymous0.__base__ == o.__anonymous0.a_op.__base__ assert o.__anonymous0.__base__ == o.__anonymous0.b_op.__base__ assert o.aaa.__base__ > o.__anonymous0.__base__ - assert o.pack() == b'ciao_ciao\x00\x00\xbc\x08\xe4\xb0\x0cX4\xe6\x07\x00\x003333' - assert o.__anonymous0.pack() == b'\xe6\x07\x00\x00' - assert o.aaa.pack() == b'3333' + assert o.pack() == b"ciao_ciao\x00\x00\xbc\x08\xe4\xb0\x0cX4\xe6\x07\x00\x003333" + assert o.__anonymous0.pack() == b"\xe6\x07\x00\x00" + assert o.aaa.pack() == b"3333" assert o.__anonymous0.inspect() == "00000000 e6 07 00 00 |.... |\n" assert o.__anonymous0.a_op.inspect() == "00000000 e6 07 00 00 |.... |\n" assert o.__anonymous0.b_op.inspect() == "00000000 e6 07 00 |... |\n" o = Opu() - o.preamble = b'ciao_ciao' + o.preamble = b"ciao_ciao" o.magic = 3771778641802345472 o.a_op.a = 2022 o.aaa.a = 0x33333333 - assert o.b_op.a == b'\xe6' - assert o.b_op.b == b'\x07' - assert o.b_op.c == b'\x00' + assert o.b_op.a == b"\xe6" + assert o.b_op.b == b"\x07" + assert o.b_op.c == b"\x00" assert o.__base__ == 0 assert o.__anonymous0.__base__ >= 10 assert o.__anonymous0.__base__ == o.a_op.__base__ assert o.__anonymous0.__base__ == o.b_op.__base__ assert o.aaa.__base__ > o.__base__ - assert o.pack() == b'ciao_ciao\x00\x00\xbc\x08\xe4\xb0\x0cX4\xe6\x07\x00\x003333' - assert o.a_op.pack() == b'\xe6\x07\x00\x00' - assert o.aaa.pack() == b'3333' + assert o.pack() == b"ciao_ciao\x00\x00\xbc\x08\xe4\xb0\x0cX4\xe6\x07\x00\x003333" + assert o.a_op.pack() == b"\xe6\x07\x00\x00" + assert o.aaa.pack() == b"3333" assert o.a_op.inspect() == "00000000 e6 07 00 00 |.... |\n" assert o.b_op.inspect() == "00000000 e6 07 00 |... |\n" From 7b0b90ce73b305a8328aad10fa1c0cb86971639c Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Sat, 10 Jan 2026 15:40:27 +0100 Subject: [PATCH 92/95] perform 32bit test using Python 3.7 --- docker/i386/Dockerfile | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docker/i386/Dockerfile b/docker/i386/Dockerfile index 9037dda..356a863 100644 --- a/docker/i386/Dockerfile +++ b/docker/i386/Dockerfile @@ -2,13 +2,13 @@ FROM i386/ubuntu RUN apt-get update && \ apt-get -y install \ - python3.6 \ - python3.6-dev \ - python3.6-distutils \ + python3.7 \ + python3.7-dev \ + python3.7-distutils \ curl \ make && \ rm -rf /var/lib/apt/lists/* -RUN curl -sSL https://bootstrap.pypa.io/pip/3.6/get-pip.py -o get-pip.py && \ - python3.6 get-pip.py +RUN curl -sSL https://bootstrap.pypa.io/pip/3.7/get-pip.py -o get-pip.py && \ + python3.7 get-pip.py RUN pip install pytest WORKDIR /app From b5233e2686964a60437784096f613c8a393bfd76 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Sat, 10 Jan 2026 15:47:04 +0100 Subject: [PATCH 93/95] use pyproject.toml instead of setup.cfg --- .github/workflows/release.yml | 73 +- .github/workflows/tests.yml | 11 +- .gitignore | 1 + .python-version | 1 + Makefile | 53 +- pyproject.toml | 70 +- requirements-dev.txt | 12 - requirements.txt | 0 setup.cfg | 63 - setup.py | 5 - uv.lock | 2939 +++++++++++++++++++++++++++++++++ 11 files changed, 3074 insertions(+), 154 deletions(-) create mode 100644 .python-version delete mode 100644 requirements.txt delete mode 100644 setup.cfg delete mode 100644 setup.py create mode 100644 uv.lock diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6b8f5e0..03e2376 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,6 +5,9 @@ on: tags: - v* +permissions: + contents: read + jobs: wait: @@ -25,13 +28,12 @@ jobs: if: steps.wait-for-tests.outputs.conclusion != 'success' build: - name: Build package runs-on: ubuntu-latest - needs: [wait] strategy: matrix: - python-version: ['3.12'] + python-version: + - "3.12" steps: - name: Checkout code @@ -42,50 +44,39 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Cache pip - uses: actions/cache@v3 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-python-${{ matrix.python }}-pip-${{ hashFiles('**/requirements*.txt') }}-git-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-python-${{ matrix.python }}-pip-${{ hashFiles('**/requirements*.txt') }} - ${{ runner.os }}-python-${{ matrix.python }}-pip- - ${{ runner.os }}-python - ${{ runner.os }}- - - - name: Upgrade pip and install dependencies + - name: Install uv run: | - python -m pip install --upgrade pip - python -m pip install --upgrade setuptools wheel - python -m pip install -r requirements.txt - python -m pip install -r requirements-dev.txt - - - name: Build package - run: python -m build + pip install uv - - name: Check package - run: twine check dist/* - - - name: Publish package - env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + - name: Build release distributions run: | - twine upload --skip-existing dist/* + uv pip install --system build + python -m build - release: - name: Release version + - name: Upload distributions + uses: actions/upload-artifact@v4 + with: + name: release-dists + path: dist/ + + pypi-publish: runs-on: ubuntu-latest needs: [wait, build] + permissions: + id-token: write + + environment: + name: pypi + url: https://pypi.org/project/otp-mcp-server/ steps: - - name: Create release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Retrieve release distributions + uses: actions/download-artifact@v4 + with: + name: release-dists + path: dist/ + + - name: Publish release distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 with: - tag_name: ${{ github.ref }} - release_name: ${{ github.ref }} - draft: false - prerelease: false + packages-dir: dist/ diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 20a2332..b902c63 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,7 +11,6 @@ jobs: strategy: matrix: python: - - "3.8" - "3.9" - "3.10" - "3.11" @@ -26,7 +25,7 @@ jobs: uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} @@ -34,9 +33,9 @@ jobs: uses: actions/cache@v3 with: path: ~/.cache/pip - key: ${{ runner.os }}-python-${{ matrix.python }}-pip-${{ hashFiles('**/requirements*.txt') }}-git-${{ github.sha }} + key: ${{ runner.os }}-python-${{ matrix.python }}-pip-${{ hashFiles('**/pyproject.toml') }}-git-${{ github.sha }} restore-keys: | - ${{ runner.os }}-python-${{ matrix.python }}-pip-${{ hashFiles('**/requirements*.txt') }} + ${{ runner.os }}-python-${{ matrix.python }}-pip-${{ hashFiles('**/pyproject.toml') }} ${{ runner.os }}-python-${{ matrix.python }}-pip- ${{ runner.os }}-python-${{ matrix.python }}- ${{ runner.os }}-python @@ -45,9 +44,7 @@ jobs: - name: Upgrade pip and install dependencies run: | python -m pip install --upgrade pip - python -m pip install --upgrade setuptools wheel - python -m pip install -r requirements.txt - python -m pip install pytest + pip install -e .[test] - name: Run tests run: pytest diff --git a/.gitignore b/.gitignore index 553e4ea..e786812 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,4 @@ nosetests.xml pyvenv.cfg site/ __pycache__ +.venv diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/Makefile b/Makefile index 4cfe394..3e523b0 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,8 @@ SHELL=/bin/bash -e +VERSION := $(shell grep '__version__ = ' cstruct/__init__.py | sed 's/__version__ = "\(.*\)"/\1/') help: - @echo - make black ------ Format code - @echo - make isort ------ Sort imports + @echo - make ruff ------- Format code and sort imports @echo - make clean ------ Clean virtual environment @echo - make coverage --- Run tests coverage @echo - make docs ------- Make docs @@ -12,44 +12,49 @@ help: @echo - make typecheck -- Typecheck @echo - make venv ------- Create virtual environment -.PHONY: isort +.PHONY: codespell codespell: - @codespell -w cstruct tests examples setup.py + uv run codespell -w cstruct tests examples -.PHONY: isort -isort: - @isort --profile black cstruct tests examples setup.py - -.PHONY: black -black: isort - @black -S cstruct tests examples setup.py +.PHONY: ruff +ruff: + uv run ruff format cstruct tests examples +.PHONY: clean clean: - -rm -rf build dist - -rm -rf *.egg-info - -rm -rf bin lib lib64 share include pyvenv.cfg + -rm -rf build dist pyvenv.cfg *.egg-info .venv +.PHONY: coverage coverage: - @pytest --cov --cov-report=term-missing + uv run pytest --cov --cov-report=term-missing .PHONY: docs docs: - @mkdocs build - @mkdocs gh-deploy + uv run mkdocs build + uv run mkdocs gh-deploy +.PHONY: lint lint: - flake8 cstruct tests + uv run ruff check cstruct tests +.PHONY: test test: - pytest + uv run pytest +.PHONY: test-32bit test-32bit: - @make -C docker/i386 test + make -C docker/i386 test +.PHONY: typecheck typecheck: - mypy --strict --no-warn-unused-ignores cstruct + uv run mypy --strict --no-warn-unused-ignores cstruct + +.PHONY: tag +tag: + git tag v${VERSION} +.PHONY: venv venv: - python3 -m venv . || python3 -m virtualenv . - . bin/activate; pip install -Ur requirements.txt - . bin/activate; pip install -Ur requirements-dev.txt + uv venv + uv pip install -e . + uv pip install -e ".[dev]" diff --git a/pyproject.toml b/pyproject.toml index ccb95b4..15a5176 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,73 @@ [build-system] -requires = ["setuptools"] +requires = ["setuptools>=61.0", "wheel"] build-backend = "setuptools.build_meta" -[tool.black] +[project] +name = "cstruct" +dynamic = ["version"] +description = "C-style structs for Python" +readme = "README.md" +license = "MIT" +license-files = ["LICENSE"] +authors = [ + { name = "Andrea Bonomi", email = "andrea.bonomi@gmail.com" } +] +keywords = ["struct", "cstruct", "enum", "binary", "pack", "unpack"] +requires-python = ">=3.7" + +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Topic :: Software Development :: Libraries :: Python Modules", + "Programming Language :: Python :: 3", +] + +[project.urls] +Homepage = "http://github.com/andreax79/python-cstruct" +Source = "http://github.com/andreax79/python-cstruct" +Issues = "http://github.com/andreax79/python-cstruct/issues" +Documentation = "https://python-cstruct.readthedocs.io/en/latest/" + +[project.optional-dependencies] +dev = [ + "pytest", + "pytest-cov", + "codespell", + "coverage[toml]", + "ruff", + "markdown_include", + "mkdocs", + "mkdocs-material", + "mkdocstrings", + "mkdocstrings-python", + "mypy" +] +test = [ + "pytest", + "pytest-cov", + "coverage[toml]" +] + +[tool.setuptools] +zip-safe = true +include-package-data = true + +[tool.setuptools.dynamic] +version = { attr = "cstruct.__version__" } + +[tool.setuptools.packages.find] +include = ["cstruct*"] +exclude = ["ez_setup", "examples", "tests"] + +[tool.ruff] line-length = 132 +[tool.ruff.lint] +ignore = [ "E731", "E711" ] + [tool.coverage.run] source = ["cstruct"] @@ -13,3 +76,6 @@ exclude_lines = [ "# pragma: no cover", "if TYPE_CHECKING:" ] [tool.isort] profile = "black" + +[tool.black] +line-length = 132 diff --git a/requirements-dev.txt b/requirements-dev.txt index ea86fe9..752a6ce 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,16 +1,4 @@ -black -build -codespell -coverage[toml] -flake8 -isort markdown_include mkdocs mkdocs-material mkdocstrings[python] -mypy -pytest -pytest-cov -setuptools>=65.5.1 # not directly required, pinned by Snyk to avoid a vulnerability -tox -twine<3.4 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index e69de29..0000000 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index f737c08..0000000 --- a/setup.cfg +++ /dev/null @@ -1,63 +0,0 @@ -[metadata] -name = cstruct -version = attr: cstruct.__version__ -keywords = struct, cstruct, enum, binary, pack, unpack -description = C-style structs for Python -author = Andrea Bonomi -author_email = andrea.bonomi@gmail.com -url = http://github.com/andreax79/python-cstruct -long_description = file: README.md -long_description_content_type = text/markdown -license = MIT -license_files = LICENSE -platforms = any -classifiers = - Development Status :: 5 - Production/Stable - Environment :: Console - Intended Audience :: Developers - License :: OSI Approved :: MIT License - Operating System :: OS Independent - Programming Language :: Python - Topic :: Software Development :: Libraries :: Python Modules - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Programming Language :: Python :: 3.11 - Programming Language :: Python :: 3.12 - Programming Language :: Python :: 3.13 -project_urls = - Bug Tracker = http://github.com/andreax79/python-cstruct/issues - Documentation = https://python-cstruct.readthedocs.io/en/latest/ - Source Code = http://github.com/andreax79/python-cstruct - -[options] -zip_safe = True -include_package_data = True -python_requires = >=3.6 -packages = find: - -[options.packages.find] -include = cstruct* -exclude = - ez_setup - examples - tests - -[options.extras_require] -test = pytest - -[aliases] -test = pytest - -[bdist_wheel] -universal = 1 - -[flake8] -max-line-length = 132 -extend-ignore = - E203 - E401 - W504 - E221 diff --git a/setup.py b/setup.py deleted file mode 100644 index dbe9716..0000000 --- a/setup.py +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env python -import setuptools - -if __name__ == "__main__": - setuptools.setup() diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..dd7cdaa --- /dev/null +++ b/uv.lock @@ -0,0 +1,2939 @@ +version = 1 +revision = 3 +requires-python = ">=3.7" +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", + "python_full_version < '3.8'", +] + +[[package]] +name = "astunparse" +version = "1.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six", marker = "python_full_version == '3.8.*'" }, + { name = "wheel", marker = "python_full_version == '3.8.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/af/4182184d3c338792894f34a62672919db7ca008c89abee9b564dd34d8029/astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872", size = 18290, upload-time = "2019-12-22T18:12:13.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/03/13dde6512ad7b4557eb792fbcf0c653af6076b81e5941d36ec61f7ce6028/astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8", size = 12732, upload-time = "2019-12-22T18:12:11.297Z" }, +] + +[[package]] +name = "babel" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "pytz", marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/80/cfbe44a9085d112e983282ee7ca4c00429bc4d1ce86ee5f4e60259ddff7f/Babel-2.14.0.tar.gz", hash = "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363", size = 10795622, upload-time = "2023-12-12T13:33:16.473Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/35/4196b21041e29a42dc4f05866d0c94fa26c9da88ce12c38c2265e42c82fb/Babel-2.14.0-py3-none-any.whl", hash = "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287", size = 11034798, upload-time = "2023-12-12T13:33:13.288Z" }, +] + +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", +] +dependencies = [ + { name = "pytz", marker = "python_full_version == '3.8.*'" }, +] +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, upload-time = "2025-02-01T15:17:37.39Z" }, +] + +[[package]] +name = "backrefs" +version = "5.7.post1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/df/30/903f35159c87ff1d92aa3fcf8cb52de97632a21e0ae43ed940f5d033e01a/backrefs-5.7.post1.tar.gz", hash = "sha256:8b0f83b770332ee2f1c8244f4e03c77d127a0fa529328e6a0e77fa25bee99678", size = 6582270, upload-time = "2024-06-16T18:38:20.166Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/bb/47fc255d1060dcfd55b460236380edd8ebfc5b2a42a0799ca90c9fc983e3/backrefs-5.7.post1-py310-none-any.whl", hash = "sha256:c5e3fd8fd185607a7cb1fefe878cfb09c34c0be3c18328f12c574245f1c0287e", size = 380429, upload-time = "2024-06-16T18:38:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/89/72/39ef491caef3abae945f5a5fd72830d3b596bfac0630508629283585e213/backrefs-5.7.post1-py311-none-any.whl", hash = "sha256:712ea7e494c5bf3291156e28954dd96d04dc44681d0e5c030adf2623d5606d51", size = 392234, upload-time = "2024-06-16T18:38:12.283Z" }, + { url = "https://files.pythonhosted.org/packages/6a/00/33403f581b732ca70fdebab558e8bbb426a29c34e0c3ed674a479b74beea/backrefs-5.7.post1-py312-none-any.whl", hash = "sha256:a6142201c8293e75bce7577ac29e1a9438c12e730d73a59efdd1b75528d1a6c5", size = 398110, upload-time = "2024-06-16T18:38:14.257Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ea/df0ac74a26838f6588aa012d5d801831448b87d0a7d0aefbbfabbe894870/backrefs-5.7.post1-py38-none-any.whl", hash = "sha256:ec61b1ee0a4bfa24267f6b67d0f8c5ffdc8e0d7dc2f18a2685fd1d8d9187054a", size = 369477, upload-time = "2024-06-16T18:38:16.196Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e8/e43f535c0a17a695e5768670fc855a0e5d52dc0d4135b3915bfa355f65ac/backrefs-5.7.post1-py39-none-any.whl", hash = "sha256:05c04af2bf752bb9a6c9dcebb2aff2fab372d3d9d311f2a138540e307756bd3a", size = 380429, upload-time = "2024-06-16T18:38:18.079Z" }, +] + +[[package]] +name = "backrefs" +version = "6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/86/e3/bb3a439d5cb255c4774724810ad8073830fac9c9dee123555820c1bcc806/backrefs-6.1.tar.gz", hash = "sha256:3bba1749aafe1db9b915f00e0dd166cba613b6f788ffd63060ac3485dc9be231", size = 7011962, upload-time = "2025-11-15T14:52:08.323Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ee/c216d52f58ea75b5e1841022bbae24438b19834a29b163cb32aa3a2a7c6e/backrefs-6.1-py310-none-any.whl", hash = "sha256:2a2ccb96302337ce61ee4717ceacfbf26ba4efb1d55af86564b8bbaeda39cac1", size = 381059, upload-time = "2025-11-15T14:51:59.758Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9a/8da246d988ded941da96c7ed945d63e94a445637eaad985a0ed88787cb89/backrefs-6.1-py311-none-any.whl", hash = "sha256:e82bba3875ee4430f4de4b6db19429a27275d95a5f3773c57e9e18abc23fd2b7", size = 392854, upload-time = "2025-11-15T14:52:01.194Z" }, + { url = "https://files.pythonhosted.org/packages/37/c9/fd117a6f9300c62bbc33bc337fd2b3c6bfe28b6e9701de336b52d7a797ad/backrefs-6.1-py312-none-any.whl", hash = "sha256:c64698c8d2269343d88947c0735cb4b78745bd3ba590e10313fbf3f78c34da5a", size = 398770, upload-time = "2025-11-15T14:52:02.584Z" }, + { url = "https://files.pythonhosted.org/packages/eb/95/7118e935b0b0bd3f94dfec2d852fd4e4f4f9757bdb49850519acd245cd3a/backrefs-6.1-py313-none-any.whl", hash = "sha256:4c9d3dc1e2e558965202c012304f33d4e0e477e1c103663fd2c3cc9bb18b0d05", size = 400726, upload-time = "2025-11-15T14:52:04.093Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/6296bad135bfafd3254ae3648cd152980a424bd6fed64a101af00cc7ba31/backrefs-6.1-py314-none-any.whl", hash = "sha256:13eafbc9ccd5222e9c1f0bec563e6d2a6d21514962f11e7fc79872fd56cbc853", size = 412584, upload-time = "2025-11-15T14:52:05.233Z" }, + { url = "https://files.pythonhosted.org/packages/02/e3/a4fa1946722c4c7b063cc25043a12d9ce9b4323777f89643be74cef2993c/backrefs-6.1-py39-none-any.whl", hash = "sha256:a9e99b8a4867852cad177a6430e31b0f6e495d65f8c6c134b68c14c3c95bf4b0", size = 381058, upload-time = "2025-11-15T14:52:06.698Z" }, +] + +[[package]] +name = "cached-property" +version = "1.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/2c/d21c1c23c2895c091fa7a91a54b6872098fea913526932d21902088a7c41/cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130", size = 12244, upload-time = "2020-09-21T18:39:27.069Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/19/f2090f7dad41e225c7f2326e4cfe6fff49e57dedb5b53636c9551f86b069/cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0", size = 7573, upload-time = "2020-09-21T18:39:25.338Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4e/3926a1c11f0433791985727965263f788af00db3482d89a7545ca5ecc921/charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84", size = 198599, upload-time = "2025-10-14T04:41:53.213Z" }, + { url = "https://files.pythonhosted.org/packages/ec/7c/b92d1d1dcffc34592e71ea19c882b6709e43d20fa498042dea8b815638d7/charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3", size = 143090, upload-time = "2025-10-14T04:41:54.385Z" }, + { url = "https://files.pythonhosted.org/packages/84/ce/61a28d3bb77281eb24107b937a497f3c43089326d27832a63dcedaab0478/charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac", size = 139490, upload-time = "2025-10-14T04:41:55.551Z" }, + { url = "https://files.pythonhosted.org/packages/c0/bd/c9e59a91b2061c6f8bb98a150670cb16d4cd7c4ba7d11ad0cdf789155f41/charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af", size = 155334, upload-time = "2025-10-14T04:41:56.724Z" }, + { url = "https://files.pythonhosted.org/packages/bf/37/f17ae176a80f22ff823456af91ba3bc59df308154ff53aef0d39eb3d3419/charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2", size = 152823, upload-time = "2025-10-14T04:41:58.236Z" }, + { url = "https://files.pythonhosted.org/packages/bf/fa/cf5bb2409a385f78750e78c8d2e24780964976acdaaed65dbd6083ae5b40/charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d", size = 147618, upload-time = "2025-10-14T04:41:59.409Z" }, + { url = "https://files.pythonhosted.org/packages/9b/63/579784a65bc7de2d4518d40bb8f1870900163e86f17f21fd1384318c459d/charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3", size = 145516, upload-time = "2025-10-14T04:42:00.579Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a9/94ec6266cd394e8f93a4d69cca651d61bf6ac58d2a0422163b30c698f2c7/charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63", size = 145266, upload-time = "2025-10-14T04:42:01.684Z" }, + { url = "https://files.pythonhosted.org/packages/09/14/d6626eb97764b58c2779fa7928fa7d1a49adb8ce687c2dbba4db003c1939/charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7", size = 139559, upload-time = "2025-10-14T04:42:02.902Z" }, + { url = "https://files.pythonhosted.org/packages/09/01/ddbe6b01313ba191dbb0a43c7563bc770f2448c18127f9ea4b119c44dff0/charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4", size = 156653, upload-time = "2025-10-14T04:42:04.005Z" }, + { url = "https://files.pythonhosted.org/packages/95/c8/d05543378bea89296e9af4510b44c704626e191da447235c8fdedfc5b7b2/charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf", size = 145644, upload-time = "2025-10-14T04:42:05.211Z" }, + { url = "https://files.pythonhosted.org/packages/72/01/2866c4377998ef8a1f6802f6431e774a4c8ebe75b0a6e569ceec55c9cbfb/charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074", size = 153964, upload-time = "2025-10-14T04:42:06.341Z" }, + { url = "https://files.pythonhosted.org/packages/4a/66/66c72468a737b4cbd7851ba2c522fe35c600575fbeac944460b4fd4a06fe/charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a", size = 148777, upload-time = "2025-10-14T04:42:07.535Z" }, + { url = "https://files.pythonhosted.org/packages/50/94/d0d56677fdddbffa8ca00ec411f67bb8c947f9876374ddc9d160d4f2c4b3/charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa", size = 98687, upload-time = "2025-10-14T04:42:08.678Z" }, + { url = "https://files.pythonhosted.org/packages/00/64/c3bc303d1b586480b1c8e6e1e2191a6d6dd40255244e5cf16763dcec52e6/charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576", size = 106115, upload-time = "2025-10-14T04:42:09.793Z" }, + { url = "https://files.pythonhosted.org/packages/46/7c/0c4760bccf082737ca7ab84a4c2034fcc06b1f21cf3032ea98bd6feb1725/charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", size = 209609, upload-time = "2025-10-14T04:42:10.922Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a4/69719daef2f3d7f1819de60c9a6be981b8eeead7542d5ec4440f3c80e111/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", size = 149029, upload-time = "2025-10-14T04:42:12.38Z" }, + { url = "https://files.pythonhosted.org/packages/e6/21/8d4e1d6c1e6070d3672908b8e4533a71b5b53e71d16828cc24d0efec564c/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608", size = 144580, upload-time = "2025-10-14T04:42:13.549Z" }, + { url = "https://files.pythonhosted.org/packages/a7/0a/a616d001b3f25647a9068e0b9199f697ce507ec898cacb06a0d5a1617c99/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", size = 162340, upload-time = "2025-10-14T04:42:14.892Z" }, + { url = "https://files.pythonhosted.org/packages/85/93/060b52deb249a5450460e0585c88a904a83aec474ab8e7aba787f45e79f2/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", size = 159619, upload-time = "2025-10-14T04:42:16.676Z" }, + { url = "https://files.pythonhosted.org/packages/dd/21/0274deb1cc0632cd587a9a0ec6b4674d9108e461cb4cd40d457adaeb0564/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", size = 153980, upload-time = "2025-10-14T04:42:17.917Z" }, + { url = "https://files.pythonhosted.org/packages/28/2b/e3d7d982858dccc11b31906976323d790dded2017a0572f093ff982d692f/charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", size = 152174, upload-time = "2025-10-14T04:42:19.018Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ff/4a269f8e35f1e58b2df52c131a1fa019acb7ef3f8697b7d464b07e9b492d/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", size = 151666, upload-time = "2025-10-14T04:42:20.171Z" }, + { url = "https://files.pythonhosted.org/packages/da/c9/ec39870f0b330d58486001dd8e532c6b9a905f5765f58a6f8204926b4a93/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", size = 145550, upload-time = "2025-10-14T04:42:21.324Z" }, + { url = "https://files.pythonhosted.org/packages/75/8f/d186ab99e40e0ed9f82f033d6e49001701c81244d01905dd4a6924191a30/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", size = 163721, upload-time = "2025-10-14T04:42:22.46Z" }, + { url = "https://files.pythonhosted.org/packages/96/b1/6047663b9744df26a7e479ac1e77af7134b1fcf9026243bb48ee2d18810f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", size = 152127, upload-time = "2025-10-14T04:42:23.712Z" }, + { url = "https://files.pythonhosted.org/packages/59/78/e5a6eac9179f24f704d1be67d08704c3c6ab9f00963963524be27c18ed87/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", size = 161175, upload-time = "2025-10-14T04:42:24.87Z" }, + { url = "https://files.pythonhosted.org/packages/e5/43/0e626e42d54dd2f8dd6fc5e1c5ff00f05fbca17cb699bedead2cae69c62f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", size = 155375, upload-time = "2025-10-14T04:42:27.246Z" }, + { url = "https://files.pythonhosted.org/packages/e9/91/d9615bf2e06f35e4997616ff31248c3657ed649c5ab9d35ea12fce54e380/charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", size = 99692, upload-time = "2025-10-14T04:42:28.425Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a9/6c040053909d9d1ef4fcab45fddec083aedc9052c10078339b47c8573ea8/charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", size = 107192, upload-time = "2025-10-14T04:42:29.482Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c6/4fa536b2c0cd3edfb7ccf8469fa0f363ea67b7213a842b90909ca33dd851/charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", size = 100220, upload-time = "2025-10-14T04:42:30.632Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", + "python_full_version < '3.8'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "importlib-metadata", version = "6.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, +] +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.3.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/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "codespell" +version = "2.2.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://files.pythonhosted.org/packages/81/30/e1b32067c551d745df2bdc9f1d510422d8a5819ca3b610b4433654cf578c/codespell-2.2.5.tar.gz", hash = "sha256:6d9faddf6eedb692bf80c9a94ec13ab4f5fb585aabae5f3750727148d7b5be56", size = 242918, upload-time = "2023-06-14T18:00:23.441Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/bc/4bd1cdb7cf940ab8e8e619d3ad24c88b0257b030c6b0dd64cba3fdfa7bb8/codespell-2.2.5-py3-none-any.whl", hash = "sha256:efa037f54b73c84f7bd14ce8e853d5f822cdd6386ef0ff32e957a3919435b9ec", size = 242673, upload-time = "2023-06-14T18:00:21.806Z" }, +] + +[[package]] +name = "codespell" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/15/e0/709453393c0ea77d007d907dd436b3ee262e28b30995ea1aa36c6ffbccaf/codespell-2.4.1.tar.gz", hash = "sha256:299fcdcb09d23e81e35a671bbe746d5ad7e8385972e65dbb833a2eaac33c01e5", size = 344740, upload-time = "2025-01-28T18:52:39.411Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/01/b394922252051e97aab231d416c86da3d8a6d781eeadcdca1082867de64e/codespell-2.4.1-py3-none-any.whl", hash = "sha256:3dadafa67df7e4a3dbf51e0d7315061b80d265f9552ebd699b3dd6834b47e425", size = 344501, upload-time = "2025-01-28T18:52:37.057Z" }, +] + +[[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, 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, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.2.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://files.pythonhosted.org/packages/45/8b/421f30467e69ac0e414214856798d4bc32da1336df745e49e49ae5c1e2a8/coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59", size = 762575, upload-time = "2023-05-29T20:08:50.273Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/24/be01e62a7bce89bcffe04729c540382caa5a06bee45ae42136c93e2499f5/coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8", size = 200724, upload-time = "2023-05-29T20:07:03.422Z" }, + { url = "https://files.pythonhosted.org/packages/3d/80/7060a445e1d2c9744b683dc935248613355657809d6c6b2716cdf4ca4766/coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb", size = 201024, upload-time = "2023-05-29T20:07:05.694Z" }, + { url = "https://files.pythonhosted.org/packages/b8/9d/926fce7e03dbfc653104c2d981c0fa71f0572a9ebd344d24c573bd6f7c4f/coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6", size = 229528, upload-time = "2023-05-29T20:07:07.307Z" }, + { url = "https://files.pythonhosted.org/packages/d1/3a/67f5d18f911abf96857f6f7e4df37ca840e38179e2cc9ab6c0b9c3380f19/coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2", size = 227842, upload-time = "2023-05-29T20:07:09.331Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bd/1b2331e3a04f4cc9b7b332b1dd0f3a1261dfc4114f8479bebfcc2afee9e8/coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063", size = 228717, upload-time = "2023-05-29T20:07:11.38Z" }, + { url = "https://files.pythonhosted.org/packages/2b/86/3dbf9be43f8bf6a5ca28790a713e18902b2d884bc5fa9512823a81dff601/coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1", size = 234632, upload-time = "2023-05-29T20:07:13.376Z" }, + { url = "https://files.pythonhosted.org/packages/91/e8/469ed808a782b9e8305a08bad8c6fa5f8e73e093bda6546c5aec68275bff/coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353", size = 232875, upload-time = "2023-05-29T20:07:15.093Z" }, + { url = "https://files.pythonhosted.org/packages/29/8f/4fad1c2ba98104425009efd7eaa19af9a7c797e92d40cd2ec026fa1f58cb/coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495", size = 234094, upload-time = "2023-05-29T20:07:17.013Z" }, + { url = "https://files.pythonhosted.org/packages/94/4e/d4e46a214ae857be3d7dc5de248ba43765f60daeb1ab077cb6c1536c7fba/coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818", size = 203184, upload-time = "2023-05-29T20:07:18.69Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e9/d6730247d8dec2a3dddc520ebe11e2e860f0f98cee3639e23de6cf920255/coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850", size = 204096, upload-time = "2023-05-29T20:07:20.153Z" }, + { url = "https://files.pythonhosted.org/packages/c6/fa/529f55c9a1029c840bcc9109d5a15ff00478b7ff550a1ae361f8745f8ad5/coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f", size = 200895, upload-time = "2023-05-29T20:07:21.963Z" }, + { url = "https://files.pythonhosted.org/packages/67/d7/cd8fe689b5743fffac516597a1222834c42b80686b99f5b44ef43ccc2a43/coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe", size = 201120, upload-time = "2023-05-29T20:07:23.765Z" }, + { url = "https://files.pythonhosted.org/packages/8c/95/16eed713202406ca0a37f8ac259bbf144c9d24f9b8097a8e6ead61da2dbb/coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3", size = 233178, upload-time = "2023-05-29T20:07:25.281Z" }, + { url = "https://files.pythonhosted.org/packages/c1/49/4d487e2ad5d54ed82ac1101e467e8994c09d6123c91b2a962145f3d262c2/coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f", size = 230754, upload-time = "2023-05-29T20:07:27.044Z" }, + { url = "https://files.pythonhosted.org/packages/a7/cd/3ce94ad9d407a052dc2a74fbeb1c7947f442155b28264eb467ee78dea812/coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb", size = 232558, upload-time = "2023-05-29T20:07:28.743Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a8/12cc7b261f3082cc299ab61f677f7e48d93e35ca5c3c2f7241ed5525ccea/coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833", size = 241509, upload-time = "2023-05-29T20:07:30.434Z" }, + { url = "https://files.pythonhosted.org/packages/04/fa/43b55101f75a5e9115259e8be70ff9279921cb6b17f04c34a5702ff9b1f7/coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97", size = 239924, upload-time = "2023-05-29T20:07:32.065Z" }, + { url = "https://files.pythonhosted.org/packages/68/5f/d2bd0f02aa3c3e0311986e625ccf97fdc511b52f4f1a063e4f37b624772f/coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a", size = 240977, upload-time = "2023-05-29T20:07:34.184Z" }, + { url = "https://files.pythonhosted.org/packages/ba/92/69c0722882643df4257ecc5437b83f4c17ba9e67f15dc6b77bad89b6982e/coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a", size = 203168, upload-time = "2023-05-29T20:07:35.869Z" }, + { url = "https://files.pythonhosted.org/packages/b1/96/c12ed0dfd4ec587f3739f53eb677b9007853fd486ccb0e7d5512a27bab2e/coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562", size = 204185, upload-time = "2023-05-29T20:07:37.39Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d5/52fa1891d1802ab2e1b346d37d349cb41cdd4fd03f724ebbf94e80577687/coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4", size = 201020, upload-time = "2023-05-29T20:07:38.724Z" }, + { url = "https://files.pythonhosted.org/packages/24/df/6765898d54ea20e3197a26d26bb65b084deefadd77ce7de946b9c96dfdc5/coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4", size = 233994, upload-time = "2023-05-29T20:07:40.274Z" }, + { url = "https://files.pythonhosted.org/packages/15/81/b108a60bc758b448c151e5abceed027ed77a9523ecbc6b8a390938301841/coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01", size = 231358, upload-time = "2023-05-29T20:07:41.998Z" }, + { url = "https://files.pythonhosted.org/packages/61/90/c76b9462f39897ebd8714faf21bc985b65c4e1ea6dff428ea9dc711ed0dd/coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6", size = 233316, upload-time = "2023-05-29T20:07:43.539Z" }, + { url = "https://files.pythonhosted.org/packages/04/d6/8cba3bf346e8b1a4fb3f084df7d8cea25a6b6c56aaca1f2e53829be17e9e/coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d", size = 240159, upload-time = "2023-05-29T20:07:44.982Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ea/4a252dc77ca0605b23d477729d139915e753ee89e4c9507630e12ad64a80/coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de", size = 238127, upload-time = "2023-05-29T20:07:46.522Z" }, + { url = "https://files.pythonhosted.org/packages/9f/5c/d9760ac497c41f9c4841f5972d0edf05d50cad7814e86ee7d133ec4a0ac8/coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d", size = 239833, upload-time = "2023-05-29T20:07:47.992Z" }, + { url = "https://files.pythonhosted.org/packages/69/8c/26a95b08059db1cbb01e4b0e6d40f2e9debb628c6ca86b78f625ceaf9bab/coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511", size = 203463, upload-time = "2023-05-29T20:07:49.939Z" }, + { url = "https://files.pythonhosted.org/packages/b7/00/14b00a0748e9eda26e97be07a63cc911108844004687321ddcc213be956c/coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3", size = 204347, upload-time = "2023-05-29T20:07:51.909Z" }, + { url = "https://files.pythonhosted.org/packages/80/d7/67937c80b8fd4c909fdac29292bc8b35d9505312cff6bcab41c53c5b1df6/coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f", size = 200580, upload-time = "2023-05-29T20:07:54.076Z" }, + { url = "https://files.pythonhosted.org/packages/7a/05/084864fa4bbf8106f44fb72a56e67e0cd372d3bf9d893be818338c81af5d/coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb", size = 226237, upload-time = "2023-05-29T20:07:56.28Z" }, + { url = "https://files.pythonhosted.org/packages/67/a2/6fa66a50e6e894286d79a3564f42bd54a9bd27049dc0a63b26d9924f0aa3/coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9", size = 224256, upload-time = "2023-05-29T20:07:58.189Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c0/73f139794c742840b9ab88e2e17fe14a3d4668a166ff95d812ac66c0829d/coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd", size = 225550, upload-time = "2023-05-29T20:08:00.383Z" }, + { url = "https://files.pythonhosted.org/packages/03/ec/6f30b4e0c96ce03b0e64aec46b4af2a8c49b70d1b5d0d69577add757b946/coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a", size = 232440, upload-time = "2023-05-29T20:08:02.495Z" }, + { url = "https://files.pythonhosted.org/packages/22/c1/2f6c1b6f01a0996c9e067a9c780e1824351dbe17faae54388a4477e6d86f/coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959", size = 230897, upload-time = "2023-05-29T20:08:04.382Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d6/53e999ec1bf7498ca4bc5f3b8227eb61db39068d2de5dcc359dec5601b5a/coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02", size = 232024, upload-time = "2023-05-29T20:08:06.031Z" }, + { url = "https://files.pythonhosted.org/packages/e9/40/383305500d24122dbed73e505a4d6828f8f3356d1f68ab6d32c781754b81/coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f", size = 203293, upload-time = "2023-05-29T20:08:07.598Z" }, + { url = "https://files.pythonhosted.org/packages/0e/bc/7e3a31534fabb043269f14fb64e2bb2733f85d4cf39e5bbc71357c57553a/coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0", size = 204040, upload-time = "2023-05-29T20:08:09.919Z" }, + { url = "https://files.pythonhosted.org/packages/c6/fc/be19131010930a6cf271da48202c8cc1d3f971f68c02fb2d3a78247f43dc/coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5", size = 200689, upload-time = "2023-05-29T20:08:11.594Z" }, + { url = "https://files.pythonhosted.org/packages/28/d7/9a8de57d87f4bbc6f9a6a5ded1eaac88a89bf71369bb935dac3c0cf2893e/coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5", size = 200986, upload-time = "2023-05-29T20:08:13.228Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e4/e6182e4697665fb594a7f4e4f27cb3a4dd00c2e3d35c5c706765de8c7866/coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9", size = 230648, upload-time = "2023-05-29T20:08:15.11Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e3/f552d5871943f747165b92a924055c5d6daa164ae659a13f9018e22f3990/coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6", size = 228511, upload-time = "2023-05-29T20:08:16.877Z" }, + { url = "https://files.pythonhosted.org/packages/44/55/49f65ccdd4dfd6d5528e966b28c37caec64170c725af32ab312889d2f857/coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e", size = 229852, upload-time = "2023-05-29T20:08:18.47Z" }, + { url = "https://files.pythonhosted.org/packages/0d/31/340428c238eb506feb96d4fb5c9ea614db1149517f22cc7ab8c6035ef6d9/coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050", size = 235578, upload-time = "2023-05-29T20:08:20.298Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ce/97c1dd6592c908425622fe7f31c017d11cf0421729b09101d4de75bcadc8/coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5", size = 234079, upload-time = "2023-05-29T20:08:22.365Z" }, + { url = "https://files.pythonhosted.org/packages/de/a3/5a98dc9e239d0dc5f243ef5053d5b1bdcaa1dee27a691dfc12befeccf878/coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f", size = 234991, upload-time = "2023-05-29T20:08:24.974Z" }, + { url = "https://files.pythonhosted.org/packages/4a/fb/78986d3022e5ccf2d4370bc43a5fef8374f092b3c21d32499dee8e30b7b6/coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e", size = 203160, upload-time = "2023-05-29T20:08:26.701Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1c/6b3c9c363fb1433c79128e0d692863deb761b1b78162494abb9e5c328bc0/coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c", size = 204085, upload-time = "2023-05-29T20:08:28.146Z" }, + { url = "https://files.pythonhosted.org/packages/88/da/495944ebf0ad246235a6bd523810d9f81981f9b81c6059ba1f56e943abe0/coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9", size = 200725, upload-time = "2023-05-29T20:08:29.851Z" }, + { url = "https://files.pythonhosted.org/packages/ca/0c/3dfeeb1006c44b911ee0ed915350db30325d01808525ae7cc8d57643a2ce/coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2", size = 201022, upload-time = "2023-05-29T20:08:31.429Z" }, + { url = "https://files.pythonhosted.org/packages/61/af/5964b8d7d9a5c767785644d9a5a63cacba9a9c45cc42ba06d25895ec87be/coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7", size = 229102, upload-time = "2023-05-29T20:08:32.982Z" }, + { url = "https://files.pythonhosted.org/packages/d9/1d/cd467fceb62c371f9adb1d739c92a05d4e550246daa90412e711226bd320/coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e", size = 227441, upload-time = "2023-05-29T20:08:35.044Z" }, + { url = "https://files.pythonhosted.org/packages/fe/57/e4f8ad64d84ca9e759d783a052795f62a9f9111585e46068845b1cb52c2b/coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1", size = 228265, upload-time = "2023-05-29T20:08:36.861Z" }, + { url = "https://files.pythonhosted.org/packages/88/8b/b0d9fe727acae907fa7f1c8194ccb6fe9d02e1c3e9001ecf74c741f86110/coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9", size = 234217, upload-time = "2023-05-29T20:08:38.837Z" }, + { url = "https://files.pythonhosted.org/packages/66/2e/c99fe1f6396d93551aa352c75410686e726cd4ea104479b9af1af22367ce/coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250", size = 232466, upload-time = "2023-05-29T20:08:40.768Z" }, + { url = "https://files.pythonhosted.org/packages/bb/e9/88747b40c8fb4a783b40222510ce6d66170217eb05d7f46462c36b4fa8cc/coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2", size = 233669, upload-time = "2023-05-29T20:08:42.944Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d5/a8e276bc005e42114468d4fe03e0a9555786bc51cbfe0d20827a46c1565a/coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb", size = 203199, upload-time = "2023-05-29T20:08:44.734Z" }, + { url = "https://files.pythonhosted.org/packages/a9/0c/4a848ae663b47f1195abcb09a951751dd61f80b503303b9b9d768e0fd321/coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27", size = 204109, upload-time = "2023-05-29T20:08:46.417Z" }, + { url = "https://files.pythonhosted.org/packages/67/fb/b3b1d7887e1ea25a9608b0776e480e4bbc303ca95a31fd585555ec4fff5a/coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d", size = 193207, upload-time = "2023-05-29T20:08:48.153Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", version = "2.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, +] + +[[package]] +name = "coverage" +version = "7.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/08/7e37f82e4d1aead42a7443ff06a1e406aabf7302c4f00a546e4b320b994c/coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d", size = 798791, upload-time = "2024-08-04T19:45:30.9Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/61/eb7ce5ed62bacf21beca4937a90fe32545c91a3c8a42a30c6616d48fc70d/coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16", size = 206690, upload-time = "2024-08-04T19:43:07.695Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/041928e434442bd3afde5584bdc3f932fb4562b1597629f537387cec6f3d/coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36", size = 207127, upload-time = "2024-08-04T19:43:10.15Z" }, + { url = "https://files.pythonhosted.org/packages/c7/c8/6ca52b5147828e45ad0242388477fdb90df2c6cbb9a441701a12b3c71bc8/coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02", size = 235654, upload-time = "2024-08-04T19:43:12.405Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/9ac2b62557f4340270942011d6efeab9833648380109e897d48ab7c1035d/coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc", size = 233598, upload-time = "2024-08-04T19:43:14.078Z" }, + { url = "https://files.pythonhosted.org/packages/53/23/9e2c114d0178abc42b6d8d5281f651a8e6519abfa0ef460a00a91f80879d/coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23", size = 234732, upload-time = "2024-08-04T19:43:16.632Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7e/a0230756fb133343a52716e8b855045f13342b70e48e8ad41d8a0d60ab98/coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34", size = 233816, upload-time = "2024-08-04T19:43:19.049Z" }, + { url = "https://files.pythonhosted.org/packages/28/7c/3753c8b40d232b1e5eeaed798c875537cf3cb183fb5041017c1fdb7ec14e/coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c", size = 232325, upload-time = "2024-08-04T19:43:21.246Z" }, + { url = "https://files.pythonhosted.org/packages/57/e3/818a2b2af5b7573b4b82cf3e9f137ab158c90ea750a8f053716a32f20f06/coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959", size = 233418, upload-time = "2024-08-04T19:43:22.945Z" }, + { url = "https://files.pythonhosted.org/packages/c8/fb/4532b0b0cefb3f06d201648715e03b0feb822907edab3935112b61b885e2/coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232", size = 209343, upload-time = "2024-08-04T19:43:25.121Z" }, + { url = "https://files.pythonhosted.org/packages/5a/25/af337cc7421eca1c187cc9c315f0a755d48e755d2853715bfe8c418a45fa/coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0", size = 210136, upload-time = "2024-08-04T19:43:26.851Z" }, + { url = "https://files.pythonhosted.org/packages/ad/5f/67af7d60d7e8ce61a4e2ddcd1bd5fb787180c8d0ae0fbd073f903b3dd95d/coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93", size = 206796, upload-time = "2024-08-04T19:43:29.115Z" }, + { url = "https://files.pythonhosted.org/packages/e1/0e/e52332389e057daa2e03be1fbfef25bb4d626b37d12ed42ae6281d0a274c/coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3", size = 207244, upload-time = "2024-08-04T19:43:31.285Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cd/766b45fb6e090f20f8927d9c7cb34237d41c73a939358bc881883fd3a40d/coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff", size = 239279, upload-time = "2024-08-04T19:43:33.581Z" }, + { url = "https://files.pythonhosted.org/packages/70/6c/a9ccd6fe50ddaf13442a1e2dd519ca805cbe0f1fcd377fba6d8339b98ccb/coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d", size = 236859, upload-time = "2024-08-04T19:43:35.301Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/8351b465febb4dbc1ca9929505202db909c5a635c6fdf33e089bbc3d7d85/coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6", size = 238549, upload-time = "2024-08-04T19:43:37.578Z" }, + { url = "https://files.pythonhosted.org/packages/68/3c/289b81fa18ad72138e6d78c4c11a82b5378a312c0e467e2f6b495c260907/coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56", size = 237477, upload-time = "2024-08-04T19:43:39.92Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1c/aa1efa6459d822bd72c4abc0b9418cf268de3f60eeccd65dc4988553bd8d/coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234", size = 236134, upload-time = "2024-08-04T19:43:41.453Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c8/521c698f2d2796565fe9c789c2ee1ccdae610b3aa20b9b2ef980cc253640/coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133", size = 236910, upload-time = "2024-08-04T19:43:43.037Z" }, + { url = "https://files.pythonhosted.org/packages/7d/30/033e663399ff17dca90d793ee8a2ea2890e7fdf085da58d82468b4220bf7/coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c", size = 209348, upload-time = "2024-08-04T19:43:44.787Z" }, + { url = "https://files.pythonhosted.org/packages/20/05/0d1ccbb52727ccdadaa3ff37e4d2dc1cd4d47f0c3df9eb58d9ec8508ca88/coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6", size = 210230, upload-time = "2024-08-04T19:43:46.707Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d4/300fc921dff243cd518c7db3a4c614b7e4b2431b0d1145c1e274fd99bd70/coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778", size = 206983, upload-time = "2024-08-04T19:43:49.082Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ab/6bf00de5327ecb8db205f9ae596885417a31535eeda6e7b99463108782e1/coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391", size = 207221, upload-time = "2024-08-04T19:43:52.15Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/2ead05e735022d1a7f3a0a683ac7f737de14850395a826192f0288703472/coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8", size = 240342, upload-time = "2024-08-04T19:43:53.746Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ef/94043e478201ffa85b8ae2d2c79b4081e5a1b73438aafafccf3e9bafb6b5/coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d", size = 237371, upload-time = "2024-08-04T19:43:55.993Z" }, + { url = "https://files.pythonhosted.org/packages/1f/0f/c890339dd605f3ebc269543247bdd43b703cce6825b5ed42ff5f2d6122c7/coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca", size = 239455, upload-time = "2024-08-04T19:43:57.618Z" }, + { url = "https://files.pythonhosted.org/packages/d1/04/7fd7b39ec7372a04efb0f70c70e35857a99b6a9188b5205efb4c77d6a57a/coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163", size = 238924, upload-time = "2024-08-04T19:44:00.012Z" }, + { url = "https://files.pythonhosted.org/packages/ed/bf/73ce346a9d32a09cf369f14d2a06651329c984e106f5992c89579d25b27e/coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a", size = 237252, upload-time = "2024-08-04T19:44:01.713Z" }, + { url = "https://files.pythonhosted.org/packages/86/74/1dc7a20969725e917b1e07fe71a955eb34bc606b938316bcc799f228374b/coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d", size = 238897, upload-time = "2024-08-04T19:44:03.898Z" }, + { url = "https://files.pythonhosted.org/packages/b6/e9/d9cc3deceb361c491b81005c668578b0dfa51eed02cd081620e9a62f24ec/coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5", size = 209606, upload-time = "2024-08-04T19:44:05.532Z" }, + { url = "https://files.pythonhosted.org/packages/47/c8/5a2e41922ea6740f77d555c4d47544acd7dc3f251fe14199c09c0f5958d3/coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb", size = 210373, upload-time = "2024-08-04T19:44:07.079Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f9/9aa4dfb751cb01c949c990d136a0f92027fbcc5781c6e921df1cb1563f20/coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106", size = 207007, upload-time = "2024-08-04T19:44:09.453Z" }, + { url = "https://files.pythonhosted.org/packages/b9/67/e1413d5a8591622a46dd04ff80873b04c849268831ed5c304c16433e7e30/coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9", size = 207269, upload-time = "2024-08-04T19:44:11.045Z" }, + { url = "https://files.pythonhosted.org/packages/14/5b/9dec847b305e44a5634d0fb8498d135ab1d88330482b74065fcec0622224/coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c", size = 239886, upload-time = "2024-08-04T19:44:12.83Z" }, + { url = "https://files.pythonhosted.org/packages/7b/b7/35760a67c168e29f454928f51f970342d23cf75a2bb0323e0f07334c85f3/coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a", size = 237037, upload-time = "2024-08-04T19:44:15.393Z" }, + { url = "https://files.pythonhosted.org/packages/f7/95/d2fd31f1d638df806cae59d7daea5abf2b15b5234016a5ebb502c2f3f7ee/coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060", size = 239038, upload-time = "2024-08-04T19:44:17.466Z" }, + { url = "https://files.pythonhosted.org/packages/6e/bd/110689ff5752b67924efd5e2aedf5190cbbe245fc81b8dec1abaffba619d/coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862", size = 238690, upload-time = "2024-08-04T19:44:19.336Z" }, + { url = "https://files.pythonhosted.org/packages/d3/a8/08d7b38e6ff8df52331c83130d0ab92d9c9a8b5462f9e99c9f051a4ae206/coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388", size = 236765, upload-time = "2024-08-04T19:44:20.994Z" }, + { url = "https://files.pythonhosted.org/packages/d6/6a/9cf96839d3147d55ae713eb2d877f4d777e7dc5ba2bce227167d0118dfe8/coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155", size = 238611, upload-time = "2024-08-04T19:44:22.616Z" }, + { url = "https://files.pythonhosted.org/packages/74/e4/7ff20d6a0b59eeaab40b3140a71e38cf52547ba21dbcf1d79c5a32bba61b/coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a", size = 209671, upload-time = "2024-08-04T19:44:24.418Z" }, + { url = "https://files.pythonhosted.org/packages/35/59/1812f08a85b57c9fdb6d0b383d779e47b6f643bc278ed682859512517e83/coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129", size = 210368, upload-time = "2024-08-04T19:44:26.276Z" }, + { url = "https://files.pythonhosted.org/packages/9c/15/08913be1c59d7562a3e39fce20661a98c0a3f59d5754312899acc6cb8a2d/coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e", size = 207758, upload-time = "2024-08-04T19:44:29.028Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ae/b5d58dff26cade02ada6ca612a76447acd69dccdbb3a478e9e088eb3d4b9/coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962", size = 208035, upload-time = "2024-08-04T19:44:30.673Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d7/62095e355ec0613b08dfb19206ce3033a0eedb6f4a67af5ed267a8800642/coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb", size = 250839, upload-time = "2024-08-04T19:44:32.412Z" }, + { url = "https://files.pythonhosted.org/packages/7c/1e/c2967cb7991b112ba3766df0d9c21de46b476d103e32bb401b1b2adf3380/coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704", size = 246569, upload-time = "2024-08-04T19:44:34.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/61/a7a6a55dd266007ed3b1df7a3386a0d760d014542d72f7c2c6938483b7bd/coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b", size = 248927, upload-time = "2024-08-04T19:44:36.313Z" }, + { url = "https://files.pythonhosted.org/packages/c8/fa/13a6f56d72b429f56ef612eb3bc5ce1b75b7ee12864b3bd12526ab794847/coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f", size = 248401, upload-time = "2024-08-04T19:44:38.155Z" }, + { url = "https://files.pythonhosted.org/packages/75/06/0429c652aa0fb761fc60e8c6b291338c9173c6aa0f4e40e1902345b42830/coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223", size = 246301, upload-time = "2024-08-04T19:44:39.883Z" }, + { url = "https://files.pythonhosted.org/packages/52/76/1766bb8b803a88f93c3a2d07e30ffa359467810e5cbc68e375ebe6906efb/coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3", size = 247598, upload-time = "2024-08-04T19:44:41.59Z" }, + { url = "https://files.pythonhosted.org/packages/66/8b/f54f8db2ae17188be9566e8166ac6df105c1c611e25da755738025708d54/coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", size = 210307, upload-time = "2024-08-04T19:44:43.301Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b0/e0dca6da9170aefc07515cce067b97178cefafb512d00a87a1c717d2efd5/coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", size = 211453, upload-time = "2024-08-04T19:44:45.677Z" }, + { url = "https://files.pythonhosted.org/packages/81/d0/d9e3d554e38beea5a2e22178ddb16587dbcbe9a1ef3211f55733924bf7fa/coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0", size = 206674, upload-time = "2024-08-04T19:44:47.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/ea/cab2dc248d9f45b2b7f9f1f596a4d75a435cb364437c61b51d2eb33ceb0e/coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a", size = 207101, upload-time = "2024-08-04T19:44:49.32Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6f/f82f9a500c7c5722368978a5390c418d2a4d083ef955309a8748ecaa8920/coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b", size = 236554, upload-time = "2024-08-04T19:44:51.631Z" }, + { url = "https://files.pythonhosted.org/packages/a6/94/d3055aa33d4e7e733d8fa309d9adf147b4b06a82c1346366fc15a2b1d5fa/coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3", size = 234440, upload-time = "2024-08-04T19:44:53.464Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6e/885bcd787d9dd674de4a7d8ec83faf729534c63d05d51d45d4fa168f7102/coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de", size = 235889, upload-time = "2024-08-04T19:44:55.165Z" }, + { url = "https://files.pythonhosted.org/packages/f4/63/df50120a7744492710854860783d6819ff23e482dee15462c9a833cc428a/coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6", size = 235142, upload-time = "2024-08-04T19:44:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5d/9d0acfcded2b3e9ce1c7923ca52ccc00c78a74e112fc2aee661125b7843b/coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569", size = 233805, upload-time = "2024-08-04T19:44:59.033Z" }, + { url = "https://files.pythonhosted.org/packages/c4/56/50abf070cb3cd9b1dd32f2c88f083aab561ecbffbcd783275cb51c17f11d/coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989", size = 234655, upload-time = "2024-08-04T19:45:01.398Z" }, + { url = "https://files.pythonhosted.org/packages/25/ee/b4c246048b8485f85a2426ef4abab88e48c6e80c74e964bea5cd4cd4b115/coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7", size = 209296, upload-time = "2024-08-04T19:45:03.819Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1c/96cf86b70b69ea2b12924cdf7cabb8ad10e6130eab8d767a1099fbd2a44f/coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8", size = 210137, upload-time = "2024-08-04T19:45:06.25Z" }, + { url = "https://files.pythonhosted.org/packages/19/d3/d54c5aa83268779d54c86deb39c1c4566e5d45c155369ca152765f8db413/coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255", size = 206688, upload-time = "2024-08-04T19:45:08.358Z" }, + { url = "https://files.pythonhosted.org/packages/a5/fe/137d5dca72e4a258b1bc17bb04f2e0196898fe495843402ce826a7419fe3/coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8", size = 207120, upload-time = "2024-08-04T19:45:11.526Z" }, + { url = "https://files.pythonhosted.org/packages/78/5b/a0a796983f3201ff5485323b225d7c8b74ce30c11f456017e23d8e8d1945/coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2", size = 235249, upload-time = "2024-08-04T19:45:13.202Z" }, + { url = "https://files.pythonhosted.org/packages/4e/e1/76089d6a5ef9d68f018f65411fcdaaeb0141b504587b901d74e8587606ad/coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a", size = 233237, upload-time = "2024-08-04T19:45:14.961Z" }, + { url = "https://files.pythonhosted.org/packages/9a/6f/eef79b779a540326fee9520e5542a8b428cc3bfa8b7c8f1022c1ee4fc66c/coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc", size = 234311, upload-time = "2024-08-04T19:45:16.924Z" }, + { url = "https://files.pythonhosted.org/packages/75/e1/656d65fb126c29a494ef964005702b012f3498db1a30dd562958e85a4049/coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004", size = 233453, upload-time = "2024-08-04T19:45:18.672Z" }, + { url = "https://files.pythonhosted.org/packages/68/6a/45f108f137941a4a1238c85f28fd9d048cc46b5466d6b8dda3aba1bb9d4f/coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb", size = 231958, upload-time = "2024-08-04T19:45:20.63Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e7/47b809099168b8b8c72ae311efc3e88c8d8a1162b3ba4b8da3cfcdb85743/coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36", size = 232938, upload-time = "2024-08-04T19:45:23.062Z" }, + { url = "https://files.pythonhosted.org/packages/52/80/052222ba7058071f905435bad0ba392cc12006380731c37afaf3fe749b88/coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c", size = 209352, upload-time = "2024-08-04T19:45:25.042Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d8/1b92e0b3adcf384e98770a00ca095da1b5f7b483e6563ae4eb5e935d24a1/coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca", size = 210153, upload-time = "2024-08-04T19:45:27.079Z" }, + { url = "https://files.pythonhosted.org/packages/a5/2b/0354ed096bca64dc8e32a7cbcae28b34cb5ad0b1fe2125d6d99583313ac0/coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df", size = 198926, upload-time = "2024-08-04T19:45:28.875Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, +] + +[[package]] +name = "coverage" +version = "7.10.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", size = 217987, upload-time = "2025-09-21T20:00:57.218Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", size = 218388, upload-time = "2025-09-21T20:01:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17", size = 245148, upload-time = "2025-09-21T20:01:01.768Z" }, + { url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", size = 246958, upload-time = "2025-09-21T20:01:03.355Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", size = 248819, upload-time = "2025-09-21T20:01:04.968Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e", size = 245754, upload-time = "2025-09-21T20:01:06.321Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e", size = 246860, upload-time = "2025-09-21T20:01:07.605Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df", size = 244877, upload-time = "2025-09-21T20:01:08.829Z" }, + { url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0", size = 245108, upload-time = "2025-09-21T20:01:10.527Z" }, + { url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13", size = 245752, upload-time = "2025-09-21T20:01:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b", size = 220497, upload-time = "2025-09-21T20:01:13.459Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807", size = 221392, upload-time = "2025-09-21T20:01:14.722Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102, upload-time = "2025-09-21T20:01:16.089Z" }, + { url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505, upload-time = "2025-09-21T20:01:17.788Z" }, + { url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898, upload-time = "2025-09-21T20:01:19.488Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", size = 250831, upload-time = "2025-09-21T20:01:20.817Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", size = 252937, upload-time = "2025-09-21T20:01:22.171Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23", size = 249021, upload-time = "2025-09-21T20:01:23.907Z" }, + { url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab", size = 250626, upload-time = "2025-09-21T20:01:25.721Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82", size = 248682, upload-time = "2025-09-21T20:01:27.105Z" }, + { url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2", size = 248402, upload-time = "2025-09-21T20:01:28.629Z" }, + { url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61", size = 249320, upload-time = "2025-09-21T20:01:30.004Z" }, + { url = "https://files.pythonhosted.org/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14", size = 220536, upload-time = "2025-09-21T20:01:32.184Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2", size = 221425, upload-time = "2025-09-21T20:01:33.557Z" }, + { url = "https://files.pythonhosted.org/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a", size = 220103, upload-time = "2025-09-21T20:01:34.929Z" }, + { url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290, upload-time = "2025-09-21T20:01:36.455Z" }, + { url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515, upload-time = "2025-09-21T20:01:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020, upload-time = "2025-09-21T20:01:39.617Z" }, + { url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769, upload-time = "2025-09-21T20:01:41.341Z" }, + { url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901, upload-time = "2025-09-21T20:01:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413, upload-time = "2025-09-21T20:01:44.469Z" }, + { url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820, upload-time = "2025-09-21T20:01:45.915Z" }, + { url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941, upload-time = "2025-09-21T20:01:47.296Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519, upload-time = "2025-09-21T20:01:48.73Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375, upload-time = "2025-09-21T20:01:50.529Z" }, + { url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256", size = 220699, upload-time = "2025-09-21T20:01:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba", size = 221512, upload-time = "2025-09-21T20:01:53.481Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf", size = 220147, upload-time = "2025-09-21T20:01:55.2Z" }, + { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" }, + { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" }, + { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" }, + { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" }, + { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" }, + { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" }, + { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" }, + { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" }, + { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" }, + { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" }, + { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" }, + { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" }, + { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" }, + { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" }, + { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" }, + { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" }, + { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" }, + { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" }, + { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" }, + { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" }, + { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" }, + { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" }, + { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" }, + { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" }, + { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" }, + { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" }, + { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" }, + { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" }, + { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" }, + { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" }, + { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/d1c25053764b4c42eb294aae92ab617d2e4f803397f9c7c8295caa77a260/coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3", size = 217978, upload-time = "2025-09-21T20:03:30.362Z" }, + { url = "https://files.pythonhosted.org/packages/52/2f/b9f9daa39b80ece0b9548bbb723381e29bc664822d9a12c2135f8922c22b/coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c", size = 218370, upload-time = "2025-09-21T20:03:32.147Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6e/30d006c3b469e58449650642383dddf1c8fb63d44fdf92994bfd46570695/coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396", size = 244802, upload-time = "2025-09-21T20:03:33.919Z" }, + { url = "https://files.pythonhosted.org/packages/b0/49/8a070782ce7e6b94ff6a0b6d7c65ba6bc3091d92a92cef4cd4eb0767965c/coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40", size = 246625, upload-time = "2025-09-21T20:03:36.09Z" }, + { url = "https://files.pythonhosted.org/packages/6a/92/1c1c5a9e8677ce56d42b97bdaca337b2d4d9ebe703d8c174ede52dbabd5f/coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594", size = 248399, upload-time = "2025-09-21T20:03:38.342Z" }, + { url = "https://files.pythonhosted.org/packages/c0/54/b140edee7257e815de7426d5d9846b58505dffc29795fff2dfb7f8a1c5a0/coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a", size = 245142, upload-time = "2025-09-21T20:03:40.591Z" }, + { url = "https://files.pythonhosted.org/packages/e4/9e/6d6b8295940b118e8b7083b29226c71f6154f7ff41e9ca431f03de2eac0d/coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b", size = 246284, upload-time = "2025-09-21T20:03:42.355Z" }, + { url = "https://files.pythonhosted.org/packages/db/e5/5e957ca747d43dbe4d9714358375c7546cb3cb533007b6813fc20fce37ad/coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3", size = 244353, upload-time = "2025-09-21T20:03:44.218Z" }, + { url = "https://files.pythonhosted.org/packages/9a/45/540fc5cc92536a1b783b7ef99450bd55a4b3af234aae35a18a339973ce30/coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0", size = 244430, upload-time = "2025-09-21T20:03:46.065Z" }, + { url = "https://files.pythonhosted.org/packages/75/0b/8287b2e5b38c8fe15d7e3398849bb58d382aedc0864ea0fa1820e8630491/coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f", size = 245311, upload-time = "2025-09-21T20:03:48.19Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1d/29724999984740f0c86d03e6420b942439bf5bd7f54d4382cae386a9d1e9/coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431", size = 220500, upload-time = "2025-09-21T20:03:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/43/11/4b1e6b129943f905ca54c339f343877b55b365ae2558806c1be4f7476ed5/coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07", size = 221408, upload-time = "2025-09-21T20:03:51.803Z" }, + { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, +] + +[[package]] +name = "coverage" +version = "7.13.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/9a/3742e58fd04b233df95c012ee9f3dfe04708a5e1d32613bd2d47d4e1be0d/coverage-7.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147", size = 218633, upload-time = "2025-12-28T15:40:10.165Z" }, + { url = "https://files.pythonhosted.org/packages/7e/45/7e6bdc94d89cd7c8017ce735cf50478ddfe765d4fbf0c24d71d30ea33d7a/coverage-7.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d", size = 219147, upload-time = "2025-12-28T15:40:12.069Z" }, + { url = "https://files.pythonhosted.org/packages/f7/38/0d6a258625fd7f10773fe94097dc16937a5f0e3e0cdf3adef67d3ac6baef/coverage-7.13.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339dc63b3eba969067b00f41f15ad161bf2946613156fb131266d8debc8e44d0", size = 245894, upload-time = "2025-12-28T15:40:13.556Z" }, + { url = "https://files.pythonhosted.org/packages/27/58/409d15ea487986994cbd4d06376e9860e9b157cfbfd402b1236770ab8dd2/coverage-7.13.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db622b999ffe49cb891f2fff3b340cdc2f9797d01a0a202a0973ba2562501d90", size = 247721, upload-time = "2025-12-28T15:40:15.37Z" }, + { url = "https://files.pythonhosted.org/packages/da/bf/6e8056a83fd7a96c93341f1ffe10df636dd89f26d5e7b9ca511ce3bcf0df/coverage-7.13.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1443ba9acbb593fa7c1c29e011d7c9761545fe35e7652e85ce7f51a16f7e08d", size = 249585, upload-time = "2025-12-28T15:40:17.226Z" }, + { url = "https://files.pythonhosted.org/packages/f4/15/e1daff723f9f5959acb63cbe35b11203a9df77ee4b95b45fffd38b318390/coverage-7.13.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c832ec92c4499ac463186af72f9ed4d8daec15499b16f0a879b0d1c8e5cf4a3b", size = 246597, upload-time = "2025-12-28T15:40:19.028Z" }, + { url = "https://files.pythonhosted.org/packages/74/a6/1efd31c5433743a6ddbc9d37ac30c196bb07c7eab3d74fbb99b924c93174/coverage-7.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:562ec27dfa3f311e0db1ba243ec6e5f6ab96b1edfcfc6cf86f28038bc4961ce6", size = 247626, upload-time = "2025-12-28T15:40:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9f/1609267dd3e749f57fdd66ca6752567d1c13b58a20a809dc409b263d0b5f/coverage-7.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4de84e71173d4dada2897e5a0e1b7877e5eefbfe0d6a44edee6ce31d9b8ec09e", size = 245629, upload-time = "2025-12-28T15:40:22.397Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f6/6815a220d5ec2466383d7cc36131b9fa6ecbe95c50ec52a631ba733f306a/coverage-7.13.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a5a68357f686f8c4d527a2dc04f52e669c2fc1cbde38f6f7eb6a0e58cbd17cae", size = 245901, upload-time = "2025-12-28T15:40:23.836Z" }, + { url = "https://files.pythonhosted.org/packages/ac/58/40576554cd12e0872faf6d2c0eb3bc85f71d78427946ddd19ad65201e2c0/coverage-7.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:77cc258aeb29a3417062758975521eae60af6f79e930d6993555eeac6a8eac29", size = 246505, upload-time = "2025-12-28T15:40:25.421Z" }, + { url = "https://files.pythonhosted.org/packages/3b/77/9233a90253fba576b0eee81707b5781d0e21d97478e5377b226c5b096c0f/coverage-7.13.1-cp310-cp310-win32.whl", hash = "sha256:bb4f8c3c9a9f34423dba193f241f617b08ffc63e27f67159f60ae6baf2dcfe0f", size = 221257, upload-time = "2025-12-28T15:40:27.217Z" }, + { url = "https://files.pythonhosted.org/packages/e0/43/e842ff30c1a0a623ec80db89befb84a3a7aad7bfe44a6ea77d5a3e61fedd/coverage-7.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:c8e2706ceb622bc63bac98ebb10ef5da80ed70fbd8a7999a5076de3afaef0fb1", size = 222191, upload-time = "2025-12-28T15:40:28.916Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9b/77baf488516e9ced25fc215a6f75d803493fc3f6a1a1227ac35697910c2a/coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88", size = 218755, upload-time = "2025-12-28T15:40:30.812Z" }, + { url = "https://files.pythonhosted.org/packages/d7/cd/7ab01154e6eb79ee2fab76bf4d89e94c6648116557307ee4ebbb85e5c1bf/coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3", size = 219257, upload-time = "2025-12-28T15:40:32.333Z" }, + { url = "https://files.pythonhosted.org/packages/01/d5/b11ef7863ffbbdb509da0023fad1e9eda1c0eaea61a6d2ea5b17d4ac706e/coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9", size = 249657, upload-time = "2025-12-28T15:40:34.1Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7c/347280982982383621d29b8c544cf497ae07ac41e44b1ca4903024131f55/coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee", size = 251581, upload-time = "2025-12-28T15:40:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/82/f6/ebcfed11036ade4c0d75fa4453a6282bdd225bc073862766eec184a4c643/coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf", size = 253691, upload-time = "2025-12-28T15:40:37.626Z" }, + { url = "https://files.pythonhosted.org/packages/02/92/af8f5582787f5d1a8b130b2dcba785fa5e9a7a8e121a0bb2220a6fdbdb8a/coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3", size = 249799, upload-time = "2025-12-28T15:40:39.47Z" }, + { url = "https://files.pythonhosted.org/packages/24/aa/0e39a2a3b16eebf7f193863323edbff38b6daba711abaaf807d4290cf61a/coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef", size = 251389, upload-time = "2025-12-28T15:40:40.954Z" }, + { url = "https://files.pythonhosted.org/packages/73/46/7f0c13111154dc5b978900c0ccee2e2ca239b910890e674a77f1363d483e/coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851", size = 249450, upload-time = "2025-12-28T15:40:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ca/e80da6769e8b669ec3695598c58eef7ad98b0e26e66333996aee6316db23/coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb", size = 249170, upload-time = "2025-12-28T15:40:44.279Z" }, + { url = "https://files.pythonhosted.org/packages/af/18/9e29baabdec1a8644157f572541079b4658199cfd372a578f84228e860de/coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba", size = 250081, upload-time = "2025-12-28T15:40:45.748Z" }, + { url = "https://files.pythonhosted.org/packages/00/f8/c3021625a71c3b2f516464d322e41636aea381018319050a8114105872ee/coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19", size = 221281, upload-time = "2025-12-28T15:40:47.232Z" }, + { url = "https://files.pythonhosted.org/packages/27/56/c216625f453df6e0559ed666d246fcbaaa93f3aa99eaa5080cea1229aa3d/coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a", size = 222215, upload-time = "2025-12-28T15:40:49.19Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/be342e76f6e531cae6406dc46af0d350586f24d9b67fdfa6daee02df71af/coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c", size = 220886, upload-time = "2025-12-28T15:40:51.067Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3", size = 218927, upload-time = "2025-12-28T15:40:52.814Z" }, + { url = "https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e", size = 219288, upload-time = "2025-12-28T15:40:54.262Z" }, + { url = "https://files.pythonhosted.org/packages/d0/0a/853a76e03b0f7c4375e2ca025df45c918beb367f3e20a0a8e91967f6e96c/coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c", size = 250786, upload-time = "2025-12-28T15:40:56.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62", size = 253543, upload-time = "2025-12-28T15:40:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/96/b2/7f1f0437a5c855f87e17cf5d0dc35920b6440ff2b58b1ba9788c059c26c8/coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968", size = 254635, upload-time = "2025-12-28T15:40:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d1/73c3fdb8d7d3bddd9473c9c6a2e0682f09fc3dfbcb9c3f36412a7368bcab/coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e", size = 251202, upload-time = "2025-12-28T15:41:01.328Z" }, + { url = "https://files.pythonhosted.org/packages/66/3c/f0edf75dcc152f145d5598329e864bbbe04ab78660fe3e8e395f9fff010f/coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f", size = 252566, upload-time = "2025-12-28T15:41:03.319Z" }, + { url = "https://files.pythonhosted.org/packages/17/b3/e64206d3c5f7dcbceafd14941345a754d3dbc78a823a6ed526e23b9cdaab/coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee", size = 250711, upload-time = "2025-12-28T15:41:06.411Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ad/28a3eb970a8ef5b479ee7f0c484a19c34e277479a5b70269dc652b730733/coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf", size = 250278, upload-time = "2025-12-28T15:41:08.285Z" }, + { url = "https://files.pythonhosted.org/packages/54/e3/c8f0f1a93133e3e1291ca76cbb63565bd4b5c5df63b141f539d747fff348/coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c", size = 252154, upload-time = "2025-12-28T15:41:09.969Z" }, + { url = "https://files.pythonhosted.org/packages/d0/bf/9939c5d6859c380e405b19e736321f1c7d402728792f4c752ad1adcce005/coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7", size = 221487, upload-time = "2025-12-28T15:41:11.468Z" }, + { url = "https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6", size = 222299, upload-time = "2025-12-28T15:41:13.386Z" }, + { url = "https://files.pythonhosted.org/packages/10/79/176a11203412c350b3e9578620013af35bcdb79b651eb976f4a4b32044fa/coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c", size = 220941, upload-time = "2025-12-28T15:41:14.975Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a4/e98e689347a1ff1a7f67932ab535cef82eb5e78f32a9e4132e114bbb3a0a/coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78", size = 218951, upload-time = "2025-12-28T15:41:16.653Z" }, + { url = "https://files.pythonhosted.org/packages/32/33/7cbfe2bdc6e2f03d6b240d23dc45fdaf3fd270aaf2d640be77b7f16989ab/coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b", size = 219325, upload-time = "2025-12-28T15:41:18.609Z" }, + { url = "https://files.pythonhosted.org/packages/59/f6/efdabdb4929487baeb7cb2a9f7dac457d9356f6ad1b255be283d58b16316/coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd", size = 250309, upload-time = "2025-12-28T15:41:20.629Z" }, + { url = "https://files.pythonhosted.org/packages/12/da/91a52516e9d5aea87d32d1523f9cdcf7a35a3b298e6be05d6509ba3cfab2/coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992", size = 252907, upload-time = "2025-12-28T15:41:22.257Z" }, + { url = "https://files.pythonhosted.org/packages/75/38/f1ea837e3dc1231e086db1638947e00d264e7e8c41aa8ecacf6e1e0c05f4/coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4", size = 254148, upload-time = "2025-12-28T15:41:23.87Z" }, + { url = "https://files.pythonhosted.org/packages/7f/43/f4f16b881aaa34954ba446318dea6b9ed5405dd725dd8daac2358eda869a/coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a", size = 250515, upload-time = "2025-12-28T15:41:25.437Z" }, + { url = "https://files.pythonhosted.org/packages/84/34/8cba7f00078bd468ea914134e0144263194ce849ec3baad187ffb6203d1c/coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766", size = 252292, upload-time = "2025-12-28T15:41:28.459Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/cffac66c7652d84ee4ac52d3ccb94c015687d3b513f9db04bfcac2ac800d/coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4", size = 250242, upload-time = "2025-12-28T15:41:30.02Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/9a64d462263dde416f3c0067efade7b52b52796f489b1037a95b0dc389c9/coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398", size = 250068, upload-time = "2025-12-28T15:41:32.007Z" }, + { url = "https://files.pythonhosted.org/packages/69/c8/a8994f5fece06db7c4a97c8fc1973684e178599b42e66280dded0524ef00/coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784", size = 251846, upload-time = "2025-12-28T15:41:33.946Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f7/91fa73c4b80305c86598a2d4e54ba22df6bf7d0d97500944af7ef155d9f7/coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461", size = 221512, upload-time = "2025-12-28T15:41:35.519Z" }, + { url = "https://files.pythonhosted.org/packages/45/0b/0768b4231d5a044da8f75e097a8714ae1041246bb765d6b5563bab456735/coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500", size = 222321, upload-time = "2025-12-28T15:41:37.371Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b8/bdcb7253b7e85157282450262008f1366aa04663f3e3e4c30436f596c3e2/coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9", size = 220949, upload-time = "2025-12-28T15:41:39.553Z" }, + { url = "https://files.pythonhosted.org/packages/70/52/f2be52cc445ff75ea8397948c96c1b4ee14f7f9086ea62fc929c5ae7b717/coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc", size = 219643, upload-time = "2025-12-28T15:41:41.567Z" }, + { url = "https://files.pythonhosted.org/packages/47/79/c85e378eaa239e2edec0c5523f71542c7793fe3340954eafb0bc3904d32d/coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a", size = 219997, upload-time = "2025-12-28T15:41:43.418Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9b/b1ade8bfb653c0bbce2d6d6e90cc6c254cbb99b7248531cc76253cb4da6d/coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4", size = 261296, upload-time = "2025-12-28T15:41:45.207Z" }, + { url = "https://files.pythonhosted.org/packages/1f/af/ebf91e3e1a2473d523e87e87fd8581e0aa08741b96265730e2d79ce78d8d/coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6", size = 263363, upload-time = "2025-12-28T15:41:47.163Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8b/fb2423526d446596624ac7fde12ea4262e66f86f5120114c3cfd0bb2befa/coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1", size = 265783, upload-time = "2025-12-28T15:41:49.03Z" }, + { url = "https://files.pythonhosted.org/packages/9b/26/ef2adb1e22674913b89f0fe7490ecadcef4a71fa96f5ced90c60ec358789/coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd", size = 260508, upload-time = "2025-12-28T15:41:51.035Z" }, + { url = "https://files.pythonhosted.org/packages/ce/7d/f0f59b3404caf662e7b5346247883887687c074ce67ba453ea08c612b1d5/coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c", size = 263357, upload-time = "2025-12-28T15:41:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b1/29896492b0b1a047604d35d6fa804f12818fa30cdad660763a5f3159e158/coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0", size = 260978, upload-time = "2025-12-28T15:41:54.589Z" }, + { url = "https://files.pythonhosted.org/packages/48/f2/971de1238a62e6f0a4128d37adadc8bb882ee96afbe03ff1570291754629/coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e", size = 259877, upload-time = "2025-12-28T15:41:56.263Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fc/0474efcbb590ff8628830e9aaec5f1831594874360e3251f1fdec31d07a3/coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53", size = 262069, upload-time = "2025-12-28T15:41:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/88/4f/3c159b7953db37a7b44c0eab8a95c37d1aa4257c47b4602c04022d5cb975/coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842", size = 222184, upload-time = "2025-12-28T15:41:59.763Z" }, + { url = "https://files.pythonhosted.org/packages/58/a5/6b57d28f81417f9335774f20679d9d13b9a8fb90cd6160957aa3b54a2379/coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2", size = 223250, upload-time = "2025-12-28T15:42:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/81/7c/160796f3b035acfbb58be80e02e484548595aa67e16a6345e7910ace0a38/coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09", size = 221521, upload-time = "2025-12-28T15:42:03.275Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8e/ba0e597560c6563fc0adb902fda6526df5d4aa73bb10adf0574d03bd2206/coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894", size = 218996, upload-time = "2025-12-28T15:42:04.978Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8e/764c6e116f4221dc7aa26c4061181ff92edb9c799adae6433d18eeba7a14/coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a", size = 219326, upload-time = "2025-12-28T15:42:06.691Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a6/6130dc6d8da28cdcbb0f2bf8865aeca9b157622f7c0031e48c6cf9a0e591/coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f", size = 250374, upload-time = "2025-12-28T15:42:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/82/2b/783ded568f7cd6b677762f780ad338bf4b4750205860c17c25f7c708995e/coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909", size = 252882, upload-time = "2025-12-28T15:42:10.515Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b2/9808766d082e6a4d59eb0cc881a57fc1600eb2c5882813eefff8254f71b5/coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4", size = 254218, upload-time = "2025-12-28T15:42:12.208Z" }, + { url = "https://files.pythonhosted.org/packages/44/ea/52a985bb447c871cb4d2e376e401116520991b597c85afdde1ea9ef54f2c/coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75", size = 250391, upload-time = "2025-12-28T15:42:14.21Z" }, + { url = "https://files.pythonhosted.org/packages/7f/1d/125b36cc12310718873cfc8209ecfbc1008f14f4f5fa0662aa608e579353/coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9", size = 252239, upload-time = "2025-12-28T15:42:16.292Z" }, + { url = "https://files.pythonhosted.org/packages/6a/16/10c1c164950cade470107f9f14bbac8485f8fb8515f515fca53d337e4a7f/coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465", size = 250196, upload-time = "2025-12-28T15:42:18.54Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c6/cd860fac08780c6fd659732f6ced1b40b79c35977c1356344e44d72ba6c4/coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864", size = 250008, upload-time = "2025-12-28T15:42:20.365Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/a8c58d3d38f82a5711e1e0a67268362af48e1a03df27c03072ac30feefcf/coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9", size = 251671, upload-time = "2025-12-28T15:42:22.114Z" }, + { url = "https://files.pythonhosted.org/packages/f0/bc/fd4c1da651d037a1e3d53e8cb3f8182f4b53271ffa9a95a2e211bacc0349/coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5", size = 221777, upload-time = "2025-12-28T15:42:23.919Z" }, + { url = "https://files.pythonhosted.org/packages/4b/50/71acabdc8948464c17e90b5ffd92358579bd0910732c2a1c9537d7536aa6/coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a", size = 222592, upload-time = "2025-12-28T15:42:25.619Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c8/a6fb943081bb0cc926499c7907731a6dc9efc2cbdc76d738c0ab752f1a32/coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0", size = 221169, upload-time = "2025-12-28T15:42:27.629Z" }, + { url = "https://files.pythonhosted.org/packages/16/61/d5b7a0a0e0e40d62e59bc8c7aa1afbd86280d82728ba97f0673b746b78e2/coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a", size = 219730, upload-time = "2025-12-28T15:42:29.306Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2c/8881326445fd071bb49514d1ce97d18a46a980712b51fee84f9ab42845b4/coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6", size = 220001, upload-time = "2025-12-28T15:42:31.319Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d7/50de63af51dfa3a7f91cc37ad8fcc1e244b734232fbc8b9ab0f3c834a5cd/coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673", size = 261370, upload-time = "2025-12-28T15:42:32.992Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2c/d31722f0ec918fd7453b2758312729f645978d212b410cd0f7c2aed88a94/coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5", size = 263485, upload-time = "2025-12-28T15:42:34.759Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7a/2c114fa5c5fc08ba0777e4aec4c97e0b4a1afcb69c75f1f54cff78b073ab/coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d", size = 265890, upload-time = "2025-12-28T15:42:36.517Z" }, + { url = "https://files.pythonhosted.org/packages/65/d9/f0794aa1c74ceabc780fe17f6c338456bbc4e96bd950f2e969f48ac6fb20/coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8", size = 260445, upload-time = "2025-12-28T15:42:38.646Z" }, + { url = "https://files.pythonhosted.org/packages/49/23/184b22a00d9bb97488863ced9454068c79e413cb23f472da6cbddc6cfc52/coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486", size = 263357, upload-time = "2025-12-28T15:42:40.788Z" }, + { url = "https://files.pythonhosted.org/packages/7d/bd/58af54c0c9199ea4190284f389005779d7daf7bf3ce40dcd2d2b2f96da69/coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564", size = 260959, upload-time = "2025-12-28T15:42:42.808Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2a/6839294e8f78a4891bf1df79d69c536880ba2f970d0ff09e7513d6e352e9/coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7", size = 259792, upload-time = "2025-12-28T15:42:44.818Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c3/528674d4623283310ad676c5af7414b9850ab6d55c2300e8aa4b945ec554/coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416", size = 262123, upload-time = "2025-12-28T15:42:47.108Z" }, + { url = "https://files.pythonhosted.org/packages/06/c5/8c0515692fb4c73ac379d8dc09b18eaf0214ecb76ea6e62467ba7a1556ff/coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f", size = 222562, upload-time = "2025-12-28T15:42:49.144Z" }, + { url = "https://files.pythonhosted.org/packages/05/0e/c0a0c4678cb30dac735811db529b321d7e1c9120b79bd728d4f4d6b010e9/coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79", size = 223670, upload-time = "2025-12-28T15:42:51.218Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/b177aa0011f354abf03a8f30a85032686d290fdeed4222b27d36b4372a50/coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4", size = 221707, upload-time = "2025-12-28T15:42:53.034Z" }, + { url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and python_full_version <= '3.11'" }, +] + +[[package]] +name = "cstruct" +source = { editable = "." } + +[package.optional-dependencies] +dev = [ + { name = "codespell", version = "2.2.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "codespell", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8'" }, + { name = "coverage", version = "7.2.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.8'" }, + { name = "coverage", version = "7.6.1", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version == '3.8.*'" }, + { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version == '3.9.*'" }, + { name = "coverage", version = "7.13.1", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" }, + { name = "markdown-include" }, + { name = "mkdocs", version = "1.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "mkdocs", version = "1.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8'" }, + { name = "mkdocs-material", version = "9.2.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "mkdocs-material", version = "9.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8'" }, + { name = "mkdocstrings", version = "0.22.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "mkdocstrings", version = "0.26.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "mkdocstrings", version = "0.30.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "mkdocstrings", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mkdocstrings-python", version = "1.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "mkdocstrings-python", version = "1.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "mkdocstrings-python", version = "1.18.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "mkdocstrings-python", version = "2.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mypy", version = "1.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "mypy", version = "1.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "mypy", version = "1.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pytest", version = "7.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest-cov", version = "4.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "pytest-cov", version = "5.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "pytest-cov", version = "7.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "ruff" }, +] +test = [ + { name = "coverage", version = "7.2.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.8'" }, + { name = "coverage", version = "7.6.1", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version == '3.8.*'" }, + { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version == '3.9.*'" }, + { name = "coverage", version = "7.13.1", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" }, + { name = "pytest", version = "7.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest-cov", version = "4.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "pytest-cov", version = "5.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "pytest-cov", version = "7.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] + +[package.metadata] +requires-dist = [ + { name = "codespell", marker = "extra == 'dev'" }, + { name = "coverage", extras = ["toml"], marker = "extra == 'dev'" }, + { name = "coverage", extras = ["toml"], marker = "extra == 'test'" }, + { name = "markdown-include", marker = "extra == 'dev'" }, + { name = "mkdocs", marker = "extra == 'dev'" }, + { name = "mkdocs-material", marker = "extra == 'dev'" }, + { name = "mkdocstrings", marker = "extra == 'dev'" }, + { name = "mkdocstrings-python", marker = "extra == 'dev'" }, + { name = "mypy", marker = "extra == 'dev'" }, + { name = "pytest", marker = "extra == 'dev'" }, + { name = "pytest", marker = "extra == 'test'" }, + { name = "pytest-cov", marker = "extra == 'dev'" }, + { name = "pytest-cov", marker = "extra == 'test'" }, + { name = "ruff", marker = "extra == 'dev'" }, +] +provides-extras = ["dev", "test"] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", version = "4.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[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, 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, upload-time = "2022-05-02T15:47:14.552Z" }, +] + +[[package]] +name = "griffe" +version = "0.30.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "cached-property", marker = "python_full_version < '3.8'" }, + { name = "colorama", marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/1e/57a435627c00c2e122a9404c4f76b4ef0cd19b9cf69f806b6db9a372f9a5/griffe-0.30.1.tar.gz", hash = "sha256:007cc11acd20becf1bb8f826419a52b9d403bbad9d8c8535699f5440ddc0a109", size = 110164, upload-time = "2023-07-02T15:56:32.124Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/3e/3ea68eddeab546a0c02a3d5c1b4e9440d45fe83027088665a2b189d838fc/griffe-0.30.1-py3-none-any.whl", hash = "sha256:b2f3df6952995a6bebe19f797189d67aba7c860755d3d21cc80f64d076d0154c", size = 97343, upload-time = "2023-07-02T15:56:29.948Z" }, +] + +[[package]] +name = "griffe" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +dependencies = [ + { name = "astunparse", marker = "python_full_version == '3.8.*'" }, + { name = "colorama", marker = "python_full_version == '3.8.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/e9/b2c86ad9d69053e497a24ceb25d661094fb321ab4ed39a8b71793dcbae82/griffe-1.4.0.tar.gz", hash = "sha256:8fccc585896d13f1221035d32c50dec65830c87d23f9adb9b1e6f3d63574f7f5", size = 381028, upload-time = "2024-10-11T12:53:54.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/7c/e9e66869c2e4c9b378474e49c993128ec0131ef4721038b6d06e50538caf/griffe-1.4.0-py3-none-any.whl", hash = "sha256:e589de8b8c137e99a46ec45f9598fc0ac5b6868ce824b24db09c02d117b89bc5", size = 127015, upload-time = "2024-10-11T12:53:52.383Z" }, +] + +[[package]] +name = "griffe" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version == '3.9.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/d7/6c09dd7ce4c7837e4cdb11dce980cb45ae3cd87677298dc3b781b6bce7d3/griffe-1.14.0.tar.gz", hash = "sha256:9d2a15c1eca966d68e00517de5d69dd1bc5c9f2335ef6c1775362ba5b8651a13", size = 424684, upload-time = "2025-09-05T15:02:29.167Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/b1/9ff6578d789a89812ff21e4e0f80ffae20a65d5dd84e7a17873fe3b365be/griffe-1.14.0-py3-none-any.whl", hash = "sha256:0e9d52832cccf0f7188cfe585ba962d2674b241c01916d780925df34873bceb0", size = 144439, upload-time = "2025-09-05T15:02:27.511Z" }, +] + +[[package]] +name = "griffe" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/0c/3a471b6e31951dce2360477420d0a8d1e00dea6cf33b70f3e8c3ab6e28e1/griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea", size = 424112, upload-time = "2025-11-10T15:03:15.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3", size = 150705, upload-time = "2025-11-10T15:03:13.549Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +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, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "6.7.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "typing-extensions", version = "4.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "zipp", version = "3.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/82/f6e29c8d5c098b6be61460371c2c5591f4a335923639edec43b3830650a4/importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4", size = 53569, upload-time = "2023-06-18T21:44:35.024Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/94/64287b38c7de4c90683630338cf28f129decbba0a44f0c6db35a873c73c4/importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5", size = 22934, upload-time = "2023-06-18T21:44:33.441Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +dependencies = [ + { name = "zipp", version = "3.20.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304, upload-time = "2024-09-11T14:56:08.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514, upload-time = "2024-09-11T14:56:07.019Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "zipp", version = "3.23.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +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, upload-time = "2023-01-07T11:08:09.864Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", +] +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/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]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +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/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]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe", version = "2.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "markupsafe", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +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, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "librt" +version = "0.7.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/29/47f29026ca17f35cf299290292d5f8331f5077364974b7675a353179afa2/librt-0.7.7.tar.gz", hash = "sha256:81d957b069fed1890953c3b9c3895c7689960f233eea9a1d9607f71ce7f00b2c", size = 145910, upload-time = "2026-01-01T23:52:22.87Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/84/2cfb1f3b9b60bab52e16a220c931223fc8e963d0d7bb9132bef012aafc3f/librt-0.7.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4836c5645f40fbdc275e5670819bde5ab5f2e882290d304e3c6ddab1576a6d0", size = 54709, upload-time = "2026-01-01T23:50:48.326Z" }, + { url = "https://files.pythonhosted.org/packages/19/a1/3127b277e9d3784a8040a54e8396d9ae5c64d6684dc6db4b4089b0eedcfb/librt-0.7.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ae8aec43117a645a31e5f60e9e3a0797492e747823b9bda6972d521b436b4e8", size = 56658, upload-time = "2026-01-01T23:50:49.74Z" }, + { url = "https://files.pythonhosted.org/packages/3a/e9/b91b093a5c42eb218120445f3fef82e0b977fa2225f4d6fc133d25cdf86a/librt-0.7.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:aea05f701ccd2a76b34f0daf47ca5068176ff553510b614770c90d76ac88df06", size = 161026, upload-time = "2026-01-01T23:50:50.853Z" }, + { url = "https://files.pythonhosted.org/packages/c7/cb/1ded77d5976a79d7057af4a010d577ce4f473ff280984e68f4974a3281e5/librt-0.7.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b16ccaeff0ed4355dfb76fe1ea7a5d6d03b5ad27f295f77ee0557bc20a72495", size = 169529, upload-time = "2026-01-01T23:50:52.24Z" }, + { url = "https://files.pythonhosted.org/packages/da/6e/6ca5bdaa701e15f05000ac1a4c5d1475c422d3484bd3d1ca9e8c2f5be167/librt-0.7.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c48c7e150c095d5e3cea7452347ba26094be905d6099d24f9319a8b475fcd3e0", size = 183271, upload-time = "2026-01-01T23:50:55.287Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2d/55c0e38073997b4bbb5ddff25b6d1bbba8c2f76f50afe5bb9c844b702f34/librt-0.7.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4dcee2f921a8632636d1c37f1bbdb8841d15666d119aa61e5399c5268e7ce02e", size = 179039, upload-time = "2026-01-01T23:50:56.807Z" }, + { url = "https://files.pythonhosted.org/packages/33/4e/3662a41ae8bb81b226f3968426293517b271d34d4e9fd4b59fc511f1ae40/librt-0.7.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:14ef0f4ac3728ffd85bfc58e2f2f48fb4ef4fa871876f13a73a7381d10a9f77c", size = 173505, upload-time = "2026-01-01T23:50:58.291Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5d/cf768deb8bdcbac5f8c21fcb32dd483d038d88c529fd351bbe50590b945d/librt-0.7.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e4ab69fa37f8090f2d971a5d2bc606c7401170dbdae083c393d6cbf439cb45b8", size = 193570, upload-time = "2026-01-01T23:50:59.546Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ea/ee70effd13f1d651976d83a2812391f6203971740705e3c0900db75d4bce/librt-0.7.7-cp310-cp310-win32.whl", hash = "sha256:4bf3cc46d553693382d2abf5f5bd493d71bb0f50a7c0beab18aa13a5545c8900", size = 42600, upload-time = "2026-01-01T23:51:00.694Z" }, + { url = "https://files.pythonhosted.org/packages/f0/eb/dc098730f281cba76c279b71783f5de2edcba3b880c1ab84a093ef826062/librt-0.7.7-cp310-cp310-win_amd64.whl", hash = "sha256:f0c8fe5aeadd8a0e5b0598f8a6ee3533135ca50fd3f20f130f9d72baf5c6ac58", size = 48977, upload-time = "2026-01-01T23:51:01.726Z" }, + { url = "https://files.pythonhosted.org/packages/f0/56/30b5c342518005546df78841cb0820ae85a17e7d07d521c10ef367306d0d/librt-0.7.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a487b71fbf8a9edb72a8c7a456dda0184642d99cd007bc819c0b7ab93676a8ee", size = 54709, upload-time = "2026-01-01T23:51:02.774Z" }, + { url = "https://files.pythonhosted.org/packages/72/78/9f120e3920b22504d4f3835e28b55acc2cc47c9586d2e1b6ba04c3c1bf01/librt-0.7.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f4d4efb218264ecf0f8516196c9e2d1a0679d9fb3bb15df1155a35220062eba8", size = 56663, upload-time = "2026-01-01T23:51:03.838Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ea/7d7a1ee7dfc1151836028eba25629afcf45b56bbc721293e41aa2e9b8934/librt-0.7.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b8bb331aad734b059c4b450cd0a225652f16889e286b2345af5e2c3c625c3d85", size = 161705, upload-time = "2026-01-01T23:51:04.917Z" }, + { url = "https://files.pythonhosted.org/packages/45/a5/952bc840ac8917fbcefd6bc5f51ad02b89721729814f3e2bfcc1337a76d6/librt-0.7.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:467dbd7443bda08338fc8ad701ed38cef48194017554f4c798b0a237904b3f99", size = 171029, upload-time = "2026-01-01T23:51:06.09Z" }, + { url = "https://files.pythonhosted.org/packages/fa/bf/c017ff7da82dc9192cf40d5e802a48a25d00e7639b6465cfdcee5893a22c/librt-0.7.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50d1d1ee813d2d1a3baf2873634ba506b263032418d16287c92ec1cc9c1a00cb", size = 184704, upload-time = "2026-01-01T23:51:07.549Z" }, + { url = "https://files.pythonhosted.org/packages/77/ec/72f3dd39d2cdfd6402ab10836dc9cbf854d145226062a185b419c4f1624a/librt-0.7.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c7e5070cf3ec92d98f57574da0224f8c73faf1ddd6d8afa0b8c9f6e86997bc74", size = 180719, upload-time = "2026-01-01T23:51:09.062Z" }, + { url = "https://files.pythonhosted.org/packages/78/86/06e7a1a81b246f3313bf515dd9613a1c81583e6fd7843a9f4d625c4e926d/librt-0.7.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bdb9f3d865b2dafe7f9ad7f30ef563c80d0ddd2fdc8cc9b8e4f242f475e34d75", size = 174537, upload-time = "2026-01-01T23:51:10.611Z" }, + { url = "https://files.pythonhosted.org/packages/83/08/f9fb2edc9c7a76e95b2924ce81d545673f5b034e8c5dd92159d1c7dae0c6/librt-0.7.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8185c8497d45164e256376f9da5aed2bb26ff636c798c9dabe313b90e9f25b28", size = 195238, upload-time = "2026-01-01T23:51:11.762Z" }, + { url = "https://files.pythonhosted.org/packages/ba/56/ea2d2489d3ea1f47b301120e03a099e22de7b32c93df9a211e6ff4f9bf38/librt-0.7.7-cp311-cp311-win32.whl", hash = "sha256:44d63ce643f34a903f09ff7ca355aae019a3730c7afd6a3c037d569beeb5d151", size = 42939, upload-time = "2026-01-01T23:51:13.192Z" }, + { url = "https://files.pythonhosted.org/packages/58/7b/c288f417e42ba2a037f1c0753219e277b33090ed4f72f292fb6fe175db4c/librt-0.7.7-cp311-cp311-win_amd64.whl", hash = "sha256:7d13cc340b3b82134f8038a2bfe7137093693dcad8ba5773da18f95ad6b77a8a", size = 49240, upload-time = "2026-01-01T23:51:14.264Z" }, + { url = "https://files.pythonhosted.org/packages/7c/24/738eb33a6c1516fdb2dfd2a35db6e5300f7616679b573585be0409bc6890/librt-0.7.7-cp311-cp311-win_arm64.whl", hash = "sha256:983de36b5a83fe9222f4f7dcd071f9b1ac6f3f17c0af0238dadfb8229588f890", size = 42613, upload-time = "2026-01-01T23:51:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/56/72/1cd9d752070011641e8aee046c851912d5f196ecd726fffa7aed2070f3e0/librt-0.7.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2a85a1fc4ed11ea0eb0a632459ce004a2d14afc085a50ae3463cd3dfe1ce43fc", size = 55687, upload-time = "2026-01-01T23:51:16.291Z" }, + { url = "https://files.pythonhosted.org/packages/50/aa/d5a1d4221c4fe7e76ae1459d24d6037783cb83c7645164c07d7daf1576ec/librt-0.7.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c87654e29a35938baead1c4559858f346f4a2a7588574a14d784f300ffba0efd", size = 57136, upload-time = "2026-01-01T23:51:17.363Z" }, + { url = "https://files.pythonhosted.org/packages/23/6f/0c86b5cb5e7ef63208c8cc22534df10ecc5278efc0d47fb8815577f3ca2f/librt-0.7.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c9faaebb1c6212c20afd8043cd6ed9de0a47d77f91a6b5b48f4e46ed470703fe", size = 165320, upload-time = "2026-01-01T23:51:18.455Z" }, + { url = "https://files.pythonhosted.org/packages/16/37/df4652690c29f645ffe405b58285a4109e9fe855c5bb56e817e3e75840b3/librt-0.7.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1908c3e5a5ef86b23391448b47759298f87f997c3bd153a770828f58c2bb4630", size = 174216, upload-time = "2026-01-01T23:51:19.599Z" }, + { url = "https://files.pythonhosted.org/packages/9a/d6/d3afe071910a43133ec9c0f3e4ce99ee6df0d4e44e4bddf4b9e1c6ed41cc/librt-0.7.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dbc4900e95a98fc0729523be9d93a8fedebb026f32ed9ffc08acd82e3e181503", size = 189005, upload-time = "2026-01-01T23:51:21.052Z" }, + { url = "https://files.pythonhosted.org/packages/d5/18/74060a870fe2d9fd9f47824eba6717ce7ce03124a0d1e85498e0e7efc1b2/librt-0.7.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a7ea4e1fbd253e5c68ea0fe63d08577f9d288a73f17d82f652ebc61fa48d878d", size = 183961, upload-time = "2026-01-01T23:51:22.493Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5e/918a86c66304af66a3c1d46d54df1b2d0b8894babc42a14fb6f25511497f/librt-0.7.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ef7699b7a5a244b1119f85c5bbc13f152cd38240cbb2baa19b769433bae98e50", size = 177610, upload-time = "2026-01-01T23:51:23.874Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d7/b5e58dc2d570f162e99201b8c0151acf40a03a39c32ab824dd4febf12736/librt-0.7.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:955c62571de0b181d9e9e0a0303c8bc90d47670a5eff54cf71bf5da61d1899cf", size = 199272, upload-time = "2026-01-01T23:51:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/18/87/8202c9bd0968bdddc188ec3811985f47f58ed161b3749299f2c0dd0f63fb/librt-0.7.7-cp312-cp312-win32.whl", hash = "sha256:1bcd79be209313b270b0e1a51c67ae1af28adad0e0c7e84c3ad4b5cb57aaa75b", size = 43189, upload-time = "2026-01-01T23:51:26.799Z" }, + { url = "https://files.pythonhosted.org/packages/61/8d/80244b267b585e7aa79ffdac19f66c4861effc3a24598e77909ecdd0850e/librt-0.7.7-cp312-cp312-win_amd64.whl", hash = "sha256:4353ee891a1834567e0302d4bd5e60f531912179578c36f3d0430f8c5e16b456", size = 49462, upload-time = "2026-01-01T23:51:27.813Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1f/75db802d6a4992d95e8a889682601af9b49d5a13bbfa246d414eede1b56c/librt-0.7.7-cp312-cp312-win_arm64.whl", hash = "sha256:a76f1d679beccccdf8c1958e732a1dfcd6e749f8821ee59d7bec009ac308c029", size = 42828, upload-time = "2026-01-01T23:51:28.804Z" }, + { url = "https://files.pythonhosted.org/packages/8d/5e/d979ccb0a81407ec47c14ea68fb217ff4315521730033e1dd9faa4f3e2c1/librt-0.7.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f4a0b0a3c86ba9193a8e23bb18f100d647bf192390ae195d84dfa0a10fb6244", size = 55746, upload-time = "2026-01-01T23:51:29.828Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/3b65861fb32f802c3783d6ac66fc5589564d07452a47a8cf9980d531cad3/librt-0.7.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5335890fea9f9e6c4fdf8683061b9ccdcbe47c6dc03ab8e9b68c10acf78be78d", size = 57174, upload-time = "2026-01-01T23:51:31.226Z" }, + { url = "https://files.pythonhosted.org/packages/50/df/030b50614b29e443607220097ebaf438531ea218c7a9a3e21ea862a919cd/librt-0.7.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b4346b1225be26def3ccc6c965751c74868f0578cbcba293c8ae9168483d811", size = 165834, upload-time = "2026-01-01T23:51:32.278Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e1/bd8d1eacacb24be26a47f157719553bbd1b3fe812c30dddf121c0436fd0b/librt-0.7.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a10b8eebdaca6e9fdbaf88b5aefc0e324b763a5f40b1266532590d5afb268a4c", size = 174819, upload-time = "2026-01-01T23:51:33.461Z" }, + { url = "https://files.pythonhosted.org/packages/46/7d/91d6c3372acf54a019c1ad8da4c9ecf4fc27d039708880bf95f48dbe426a/librt-0.7.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:067be973d90d9e319e6eb4ee2a9b9307f0ecd648b8a9002fa237289a4a07a9e7", size = 189607, upload-time = "2026-01-01T23:51:34.604Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ac/44604d6d3886f791fbd1c6ae12d5a782a8f4aca927484731979f5e92c200/librt-0.7.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:23d2299ed007812cccc1ecef018db7d922733382561230de1f3954db28433977", size = 184586, upload-time = "2026-01-01T23:51:35.845Z" }, + { url = "https://files.pythonhosted.org/packages/5c/26/d8a6e4c17117b7f9b83301319d9a9de862ae56b133efb4bad8b3aa0808c9/librt-0.7.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6b6f8ea465524aa4c7420c7cc4ca7d46fe00981de8debc67b1cc2e9957bb5b9d", size = 178251, upload-time = "2026-01-01T23:51:37.018Z" }, + { url = "https://files.pythonhosted.org/packages/99/ab/98d857e254376f8e2f668e807daccc1f445e4b4fc2f6f9c1cc08866b0227/librt-0.7.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8df32a99cc46eb0ee90afd9ada113ae2cafe7e8d673686cf03ec53e49635439", size = 199853, upload-time = "2026-01-01T23:51:38.195Z" }, + { url = "https://files.pythonhosted.org/packages/7c/55/4523210d6ae5134a5da959900be43ad8bab2e4206687b6620befddb5b5fd/librt-0.7.7-cp313-cp313-win32.whl", hash = "sha256:86f86b3b785487c7760247bcdac0b11aa8bf13245a13ed05206286135877564b", size = 43247, upload-time = "2026-01-01T23:51:39.629Z" }, + { url = "https://files.pythonhosted.org/packages/25/40/3ec0fed5e8e9297b1cf1a3836fb589d3de55f9930e3aba988d379e8ef67c/librt-0.7.7-cp313-cp313-win_amd64.whl", hash = "sha256:4862cb2c702b1f905c0503b72d9d4daf65a7fdf5a9e84560e563471e57a56949", size = 49419, upload-time = "2026-01-01T23:51:40.674Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7a/aab5f0fb122822e2acbc776addf8b9abfb4944a9056c00c393e46e543177/librt-0.7.7-cp313-cp313-win_arm64.whl", hash = "sha256:0996c83b1cb43c00e8c87835a284f9057bc647abd42b5871e5f941d30010c832", size = 42828, upload-time = "2026-01-01T23:51:41.731Z" }, + { url = "https://files.pythonhosted.org/packages/69/9c/228a5c1224bd23809a635490a162e9cbdc68d99f0eeb4a696f07886b8206/librt-0.7.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:23daa1ab0512bafdd677eb1bfc9611d8ffbe2e328895671e64cb34166bc1b8c8", size = 55188, upload-time = "2026-01-01T23:51:43.14Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c2/0e7c6067e2b32a156308205e5728f4ed6478c501947e9142f525afbc6bd2/librt-0.7.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:558a9e5a6f3cc1e20b3168fb1dc802d0d8fa40731f6e9932dcc52bbcfbd37111", size = 56895, upload-time = "2026-01-01T23:51:44.534Z" }, + { url = "https://files.pythonhosted.org/packages/0e/77/de50ff70c80855eb79d1d74035ef06f664dd073fb7fb9d9fb4429651b8eb/librt-0.7.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2567cb48dc03e5b246927ab35cbb343376e24501260a9b5e30b8e255dca0d1d2", size = 163724, upload-time = "2026-01-01T23:51:45.571Z" }, + { url = "https://files.pythonhosted.org/packages/6e/19/f8e4bf537899bdef9e0bb9f0e4b18912c2d0f858ad02091b6019864c9a6d/librt-0.7.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6066c638cdf85ff92fc6f932d2d73c93a0e03492cdfa8778e6d58c489a3d7259", size = 172470, upload-time = "2026-01-01T23:51:46.823Z" }, + { url = "https://files.pythonhosted.org/packages/42/4c/dcc575b69d99076768e8dd6141d9aecd4234cba7f0e09217937f52edb6ed/librt-0.7.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a609849aca463074c17de9cda173c276eb8fee9e441053529e7b9e249dc8b8ee", size = 186806, upload-time = "2026-01-01T23:51:48.009Z" }, + { url = "https://files.pythonhosted.org/packages/fe/f8/4094a2b7816c88de81239a83ede6e87f1138477d7ee956c30f136009eb29/librt-0.7.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:add4e0a000858fe9bb39ed55f31085506a5c38363e6eb4a1e5943a10c2bfc3d1", size = 181809, upload-time = "2026-01-01T23:51:49.35Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ac/821b7c0ab1b5a6cd9aee7ace8309c91545a2607185101827f79122219a7e/librt-0.7.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a3bfe73a32bd0bdb9a87d586b05a23c0a1729205d79df66dee65bb2e40d671ba", size = 175597, upload-time = "2026-01-01T23:51:50.636Z" }, + { url = "https://files.pythonhosted.org/packages/71/f9/27f6bfbcc764805864c04211c6ed636fe1d58f57a7b68d1f4ae5ed74e0e0/librt-0.7.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0ecce0544d3db91a40f8b57ae26928c02130a997b540f908cefd4d279d6c5848", size = 196506, upload-time = "2026-01-01T23:51:52.535Z" }, + { url = "https://files.pythonhosted.org/packages/46/ba/c9b9c6fc931dd7ea856c573174ccaf48714905b1a7499904db2552e3bbaf/librt-0.7.7-cp314-cp314-win32.whl", hash = "sha256:8f7a74cf3a80f0c3b0ec75b0c650b2f0a894a2cec57ef75f6f72c1e82cdac61d", size = 39747, upload-time = "2026-01-01T23:51:53.683Z" }, + { url = "https://files.pythonhosted.org/packages/c5/69/cd1269337c4cde3ee70176ee611ab0058aa42fc8ce5c9dce55f48facfcd8/librt-0.7.7-cp314-cp314-win_amd64.whl", hash = "sha256:3d1fe2e8df3268dd6734dba33ededae72ad5c3a859b9577bc00b715759c5aaab", size = 45971, upload-time = "2026-01-01T23:51:54.697Z" }, + { url = "https://files.pythonhosted.org/packages/79/fd/e0844794423f5583108c5991313c15e2b400995f44f6ec6871f8aaf8243c/librt-0.7.7-cp314-cp314-win_arm64.whl", hash = "sha256:2987cf827011907d3dfd109f1be0d61e173d68b1270107bb0e89f2fca7f2ed6b", size = 39075, upload-time = "2026-01-01T23:51:55.726Z" }, + { url = "https://files.pythonhosted.org/packages/42/02/211fd8f7c381e7b2a11d0fdfcd410f409e89967be2e705983f7c6342209a/librt-0.7.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8e92c8de62b40bfce91d5e12c6e8b15434da268979b1af1a6589463549d491e6", size = 57368, upload-time = "2026-01-01T23:51:56.706Z" }, + { url = "https://files.pythonhosted.org/packages/4c/b6/aca257affae73ece26041ae76032153266d110453173f67d7603058e708c/librt-0.7.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f683dcd49e2494a7535e30f779aa1ad6e3732a019d80abe1309ea91ccd3230e3", size = 59238, upload-time = "2026-01-01T23:51:58.066Z" }, + { url = "https://files.pythonhosted.org/packages/96/47/7383a507d8e0c11c78ca34c9d36eab9000db5989d446a2f05dc40e76c64f/librt-0.7.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b15e5d17812d4d629ff576699954f74e2cc24a02a4fc401882dd94f81daba45", size = 183870, upload-time = "2026-01-01T23:51:59.204Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b8/50f3d8eec8efdaf79443963624175c92cec0ba84827a66b7fcfa78598e51/librt-0.7.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c084841b879c4d9b9fa34e5d5263994f21aea7fd9c6add29194dbb41a6210536", size = 194608, upload-time = "2026-01-01T23:52:00.419Z" }, + { url = "https://files.pythonhosted.org/packages/23/d9/1b6520793aadb59d891e3b98ee057a75de7f737e4a8b4b37fdbecb10d60f/librt-0.7.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c8fb9966f84737115513fecbaf257f9553d067a7dd45a69c2c7e5339e6a8dc", size = 206776, upload-time = "2026-01-01T23:52:01.705Z" }, + { url = "https://files.pythonhosted.org/packages/ff/db/331edc3bba929d2756fa335bfcf736f36eff4efcb4f2600b545a35c2ae58/librt-0.7.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5fb1ecb2c35362eab2dbd354fd1efa5a8440d3e73a68be11921042a0edc0ff", size = 203206, upload-time = "2026-01-01T23:52:03.315Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e1/6af79ec77204e85f6f2294fc171a30a91bb0e35d78493532ed680f5d98be/librt-0.7.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d1454899909d63cc9199a89fcc4f81bdd9004aef577d4ffc022e600c412d57f3", size = 196697, upload-time = "2026-01-01T23:52:04.857Z" }, + { url = "https://files.pythonhosted.org/packages/f3/46/de55ecce4b2796d6d243295c221082ca3a944dc2fb3a52dcc8660ce7727d/librt-0.7.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7ef28f2e7a016b29792fe0a2dd04dec75725b32a1264e390c366103f834a9c3a", size = 217193, upload-time = "2026-01-01T23:52:06.159Z" }, + { url = "https://files.pythonhosted.org/packages/41/61/33063e271949787a2f8dd33c5260357e3d512a114fc82ca7890b65a76e2d/librt-0.7.7-cp314-cp314t-win32.whl", hash = "sha256:5e419e0db70991b6ba037b70c1d5bbe92b20ddf82f31ad01d77a347ed9781398", size = 40277, upload-time = "2026-01-01T23:52:07.625Z" }, + { url = "https://files.pythonhosted.org/packages/06/21/1abd972349f83a696ea73159ac964e63e2d14086fdd9bc7ca878c25fced4/librt-0.7.7-cp314-cp314t-win_amd64.whl", hash = "sha256:d6b7d93657332c817b8d674ef6bf1ab7796b4f7ce05e420fd45bd258a72ac804", size = 46765, upload-time = "2026-01-01T23:52:08.647Z" }, + { url = "https://files.pythonhosted.org/packages/51/0e/b756c7708143a63fca65a51ca07990fa647db2cc8fcd65177b9e96680255/librt-0.7.7-cp314-cp314t-win_arm64.whl", hash = "sha256:142c2cd91794b79fd0ce113bd658993b7ede0fe93057668c2f98a45ca00b7e91", size = 39724, upload-time = "2026-01-01T23:52:09.745Z" }, + { url = "https://files.pythonhosted.org/packages/e2/34/b88347b7bac496c1433e2f9bf124b0024733654b1bb4bcbf6ccf24d83e2e/librt-0.7.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c8ffe3431d98cc043a14e88b21288b5ec7ee12cb01260e94385887f285ef9389", size = 54841, upload-time = "2026-01-01T23:52:10.751Z" }, + { url = "https://files.pythonhosted.org/packages/01/fc/394ef13f4a9a407e43e76a8b0002042f53e22401014ee19544bab99ba2c9/librt-0.7.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e40d20ae1722d6b8ea6acf4597e789604649dcd9c295eb7361a28225bc2e9e12", size = 56804, upload-time = "2026-01-01T23:52:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/88/53/0d49f17dd11495f0274d34318bd5d1c1aa183ce97c45a2dce8fda9b650af/librt-0.7.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f2cb63c49bc96847c3bb8dca350970e4dcd19936f391cfdfd057dcb37c4fa97e", size = 159682, upload-time = "2026-01-01T23:52:13.34Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/cce20900af63bbc22abacb197622287cf210cfdf2da352131fa48c3e490e/librt-0.7.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f2f8dcf5ab9f80fb970c6fd780b398efb2f50c1962485eb8d3ab07788595a48", size = 168512, upload-time = "2026-01-01T23:52:14.52Z" }, + { url = "https://files.pythonhosted.org/packages/18/aa/4d5e0e98b47998297ec58e14561346f38bc4ad2d7c4d100e0a3baead06e8/librt-0.7.7-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1f5cc41a570269d1be7a676655875e3a53de4992a9fa38efb7983e97cf73d7c", size = 182231, upload-time = "2026-01-01T23:52:15.656Z" }, + { url = "https://files.pythonhosted.org/packages/d7/76/6dbde6632fd959f4ffb1b9a6ee67ae096adce6222282c7b9cd131787ea16/librt-0.7.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ff1fb2dfef035549565a4124998fadcb7a3d4957131ddf004a56edeb029626b3", size = 178268, upload-time = "2026-01-01T23:52:16.851Z" }, + { url = "https://files.pythonhosted.org/packages/83/7d/a3ce1a98fa5a79c87e8d24a6595ba5beff40f500051d933f771975b81df9/librt-0.7.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ab2a2a9cd7d044e1a11ca64a86ad3361d318176924bbe5152fbc69f99be20b8c", size = 172569, upload-time = "2026-01-01T23:52:18.033Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a7/01f6cbc77b0ccb22d9ad939ddcd1529a521d3e79c5b1eb3ed5b2c158e8dd/librt-0.7.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ad3fc2d859a709baf9dd9607bb72f599b1cfb8a39eafd41307d0c3c4766763cb", size = 192746, upload-time = "2026-01-01T23:52:19.235Z" }, + { url = "https://files.pythonhosted.org/packages/5b/83/9da96065a4f5a44eb1b7e6611c729544b84bb5dd6806acbf0c82ba3e3c27/librt-0.7.7-cp39-cp39-win32.whl", hash = "sha256:f83c971eb9d2358b6a18da51dc0ae00556ac7c73104dde16e9e14c15aaf685ca", size = 42550, upload-time = "2026-01-01T23:52:20.392Z" }, + { url = "https://files.pythonhosted.org/packages/34/b3/85aef151a052a40521f5b54005908a22c437dd4c952800d5e5efce99a47d/librt-0.7.7-cp39-cp39-win_amd64.whl", hash = "sha256:264720fc288c86039c091a4ad63419a5d7cabbf1c1c9933336a957ed2483e570", size = 48957, upload-time = "2026-01-01T23:52:21.43Z" }, +] + +[[package]] +name = "markdown" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "importlib-metadata", version = "6.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/2a/62841f4fb1fef5fa015ded48d02401cd95643ca03b6760b29437b62a04a4/Markdown-3.4.4.tar.gz", hash = "sha256:225c6123522495d4119a90b3a3ba31a1e87a70369e03f14799ea9c0d7183a3d6", size = 324459, upload-time = "2023-07-25T15:13:45.311Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/b5/228c1cdcfe138f1a8e01ab1b54284c8b83735476cb22b6ba251656ed13ad/Markdown-3.4.4-py3-none-any.whl", hash = "sha256:a4c1b65c0957b4bd9e7d86ddc7b3c9868fb9670660f6f99f6d1bca8954d5a941", size = 94174, upload-time = "2023-07-25T15:13:43.124Z" }, +] + +[[package]] +name = "markdown" +version = "3.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +dependencies = [ + { name = "importlib-metadata", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, +] +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" }, +] + +[[package]] +name = "markdown" +version = "3.9" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "importlib-metadata", version = "8.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/37/02347f6d6d8279247a5837082ebc26fc0d5aaeaf75aa013fcbb433c777ab/markdown-3.9.tar.gz", hash = "sha256:d2900fe1782bd33bdbbd56859defef70c2e78fc46668f8eb9df3128138f2cb6a", size = 364585, upload-time = "2025-09-04T20:25:22.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/ae/44c4a6a4cbb496d93c6257954260fe3a6e91b7bed2240e5dad2a717f5111/markdown-3.9-py3-none-any.whl", hash = "sha256:9f4d91ed810864ea88a6f32c07ba8bee1346c0cc1f6b1f9f6c822f2a9667d280", size = 107441, upload-time = "2025-09-04T20:25:21.784Z" }, +] + +[[package]] +name = "markdown" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/7dd27d9d863b3376fcf23a5a13cb5d024aed1db46f963f1b5735ae43b3be/markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e", size = 364931, upload-time = "2025-11-03T19:51:15.007Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c", size = 107678, upload-time = "2025-11-03T19:51:13.887Z" }, +] + +[[package]] +name = "markdown-include" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown", version = "3.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "markdown", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "markdown", version = "3.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/d8/66bf162fe6c1adb619f94a6da599323eecacf15b6d57469d0fd0421c10df/markdown-include-0.8.1.tar.gz", hash = "sha256:1d0623e0fc2757c38d35df53752768356162284259d259c486b4ab6285cdbbe3", size = 21873, upload-time = "2023-02-07T09:47:26.608Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/e2/c4d20b21a05fe0fee571649cebc05f7f72e80b1a743f932e7326125e6c9e/markdown_include-0.8.1-py3-none-any.whl", hash = "sha256:32f0635b9cfef46997b307e2430022852529f7a5b87c0075c504283e7cc7db53", size = 18837, upload-time = "2023-02-07T09:47:25.03Z" }, +] + +[[package]] +name = "markupsafe" +version = "2.1.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", + "python_full_version < '3.8'", +] +sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384, upload-time = "2024-02-02T16:31:22.863Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/54/ad5eb37bf9d51800010a74e4665425831a9db4e7c4e0fde4352e391e808e/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", size = 18206, upload-time = "2024-02-02T16:30:04.105Z" }, + { url = "https://files.pythonhosted.org/packages/6a/4a/a4d49415e600bacae038c67f9fecc1d5433b9d3c71a4de6f33537b89654c/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", size = 14079, upload-time = "2024-02-02T16:30:06.5Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7b/85681ae3c33c385b10ac0f8dd025c30af83c78cec1c37a6aa3b55e67f5ec/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", size = 26620, upload-time = "2024-02-02T16:30:08.31Z" }, + { url = "https://files.pythonhosted.org/packages/7c/52/2b1b570f6b8b803cef5ac28fdf78c0da318916c7d2fe9402a84d591b394c/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", size = 25818, upload-time = "2024-02-02T16:30:09.577Z" }, + { url = "https://files.pythonhosted.org/packages/29/fe/a36ba8c7ca55621620b2d7c585313efd10729e63ef81e4e61f52330da781/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", size = 25493, upload-time = "2024-02-02T16:30:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/60/ae/9c60231cdfda003434e8bd27282b1f4e197ad5a710c14bee8bea8a9ca4f0/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", size = 30630, upload-time = "2024-02-02T16:30:13.144Z" }, + { url = "https://files.pythonhosted.org/packages/65/dc/1510be4d179869f5dafe071aecb3f1f41b45d37c02329dfba01ff59e5ac5/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", size = 29745, upload-time = "2024-02-02T16:30:14.222Z" }, + { url = "https://files.pythonhosted.org/packages/30/39/8d845dd7d0b0613d86e0ef89549bfb5f61ed781f59af45fc96496e897f3a/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", size = 30021, upload-time = "2024-02-02T16:30:16.032Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5c/356a6f62e4f3c5fbf2602b4771376af22a3b16efa74eb8716fb4e328e01e/MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", size = 16659, upload-time = "2024-02-02T16:30:17.079Z" }, + { url = "https://files.pythonhosted.org/packages/69/48/acbf292615c65f0604a0c6fc402ce6d8c991276e16c80c46a8f758fbd30c/MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", size = 17213, upload-time = "2024-02-02T16:30:18.251Z" }, + { url = "https://files.pythonhosted.org/packages/11/e7/291e55127bb2ae67c64d66cef01432b5933859dfb7d6949daa721b89d0b3/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", size = 18219, upload-time = "2024-02-02T16:30:19.988Z" }, + { url = "https://files.pythonhosted.org/packages/6b/cb/aed7a284c00dfa7c0682d14df85ad4955a350a21d2e3b06d8240497359bf/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", size = 14098, upload-time = "2024-02-02T16:30:21.063Z" }, + { url = "https://files.pythonhosted.org/packages/1c/cf/35fe557e53709e93feb65575c93927942087e9b97213eabc3fe9d5b25a55/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", size = 29014, upload-time = "2024-02-02T16:30:22.926Z" }, + { url = "https://files.pythonhosted.org/packages/97/18/c30da5e7a0e7f4603abfc6780574131221d9148f323752c2755d48abad30/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", size = 28220, upload-time = "2024-02-02T16:30:24.76Z" }, + { url = "https://files.pythonhosted.org/packages/0c/40/2e73e7d532d030b1e41180807a80d564eda53babaf04d65e15c1cf897e40/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", size = 27756, upload-time = "2024-02-02T16:30:25.877Z" }, + { url = "https://files.pythonhosted.org/packages/18/46/5dca760547e8c59c5311b332f70605d24c99d1303dd9a6e1fc3ed0d73561/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", size = 33988, upload-time = "2024-02-02T16:30:26.935Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c5/27febe918ac36397919cd4a67d5579cbbfa8da027fa1238af6285bb368ea/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", size = 32718, upload-time = "2024-02-02T16:30:28.111Z" }, + { url = "https://files.pythonhosted.org/packages/f8/81/56e567126a2c2bc2684d6391332e357589a96a76cb9f8e5052d85cb0ead8/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", size = 33317, upload-time = "2024-02-02T16:30:29.214Z" }, + { url = "https://files.pythonhosted.org/packages/00/0b/23f4b2470accb53285c613a3ab9ec19dc944eaf53592cb6d9e2af8aa24cc/MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", size = 16670, upload-time = "2024-02-02T16:30:30.915Z" }, + { url = "https://files.pythonhosted.org/packages/b7/a2/c78a06a9ec6d04b3445a949615c4c7ed86a0b2eb68e44e7541b9d57067cc/MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", size = 17224, upload-time = "2024-02-02T16:30:32.09Z" }, + { url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215, upload-time = "2024-02-02T16:30:33.081Z" }, + { url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069, upload-time = "2024-02-02T16:30:34.148Z" }, + { url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452, upload-time = "2024-02-02T16:30:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462, upload-time = "2024-02-02T16:30:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869, upload-time = "2024-02-02T16:30:37.834Z" }, + { url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906, upload-time = "2024-02-02T16:30:39.366Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296, upload-time = "2024-02-02T16:30:40.413Z" }, + { url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038, upload-time = "2024-02-02T16:30:42.243Z" }, + { url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572, upload-time = "2024-02-02T16:30:43.326Z" }, + { url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127, upload-time = "2024-02-02T16:30:44.418Z" }, + { url = "https://files.pythonhosted.org/packages/a7/88/a940e11827ea1c136a34eca862486178294ae841164475b9ab216b80eb8e/MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f", size = 13982, upload-time = "2024-02-02T16:30:46.06Z" }, + { url = "https://files.pythonhosted.org/packages/cb/06/0d28bd178db529c5ac762a625c335a9168a7a23f280b4db9c95e97046145/MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf", size = 26335, upload-time = "2024-02-02T16:30:47.676Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1d/c4f5016f87ced614eacc7d5fb85b25bcc0ff53e8f058d069fc8cbfdc3c7a/MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a", size = 25557, upload-time = "2024-02-02T16:30:48.936Z" }, + { url = "https://files.pythonhosted.org/packages/b3/fb/c18b8c9fbe69e347fdbf782c6478f1bc77f19a830588daa224236678339b/MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52", size = 25245, upload-time = "2024-02-02T16:30:50.711Z" }, + { url = "https://files.pythonhosted.org/packages/2f/69/30d29adcf9d1d931c75001dd85001adad7374381c9c2086154d9f6445be6/MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9", size = 31013, upload-time = "2024-02-02T16:30:51.795Z" }, + { url = "https://files.pythonhosted.org/packages/3a/03/63498d05bd54278b6ca340099e5b52ffb9cdf2ee4f2d9b98246337e21689/MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df", size = 30178, upload-time = "2024-02-02T16:30:52.866Z" }, + { url = "https://files.pythonhosted.org/packages/68/79/11b4fe15124692f8673b603433e47abca199a08ecd2a4851bfbdc97dc62d/MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50", size = 30429, upload-time = "2024-02-02T16:30:53.983Z" }, + { url = "https://files.pythonhosted.org/packages/ed/88/408bdbf292eb86f03201c17489acafae8358ba4e120d92358308c15cea7c/MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371", size = 16633, upload-time = "2024-02-02T16:30:55.317Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4c/3577a52eea1880538c435176bc85e5b3379b7ab442327ccd82118550758f/MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2", size = 17215, upload-time = "2024-02-02T16:30:56.6Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ff/2c942a82c35a49df5de3a630ce0a8456ac2969691b230e530ac12314364c/MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", size = 18192, upload-time = "2024-02-02T16:30:57.715Z" }, + { url = "https://files.pythonhosted.org/packages/4f/14/6f294b9c4f969d0c801a4615e221c1e084722ea6114ab2114189c5b8cbe0/MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", size = 14072, upload-time = "2024-02-02T16:30:58.844Z" }, + { url = "https://files.pythonhosted.org/packages/81/d4/fd74714ed30a1dedd0b82427c02fa4deec64f173831ec716da11c51a50aa/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", size = 26928, upload-time = "2024-02-02T16:30:59.922Z" }, + { url = "https://files.pythonhosted.org/packages/c7/bd/50319665ce81bb10e90d1cf76f9e1aa269ea6f7fa30ab4521f14d122a3df/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", size = 26106, upload-time = "2024-02-02T16:31:01.582Z" }, + { url = "https://files.pythonhosted.org/packages/4c/6f/f2b0f675635b05f6afd5ea03c094557bdb8622fa8e673387444fe8d8e787/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68", size = 25781, upload-time = "2024-02-02T16:31:02.71Z" }, + { url = "https://files.pythonhosted.org/packages/51/e0/393467cf899b34a9d3678e78961c2c8cdf49fb902a959ba54ece01273fb1/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", size = 30518, upload-time = "2024-02-02T16:31:04.392Z" }, + { url = "https://files.pythonhosted.org/packages/f6/02/5437e2ad33047290dafced9df741d9efc3e716b75583bbd73a9984f1b6f7/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", size = 29669, upload-time = "2024-02-02T16:31:05.53Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7d/968284145ffd9d726183ed6237c77938c021abacde4e073020f920e060b2/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", size = 29933, upload-time = "2024-02-02T16:31:06.636Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f3/ecb00fc8ab02b7beae8699f34db9357ae49d9f21d4d3de6f305f34fa949e/MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", size = 16656, upload-time = "2024-02-02T16:31:07.767Z" }, + { url = "https://files.pythonhosted.org/packages/92/21/357205f03514a49b293e214ac39de01fadd0970a6e05e4bf1ddd0ffd0881/MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", size = 17206, upload-time = "2024-02-02T16:31:08.843Z" }, + { url = "https://files.pythonhosted.org/packages/0f/31/780bb297db036ba7b7bbede5e1d7f1e14d704ad4beb3ce53fb495d22bc62/MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", size = 18193, upload-time = "2024-02-02T16:31:10.155Z" }, + { url = "https://files.pythonhosted.org/packages/6c/77/d77701bbef72892affe060cdacb7a2ed7fd68dae3b477a8642f15ad3b132/MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", size = 14073, upload-time = "2024-02-02T16:31:11.442Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a7/1e558b4f78454c8a3a0199292d96159eb4d091f983bc35ef258314fe7269/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", size = 26486, upload-time = "2024-02-02T16:31:12.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5a/360da85076688755ea0cceb92472923086993e86b5613bbae9fbc14136b0/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", size = 25685, upload-time = "2024-02-02T16:31:13.726Z" }, + { url = "https://files.pythonhosted.org/packages/6a/18/ae5a258e3401f9b8312f92b028c54d7026a97ec3ab20bfaddbdfa7d8cce8/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", size = 25338, upload-time = "2024-02-02T16:31:14.812Z" }, + { url = "https://files.pythonhosted.org/packages/0b/cc/48206bd61c5b9d0129f4d75243b156929b04c94c09041321456fd06a876d/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", size = 30439, upload-time = "2024-02-02T16:31:15.946Z" }, + { url = "https://files.pythonhosted.org/packages/d1/06/a41c112ab9ffdeeb5f77bc3e331fdadf97fa65e52e44ba31880f4e7f983c/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", size = 29531, upload-time = "2024-02-02T16:31:17.13Z" }, + { url = "https://files.pythonhosted.org/packages/02/8c/ab9a463301a50dab04d5472e998acbd4080597abc048166ded5c7aa768c8/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", size = 29823, upload-time = "2024-02-02T16:31:18.247Z" }, + { url = "https://files.pythonhosted.org/packages/bc/29/9bc18da763496b055d8e98ce476c8e718dcfd78157e17f555ce6dd7d0895/MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", size = 16658, upload-time = "2024-02-02T16:31:19.583Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f8/4da07de16f10551ca1f640c92b5f316f9394088b183c6a57183df6de5ae4/MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", size = 17211, upload-time = "2024-02-02T16:31:20.96Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +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/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/56/23/0d8c13a44bde9154821586520840643467aee574d8ce79a17da539ee7fed/markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26", size = 11623, upload-time = "2025-09-27T18:37:29.296Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/07a2cb9a8045d5f3f0890a8c3bc0859d7a47bfd9a560b563899bec7b72ed/markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc", size = 12049, upload-time = "2025-09-27T18:37:30.234Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e4/6be85eb81503f8e11b61c0b6369b6e077dcf0a74adbd9ebf6b349937b4e9/markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c", size = 21923, upload-time = "2025-09-27T18:37:31.177Z" }, + { url = "https://files.pythonhosted.org/packages/6f/bc/4dc914ead3fe6ddaef035341fee0fc956949bbd27335b611829292b89ee2/markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42", size = 20543, upload-time = "2025-09-27T18:37:32.168Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/5fe81fbcfba4aef4093d5f856e5c774ec2057946052d18d168219b7bd9f9/markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b", size = 20585, upload-time = "2025-09-27T18:37:33.166Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f6/e0e5a3d3ae9c4020f696cd055f940ef86b64fe88de26f3a0308b9d3d048c/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758", size = 21387, upload-time = "2025-09-27T18:37:34.185Z" }, + { url = "https://files.pythonhosted.org/packages/c8/25/651753ef4dea08ea790f4fbb65146a9a44a014986996ca40102e237aa49a/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2", size = 20133, upload-time = "2025-09-27T18:37:35.138Z" }, + { url = "https://files.pythonhosted.org/packages/dc/0a/c3cf2b4fef5f0426e8a6d7fce3cb966a17817c568ce59d76b92a233fdbec/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d", size = 20588, upload-time = "2025-09-27T18:37:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/cd/1b/a7782984844bd519ad4ffdbebbba2671ec5d0ebbeac34736c15fb86399e8/markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7", size = 14566, upload-time = "2025-09-27T18:37:37.09Z" }, + { url = "https://files.pythonhosted.org/packages/18/1f/8d9c20e1c9440e215a44be5ab64359e207fcb4f675543f1cf9a2a7f648d0/markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e", size = 15053, upload-time = "2025-09-27T18:37:38.054Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d3/fe08482b5cd995033556d45041a4f4e76e7f0521112a9c9991d40d39825f/markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8", size = 13928, upload-time = "2025-09-27T18:37:39.037Z" }, +] + +[[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, 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, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "colorama", marker = "python_full_version < '3.8' and sys_platform == 'win32'" }, + { name = "ghp-import", marker = "python_full_version < '3.8'" }, + { name = "importlib-metadata", version = "6.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "jinja2", marker = "python_full_version < '3.8'" }, + { name = "markdown", version = "3.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "markupsafe", version = "2.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "mergedeep", marker = "python_full_version < '3.8'" }, + { name = "packaging", version = "24.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "pathspec", version = "0.11.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "platformdirs", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "pyyaml", version = "6.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "pyyaml-env-tag", version = "0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "typing-extensions", version = "4.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "watchdog", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/bb/24a22f8154cf79b07b45da070633613837d6e59c7d870076f693b7b1c556/mkdocs-1.5.3.tar.gz", hash = "sha256:eb7c99214dcb945313ba30426c2451b735992c73c2e10838f76d09e39ff4d0e2", size = 3654364, upload-time = "2023-09-18T21:26:11.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/58/aa3301b23966a71d7f8e55233f467b3cec94a651434e9cd9053811342539/mkdocs-1.5.3-py3-none-any.whl", hash = "sha256:3b3a78e736b31158d64dbb2f8ba29bd46a379d0c6e324c2246c3bc3d2189cfc1", size = 3694750, upload-time = "2023-09-18T21:26:09.089Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", +] +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8' and python_full_version < '3.10'" }, + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "colorama", marker = "python_full_version >= '3.8' and sys_platform == 'win32'" }, + { name = "ghp-import", marker = "python_full_version >= '3.8'" }, + { name = "importlib-metadata", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "importlib-metadata", version = "8.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "jinja2", marker = "python_full_version >= '3.8'" }, + { name = "markdown", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "markdown", version = "3.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "markupsafe", version = "2.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "markupsafe", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "mergedeep", marker = "python_full_version >= '3.8'" }, + { name = "mkdocs-get-deps", marker = "python_full_version >= '3.8'" }, + { name = "packaging", version = "25.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8'" }, + { name = "pathspec", version = "0.12.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "pathspec", version = "1.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pyyaml", version = "6.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8'" }, + { name = "pyyaml-env-tag", version = "0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "pyyaml-env-tag", version = "1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "watchdog", version = "4.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "watchdog", version = "6.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +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, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "markdown", version = "3.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "mkdocs", version = "1.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/3f/9531888bc92bafb1bffddca5d9240a7bae9a479d465528883b61808ef9d6/mkdocs-autorefs-0.4.1.tar.gz", hash = "sha256:70748a7bd025f9ecd6d6feeba8ba63f8e891a1af55f48e366d6d6e78493aba84", size = 13142, upload-time = "2022-03-07T16:43:49.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/5c/6594400290df38f99bf8d9ef249387b56f4ad962667836266f6fe7da8597/mkdocs_autorefs-0.4.1-py3-none-any.whl", hash = "sha256:a2248a9501b29dc0cc8ba4c09f4f47ff121945f6ce33d760f145d6f89d313f5b", size = 9802, upload-time = "2022-03-07T16:43:47.394Z" }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +dependencies = [ + { name = "markdown", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "markupsafe", version = "2.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "mkdocs", version = "1.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/ae/0f1154c614d6a8b8a36fff084e5b82af3a15f7d2060cf0dcdb1c53297a71/mkdocs_autorefs-1.2.0.tar.gz", hash = "sha256:a86b93abff653521bda71cf3fc5596342b7a23982093915cb74273f67522190f", size = 40262, upload-time = "2024-09-01T18:29:18.514Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/26/4d39d52ea2219604053a4d05b98e90d6a335511cc01806436ec4886b1028/mkdocs_autorefs-1.2.0-py3-none-any.whl", hash = "sha256:d588754ae89bd0ced0c70c06f58566a4ee43471eeeee5202427da7de9ef85a2f", size = 16522, upload-time = "2024-09-01T18:29:16.605Z" }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "markdown", version = "3.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "markupsafe", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "mkdocs", version = "1.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +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/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]] +name = "mkdocs-get-deps" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "importlib-metadata", version = "8.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "mergedeep", marker = "python_full_version >= '3.8'" }, + { name = "platformdirs", version = "4.3.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "platformdirs", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyyaml", version = "6.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8'" }, +] +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, upload-time = "2023-11-20T17:51:08.587Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.2.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "babel", version = "2.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "colorama", marker = "python_full_version < '3.8'" }, + { name = "jinja2", marker = "python_full_version < '3.8'" }, + { name = "markdown", version = "3.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "mkdocs", version = "1.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "mkdocs-material-extensions", version = "1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "paginate", marker = "python_full_version < '3.8'" }, + { name = "pygments", version = "2.17.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "pymdown-extensions", version = "10.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "regex", marker = "python_full_version < '3.8'" }, + { name = "requests", version = "2.31.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/20/d63a01b9890184e7bd7fffed915a0636f0682c74900b1b238bc216556049/mkdocs_material-9.2.7.tar.gz", hash = "sha256:b44da35b0d98cd762d09ef74f1ddce5b6d6e35c13f13beb0c9d82a629e5f229e", size = 3788625, upload-time = "2023-09-02T17:07:21.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/bd/f5d39a0c52865dbf03503d177dd05368a08d79e6c746331b5d685899ee63/mkdocs_material-9.2.7-py3-none-any.whl", hash = "sha256:92e4160d191cc76121fed14ab9f14638e43a6da0f2e9d7a9194d377f0a4e7f18", size = 8016474, upload-time = "2023-09-02T17:07:17.993Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.7.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", +] +dependencies = [ + { name = "babel", version = "2.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8'" }, + { name = "backrefs", version = "5.7.post1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "backrefs", version = "6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "colorama", marker = "python_full_version >= '3.8'" }, + { name = "jinja2", marker = "python_full_version >= '3.8'" }, + { name = "markdown", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "markdown", version = "3.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mkdocs", version = "1.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8'" }, + { name = "mkdocs-material-extensions", version = "1.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8'" }, + { name = "paginate", marker = "python_full_version >= '3.8'" }, + { name = "pygments", version = "2.19.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8'" }, + { name = "pymdown-extensions", version = "10.15", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "pymdown-extensions", version = "10.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "requests", version = "2.32.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/e2/2ffc356cd72f1473d07c7719d82a8f2cbd261666828614ecb95b12169f41/mkdocs_material-9.7.1.tar.gz", hash = "sha256:89601b8f2c3e6c6ee0a918cc3566cb201d40bf37c3cd3c2067e26fadb8cce2b8", size = 4094392, upload-time = "2025-12-18T09:49:00.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/32/ed071cb721aca8c227718cffcf7bd539620e9799bbf2619e90c757bfd030/mkdocs_material-9.7.1-py3-none-any.whl", hash = "sha256:3f6100937d7d731f87f1e3e3b021c97f7239666b9ba1151ab476cabb96c60d5c", size = 9297166, upload-time = "2025-12-18T09:48:56.664Z" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://files.pythonhosted.org/packages/05/3b/ceb3b4fc3810184e6d21dbe909a289884d5d183f1830fd44bcbce8027c66/mkdocs_material_extensions-1.2.tar.gz", hash = "sha256:27e2d1ed2d031426a6e10d5ea06989d67e90bb02acd588bc5673106b5ee5eedf", size = 11105, upload-time = "2023-09-20T15:44:35.591Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/db/6ada1f1cfd32808507c901ca4616f8c0907113c7a7c1eca7b03c89bb0fcf/mkdocs_material_extensions-1.2-py3-none-any.whl", hash = "sha256:c767bd6d6305f6420a50f0b541b0c9966d52068839af97029be14443849fb8a1", size = 7987, upload-time = "2023-09-20T15:44:33.972Z" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", +] +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, upload-time = "2023-11-22T19:09:43.465Z" }, +] + +[[package]] +name = "mkdocstrings" +version = "0.22.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "importlib-metadata", version = "6.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "jinja2", marker = "python_full_version < '3.8'" }, + { name = "markdown", version = "3.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "markupsafe", version = "2.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "mkdocs", version = "1.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "mkdocs-autorefs", version = "0.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "pymdown-extensions", version = "10.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "typing-extensions", version = "4.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/89/39b7da1cd3d7bc9d3626a2030349443276bd4c8428b676b010ffb96ec9be/mkdocstrings-0.22.0.tar.gz", hash = "sha256:82a33b94150ebb3d4b5c73bab4598c3e21468c79ec072eff6931c8f3bfc38256", size = 30419, upload-time = "2023-05-26T10:45:12.878Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/26/5816407b5dd51821a3d23f53bdbd013ab1878b6246e520dc014d200ee1d2/mkdocstrings-0.22.0-py3-none-any.whl", hash = "sha256:2d4095d461554ff6a778fdabdca3c00c468c2f1459d469f7a7f622a2b23212ba", size = 26747, upload-time = "2023-05-26T10:45:10.475Z" }, +] + +[[package]] +name = "mkdocstrings" +version = "0.26.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "importlib-metadata", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "jinja2", marker = "python_full_version == '3.8.*'" }, + { name = "markdown", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "markupsafe", version = "2.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "mkdocs", version = "1.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "mkdocs-autorefs", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "platformdirs", version = "4.3.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "pymdown-extensions", version = "10.15", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e6/bf/170ff04de72227f715d67da32950c7b8434449f3805b2ec3dd1085db4d7c/mkdocstrings-0.26.1.tar.gz", hash = "sha256:bb8b8854d6713d5348ad05b069a09f3b79edbc6a0f33a34c6821141adb03fe33", size = 92677, upload-time = "2024-09-06T10:26:06.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/cc/8ba127aaee5d1e9046b0d33fa5b3d17da95a9d705d44902792e0569257fd/mkdocstrings-0.26.1-py3-none-any.whl", hash = "sha256:29738bfb72b4608e8e55cc50fb8a54f325dc7ebd2014e4e3881a49892d5983cf", size = 29643, upload-time = "2024-09-06T10:26:04.498Z" }, +] + +[[package]] +name = "mkdocstrings" +version = "0.30.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "importlib-metadata", version = "8.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "jinja2", marker = "python_full_version == '3.9.*'" }, + { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "markupsafe", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "mkdocs", version = "1.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "mkdocs-autorefs", version = "1.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "pymdown-extensions", version = "10.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/33/2fa3243439f794e685d3e694590d28469a9b8ea733af4b48c250a3ffc9a0/mkdocstrings-0.30.1.tar.gz", hash = "sha256:84a007aae9b707fb0aebfc9da23db4b26fc9ab562eb56e335e9ec480cb19744f", size = 106350, upload-time = "2025-09-19T10:49:26.446Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/2c/f0dc4e1ee7f618f5bff7e05898d20bf8b6e7fa612038f768bfa295f136a4/mkdocstrings-0.30.1-py3-none-any.whl", hash = "sha256:41bd71f284ca4d44a668816193e4025c950b002252081e387433656ae9a70a82", size = 36704, upload-time = "2025-09-19T10:49:24.805Z" }, +] + +[[package]] +name = "mkdocstrings" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "jinja2", marker = "python_full_version >= '3.10'" }, + { name = "markdown", version = "3.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "markupsafe", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mkdocs", version = "1.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mkdocs-autorefs", version = "1.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pymdown-extensions", version = "10.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e5/13/10bbf9d56565fd91b91e6f5a8cd9b9d8a2b101c4e8ad6eeafa35a706301d/mkdocstrings-1.0.0.tar.gz", hash = "sha256:351a006dbb27aefce241ade110d3cd040c1145b7a3eb5fd5ac23f03ed67f401a", size = 101086, upload-time = "2025-11-27T15:39:40.534Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/fc/80aa31b79133634721cf7855d37b76ea49773599214896f2ff10be03de2a/mkdocstrings-1.0.0-py3-none-any.whl", hash = "sha256:4c50eb960bff6e05dfc631f6bc00dfabffbcb29c5ff25f676d64daae05ed82fa", size = 35135, upload-time = "2025-11-27T15:39:39.301Z" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "1.1.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "griffe", version = "0.30.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "mkdocstrings", version = "0.22.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/22/eaa1ccd8aaeac72953a780f4b6c82650475d241dd953582706db547db004/mkdocstrings_python-1.1.2.tar.gz", hash = "sha256:f28bdcacb9bcdb44b6942a5642c1ea8b36870614d33e29e3c923e204a8d8ed61", size = 24613, upload-time = "2023-06-04T16:19:16.877Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/81/8c1de2573c4fa5fff7fc20eba1dad2892f43ed22ba145b595243e375c274/mkdocstrings_python-1.1.2-py3-none-any.whl", hash = "sha256:c2b652a850fec8e85034a9cdb3b45f8ad1a558686edc20ed1f40b4e17e62070f", size = 40366, upload-time = "2023-06-04T16:19:14.831Z" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "1.11.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +dependencies = [ + { name = "griffe", version = "1.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "mkdocs-autorefs", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "mkdocstrings", version = "0.26.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/ba/534c934cd0a809f51c91332d6ed278782ee4126b8ba8db02c2003f162b47/mkdocstrings_python-1.11.1.tar.gz", hash = "sha256:8824b115c5359304ab0b5378a91f6202324a849e1da907a3485b59208b797322", size = 166890, upload-time = "2024-09-03T17:20:54.904Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/f2/2a2c48fda645ac6bbe73bcc974587a579092b6868e6ff8bc6d177f4db38a/mkdocstrings_python-1.11.1-py3-none-any.whl", hash = "sha256:a21a1c05acef129a618517bb5aae3e33114f569b11588b1e7af3e9d4061a71af", size = 109297, upload-time = "2024-09-03T17:20:52.621Z" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "1.18.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "griffe", version = "1.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "mkdocs-autorefs", version = "1.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "mkdocstrings", version = "0.30.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/ae/58ab2bfbee2792e92a98b97e872f7c003deb903071f75d8d83aa55db28fa/mkdocstrings_python-1.18.2.tar.gz", hash = "sha256:4ad536920a07b6336f50d4c6d5603316fafb1172c5c882370cbbc954770ad323", size = 207972, upload-time = "2025-08-28T16:11:19.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/8f/ce008599d9adebf33ed144e7736914385e8537f5fc686fdb7cceb8c22431/mkdocstrings_python-1.18.2-py3-none-any.whl", hash = "sha256:944fe6deb8f08f33fa936d538233c4036e9f53e840994f6146e8e94eb71b600d", size = 138215, upload-time = "2025-08-28T16:11:18.176Z" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "griffe", version = "1.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mkdocs-autorefs", version = "1.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mkdocstrings", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/24/75/d30af27a2906f00eb90143470272376d728521997800f5dce5b340ba35bc/mkdocstrings_python-2.0.1.tar.gz", hash = "sha256:843a562221e6a471fefdd4b45cc6c22d2607ccbad632879234fa9692e9cf7732", size = 199345, upload-time = "2025-12-03T14:26:11.755Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/06/c5f8deba7d2cbdfa7967a716ae801aa9ca5f734b8f54fd473ef77a088dbe/mkdocstrings_python-2.0.1-py3-none-any.whl", hash = "sha256:66ecff45c5f8b71bf174e11d49afc845c2dfc7fc0ab17a86b6b337e0f24d8d90", size = 105055, upload-time = "2025-12-03T14:26:10.184Z" }, +] + +[[package]] +name = "mypy" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "mypy-extensions", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "tomli", version = "2.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "typed-ast", marker = "python_full_version < '3.8'" }, + { name = "typing-extensions", version = "4.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/28/d8a8233ff167d06108e53b7aefb4a8d7350adbbf9d7abd980f17fdb7a3a6/mypy-1.4.1.tar.gz", hash = "sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b", size = 2855162, upload-time = "2023-06-25T23:22:54.364Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/3b/1c7363863b56c059f60a1dfdca9ac774a22ba64b7a4da0ee58ee53e5243f/mypy-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8", size = 10451043, upload-time = "2023-06-25T23:22:02.502Z" }, + { url = "https://files.pythonhosted.org/packages/a7/24/6f0df1874118839db1155fed62a4bd7e80c181367ff8ea07d40fbaffcfb4/mypy-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878", size = 9542079, upload-time = "2023-06-25T23:22:37.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5c/deeac94fcccd11aa621e6b350df333e1b809b11443774ea67582cc0205da/mypy-1.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd", size = 11974913, upload-time = "2023-06-25T23:21:14.603Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2f/de3c455c54e8cf5e37ea38705c1920f2df470389f8fc051084d2dd8c9c59/mypy-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8c4d8e89aa7de683e2056a581ce63c46a0c41e31bd2b6d34144e2c80f5ea53dc", size = 12044492, upload-time = "2023-06-25T23:22:17.551Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d3/6f65357dcb68109946de70cd55bd2e60f10114f387471302f48d54ff5dae/mypy-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:bfdca17c36ae01a21274a3c387a63aa1aafe72bff976522886869ef131b937f1", size = 8831655, upload-time = "2023-06-25T23:21:40.201Z" }, + { url = "https://files.pythonhosted.org/packages/94/01/e34e37a044325af4d4af9825c15e8a0d26d89b5a9624b4d0908449d3411b/mypy-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7549fbf655e5825d787bbc9ecf6028731973f78088fbca3a1f4145c39ef09462", size = 10338636, upload-time = "2023-06-25T23:22:43.45Z" }, + { url = "https://files.pythonhosted.org/packages/92/58/ccc0b714ecbd1a64b34d8ce1c38763ff6431de1d82551904ecc3711fbe05/mypy-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98324ec3ecf12296e6422939e54763faedbfcc502ea4a4c38502082711867258", size = 9444172, upload-time = "2023-06-25T23:21:25.502Z" }, + { url = "https://files.pythonhosted.org/packages/73/72/dfc0b46e6905eafd598e7c48c0c4f2e232647e4e36547425c64e6c850495/mypy-1.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141dedfdbfe8a04142881ff30ce6e6653c9685b354876b12e4fe6c78598b45e2", size = 11855450, upload-time = "2023-06-25T23:21:37.234Z" }, + { url = "https://files.pythonhosted.org/packages/66/f4/60739a2d336f3adf5628e7c9b920d16e8af6dc078550d615e4ba2a1d7759/mypy-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8207b7105829eca6f3d774f64a904190bb2231de91b8b186d21ffd98005f14a7", size = 11928679, upload-time = "2023-06-25T23:22:40.757Z" }, + { url = "https://files.pythonhosted.org/packages/8c/26/6ff2b55bf8b605a4cc898883654c2ca4dd4feedf0bb04ecaacf60d165cde/mypy-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:16f0db5b641ba159eff72cff08edc3875f2b62b2fa2bc24f68c1e7a4e8232d01", size = 8831134, upload-time = "2023-06-25T23:22:09.178Z" }, + { url = "https://files.pythonhosted.org/packages/95/47/fb69dad9634af9f1dab69f8b4031d674592384b59c7171852b1fbed6de15/mypy-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:470c969bb3f9a9efcedbadcd19a74ffb34a25f8e6b0e02dae7c0e71f8372f97b", size = 10101278, upload-time = "2023-06-25T23:22:51.525Z" }, + { url = "https://files.pythonhosted.org/packages/65/f7/77339904a3415cadca5551f2ea0c74feefc9b7187636a292690788f4d4b3/mypy-1.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5952d2d18b79f7dc25e62e014fe5a23eb1a3d2bc66318df8988a01b1a037c5b", size = 11643877, upload-time = "2023-06-25T23:22:20.963Z" }, + { url = "https://files.pythonhosted.org/packages/f5/93/ae39163ae84266d24d1fcf8ee1e2db1e0346e09de97570dd101a07ccf876/mypy-1.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:190b6bab0302cec4e9e6767d3eb66085aef2a1cc98fe04936d8a42ed2ba77bb7", size = 11702718, upload-time = "2023-06-25T23:22:32.056Z" }, + { url = "https://files.pythonhosted.org/packages/13/3b/3b7de921626547b36c34b91c74cfbda260210df7c49bd3d315015cfd6005/mypy-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9d40652cc4fe33871ad3338581dca3297ff5f2213d0df345bcfbde5162abf0c9", size = 8551181, upload-time = "2023-06-25T23:22:27.656Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/63bab763e4d44e1a7c341fb64496ddf20970780935596ffed9ed2d85eae7/mypy-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:01fd2e9f85622d981fd9063bfaef1aed6e336eaacca00892cd2d82801ab7c042", size = 10390236, upload-time = "2023-06-25T23:21:30.367Z" }, + { url = "https://files.pythonhosted.org/packages/23/3f/54a87d933440416a1efd7a42b45f8cf22e353efe889eb3903cc34177ab44/mypy-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2460a58faeea905aeb1b9b36f5065f2dc9a9c6e4c992a6499a2360c6c74ceca3", size = 9496760, upload-time = "2023-06-25T23:21:33.753Z" }, + { url = "https://files.pythonhosted.org/packages/4e/89/26230b46e27724bd54f76cd73a2759eaaf35292b32ba64f36c7c47836d4b/mypy-1.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2746d69a8196698146a3dbe29104f9eb6a2a4d8a27878d92169a6c0b74435b6", size = 11927489, upload-time = "2023-06-25T23:21:58.934Z" }, + { url = "https://files.pythonhosted.org/packages/64/7d/156e721376951c449554942eedf4d53e9ca2a57e94bf0833ad2821d59bfa/mypy-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae704dcfaa180ff7c4cfbad23e74321a2b774f92ca77fd94ce1049175a21c97f", size = 11990009, upload-time = "2023-06-25T23:21:55.909Z" }, + { url = "https://files.pythonhosted.org/packages/27/ab/21230851e8137c9ef9a095cc8cb70d8ff8cac21014e4b249ac7a9eae7df9/mypy-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:43d24f6437925ce50139a310a64b2ab048cb2d3694c84c71c3f2a1626d8101dc", size = 8816535, upload-time = "2023-06-25T23:21:45.703Z" }, + { url = "https://files.pythonhosted.org/packages/1d/1b/9050b5c444ef82c3d59bdbf21f91b259cf20b2ac1df37d55bc6b91d609a1/mypy-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c482e1246726616088532b5e964e39765b6d1520791348e6c9dc3af25b233828", size = 10447897, upload-time = "2023-06-25T23:21:22.06Z" }, + { url = "https://files.pythonhosted.org/packages/da/00/ac2b58b321d85cac25be0dcd1bc2427dfc6cf403283fc205a0031576f14b/mypy-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43b592511672017f5b1a483527fd2684347fdffc041c9ef53428c8dc530f79a3", size = 9534091, upload-time = "2023-06-25T23:22:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/26240f14e854a95af87d577b288d607ebe0ccb75cb37052f6386402f022d/mypy-1.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34a9239d5b3502c17f07fd7c0b2ae6b7dd7d7f6af35fbb5072c6208e76295816", size = 11970165, upload-time = "2023-06-25T23:22:05.673Z" }, + { url = "https://files.pythonhosted.org/packages/b7/34/a3edaec8762181bfe97439c7e094f4c2f411ed9b79ac8f4d72156e88d5ce/mypy-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5703097c4936bbb9e9bce41478c8d08edd2865e177dc4c52be759f81ee4dd26c", size = 12040792, upload-time = "2023-06-25T23:21:49.878Z" }, + { url = "https://files.pythonhosted.org/packages/d1/f3/0d0622d5a83859a992b01741a7b97949d6fb9efc9f05f20a09f0df10dc1e/mypy-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e02d700ec8d9b1859790c0475df4e4092c7bf3272a4fd2c9f33d87fac4427b8f", size = 8831367, upload-time = "2023-06-25T23:21:43.065Z" }, + { url = "https://files.pythonhosted.org/packages/3d/9a/e13addb8d652cb068f835ac2746d9d42f85b730092f581bb17e2059c28f1/mypy-1.4.1-py3-none-any.whl", hash = "sha256:45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4", size = 2451741, upload-time = "2023-06-25T23:22:49.033Z" }, +] + +[[package]] +name = "mypy" +version = "1.14.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +dependencies = [ + { name = "mypy-extensions", version = "1.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "tomli", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051, upload-time = "2024-12-30T16:39:07.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/7a/87ae2adb31d68402da6da1e5f30c07ea6063e9f09b5e7cfc9dfa44075e74/mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb", size = 11211002, upload-time = "2024-12-30T16:37:22.435Z" }, + { url = "https://files.pythonhosted.org/packages/e1/23/eada4c38608b444618a132be0d199b280049ded278b24cbb9d3fc59658e4/mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0", size = 10358400, upload-time = "2024-12-30T16:37:53.526Z" }, + { url = "https://files.pythonhosted.org/packages/43/c9/d6785c6f66241c62fd2992b05057f404237deaad1566545e9f144ced07f5/mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d", size = 12095172, upload-time = "2024-12-30T16:37:50.332Z" }, + { url = "https://files.pythonhosted.org/packages/c3/62/daa7e787770c83c52ce2aaf1a111eae5893de9e004743f51bfcad9e487ec/mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b", size = 12828732, upload-time = "2024-12-30T16:37:29.96Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a2/5fb18318a3637f29f16f4e41340b795da14f4751ef4f51c99ff39ab62e52/mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427", size = 13012197, upload-time = "2024-12-30T16:38:05.037Z" }, + { url = "https://files.pythonhosted.org/packages/28/99/e153ce39105d164b5f02c06c35c7ba958aaff50a2babba7d080988b03fe7/mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f", size = 9780836, upload-time = "2024-12-30T16:37:19.726Z" }, + { url = "https://files.pythonhosted.org/packages/da/11/a9422850fd506edbcdc7f6090682ecceaf1f87b9dd847f9df79942da8506/mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c", size = 11120432, upload-time = "2024-12-30T16:37:11.533Z" }, + { url = "https://files.pythonhosted.org/packages/b6/9e/47e450fd39078d9c02d620545b2cb37993a8a8bdf7db3652ace2f80521ca/mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1", size = 10279515, upload-time = "2024-12-30T16:37:40.724Z" }, + { url = "https://files.pythonhosted.org/packages/01/b5/6c8d33bd0f851a7692a8bfe4ee75eb82b6983a3cf39e5e32a5d2a723f0c1/mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8", size = 12025791, upload-time = "2024-12-30T16:36:58.73Z" }, + { url = "https://files.pythonhosted.org/packages/f0/4c/e10e2c46ea37cab5c471d0ddaaa9a434dc1d28650078ac1b56c2d7b9b2e4/mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f", size = 12749203, upload-time = "2024-12-30T16:37:03.741Z" }, + { url = "https://files.pythonhosted.org/packages/88/55/beacb0c69beab2153a0f57671ec07861d27d735a0faff135a494cd4f5020/mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1", size = 12885900, upload-time = "2024-12-30T16:37:57.948Z" }, + { url = "https://files.pythonhosted.org/packages/a2/75/8c93ff7f315c4d086a2dfcde02f713004357d70a163eddb6c56a6a5eff40/mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae", size = 9777869, upload-time = "2024-12-30T16:37:33.428Z" }, + { url = "https://files.pythonhosted.org/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", size = 11266668, upload-time = "2024-12-30T16:38:02.211Z" }, + { url = "https://files.pythonhosted.org/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", size = 10254060, upload-time = "2024-12-30T16:37:46.131Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", size = 11933167, upload-time = "2024-12-30T16:37:43.534Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", size = 12864341, upload-time = "2024-12-30T16:37:36.249Z" }, + { url = "https://files.pythonhosted.org/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89", size = 12972991, upload-time = "2024-12-30T16:37:06.743Z" }, + { url = "https://files.pythonhosted.org/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", size = 9879016, upload-time = "2024-12-30T16:37:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/9e/15/bb6a686901f59222275ab228453de741185f9d54fecbaacec041679496c6/mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255", size = 11252097, upload-time = "2024-12-30T16:37:25.144Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b3/8b0f74dfd072c802b7fa368829defdf3ee1566ba74c32a2cb2403f68024c/mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34", size = 10239728, upload-time = "2024-12-30T16:38:08.634Z" }, + { url = "https://files.pythonhosted.org/packages/c5/9b/4fd95ab20c52bb5b8c03cc49169be5905d931de17edfe4d9d2986800b52e/mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a", size = 11924965, upload-time = "2024-12-30T16:38:12.132Z" }, + { url = "https://files.pythonhosted.org/packages/56/9d/4a236b9c57f5d8f08ed346914b3f091a62dd7e19336b2b2a0d85485f82ff/mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9", size = 12867660, upload-time = "2024-12-30T16:38:17.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/88/a61a5497e2f68d9027de2bb139c7bb9abaeb1be1584649fa9d807f80a338/mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd", size = 12969198, upload-time = "2024-12-30T16:38:32.839Z" }, + { url = "https://files.pythonhosted.org/packages/54/da/3d6fc5d92d324701b0c23fb413c853892bfe0e1dbe06c9138037d459756b/mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107", size = 9885276, upload-time = "2024-12-30T16:38:20.828Z" }, + { url = "https://files.pythonhosted.org/packages/39/02/1817328c1372be57c16148ce7d2bfcfa4a796bedaed897381b1aad9b267c/mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31", size = 11143050, upload-time = "2024-12-30T16:38:29.743Z" }, + { url = "https://files.pythonhosted.org/packages/b9/07/99db9a95ece5e58eee1dd87ca456a7e7b5ced6798fd78182c59c35a7587b/mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6", size = 10321087, upload-time = "2024-12-30T16:38:14.739Z" }, + { url = "https://files.pythonhosted.org/packages/9a/eb/85ea6086227b84bce79b3baf7f465b4732e0785830726ce4a51528173b71/mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319", size = 12066766, upload-time = "2024-12-30T16:38:47.038Z" }, + { url = "https://files.pythonhosted.org/packages/4b/bb/f01bebf76811475d66359c259eabe40766d2f8ac8b8250d4e224bb6df379/mypy-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac", size = 12787111, upload-time = "2024-12-30T16:39:02.444Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c9/84837ff891edcb6dcc3c27d85ea52aab0c4a34740ff5f0ccc0eb87c56139/mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b", size = 12974331, upload-time = "2024-12-30T16:38:23.849Z" }, + { url = "https://files.pythonhosted.org/packages/84/5f/901e18464e6a13f8949b4909535be3fa7f823291b8ab4e4b36cfe57d6769/mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837", size = 9763210, upload-time = "2024-12-30T16:38:36.299Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1f/186d133ae2514633f8558e78cd658070ba686c0e9275c5a5c24a1e1f0d67/mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35", size = 11200493, upload-time = "2024-12-30T16:38:26.935Z" }, + { url = "https://files.pythonhosted.org/packages/af/fc/4842485d034e38a4646cccd1369f6b1ccd7bc86989c52770d75d719a9941/mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc", size = 10357702, upload-time = "2024-12-30T16:38:50.623Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e6/457b83f2d701e23869cfec013a48a12638f75b9d37612a9ddf99072c1051/mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9", size = 12091104, upload-time = "2024-12-30T16:38:53.735Z" }, + { url = "https://files.pythonhosted.org/packages/f1/bf/76a569158db678fee59f4fd30b8e7a0d75bcbaeef49edd882a0d63af6d66/mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb", size = 12830167, upload-time = "2024-12-30T16:38:56.437Z" }, + { url = "https://files.pythonhosted.org/packages/43/bc/0bc6b694b3103de9fed61867f1c8bd33336b913d16831431e7cb48ef1c92/mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60", size = 13013834, upload-time = "2024-12-30T16:38:59.204Z" }, + { url = "https://files.pythonhosted.org/packages/b0/79/5f5ec47849b6df1e6943d5fd8e6632fbfc04b4fd4acfa5a5a9535d11b4e2/mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c", size = 9781231, upload-time = "2024-12-30T16:39:05.124Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", size = 2752905, upload-time = "2024-12-30T16:38:42.021Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "librt", marker = "python_full_version >= '3.9' and platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions", version = "1.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pathspec", version = "1.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "tomli", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, + { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, + { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f7/88436084550ca9af5e610fa45286be04c3b63374df3e021c762fe8c4369f/mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3", size = 13102606, upload-time = "2025-12-15T05:02:46.833Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a5/43dfad311a734b48a752790571fd9e12d61893849a01bff346a54011957f/mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a", size = 12164496, upload-time = "2025-12-15T05:03:41.947Z" }, + { url = "https://files.pythonhosted.org/packages/88/f0/efbfa391395cce2f2771f937e0620cfd185ec88f2b9cd88711028a768e96/mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67", size = 12772068, upload-time = "2025-12-15T05:02:53.689Z" }, + { url = "https://files.pythonhosted.org/packages/25/05/58b3ba28f5aed10479e899a12d2120d582ba9fa6288851b20bf1c32cbb4f/mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e", size = 13520385, upload-time = "2025-12-15T05:02:38.328Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a0/c006ccaff50b31e542ae69b92fe7e2f55d99fba3a55e01067dd564325f85/mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376", size = 13796221, upload-time = "2025-12-15T05:03:22.147Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ff/8bdb051cd710f01b880472241bd36b3f817a8e1c5d5540d0b761675b6de2/mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24", size = 10055456, upload-time = "2025-12-15T05:03:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433, upload-time = "2023-02-04T12:11:27.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695, upload-time = "2023-02-04T12:11:25.002Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "packaging" +version = "24.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/b5/b43a27ac7472e1818c4bafd44430e69605baefe1f34440593e0332ec8b4d/packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9", size = 147882, upload-time = "2024-03-10T09:39:28.33Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/df/1fceb2f8900f8639e278b056416d49134fb8d84c5942ffaa01ad34782422/packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", size = 53488, upload-time = "2024-03-10T09:39:25.947Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", +] +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/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[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, 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, upload-time = "2024-08-25T14:17:22.55Z" }, +] + +[[package]] +name = "pathspec" +version = "0.11.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a0/2a/bd167cdf116d4f3539caaa4c332752aac0b3a0cc0174cdb302ee68933e81/pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3", size = 47032, upload-time = "2023-07-29T01:05:04.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/2a/9b1be29146139ef459188f5e420a66e835dda921208db600b7037093891f/pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20", size = 29603, upload-time = "2023-07-29T01:05:02.656Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +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, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "typing-extensions", version = "4.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/28/e40d24d2e2eb23135f8533ad33d582359c7825623b1e022f9d460def7c05/platformdirs-4.0.0.tar.gz", hash = "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731", size = 19914, upload-time = "2023-11-10T16:43:08.316Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/16/70be3b725073035aa5fc3229321d06e22e73e3e09f6af78dcfdf16c7636c/platformdirs-4.0.0-py3-none-any.whl", hash = "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b", size = 17562, upload-time = "2023-11-10T16:43:06.949Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +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, upload-time = "2024-09-17T19:06:49.212Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +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/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 = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, +] + +[[package]] +name = "pluggy" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "importlib-metadata", version = "6.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/42/8f2833655a29c4e9cb52ee8a2be04ceac61bcff4a680fb338cbd3d1e322d/pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3", size = 61613, upload-time = "2023-06-21T09:12:28.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/32/4a79112b8b87b21450b066e102d6608907f4c885ed7b04c3fdb085d4d6ae/pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849", size = 17695, upload-time = "2023-06-21T09:12:27.397Z" }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +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, upload-time = "2024-04-20T21:34:40.434Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +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/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.17.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://files.pythonhosted.org/packages/55/59/8bccf4157baf25e4aa5a0bb7fa3ba8600907de105ebc22b0c78cfbf6f565/pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367", size = 4827772, upload-time = "2023-11-21T20:43:53.875Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/9c/372fef8377a6e340b1704768d20daaded98bf13282b5327beb2e2fe2c7ef/pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c", size = 1179756, upload-time = "2023-11-21T20:43:49.423Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", +] +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/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 = "pymdown-extensions" +version = "10.2.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "markdown", version = "3.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "pyyaml", version = "6.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/41/7d67a7b6974fe3ffa03c817c9772f593535a85a72f4ba80af47168615098/pymdown_extensions-10.2.1.tar.gz", hash = "sha256:d0c534b4a5725a4be7ccef25d65a4c97dba58b54ad7c813babf0eb5ba9c81591", size = 784912, upload-time = "2023-08-30T15:17:13.902Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/5d/aaadfd7c9cc1a1a720c487fd28ecb18418728cd61dd3451e8a831e8030ea/pymdown_extensions-10.2.1-py3-none-any.whl", hash = "sha256:bded105eb8d93f88f2f821f00108cb70cef1269db6a40128c09c5f48bfc60ea4", size = 241051, upload-time = "2023-08-30T15:17:11.449Z" }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.15" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +dependencies = [ + { name = "markdown", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "pyyaml", version = "6.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/92/a7296491dbf5585b3a987f3f3fc87af0e632121ff3e490c14b5f2d2b4eb5/pymdown_extensions-10.15.tar.gz", hash = "sha256:0e5994e32155f4b03504f939e501b981d306daf7ec2aa1cd2eb6bd300784f8f7", size = 852320, upload-time = "2025-04-27T23:48:29.183Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/d1/c54e608505776ce4e7966d03358ae635cfd51dff1da6ee421c090dbc797b/pymdown_extensions-10.15-py3-none-any.whl", hash = "sha256:46e99bb272612b0de3b7e7caf6da8dd5f4ca5212c0b273feb9304e236c484e5f", size = 265845, upload-time = "2025-04-27T23:48:27.359Z" }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.20" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "markdown", version = "3.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyyaml", version = "6.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/35/e3814a5b7df295df69d035cfb8aab78b2967cdf11fcfae7faed726b66664/pymdown_extensions-10.20.tar.gz", hash = "sha256:5c73566ab0cf38c6ba084cb7c5ea64a119ae0500cce754ccb682761dfea13a52", size = 852774, upload-time = "2025-12-31T19:59:42.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/10/47caf89cbb52e5bb764696fd52a8c591a2f0e851a93270c05a17f36000b5/pymdown_extensions-10.20-py3-none-any.whl", hash = "sha256:ea9e62add865da80a271d00bfa1c0fa085b20d133fb3fc97afdc88e682f60b2f", size = 268733, upload-time = "2025-12-31T19:59:40.652Z" }, +] + +[[package]] +name = "pytest" +version = "7.4.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.8' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.8'" }, + { name = "importlib-metadata", version = "6.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "iniconfig", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "packaging", version = "24.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "pluggy", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "tomli", version = "2.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/1f/9d8e98e4133ffb16c90f3b405c43e38d3abb715bb5d7a63a5a684f7e46a3/pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", size = 1357116, upload-time = "2023-12-31T12:00:18.035Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8", size = 325287, upload-time = "2023-12-31T12:00:13.963Z" }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version == '3.8.*' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.8.*'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "packaging", version = "25.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "pluggy", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "tomli", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, +] +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, upload-time = "2025-03-02T12:54:52.069Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version == '3.9.*' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.9.*'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "packaging", version = "25.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "pygments", version = "2.19.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "tomli", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +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 = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging", version = "25.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pygments", version = "2.19.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "tomli", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "coverage", version = "7.2.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.8'" }, + { name = "pytest", version = "7.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/15/da3df99fd551507694a9b01f512a2f6cf1254f33601605843c3775f39460/pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6", size = 63245, upload-time = "2023-05-24T18:44:56.845Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/4b/8b78d126e275efa2379b1c2e09dc52cf70df16fc3b90613ef82531499d73/pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a", size = 21949, upload-time = "2023-05-24T18:44:54.079Z" }, +] + +[[package]] +name = "pytest-cov" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +dependencies = [ + { name = "coverage", version = "7.6.1", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version == '3.8.*'" }, + { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/67/00efc8d11b630c56f15f4ad9c7f9223f1e5ec275aaae3fa9118c6a223ad2/pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857", size = 63042, upload-time = "2024-03-24T20:16:34.856Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", size = 21990, upload-time = "2024-03-24T20:16:32.444Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version == '3.9.*'" }, + { name = "coverage", version = "7.13.1", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" }, + { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[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, 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 = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/e5/af35f7ea75cf72f2cd079c95ee16797de7cd71f29ea7c68ae5ce7be1eda0/PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", size = 125201, upload-time = "2023-07-18T00:00:23.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/06/4beb652c0fe16834032e54f0956443d4cc797fe645527acee59e7deaa0a2/PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", size = 189447, upload-time = "2023-07-17T23:57:04.325Z" }, + { url = "https://files.pythonhosted.org/packages/5b/07/10033a403b23405a8fc48975444463d3d10a5c2736b7eb2550b07b367429/PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f", size = 169264, upload-time = "2023-07-17T23:57:07.787Z" }, + { url = "https://files.pythonhosted.org/packages/f1/26/55e4f21db1f72eaef092015d9017c11510e7e6301c62a6cfee91295d13c6/PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", size = 677003, upload-time = "2023-07-17T23:57:13.144Z" }, + { url = "https://files.pythonhosted.org/packages/ba/91/090818dfa62e85181f3ae23dd1e8b7ea7f09684864a900cab72d29c57346/PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", size = 699070, upload-time = "2023-07-17T23:57:19.402Z" }, + { url = "https://files.pythonhosted.org/packages/29/61/bf33c6c85c55bc45a29eee3195848ff2d518d84735eb0e2d8cb42e0d285e/PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", size = 705525, upload-time = "2023-07-17T23:57:25.272Z" }, + { url = "https://files.pythonhosted.org/packages/07/91/45dfd0ef821a7f41d9d0136ea3608bb5b1653e42fd56a7970532cb5c003f/PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", size = 707514, upload-time = "2023-08-28T18:43:20.945Z" }, + { url = "https://files.pythonhosted.org/packages/b6/a0/b6700da5d49e9fed49dc3243d3771b598dad07abb37cc32e524607f96adc/PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", size = 130488, upload-time = "2023-07-17T23:57:28.144Z" }, + { url = "https://files.pythonhosted.org/packages/24/97/9b59b43431f98d01806b288532da38099cc6f2fea0f3d712e21e269c0279/PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", size = 145338, upload-time = "2023-07-17T23:57:31.118Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0d/26fb23e8863e0aeaac0c64e03fd27367ad2ae3f3cccf3798ee98ce160368/PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", size = 187867, upload-time = "2023-07-17T23:57:34.35Z" }, + { url = "https://files.pythonhosted.org/packages/28/09/55f715ddbf95a054b764b547f617e22f1d5e45d83905660e9a088078fe67/PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", size = 167530, upload-time = "2023-07-17T23:57:36.975Z" }, + { url = "https://files.pythonhosted.org/packages/5e/94/7d5ee059dfb92ca9e62f4057dcdec9ac08a9e42679644854dc01177f8145/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", size = 732244, upload-time = "2023-07-17T23:57:43.774Z" }, + { url = "https://files.pythonhosted.org/packages/06/92/e0224aa6ebf9dc54a06a4609da37da40bb08d126f5535d81bff6b417b2ae/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", size = 752871, upload-time = "2023-07-17T23:57:51.921Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5e/efd033ab7199a0b2044dab3b9f7a4f6670e6a52c089de572e928d2873b06/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", size = 757729, upload-time = "2023-07-17T23:57:59.865Z" }, + { url = "https://files.pythonhosted.org/packages/03/5c/c4671451b2f1d76ebe352c0945d4cd13500adb5d05f5a51ee296d80152f7/PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", size = 748528, upload-time = "2023-08-28T18:43:23.207Z" }, + { url = "https://files.pythonhosted.org/packages/73/9c/766e78d1efc0d1fca637a6b62cea1b4510a7fb93617eb805223294fef681/PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", size = 130286, upload-time = "2023-07-17T23:58:02.964Z" }, + { url = "https://files.pythonhosted.org/packages/b3/34/65bb4b2d7908044963ebf614fe0fdb080773fc7030d7e39c8d3eddcd4257/PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", size = 144699, upload-time = "2023-07-17T23:58:05.586Z" }, + { url = "https://files.pythonhosted.org/packages/bc/06/1b305bf6aa704343be85444c9d011f626c763abb40c0edc1cad13bfd7f86/PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", size = 178692, upload-time = "2023-08-28T18:43:24.924Z" }, + { url = "https://files.pythonhosted.org/packages/84/02/404de95ced348b73dd84f70e15a41843d817ff8c1744516bf78358f2ffd2/PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", size = 165622, upload-time = "2023-08-28T18:43:26.54Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4c/4a2908632fc980da6d918b9de9c1d9d7d7e70b2672b1ad5166ed27841ef7/PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef", size = 696937, upload-time = "2024-01-18T20:40:22.92Z" }, + { url = "https://files.pythonhosted.org/packages/b4/33/720548182ffa8344418126017aa1d4ab4aeec9a2275f04ce3f3573d8ace8/PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", size = 724969, upload-time = "2023-08-28T18:43:28.56Z" }, + { url = "https://files.pythonhosted.org/packages/4f/78/77b40157b6cb5f2d3d31a3d9b2efd1ba3505371f76730d267e8b32cf4b7f/PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", size = 712604, upload-time = "2023-08-28T18:43:30.206Z" }, + { url = "https://files.pythonhosted.org/packages/2e/97/3e0e089ee85e840f4b15bfa00e4e63d84a3691ababbfea92d6f820ea6f21/PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", size = 126098, upload-time = "2023-08-28T18:43:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/fbade56564ad486809c27b322d0f7e6a89c01f6b4fe208402e90d4443a99/PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", size = 138675, upload-time = "2023-08-28T18:43:33.613Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d1/02baa09d39b1bb1ebaf0d850d106d1bdcb47c91958557f471153c49dc03b/PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", size = 189627, upload-time = "2023-07-17T23:58:40.188Z" }, + { url = "https://files.pythonhosted.org/packages/e5/31/ba812efa640a264dbefd258986a5e4e786230cb1ee4a9f54eb28ca01e14a/PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", size = 658438, upload-time = "2023-07-17T23:58:48.34Z" }, + { url = "https://files.pythonhosted.org/packages/4d/f1/08f06159739254c8947899c9fc901241614195db15ba8802ff142237664c/PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", size = 680304, upload-time = "2023-07-17T23:58:57.396Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8f/db62b0df635b9008fe90aa68424e99cee05e68b398740c8a666a98455589/PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", size = 670140, upload-time = "2023-07-17T23:59:04.291Z" }, + { url = "https://files.pythonhosted.org/packages/cc/5c/fcabd17918348c7db2eeeb0575705aaf3f7ab1657f6ce29b2e31737dd5d1/PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", size = 137577, upload-time = "2023-07-17T23:59:07.267Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ae/964ccb88a938f20ece5754878f182cfbd846924930d02d29d06af8d4c69e/PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", size = 153248, upload-time = "2023-07-17T23:59:10.608Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5d/2779ea035ba1e533c32ed4a249b4e0448f583ba10830b21a3cddafe11a4e/PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", size = 191734, upload-time = "2023-07-17T23:59:13.869Z" }, + { url = "https://files.pythonhosted.org/packages/e1/a1/27bfac14b90adaaccf8c8289f441e9f76d94795ec1e7a8f134d9f2cb3d0b/PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", size = 723767, upload-time = "2023-07-17T23:59:20.686Z" }, + { url = "https://files.pythonhosted.org/packages/c1/39/47ed4d65beec9ce07267b014be85ed9c204fa373515355d3efa62d19d892/PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", size = 749067, upload-time = "2023-07-17T23:59:28.747Z" }, + { url = "https://files.pythonhosted.org/packages/c8/6b/6600ac24725c7388255b2f5add93f91e58a5d7efaf4af244fdbcc11a541b/PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", size = 736569, upload-time = "2023-07-17T23:59:37.216Z" }, + { url = "https://files.pythonhosted.org/packages/0d/46/62ae77677e532c0af6c81ddd6f3dbc16bdcc1208b077457354442d220bfb/PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6", size = 787738, upload-time = "2023-08-28T18:43:35.582Z" }, + { url = "https://files.pythonhosted.org/packages/d6/6a/439d1a6f834b9a9db16332ce16c4a96dd0e3970b65fe08cbecd1711eeb77/PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", size = 139797, upload-time = "2023-07-17T23:59:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/29/0f/9782fa5b10152abf033aec56a601177ead85ee03b57781f2d9fced09eefc/PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", size = 157350, upload-time = "2023-07-17T23:59:42.94Z" }, + { url = "https://files.pythonhosted.org/packages/57/c5/5d09b66b41d549914802f482a2118d925d876dc2a35b2d127694c1345c34/PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", size = 197846, upload-time = "2023-07-17T23:59:46.424Z" }, + { url = "https://files.pythonhosted.org/packages/0e/88/21b2f16cb2123c1e9375f2c93486e35fdc86e63f02e274f0e99c589ef153/PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", size = 174396, upload-time = "2023-07-17T23:59:49.538Z" }, + { url = "https://files.pythonhosted.org/packages/ac/6c/967d91a8edf98d2b2b01d149bd9e51b8f9fb527c98d80ebb60c6b21d60c4/PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", size = 731824, upload-time = "2023-07-17T23:59:58.111Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4b/c71ef18ef83c82f99e6da8332910692af78ea32bd1d1d76c9787dfa36aea/PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", size = 754777, upload-time = "2023-07-18T00:00:06.716Z" }, + { url = "https://files.pythonhosted.org/packages/7d/39/472f2554a0f1e825bd7c5afc11c817cd7a2f3657460f7159f691fbb37c51/PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", size = 738883, upload-time = "2023-07-18T00:00:14.423Z" }, + { url = "https://files.pythonhosted.org/packages/40/da/a175a35cf5583580e90ac3e2a3dbca90e43011593ae62ce63f79d7b28d92/PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", size = 750294, upload-time = "2023-08-28T18:43:37.153Z" }, + { url = "https://files.pythonhosted.org/packages/24/62/7fcc372442ec8ea331da18c24b13710e010c5073ab851ef36bf9dacb283f/PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", size = 136936, upload-time = "2023-07-18T00:00:17.167Z" }, + { url = "https://files.pythonhosted.org/packages/84/4d/82704d1ab9290b03da94e6425f5e87396b999fd7eb8e08f3a92c158402bf/PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", size = 152751, upload-time = "2023-07-18T00:00:19.939Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", +] +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/0d/a2/09f67a3589cb4320fb5ce90d3fd4c9752636b8b6ad8f34b54d76c5a54693/PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f", size = 186824, upload-time = "2025-09-29T20:27:35.918Z" }, + { url = "https://files.pythonhosted.org/packages/02/72/d972384252432d57f248767556ac083793292a4adf4e2d85dfe785ec2659/PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4", size = 795069, upload-time = "2025-09-29T20:27:38.15Z" }, + { url = "https://files.pythonhosted.org/packages/a7/3b/6c58ac0fa7c4e1b35e48024eb03d00817438310447f93ef4431673c24138/PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3", size = 862585, upload-time = "2025-09-29T20:27:39.715Z" }, + { url = "https://files.pythonhosted.org/packages/25/a2/b725b61ac76a75583ae7104b3209f75ea44b13cfd026aa535ece22b7f22e/PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6", size = 806018, upload-time = "2025-09-29T20:27:41.444Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b0/b2227677b2d1036d84f5ee95eb948e7af53d59fe3e4328784e4d290607e0/PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369", size = 802822, upload-time = "2025-09-29T20:27:42.885Z" }, + { url = "https://files.pythonhosted.org/packages/99/a5/718a8ea22521e06ef19f91945766a892c5ceb1855df6adbde67d997ea7ed/PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295", size = 143744, upload-time = "2025-09-29T20:27:44.487Z" }, + { url = "https://files.pythonhosted.org/packages/76/b2/2b69cee94c9eb215216fc05778675c393e3aa541131dc910df8e52c83776/PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b", size = 160082, upload-time = "2025-09-29T20:27:46.049Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/9f/62/67fc8e68a75f738c9200422bf65693fb79a4cd0dc5b23310e5202e978090/pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", size = 184450, upload-time = "2025-09-25T21:33:00.618Z" }, + { url = "https://files.pythonhosted.org/packages/ae/92/861f152ce87c452b11b9d0977952259aa7df792d71c1053365cc7b09cc08/pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", size = 174319, upload-time = "2025-09-25T21:33:02.086Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cd/f0cfc8c74f8a030017a2b9c771b7f47e5dd702c3e28e5b2071374bda2948/pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", size = 737631, upload-time = "2025-09-25T21:33:03.25Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b2/18f2bd28cd2055a79a46c9b0895c0b3d987ce40ee471cecf58a1a0199805/pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", size = 836795, upload-time = "2025-09-25T21:33:05.014Z" }, + { url = "https://files.pythonhosted.org/packages/73/b9/793686b2d54b531203c160ef12bec60228a0109c79bae6c1277961026770/pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", size = 750767, upload-time = "2025-09-25T21:33:06.398Z" }, + { url = "https://files.pythonhosted.org/packages/a9/86/a137b39a611def2ed78b0e66ce2fe13ee701a07c07aebe55c340ed2a050e/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", size = 727982, upload-time = "2025-09-25T21:33:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/dd/62/71c27c94f457cf4418ef8ccc71735324c549f7e3ea9d34aba50874563561/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", size = 755677, upload-time = "2025-09-25T21:33:09.876Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/6f5e0d58bd924fb0d06c3a6bad00effbdae2de5adb5cda5648006ffbd8d3/pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", size = 142592, upload-time = "2025-09-25T21:33:10.983Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0c/25113e0b5e103d7f1490c0e947e303fe4a696c10b501dea7a9f49d4e876c/pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", size = 158777, upload-time = "2025-09-25T21:33:15.55Z" }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", + "python_full_version < '3.8'", +] +dependencies = [ + { name = "pyyaml", version = "6.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "pyyaml", version = "6.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, +] +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, upload-time = "2020-11-12T02:38:24.638Z" }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "pyyaml", version = "6.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +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/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 = "regex" +version = "2022.10.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/b5/92d404279fd5f4f0a17235211bb0f5ae7a0d9afb7f439086ec247441ed28/regex-2022.10.31.tar.gz", hash = "sha256:a3a98921da9a1bf8457aeee6a551948a83601689e5ecdd736894ea9bbec77e83", size = 391554, upload-time = "2022-10-31T03:30:47.752Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/93/67595e62890fa944da394795f0425140917340d35d9cfd49672a8dc48c1a/regex-2022.10.31-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a8ff454ef0bb061e37df03557afda9d785c905dab15584860f982e88be73015f", size = 293917, upload-time = "2022-10-31T03:26:14.12Z" }, + { url = "https://files.pythonhosted.org/packages/8d/50/7dd264adf08bf3ca588562bac344a825174e8e57c75ad3e5ed169aba5718/regex-2022.10.31-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1eba476b1b242620c266edf6325b443a2e22b633217a9835a52d8da2b5c051f9", size = 287178, upload-time = "2022-10-31T03:26:18.034Z" }, + { url = "https://files.pythonhosted.org/packages/30/eb/a28fad5b882d3e711c75414b3c99fb2954f78fa450deeed9fe9ad3bf2534/regex-2022.10.31-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0e5af9a9effb88535a472e19169e09ce750c3d442fb222254a276d77808620b", size = 769845, upload-time = "2022-10-31T03:26:20.534Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ba/92096d78cbdd34dce674962392a0e57ce748a9e5f737f12b0001723d959a/regex-2022.10.31-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d03fe67b2325cb3f09be029fd5da8df9e6974f0cde2c2ac6a79d2634e791dd57", size = 809592, upload-time = "2022-10-31T03:26:23.597Z" }, + { url = "https://files.pythonhosted.org/packages/48/1e/829551abceba73e7e9b1f94a311a53e9c0f60c7deec8821633fc3b343a58/regex-2022.10.31-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9d0b68ac1743964755ae2d89772c7e6fb0118acd4d0b7464eaf3921c6b49dd4", size = 795944, upload-time = "2022-10-31T03:26:26.851Z" }, + { url = "https://files.pythonhosted.org/packages/be/d3/7e334b8bc597dea6200f7bb969fc693d4c71c4a395750e28d09c8e5a8104/regex-2022.10.31-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a45b6514861916c429e6059a55cf7db74670eaed2052a648e3e4d04f070e001", size = 770479, upload-time = "2022-10-31T03:26:31.625Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ca/105a8f6d70499f2687a857570dcd411c0621a347b06c27126cffc32e77e0/regex-2022.10.31-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8b0886885f7323beea6f552c28bff62cbe0983b9fbb94126531693ea6c5ebb90", size = 757876, upload-time = "2022-10-31T03:26:34.351Z" }, + { url = "https://files.pythonhosted.org/packages/7c/cf/50844f62052bb858987fe3970315134e3be6167fc76e11d328e7fcf876ff/regex-2022.10.31-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5aefb84a301327ad115e9d346c8e2760009131d9d4b4c6b213648d02e2abe144", size = 685151, upload-time = "2022-10-31T03:26:39.941Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c2/6d41a7a9690d4543b1f438f45576af96523c4f1caeb5307fff3350ec7d0b/regex-2022.10.31-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:702d8fc6f25bbf412ee706bd73019da5e44a8400861dfff7ff31eb5b4a1276dc", size = 739977, upload-time = "2022-10-31T03:26:43.157Z" }, + { url = "https://files.pythonhosted.org/packages/3c/d1/49b9a2cb289c20888b23bb7f8f29e3ad7982785b10041477fd56ed5783c5/regex-2022.10.31-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a3c1ebd4ed8e76e886507c9eddb1a891673686c813adf889b864a17fafcf6d66", size = 728265, upload-time = "2022-10-31T03:26:45.564Z" }, + { url = "https://files.pythonhosted.org/packages/08/cb/0445a970e755eb806945a166729210861391f645223187aa11fcbbb606ce/regex-2022.10.31-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:50921c140561d3db2ab9f5b11c5184846cde686bb5a9dc64cae442926e86f3af", size = 762419, upload-time = "2022-10-31T03:26:48.032Z" }, + { url = "https://files.pythonhosted.org/packages/23/8d/1df5d30ce1e5ae3edfb775b892c93882d13ba93991314871fec569f16829/regex-2022.10.31-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:7db345956ecce0c99b97b042b4ca7326feeec6b75facd8390af73b18e2650ffc", size = 763152, upload-time = "2022-10-31T03:26:51.093Z" }, + { url = "https://files.pythonhosted.org/packages/00/7e/ab5a54f60e36f4de0610850866b848839a7b02ad4f05755bce429fbc1a5a/regex-2022.10.31-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:763b64853b0a8f4f9cfb41a76a4a85a9bcda7fdda5cb057016e7706fde928e66", size = 741683, upload-time = "2022-10-31T03:26:53.738Z" }, + { url = "https://files.pythonhosted.org/packages/2d/db/45ca83007d69cc594c32d7feae20b1b6067f829b2b0d27bb769d7188dfa1/regex-2022.10.31-cp310-cp310-win32.whl", hash = "sha256:44136355e2f5e06bf6b23d337a75386371ba742ffa771440b85bed367c1318d1", size = 255766, upload-time = "2022-10-31T03:26:56.473Z" }, + { url = "https://files.pythonhosted.org/packages/b7/0a/c865345e6ece671f16ac1fe79bf4ba771c528c2e4a56607898cdf065c285/regex-2022.10.31-cp310-cp310-win_amd64.whl", hash = "sha256:bfff48c7bd23c6e2aec6454aaf6edc44444b229e94743b34bdcdda2e35126cf5", size = 267701, upload-time = "2022-10-31T03:26:59.275Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1a/e7ae9a041d3e103f98c9a79d8abb235cca738b7bd6da3fb5e4066d30e4d7/regex-2022.10.31-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b4b1fe58cd102d75ef0552cf17242705ce0759f9695334a56644ad2d83903fe", size = 293971, upload-time = "2022-10-31T03:27:01.908Z" }, + { url = "https://files.pythonhosted.org/packages/fa/54/acb97b65bc556520d61262ff22ad7d4baff96e3219fa1dc5425269def873/regex-2022.10.31-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:542e3e306d1669b25936b64917285cdffcd4f5c6f0247636fec037187bd93542", size = 287195, upload-time = "2022-10-31T03:27:05.141Z" }, + { url = "https://files.pythonhosted.org/packages/c1/65/3ee862c7a78ce1f9bd748d460e379317464c2658e645a1a7c1304d36e819/regex-2022.10.31-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c27cc1e4b197092e50ddbf0118c788d9977f3f8f35bfbbd3e76c1846a3443df7", size = 781858, upload-time = "2022-10-31T03:27:08.773Z" }, + { url = "https://files.pythonhosted.org/packages/55/73/f71734c0357e41673b00bff0a8675ffb67328ba18f24614ec5af2073b56f/regex-2022.10.31-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8e38472739028e5f2c3a4aded0ab7eadc447f0d84f310c7a8bb697ec417229e", size = 821531, upload-time = "2022-10-31T03:27:11.668Z" }, + { url = "https://files.pythonhosted.org/packages/83/ad/defd48762ff8fb2d06667b1e8bef471c2cc71a1b3d6ead26b841bfd9da99/regex-2022.10.31-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:76c598ca73ec73a2f568e2a72ba46c3b6c8690ad9a07092b18e48ceb936e9f0c", size = 808819, upload-time = "2022-10-31T03:27:15.369Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cf/97a89e2b798988118beed6620dbfbc9b4bd72d8177b3b4ed47d80da26df9/regex-2022.10.31-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c28d3309ebd6d6b2cf82969b5179bed5fefe6142c70f354ece94324fa11bf6a1", size = 781085, upload-time = "2022-10-31T03:27:18.436Z" }, + { url = "https://files.pythonhosted.org/packages/fd/12/c5d64d860c2d1be211a91b2416097d5e40699b80296cb4e99a064d4b4ff2/regex-2022.10.31-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9af69f6746120998cd9c355e9c3c6aec7dff70d47247188feb4f829502be8ab4", size = 769611, upload-time = "2022-10-31T03:27:20.957Z" }, + { url = "https://files.pythonhosted.org/packages/04/de/e8ed731b334e5f962ef035a32f151fffb2f839eccfba40c3ebdac9b26e03/regex-2022.10.31-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a5f9505efd574d1e5b4a76ac9dd92a12acb2b309551e9aa874c13c11caefbe4f", size = 746313, upload-time = "2022-10-31T03:27:23.905Z" }, + { url = "https://files.pythonhosted.org/packages/18/9c/b52170b2dc8d65a69f3369d0bd1a3102df295edfccfef1b41e82b6aef796/regex-2022.10.31-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5ff525698de226c0ca743bfa71fc6b378cda2ddcf0d22d7c37b1cc925c9650a5", size = 737036, upload-time = "2022-10-31T03:27:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/d2/a6/2af9cc002057b75868ec7740fe3acb8f89796c9d29caf5775fefd96c3240/regex-2022.10.31-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:4fe7fda2fe7c8890d454f2cbc91d6c01baf206fbc96d89a80241a02985118c0c", size = 771052, upload-time = "2022-10-31T03:27:32.689Z" }, + { url = "https://files.pythonhosted.org/packages/87/50/e237090e90a0b0c8eab40af7d6f2faaf1432c4dca232de9a9c789faf3154/regex-2022.10.31-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:2cdc55ca07b4e70dda898d2ab7150ecf17c990076d3acd7a5f3b25cb23a69f1c", size = 772708, upload-time = "2022-10-31T03:27:35.745Z" }, + { url = "https://files.pythonhosted.org/packages/07/ba/7021c60d02f7fe7c3e4ee9636d8a2d93bd894a5063c2b5f35e2e31b1f3ad/regex-2022.10.31-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:44a6c2f6374e0033873e9ed577a54a3602b4f609867794c1a3ebba65e4c93ee7", size = 750463, upload-time = "2022-10-31T03:27:39.832Z" }, + { url = "https://files.pythonhosted.org/packages/08/28/f038ff3c5cfd30760bccefbe0b98d51cf61192ec8d3d55dd51564bf6c6b8/regex-2022.10.31-cp311-cp311-win32.whl", hash = "sha256:d8716f82502997b3d0895d1c64c3b834181b1eaca28f3f6336a71777e437c2af", size = 255769, upload-time = "2022-10-31T03:27:42.847Z" }, + { url = "https://files.pythonhosted.org/packages/91/4e/fb78efdac24862ef6ea8009b0b9cdb5f25968d1b262cc32abd9d483f50b1/regex-2022.10.31-cp311-cp311-win_amd64.whl", hash = "sha256:61edbca89aa3f5ef7ecac8c23d975fe7261c12665f1d90a6b1af527bba86ce61", size = 267703, upload-time = "2022-10-31T03:27:45.728Z" }, + { url = "https://files.pythonhosted.org/packages/e6/4a/48779981af80558ac01f0f2c0d71c1214215bc74c9b824eb6581e94a847c/regex-2022.10.31-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4cac3405d8dda8bc6ed499557625585544dd5cbf32072dcc72b5a176cb1271c8", size = 294374, upload-time = "2022-10-31T03:28:29.56Z" }, + { url = "https://files.pythonhosted.org/packages/56/e3/351029c41f42e29d9c6ae3d217ad332761945b41dfbddb64adc31d434c6b/regex-2022.10.31-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23cbb932cc53a86ebde0fb72e7e645f9a5eec1a5af7aa9ce333e46286caef783", size = 752940, upload-time = "2022-10-31T03:28:32.341Z" }, + { url = "https://files.pythonhosted.org/packages/3c/4f/33b5cbd85fb0272e5c1dc00e3cfc89874b37705613455d7ab1c1f3ff7906/regex-2022.10.31-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74bcab50a13960f2a610cdcd066e25f1fd59e23b69637c92ad470784a51b1347", size = 794992, upload-time = "2022-10-31T03:28:35.197Z" }, + { url = "https://files.pythonhosted.org/packages/e5/7d/0b0d25b7bb9a38cdccffd3fdcbf4ad7dd124fdf6ca6067cd973edff804bc/regex-2022.10.31-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78d680ef3e4d405f36f0d6d1ea54e740366f061645930072d39bca16a10d8c93", size = 781796, upload-time = "2022-10-31T03:28:38.149Z" }, + { url = "https://files.pythonhosted.org/packages/42/d8/8a7131e7d0bf237f7bcd3191541a4bf21863c253fe6bee0796900a1a9a29/regex-2022.10.31-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6910b56b700bea7be82c54ddf2e0ed792a577dfaa4a76b9af07d550af435c6", size = 757070, upload-time = "2022-10-31T03:28:41.442Z" }, + { url = "https://files.pythonhosted.org/packages/0a/cd/4dfdfddca4478ad0ebb6053b2c2923eef1a8660ceb9f495e7a6abb62da15/regex-2022.10.31-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:659175b2144d199560d99a8d13b2228b85e6019b6e09e556209dfb8c37b78a11", size = 743506, upload-time = "2022-10-31T03:28:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/fe/f2/20be658beb9ebef677550be562eae86c5433119b4b2fdb67035e9a841b0f/regex-2022.10.31-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1ddf14031a3882f684b8642cb74eea3af93a2be68893901b2b387c5fd92a03ec", size = 676318, upload-time = "2022-10-31T03:28:47.841Z" }, + { url = "https://files.pythonhosted.org/packages/43/5b/6ba9b08ea991993ad61e4098d88069c86f6d6cc0e52a26fa35f6a66d90ee/regex-2022.10.31-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b683e5fd7f74fb66e89a1ed16076dbab3f8e9f34c18b1979ded614fe10cdc4d9", size = 728650, upload-time = "2022-10-31T03:28:50.887Z" }, + { url = "https://files.pythonhosted.org/packages/a3/60/6084d08f56d424f46ecbfedebd11b2c2d7eb2f9bc36ccd8801821024262c/regex-2022.10.31-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2bde29cc44fa81c0a0c8686992c3080b37c488df167a371500b2a43ce9f026d1", size = 718298, upload-time = "2022-10-31T03:28:53.734Z" }, + { url = "https://files.pythonhosted.org/packages/10/13/95d658ca010507b5a179d7fe8376d37d20c22f9be5abdd301832618463a8/regex-2022.10.31-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4919899577ba37f505aaebdf6e7dc812d55e8f097331312db7f1aab18767cce8", size = 752789, upload-time = "2022-10-31T03:28:56.649Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/b6819a467182e94e7648120cedcb6019751ceff9f5f3ef9c340e14ea7992/regex-2022.10.31-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:9c94f7cc91ab16b36ba5ce476f1904c91d6c92441f01cd61a8e2729442d6fcf5", size = 754164, upload-time = "2022-10-31T03:28:59.321Z" }, + { url = "https://files.pythonhosted.org/packages/00/92/25b0b709d591ecd27e1bfb48c64d813a4ed4be0feb0321ea0b55db012099/regex-2022.10.31-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ae1e96785696b543394a4e3f15f3f225d44f3c55dafe3f206493031419fedf95", size = 730547, upload-time = "2022-10-31T03:29:02.247Z" }, + { url = "https://files.pythonhosted.org/packages/c2/52/b71ff1a281f37016cab322e176e3c63fe1b5c27d68cdacdec769708e49b7/regex-2022.10.31-cp37-cp37m-win32.whl", hash = "sha256:c670f4773f2f6f1957ff8a3962c7dd12e4be54d05839b216cb7fd70b5a1df394", size = 255505, upload-time = "2022-10-31T03:29:04.783Z" }, + { url = "https://files.pythonhosted.org/packages/01/b3/a01602507224e611caa3c0f2a4aa96f4c03fdce482fa4527de61678a3018/regex-2022.10.31-cp37-cp37m-win_amd64.whl", hash = "sha256:8e0caeff18b96ea90fc0eb6e3bdb2b10ab5b01a95128dfeccb64a7238decf5f0", size = 268006, upload-time = "2022-10-31T03:29:07.245Z" }, + { url = "https://files.pythonhosted.org/packages/ad/29/4efb589803fa476e649fcc256886837b74931c4ca1878e69cd5018f77e03/regex-2022.10.31-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:131d4be09bea7ce2577f9623e415cab287a3c8e0624f778c1d955ec7c281bd4d", size = 294035, upload-time = "2022-10-31T03:29:10.898Z" }, + { url = "https://files.pythonhosted.org/packages/78/74/c8659c8e1b6745299df62099d162002deeb32a9a933bc7632836a3c22374/regex-2022.10.31-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e613a98ead2005c4ce037c7b061f2409a1a4e45099edb0ef3200ee26ed2a69a8", size = 287154, upload-time = "2022-10-31T03:29:13.831Z" }, + { url = "https://files.pythonhosted.org/packages/dd/08/67feb849ab7288465b7b577cf076c0db5244dfd64bec8740cd8f0e074897/regex-2022.10.31-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:052b670fafbe30966bbe5d025e90b2a491f85dfe5b2583a163b5e60a85a321ad", size = 771084, upload-time = "2022-10-31T03:29:16.447Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/daeb6806a2b2e10e548c95b136aefb12818ef81a0aa5f865705bf19e7cd7/regex-2022.10.31-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa62a07ac93b7cb6b7d0389d8ef57ffc321d78f60c037b19dfa78d6b17c928ee", size = 811472, upload-time = "2022-10-31T03:29:19.715Z" }, + { url = "https://files.pythonhosted.org/packages/56/4b/22c965c2f6847b0581a8d4407b265c04f989cb6df09ddfd7205744b14cbc/regex-2022.10.31-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5352bea8a8f84b89d45ccc503f390a6be77917932b1c98c4cdc3565137acc714", size = 796329, upload-time = "2022-10-31T03:29:22.999Z" }, + { url = "https://files.pythonhosted.org/packages/21/1f/f54c156ac95a89d33113d78a18c03db8c00600392d6d6c5a18249c563c58/regex-2022.10.31-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20f61c9944f0be2dc2b75689ba409938c14876c19d02f7585af4460b6a21403e", size = 772348, upload-time = "2022-10-31T03:29:25.809Z" }, + { url = "https://files.pythonhosted.org/packages/b3/60/38ea6f8808bf58852b3e08faa2d7418b8887144f891284bc2a1afb7b6967/regex-2022.10.31-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29c04741b9ae13d1e94cf93fca257730b97ce6ea64cfe1eba11cf9ac4e85afb6", size = 760439, upload-time = "2022-10-31T03:29:29.02Z" }, + { url = "https://files.pythonhosted.org/packages/9c/1a/63bcd0f28f74619190c4f6f3cf90e3fdccb4b1437aac7e19598e18b51901/regex-2022.10.31-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:543883e3496c8b6d58bd036c99486c3c8387c2fc01f7a342b760c1ea3158a318", size = 690577, upload-time = "2022-10-31T03:29:32.414Z" }, + { url = "https://files.pythonhosted.org/packages/72/cf/da36a722626572ea66ab799e7019eb9a367fa563d43e3b1ec65a934d12d3/regex-2022.10.31-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7a8b43ee64ca8f4befa2bea4083f7c52c92864d8518244bfa6e88c751fa8fff", size = 744029, upload-time = "2022-10-31T03:29:35.626Z" }, + { url = "https://files.pythonhosted.org/packages/08/ef/96ef949ee331d39489799b44f2d5aa8a252a2d7aa4a96edbb05425d344f6/regex-2022.10.31-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6a9a19bea8495bb419dc5d38c4519567781cd8d571c72efc6aa959473d10221a", size = 732602, upload-time = "2022-10-31T03:29:38.812Z" }, + { url = "https://files.pythonhosted.org/packages/d8/5c/40e197174793b44637dd542c1dee45a5517023d1cac5ca5a68fbe60e4105/regex-2022.10.31-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6ffd55b5aedc6f25fd8d9f905c9376ca44fcf768673ffb9d160dd6f409bfda73", size = 766937, upload-time = "2022-10-31T03:29:42.183Z" }, + { url = "https://files.pythonhosted.org/packages/08/e2/94af654d5fdfdad3a05991e104df66c42945650d31713fe290cd446178f1/regex-2022.10.31-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4bdd56ee719a8f751cf5a593476a441c4e56c9b64dc1f0f30902858c4ef8771d", size = 767790, upload-time = "2022-10-31T03:29:45.209Z" }, + { url = "https://files.pythonhosted.org/packages/09/d3/70714b99c25bac40f81eaf3fe06eb016c5b9b9ac88815145dc6aa7d06b68/regex-2022.10.31-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ca88da1bd78990b536c4a7765f719803eb4f8f9971cc22d6ca965c10a7f2c4c", size = 747112, upload-time = "2022-10-31T03:29:48.876Z" }, + { url = "https://files.pythonhosted.org/packages/63/89/7035055b960428a3af1fb1bfdf805cada83a81f88459350dad82a260a08d/regex-2022.10.31-cp38-cp38-win32.whl", hash = "sha256:5a260758454580f11dd8743fa98319bb046037dfab4f7828008909d0aa5292bc", size = 255779, upload-time = "2022-10-31T03:29:51.914Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/895ba11bc0243becd38f8b7560d2e329c465ead247cfb815611c347d7fc1/regex-2022.10.31-cp38-cp38-win_amd64.whl", hash = "sha256:5e6a5567078b3eaed93558842346c9d678e116ab0135e22eb72db8325e90b453", size = 267716, upload-time = "2022-10-31T03:29:54.755Z" }, + { url = "https://files.pythonhosted.org/packages/c7/6a/386254696e2ab59ccce2eeee1e014f95538004e3c840606ef817192dbf8a/regex-2022.10.31-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5217c25229b6a85049416a5c1e6451e9060a1edcf988641e309dbe3ab26d3e49", size = 293917, upload-time = "2022-10-31T03:29:58.174Z" }, + { url = "https://files.pythonhosted.org/packages/5f/7e/23ddf7d405aad0d0a8fa478ba60fc1c46f661403fe4a49e04d48ea1095b4/regex-2022.10.31-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4bf41b8b0a80708f7e0384519795e80dcb44d7199a35d52c15cc674d10b3081b", size = 287193, upload-time = "2022-10-31T03:30:01.127Z" }, + { url = "https://files.pythonhosted.org/packages/58/4e/0f0a7b674d6164809db80eac36a3a70bbd3bcf6dc8fb6f89f70f0893b85b/regex-2022.10.31-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf0da36a212978be2c2e2e2d04bdff46f850108fccc1851332bcae51c8907cc", size = 769260, upload-time = "2022-10-31T03:30:04.244Z" }, + { url = "https://files.pythonhosted.org/packages/59/68/5d77731c6cb3cfcf8aece4c650cc4a601795387292e2bd61826ed75310eb/regex-2022.10.31-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d403d781b0e06d2922435ce3b8d2376579f0c217ae491e273bab8d092727d244", size = 809109, upload-time = "2022-10-31T03:30:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/ad/56/c6344d2f3e170229fbd9e7928f85969084905e52ea06446f4d1763c712ce/regex-2022.10.31-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a37d51fa9a00d265cf73f3de3930fa9c41548177ba4f0faf76e61d512c774690", size = 795308, upload-time = "2022-10-31T03:30:12.711Z" }, + { url = "https://files.pythonhosted.org/packages/de/82/1e868572aaa6b5468f07512fd184650bf9ade15943d4f1ae83d0dc512872/regex-2022.10.31-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4f781ffedd17b0b834c8731b75cce2639d5a8afe961c1e58ee7f1f20b3af185", size = 769980, upload-time = "2022-10-31T03:30:15.424Z" }, + { url = "https://files.pythonhosted.org/packages/69/a4/d8cb52db0a918f8a1cad766c4bc5cf968b2a00a06183aa9b5f71ff6094e3/regex-2022.10.31-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d243b36fbf3d73c25e48014961e83c19c9cc92530516ce3c43050ea6276a2ab7", size = 757362, upload-time = "2022-10-31T03:30:19.225Z" }, + { url = "https://files.pythonhosted.org/packages/28/9c/e392e9aac4d4c10d81e0991e31e50755bd5f15a924284de4fac1d728b145/regex-2022.10.31-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:370f6e97d02bf2dd20d7468ce4f38e173a124e769762d00beadec3bc2f4b3bc4", size = 684624, upload-time = "2022-10-31T03:30:22.433Z" }, + { url = "https://files.pythonhosted.org/packages/4e/fa/efe2c65d2555a01c61a6522b63f98dd7f77dbfeea810e96d8f7e1d9552a3/regex-2022.10.31-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:597f899f4ed42a38df7b0e46714880fb4e19a25c2f66e5c908805466721760f5", size = 739283, upload-time = "2022-10-31T03:30:25.135Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a2/1c165d7759f501184214e788dccfc0bbca068eb70d6bc4fd7999712a2674/regex-2022.10.31-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7dbdce0c534bbf52274b94768b3498abdf675a691fec5f751b6057b3030f34c1", size = 727792, upload-time = "2022-10-31T03:30:28.094Z" }, + { url = "https://files.pythonhosted.org/packages/ec/26/6577862030d42967657f1132956c4600a95bb7e999741bfa32cc0c5441ff/regex-2022.10.31-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:22960019a842777a9fa5134c2364efaed5fbf9610ddc5c904bd3a400973b0eb8", size = 761812, upload-time = "2022-10-31T03:30:31.116Z" }, + { url = "https://files.pythonhosted.org/packages/cc/45/1ecb7ee4f479da2bc23e16a0266a90a5ecd918e1410d9188a1ae457f7c3e/regex-2022.10.31-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7f5a3ffc731494f1a57bd91c47dc483a1e10048131ffb52d901bfe2beb6102e8", size = 762574, upload-time = "2022-10-31T03:30:34.142Z" }, + { url = "https://files.pythonhosted.org/packages/48/4e/4c1e7dfab3255f4476faa11a9fcc867e03d2c4abb2e101505deb7ef790e0/regex-2022.10.31-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7ef6b5942e6bfc5706301a18a62300c60db9af7f6368042227ccb7eeb22d0892", size = 741567, upload-time = "2022-10-31T03:30:37.561Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ac/519de46093b4162e154f055ec020ba2f3641ba2cf6f1ddefd1abea5043b3/regex-2022.10.31-cp39-cp39-win32.whl", hash = "sha256:395161bbdbd04a8333b9ff9763a05e9ceb4fe210e3c7690f5e68cedd3d65d8e1", size = 255797, upload-time = "2022-10-31T03:30:40.759Z" }, + { url = "https://files.pythonhosted.org/packages/92/3c/17432c77b7d3929adb73077584606b236be4ed832243d426f51f5a0f72f9/regex-2022.10.31-cp39-cp39-win_amd64.whl", hash = "sha256:957403a978e10fb3ca42572a23e6f7badff39aa1ce2f4ade68ee452dc6807692", size = 267752, upload-time = "2022-10-31T03:30:44.09Z" }, +] + +[[package]] +name = "requests" +version = "2.31.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "certifi", marker = "python_full_version < '3.8'" }, + { name = "charset-normalizer", marker = "python_full_version < '3.8'" }, + { name = "idna", version = "3.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "urllib3", version = "2.0.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/be/10918a2eac4ae9f02f6cfe6414b7a155ccd8f7f9d4380d62fd5b955065c3/requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1", size = 110794, upload-time = "2023-05-22T15:12:44.175Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/8e/0e2d847013cb52cd35b38c009bb167a1a26b2ce6cd6965bf26b47bc0bf44/requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", size = 62574, upload-time = "2023-05-22T15:12:42.313Z" }, +] + +[[package]] +name = "requests" +version = "2.32.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +dependencies = [ + { name = "certifi", marker = "python_full_version == '3.8.*'" }, + { name = "charset-normalizer", marker = "python_full_version == '3.8.*'" }, + { name = "idna", version = "3.11", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "urllib3", version = "2.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "certifi", marker = "python_full_version >= '3.9'" }, + { name = "charset-normalizer", marker = "python_full_version >= '3.9'" }, + { name = "idna", version = "3.11", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +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/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.14.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/77/9a7fe084d268f8855d493e5031ea03fa0af8cc05887f638bf1c4e3363eb8/ruff-0.14.11.tar.gz", hash = "sha256:f6dc463bfa5c07a59b1ff2c3b9767373e541346ea105503b4c0369c520a66958", size = 5993417, upload-time = "2026-01-08T19:11:58.322Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/a6/a4c40a5aaa7e331f245d2dc1ac8ece306681f52b636b40ef87c88b9f7afd/ruff-0.14.11-py3-none-linux_armv6l.whl", hash = "sha256:f6ff2d95cbd335841a7217bdfd9c1d2e44eac2c584197ab1385579d55ff8830e", size = 12951208, upload-time = "2026-01-08T19:12:09.218Z" }, + { url = "https://files.pythonhosted.org/packages/5c/5c/360a35cb7204b328b685d3129c08aca24765ff92b5a7efedbdd6c150d555/ruff-0.14.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f6eb5c1c8033680f4172ea9c8d3706c156223010b8b97b05e82c59bdc774ee6", size = 13330075, upload-time = "2026-01-08T19:12:02.549Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9e/0cc2f1be7a7d33cae541824cf3f95b4ff40d03557b575912b5b70273c9ec/ruff-0.14.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f2fc34cc896f90080fca01259f96c566f74069a04b25b6205d55379d12a6855e", size = 12257809, upload-time = "2026-01-08T19:12:00.366Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e5/5faab97c15bb75228d9f74637e775d26ac703cc2b4898564c01ab3637c02/ruff-0.14.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53386375001773ae812b43205d6064dae49ff0968774e6befe16a994fc233caa", size = 12678447, upload-time = "2026-01-08T19:12:13.899Z" }, + { url = "https://files.pythonhosted.org/packages/1b/33/e9767f60a2bef779fb5855cab0af76c488e0ce90f7bb7b8a45c8a2ba4178/ruff-0.14.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a697737dce1ca97a0a55b5ff0434ee7205943d4874d638fe3ae66166ff46edbe", size = 12758560, upload-time = "2026-01-08T19:11:42.55Z" }, + { url = "https://files.pythonhosted.org/packages/eb/84/4c6cf627a21462bb5102f7be2a320b084228ff26e105510cd2255ea868e5/ruff-0.14.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6845ca1da8ab81ab1dce755a32ad13f1db72e7fba27c486d5d90d65e04d17b8f", size = 13599296, upload-time = "2026-01-08T19:11:30.371Z" }, + { url = "https://files.pythonhosted.org/packages/88/e1/92b5ed7ea66d849f6157e695dc23d5d6d982bd6aa8d077895652c38a7cae/ruff-0.14.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e36ce2fd31b54065ec6f76cb08d60159e1b32bdf08507862e32f47e6dde8bcbf", size = 15048981, upload-time = "2026-01-08T19:12:04.742Z" }, + { url = "https://files.pythonhosted.org/packages/61/df/c1bd30992615ac17c2fb64b8a7376ca22c04a70555b5d05b8f717163cf9f/ruff-0.14.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:590bcc0e2097ecf74e62a5c10a6b71f008ad82eb97b0a0079e85defe19fe74d9", size = 14633183, upload-time = "2026-01-08T19:11:40.069Z" }, + { url = "https://files.pythonhosted.org/packages/04/e9/fe552902f25013dd28a5428a42347d9ad20c4b534834a325a28305747d64/ruff-0.14.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53fe71125fc158210d57fe4da26e622c9c294022988d08d9347ec1cf782adafe", size = 14050453, upload-time = "2026-01-08T19:11:37.555Z" }, + { url = "https://files.pythonhosted.org/packages/ae/93/f36d89fa021543187f98991609ce6e47e24f35f008dfe1af01379d248a41/ruff-0.14.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a35c9da08562f1598ded8470fcfef2afb5cf881996e6c0a502ceb61f4bc9c8a3", size = 13757889, upload-time = "2026-01-08T19:12:07.094Z" }, + { url = "https://files.pythonhosted.org/packages/b7/9f/c7fb6ecf554f28709a6a1f2a7f74750d400979e8cd47ed29feeaa1bd4db8/ruff-0.14.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0f3727189a52179393ecf92ec7057c2210203e6af2676f08d92140d3e1ee72c1", size = 13955832, upload-time = "2026-01-08T19:11:55.064Z" }, + { url = "https://files.pythonhosted.org/packages/db/a0/153315310f250f76900a98278cf878c64dfb6d044e184491dd3289796734/ruff-0.14.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:eb09f849bd37147a789b85995ff734a6c4a095bed5fd1608c4f56afc3634cde2", size = 12586522, upload-time = "2026-01-08T19:11:35.356Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2b/a73a2b6e6d2df1d74bf2b78098be1572191e54bec0e59e29382d13c3adc5/ruff-0.14.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c61782543c1231bf71041461c1f28c64b961d457d0f238ac388e2ab173d7ecb7", size = 12724637, upload-time = "2026-01-08T19:11:47.796Z" }, + { url = "https://files.pythonhosted.org/packages/f0/41/09100590320394401cd3c48fc718a8ba71c7ddb1ffd07e0ad6576b3a3df2/ruff-0.14.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:82ff352ea68fb6766140381748e1f67f83c39860b6446966cff48a315c3e2491", size = 13145837, upload-time = "2026-01-08T19:11:32.87Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d8/e035db859d1d3edf909381eb8ff3e89a672d6572e9454093538fe6f164b0/ruff-0.14.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:728e56879df4ca5b62a9dde2dd0eb0edda2a55160c0ea28c4025f18c03f86984", size = 13850469, upload-time = "2026-01-08T19:12:11.694Z" }, + { url = "https://files.pythonhosted.org/packages/4e/02/bb3ff8b6e6d02ce9e3740f4c17dfbbfb55f34c789c139e9cd91985f356c7/ruff-0.14.11-py3-none-win32.whl", hash = "sha256:337c5dd11f16ee52ae217757d9b82a26400be7efac883e9e852646f1557ed841", size = 12851094, upload-time = "2026-01-08T19:11:45.163Z" }, + { url = "https://files.pythonhosted.org/packages/58/f1/90ddc533918d3a2ad628bc3044cdfc094949e6d4b929220c3f0eb8a1c998/ruff-0.14.11-py3-none-win_amd64.whl", hash = "sha256:f981cea63d08456b2c070e64b79cb62f951aa1305282974d4d5216e6e0178ae6", size = 14001379, upload-time = "2026-01-08T19:11:52.591Z" }, + { url = "https://files.pythonhosted.org/packages/c4/1c/1dbe51782c0e1e9cfce1d1004752672d2d4629ea46945d19d731ad772b3b/ruff-0.14.11-py3-none-win_arm64.whl", hash = "sha256:649fb6c9edd7f751db276ef42df1f3df41c38d67d199570ae2a7bd6cbc3590f0", size = 12938644, upload-time = "2026-01-08T19:11:50.027Z" }, +] + +[[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, 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, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "tomli" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/3f/d7af728f075fb08564c5949a9c95e44352e23dee646869fa104a3b2060a3/tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f", size = 15164, upload-time = "2022-02-08T10:54:04.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", size = 12757, upload-time = "2022-02-08T10:54:02.017Z" }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] + +[[package]] +name = "typed-ast" +version = "1.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/7e/a424029f350aa8078b75fd0d360a787a273ca753a678d1104c5fa4f3072a/typed_ast-1.5.5.tar.gz", hash = "sha256:94282f7a354f36ef5dbce0ef3467ebf6a258e370ab33d5b40c249fa996e590dd", size = 252841, upload-time = "2023-07-04T18:38:08.524Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/07/5defe18d4fc16281cd18c4374270abc430c3d852d8ac29b5db6599d45cfe/typed_ast-1.5.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4bc1efe0ce3ffb74784e06460f01a223ac1f6ab31c6bc0376a21184bf5aabe3b", size = 223267, upload-time = "2023-07-04T18:37:00.129Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5c/e379b00028680bfcd267d845cf46b60e76d8ac6f7009fd440d6ce030cc92/typed_ast-1.5.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f7a8c46a8b333f71abd61d7ab9255440d4a588f34a21f126bbfc95f6049e686", size = 208260, upload-time = "2023-07-04T18:37:03.069Z" }, + { url = "https://files.pythonhosted.org/packages/3b/99/5cc31ef4f3c80e1ceb03ed2690c7085571e3fbf119cbd67a111ec0b6622f/typed_ast-1.5.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597fc66b4162f959ee6a96b978c0435bd63791e31e4f410622d19f1686d5e769", size = 842272, upload-time = "2023-07-04T18:37:04.916Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ed/b9b8b794b37b55c9247b1e8d38b0361e8158795c181636d34d6c11b506e7/typed_ast-1.5.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d41b7a686ce653e06c2609075d397ebd5b969d821b9797d029fccd71fdec8e04", size = 824651, upload-time = "2023-07-04T18:37:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/ca/59/dbbbe5a0e91c15d14a0896b539a5ed01326b0d468e75c1a33274d128d2d1/typed_ast-1.5.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5fe83a9a44c4ce67c796a1b466c270c1272e176603d5e06f6afbc101a572859d", size = 854960, upload-time = "2023-07-04T18:37:08.474Z" }, + { url = "https://files.pythonhosted.org/packages/90/f0/0956d925f87bd81f6e0f8cf119eac5e5c8f4da50ca25bb9f5904148d4611/typed_ast-1.5.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d5c0c112a74c0e5db2c75882a0adf3133adedcdbfd8cf7c9d6ed77365ab90a1d", size = 839321, upload-time = "2023-07-04T18:37:10.417Z" }, + { url = "https://files.pythonhosted.org/packages/43/17/4bdece9795da6f3345c4da5667ac64bc25863617f19c28d81f350f515be6/typed_ast-1.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:e1a976ed4cc2d71bb073e1b2a250892a6e968ff02aa14c1f40eba4f365ffec02", size = 139380, upload-time = "2023-07-04T18:37:12.157Z" }, + { url = "https://files.pythonhosted.org/packages/75/53/b685e10da535c7b3572735f8bea0d4abb35a04722a7d44ca9c163a0cf822/typed_ast-1.5.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c631da9710271cb67b08bd3f3813b7af7f4c69c319b75475436fcab8c3d21bee", size = 223264, upload-time = "2023-07-04T18:37:13.637Z" }, + { url = "https://files.pythonhosted.org/packages/96/fd/fc8ccf19fc16a40a23e7c7802d0abc78c1f38f1abb6e2447c474f8a076d8/typed_ast-1.5.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b445c2abfecab89a932b20bd8261488d574591173d07827c1eda32c457358b18", size = 208158, upload-time = "2023-07-04T18:37:15.141Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9a/598e47f2c3ecd19d7f1bb66854d0d3ba23ffd93c846448790a92524b0a8d/typed_ast-1.5.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc95ffaaab2be3b25eb938779e43f513e0e538a84dd14a5d844b8f2932593d88", size = 878366, upload-time = "2023-07-04T18:37:16.614Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/765e8bf8b24d0ed7b9fc669f6826c5bc3eb7412fc765691f59b83ae195b2/typed_ast-1.5.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61443214d9b4c660dcf4b5307f15c12cb30bdfe9588ce6158f4a005baeb167b2", size = 860314, upload-time = "2023-07-04T18:37:18.215Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3c/4af750e6c673a0dd6c7b9f5b5e5ed58ec51a2e4e744081781c664d369dfa/typed_ast-1.5.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6eb936d107e4d474940469e8ec5b380c9b329b5f08b78282d46baeebd3692dc9", size = 898108, upload-time = "2023-07-04T18:37:20.095Z" }, + { url = "https://files.pythonhosted.org/packages/03/8d/d0a4d1e060e1e8dda2408131a0cc7633fc4bc99fca5941dcb86c461dfe01/typed_ast-1.5.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e48bf27022897577d8479eaed64701ecaf0467182448bd95759883300ca818c8", size = 881971, upload-time = "2023-07-04T18:37:21.912Z" }, + { url = "https://files.pythonhosted.org/packages/90/83/f28d2c912cd010a09b3677ac69d23181045eb17e358914ab739b7fdee530/typed_ast-1.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:83509f9324011c9a39faaef0922c6f720f9623afe3fe220b6d0b15638247206b", size = 139286, upload-time = "2023-07-04T18:37:23.625Z" }, + { url = "https://files.pythonhosted.org/packages/d5/00/635353c31b71ed307ab020eff6baed9987da59a1b2ba489f885ecbe293b8/typed_ast-1.5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2188bc33d85951ea4ddad55d2b35598b2709d122c11c75cffd529fbc9965508e", size = 222315, upload-time = "2023-07-04T18:37:36.008Z" }, + { url = "https://files.pythonhosted.org/packages/01/95/11be104446bb20212a741d30d40eab52a9cfc05ea34efa074ff4f7c16983/typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0635900d16ae133cab3b26c607586131269f88266954eb04ec31535c9a12ef1e", size = 793541, upload-time = "2023-07-04T18:37:37.614Z" }, + { url = "https://files.pythonhosted.org/packages/32/f1/75bd58fb1410cb72fbc6e8adf163015720db2c38844b46a9149c5ff6bf38/typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57bfc3cf35a0f2fdf0a88a3044aafaec1d2f24d8ae8cd87c4f58d615fb5b6311", size = 778348, upload-time = "2023-07-04T18:37:39.332Z" }, + { url = "https://files.pythonhosted.org/packages/47/97/0bb4dba688a58ff9c08e63b39653e4bcaa340ce1bb9c1d58163e5c2c66f1/typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:fe58ef6a764de7b4b36edfc8592641f56e69b7163bba9f9c8089838ee596bfb2", size = 809447, upload-time = "2023-07-04T18:37:41.017Z" }, + { url = "https://files.pythonhosted.org/packages/a8/cd/9a867f5a96d83a9742c43914e10d3a2083d8fe894ab9bf60fd467c6c497f/typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d09d930c2d1d621f717bb217bf1fe2584616febb5138d9b3e8cdd26506c3f6d4", size = 796707, upload-time = "2023-07-04T18:37:42.625Z" }, + { url = "https://files.pythonhosted.org/packages/eb/06/73ca55ee5303b41d08920de775f02d2a3e1e59430371f5adf7fbb1a21127/typed_ast-1.5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:d40c10326893ecab8a80a53039164a224984339b2c32a6baf55ecbd5b1df6431", size = 138403, upload-time = "2023-07-04T18:37:44.399Z" }, + { url = "https://files.pythonhosted.org/packages/19/e3/88b65e46643006592f39e0fdef3e29454244a9fdaa52acfb047dc68cae6a/typed_ast-1.5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fd946abf3c31fb50eee07451a6aedbfff912fcd13cf357363f5b4e834cc5e71a", size = 222951, upload-time = "2023-07-04T18:37:45.745Z" }, + { url = "https://files.pythonhosted.org/packages/15/e0/182bdd9edb6c6a1c068cecaa87f58924a817f2807a0b0d940f578b3328df/typed_ast-1.5.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ed4a1a42df8a3dfb6b40c3d2de109e935949f2f66b19703eafade03173f8f437", size = 208247, upload-time = "2023-07-04T18:37:47.28Z" }, + { url = "https://files.pythonhosted.org/packages/8d/09/bba083f2c11746288eaf1859e512130420405033de84189375fe65d839ba/typed_ast-1.5.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:045f9930a1550d9352464e5149710d56a2aed23a2ffe78946478f7b5416f1ede", size = 861010, upload-time = "2023-07-04T18:37:48.847Z" }, + { url = "https://files.pythonhosted.org/packages/31/f3/38839df509b04fb54205e388fc04b47627377e0ad628870112086864a441/typed_ast-1.5.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:381eed9c95484ceef5ced626355fdc0765ab51d8553fec08661dce654a935db4", size = 840026, upload-time = "2023-07-04T18:37:50.631Z" }, + { url = "https://files.pythonhosted.org/packages/45/1e/aa5f1dae4b92bc665ae9a655787bb2fe007a881fa2866b0408ce548bb24c/typed_ast-1.5.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bfd39a41c0ef6f31684daff53befddae608f9daf6957140228a08e51f312d7e6", size = 875615, upload-time = "2023-07-04T18:37:52.27Z" }, + { url = "https://files.pythonhosted.org/packages/94/88/71a1c249c01fbbd66f9f28648f8249e737a7fe19056c1a78e7b3b9250eb1/typed_ast-1.5.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8c524eb3024edcc04e288db9541fe1f438f82d281e591c548903d5b77ad1ddd4", size = 858320, upload-time = "2023-07-04T18:37:54.23Z" }, + { url = "https://files.pythonhosted.org/packages/12/1e/19f53aad3984e351e6730e4265fde4b949a66c451e10828fdbc4dfb050f1/typed_ast-1.5.5-cp38-cp38-win_amd64.whl", hash = "sha256:7f58fabdde8dcbe764cef5e1a7fcb440f2463c1bbbec1cf2a86ca7bc1f95184b", size = 139414, upload-time = "2023-07-04T18:37:55.912Z" }, + { url = "https://files.pythonhosted.org/packages/b1/88/6e7f36f5fab6fbf0586a2dd866ac337924b7d4796a4d1b2b04443a864faf/typed_ast-1.5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:042eb665ff6bf020dd2243307d11ed626306b82812aba21836096d229fdc6a10", size = 223329, upload-time = "2023-07-04T18:37:57.344Z" }, + { url = "https://files.pythonhosted.org/packages/71/30/09d27e13824495547bcc665bd07afc593b22b9484f143b27565eae4ccaac/typed_ast-1.5.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:622e4a006472b05cf6ef7f9f2636edc51bda670b7bbffa18d26b255269d3d814", size = 208314, upload-time = "2023-07-04T18:37:59.073Z" }, + { url = "https://files.pythonhosted.org/packages/07/3d/564308b7a432acb1f5399933cbb1b376a1a64d2544b90f6ba91894674260/typed_ast-1.5.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1efebbbf4604ad1283e963e8915daa240cb4bf5067053cf2f0baadc4d4fb51b8", size = 840900, upload-time = "2023-07-04T18:38:00.562Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f4/262512d14f777ea3666a089e2675a9b1500a85b8329a36de85d63433fb0e/typed_ast-1.5.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0aefdd66f1784c58f65b502b6cf8b121544680456d1cebbd300c2c813899274", size = 823435, upload-time = "2023-07-04T18:38:02.532Z" }, + { url = "https://files.pythonhosted.org/packages/a1/25/b3ccb948166d309ab75296ac9863ebe2ff209fbc063f1122a2d3979e47c3/typed_ast-1.5.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:48074261a842acf825af1968cd912f6f21357316080ebaca5f19abbb11690c8a", size = 853125, upload-time = "2023-07-04T18:38:04.128Z" }, + { url = "https://files.pythonhosted.org/packages/1c/09/012da182242f168bb5c42284297dcc08dc0a1b3668db5b3852aec467f56f/typed_ast-1.5.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:429ae404f69dc94b9361bb62291885894b7c6fb4640d561179548c849f8492ba", size = 837280, upload-time = "2023-07-04T18:38:05.968Z" }, + { url = "https://files.pythonhosted.org/packages/30/bd/c815051404c4293265634d9d3e292f04fcf681d0502a9484c38b8f224d04/typed_ast-1.5.5-cp39-cp39-win_amd64.whl", hash = "sha256:335f22ccb244da2b5c296e6f96b06ee9bed46526db0de38d2f0e5a6597b81155", size = 139486, upload-time = "2023-07-04T18:38:07.249Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.7.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/8b/0111dd7d6c1478bf83baa1cab85c686426c7a6274119aceb2bd9d35395ad/typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2", size = 72876, upload-time = "2023-07-02T14:20:55.045Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/6b/63cc3df74987c36fe26157ee12e09e8f9db4de771e0f3404263117e75b95/typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36", size = 33232, upload-time = "2023-07-02T14:20:53.275Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +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/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.0.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://files.pythonhosted.org/packages/af/47/b215df9f71b4fdba1025fc05a77db2ad243fa0926755a52c5e71659f4e3c/urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84", size = 282546, upload-time = "2023-10-17T17:46:50.542Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/b2/b157855192a68541a91ba7b2bbcb91f1b4faa51f8bae38d8005c034be524/urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e", size = 124213, upload-time = "2023-10-17T17:46:48.538Z" }, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677, upload-time = "2024-09-12T10:52:18.401Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338, upload-time = "2024-09-12T10:52:16.589Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +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/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]] +name = "watchdog" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://files.pythonhosted.org/packages/95/a6/d6ef450393dac5734c63c40a131f66808d2e6f59f6165ab38c98fbe4e6ec/watchdog-3.0.0.tar.gz", hash = "sha256:4d98a320595da7a7c5a18fc48cb633c2e73cda78f93cac2ef42d42bf609a33f9", size = 124593, upload-time = "2023-03-20T09:21:11.367Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/fd/58b82550ebe4883bb2a5e1b6c14d8702b5ce0f36c58470bba51dc777df46/watchdog-3.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:336adfc6f5cc4e037d52db31194f7581ff744b67382eb6021c868322e32eef41", size = 100697, upload-time = "2023-03-20T09:20:25.047Z" }, + { url = "https://files.pythonhosted.org/packages/92/dd/42f47ffdfadff4c41b89c54163f323f875eb963bf90088e477c43b8f7b15/watchdog-3.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a70a8dcde91be523c35b2bf96196edc5730edb347e374c7de7cd20c43ed95397", size = 91219, upload-time = "2023-03-20T09:20:26.864Z" }, + { url = "https://files.pythonhosted.org/packages/9b/39/30bb3c2e4f8e89b5c60e98589acf5c5a001cb0efde249aa05d748d1734a2/watchdog-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:adfdeab2da79ea2f76f87eb42a3ab1966a5313e5a69a0213a3cc06ef692b0e96", size = 91756, upload-time = "2023-03-20T09:20:28.309Z" }, + { url = "https://files.pythonhosted.org/packages/00/9e/a9711f35f1ad6571e92dc2e955e7de9dfac21a1b33e9cd212f066a60a387/watchdog-3.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b57a1e730af3156d13b7fdddfc23dea6487fceca29fc75c5a868beed29177ae", size = 100700, upload-time = "2023-03-20T09:20:29.847Z" }, + { url = "https://files.pythonhosted.org/packages/84/ab/67001e62603bf2ea35ace40023f7c74f61e8b047160d6bb078373cec1a67/watchdog-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7ade88d0d778b1b222adebcc0927428f883db07017618a5e684fd03b83342bd9", size = 91251, upload-time = "2023-03-20T09:20:31.892Z" }, + { url = "https://files.pythonhosted.org/packages/58/db/d419fdbd3051b42b0a8091ddf78f70540b6d9d277a84845f7c5955f9de92/watchdog-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7e447d172af52ad204d19982739aa2346245cc5ba6f579d16dac4bfec226d2e7", size = 91753, upload-time = "2023-03-20T09:20:33.337Z" }, + { url = "https://files.pythonhosted.org/packages/3a/9d/d6586a065968f3e5d89a2565dffa6ea9151ce9d46c541340bfff27b41231/watchdog-3.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9fac43a7466eb73e64a9940ac9ed6369baa39b3bf221ae23493a9ec4d0022674", size = 91185, upload-time = "2023-03-20T09:20:35.407Z" }, + { url = "https://files.pythonhosted.org/packages/7f/6e/7ca8ed16928d7b11da69372f55c64a09dce649d2b24b03f7063cd8683c4b/watchdog-3.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8ae9cda41fa114e28faf86cb137d751a17ffd0316d1c34ccf2235e8a84365c7f", size = 100655, upload-time = "2023-03-20T09:20:37.473Z" }, + { url = "https://files.pythonhosted.org/packages/2e/54/48527f3aea4f7ed331072352fee034a7f3d6ec7a2ed873681738b2586498/watchdog-3.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:25f70b4aa53bd743729c7475d7ec41093a580528b100e9a8c5b5efe8899592fc", size = 91216, upload-time = "2023-03-20T09:20:39.793Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/3a3ce6dd01807ff918aec3bbcabc92ed1a7edc5bb2266c720bb39fec1bec/watchdog-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4f94069eb16657d2c6faada4624c39464f65c05606af50bb7902e036e3219be3", size = 91752, upload-time = "2023-03-20T09:20:41.395Z" }, + { url = "https://files.pythonhosted.org/packages/75/fe/d9a37d8df76878853f68dd665ec6d2c7a984645de460164cb880a93ffe6b/watchdog-3.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7c5f84b5194c24dd573fa6472685b2a27cc5a17fe5f7b6fd40345378ca6812e3", size = 100653, upload-time = "2023-03-20T09:20:42.936Z" }, + { url = "https://files.pythonhosted.org/packages/94/ce/70c65a6c4b0330129c402624d42f67ce82d6a0ba2036de67628aeffda3c1/watchdog-3.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3aa7f6a12e831ddfe78cdd4f8996af9cf334fd6346531b16cec61c3b3c0d8da0", size = 91247, upload-time = "2023-03-20T09:20:45.157Z" }, + { url = "https://files.pythonhosted.org/packages/51/b9/444a984b1667013bac41b31b45d9718e069cc7502a43a924896806605d83/watchdog-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:233b5817932685d39a7896b1090353fc8efc1ef99c9c054e46c8002561252fb8", size = 91753, upload-time = "2023-03-20T09:20:46.913Z" }, + { url = "https://files.pythonhosted.org/packages/67/e4/229144d23093436a21a8b84aa5931d70759b81743dc8c10d0e836dbfd752/watchdog-3.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:13bbbb462ee42ec3c5723e1205be8ced776f05b100e4737518c67c8325cf6100", size = 90424, upload-time = "2023-03-20T09:20:49.103Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/bef1c6f6ac18041234a9f3e8bc995d611e255c44f10433bfaf255968c269/watchdog-3.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8f3ceecd20d71067c7fd4c9e832d4e22584318983cabc013dbf3f70ea95de346", size = 90419, upload-time = "2023-03-20T09:20:50.715Z" }, + { url = "https://files.pythonhosted.org/packages/30/65/9e36a3c821d47a22e54a8fc73681586b2d26e82d24ea3af63acf2ef78f97/watchdog-3.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c9d8c8ec7efb887333cf71e328e39cffbf771d8f8f95d308ea4125bf5f90ba64", size = 90428, upload-time = "2023-03-20T09:20:52.216Z" }, + { url = "https://files.pythonhosted.org/packages/92/28/631872d7fbc45527037060db8c838b47a129a6c09d2297d6dddcfa283cf2/watchdog-3.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0e06ab8858a76e1219e68c7573dfeba9dd1c0219476c5a44d5333b01d7e1743a", size = 82049, upload-time = "2023-03-20T09:20:53.951Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a2/4e3230bdc1fb878b152a2c66aa941732776f4545bd68135d490591d66713/watchdog-3.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:d00e6be486affb5781468457b21a6cbe848c33ef43f9ea4a73b4882e5f188a44", size = 82049, upload-time = "2023-03-20T09:20:55.583Z" }, + { url = "https://files.pythonhosted.org/packages/21/72/46fd174352cd88b9157ade77e3b8835125d4b1e5186fc7f1e8c44664e029/watchdog-3.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:c07253088265c363d1ddf4b3cdb808d59a0468ecd017770ed716991620b8f77a", size = 82052, upload-time = "2023-03-20T09:20:57.124Z" }, + { url = "https://files.pythonhosted.org/packages/74/3c/e4b77f4f069aca2b6e35925db7a1aa6cb600dcb52fc3e962284640ca37f3/watchdog-3.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:5113334cf8cf0ac8cd45e1f8309a603291b614191c9add34d33075727a967709", size = 82050, upload-time = "2023-03-20T09:20:58.864Z" }, + { url = "https://files.pythonhosted.org/packages/71/3a/b12740f4f60861240d57b42a2ac6ac0a2821db506c4435f7872c1fad867d/watchdog-3.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:51f90f73b4697bac9c9a78394c3acbbd331ccd3655c11be1a15ae6fe289a8c83", size = 82050, upload-time = "2023-03-20T09:21:00.452Z" }, + { url = "https://files.pythonhosted.org/packages/40/1b/4e6d3e0f587587931f590531b4ed08070d71a9efb35541d792a68d8ee593/watchdog-3.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:ba07e92756c97e3aca0912b5cbc4e5ad802f4557212788e72a72a47ff376950d", size = 82049, upload-time = "2023-03-20T09:21:01.979Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f0/456948b865ab259784f774154e7d65844fa9757522fdb11533fbf8ae7aca/watchdog-3.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:d429c2430c93b7903914e4db9a966c7f2b068dd2ebdd2fa9b9ce094c7d459f33", size = 82051, upload-time = "2023-03-20T09:21:03.67Z" }, + { url = "https://files.pythonhosted.org/packages/55/0d/bfc2a0d425b12444a2dc245a934c065bbb7bd9833fff071cba79c21bb76e/watchdog-3.0.0-py3-none-win32.whl", hash = "sha256:3ed7c71a9dccfe838c2f0b6314ed0d9b22e77d268c67e015450a29036a81f60f", size = 82038, upload-time = "2023-03-20T09:21:05.492Z" }, + { url = "https://files.pythonhosted.org/packages/9b/6e/ce8d124d03cd3f2941365d9c81d62e3afe43f2dc7e6e86274fa9c2ec2d5b/watchdog-3.0.0-py3-none-win_amd64.whl", hash = "sha256:4c9956d27be0bb08fc5f30d9d0179a855436e655f046d288e2bcc11adfae893c", size = 82040, upload-time = "2023-03-20T09:21:07.609Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0c/cd0337069c468f22ef256e768ece74c78b511092f1004ab260268e1af4a9/watchdog-3.0.0-py3-none-win_ia64.whl", hash = "sha256:5d9f3a10e02d7371cd929b5d8f11e87d4bad890212ed3901f9b4d68767bee759", size = 82040, upload-time = "2023-03-20T09:21:09.178Z" }, +] + +[[package]] +name = "watchdog" +version = "4.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/4f/38/764baaa25eb5e35c9a043d4c4588f9836edfe52a708950f4b6d5f714fd42/watchdog-4.0.2.tar.gz", hash = "sha256:b4dfbb6c49221be4535623ea4474a4d6ee0a9cef4a80b20c28db4d858b64e270", size = 126587, upload-time = "2024-08-11T07:38:01.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/b0/219893d41c16d74d0793363bf86df07d50357b81f64bba4cb94fe76e7af4/watchdog-4.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ede7f010f2239b97cc79e6cb3c249e72962404ae3865860855d5cbe708b0fd22", size = 100257, upload-time = "2024-08-11T07:37:04.209Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c6/8e90c65693e87d98310b2e1e5fd7e313266990853b489e85ce8396cc26e3/watchdog-4.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a2cffa171445b0efa0726c561eca9a27d00a1f2b83846dbd5a4f639c4f8ca8e1", size = 92249, upload-time = "2024-08-11T07:37:06.364Z" }, + { url = "https://files.pythonhosted.org/packages/6f/cd/2e306756364a934532ff8388d90eb2dc8bb21fe575cd2b33d791ce05a02f/watchdog-4.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c50f148b31b03fbadd6d0b5980e38b558046b127dc483e5e4505fcef250f9503", size = 92888, upload-time = "2024-08-11T07:37:08.275Z" }, + { url = "https://files.pythonhosted.org/packages/de/78/027ad372d62f97642349a16015394a7680530460b1c70c368c506cb60c09/watchdog-4.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7c7d4bf585ad501c5f6c980e7be9c4f15604c7cc150e942d82083b31a7548930", size = 100256, upload-time = "2024-08-11T07:37:11.017Z" }, + { url = "https://files.pythonhosted.org/packages/59/a9/412b808568c1814d693b4ff1cec0055dc791780b9dc947807978fab86bc1/watchdog-4.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:914285126ad0b6eb2258bbbcb7b288d9dfd655ae88fa28945be05a7b475a800b", size = 92252, upload-time = "2024-08-11T07:37:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/04/57/179d76076cff264982bc335dd4c7da6d636bd3e9860bbc896a665c3447b6/watchdog-4.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:984306dc4720da5498b16fc037b36ac443816125a3705dfde4fd90652d8028ef", size = 92888, upload-time = "2024-08-11T07:37:15.077Z" }, + { url = "https://files.pythonhosted.org/packages/92/f5/ea22b095340545faea37ad9a42353b265ca751f543da3fb43f5d00cdcd21/watchdog-4.0.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1cdcfd8142f604630deef34722d695fb455d04ab7cfe9963055df1fc69e6727a", size = 100342, upload-time = "2024-08-11T07:37:16.393Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d2/8ce97dff5e465db1222951434e3115189ae54a9863aef99c6987890cc9ef/watchdog-4.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d7ab624ff2f663f98cd03c8b7eedc09375a911794dfea6bf2a359fcc266bff29", size = 92306, upload-time = "2024-08-11T07:37:17.997Z" }, + { url = "https://files.pythonhosted.org/packages/49/c4/1aeba2c31b25f79b03b15918155bc8c0b08101054fc727900f1a577d0d54/watchdog-4.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:132937547a716027bd5714383dfc40dc66c26769f1ce8a72a859d6a48f371f3a", size = 92915, upload-time = "2024-08-11T07:37:19.967Z" }, + { url = "https://files.pythonhosted.org/packages/79/63/eb8994a182672c042d85a33507475c50c2ee930577524dd97aea05251527/watchdog-4.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd67c7df93eb58f360c43802acc945fa8da70c675b6fa37a241e17ca698ca49b", size = 100343, upload-time = "2024-08-11T07:37:21.935Z" }, + { url = "https://files.pythonhosted.org/packages/ce/82/027c0c65c2245769580605bcd20a1dc7dfd6c6683c8c4e2ef43920e38d27/watchdog-4.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcfd02377be80ef3b6bc4ce481ef3959640458d6feaae0bd43dd90a43da90a7d", size = 92313, upload-time = "2024-08-11T07:37:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/2a/89/ad4715cbbd3440cb0d336b78970aba243a33a24b1a79d66f8d16b4590d6a/watchdog-4.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:980b71510f59c884d684b3663d46e7a14b457c9611c481e5cef08f4dd022eed7", size = 92919, upload-time = "2024-08-11T07:37:24.715Z" }, + { url = "https://files.pythonhosted.org/packages/55/08/1a9086a3380e8828f65b0c835b86baf29ebb85e5e94a2811a2eb4f889cfd/watchdog-4.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:aa160781cafff2719b663c8a506156e9289d111d80f3387cf3af49cedee1f040", size = 100255, upload-time = "2024-08-11T07:37:26.862Z" }, + { url = "https://files.pythonhosted.org/packages/6c/3e/064974628cf305831f3f78264800bd03b3358ec181e3e9380a36ff156b93/watchdog-4.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f6ee8dedd255087bc7fe82adf046f0b75479b989185fb0bdf9a98b612170eac7", size = 92257, upload-time = "2024-08-11T07:37:28.253Z" }, + { url = "https://files.pythonhosted.org/packages/23/69/1d2ad9c12d93bc1e445baa40db46bc74757f3ffc3a3be592ba8dbc51b6e5/watchdog-4.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0b4359067d30d5b864e09c8597b112fe0a0a59321a0f331498b013fb097406b4", size = 92886, upload-time = "2024-08-11T07:37:29.52Z" }, + { url = "https://files.pythonhosted.org/packages/68/eb/34d3173eceab490d4d1815ba9a821e10abe1da7a7264a224e30689b1450c/watchdog-4.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:770eef5372f146997638d737c9a3c597a3b41037cfbc5c41538fc27c09c3a3f9", size = 100254, upload-time = "2024-08-11T07:37:30.888Z" }, + { url = "https://files.pythonhosted.org/packages/18/a1/4bbafe7ace414904c2cc9bd93e472133e8ec11eab0b4625017f0e34caad8/watchdog-4.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eeea812f38536a0aa859972d50c76e37f4456474b02bd93674d1947cf1e39578", size = 92249, upload-time = "2024-08-11T07:37:32.193Z" }, + { url = "https://files.pythonhosted.org/packages/f3/11/ec5684e0ca692950826af0de862e5db167523c30c9cbf9b3f4ce7ec9cc05/watchdog-4.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b2c45f6e1e57ebb4687690c05bc3a2c1fb6ab260550c4290b8abb1335e0fd08b", size = 92891, upload-time = "2024-08-11T07:37:34.212Z" }, + { url = "https://files.pythonhosted.org/packages/3b/9a/6f30f023324de7bad8a3eb02b0afb06bd0726003a3550e9964321315df5a/watchdog-4.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:10b6683df70d340ac3279eff0b2766813f00f35a1d37515d2c99959ada8f05fa", size = 91775, upload-time = "2024-08-11T07:37:35.567Z" }, + { url = "https://files.pythonhosted.org/packages/87/62/8be55e605d378a154037b9ba484e00a5478e627b69c53d0f63e3ef413ba6/watchdog-4.0.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:f7c739888c20f99824f7aa9d31ac8a97353e22d0c0e54703a547a218f6637eb3", size = 92255, upload-time = "2024-08-11T07:37:37.596Z" }, + { url = "https://files.pythonhosted.org/packages/6b/59/12e03e675d28f450bade6da6bc79ad6616080b317c472b9ae688d2495a03/watchdog-4.0.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c100d09ac72a8a08ddbf0629ddfa0b8ee41740f9051429baa8e31bb903ad7508", size = 91682, upload-time = "2024-08-11T07:37:38.901Z" }, + { url = "https://files.pythonhosted.org/packages/ef/69/241998de9b8e024f5c2fbdf4324ea628b4231925305011ca8b7e1c3329f6/watchdog-4.0.2-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:f5315a8c8dd6dd9425b974515081fc0aadca1d1d61e078d2246509fd756141ee", size = 92249, upload-time = "2024-08-11T07:37:40.143Z" }, + { url = "https://files.pythonhosted.org/packages/70/3f/2173b4d9581bc9b5df4d7f2041b6c58b5e5448407856f68d4be9981000d0/watchdog-4.0.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:2d468028a77b42cc685ed694a7a550a8d1771bb05193ba7b24006b8241a571a1", size = 91773, upload-time = "2024-08-11T07:37:42.095Z" }, + { url = "https://files.pythonhosted.org/packages/f0/de/6fff29161d5789048f06ef24d94d3ddcc25795f347202b7ea503c3356acb/watchdog-4.0.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f15edcae3830ff20e55d1f4e743e92970c847bcddc8b7509bcd172aa04de506e", size = 92250, upload-time = "2024-08-11T07:37:44.052Z" }, + { url = "https://files.pythonhosted.org/packages/8a/b1/25acf6767af6f7e44e0086309825bd8c098e301eed5868dc5350642124b9/watchdog-4.0.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:936acba76d636f70db8f3c66e76aa6cb5136a936fc2a5088b9ce1c7a3508fc83", size = 82947, upload-time = "2024-08-11T07:37:45.388Z" }, + { url = "https://files.pythonhosted.org/packages/e8/90/aebac95d6f954bd4901f5d46dcd83d68e682bfd21798fd125a95ae1c9dbf/watchdog-4.0.2-py3-none-manylinux2014_armv7l.whl", hash = "sha256:e252f8ca942a870f38cf785aef420285431311652d871409a64e2a0a52a2174c", size = 82942, upload-time = "2024-08-11T07:37:46.722Z" }, + { url = "https://files.pythonhosted.org/packages/15/3a/a4bd8f3b9381824995787488b9282aff1ed4667e1110f31a87b871ea851c/watchdog-4.0.2-py3-none-manylinux2014_i686.whl", hash = "sha256:0e83619a2d5d436a7e58a1aea957a3c1ccbf9782c43c0b4fed80580e5e4acd1a", size = 82947, upload-time = "2024-08-11T07:37:48.941Z" }, + { url = "https://files.pythonhosted.org/packages/09/cc/238998fc08e292a4a18a852ed8274159019ee7a66be14441325bcd811dfd/watchdog-4.0.2-py3-none-manylinux2014_ppc64.whl", hash = "sha256:88456d65f207b39f1981bf772e473799fcdc10801062c36fd5ad9f9d1d463a73", size = 82946, upload-time = "2024-08-11T07:37:50.279Z" }, + { url = "https://files.pythonhosted.org/packages/80/f1/d4b915160c9d677174aa5fae4537ae1f5acb23b3745ab0873071ef671f0a/watchdog-4.0.2-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:32be97f3b75693a93c683787a87a0dc8db98bb84701539954eef991fb35f5fbc", size = 82947, upload-time = "2024-08-11T07:37:51.55Z" }, + { url = "https://files.pythonhosted.org/packages/db/02/56ebe2cf33b352fe3309588eb03f020d4d1c061563d9858a9216ba004259/watchdog-4.0.2-py3-none-manylinux2014_s390x.whl", hash = "sha256:c82253cfc9be68e3e49282831afad2c1f6593af80c0daf1287f6a92657986757", size = 82944, upload-time = "2024-08-11T07:37:52.855Z" }, + { url = "https://files.pythonhosted.org/packages/01/d2/c8931ff840a7e5bd5dcb93f2bb2a1fd18faf8312e9f7f53ff1cf76ecc8ed/watchdog-4.0.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c0b14488bd336c5b1845cee83d3e631a1f8b4e9c5091ec539406e4a324f882d8", size = 82947, upload-time = "2024-08-11T07:37:55.172Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d8/cdb0c21a4a988669d7c210c75c6a2c9a0e16a3b08d9f7e633df0d9a16ad8/watchdog-4.0.2-py3-none-win32.whl", hash = "sha256:0d8a7e523ef03757a5aa29f591437d64d0d894635f8a50f370fe37f913ce4e19", size = 82935, upload-time = "2024-08-11T07:37:56.668Z" }, + { url = "https://files.pythonhosted.org/packages/99/2e/b69dfaae7a83ea64ce36538cc103a3065e12c447963797793d5c0a1d5130/watchdog-4.0.2-py3-none-win_amd64.whl", hash = "sha256:c344453ef3bf875a535b0488e3ad28e341adbd5a9ffb0f7d62cefacc8824ef2b", size = 82934, upload-time = "2024-08-11T07:37:57.991Z" }, + { url = "https://files.pythonhosted.org/packages/b0/0b/43b96a9ecdd65ff5545b1b13b687ca486da5c6249475b1a45f24d63a1858/watchdog-4.0.2-py3-none-win_ia64.whl", hash = "sha256:baececaa8edff42cd16558a639a9b0ddf425f93d892e8392a56bf904f5eff22c", size = 82933, upload-time = "2024-08-11T07:37:59.573Z" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +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" }, + { 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/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" }, + { 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" }, +] + +[[package]] +name = "wheel" +version = "0.45.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/98/2d9906746cdc6a6ef809ae6338005b3f21bb568bea3165cfc6a243fdc25c/wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729", size = 107545, upload-time = "2024-11-23T00:18:23.513Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248", size = 72494, upload-time = "2024-11-23T00:18:21.207Z" }, +] + +[[package]] +name = "zipp" +version = "3.15.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://files.pythonhosted.org/packages/00/27/f0ac6b846684cecce1ee93d32450c45ab607f65c2e0255f0092032d91f07/zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b", size = 18454, upload-time = "2023-02-25T02:17:22.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/fa/c9e82bbe1af6266adf08afb563905eb87cab83fde00a0a08963510621047/zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556", size = 6758, upload-time = "2023-02-25T02:17:20.807Z" }, +] + +[[package]] +name = "zipp" +version = "3.20.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/54/bf/5c0000c44ebc80123ecbdddba1f5dcd94a5ada602a9c225d84b5aaa55e86/zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29", size = 24199, upload-time = "2024-09-13T13:44:16.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/8b/5ba542fa83c90e09eac972fc9baca7a88e7e7ca4b221a89251954019308b/zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350", size = 9200, upload-time = "2024-09-13T13:44:14.38Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +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 c4842e197a07eea988e148d3964d44840cf2dd1b Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Sat, 10 Jan 2026 16:05:43 +0100 Subject: [PATCH 94/95] version 6.2 --- changelog.txt | 12 ++++++++++++ cstruct/__init__.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 8d03ecf..791054c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -198,3 +198,15 @@ ### Fix - fix CStruct.pack() padding + +## [6.2] - 2026-01-10 + +### Added + +- llms.txt added + +### Improved + +- Python 3.14 support +- use ruff instead of flake8/isort/black +- use pyproject.toml instead of setup.cfg diff --git a/cstruct/__init__.py b/cstruct/__init__.py index 149b919..7264908 100644 --- a/cstruct/__init__.py +++ b/cstruct/__init__.py @@ -24,7 +24,7 @@ __author__ = "Andrea Bonomi " __license__ = "MIT" -__version__ = "6.1" +__version__ = "6.2" __date__ = "15 August 2013" from typing import Any, Dict, Optional, Type, Union From d23dfd2b6a674bbf3c66d2461a5c01b2ea8349f9 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Sat, 10 Jan 2026 16:18:11 +0100 Subject: [PATCH 95/95] fix github release.yml workflow --- .github/workflows/release.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 03e2376..fdcc274 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -80,3 +80,20 @@ jobs: uses: pypa/gh-action-pypi-publish@release/v1 with: packages-dir: dist/ + + release: + name: Release version + runs-on: ubuntu-latest + needs: [wait, build] + + steps: + - name: Create release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: ${{ github.ref }} + draft: false + prerelease: false