├── checksumdir ├── py.typed ├── cli.pyi ├── __init__.pyi ├── cli.py └── __init__.py ├── .flake8 ├── .gitignore ├── pyproject.toml ├── LICENSE.txt ├── README.rst ├── .github └── workflows │ └── publish.yml └── poetry.lock /checksumdir/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /checksumdir/cli.pyi: -------------------------------------------------------------------------------- 1 | def main() -> None: ... 2 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = docs, .eggs, setup.py, example, .aws-sam, .git, dist, *.md, *.yaml, example/samconfig.toml, *.txt, *.ini 3 | ignore = E203, E266, W503, BLK100, W291, I004 4 | max-line-length = 120 5 | max-complexity = 15 6 | 7 | [isort] 8 | multi_line_output = 3 9 | include_trailing_comma = true 10 | force_grid_wrap = 0 11 | use_parentheses = true 12 | line_length = 120 -------------------------------------------------------------------------------- /checksumdir/__init__.pyi: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Callable, List, Optional, Union 3 | 4 | def dirhash( 5 | dirname: Union[str, Path], 6 | hashfunc: str = "md5", 7 | excluded_files: Optional[List[str]] = None, 8 | ignore_hidden: bool = False, 9 | followlinks: bool = False, 10 | excluded_extensions: Optional[List[str]] = None, 11 | include_paths: bool = False, 12 | ) -> str: ... 13 | def _filehash(filepath: str, hashfunc: Callable) -> str: ... 14 | def _reduce_hash(hashlist: List[str], hashfunc: Callable) -> str: ... 15 | 16 | __version__: str 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | .idea 60 | 61 | # Test folder 62 | test/ 63 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool] 2 | [tool.poetry] 3 | name = "checksumdir" 4 | version = "1.3.0" 5 | description = "Compute a single hash of the file contents of a directory." 6 | license = "MIT" 7 | keywords = ["hash", "checksum", "md5", "directory"] 8 | classifiers = [ 9 | "Topic :: Utilities", 10 | "License :: OSI Approved :: MIT License", 11 | "Operating System :: OS Independent", 12 | "Programming Language :: Python", 13 | "Programming Language :: Python :: 3", 14 | "Programming Language :: Python :: 3.9", 15 | "Programming Language :: Python :: 3.10", 16 | "Programming Language :: Python :: 3.11", 17 | "Programming Language :: Python :: 3.12", 18 | "Programming Language :: Python :: 3.13" 19 | ] 20 | homepage = "http://github.com/cakepietoast/checksumdir" 21 | authors = ["Tom McCarthy "] 22 | readme = "README.rst" 23 | 24 | [tool.poetry.dependencies] 25 | python = "^3.9" 26 | 27 | [tool.poetry.group.dev.dependencies] 28 | flake8 = "^7.0.0" 29 | black = "^24.0.0" 30 | isort = "^5.13.0" 31 | 32 | [tool.poetry.scripts] 33 | checksumdir = 'checksumdir.cli:main' -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 cakepietoast 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | *********** 2 | Checksumdir 3 | *********** 4 | 5 | |badge1| |badge2| 6 | 7 | 8 | .. |badge1| image:: https://img.shields.io/pypi/dm/checksumdir 9 | :alt: PyPI - Downloads 10 | :target: https://pypistats.org/packages/checksumdir 11 | 12 | .. |badge2| image:: https://badge.fury.io/py/checksumdir.svg 13 | :target: https://pypi.org/project/checksumdir/ 14 | 15 | A simple module for creating a single hash for a directory of files, with file contents; 16 | ignoring any metadata such as file name. Options exist to also exclude specific files 17 | or files with specific extensions. 18 | 19 | ===== 20 | Usage 21 | ===== 22 | 23 | .. code-block:: python 24 | 25 | from checksumdir import dirhash 26 | 27 | directory = '/path/to/directory/' 28 | md5hash = dirhash(directory, 'md5') 29 | sha1hash = dirhash(directory, 'sha1', excluded_files=['package.json']) 30 | sha256hash = dirhash(directory, 'sha256', excluded_extensions=['pyc']) 31 | 32 | 33 | Or to use the CLI: 34 | 35 | .. code-block:: bash 36 | 37 | # Defaults to md5. 38 | $ checksumdir /path/to/directory 39 | 40 | # Create sha1 hash: 41 | $ checksumdir -a sha1 /path/to/directory 42 | 43 | # Exclude files: 44 | $ checksumdir -e /path/to/directory 45 | 46 | # Exclude files with specific extensions: 47 | $ checksumdir -x /path/to/directory 48 | 49 | # Follow soft links: 50 | $ checksumdir --follow-links /path/to/directory 51 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPi 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | upload: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Set up Python 13 | uses: actions/setup-python@v4 14 | with: 15 | python-version: "3.13" 16 | - name: Set release notes tag 17 | run: | 18 | echo "RELEASE_TAG_VERSION=${{ github.event.release.tag_name }}" >> $GITHUB_ENV 19 | - name: Ensure new version is also set in pyproject 20 | run: | 21 | grep --regexp "version \= \"${RELEASE_TAG_VERSION}\"" pyproject.toml 22 | - name: Install dependencies 23 | run: | 24 | pip install --upgrade pip poetry 25 | poetry install 26 | - name: Run all tests, linting and baselines 27 | run: | 28 | poetry run flake8 checksumdir/ 29 | poetry run isort checksumdir/ --check --diff 30 | poetry run black checksumdir/ --check 31 | - name: Build python package and wheel 32 | run: poetry build 33 | - name: Upload to PyPi test 34 | run: | 35 | poetry config repositories.testpypi https://test.pypi.org/legacy/ 36 | poetry config pypi-token.testpypi ${PYPI_TEST_TOKEN} 37 | poetry publish --repository testpypi -n 38 | env: 39 | PYPI_USERNAME: __token__ 40 | PYPI_TEST_TOKEN: ${{ secrets.PYPI_TEST_TOKEN }} 41 | - name: Upload to PyPi prod 42 | run: | 43 | poetry config pypi-token.pypi ${PYPI_TOKEN} 44 | poetry publish -n 45 | env: 46 | PYPI_USERNAME: __token__ 47 | PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} 48 | -------------------------------------------------------------------------------- /checksumdir/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Function for deterministically generating a single hash for a directory of files, 4 | taking into account only file contents and not filenames. 5 | 6 | Usage: 7 | 8 | $ dirhash('/path/to/directory', 'md5') 9 | 10 | """ 11 | 12 | 13 | import argparse 14 | 15 | import checksumdir 16 | 17 | VERSION = checksumdir.__version__ 18 | 19 | 20 | def main() -> None: 21 | parser = argparse.ArgumentParser(description="Determine the hash for directory.") 22 | parser.add_argument( 23 | "-v", "--version", action="version", version=f"checksumdir {VERSION}" 24 | ) 25 | parser.add_argument("directory", help="Directory for which to generate hash.") 26 | parser.add_argument( 27 | "-a", "--algorithm", choices=("md5", "sha1", "sha256", "sha512"), default="md5" 28 | ) 29 | parser.add_argument( 30 | "-e", "--excluded-files", nargs="+", help="List of excluded files." 31 | ) 32 | parser.add_argument( 33 | "-i", 34 | "--ignore-hidden", 35 | action="store_true", 36 | default=False, 37 | help="Ignore hidden files", 38 | ) 39 | parser.add_argument( 40 | "-f", 41 | "--follow-links", 42 | action="store_true", 43 | default=False, 44 | help="Follow soft links", 45 | ) 46 | parser.add_argument( 47 | "-x", 48 | "--excluded-extensions", 49 | nargs="+", 50 | help="List of excluded file extensions.", 51 | ) 52 | parser.add_argument( 53 | "-p", 54 | "--include-paths", 55 | action="store_true", 56 | default=False, 57 | help="Include file path in the hash", 58 | ) 59 | 60 | args = parser.parse_args() 61 | print( 62 | checksumdir.dirhash( 63 | dirname=args.directory, 64 | hashfunc=args.algorithm, 65 | excluded_files=args.excluded_files, 66 | ignore_hidden=args.ignore_hidden, 67 | followlinks=args.follow_links, 68 | excluded_extensions=args.excluded_extensions, 69 | include_paths=args.include_paths, 70 | ) 71 | ) 72 | 73 | 74 | if __name__ == "__main__": 75 | main() 76 | -------------------------------------------------------------------------------- /checksumdir/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Function for deterministically creating a single hash for a directory of files, 3 | taking into account only file contents and not filenames. 4 | 5 | Usage: 6 | 7 | from checksumdir import dirhash 8 | 9 | dirhash('/path/to/directory', 'md5') 10 | 11 | """ 12 | 13 | import hashlib 14 | import os 15 | import re 16 | from typing import Callable, List, Optional 17 | 18 | try: 19 | from importlib.metadata import version 20 | 21 | __version__ = version("checksumdir") 22 | except ImportError: 23 | # Fallback for older Python versions 24 | import pkg_resources 25 | 26 | __version__ = pkg_resources.require("checksumdir")[0].version 27 | 28 | HASH_FUNCS = { 29 | "md5": hashlib.md5, 30 | "sha1": hashlib.sha1, 31 | "sha256": hashlib.sha256, 32 | "sha512": hashlib.sha512, 33 | } 34 | 35 | 36 | def dirhash( 37 | dirname: str, 38 | hashfunc: str = "md5", 39 | excluded_files: Optional[List[str]] = None, 40 | ignore_hidden: bool = False, 41 | followlinks: bool = False, 42 | excluded_extensions: Optional[List[str]] = None, 43 | include_paths: bool = False, 44 | ) -> str: 45 | hash_func = HASH_FUNCS.get(hashfunc) 46 | if not hash_func: 47 | raise NotImplementedError(f"{hashfunc} not implemented.") 48 | 49 | if not excluded_files: 50 | excluded_files = [] 51 | 52 | if not excluded_extensions: 53 | excluded_extensions = [] 54 | 55 | if not os.path.isdir(dirname): 56 | raise TypeError(f"{dirname} is not a directory.") 57 | 58 | hashvalues = [] 59 | for root, dirs, files in os.walk(dirname, topdown=True, followlinks=followlinks): 60 | if ignore_hidden and re.search(r"/\.", root): 61 | continue 62 | 63 | dirs.sort() 64 | files.sort() 65 | 66 | for fname in files: 67 | if ignore_hidden and fname.startswith("."): 68 | continue 69 | 70 | if fname.split(".")[-1:][0] in excluded_extensions: 71 | continue 72 | 73 | if fname in excluded_files: 74 | continue 75 | 76 | hashvalues.append(_filehash(os.path.join(root, fname), hash_func)) 77 | 78 | if include_paths: 79 | hasher = hash_func() 80 | # get the resulting relative path into array of elements 81 | path_list = os.path.relpath(os.path.join(root, fname)).split(os.sep) 82 | # compute the hash on joined list, removes all os specific separators 83 | hasher.update("".join(path_list).encode("utf-8")) 84 | hashvalues.append(hasher.hexdigest()) 85 | 86 | return _reduce_hash(hashvalues, hash_func) 87 | 88 | 89 | def _filehash(filepath: str, hashfunc: Callable) -> str: 90 | hasher = hashfunc() 91 | blocksize = 64 * 1024 92 | 93 | if not os.path.exists(filepath): 94 | return hasher.hexdigest() 95 | 96 | with open(filepath, "rb") as fp: 97 | while True: 98 | data = fp.read(blocksize) 99 | if not data: 100 | break 101 | hasher.update(data) 102 | return hasher.hexdigest() 103 | 104 | 105 | def _reduce_hash(hashlist: List[str], hashfunc: Callable) -> str: 106 | hasher = hashfunc() 107 | for hashvalue in sorted(hashlist): 108 | hasher.update(hashvalue.encode("utf-8")) 109 | return hasher.hexdigest() 110 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "black" 5 | version = "24.10.0" 6 | description = "The uncompromising code formatter." 7 | optional = false 8 | python-versions = ">=3.9" 9 | groups = ["dev"] 10 | files = [ 11 | {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, 12 | {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, 13 | {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"}, 14 | {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"}, 15 | {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"}, 16 | {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"}, 17 | {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"}, 18 | {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"}, 19 | {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"}, 20 | {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"}, 21 | {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"}, 22 | {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"}, 23 | {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"}, 24 | {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"}, 25 | {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"}, 26 | {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"}, 27 | {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"}, 28 | {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"}, 29 | {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"}, 30 | {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"}, 31 | {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"}, 32 | {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"}, 33 | ] 34 | 35 | [package.dependencies] 36 | click = ">=8.0.0" 37 | mypy-extensions = ">=0.4.3" 38 | packaging = ">=22.0" 39 | pathspec = ">=0.9.0" 40 | platformdirs = ">=2" 41 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 42 | typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} 43 | 44 | [package.extras] 45 | colorama = ["colorama (>=0.4.3)"] 46 | d = ["aiohttp (>=3.10)"] 47 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 48 | uvloop = ["uvloop (>=0.15.2)"] 49 | 50 | [[package]] 51 | name = "click" 52 | version = "8.1.8" 53 | description = "Composable command line interface toolkit" 54 | optional = false 55 | python-versions = ">=3.7" 56 | groups = ["dev"] 57 | files = [ 58 | {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, 59 | {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, 60 | ] 61 | 62 | [package.dependencies] 63 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 64 | 65 | [[package]] 66 | name = "colorama" 67 | version = "0.4.6" 68 | description = "Cross-platform colored terminal text." 69 | optional = false 70 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 71 | groups = ["dev"] 72 | markers = "platform_system == \"Windows\"" 73 | files = [ 74 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 75 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 76 | ] 77 | 78 | [[package]] 79 | name = "flake8" 80 | version = "7.3.0" 81 | description = "the modular source code checker: pep8 pyflakes and co" 82 | optional = false 83 | python-versions = ">=3.9" 84 | groups = ["dev"] 85 | files = [ 86 | {file = "flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e"}, 87 | {file = "flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872"}, 88 | ] 89 | 90 | [package.dependencies] 91 | mccabe = ">=0.7.0,<0.8.0" 92 | pycodestyle = ">=2.14.0,<2.15.0" 93 | pyflakes = ">=3.4.0,<3.5.0" 94 | 95 | [[package]] 96 | name = "isort" 97 | version = "5.13.2" 98 | description = "A Python utility / library to sort Python imports." 99 | optional = false 100 | python-versions = ">=3.8.0" 101 | groups = ["dev"] 102 | files = [ 103 | {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, 104 | {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, 105 | ] 106 | 107 | [package.extras] 108 | colors = ["colorama (>=0.4.6)"] 109 | 110 | [[package]] 111 | name = "mccabe" 112 | version = "0.7.0" 113 | description = "McCabe checker, plugin for flake8" 114 | optional = false 115 | python-versions = ">=3.6" 116 | groups = ["dev"] 117 | files = [ 118 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 119 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 120 | ] 121 | 122 | [[package]] 123 | name = "mypy-extensions" 124 | version = "1.1.0" 125 | description = "Type system extensions for programs checked with the mypy type checker." 126 | optional = false 127 | python-versions = ">=3.8" 128 | groups = ["dev"] 129 | files = [ 130 | {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, 131 | {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, 132 | ] 133 | 134 | [[package]] 135 | name = "packaging" 136 | version = "25.0" 137 | description = "Core utilities for Python packages" 138 | optional = false 139 | python-versions = ">=3.8" 140 | groups = ["dev"] 141 | files = [ 142 | {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, 143 | {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, 144 | ] 145 | 146 | [[package]] 147 | name = "pathspec" 148 | version = "0.12.1" 149 | description = "Utility library for gitignore style pattern matching of file paths." 150 | optional = false 151 | python-versions = ">=3.8" 152 | groups = ["dev"] 153 | files = [ 154 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 155 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 156 | ] 157 | 158 | [[package]] 159 | name = "platformdirs" 160 | version = "4.3.8" 161 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 162 | optional = false 163 | python-versions = ">=3.9" 164 | groups = ["dev"] 165 | files = [ 166 | {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, 167 | {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, 168 | ] 169 | 170 | [package.extras] 171 | docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] 172 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] 173 | type = ["mypy (>=1.14.1)"] 174 | 175 | [[package]] 176 | name = "pycodestyle" 177 | version = "2.14.0" 178 | description = "Python style guide checker" 179 | optional = false 180 | python-versions = ">=3.9" 181 | groups = ["dev"] 182 | files = [ 183 | {file = "pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d"}, 184 | {file = "pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783"}, 185 | ] 186 | 187 | [[package]] 188 | name = "pyflakes" 189 | version = "3.4.0" 190 | description = "passive checker of Python programs" 191 | optional = false 192 | python-versions = ">=3.9" 193 | groups = ["dev"] 194 | files = [ 195 | {file = "pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f"}, 196 | {file = "pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58"}, 197 | ] 198 | 199 | [[package]] 200 | name = "tomli" 201 | version = "2.2.1" 202 | description = "A lil' TOML parser" 203 | optional = false 204 | python-versions = ">=3.8" 205 | groups = ["dev"] 206 | markers = "python_version < \"3.11\"" 207 | files = [ 208 | {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, 209 | {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, 210 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, 211 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, 212 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, 213 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, 214 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, 215 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, 216 | {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, 217 | {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, 218 | {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, 219 | {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, 220 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, 221 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, 222 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, 223 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, 224 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, 225 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, 226 | {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, 227 | {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, 228 | {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, 229 | {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, 230 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, 231 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, 232 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, 233 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, 234 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, 235 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, 236 | {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, 237 | {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, 238 | {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, 239 | {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, 240 | ] 241 | 242 | [[package]] 243 | name = "typing-extensions" 244 | version = "4.14.1" 245 | description = "Backported and Experimental Type Hints for Python 3.9+" 246 | optional = false 247 | python-versions = ">=3.9" 248 | groups = ["dev"] 249 | markers = "python_version < \"3.11\"" 250 | files = [ 251 | {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, 252 | {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, 253 | ] 254 | 255 | [metadata] 256 | lock-version = "2.1" 257 | python-versions = "^3.9" 258 | content-hash = "32eea80b595d5b5c01025a51970b561468f701562f0c0a02c9cf86e3f9edebc6" 259 | --------------------------------------------------------------------------------