diff --git a/.pre-commit-config.ruff.yaml b/.pre-commit-config.ruff.yaml new file mode 100644 index 00000000..e687f73b --- /dev/null +++ b/.pre-commit-config.ruff.yaml @@ -0,0 +1,7 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.14.5 + hooks: + - id: ruff + args: [ --fix ] + - id: ruff-format diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..2f3c3328 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,27 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.14.5 + hooks: + - id: ruff + args: [ --fix ] + - id: ruff-format + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-yaml + args: [ --allow-multiple-documents ] + - id: check-json + - id: check-toml + - id: check-merge-conflict + - id: mixed-line-ending + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/pre-commit/mirrors-mypy + rev: 'v1.18.2' + hooks: + - id: mypy + args: [--strict, --ignore-missing-imports, --check-untyped-defs] + additional_dependencies: + - types-click + - types-PyYAML + - types-requests diff --git a/Makefile b/Makefile index 42b9a93a..d9bc3e70 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ help: @echo " install - Install the package and dependencies" @echo " install-dev - Install the package and dev dependencies" @echo " test - Run tests" - @echo " format - Format code with black" + @echo " format - Format code with ruff" @echo " lint - Run linting checks" @echo " security - Run security checks with bandit" @echo " docs - Build the documentation" @@ -59,14 +59,9 @@ test-trace: install-test $(POETRY) run pytest -k "not kms" -vvv --log-cli-level=DEBUG format: install-dev - $(POETRY) run black --extend-exclude test-data/gardenlinux . + $(POETRY) run -c .pre-commit-config.ruff.yaml --all-files lint: install-dev - @echo - @echo "------------------------------------------------------------------------------------------------------------------------" - @echo "--// BLACK //-----------------------------------------------------------------------------------------------------------" - @echo "------------------------------------------------------------------------------------------------------------------------" - $(POETRY) run black --diff --extend-exclude test-data/gardenlinux . @echo @echo "------------------------------------------------------------------------------------------------------------------------" @echo "--// ISORT //-----------------------------------------------------------------------------------------------------------" @@ -74,9 +69,9 @@ lint: install-dev $(POETRY) run isort --check-only . @echo @echo "------------------------------------------------------------------------------------------------------------------------" - @echo "--// PYRIGHT //---------------------------------------------------------------------------------------------------------" + @echo "--// PRE-COMMIT //------------------------------------------------------------------------------------------------------" @echo "------------------------------------------------------------------------------------------------------------------------" - $(POETRY) run pyright + $(POETRY) run pre-commit run --all-files security: install-dev @if [ "$(CI)" = "true" ]; then \ diff --git a/docs/index.rst b/docs/index.rst index b3257a88..c6fa838c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -26,4 +26,3 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` - diff --git a/hack/print_feature_extensions.sh b/hack/print_feature_extensions.sh index 967d57bf..5f54dc2e 100755 --- a/hack/print_feature_extensions.sh +++ b/hack/print_feature_extensions.sh @@ -3,14 +3,14 @@ search_and_print_directories() { local pattern="$1" - local base_pattern="${pattern%%.*}" - + local base_pattern="${pattern%%.*}" + while IFS= read -r file; do dir=$(dirname "$file" | sed 's|^\./||') - + suffix="${file##*/}" - suffix="${suffix#"$base_pattern"}" - + suffix="${suffix#"$base_pattern"}" + echo "('$dir', '$suffix')," done < <(find . -type f -name "$pattern" | sort -u) } diff --git a/poetry.lock b/poetry.lock index 3786aece..4429bf3b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.0 and should not be changed by hand. [[package]] name = "alabaster" @@ -75,71 +75,20 @@ test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", toml = ["tomli (>=1.1.0) ; python_version < \"3.11\""] yaml = ["PyYAML"] -[[package]] -name = "black" -version = "25.12.0" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "black-25.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f85ba1ad15d446756b4ab5f3044731bf68b777f8f9ac9cdabd2425b97cd9c4e8"}, - {file = "black-25.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:546eecfe9a3a6b46f9d69d8a642585a6eaf348bcbbc4d87a19635570e02d9f4a"}, - {file = "black-25.12.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17dcc893da8d73d8f74a596f64b7c98ef5239c2cd2b053c0f25912c4494bf9ea"}, - {file = "black-25.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:09524b0e6af8ba7a3ffabdfc7a9922fb9adef60fed008c7cd2fc01f3048e6e6f"}, - {file = "black-25.12.0-cp310-cp310-win_arm64.whl", hash = "sha256:b162653ed89eb942758efeb29d5e333ca5bb90e5130216f8369857db5955a7da"}, - {file = "black-25.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0cfa263e85caea2cff57d8f917f9f51adae8e20b610e2b23de35b5b11ce691a"}, - {file = "black-25.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a2f578ae20c19c50a382286ba78bfbeafdf788579b053d8e4980afb079ab9be"}, - {file = "black-25.12.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e1b65634b0e471d07ff86ec338819e2ef860689859ef4501ab7ac290431f9b"}, - {file = "black-25.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a3fa71e3b8dd9f7c6ac4d818345237dfb4175ed3bf37cd5a581dbc4c034f1ec5"}, - {file = "black-25.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:51e267458f7e650afed8445dc7edb3187143003d52a1b710c7321aef22aa9655"}, - {file = "black-25.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:31f96b7c98c1ddaeb07dc0f56c652e25bdedaac76d5b68a059d998b57c55594a"}, - {file = "black-25.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:05dd459a19e218078a1f98178c13f861fe6a9a5f88fc969ca4d9b49eb1809783"}, - {file = "black-25.12.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1f68c5eff61f226934be6b5b80296cf6939e5d2f0c2f7d543ea08b204bfaf59"}, - {file = "black-25.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:274f940c147ddab4442d316b27f9e332ca586d39c85ecf59ebdea82cc9ee8892"}, - {file = "black-25.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:169506ba91ef21e2e0591563deda7f00030cb466e747c4b09cb0a9dae5db2f43"}, - {file = "black-25.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a05ddeb656534c3e27a05a29196c962877c83fa5503db89e68857d1161ad08a5"}, - {file = "black-25.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9ec77439ef3e34896995503865a85732c94396edcc739f302c5673a2315e1e7f"}, - {file = "black-25.12.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e509c858adf63aa61d908061b52e580c40eae0dfa72415fa47ac01b12e29baf"}, - {file = "black-25.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:252678f07f5bac4ff0d0e9b261fbb029fa530cfa206d0a636a34ab445ef8ca9d"}, - {file = "black-25.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bc5b1c09fe3c931ddd20ee548511c64ebf964ada7e6f0763d443947fd1c603ce"}, - {file = "black-25.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0a0953b134f9335c2434864a643c842c44fba562155c738a2a37a4d61f00cad5"}, - {file = "black-25.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2355bbb6c3b76062870942d8cc450d4f8ac71f9c93c40122762c8784df49543f"}, - {file = "black-25.12.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9678bd991cc793e81d19aeeae57966ee02909877cb65838ccffef24c3ebac08f"}, - {file = "black-25.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:97596189949a8aad13ad12fcbb4ae89330039b96ad6742e6f6b45e75ad5cfd83"}, - {file = "black-25.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:778285d9ea197f34704e3791ea9404cd6d07595745907dd2ce3da7a13627b29b"}, - {file = "black-25.12.0-py3-none-any.whl", hash = "sha256:48ceb36c16dbc84062740049eef990bb2ce07598272e673c17d1a7720c71c828"}, - {file = "black-25.12.0.tar.gz", hash = "sha256:8d3dd9cea14bff7ddc0eb243c811cdb1a011ebb4800a5f0335a01a68654796a7"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" -pytokens = ">=0.3.0" - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.10)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - [[package]] name = "boto3" -version = "1.42.4" +version = "1.42.10" description = "The AWS SDK for Python" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "boto3-1.42.4-py3-none-any.whl", hash = "sha256:0f4089e230d55f981d67376e48cefd41c3d58c7f694480f13288e6ff7b1fefbc"}, - {file = "boto3-1.42.4.tar.gz", hash = "sha256:65f0d98a3786ec729ba9b5f70448895b2d1d1f27949aa7af5cb4f39da341bbc4"}, + {file = "boto3-1.42.10-py3-none-any.whl", hash = "sha256:70720926eab4306a724414286480ec4efa301f3e67e5a53ad4b62f6eb6dbd5b4"}, + {file = "boto3-1.42.10.tar.gz", hash = "sha256:8b7a1eb83ab7f0c89bb449ccac400eeca6f4ba6e33ba312e2281c6d864602bc3"}, ] [package.dependencies] -botocore = ">=1.42.4,<1.43.0" +botocore = ">=1.42.10,<1.43.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.16.0,<0.17.0" @@ -148,14 +97,14 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.42.4" +version = "1.42.10" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "botocore-1.42.4-py3-none-any.whl", hash = "sha256:c3b091fd33809f187824b6434e518b889514ded5164cb379358367c18e8b0d7d"}, - {file = "botocore-1.42.4.tar.gz", hash = "sha256:d4816023492b987a804f693c2d76fb751fdc8755d49933106d69e2489c4c0f98"}, + {file = "botocore-1.42.10-py3-none-any.whl", hash = "sha256:41eaa73694c0f9e5e281d81f18325f1181d332dce21ea47f58426250b31889fe"}, + {file = "botocore-1.42.10.tar.gz", hash = "sha256:84312c37ddc34cd0cce25436f26370af1edb9e1b1944359ee15350239537cdaa"}, ] [package.dependencies] @@ -168,14 +117,14 @@ crt = ["awscrt (==0.29.2)"] [[package]] name = "certifi" -version = "2025.10.5" +version = "2025.11.12" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" groups = ["main", "dev", "docs"] files = [ - {file = "certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de"}, - {file = "certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43"}, + {file = "certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b"}, + {file = "certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316"}, ] [[package]] @@ -276,6 +225,18 @@ markers = {dev = "platform_python_implementation != \"PyPy\""} [package.dependencies] pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} +[[package]] +name = "cfgv" +version = "3.5.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0"}, + {file = "cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132"}, +] + [[package]] name = "charset-normalizer" version = "3.4.4" @@ -405,7 +366,7 @@ version = "8.3.1" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"}, {file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"}, @@ -429,104 +390,104 @@ markers = {main = "platform_system == \"Windows\"", dev = "platform_system == \" [[package]] name = "coverage" -version = "7.11.0" +version = "7.13.0" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "coverage-7.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eb53f1e8adeeb2e78962bade0c08bfdc461853c7969706ed901821e009b35e31"}, - {file = "coverage-7.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9a03ec6cb9f40a5c360f138b88266fd8f58408d71e89f536b4f91d85721d075"}, - {file = "coverage-7.11.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d7f0616c557cbc3d1c2090334eddcbb70e1ae3a40b07222d62b3aa47f608fab"}, - {file = "coverage-7.11.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e44a86a47bbdf83b0a3ea4d7df5410d6b1a0de984fbd805fa5101f3624b9abe0"}, - {file = "coverage-7.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:596763d2f9a0ee7eec6e643e29660def2eef297e1de0d334c78c08706f1cb785"}, - {file = "coverage-7.11.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ef55537ff511b5e0a43edb4c50a7bf7ba1c3eea20b4f49b1490f1e8e0e42c591"}, - {file = "coverage-7.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cbabd8f4d0d3dc571d77ae5bdbfa6afe5061e679a9d74b6797c48d143307088"}, - {file = "coverage-7.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e24045453384e0ae2a587d562df2a04d852672eb63051d16096d3f08aa4c7c2f"}, - {file = "coverage-7.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:7161edd3426c8d19bdccde7d49e6f27f748f3c31cc350c5de7c633fea445d866"}, - {file = "coverage-7.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d4ed4de17e692ba6415b0587bc7f12bc80915031fc9db46a23ce70fc88c9841"}, - {file = "coverage-7.11.0-cp310-cp310-win32.whl", hash = "sha256:765c0bc8fe46f48e341ef737c91c715bd2a53a12792592296a095f0c237e09cf"}, - {file = "coverage-7.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:24d6f3128f1b2d20d84b24f4074475457faedc3d4613a7e66b5e769939c7d969"}, - {file = "coverage-7.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d58ecaa865c5b9fa56e35efc51d1014d4c0d22838815b9fce57a27dd9576847"}, - {file = "coverage-7.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b679e171f1c104a5668550ada700e3c4937110dbdd153b7ef9055c4f1a1ee3cc"}, - {file = "coverage-7.11.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ca61691ba8c5b6797deb221a0d09d7470364733ea9c69425a640f1f01b7c5bf0"}, - {file = "coverage-7.11.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:aef1747ede4bd8ca9cfc04cc3011516500c6891f1b33a94add3253f6f876b7b7"}, - {file = "coverage-7.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1839d08406e4cba2953dcc0ffb312252f14d7c4c96919f70167611f4dee2623"}, - {file = "coverage-7.11.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e0eb0a2dcc62478eb5b4cbb80b97bdee852d7e280b90e81f11b407d0b81c4287"}, - {file = "coverage-7.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bc1fbea96343b53f65d5351d8fd3b34fd415a2670d7c300b06d3e14a5af4f552"}, - {file = "coverage-7.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:214b622259dd0cf435f10241f1333d32caa64dbc27f8790ab693428a141723de"}, - {file = "coverage-7.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:258d9967520cca899695d4eb7ea38be03f06951d6ca2f21fb48b1235f791e601"}, - {file = "coverage-7.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cf9e6ff4ca908ca15c157c409d608da77a56a09877b97c889b98fb2c32b6465e"}, - {file = "coverage-7.11.0-cp311-cp311-win32.whl", hash = "sha256:fcc15fc462707b0680cff6242c48625da7f9a16a28a41bb8fd7a4280920e676c"}, - {file = "coverage-7.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:865965bf955d92790f1facd64fe7ff73551bd2c1e7e6b26443934e9701ba30b9"}, - {file = "coverage-7.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:5693e57a065760dcbeb292d60cc4d0231a6d4b6b6f6a3191561e1d5e8820b745"}, - {file = "coverage-7.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9c49e77811cf9d024b95faf86c3f059b11c0c9be0b0d61bc598f453703bd6fd1"}, - {file = "coverage-7.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a61e37a403a778e2cda2a6a39abcc895f1d984071942a41074b5c7ee31642007"}, - {file = "coverage-7.11.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c79cae102bb3b1801e2ef1511fb50e91ec83a1ce466b2c7c25010d884336de46"}, - {file = "coverage-7.11.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16ce17ceb5d211f320b62df002fa7016b7442ea0fd260c11cec8ce7730954893"}, - {file = "coverage-7.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80027673e9d0bd6aef86134b0771845e2da85755cf686e7c7c59566cf5a89115"}, - {file = "coverage-7.11.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d3ffa07a08657306cd2215b0da53761c4d73cb54d9143b9303a6481ec0cd415"}, - {file = "coverage-7.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a3b6a5f8b2524fd6c1066bc85bfd97e78709bb5e37b5b94911a6506b65f47186"}, - {file = "coverage-7.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fcc0a4aa589de34bc56e1a80a740ee0f8c47611bdfb28cd1849de60660f3799d"}, - {file = "coverage-7.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dba82204769d78c3fd31b35c3d5f46e06511936c5019c39f98320e05b08f794d"}, - {file = "coverage-7.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:81b335f03ba67309a95210caf3eb43bd6fe75a4e22ba653ef97b4696c56c7ec2"}, - {file = "coverage-7.11.0-cp312-cp312-win32.whl", hash = "sha256:037b2d064c2f8cc8716fe4d39cb705779af3fbf1ba318dc96a1af858888c7bb5"}, - {file = "coverage-7.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:d66c0104aec3b75e5fd897e7940188ea1892ca1d0235316bf89286d6a22568c0"}, - {file = "coverage-7.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:d91ebeac603812a09cf6a886ba6e464f3bbb367411904ae3790dfe28311b15ad"}, - {file = "coverage-7.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cc3f49e65ea6e0d5d9bd60368684fe52a704d46f9e7fc413918f18d046ec40e1"}, - {file = "coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f39ae2f63f37472c17b4990f794035c9890418b1b8cca75c01193f3c8d3e01be"}, - {file = "coverage-7.11.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7db53b5cdd2917b6eaadd0b1251cf4e7d96f4a8d24e174bdbdf2f65b5ea7994d"}, - {file = "coverage-7.11.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10ad04ac3a122048688387828b4537bc9cf60c0bf4869c1e9989c46e45690b82"}, - {file = "coverage-7.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4036cc9c7983a2b1f2556d574d2eb2154ac6ed55114761685657e38782b23f52"}, - {file = "coverage-7.11.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ab934dd13b1c5e94b692b1e01bd87e4488cb746e3a50f798cb9464fd128374b"}, - {file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59a6e5a265f7cfc05f76e3bb53eca2e0dfe90f05e07e849930fecd6abb8f40b4"}, - {file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:df01d6c4c81e15a7c88337b795bb7595a8596e92310266b5072c7e301168efbd"}, - {file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8c934bd088eed6174210942761e38ee81d28c46de0132ebb1801dbe36a390dcc"}, - {file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a03eaf7ec24078ad64a07f02e30060aaf22b91dedf31a6b24d0d98d2bba7f48"}, - {file = "coverage-7.11.0-cp313-cp313-win32.whl", hash = "sha256:695340f698a5f56f795b2836abe6fb576e7c53d48cd155ad2f80fd24bc63a040"}, - {file = "coverage-7.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:2727d47fce3ee2bac648528e41455d1b0c46395a087a229deac75e9f88ba5a05"}, - {file = "coverage-7.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:0efa742f431529699712b92ecdf22de8ff198df41e43aeaaadf69973eb93f17a"}, - {file = "coverage-7.11.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:587c38849b853b157706407e9ebdca8fd12f45869edb56defbef2daa5fb0812b"}, - {file = "coverage-7.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b971bdefdd75096163dd4261c74be813c4508477e39ff7b92191dea19f24cd37"}, - {file = "coverage-7.11.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:269bfe913b7d5be12ab13a95f3a76da23cf147be7fa043933320ba5625f0a8de"}, - {file = "coverage-7.11.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dadbcce51a10c07b7c72b0ce4a25e4b6dcb0c0372846afb8e5b6307a121eb99f"}, - {file = "coverage-7.11.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ed43fa22c6436f7957df036331f8fe4efa7af132054e1844918866cd228af6c"}, - {file = "coverage-7.11.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9516add7256b6713ec08359b7b05aeff8850c98d357784c7205b2e60aa2513fa"}, - {file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb92e47c92fcbcdc692f428da67db33337fa213756f7adb6a011f7b5a7a20740"}, - {file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d06f4fc7acf3cabd6d74941d53329e06bab00a8fe10e4df2714f0b134bfc64ef"}, - {file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:6fbcee1a8f056af07ecd344482f711f563a9eb1c2cad192e87df00338ec3cdb0"}, - {file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dbbf012be5f32533a490709ad597ad8a8ff80c582a95adc8d62af664e532f9ca"}, - {file = "coverage-7.11.0-cp313-cp313t-win32.whl", hash = "sha256:cee6291bb4fed184f1c2b663606a115c743df98a537c969c3c64b49989da96c2"}, - {file = "coverage-7.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a386c1061bf98e7ea4758e4313c0ab5ecf57af341ef0f43a0bf26c2477b5c268"}, - {file = "coverage-7.11.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f9ea02ef40bb83823b2b04964459d281688fe173e20643870bb5d2edf68bc836"}, - {file = "coverage-7.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c770885b28fb399aaf2a65bbd1c12bf6f307ffd112d6a76c5231a94276f0c497"}, - {file = "coverage-7.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a3d0e2087dba64c86a6b254f43e12d264b636a39e88c5cc0a01a7c71bcfdab7e"}, - {file = "coverage-7.11.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73feb83bb41c32811973b8565f3705caf01d928d972b72042b44e97c71fd70d1"}, - {file = "coverage-7.11.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c6f31f281012235ad08f9a560976cc2fc9c95c17604ff3ab20120fe480169bca"}, - {file = "coverage-7.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9570ad567f880ef675673992222746a124b9595506826b210fbe0ce3f0499cd"}, - {file = "coverage-7.11.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8badf70446042553a773547a61fecaa734b55dc738cacf20c56ab04b77425e43"}, - {file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a09c1211959903a479e389685b7feb8a17f59ec5a4ef9afde7650bd5eabc2777"}, - {file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:5ef83b107f50db3f9ae40f69e34b3bd9337456c5a7fe3461c7abf8b75dd666a2"}, - {file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f91f927a3215b8907e214af77200250bb6aae36eca3f760f89780d13e495388d"}, - {file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbcd376716d6b7fbfeedd687a6c4be019c5a5671b35f804ba76a4c0a778cba4"}, - {file = "coverage-7.11.0-cp314-cp314-win32.whl", hash = "sha256:bab7ec4bb501743edc63609320aaec8cd9188b396354f482f4de4d40a9d10721"}, - {file = "coverage-7.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:3d4ba9a449e9364a936a27322b20d32d8b166553bfe63059bd21527e681e2fad"}, - {file = "coverage-7.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:ce37f215223af94ef0f75ac68ea096f9f8e8c8ec7d6e8c346ee45c0d363f0479"}, - {file = "coverage-7.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f413ce6e07e0d0dc9c433228727b619871532674b45165abafe201f200cc215f"}, - {file = "coverage-7.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:05791e528a18f7072bf5998ba772fe29db4da1234c45c2087866b5ba4dea710e"}, - {file = "coverage-7.11.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cacb29f420cfeb9283b803263c3b9a068924474ff19ca126ba9103e1278dfa44"}, - {file = "coverage-7.11.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314c24e700d7027ae3ab0d95fbf8d53544fca1f20345fd30cd219b737c6e58d3"}, - {file = "coverage-7.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:630d0bd7a293ad2fc8b4b94e5758c8b2536fdf36c05f1681270203e463cbfa9b"}, - {file = "coverage-7.11.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e89641f5175d65e2dbb44db15fe4ea48fade5d5bbb9868fdc2b4fce22f4a469d"}, - {file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c9f08ea03114a637dab06cedb2e914da9dc67fa52c6015c018ff43fdde25b9c2"}, - {file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce9f3bde4e9b031eaf1eb61df95c1401427029ea1bfddb8621c1161dcb0fa02e"}, - {file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:e4dc07e95495923d6fd4d6c27bf70769425b71c89053083843fd78f378558996"}, - {file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:424538266794db2861db4922b05d729ade0940ee69dcf0591ce8f69784db0e11"}, - {file = "coverage-7.11.0-cp314-cp314t-win32.whl", hash = "sha256:4c1eeb3fb8eb9e0190bebafd0462936f75717687117339f708f395fe455acc73"}, - {file = "coverage-7.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b56efee146c98dbf2cf5cffc61b9829d1e94442df4d7398b26892a53992d3547"}, - {file = "coverage-7.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b5c2705afa83f49bd91962a4094b6b082f94aef7626365ab3f8f4bd159c5acf3"}, - {file = "coverage-7.11.0-py3-none-any.whl", hash = "sha256:4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68"}, - {file = "coverage-7.11.0.tar.gz", hash = "sha256:167bd504ac1ca2af7ff3b81d245dfea0292c5032ebef9d66cc08a7d28c1b8050"}, + {file = "coverage-7.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:02d9fb9eccd48f6843c98a37bd6817462f130b86da8660461e8f5e54d4c06070"}, + {file = "coverage-7.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:367449cf07d33dc216c083f2036bb7d976c6e4903ab31be400ad74ad9f85ce98"}, + {file = "coverage-7.13.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cdb3c9f8fef0a954c632f64328a3935988d33a6604ce4bf67ec3e39670f12ae5"}, + {file = "coverage-7.13.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d10fd186aac2316f9bbb46ef91977f9d394ded67050ad6d84d94ed6ea2e8e54e"}, + {file = "coverage-7.13.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f88ae3e69df2ab62fb0bc5219a597cb890ba5c438190ffa87490b315190bb33"}, + {file = "coverage-7.13.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4be718e51e86f553bcf515305a158a1cd180d23b72f07ae76d6017c3cc5d791"}, + {file = "coverage-7.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a00d3a393207ae12f7c49bb1c113190883b500f48979abb118d8b72b8c95c032"}, + {file = "coverage-7.13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a7b1cd820e1b6116f92c6128f1188e7afe421c7e1b35fa9836b11444e53ebd9"}, + {file = "coverage-7.13.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:37eee4e552a65866f15dedd917d5e5f3d59805994260720821e2c1b51ac3248f"}, + {file = "coverage-7.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:62d7c4f13102148c78d7353c6052af6d899a7f6df66a32bddcc0c0eb7c5326f8"}, + {file = "coverage-7.13.0-cp310-cp310-win32.whl", hash = "sha256:24e4e56304fdb56f96f80eabf840eab043b3afea9348b88be680ec5986780a0f"}, + {file = "coverage-7.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:74c136e4093627cf04b26a35dab8cbfc9b37c647f0502fc313376e11726ba303"}, + {file = "coverage-7.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0dfa3855031070058add1a59fdfda0192fd3e8f97e7c81de0596c145dea51820"}, + {file = "coverage-7.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fdb6f54f38e334db97f72fa0c701e66d8479af0bc3f9bfb5b90f1c30f54500f"}, + {file = "coverage-7.13.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7e442c013447d1d8d195be62852270b78b6e255b79b8675bad8479641e21fd96"}, + {file = "coverage-7.13.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ed5630d946859de835a85e9a43b721123a8a44ec26e2830b296d478c7fd4259"}, + {file = "coverage-7.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f15a931a668e58087bc39d05d2b4bf4b14ff2875b49c994bbdb1c2217a8daeb"}, + {file = "coverage-7.13.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30a3a201a127ea57f7e14ba43c93c9c4be8b7d17a26e03bb49e6966d019eede9"}, + {file = "coverage-7.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a485ff48fbd231efa32d58f479befce52dcb6bfb2a88bb7bf9a0b89b1bc8030"}, + {file = "coverage-7.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:22486cdafba4f9e471c816a2a5745337742a617fef68e890d8baf9f3036d7833"}, + {file = "coverage-7.13.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:263c3dbccc78e2e331e59e90115941b5f53e85cfcc6b3b2fbff1fd4e3d2c6ea8"}, + {file = "coverage-7.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5330fa0cc1f5c3c4c3bb8e101b742025933e7848989370a1d4c8c5e401ea753"}, + {file = "coverage-7.13.0-cp311-cp311-win32.whl", hash = "sha256:0f4872f5d6c54419c94c25dd6ae1d015deeb337d06e448cd890a1e89a8ee7f3b"}, + {file = "coverage-7.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51a202e0f80f241ccb68e3e26e19ab5b3bf0f813314f2c967642f13ebcf1ddfe"}, + {file = "coverage-7.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:d2a9d7f1c11487b1c69367ab3ac2d81b9b3721f097aa409a3191c3e90f8f3dd7"}, + {file = "coverage-7.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0b3d67d31383c4c68e19a88e28fc4c2e29517580f1b0ebec4a069d502ce1e0bf"}, + {file = "coverage-7.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:581f086833d24a22c89ae0fe2142cfaa1c92c930adf637ddf122d55083fb5a0f"}, + {file = "coverage-7.13.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0a3a30f0e257df382f5f9534d4ce3d4cf06eafaf5192beb1a7bd066cb10e78fb"}, + {file = "coverage-7.13.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:583221913fbc8f53b88c42e8dbb8fca1d0f2e597cb190ce45916662b8b9d9621"}, + {file = "coverage-7.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f5d9bd30756fff3e7216491a0d6d520c448d5124d3d8e8f56446d6412499e74"}, + {file = "coverage-7.13.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a23e5a1f8b982d56fa64f8e442e037f6ce29322f1f9e6c2344cd9e9f4407ee57"}, + {file = "coverage-7.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b01c22bc74a7fb44066aaf765224c0d933ddf1f5047d6cdfe4795504a4493f8"}, + {file = "coverage-7.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:898cce66d0836973f48dda4e3514d863d70142bdf6dfab932b9b6a90ea5b222d"}, + {file = "coverage-7.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3ab483ea0e251b5790c2aac03acde31bff0c736bf8a86829b89382b407cd1c3b"}, + {file = "coverage-7.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d84e91521c5e4cb6602fe11ece3e1de03b2760e14ae4fcf1a4b56fa3c801fcd"}, + {file = "coverage-7.13.0-cp312-cp312-win32.whl", hash = "sha256:193c3887285eec1dbdb3f2bd7fbc351d570ca9c02ca756c3afbc71b3c98af6ef"}, + {file = "coverage-7.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:4f3e223b2b2db5e0db0c2b97286aba0036ca000f06aca9b12112eaa9af3d92ae"}, + {file = "coverage-7.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:086cede306d96202e15a4b77ace8472e39d9f4e5f9fd92dd4fecdfb2313b2080"}, + {file = "coverage-7.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:28ee1c96109974af104028a8ef57cec21447d42d0e937c0275329272e370ebcf"}, + {file = "coverage-7.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e97353dcc5587b85986cda4ff3ec98081d7e84dd95e8b2a6d59820f0545f8a"}, + {file = "coverage-7.13.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:99acd4dfdfeb58e1937629eb1ab6ab0899b131f183ee5f23e0b5da5cba2fec74"}, + {file = "coverage-7.13.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff45e0cd8451e293b63ced93161e189780baf444119391b3e7d25315060368a6"}, + {file = "coverage-7.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4f72a85316d8e13234cafe0a9f81b40418ad7a082792fa4165bd7d45d96066b"}, + {file = "coverage-7.13.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11c21557d0e0a5a38632cbbaca5f008723b26a89d70db6315523df6df77d6232"}, + {file = "coverage-7.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76541dc8d53715fb4f7a3a06b34b0dc6846e3c69bc6204c55653a85dd6220971"}, + {file = "coverage-7.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6e9e451dee940a86789134b6b0ffbe31c454ade3b849bb8a9d2cca2541a8e91d"}, + {file = "coverage-7.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5c67dace46f361125e6b9cace8fe0b729ed8479f47e70c89b838d319375c8137"}, + {file = "coverage-7.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f59883c643cb19630500f57016f76cfdcd6845ca8c5b5ea1f6e17f74c8e5f511"}, + {file = "coverage-7.13.0-cp313-cp313-win32.whl", hash = "sha256:58632b187be6f0be500f553be41e277712baa278147ecb7559983c6d9faf7ae1"}, + {file = "coverage-7.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:73419b89f812f498aca53f757dd834919b48ce4799f9d5cad33ca0ae442bdb1a"}, + {file = "coverage-7.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:eb76670874fdd6091eedcc856128ee48c41a9bbbb9c3f1c7c3cf169290e3ffd6"}, + {file = "coverage-7.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6e63ccc6e0ad8986386461c3c4b737540f20426e7ec932f42e030320896c311a"}, + {file = "coverage-7.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:494f5459ffa1bd45e18558cd98710c36c0b8fbfa82a5eabcbe671d80ecffbfe8"}, + {file = "coverage-7.13.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:06cac81bf10f74034e055e903f5f946e3e26fc51c09fc9f584e4a1605d977053"}, + {file = "coverage-7.13.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f2ffc92b46ed6e6760f1d47a71e56b5664781bc68986dbd1836b2b70c0ce2071"}, + {file = "coverage-7.13.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0602f701057c6823e5db1b74530ce85f17c3c5be5c85fc042ac939cbd909426e"}, + {file = "coverage-7.13.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:25dc33618d45456ccb1d37bce44bc78cf269909aa14c4db2e03d63146a8a1493"}, + {file = "coverage-7.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:71936a8b3b977ddd0b694c28c6a34f4fff2e9dd201969a4ff5d5fc7742d614b0"}, + {file = "coverage-7.13.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:936bc20503ce24770c71938d1369461f0c5320830800933bc3956e2a4ded930e"}, + {file = "coverage-7.13.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:af0a583efaacc52ae2521f8d7910aff65cdb093091d76291ac5820d5e947fc1c"}, + {file = "coverage-7.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f1c23e24a7000da892a312fb17e33c5f94f8b001de44b7cf8ba2e36fbd15859e"}, + {file = "coverage-7.13.0-cp313-cp313t-win32.whl", hash = "sha256:5f8a0297355e652001015e93be345ee54393e45dc3050af4a0475c5a2b767d46"}, + {file = "coverage-7.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6abb3a4c52f05e08460bd9acf04fec027f8718ecaa0d09c40ffbc3fbd70ecc39"}, + {file = "coverage-7.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:3ad968d1e3aa6ce5be295ab5fe3ae1bf5bb4769d0f98a80a0252d543a2ef2e9e"}, + {file = "coverage-7.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:453b7ec753cf5e4356e14fe858064e5520c460d3bbbcb9c35e55c0d21155c256"}, + {file = "coverage-7.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:af827b7cbb303e1befa6c4f94fd2bf72f108089cfa0f8abab8f4ca553cf5ca5a"}, + {file = "coverage-7.13.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9987a9e4f8197a1000280f7cc089e3ea2c8b3c0a64d750537809879a7b4ceaf9"}, + {file = "coverage-7.13.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3188936845cd0cb114fa6a51842a304cdbac2958145d03be2377ec41eb285d19"}, + {file = "coverage-7.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2bdb3babb74079f021696cb46b8bb5f5661165c385d3a238712b031a12355be"}, + {file = "coverage-7.13.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7464663eaca6adba4175f6c19354feea61ebbdd735563a03d1e472c7072d27bb"}, + {file = "coverage-7.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8069e831f205d2ff1f3d355e82f511eb7c5522d7d413f5db5756b772ec8697f8"}, + {file = "coverage-7.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6fb2d5d272341565f08e962cce14cdf843a08ac43bd621783527adb06b089c4b"}, + {file = "coverage-7.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5e70f92ef89bac1ac8a99b3324923b4749f008fdbd7aa9cb35e01d7a284a04f9"}, + {file = "coverage-7.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4b5de7d4583e60d5fd246dd57fcd3a8aa23c6e118a8c72b38adf666ba8e7e927"}, + {file = "coverage-7.13.0-cp314-cp314-win32.whl", hash = "sha256:a6c6e16b663be828a8f0b6c5027d36471d4a9f90d28444aa4ced4d48d7d6ae8f"}, + {file = "coverage-7.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:0900872f2fdb3ee5646b557918d02279dc3af3dfb39029ac4e945458b13f73bc"}, + {file = "coverage-7.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:3a10260e6a152e5f03f26db4a407c4c62d3830b9af9b7c0450b183615f05d43b"}, + {file = "coverage-7.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9097818b6cc1cfb5f174e3263eba4a62a17683bcfe5c4b5d07f4c97fa51fbf28"}, + {file = "coverage-7.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0018f73dfb4301a89292c73be6ba5f58722ff79f51593352759c1790ded1cabe"}, + {file = "coverage-7.13.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:166ad2a22ee770f5656e1257703139d3533b4a0b6909af67c6b4a3adc1c98657"}, + {file = "coverage-7.13.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f6aaef16d65d1787280943f1c8718dc32e9cf141014e4634d64446702d26e0ff"}, + {file = "coverage-7.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e999e2dcc094002d6e2c7bbc1fb85b58ba4f465a760a8014d97619330cdbbbf3"}, + {file = "coverage-7.13.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:00c3d22cf6fb1cf3bf662aaaa4e563be8243a5ed2630339069799835a9cc7f9b"}, + {file = "coverage-7.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22ccfe8d9bb0d6134892cbe1262493a8c70d736b9df930f3f3afae0fe3ac924d"}, + {file = "coverage-7.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9372dff5ea15930fea0445eaf37bbbafbc771a49e70c0aeed8b4e2c2614cc00e"}, + {file = "coverage-7.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:69ac2c492918c2461bc6ace42d0479638e60719f2a4ef3f0815fa2df88e9f940"}, + {file = "coverage-7.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:739c6c051a7540608d097b8e13c76cfa85263ced467168dc6b477bae3df7d0e2"}, + {file = "coverage-7.13.0-cp314-cp314t-win32.whl", hash = "sha256:fe81055d8c6c9de76d60c94ddea73c290b416e061d40d542b24a5871bad498b7"}, + {file = "coverage-7.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:445badb539005283825959ac9fa4a28f712c214b65af3a2c464f1adc90f5fcbc"}, + {file = "coverage-7.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:de7f6748b890708578fc4b7bb967d810aeb6fcc9bff4bb77dbca77dab2f9df6a"}, + {file = "coverage-7.13.0-py3-none-any.whl", hash = "sha256:850d2998f380b1e266459ca5b47bc9e7daf9af1d070f66317972f382d46f1904"}, + {file = "coverage-7.13.0.tar.gz", hash = "sha256:a394aa27f2d7ff9bc04cf703817773a59ad6dfbd577032e690f961d2460ee936"}, ] [package.extras] @@ -609,6 +570,18 @@ ssh = ["bcrypt (>=3.1.5)"] test = ["certifi (>=2024)", "cryptography-vectors (==46.0.3)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] test-randomorder = ["pytest-randomly"] +[[package]] +name = "distlib" +version = "0.4.0" +description = "Distribution utilities" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"}, + {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, +] + [[package]] name = "docutils" version = "0.21.2" @@ -621,6 +594,18 @@ files = [ {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, ] +[[package]] +name = "filelock" +version = "3.20.1" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "filelock-3.20.1-py3-none-any.whl", hash = "sha256:15d9e9a67306188a44baa72f569d2bfd803076269365fdea0934385da4dc361a"}, + {file = "filelock-3.20.1.tar.gz", hash = "sha256:b8360948b351b80f420878d8516519a2204b07aefcdcfd24912a5d33127f188c"}, +] + [[package]] name = "gitdb" version = "4.0.12" @@ -655,6 +640,21 @@ gitdb = ">=4.0.1,<5" doc = ["sphinx (>=7.1.2,<7.2)", "sphinx-autodoc-typehints", "sphinx_rtd_theme"] test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock ; python_version < \"3.8\"", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions ; python_version < \"3.11\""] +[[package]] +name = "identify" +version = "2.6.15" +description = "File identification library for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757"}, + {file = "identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf"}, +] + +[package.extras] +license = ["ukkonen"] + [[package]] name = "idna" version = "3.11" @@ -958,18 +958,6 @@ ssm = ["PyYAML (>=5.1)"] stepfunctions = ["antlr4-python3-runtime", "jsonpath_ng"] xray = ["aws-xray-sdk (>=0.93,!=0.96)", "setuptools"] -[[package]] -name = "mypy-extensions" -version = "1.1.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, - {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, -] - [[package]] name = "networkx" version = "3.6.1" @@ -1039,28 +1027,16 @@ files = [ {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, ] -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - [[package]] name = "platformdirs" -version = "4.5.0" +version = "4.5.1" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3"}, - {file = "platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312"}, + {file = "platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31"}, + {file = "platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda"}, ] [package.extras] @@ -1084,6 +1060,25 @@ files = [ dev = ["pre-commit", "tox"] testing = ["coverage", "pytest", "pytest-benchmark"] +[[package]] +name = "pre-commit" +version = "4.5.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pre_commit-4.5.0-py2.py3-none-any.whl", hash = "sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1"}, + {file = "pre_commit-4.5.0.tar.gz", hash = "sha256:dc5a065e932b19fc1d4c653c6939068fe54325af8e741e74e88db4d28a4dd66b"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + [[package]] name = "pycparser" version = "2.23" @@ -1174,27 +1169,6 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] -[[package]] -name = "pyright" -version = "1.1.407" -description = "Command line wrapper for pyright" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "pyright-1.1.407-py3-none-any.whl", hash = "sha256:6dd419f54fcc13f03b52285796d65e639786373f433e243f8b94cf93a7444d21"}, - {file = "pyright-1.1.407.tar.gz", hash = "sha256:099674dba5c10489832d4a4b2d302636152a9a42d317986c38474c76fe562262"}, -] - -[package.dependencies] -nodeenv = ">=1.6.0" -typing-extensions = ">=4.1" - -[package.extras] -all = ["nodejs-wheel-binaries", "twine (>=3.4.1)"] -dev = ["twine (>=3.4.1)"] -nodejs = ["nodejs-wheel-binaries"] - [[package]] name = "pytest" version = "9.0.2" @@ -1267,21 +1241,6 @@ files = [ [package.extras] cli = ["click (>=5.0)"] -[[package]] -name = "pytokens" -version = "0.3.0" -description = "A Fast, spec compliant Python 3.14+ tokenizer that runs on older Pythons." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pytokens-0.3.0-py3-none-any.whl", hash = "sha256:95b2b5eaf832e469d141a378872480ede3f251a5a5041b8ec6e581d3ac71bbf3"}, - {file = "pytokens-0.3.0.tar.gz", hash = "sha256:2f932b14ed08de5fcf0b391ace2642f858f1394c0857202959000b68ed7a458a"}, -] - -[package.extras] -dev = ["black", "build", "mypy", "pytest", "pytest-cov", "setuptools", "tox", "twine", "wheel"] - [[package]] name = "pyyaml" version = "6.0.3" @@ -1461,144 +1420,155 @@ pygments = ">=2.13.0,<3.0.0" jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] -name = "roman-numerals-py" -version = "3.1.0" +name = "roman-numerals" +version = "4.0.0" description = "Manipulate well-formed Roman numerals" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["docs"] files = [ - {file = "roman_numerals_py-3.1.0-py3-none-any.whl", hash = "sha256:9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c"}, - {file = "roman_numerals_py-3.1.0.tar.gz", hash = "sha256:be4bf804f083a4ce001b5eb7e3c0862479d10f94c936f6c4e5f250aa5ff5bd2d"}, + {file = "roman_numerals-4.0.0-py3-none-any.whl", hash = "sha256:4131feb23ba1a542494873e4cee7844ec8d226a750134efc65ceb20939ed33c9"}, + {file = "roman_numerals-4.0.0.tar.gz", hash = "sha256:231287018a8788bf8c0718482a08c15b90458523ea1d840a18a791a86d4583b3"}, ] -[package.extras] -lint = ["mypy (==1.15.0)", "pyright (==1.1.394)", "ruff (==0.9.7)"] -test = ["pytest (>=8)"] +[[package]] +name = "roman-numerals-py" +version = "4.0.0" +description = "This package is deprecated, switch to roman-numerals." +optional = false +python-versions = ">=3.10" +groups = ["docs"] +files = [ + {file = "roman_numerals_py-4.0.0-py3-none-any.whl", hash = "sha256:dfcecf6e0cddbf2ee1112e7e2ebf58ba771984f075cb57a30e1811cee4f06332"}, + {file = "roman_numerals_py-4.0.0.tar.gz", hash = "sha256:f7fa8dff5b7b7251d3a7586b97c57a0698e2e28898fa42c23bcc0cf51b02aee9"}, +] + +[package.dependencies] +roman-numerals = "4.0.0" [[package]] name = "rpds-py" -version = "0.28.0" +version = "0.30.0" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "rpds_py-0.28.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7b6013db815417eeb56b2d9d7324e64fcd4fa289caeee6e7a78b2e11fc9b438a"}, - {file = "rpds_py-0.28.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a4c6b05c685c0c03f80dabaeb73e74218c49deea965ca63f76a752807397207"}, - {file = "rpds_py-0.28.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4794c6c3fbe8f9ac87699b131a1f26e7b4abcf6d828da46a3a52648c7930eba"}, - {file = "rpds_py-0.28.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2e8456b6ee5527112ff2354dd9087b030e3429e43a74f480d4a5ca79d269fd85"}, - {file = "rpds_py-0.28.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:beb880a9ca0a117415f241f66d56025c02037f7c4efc6fe59b5b8454f1eaa50d"}, - {file = "rpds_py-0.28.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6897bebb118c44b38c9cb62a178e09f1593c949391b9a1a6fe777ccab5934ee7"}, - {file = "rpds_py-0.28.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b553dd06e875249fd43efd727785efb57a53180e0fde321468222eabbeaafa"}, - {file = "rpds_py-0.28.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:f0b2044fdddeea5b05df832e50d2a06fe61023acb44d76978e1b060206a8a476"}, - {file = "rpds_py-0.28.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05cf1e74900e8da73fa08cc76c74a03345e5a3e37691d07cfe2092d7d8e27b04"}, - {file = "rpds_py-0.28.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:efd489fec7c311dae25e94fe7eeda4b3d06be71c68f2cf2e8ef990ffcd2cd7e8"}, - {file = "rpds_py-0.28.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ada7754a10faacd4f26067e62de52d6af93b6d9542f0df73c57b9771eb3ba9c4"}, - {file = "rpds_py-0.28.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c2a34fd26588949e1e7977cfcbb17a9a42c948c100cab890c6d8d823f0586457"}, - {file = "rpds_py-0.28.0-cp310-cp310-win32.whl", hash = "sha256:f9174471d6920cbc5e82a7822de8dfd4dcea86eb828b04fc8c6519a77b0ee51e"}, - {file = "rpds_py-0.28.0-cp310-cp310-win_amd64.whl", hash = "sha256:6e32dd207e2c4f8475257a3540ab8a93eff997abfa0a3fdb287cae0d6cd874b8"}, - {file = "rpds_py-0.28.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:03065002fd2e287725d95fbc69688e0c6daf6c6314ba38bdbaa3895418e09296"}, - {file = "rpds_py-0.28.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28ea02215f262b6d078daec0b45344c89e161eab9526b0d898221d96fdda5f27"}, - {file = "rpds_py-0.28.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25dbade8fbf30bcc551cb352376c0ad64b067e4fc56f90e22ba70c3ce205988c"}, - {file = "rpds_py-0.28.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c03002f54cc855860bfdc3442928ffdca9081e73b5b382ed0b9e8efe6e5e205"}, - {file = "rpds_py-0.28.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9699fa7990368b22032baf2b2dce1f634388e4ffc03dfefaaac79f4695edc95"}, - {file = "rpds_py-0.28.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9b06fe1a75e05e0713f06ea0c89ecb6452210fd60e2f1b6ddc1067b990e08d9"}, - {file = "rpds_py-0.28.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac9f83e7b326a3f9ec3ef84cda98fb0a74c7159f33e692032233046e7fd15da2"}, - {file = "rpds_py-0.28.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:0d3259ea9ad8743a75a43eb7819324cdab393263c91be86e2d1901ee65c314e0"}, - {file = "rpds_py-0.28.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a7548b345f66f6695943b4ef6afe33ccd3f1b638bd9afd0f730dd255c249c9e"}, - {file = "rpds_py-0.28.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9a40040aa388b037eb39416710fbcce9443498d2eaab0b9b45ae988b53f5c67"}, - {file = "rpds_py-0.28.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f60c7ea34e78c199acd0d3cda37a99be2c861dd2b8cf67399784f70c9f8e57d"}, - {file = "rpds_py-0.28.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1571ae4292649100d743b26d5f9c63503bb1fedf538a8f29a98dce2d5ba6b4e6"}, - {file = "rpds_py-0.28.0-cp311-cp311-win32.whl", hash = "sha256:5cfa9af45e7c1140af7321fa0bef25b386ee9faa8928c80dc3a5360971a29e8c"}, - {file = "rpds_py-0.28.0-cp311-cp311-win_amd64.whl", hash = "sha256:dd8d86b5d29d1b74100982424ba53e56033dc47720a6de9ba0259cf81d7cecaa"}, - {file = "rpds_py-0.28.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e27d3a5709cc2b3e013bf93679a849213c79ae0573f9b894b284b55e729e120"}, - {file = "rpds_py-0.28.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6b4f28583a4f247ff60cd7bdda83db8c3f5b05a7a82ff20dd4b078571747708f"}, - {file = "rpds_py-0.28.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d678e91b610c29c4b3d52a2c148b641df2b4676ffe47c59f6388d58b99cdc424"}, - {file = "rpds_py-0.28.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e819e0e37a44a78e1383bf1970076e2ccc4dc8c2bbaa2f9bd1dc987e9afff628"}, - {file = "rpds_py-0.28.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5ee514e0f0523db5d3fb171f397c54875dbbd69760a414dccf9d4d7ad628b5bd"}, - {file = "rpds_py-0.28.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3fa06d27fdcee47f07a39e02862da0100cb4982508f5ead53ec533cd5fe55e"}, - {file = "rpds_py-0.28.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:46959ef2e64f9e4a41fc89aa20dbca2b85531f9a72c21099a3360f35d10b0d5a"}, - {file = "rpds_py-0.28.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8455933b4bcd6e83fde3fefc987a023389c4b13f9a58c8d23e4b3f6d13f78c84"}, - {file = "rpds_py-0.28.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:ad50614a02c8c2962feebe6012b52f9802deec4263946cddea37aaf28dd25a66"}, - {file = "rpds_py-0.28.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e5deca01b271492553fdb6c7fd974659dce736a15bae5dad7ab8b93555bceb28"}, - {file = "rpds_py-0.28.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:735f8495a13159ce6a0d533f01e8674cec0c57038c920495f87dcb20b3ddb48a"}, - {file = "rpds_py-0.28.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:961ca621ff10d198bbe6ba4957decca61aa2a0c56695384c1d6b79bf61436df5"}, - {file = "rpds_py-0.28.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2374e16cc9131022e7d9a8f8d65d261d9ba55048c78f3b6e017971a4f5e6353c"}, - {file = "rpds_py-0.28.0-cp312-cp312-win32.whl", hash = "sha256:d15431e334fba488b081d47f30f091e5d03c18527c325386091f31718952fe08"}, - {file = "rpds_py-0.28.0-cp312-cp312-win_amd64.whl", hash = "sha256:a410542d61fc54710f750d3764380b53bf09e8c4edbf2f9141a82aa774a04f7c"}, - {file = "rpds_py-0.28.0-cp312-cp312-win_arm64.whl", hash = "sha256:1f0cfd1c69e2d14f8c892b893997fa9a60d890a0c8a603e88dca4955f26d1edd"}, - {file = "rpds_py-0.28.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e9e184408a0297086f880556b6168fa927d677716f83d3472ea333b42171ee3b"}, - {file = "rpds_py-0.28.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:edd267266a9b0448f33dc465a97cfc5d467594b600fe28e7fa2f36450e03053a"}, - {file = "rpds_py-0.28.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85beb8b3f45e4e32f6802fb6cd6b17f615ef6c6a52f265371fb916fae02814aa"}, - {file = "rpds_py-0.28.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d2412be8d00a1b895f8ad827cc2116455196e20ed994bb704bf138fe91a42724"}, - {file = "rpds_py-0.28.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf128350d384b777da0e68796afdcebc2e9f63f0e9f242217754e647f6d32491"}, - {file = "rpds_py-0.28.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a2036d09b363aa36695d1cc1a97b36865597f4478470b0697b5ee9403f4fe399"}, - {file = "rpds_py-0.28.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8e1e9be4fa6305a16be628959188e4fd5cd6f1b0e724d63c6d8b2a8adf74ea6"}, - {file = "rpds_py-0.28.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0a403460c9dd91a7f23fc3188de6d8977f1d9603a351d5db6cf20aaea95b538d"}, - {file = "rpds_py-0.28.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d7366b6553cdc805abcc512b849a519167db8f5e5c3472010cd1228b224265cb"}, - {file = "rpds_py-0.28.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b43c6a3726efd50f18d8120ec0551241c38785b68952d240c45ea553912ac41"}, - {file = "rpds_py-0.28.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0cb7203c7bc69d7c1585ebb33a2e6074492d2fc21ad28a7b9d40457ac2a51ab7"}, - {file = "rpds_py-0.28.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a52a5169c664dfb495882adc75c304ae1d50df552fbd68e100fdc719dee4ff9"}, - {file = "rpds_py-0.28.0-cp313-cp313-win32.whl", hash = "sha256:2e42456917b6687215b3e606ab46aa6bca040c77af7df9a08a6dcfe8a4d10ca5"}, - {file = "rpds_py-0.28.0-cp313-cp313-win_amd64.whl", hash = "sha256:e0a0311caedc8069d68fc2bf4c9019b58a2d5ce3cd7cb656c845f1615b577e1e"}, - {file = "rpds_py-0.28.0-cp313-cp313-win_arm64.whl", hash = "sha256:04c1b207ab8b581108801528d59ad80aa83bb170b35b0ddffb29c20e411acdc1"}, - {file = "rpds_py-0.28.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f296ea3054e11fc58ad42e850e8b75c62d9a93a9f981ad04b2e5ae7d2186ff9c"}, - {file = "rpds_py-0.28.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5a7306c19b19005ad98468fcefeb7100b19c79fc23a5f24a12e06d91181193fa"}, - {file = "rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5d9b86aa501fed9862a443c5c3116f6ead8bc9296185f369277c42542bd646b"}, - {file = "rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e5bbc701eff140ba0e872691d573b3d5d30059ea26e5785acba9132d10c8c31d"}, - {file = "rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a5690671cd672a45aa8616d7374fdf334a1b9c04a0cac3c854b1136e92374fe"}, - {file = "rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9f1d92ecea4fa12f978a367c32a5375a1982834649cdb96539dcdc12e609ab1a"}, - {file = "rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d252db6b1a78d0a3928b6190156042d54c93660ce4d98290d7b16b5296fb7cc"}, - {file = "rpds_py-0.28.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d61b355c3275acb825f8777d6c4505f42b5007e357af500939d4a35b19177259"}, - {file = "rpds_py-0.28.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:acbe5e8b1026c0c580d0321c8aae4b0a1e1676861d48d6e8c6586625055b606a"}, - {file = "rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8aa23b6f0fc59b85b4c7d89ba2965af274346f738e8d9fc2455763602e62fd5f"}, - {file = "rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7b14b0c680286958817c22d76fcbca4800ddacef6f678f3a7c79a1fe7067fe37"}, - {file = "rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bcf1d210dfee61a6c86551d67ee1031899c0fdbae88b2d44a569995d43797712"}, - {file = "rpds_py-0.28.0-cp313-cp313t-win32.whl", hash = "sha256:3aa4dc0fdab4a7029ac63959a3ccf4ed605fee048ba67ce89ca3168da34a1342"}, - {file = "rpds_py-0.28.0-cp313-cp313t-win_amd64.whl", hash = "sha256:7b7d9d83c942855e4fdcfa75d4f96f6b9e272d42fffcb72cd4bb2577db2e2907"}, - {file = "rpds_py-0.28.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:dcdcb890b3ada98a03f9f2bb108489cdc7580176cb73b4f2d789e9a1dac1d472"}, - {file = "rpds_py-0.28.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f274f56a926ba2dc02976ca5b11c32855cbd5925534e57cfe1fda64e04d1add2"}, - {file = "rpds_py-0.28.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fe0438ac4a29a520ea94c8c7f1754cdd8feb1bc490dfda1bfd990072363d527"}, - {file = "rpds_py-0.28.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8a358a32dd3ae50e933347889b6af9a1bdf207ba5d1a3f34e1a38cd3540e6733"}, - {file = "rpds_py-0.28.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e80848a71c78aa328fefaba9c244d588a342c8e03bda518447b624ea64d1ff56"}, - {file = "rpds_py-0.28.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f586db2e209d54fe177e58e0bc4946bea5fb0102f150b1b2f13de03e1f0976f8"}, - {file = "rpds_py-0.28.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ae8ee156d6b586e4292491e885d41483136ab994e719a13458055bec14cf370"}, - {file = "rpds_py-0.28.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a805e9b3973f7e27f7cab63a6b4f61d90f2e5557cff73b6e97cd5b8540276d3d"}, - {file = "rpds_py-0.28.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5d3fd16b6dc89c73a4da0b4ac8b12a7ecc75b2864b95c9e5afed8003cb50a728"}, - {file = "rpds_py-0.28.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6796079e5d24fdaba6d49bda28e2c47347e89834678f2bc2c1b4fc1489c0fb01"}, - {file = "rpds_py-0.28.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:76500820c2af232435cbe215e3324c75b950a027134e044423f59f5b9a1ba515"}, - {file = "rpds_py-0.28.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bbdc5640900a7dbf9dd707fe6388972f5bbd883633eb68b76591044cfe346f7e"}, - {file = "rpds_py-0.28.0-cp314-cp314-win32.whl", hash = "sha256:adc8aa88486857d2b35d75f0640b949759f79dc105f50aa2c27816b2e0dd749f"}, - {file = "rpds_py-0.28.0-cp314-cp314-win_amd64.whl", hash = "sha256:66e6fa8e075b58946e76a78e69e1a124a21d9a48a5b4766d15ba5b06869d1fa1"}, - {file = "rpds_py-0.28.0-cp314-cp314-win_arm64.whl", hash = "sha256:a6fe887c2c5c59413353b7c0caff25d0e566623501ccfff88957fa438a69377d"}, - {file = "rpds_py-0.28.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7a69df082db13c7070f7b8b1f155fa9e687f1d6aefb7b0e3f7231653b79a067b"}, - {file = "rpds_py-0.28.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b1cde22f2c30ebb049a9e74c5374994157b9b70a16147d332f89c99c5960737a"}, - {file = "rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5338742f6ba7a51012ea470bd4dc600a8c713c0c72adaa0977a1b1f4327d6592"}, - {file = "rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1460ebde1bcf6d496d80b191d854adedcc619f84ff17dc1c6d550f58c9efbba"}, - {file = "rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e3eb248f2feba84c692579257a043a7699e28a77d86c77b032c1d9fbb3f0219c"}, - {file = "rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3bbba5def70b16cd1c1d7255666aad3b290fbf8d0fe7f9f91abafb73611a91"}, - {file = "rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3114f4db69ac5a1f32e7e4d1cbbe7c8f9cf8217f78e6e002cedf2d54c2a548ed"}, - {file = "rpds_py-0.28.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4b0cb8a906b1a0196b863d460c0222fb8ad0f34041568da5620f9799b83ccf0b"}, - {file = "rpds_py-0.28.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf681ac76a60b667106141e11a92a3330890257e6f559ca995fbb5265160b56e"}, - {file = "rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1e8ee6413cfc677ce8898d9cde18cc3a60fc2ba756b0dec5b71eb6eb21c49fa1"}, - {file = "rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b3072b16904d0b5572a15eb9d31c1954e0d3227a585fc1351aa9878729099d6c"}, - {file = "rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b670c30fd87a6aec281c3c9896d3bae4b205fd75d79d06dc87c2503717e46092"}, - {file = "rpds_py-0.28.0-cp314-cp314t-win32.whl", hash = "sha256:8014045a15b4d2b3476f0a287fcc93d4f823472d7d1308d47884ecac9e612be3"}, - {file = "rpds_py-0.28.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7a4e59c90d9c27c561eb3160323634a9ff50b04e4f7820600a2beb0ac90db578"}, - {file = "rpds_py-0.28.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f5e7101145427087e493b9c9b959da68d357c28c562792300dd21a095118ed16"}, - {file = "rpds_py-0.28.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:31eb671150b9c62409a888850aaa8e6533635704fe2b78335f9aaf7ff81eec4d"}, - {file = "rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48b55c1f64482f7d8bd39942f376bfdf2f6aec637ee8c805b5041e14eeb771db"}, - {file = "rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24743a7b372e9a76171f6b69c01aedf927e8ac3e16c474d9fe20d552a8cb45c7"}, - {file = "rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:389c29045ee8bbb1627ea190b4976a310a295559eaf9f1464a1a6f2bf84dde78"}, - {file = "rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23690b5827e643150cf7b49569679ec13fe9a610a15949ed48b85eb7f98f34ec"}, - {file = "rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f0c9266c26580e7243ad0d72fc3e01d6b33866cfab5084a6da7576bcf1c4f72"}, - {file = "rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:4c6c4db5d73d179746951486df97fd25e92396be07fc29ee8ff9a8f5afbdfb27"}, - {file = "rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3b695a8fa799dd2cfdb4804b37096c5f6dba1ac7f48a7fbf6d0485bcd060316"}, - {file = "rpds_py-0.28.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:6aa1bfce3f83baf00d9c5fcdbba93a3ab79958b4c7d7d1f55e7fe68c20e63912"}, - {file = "rpds_py-0.28.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:7b0f9dceb221792b3ee6acb5438eb1f02b0cb2c247796a72b016dcc92c6de829"}, - {file = "rpds_py-0.28.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:5d0145edba8abd3db0ab22b5300c99dc152f5c9021fab861be0f0544dc3cbc5f"}, - {file = "rpds_py-0.28.0.tar.gz", hash = "sha256:abd4df20485a0983e2ca334a216249b6186d6e3c1627e106651943dbdb791aea"}, + {file = "rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288"}, + {file = "rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7"}, + {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff"}, + {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7"}, + {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139"}, + {file = "rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464"}, + {file = "rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169"}, + {file = "rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425"}, + {file = "rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038"}, + {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7"}, + {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed"}, + {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85"}, + {file = "rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c"}, + {file = "rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825"}, + {file = "rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229"}, + {file = "rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad"}, + {file = "rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51"}, + {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5"}, + {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e"}, + {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394"}, + {file = "rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf"}, + {file = "rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b"}, + {file = "rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e"}, + {file = "rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2"}, + {file = "rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d"}, + {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7"}, + {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31"}, + {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95"}, + {file = "rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d"}, + {file = "rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15"}, + {file = "rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1"}, + {file = "rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a"}, + {file = "rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0"}, + {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94"}, + {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08"}, + {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27"}, + {file = "rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6"}, + {file = "rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d"}, + {file = "rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0"}, + {file = "rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f"}, + {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65"}, + {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f"}, + {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53"}, + {file = "rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed"}, + {file = "rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950"}, + {file = "rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6"}, + {file = "rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb"}, + {file = "rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5"}, + {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404"}, + {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856"}, + {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40"}, + {file = "rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0"}, + {file = "rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e"}, + {file = "rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84"}, ] [[package]] @@ -1828,60 +1798,69 @@ test = ["pytest"] [[package]] name = "stevedore" -version = "5.5.0" +version = "5.6.0" description = "Manage dynamic plugins for Python applications" optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "stevedore-5.5.0-py3-none-any.whl", hash = "sha256:18363d4d268181e8e8452e71a38cd77630f345b2ef6b4a8d5614dac5ee0d18cf"}, - {file = "stevedore-5.5.0.tar.gz", hash = "sha256:d31496a4f4df9825e1a1e4f1f74d19abb0154aff311c3b376fcc89dae8fccd73"}, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -description = "Backported and Experimental Type Hints for Python 3.9+" -optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, - {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, + {file = "stevedore-5.6.0-py3-none-any.whl", hash = "sha256:4a36dccefd7aeea0c70135526cecb7766c4c84c473b1af68db23d541b6dc1820"}, + {file = "stevedore-5.6.0.tar.gz", hash = "sha256:f22d15c6ead40c5bbfa9ca54aa7e7b4a07d59b36ae03ed12ced1a54cf0b51945"}, ] [[package]] name = "urllib3" -version = "2.5.0" +version = "2.6.2" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" groups = ["main", "dev", "docs"] files = [ - {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, - {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, + {file = "urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd"}, + {file = "urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797"}, ] [package.extras] -brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] +brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] + +[[package]] +name = "virtualenv" +version = "20.35.4" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b"}, + {file = "virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] [[package]] name = "werkzeug" -version = "3.1.3" +version = "3.1.4" description = "The comprehensive WSGI web application library." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e"}, - {file = "werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"}, + {file = "werkzeug-3.1.4-py3-none-any.whl", hash = "sha256:2ad50fb9ed09cc3af22c54698351027ace879a0b60a3b5edf5730b2f7d876905"}, + {file = "werkzeug-3.1.4.tar.gz", hash = "sha256:cd3cd98b1b92dc3b7b3995038826c68097dcb16f9baa63abe35f20eafeb9fe5e"}, ] [package.dependencies] -MarkupSafe = ">=2.1.1" +markupsafe = ">=2.1.1" [package.extras] watchdog = ["watchdog (>=2.3)"] @@ -1904,4 +1883,4 @@ test = ["pytest", "pytest-cov"] [metadata] lock-version = "2.1" python-versions = ">=3.13, <3.14" -content-hash = "f53639a8fc0211b98821f0725036912f186c728086e250993ae3c30fb85f7588" +content-hash = "72c619d1320245804ef38e5d9d45f0e026329d19e2ccbff02d65ec69dc8939f4" diff --git a/pyproject.toml b/pyproject.toml index b000fd0c..7fd177bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,11 +10,11 @@ packages = [{ include = "gardenlinux", from = "src" }] [tool.poetry.dependencies] python = ">=3.13, <3.14" apt-repo = "^0.5" -boto3 = "^1.40.57" -click = "^8.2.1" -cryptography = "^46.0.1" +boto3 = "^1.42.10" +click = "^8.3.1" +cryptography = "^46.0.3" jsonschema = "^4.25.1" -networkx = "^3.5" +networkx = "^3.6" oras = "^0.2.38" pygit2 = "^1.19.0" pygments = "^2.19.2" @@ -22,15 +22,14 @@ PyYAML = "^6.0.2" gitpython = "^3.1.45" [tool.poetry.group.dev.dependencies] -bandit = "^1.8.6" -black = "^25.1.0" -moto = "^5.1.12" -python-dotenv = "^1.1.1" -pytest = "^9.0.0" +bandit = "^1.9.2" +moto = "^5.1.16" +pre-commit = "^4.5.0" +python-dotenv = "^1.2.1" +pytest = "^9.0.2" pytest-cov = "^7.0.0" isort = "^7.0.0" requests-mock = "^1.12.1" -pyright = "^1.1.403" [tool.poetry.group.docs.dependencies] sphinx-rtd-theme = "^3.0.2" @@ -54,19 +53,6 @@ line_length = 120 known_first_party = ["gardenlinux"] skip = ["test-data", ".venv", "**/__pycache", ".pytest-cache", "hack"] -[tool.pyright] -typeCheckingMode = "strict" -venvPath = "." -venv = ".venv" -exclude = [ - "test-data", - "docs", - "hack", - ".venv", - ".pytest-cache", - "**/__pycache", -] - [tool.bandit] skips = ["B101", "B404"] # allow asserts, subprocesses exclude_dirs = ["tests"] diff --git a/pyrightconfig.json b/pyrightconfig.json deleted file mode 100644 index 11698df3..00000000 --- a/pyrightconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - // Pyright is configured in 'strict' mode in pyproject.toml - // This file overrides some reports and tones them down to warnings, if they are not critical. - // It also turns some up to become warnings or errors. - "reportShadowedImports": "warning", - "reportImportCycles": "error", - "reportOptionalMemberAccess": "warning", - "reportOptionalSubscript": "warning", - "reportOptionalContextManager": "warning", - "reportOptionalCall": "warning", - "reportUnusedVariable": "warning", - "reportUnusedImport": "warning", - "reportUnusedFunction": "warning", - "reportUnusedCallResult": "none", - "reportUnusedClass": "warning", - "reportUnnecessaryCast": "warning", - "reportUnnecessaryComparison": "warning", - "reportUnnecessaryIsInstance": "warning", - "reportUnnecessaryContains": "warning", - // Becomes useful after introduction of type annotations - // "reportUnknownMemberType": "warning", - // "reportUnknownParameterType": "warning", - // "reportUnknownArgumentType": "warning", - // "reportUnknownVariableType": "warning", - "reportTypedDictNotRequiredAccess": "error", - "reportDeprecated": "warning", -} \ No newline at end of file diff --git a/src/gardenlinux/apt/debsource.py b/src/gardenlinux/apt/debsource.py index aacfcb76..0fceaf0e 100644 --- a/src/gardenlinux/apt/debsource.py +++ b/src/gardenlinux/apt/debsource.py @@ -21,7 +21,7 @@ class Debsrc: Apache License, Version 2.0 """ - def __init__(self, deb_source, deb_version): + def __init__(self, deb_source: str, deb_version: str): """ Constructor __init__(Debsrc) @@ -31,8 +31,8 @@ def __init__(self, deb_source, deb_version): :since: 0.7.0 """ - self.deb_source: str = deb_source - self.deb_version: str = deb_version + self.deb_source = deb_source + self.deb_version = deb_version def __repr__(self) -> str: """ diff --git a/src/gardenlinux/apt/package_repo_info.py b/src/gardenlinux/apt/package_repo_info.py index 5342ed18..d577b756 100644 --- a/src/gardenlinux/apt/package_repo_info.py +++ b/src/gardenlinux/apt/package_repo_info.py @@ -9,7 +9,7 @@ from apt_repo import APTRepository -class GardenLinuxRepo(APTRepository): +class GardenLinuxRepo(APTRepository): # type: ignore[misc] """ Class to reflect APT based GardenLinux repositories. diff --git a/src/gardenlinux/constants.py b/src/gardenlinux/constants.py index ba61bc1d..293086d8 100644 --- a/src/gardenlinux/constants.py +++ b/src/gardenlinux/constants.py @@ -145,6 +145,7 @@ GL_BUG_REPORT_URL = "https://github.com/gardenlinux/gardenlinux/issues" GL_COMMIT_SPECIAL_VALUES = ("local",) +GL_DEB_REPO_BASE_URL = "https://packages.gardenlinux.io/gardenlinux" GL_DISTRIBUTION_NAME = "Garden Linux" GL_HOME_URL = "https://gardenlinux.io" GL_RELEASE_ID = "gardenlinux" @@ -161,12 +162,8 @@ S3_DOWNLOADS_DIR = Path(os.path.dirname(__file__)) / ".." / "s3_downloads" -GL_DEB_REPO_BASE_URL = "https://packages.gardenlinux.io/gardenlinux" -GLVD_BASE_URL = ( - "https://security.gardenlinux.org/v1" -) - GARDENLINUX_GITHUB_RELEASE_BUCKET_NAME = "gardenlinux-github-releases" +GLVD_BASE_URL = "https://security.gardenlinux.org/v1" # https://github.com/gardenlinux/gardenlinux/issues/3044 # Empty string is the 'legacy' variant with traditional root fs and still needed/supported diff --git a/src/gardenlinux/features/__main__.py b/src/gardenlinux/features/__main__.py index e2566284..76224da2 100644 --- a/src/gardenlinux/features/__main__.py +++ b/src/gardenlinux/features/__main__.py @@ -155,13 +155,13 @@ def main() -> None: else: print_output_from_cname(args.type, cname) elif args.type == "commit_id": - print(commit_id_or_hash[:8]) + print(commit_id_or_hash[:8]) # type: ignore[index] elif args.type == "container_tag": - print(re.sub("\\W+", "-", f"{version}-{commit_id_or_hash[:8]}")) + print(re.sub("\\W+", "-", f"{version}-{commit_id_or_hash[:8]}")) # type: ignore[index] elif args.type == "version": print(version) elif args.type == "version_and_commit_id": - print(f"{version}-{commit_id_or_hash[:8]}") + print(f"{version}-{commit_id_or_hash[:8]}") # type: ignore[index] def get_version_and_commit_id_from_files(gardenlinux_root: str) -> tuple[str, str]: @@ -235,7 +235,7 @@ def print_output_from_features_parser( cname_instance: CName, parser: Parser, flavor: str, - ignores_list: set, + ignores_list: Set[str], ) -> None: """ Prints output to stdout based on the given features parser and parameters. @@ -248,7 +248,7 @@ def print_output_from_features_parser( :since: 1.0.0 """ - def additional_filter_func(node): + def additional_filter_func(node: str) -> bool: return node not in ignores_list if output_type == "features": diff --git a/src/gardenlinux/features/cname.py b/src/gardenlinux/features/cname.py index e829c328..3822d793 100644 --- a/src/gardenlinux/features/cname.py +++ b/src/gardenlinux/features/cname.py @@ -8,7 +8,7 @@ from configparser import UNNAMED_SECTION, ConfigParser from os import PathLike, environ from pathlib import Path -from typing import Optional +from typing import List, Optional from ..constants import ( ARCHS, @@ -34,7 +34,13 @@ class CName(object): Apache License, Version 2.0 """ - def __init__(self, cname, arch=None, commit_hash=None, version=None): + def __init__( + self, + cname: str, + arch: Optional[str] = None, + commit_hash: Optional[str] = None, + version: Optional[str] = None, + ): """ Constructor __init__(CName) @@ -49,17 +55,15 @@ def __init__(self, cname, arch=None, commit_hash=None, version=None): self._arch = None self._commit_hash = None self._commit_id = None - self._feature_elements_cached = None - self._feature_flags_cached = None - self._feature_platforms_cached = None - self._feature_set_cached = None - self._platform_variant_cached = None - + self._feature_elements_cached: Optional[List[str]] = None + self._feature_flags_cached: Optional[List[str]] = None + self._feature_platform_cached: Optional[str] = None + self._feature_set_cached: Optional[str] = None + self._platform_variant_cached: Optional[str] = None self._flag_multiple_platforms = bool( environ.get("GL_ALLOW_FRANKENSTEIN", False) ) - - self._flavor = None + self._flavor = "" self._version = None commit_id_or_hash = None @@ -151,7 +155,7 @@ def commit_hash(self) -> str: return self._commit_hash @commit_hash.setter - def commit_hash(self, commit_hash) -> None: + def commit_hash(self, commit_hash: str) -> None: """ Sets the commit hash @@ -178,7 +182,7 @@ def commit_id(self) -> Optional[str]: return self._commit_id @property - def flavor(self) -> str | None: + def flavor(self) -> str: """ Returns the flavor for the cname parsed. @@ -386,7 +390,7 @@ def version_epoch(self) -> Optional[int]: return epoch - def load_from_release_file(self, release_file: PathLike | str) -> None: + def load_from_release_file(self, release_file: PathLike[str] | str) -> None: """ Loads and parses a release metadata file. @@ -398,7 +402,7 @@ def load_from_release_file(self, release_file: PathLike | str) -> None: if not isinstance(release_file, PathLike): release_file = Path(release_file) - if not release_file.exists(): + if not release_file.exists(): # type: ignore[attr-defined] raise RuntimeError( f"Release metadata file given is invalid: {release_file}" ) @@ -480,7 +484,7 @@ def load_from_release_file(self, release_file: PathLike | str) -> None: ).strip("\"'") def save_to_release_file( - self, release_file: PathLike | str, overwrite: Optional[bool] = False + self, release_file: PathLike[str] | str, overwrite: Optional[bool] = False ) -> None: """ Saves the release metadata file. @@ -493,10 +497,10 @@ def save_to_release_file( if not isinstance(release_file, PathLike): release_file = Path(release_file) - if not overwrite and release_file.exists(): + if not overwrite and release_file.exists(): # type: ignore[attr-defined] raise RuntimeError( f"Refused to overwrite existing release metadata file: {release_file}" ) - with release_file.open("w") as fp: + with release_file.open("w") as fp: # type: ignore[attr-defined] fp.write(self.release_metadata_string) diff --git a/src/gardenlinux/features/cname_main.py b/src/gardenlinux/features/cname_main.py index 9f86e75e..d5cf3ba1 100644 --- a/src/gardenlinux/features/cname_main.py +++ b/src/gardenlinux/features/cname_main.py @@ -18,7 +18,7 @@ from .parser import Parser -def main(): +def main() -> None: """ gl-cname main() diff --git a/src/gardenlinux/features/metadata_main.py b/src/gardenlinux/features/metadata_main.py index 9497e02b..9bb2c1d7 100644 --- a/src/gardenlinux/features/metadata_main.py +++ b/src/gardenlinux/features/metadata_main.py @@ -15,7 +15,7 @@ ] -def main(): +def main() -> None: """ gl-metadata main() diff --git a/src/gardenlinux/features/parser.py b/src/gardenlinux/features/parser.py index 68273bbb..497e66fb 100644 --- a/src/gardenlinux/features/parser.py +++ b/src/gardenlinux/features/parser.py @@ -9,7 +9,7 @@ from functools import reduce from glob import glob from pathlib import Path -from typing import Callable, List, Optional, Set +from typing import Any, Callable, Dict, List, Optional, Set import networkx import yaml @@ -39,7 +39,7 @@ class Parser(object): def __init__( self, gardenlinux_root: Optional[str] = None, - feature_dir_name: Optional[str] = "features", + feature_dir_name: str = "features", logger: Optional[logging.Logger] = None, ): """ @@ -117,7 +117,7 @@ def graph(self) -> networkx.Graph: def filter( self, - cname: str | None, + cname: str, ignore_excludes: bool = False, additional_filter_func: Optional[Callable[[str], bool]] = None, ) -> networkx.Graph: @@ -140,10 +140,10 @@ def filter( def filter_as_dict( self, - cname: str | None, + cname: str, ignore_excludes: bool = False, additional_filter_func: Optional[Callable[[str], bool]] = None, - ) -> dict: + ) -> Dict[str, List[str]]: """ Filters the features graph and returns it as a dict. @@ -160,10 +160,10 @@ def filter_as_dict( def filter_as_list( self, - cname: str | None, + cname: str, ignore_excludes: bool = False, additional_filter_func: Optional[Callable[[str], bool]] = None, - ) -> list: + ) -> List[str]: """ Filters the features graph and returns it as a list. @@ -180,7 +180,7 @@ def filter_as_list( def filter_as_string( self, - cname: str | None, + cname: str, ignore_excludes: bool = False, additional_filter_func: Optional[Callable[[str], bool]] = None, ) -> str: @@ -201,7 +201,7 @@ def filter_as_string( def filter_graph_as_dict( self, graph: networkx.Graph, - ) -> dict: + ) -> Dict[str, List[str]]: """ Filters the features graph and returns it as a dict. @@ -213,7 +213,7 @@ def filter_graph_as_dict( features = Parser.sort_reversed_graph_nodes(graph) - features_by_type = {} + features_by_type: Dict[str, List[str]] = {} for feature in features: node_type = Parser._get_graph_node_type(graph.nodes[feature]) @@ -228,7 +228,7 @@ def filter_graph_as_dict( def filter_graph_as_list( self, graph: networkx.Graph, - ) -> list: + ) -> List[str]: """ Filters the features graph and returns it as a list. @@ -258,9 +258,9 @@ def filter_graph_as_string( def filter_based_on_feature_set( self, - feature_set: (str,), + feature_set: List[str], ignore_excludes: bool = False, - additional_filter_func: Optional[Callable[(str,), bool]] = None, + additional_filter_func: Optional[Callable[[str], bool]] = None, ) -> networkx.Graph: """ Filters the features graph based on a feature set given. @@ -301,7 +301,9 @@ def filter_based_on_feature_set( return graph - def _exclude_from_filter_set(graph, feature_set, filter_set): + def _exclude_from_filter_set( + graph: networkx.Graph, feature_set: List[str], filter_set: List[str] + ) -> None: """ Removes the given `filter_set` out of `feature_set`. @@ -331,7 +333,7 @@ def _exclude_from_filter_set(graph, feature_set, filter_set): if exclude_graph_view.edges(): raise ValueError("Including explicitly excluded feature") - def _get_node_features(self, node): + def _get_node_features(self, node: Dict[str, Any]) -> Dict[str, Any]: """ Returns the features for a given features node. @@ -341,9 +343,9 @@ def _get_node_features(self, node): :since: 0.7.0 """ - return node.get("content", {}).get("features", {}) + return node.get("content", {}).get("features", {}) # type: ignore[no-any-return] - def _read_feature_yaml(self, feature_yaml_file: str): + def _read_feature_yaml(self, feature_yaml_file: str) -> Dict[str, Any]: """ Reads and returns the content of the given features file. @@ -361,7 +363,7 @@ def _read_feature_yaml(self, feature_yaml_file: str): return {"name": name, "content": content} @staticmethod - def get_flavor_from_feature_set(sorted_features: List[str]): + def get_flavor_from_feature_set(sorted_features: List[str]) -> str: """ Get the base cname for the feature set given. @@ -376,7 +378,7 @@ def get_flavor_from_feature_set(sorted_features: List[str]): ) @staticmethod - def get_flavor_as_feature_set(cname): + def get_flavor_as_feature_set(cname: str) -> List[str]: """ Returns the features of a given canonical name. @@ -402,10 +404,13 @@ def get_flavor_as_feature_set(cname): else: features.append(feature) - return [platform] + sorted(features) + sorted(flags) + return [platform] + sorted(features) + sorted(flags) # type: ignore[return-value] @staticmethod - def _get_filter_set_callable(filter_set, additional_filter_func): + def _get_filter_set_callable( + filter_set: List[str], + additional_filter_func: Optional[Callable[[str], bool]] = None, + ) -> Callable[[str], bool]: """ Returns the filter function used for the graph. @@ -416,16 +421,17 @@ def _get_filter_set_callable(filter_set, additional_filter_func): :since: 0.7.0 """ - def filter_func(node): + def filter_func(node: str) -> bool: additional_filter_result = ( True if additional_filter_func is None else additional_filter_func(node) ) + return node in filter_set and additional_filter_result return filter_func @staticmethod - def _get_graph_view_for_attr(graph, attr): + def _get_graph_view_for_attr(graph: networkx.Graph, attr: str) -> networkx.Graph: """ Returns a graph view to return `attr` data. @@ -441,7 +447,9 @@ def _get_graph_view_for_attr(graph, attr): ) @staticmethod - def _get_graph_view_for_attr_callable(graph, attr): + def _get_graph_view_for_attr_callable( + graph: networkx.Graph, attr: str + ) -> Callable[[str, str], bool]: """ Returns the filter function used to filter for `attr` data. @@ -452,13 +460,13 @@ def _get_graph_view_for_attr_callable(graph, attr): :since: 0.7.0 """ - def filter_func(a, b): - return graph.get_edge_data(a, b)["attr"] == attr + def filter_func(a: str, b: str) -> bool: + return graph.get_edge_data(a, b)["attr"] == attr # type: ignore[no-any-return] return filter_func @staticmethod - def _get_graph_node_type(node): + def _get_graph_node_type(node: str) -> str: """ Returns the node feature type. @@ -468,10 +476,10 @@ def _get_graph_node_type(node): :since: 0.7.0 """ - return node.get("content", {}).get("type") + return node.get("content", {}).get("type") # type: ignore[attr-defined, no-any-return] @staticmethod - def set_default_gardenlinux_root_dir(root_dir): + def set_default_gardenlinux_root_dir(root_dir: str) -> None: """ Sets the default GardenLinux root directory used. @@ -483,7 +491,7 @@ def set_default_gardenlinux_root_dir(root_dir): Parser._GARDENLINUX_ROOT = root_dir @staticmethod - def sort_graph_nodes(graph): + def sort_graph_nodes(graph: networkx.Graph) -> List[str]: """ Sorts graph nodes by feature type. @@ -493,7 +501,7 @@ def sort_graph_nodes(graph): :since: 0.7.0 """ - def key_function(node): + def key_function(node: str) -> str: prefix_map = {"platform": "0", "element": "1", "flag": "2"} node_type = Parser._get_graph_node_type(graph.nodes.get(node, {})) prefix = prefix_map[node_type] @@ -517,7 +525,7 @@ def subset(input_set: Set[str], order_list: List[str]) -> List[str]: return [item for item in order_list if item in input_set] @staticmethod - def sort_reversed_graph_nodes(graph): + def sort_reversed_graph_nodes(graph: networkx.Graph) -> List[str]: """ Sorts graph nodes by feature type. diff --git a/src/gardenlinux/flavors/__main__.py b/src/gardenlinux/flavors/__main__.py index dda219d9..ea2974d3 100644 --- a/src/gardenlinux/flavors/__main__.py +++ b/src/gardenlinux/flavors/__main__.py @@ -6,16 +6,17 @@ """ import json -from argparse import ArgumentParser +from argparse import ArgumentParser, Namespace from pathlib import Path from tempfile import TemporaryDirectory +from typing import Any, List, Tuple from ..constants import GL_REPOSITORY_URL from ..git import Repository from .parser import Parser -def _get_flavors_file_data(flavors_file): +def _get_flavors_file_data(flavors_file: Path) -> str: if not flavors_file.exists(): raise RuntimeError(f"Error: {flavors_file} does not exist.") @@ -24,12 +25,11 @@ def _get_flavors_file_data(flavors_file): return fp.read() -def generate_markdown_table(combinations, no_arch): +def generate_markdown_table(combinations: List[Tuple[Any, str]]) -> str: """ Generate a markdown table of platforms and their flavors. :param combinations: List of tuples of architectures and flavors - :param no_arch: Noop :return: (str) Markdown table :since: 0.7.0 @@ -47,7 +47,7 @@ def generate_markdown_table(combinations, no_arch): return table -def parse_args(): +def parse_args() -> Namespace: """ Parses arguments used for main() @@ -125,7 +125,7 @@ def parse_args(): return parser.parse_args() -def main(): +def main() -> None: """ gl-flavors-parse main() @@ -170,7 +170,7 @@ def main(): print(json.dumps(grouped_combinations, indent=2)) elif args.markdown_table_by_platform: - print(generate_markdown_table(combinations, args.no_arch)) + print(generate_markdown_table(combinations)) else: if args.no_arch: printable_combinations = sorted(set(Parser.remove_arch(combinations))) diff --git a/src/gardenlinux/flavors/parser.py b/src/gardenlinux/flavors/parser.py index f7eec9e7..08d6e3e6 100644 --- a/src/gardenlinux/flavors/parser.py +++ b/src/gardenlinux/flavors/parser.py @@ -5,6 +5,8 @@ """ import fnmatch +from logging import Logger +from typing import Any, Dict, List, Optional, Tuple import yaml from jsonschema import validate as jsonschema_validate @@ -26,7 +28,7 @@ class Parser(object): Apache License, Version 2.0 """ - def __init__(self, data, logger=None): + def __init__(self, data: str, logger: Optional[Logger] = None): """ Constructor __init__(Parser) @@ -51,15 +53,15 @@ def __init__(self, data, logger=None): def filter( self, - include_only_patterns=[], - wildcard_excludes=[], - only_build=False, - only_test=False, - only_test_platform=False, - only_publish=False, - filter_categories=[], - exclude_categories=[], - ): + include_only_patterns: List[str] = [], + wildcard_excludes: List[str] = [], + only_build: bool = False, + only_test: bool = False, + only_test_platform: bool = False, + only_publish: bool = False, + filter_categories: List[str] = [], + exclude_categories: List[str] = [], + ) -> List[Tuple[Any, str]]: """ Filters flavors data and generates combinations. @@ -128,11 +130,12 @@ def filter( combinations.append((arch, combination)) return sorted( - combinations, key=lambda platform: platform[1].split("-")[0] + combinations, + key=lambda platform: platform[1].split("-")[0], ) # Sort by platform name @staticmethod - def group_by_arch(combinations): + def group_by_arch(combinations: List[Tuple[Any, str]]) -> Dict[str, List[str]]: """ Groups combinations by architecture into a dictionary. @@ -142,15 +145,18 @@ def group_by_arch(combinations): :since: 0.7.0 """ - arch_dict = {} + arch_dict: Dict[str, List[str]] = {} + for arch, combination in combinations: arch_dict.setdefault(arch, []).append(combination) + for arch in arch_dict: - arch_dict[arch] = sorted(set(arch_dict[arch])) # Deduplicate and sort + arch_dict[arch] = sorted(set(arch_dict[arch])) + return arch_dict @staticmethod - def remove_arch(combinations): + def remove_arch(combinations: List[Tuple[Any, str]]) -> List[str]: """ Removes the architecture from combinations. @@ -165,11 +171,13 @@ def remove_arch(combinations): ] @staticmethod - def should_exclude(combination, excludes, wildcard_excludes): + def should_exclude( + combination: str, excludes: List[str], wildcard_excludes: List[str] + ) -> bool: """ Checks if a combination should be excluded based on exact match or wildcard patterns. - :param combinations: Flavor combinations + :param combination: Feature :param excludes: List of features to exclude :param wildcard_excludes: List of feature wildcards to exclude @@ -186,12 +194,12 @@ def should_exclude(combination, excludes, wildcard_excludes): ) @staticmethod - def should_include_only(combination, include_only_patterns): + def should_include_only(combination: str, include_only_patterns: List[str]) -> bool: """ Checks if a combination should be included based on `--include-only` wildcard patterns. If no patterns are provided, all combinations are included by default. - :param combinations: Flavor combinations + :param combination: Feature :param include_only_patterns: List of features to include :return: (bool) True if included diff --git a/src/gardenlinux/git/repository.py b/src/gardenlinux/git/repository.py index 3af9190d..6b98ec7c 100755 --- a/src/gardenlinux/git/repository.py +++ b/src/gardenlinux/git/repository.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- +from logging import Logger from os import PathLike from pathlib import Path +from typing import Any, List, Optional from pygit2 import Oid from pygit2 import Repository as _Repository @@ -11,7 +13,7 @@ from ..logger import LoggerSetup -class Repository(_Repository): +class Repository(_Repository): # type: ignore[misc] """ Repository operations handler based on the given Git directory. @@ -24,7 +26,12 @@ class Repository(_Repository): Apache License, Version 2.0 """ - def __init__(self, git_directory: str | PathLike[str] = ".", logger=None, **kwargs): + def __init__( + self, + git_directory: PathLike[str] | str = ".", + logger: Optional[Logger] = None, + **kwargs: Any, + ): """ Constructor __init__(Repository) @@ -48,7 +55,7 @@ def __init__(self, git_directory: str | PathLike[str] = ".", logger=None, **kwar self._logger = logger @property - def commit_id(self): + def commit_id(self) -> str: """ Returns the commit ID for Git `HEAD`. @@ -59,7 +66,7 @@ def commit_id(self): return str(self.root_repo.head.target) @property - def root(self): + def root(self) -> Path: """ Returns the root directory of the current Git repository. @@ -88,7 +95,7 @@ def root(self): return Path(root_dir) @property - def root_repo(self): + def root_repo(self) -> Any: """ Returns the root Git `Repository` instance. @@ -105,14 +112,14 @@ def root_repo(self): @staticmethod def checkout_repo( - git_directory: str | PathLike[str], - repo_url=GL_REPOSITORY_URL, + git_directory: PathLike[str] | str, + repo_url: str = GL_REPOSITORY_URL, branch: str = "main", - commit: str | None = None, - pathspecs: list[str] | None = None, - logger=None, - **kwargs, - ): + commit: Optional[str] = None, + pathspecs: Optional[List[str]] = None, + logger: Optional[Logger] = None, + **kwargs: Any, + ) -> Any: """ Returns the root Git `Repo` instance. @@ -145,14 +152,14 @@ def checkout_repo( @staticmethod def checkout_repo_sparse( - git_directory, - pathspecs=[], - repo_url=GL_REPOSITORY_URL, - branch="main", - commit=None, - logger=None, - **kwargs, - ): + git_directory: PathLike[str] | str, + pathspecs: List[str] = [], + repo_url: str = GL_REPOSITORY_URL, + branch: str = "main", + commit: Optional[str] = None, + logger: Optional[Logger] = None, + **kwargs: Any, + ) -> Any: """ Sparse checkout given Git repository and return the `Repository` instance. diff --git a/src/gardenlinux/github/release/__main__.py b/src/gardenlinux/github/release/__main__.py index 498d4c99..61c37338 100644 --- a/src/gardenlinux/github/release/__main__.py +++ b/src/gardenlinux/github/release/__main__.py @@ -3,12 +3,12 @@ from gardenlinux.constants import GARDENLINUX_GITHUB_RELEASE_BUCKET_NAME from gardenlinux.logger import LoggerSetup +from ..release_notes import create_github_release_notes from . import ( create_github_release, upload_to_github_release_page, write_to_release_id_file, ) -from ..release_notes import create_github_release_notes LOGGER = LoggerSetup.get_logger("gardenlinux.github", "INFO") diff --git a/src/gardenlinux/logger.py b/src/gardenlinux/logger.py index 158c85ce..c362ac7a 100644 --- a/src/gardenlinux/logger.py +++ b/src/gardenlinux/logger.py @@ -1,13 +1,14 @@ # -*- coding: utf-8 -*- import logging +from typing import Optional class LoggerSetup: """Handles logging configuration for the gardenlinux library.""" @staticmethod - def get_logger(name, level=None): + def get_logger(name: str, level: Optional[int] = None) -> logging.Logger: """Create and configure a logger. Args: diff --git a/src/gardenlinux/oci/__main__.py b/src/gardenlinux/oci/__main__.py index 6f3a8fd8..1d6371ee 100755 --- a/src/gardenlinux/oci/__main__.py +++ b/src/gardenlinux/oci/__main__.py @@ -4,13 +4,16 @@ gl-oci main entrypoint """ +from typing import List + import click from .container import Container +from .image_manifest import ImageManifest @click.group() -def cli(): +def cli() -> None: """ gl-oci click argument entrypoint @@ -74,17 +77,17 @@ def cli(): help="Additional tag to push the manifest with", ) def push_manifest( - container, - cname, - arch, - version, - commit, - directory, - cosign_file, - manifest_file, - insecure, - additional_tag, -): + container: str, + cname: str, + arch: str, + version: str, + commit: str, + directory: str, + cosign_file: str, + manifest_file: str, + insecure: bool, + additional_tag: List[str], +) -> None: """ Push artifacts and the manifest from a directory to a registry. @@ -98,6 +101,9 @@ def push_manifest( manifest = container.read_or_generate_manifest(cname, arch, version, commit) + if not isinstance(manifest, ImageManifest): + raise RuntimeError("Data given for OCI image manifest is incomplete") + container.push_manifest_and_artifacts_from_directory( manifest, directory, manifest_file, additional_tag ) @@ -153,14 +159,14 @@ def push_manifest( help="Tag to push the manifest with", ) def push_manifest_tags( - container, - cname, - arch, - version, - commit, - insecure, - tag, -): + container: str, + cname: str, + arch: str, + version: str, + commit: str, + insecure: bool, + tag: List[str], +) -> None: """ Push artifacts and the manifest from a directory to a registry. @@ -207,7 +213,13 @@ def push_manifest_tags( multiple=True, help="Additional tag to push the index with", ) -def update_index(container, version, manifest_folder, insecure, additional_tag): +def update_index( + container: str, + version: str, + manifest_folder: str, + insecure: bool, + additional_tag: List[str], +) -> None: """ Push a list of files from the `manifest_folder` to an index. @@ -222,7 +234,7 @@ def update_index(container, version, manifest_folder, insecure, additional_tag): container.push_index_from_directory(manifest_folder, additional_tag) -def main(): +def main() -> None: """ gl-oci main() diff --git a/src/gardenlinux/oci/container.py b/src/gardenlinux/oci/container.py index 28b581a2..54c3eb52 100644 --- a/src/gardenlinux/oci/container.py +++ b/src/gardenlinux/oci/container.py @@ -12,7 +12,7 @@ from os import PathLike, fdopen, getenv from pathlib import Path from tempfile import mkstemp -from typing import Optional +from typing import Any, Dict, List, Optional from urllib.parse import urlsplit import jsonschema @@ -31,7 +31,7 @@ from .schemas import index as IndexSchema -class Container(Registry): +class Container(Registry): # type: ignore[misc] """ OCI container instance to provide methods for interaction. @@ -129,7 +129,7 @@ def generate_image_manifest( version: Optional[str] = None, commit: Optional[str] = None, feature_set: Optional[str] = None, - ): + ) -> ImageManifest: """ Generates an OCI image manifest @@ -159,9 +159,9 @@ def generate_image_manifest( manifest = ImageManifest() - manifest.version = version + manifest.version = version # type: ignore[assignment] manifest.cname = cname - manifest.arch = architecture + manifest.arch = architecture # type: ignore[assignment] manifest.feature_set = feature_set manifest.commit = commit @@ -182,7 +182,7 @@ def generate_image_manifest( return manifest - def generate_index(self): + def generate_index(self) -> Index: """ Generates an OCI image index @@ -196,7 +196,7 @@ def generate_manifest( self, version: Optional[str] = None, commit: Optional[str] = None, - ): + ) -> Manifest: """ Generates an OCI manifest @@ -212,14 +212,14 @@ def generate_manifest( manifest = Manifest() - manifest.version = version - manifest.commit = commit + manifest.version = version # type: ignore[assignment] + manifest.commit = commit # type: ignore[assignment] manifest.config_from_dict({}, {}) return manifest - def _get_index_without_response_parsing(self): + def _get_index_without_response_parsing(self) -> Response: """ Return the response of an OCI image index request. @@ -231,12 +231,12 @@ def _get_index_without_response_parsing(self): f"{self._container_name}:{self._container_version}" ).manifest_url() - return self.do_request( + return self.do_request( # type: ignore[no-any-return] f"{self.prefix}://{manifest_url}", headers={"Accept": OCI_IMAGE_INDEX_MEDIA_TYPE}, ) - def _get_manifest_without_response_parsing(self, reference): + def _get_manifest_without_response_parsing(self, reference: str) -> Response: """ Return the response of an OCI image manifest request. @@ -244,14 +244,16 @@ def _get_manifest_without_response_parsing(self, reference): :since: 0.7.0 """ - return self.do_request( + return self.do_request( # type: ignore[no-any-return] f"{self.prefix}://{self.hostname}/v2/{self._container_name}/manifests/{reference}", headers={"Accept": "application/vnd.oci.image.manifest.v1+json"}, ) def push_index_from_directory( - self, manifests_dir: PathLike | str, additional_tags: list = None - ): + self, + manifests_dir: PathLike[str] | str, + additional_tags: Optional[List[str]] = None, + ) -> None: """ Replaces an old manifest entries with new ones @@ -272,7 +274,7 @@ def push_index_from_directory( new_entries = 0 - for file_path_name in manifests_dir.iterdir(): + for file_path_name in manifests_dir.iterdir(): # type: ignore[attr-defined] with open(file_path_name, "r") as fp: manifest = json.loads(fp.read()) @@ -283,7 +285,7 @@ def push_index_from_directory( if manifest["digest"] == existing_manifest["digest"]: self._logger.debug( - f"Skipping manifest with digest {manifest["digest"]} - already exists" + f"Skipping manifest with digest {manifest['digest']} - already exists" ) continue @@ -291,7 +293,7 @@ def push_index_from_directory( index.append_manifest(manifest) self._logger.info( - f"Index appended locally {manifest["annotations"]["cname"]}" + f"Index appended locally {manifest['annotations']['cname']}" ) new_entries += 1 @@ -307,7 +309,7 @@ def push_index_from_directory( additional_tags, ) - def push_index_for_tags(self, index, tags): + def push_index_for_tags(self, index: Index, tags: List[str]) -> None: """ Push tags for an given OCI image index. @@ -325,7 +327,7 @@ def push_manifest( self, manifest: Manifest, manifest_file: Optional[str] = None, - additional_tags: Optional[list] = None, + additional_tags: Optional[List[str]] = None, ) -> Manifest: """ Pushes an OCI image manifest. @@ -358,9 +360,12 @@ def push_manifest( finally: Path(config_file).unlink() - manifest_container = OrasContainer( - f"{self._container_url}:{self._container_version}-{manifest.cname}-{manifest.arch}" - ) + manifest_url = f"{self._container_url}:{self._container_version}" + + if isinstance(manifest, ImageManifest): + manifest_url += f"-{manifest.cname}-{manifest.arch}" + + manifest_container = OrasContainer(manifest_url) self._check_200_response(self.upload_manifest(manifest, manifest_container)) @@ -376,7 +381,7 @@ def push_manifest( additional_tags, ) - if manifest_file is not None: + if manifest_file is not None and isinstance(manifest, ImageManifest): manifest.write_metadata_file(manifest_file) self._logger.info(f"Index entry written to {manifest_file}") @@ -384,11 +389,11 @@ def push_manifest( def push_manifest_and_artifacts( self, - manifest: Manifest, - artifacts_with_metadata: list[dict], - artifacts_dir: Optional[PathLike | str] = ".build", + manifest: ImageManifest, + artifacts_with_metadata: list[Dict[str, Any]], + artifacts_dir: PathLike[str] | str = ".build", manifest_file: Optional[str] = None, - additional_tags: Optional[list] = None, + additional_tags: Optional[List[str]] = None, ) -> Manifest: """ Pushes an OCI image manifest and its artifacts. @@ -403,7 +408,7 @@ def push_manifest_and_artifacts( :since: 0.7.0 """ - if not isinstance(manifest, Manifest): + if not isinstance(manifest, ImageManifest): raise RuntimeError("Artifacts image manifest given is invalid") if not isinstance(artifacts_dir, PathLike): @@ -413,7 +418,7 @@ def push_manifest_and_artifacts( # For each file, create sign, attach and push a layer for artifact in artifacts_with_metadata: - file_path_name = artifacts_dir.joinpath(artifact["file_name"]) + file_path_name = artifacts_dir.joinpath(artifact["file_name"]) # type: ignore[attr-defined] layer = Layer(file_path_name, artifact["media_type"]) @@ -440,7 +445,7 @@ def push_manifest_and_artifacts( ) self._logger.info( - f"Pushed {artifact["file_name"]}: {layer_dict["digest"]}" + f"Pushed {artifact['file_name']}: {layer_dict['digest']}" ) finally: if cleanup_blob and file_path_name.exists(): @@ -452,10 +457,10 @@ def push_manifest_and_artifacts( def push_manifest_and_artifacts_from_directory( self, - manifest: Manifest, - artifacts_dir: Optional[PathLike | str] = ".build", + manifest: ImageManifest, + artifacts_dir: PathLike[str] | str = ".build", manifest_file: Optional[str] = None, - additional_tags: Optional[list] = None, + additional_tags: Optional[List[str]] = None, ) -> Manifest: """ Pushes an OCI image manifest and its artifacts from the given directory. @@ -472,16 +477,18 @@ def push_manifest_and_artifacts_from_directory( if not isinstance(artifacts_dir, PathLike): artifacts_dir = Path(artifacts_dir) - if not isinstance(manifest, Manifest): + if not isinstance(manifest, ImageManifest): raise RuntimeError("Artifacts image manifest given is invalid") # Scan and extract nested artifacts - for file_path_name in artifacts_dir.glob("*.pxe.tar.gz"): + for file_path_name in artifacts_dir.glob("*.pxe.tar.gz"): # type: ignore[attr-defined] self._logger.info(f"Found nested artifact {file_path_name}") extract_targz(file_path_name, artifacts_dir) files = [ - file_name for file_name in artifacts_dir.iterdir() if file_name.is_file() + file_name + for file_name in artifacts_dir.iterdir() # type: ignore[attr-defined] + if file_name.is_file() ] artifacts_with_metadata = Container.get_artifacts_metadata_from_files( @@ -491,7 +498,7 @@ def push_manifest_and_artifacts_from_directory( for artifact in artifacts_with_metadata: if artifact["media_type"] == "application/io.gardenlinux.release": artifact_config = ConfigParser(allow_unnamed_section=True) - artifact_config.read(artifacts_dir.joinpath(artifact["file_name"])) + artifact_config.read(artifacts_dir.joinpath(artifact["file_name"])) # type: ignore[attr-defined] if artifact_config.has_option(UNNAMED_SECTION, "GARDENLINUX_FEATURES"): manifest.feature_set = artifact_config.get( @@ -513,7 +520,7 @@ def push_manifest_and_artifacts_from_directory( additional_tags, ) - def push_manifest_for_tags(self, manifest, tags): + def push_manifest_for_tags(self, manifest: Manifest, tags: List[str]) -> None: """ Push tags for an given OCI image manifest. @@ -529,11 +536,11 @@ def push_manifest_for_tags(self, manifest, tags): self._check_200_response(self.upload_manifest(manifest, manifest_container)) - def read_or_generate_index(self): + def read_or_generate_index(self) -> Index: """ Reads from registry or generates the OCI image index. - :return: OCI image manifest + :return: OCI image index :since: 0.7.0 """ @@ -555,7 +562,7 @@ def read_or_generate_manifest( version: Optional[str] = None, commit: Optional[str] = None, feature_set: Optional[str] = None, - ) -> Manifest: + ) -> Manifest | ImageManifest: """ Reads from registry or generates the OCI manifest. @@ -599,7 +606,7 @@ def read_or_generate_manifest( return manifest - def _upload_index(self, index: dict, reference: Optional[str] = None) -> Response: + def _upload_index(self, index: Index, reference: Optional[str] = None) -> Response: """ Uploads the given OCI image index and returns the response. @@ -615,7 +622,7 @@ def _upload_index(self, index: dict, reference: Optional[str] = None) -> Respons if reference is None: reference = self._container_version - return self.do_request( + return self.do_request( # type: ignore[no-any-return] f"{self.prefix}://{self.hostname}/v2/{self._container_name}/manifests/{reference}", "PUT", headers={"Content-Type": OCI_IMAGE_INDEX_MEDIA_TYPE}, @@ -623,7 +630,9 @@ def _upload_index(self, index: dict, reference: Optional[str] = None) -> Respons ) @staticmethod - def get_artifacts_metadata_from_files(files: list, arch: str) -> list: + def get_artifacts_metadata_from_files( + files: List[str], arch: str + ) -> List[Dict[str, Any]]: """ Returns OCI layer metadata for the given list of files. diff --git a/src/gardenlinux/oci/image_manifest.py b/src/gardenlinux/oci/image_manifest.py index e1a581c1..12bd6d2e 100644 --- a/src/gardenlinux/oci/image_manifest.py +++ b/src/gardenlinux/oci/image_manifest.py @@ -4,6 +4,7 @@ from copy import deepcopy from os import PathLike from pathlib import Path +from typing import Any, Dict from oras.oci import Layer @@ -27,7 +28,7 @@ class ImageManifest(Manifest): """ @property - def arch(self): + def arch(self) -> str: """ Returns the architecture of the OCI image manifest. @@ -40,10 +41,10 @@ def arch(self): "Unexpected manifest with missing config annotation 'architecture' found" ) - return self["annotations"]["architecture"] + return self["annotations"]["architecture"] # type: ignore[no-any-return] @arch.setter - def arch(self, value): + def arch(self, value: str) -> None: """ Sets the architecture of the OCI image manifest. @@ -56,7 +57,7 @@ def arch(self, value): self["annotations"]["architecture"] = value @property - def cname(self): + def cname(self) -> str: """ Returns the GardenLinux canonical name of the OCI image manifest. @@ -69,10 +70,10 @@ def cname(self): "Unexpected manifest with missing config annotation 'cname' found" ) - return self["annotations"]["cname"] + return self["annotations"]["cname"] # type: ignore[no-any-return] @cname.setter - def cname(self, value): + def cname(self, value: str) -> None: """ Sets the GardenLinux canonical name of the OCI image manifest. @@ -85,7 +86,7 @@ def cname(self, value): self["annotations"]["cname"] = value @property - def feature_set(self): + def feature_set(self) -> str: """ Returns the GardenLinux feature set of the OCI image manifest. @@ -98,10 +99,10 @@ def feature_set(self): "Unexpected manifest with missing config annotation 'feature_set' found" ) - return self["annotations"]["feature_set"] + return self["annotations"]["feature_set"] # type: ignore[no-any-return] @feature_set.setter - def feature_set(self, value): + def feature_set(self, value: str) -> None: """ Sets the GardenLinux feature set of the OCI image manifest. @@ -114,7 +115,7 @@ def feature_set(self, value): self["annotations"]["feature_set"] = value @property - def flavor(self): + def flavor(self) -> str: """ Returns the GardenLinux flavor of the OCI image manifest. @@ -125,7 +126,7 @@ def flavor(self): return CName(self.cname).flavor @property - def layers_as_dict(self): + def layers_as_dict(self) -> Dict[str, Any]: """ Returns the OCI image manifest layers as a dictionary. @@ -146,7 +147,7 @@ def layers_as_dict(self): return layers @property - def version(self): + def version(self) -> str: """ Returns the GardenLinux version of the OCI image manifest. @@ -159,10 +160,10 @@ def version(self): "Unexpected manifest with missing config annotation 'version' found" ) - return self["annotations"]["version"] + return self["annotations"]["version"] # type: ignore[no-any-return] @version.setter - def version(self, value): + def version(self, value: str) -> None: """ Sets the GardenLinux version of the OCI image manifest. @@ -174,7 +175,7 @@ def version(self, value): self._ensure_annotations_dict() self["annotations"]["version"] = value - def append_layer(self, layer): + def append_layer(self, layer: Layer) -> None: """ Appends the given OCI image manifest layer to the manifest @@ -217,7 +218,15 @@ def append_layer(self, layer): self["layers"].append(layer_dict) - def write_metadata_file(self, manifest_file_path_name): + def write_metadata_file(self, manifest_file_path_name: PathLike[str] | str) -> None: + """ + Create OCI image manifest metadata and write it to the file given. + + :param manifest_file_path_name: OCI image manifest metadata file + + :since: 0.7.0 + """ + if not isinstance(manifest_file_path_name, PathLike): manifest_file_path_name = Path(manifest_file_path_name) diff --git a/src/gardenlinux/oci/index.py b/src/gardenlinux/oci/index.py index 94a31437..14327987 100644 --- a/src/gardenlinux/oci/index.py +++ b/src/gardenlinux/oci/index.py @@ -2,11 +2,12 @@ import json from copy import deepcopy +from typing import Any, Dict from .schemas import EmptyIndex -class Index(dict): +class Index(dict): # type: ignore[type-arg] """ OCI image index @@ -19,7 +20,7 @@ class Index(dict): Apache License, Version 2.0 """ - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any): """ Constructor __init__(Index) @@ -33,7 +34,7 @@ def __init__(self, *args, **kwargs): self.update(**kwargs) @property - def json(self): + def json(self) -> bytes: """ Returns the OCI image index as a JSON @@ -44,7 +45,7 @@ def json(self): return json.dumps(self).encode("utf-8") @property - def manifests_as_dict(self): + def manifests_as_dict(self) -> Dict[str, Dict[str, Any]]: """ Returns the OCI image manifests of the index @@ -64,7 +65,7 @@ def manifests_as_dict(self): return manifests - def append_manifest(self, manifest): + def append_manifest(self, manifest: Dict[str, Any]) -> None: """ Appends the given OCI image manifest to the index diff --git a/src/gardenlinux/oci/layer.py b/src/gardenlinux/oci/layer.py index c2ec7b70..c01e01e1 100644 --- a/src/gardenlinux/oci/layer.py +++ b/src/gardenlinux/oci/layer.py @@ -3,7 +3,7 @@ from collections.abc import Mapping from os import PathLike from pathlib import Path -from typing import Optional +from typing import Any, Dict, Iterator, Optional from oras.defaults import annotation_title as ANNOTATION_TITLE from oras.oci import Layer as _Layer @@ -13,7 +13,7 @@ _SUPPORTED_MAPPING_KEYS = ("annotations",) -class Layer(_Layer, Mapping): +class Layer(_Layer, Mapping): # type: ignore[misc, type-arg] """ OCI image layer @@ -28,7 +28,7 @@ class Layer(_Layer, Mapping): def __init__( self, - blob_path: PathLike | str, + blob_path: PathLike[str] | str, media_type: Optional[str] = None, is_dir: bool = False, ): @@ -48,23 +48,24 @@ def __init__( _Layer.__init__(self, blob_path, media_type, is_dir) self._annotations = { - ANNOTATION_TITLE: blob_path.name, + ANNOTATION_TITLE: blob_path.name, # type: ignore[attr-defined] } @property - def dict(self): + def dict(self) -> Dict[Any, Any]: """ Return a dictionary representation of the layer :return: (dict) OCI manifest layer metadata dictionary :since: 0.7.2 """ + layer = _Layer.to_dict(self) layer["annotations"] = self._annotations - return layer + return layer # type: ignore[no-any-return] - def __delitem__(self, key): + def __delitem__(self, key: str) -> None: """ python.org: Called to implement deletion of self[key]. @@ -80,7 +81,7 @@ def __delitem__(self, key): f"'{self.__class__.__name__}' object is not subscriptable except for keys: {_SUPPORTED_MAPPING_KEYS}" ) - def __getitem__(self, key): + def __getitem__(self, key: str) -> Any: """ python.org: Called to implement evaluation of self[key]. @@ -97,7 +98,7 @@ def __getitem__(self, key): f"'{self.__class__.__name__}' object is not subscriptable except for keys: {_SUPPORTED_MAPPING_KEYS}" ) - def __iter__(self): + def __iter__(self) -> Iterator[str]: """ python.org: Return an iterator object. @@ -107,7 +108,7 @@ def __iter__(self): return iter(_SUPPORTED_MAPPING_KEYS) - def __len__(self): + def __len__(self) -> int: """ python.org: Called to implement the built-in function len(). @@ -117,7 +118,7 @@ def __len__(self): return len(_SUPPORTED_MAPPING_KEYS) - def __setitem__(self, key, value): + def __setitem__(self, key: str, value: Any) -> None: """ python.org: Called to implement assignment to self[key]. @@ -135,7 +136,9 @@ def __setitem__(self, key, value): ) @staticmethod - def generate_metadata_from_file_name(file_name: PathLike | str, arch: str) -> dict: + def generate_metadata_from_file_name( + file_name: PathLike[str] | str, arch: str + ) -> Dict[str, Any]: """ Generates OCI manifest layer metadata for the given file path and name. @@ -152,13 +155,13 @@ def generate_metadata_from_file_name(file_name: PathLike | str, arch: str) -> di media_type = Layer.lookup_media_type_for_file_name(file_name) return { - "file_name": file_name.name, + "file_name": file_name.name, # type: ignore[attr-defined] "media_type": media_type, "annotations": {"io.gardenlinux.image.layer.architecture": arch}, } @staticmethod - def lookup_media_type_for_file_name(file_name: str) -> str: + def lookup_media_type_for_file_name(file_name: PathLike[str] | str) -> str: """ Looks up the media type based on file name or extension. @@ -172,7 +175,7 @@ def lookup_media_type_for_file_name(file_name: str) -> str: file_name = Path(file_name) for lookup_name in GL_MEDIA_TYPES: - if file_name.match(f"*.{lookup_name}") or file_name.name == lookup_name: + if file_name.match(f"*.{lookup_name}") or file_name.name == lookup_name: # type: ignore[attr-defined] return GL_MEDIA_TYPE_LOOKUP[lookup_name] raise ValueError( diff --git a/src/gardenlinux/oci/manifest.py b/src/gardenlinux/oci/manifest.py index ed521829..8a877bf1 100644 --- a/src/gardenlinux/oci/manifest.py +++ b/src/gardenlinux/oci/manifest.py @@ -3,12 +3,13 @@ import json from copy import deepcopy from hashlib import sha256 +from typing import Any, Dict from oras.defaults import unknown_config_media_type as UNKNOWN_CONFIG_MEDIA_TYPE from oras.oci import EmptyManifest -class Manifest(dict): +class Manifest(dict): # type: ignore[type-arg] """ OCI image manifest @@ -21,7 +22,7 @@ class Manifest(dict): Apache License, Version 2.0 """ - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any): """ Constructor __init__(Manifest) @@ -37,7 +38,7 @@ def __init__(self, *args, **kwargs): self.update(**kwargs) @property - def commit(self): + def commit(self) -> str: """ Returns the GardenLinux Git commit ID of the OCI manifest. @@ -50,10 +51,10 @@ def commit(self): "Unexpected manifest with missing config annotation 'commit' found" ) - return self["annotations"]["commit"] + return self["annotations"]["commit"] # type: ignore[no-any-return] @commit.setter - def commit(self, value): + def commit(self, value: str) -> None: """ Sets the GardenLinux Git commit ID of the OCI manifest. @@ -66,7 +67,7 @@ def commit(self, value): self["annotations"]["commit"] = value @property - def config_json(self): + def config_json(self) -> bytes: """ Returns the OCI image manifest config. @@ -77,7 +78,7 @@ def config_json(self): return self._config_bytes @property - def digest(self): + def digest(self) -> str: """ Returns the OCI image manifest digest. @@ -89,7 +90,7 @@ def digest(self): return f"sha256:{digest}" @property - def json(self): + def json(self) -> bytes: """ Returns the OCI image manifest as a JSON @@ -100,7 +101,7 @@ def json(self): return json.dumps(self).encode("utf-8") @property - def size(self): + def size(self) -> int: """ Returns the OCI image manifest JSON size in bytes. @@ -111,7 +112,7 @@ def size(self): return len(self.json) @property - def version(self): + def version(self) -> str: """ Returns the GardenLinux version of the OCI image manifest. @@ -124,10 +125,10 @@ def version(self): "Unexpected manifest with missing config annotation 'version' found" ) - return self["annotations"]["version"] + return self["annotations"]["version"] # type: ignore[no-any-return] @version.setter - def version(self, value): + def version(self, value: str) -> None: """ Sets the GardenLinux version of the OCI image manifest. @@ -139,7 +140,9 @@ def version(self, value): self._ensure_annotations_dict() self["annotations"]["version"] = value - def config_from_dict(self, config: dict, annotations: dict): + def config_from_dict( + self, config: Dict[str, Any], annotations: Dict[str, Any] + ) -> None: """ Write a new OCI configuration to file, and generate oci metadata for it. @@ -162,6 +165,6 @@ def config_from_dict(self, config: dict, annotations: dict): self["config"] = config - def _ensure_annotations_dict(self): + def _ensure_annotations_dict(self) -> None: if "annotations" not in self: self["annotations"] = {} diff --git a/src/gardenlinux/oci/platform.py b/src/gardenlinux/oci/platform.py index f4085695..95dc336c 100644 --- a/src/gardenlinux/oci/platform.py +++ b/src/gardenlinux/oci/platform.py @@ -1,11 +1,12 @@ # -*- coding: utf-8 -*- from copy import deepcopy +from typing import Any, Dict from .schemas import EmptyPlatform -def NewPlatform(architecture: str, version: str) -> dict: +def NewPlatform(architecture: str, version: str) -> Dict[str, Any]: platform = deepcopy(EmptyPlatform) platform["architecture"] = architecture platform["os.version"] = version diff --git a/src/gardenlinux/s3/bucket.py b/src/gardenlinux/s3/bucket.py index 403e8c95..85142255 100644 --- a/src/gardenlinux/s3/bucket.py +++ b/src/gardenlinux/s3/bucket.py @@ -9,7 +9,7 @@ from os import PathLike from pathlib import Path from time import time -from typing import Any, Optional +from typing import Any, BinaryIO, List, Optional import boto3 @@ -62,7 +62,7 @@ def __init__( self._logger = logger @property - def objects(self): + def objects(self) -> List[Any]: """ Returns a list of all objects in a bucket. @@ -72,9 +72,9 @@ def objects(self): self._logger.debug(f"Returning all S3 bucket objects for {self._bucket.name}") - return self._bucket.objects.all() + return self._bucket.objects.all() # type: ignore[no-any-return] - def __getattr__(self, name): + def __getattr__(self, name: str) -> Any: """ python.org: Called when an attribute lookup has not found the attribute in the usual places (i.e. it is not an instance attribute nor is it found in the @@ -88,7 +88,9 @@ class tree for self). return getattr(self._bucket, name) - def download_file(self, key, file_name, *args, **kwargs): + def download_file( + self, key: str, file_name: str, *args: Any, **kwargs: Any + ) -> None: """ boto3: Download an S3 object to a file. @@ -102,7 +104,9 @@ def download_file(self, key, file_name, *args, **kwargs): self._logger.info(f"Downloaded {key} from S3 to {file_name}") - def download_fileobj(self, key, fp, *args, **kwargs): + def download_fileobj( + self, key: str, fp: BinaryIO, *args: Any, **kwargs: Any + ) -> None: """ boto3: Download an object from this bucket to a file-like-object. @@ -117,8 +121,11 @@ def download_fileobj(self, key, fp, *args, **kwargs): self._logger.info(f"Downloaded {key} from S3 as binary data") def read_cache_file_or_filter( - self, cache_file: str | PathLike[str] | None, cache_ttl: int = 3600, **kwargs - ): + self, + cache_file: Optional[PathLike[str] | str], + cache_ttl: int = 3600, + **kwargs: Any, + ) -> List[Any]: """ Read S3 object keys from cache if valid or filter for S3 object keys. @@ -138,7 +145,7 @@ def read_cache_file_or_filter( and (time() - cache_path.stat().st_mtime) < cache_ttl ): with cache_path.open("r") as fp: - return json.loads(fp.read()) + return json.loads(fp.read()) # type: ignore[no-any-return] else: cache_path = None @@ -152,7 +159,7 @@ def read_cache_file_or_filter( return artifacts - def upload_file(self, file_name, key, *args, **kwargs): + def upload_file(self, file_name: str, key: str, *args: Any, **kwargs: Any) -> None: """ boto3: Upload a file to an S3 object. @@ -166,7 +173,7 @@ def upload_file(self, file_name, key, *args, **kwargs): self._logger.info(f"Uploaded {key} to S3 for {file_name}") - def upload_fileobj(self, fp, key, *args, **kwargs): + def upload_fileobj(self, fp: BinaryIO, key: str, *args: Any, **kwargs: Any) -> None: """ boto3: Upload a file-like object to this bucket. diff --git a/src/gardenlinux/s3/s3_artifacts.py b/src/gardenlinux/s3/s3_artifacts.py index 826536a9..446206ea 100644 --- a/src/gardenlinux/s3/s3_artifacts.py +++ b/src/gardenlinux/s3/s3_artifacts.py @@ -56,7 +56,7 @@ def __init__( self._bucket = Bucket(bucket_name, endpoint_url, s3_resource_config, logger) @property - def bucket(self): + def bucket(self) -> Bucket: """ Returns the underlying S3 bucket. @@ -67,10 +67,8 @@ def bucket(self): return self._bucket def download_to_directory( - self, - cname: str, - artifacts_dir: str | PathLike[str], - ): + self, cname: str, artifacts_dir: PathLike[str] | str + ) -> None: """ Download S3 artifacts to a given directory. @@ -86,25 +84,25 @@ def download_to_directory( raise RuntimeError(f"Artifacts directory given is invalid: {artifacts_dir}") release_object = list( - self._bucket.objects.filter(Prefix=f"meta/singles/{cname}") + self._bucket.objects.filter(Prefix=f"meta/singles/{cname}") # type: ignore[attr-defined] )[0] self._bucket.download_file( - release_object.key, artifacts_dir.joinpath(f"{cname}.s3_metadata.yaml") + release_object.key, str(artifacts_dir.joinpath(f"{cname}.s3_metadata.yaml")) ) - for s3_object in self._bucket.objects.filter(Prefix=f"objects/{cname}").all(): + for s3_object in self._bucket.objects.filter(Prefix=f"objects/{cname}").all(): # type: ignore[attr-defined] self._bucket.download_file( - s3_object.key, artifacts_dir.joinpath(basename(s3_object.key)) + s3_object.key, str(artifacts_dir.joinpath(basename(s3_object.key))) ) def upload_from_directory( self, cname: str, - artifacts_dir: str | PathLike[str], - delete_before_push=False, - dry_run=False, - ): + artifacts_dir: PathLike[str] | str, + delete_before_push: bool = False, + dry_run: bool = False, + ) -> None: """ Pushes S3 artifacts to the underlying bucket. @@ -128,7 +126,14 @@ def upload_from_directory( cname_object.load_from_release_file(release_file) if cname_object.arch is None: - raise RuntimeError("Architecture could not be determined from cname") + raise RuntimeError( + "Architecture could not be determined from GardenLinux canonical name or release file" + ) + + if cname_object.version_and_commit_id is None: + raise RuntimeError( + "Version information could not be determined from GardenLinux canonical name or release file" + ) feature_list = cname_object.feature_set requirements_file = artifacts_dir.joinpath(f"{cname}.requirements") @@ -153,7 +158,7 @@ def upload_from_directory( if secureboot is None: secureboot = "_trustedboot" in feature_list - version_epoch = cname_object.version_epoch + version_epoch = str(cname_object.version_epoch) if version_epoch is None: version_epoch = "" @@ -208,7 +213,7 @@ def upload_from_directory( s3_tags = { "architecture": re_object.sub("+", cname_object.arch), "platform": re_object.sub("+", cname_object.platform), - "version": re_object.sub("+", cname_object.version), + "version": re_object.sub("+", cname_object.version), # type: ignore[arg-type] "committish": cname_object.commit_hash, "md5sum": md5sum, "sha256sum": sha256sum, @@ -219,7 +224,7 @@ def upload_from_directory( self._bucket.delete_objects(Delete={"Objects": [{"Key": s3_key}]}) self._bucket.upload_file( - artifact, + str(artifact), s3_key, ExtraArgs={"Tagging": urlencode(s3_tags)}, ) diff --git a/tests/apt/test_debsource.py b/tests/apt/test_debsource.py index cf7988a9..2e9ac2ad 100644 --- a/tests/apt/test_debsource.py +++ b/tests/apt/test_debsource.py @@ -86,7 +86,7 @@ """ -def test_parse_debsource_file(): +def test_parse_debsource_file() -> None: expected = sorted( [ "vim-common 2:9.1.0496-1", diff --git a/tests/apt/test_package_repo_info.py b/tests/apt/test_package_repo_info.py index 0742e009..917a38b0 100644 --- a/tests/apt/test_package_repo_info.py +++ b/tests/apt/test_package_repo_info.py @@ -1,4 +1,8 @@ from types import SimpleNamespace +from typing import List + +import pytest +from apt_repo import BinaryPackage import gardenlinux.apt.package_repo_info as repoinfo @@ -11,18 +15,18 @@ class FakeAPTRepo: - exposes `.packages` and `get_packages_by_name(name)` """ - def __init__(self, url, dist, components) -> None: + def __init__(self, url: str, dist: str, components: List[str]) -> None: self.url = url self.dist = dist self.components = components # list of objects with .package and .version attributes - self.packages = [] + self.packages: List[BinaryPackage] = [] - def get_packages_by_name(self, name): + def get_packages_by_name(self, name: str) -> BinaryPackage: return [p for p in self.packages if p.package == name] -def test_gardenlinuxrepo_init(monkeypatch): +def test_gardenlinuxrepo_init(monkeypatch: pytest.MonkeyPatch) -> None: """ Test if GardenLinuxRepo creates an internal APTRepo """ @@ -44,7 +48,7 @@ def test_gardenlinuxrepo_init(monkeypatch): assert gr.repo.components == gr.components -def test_get_package_version_by_name(monkeypatch): +def test_get_package_version_by_name(monkeypatch: pytest.MonkeyPatch) -> None: # Arrange monkeypatch.setattr(repoinfo, "APTRepository", FakeAPTRepo) gr = repoinfo.GardenLinuxRepo("d") @@ -52,7 +56,7 @@ def test_get_package_version_by_name(monkeypatch): gr.repo.packages = [ SimpleNamespace(package="pkg-a", version="1.0"), SimpleNamespace(package="pkg-b", version="2.0"), - ] # type: ignore + ] # Act result = gr.get_package_version_by_name("pkg-a") @@ -61,14 +65,16 @@ def test_get_package_version_by_name(monkeypatch): assert result == [("pkg-a", "1.0")] -def test_get_packages_versions_returns_all_pairs(monkeypatch): +def test_get_packages_versions_returns_all_pairs( + monkeypatch: pytest.MonkeyPatch, +) -> None: # Arrange monkeypatch.setattr(repoinfo, "APTRepository", FakeAPTRepo) gr = repoinfo.GardenLinuxRepo("d") gr.repo.packages = [ SimpleNamespace(package="aa", version="0.1"), SimpleNamespace(package="bb", version="0.2"), - ] # type: ignore + ] # Act pv = gr.get_packages_versions() @@ -77,7 +83,7 @@ def test_get_packages_versions_returns_all_pairs(monkeypatch): assert pv == [("aa", "0.1"), ("bb", "0.2")] -def test_compare_repo_union_returns_all(): +def test_compare_repo_union_returns_all() -> None: """ When available_in_both=False, compare_repo returns entries for: - only names in A @@ -100,7 +106,7 @@ def test_compare_repo_union_returns_all(): assert set(result) == expected -def test_compare_repo_intersection_only(): +def test_compare_repo_intersection_only() -> None: """ When available_in_both=True, only intersection names are considered; differences are only returned if versions differ. @@ -116,7 +122,7 @@ def test_compare_repo_intersection_only(): assert set(result) == {("b", "2", "3")} -def test_compare_same_returns_empty(): +def test_compare_same_returns_empty() -> None: """ When both sets are identical, compare_repo should return an empty set. """ @@ -128,7 +134,7 @@ def test_compare_same_returns_empty(): assert repoinfo.compare_repo(a, b, available_in_both=False) == [] # type: ignore -def test_compare_empty_returns_empty(): +def test_compare_empty_returns_empty() -> None: """ If both sets are empty, compare_repo should return an empty set. """ diff --git a/tests/conftest.py b/tests/conftest.py index b99a3485..6892fb46 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,11 @@ import json import os import shutil +import subprocess import sys from datetime import datetime, timedelta from tempfile import mkstemp +from typing import Any, Dict, Generator import pytest from cryptography import x509 @@ -29,7 +31,7 @@ from .helper import spawn_background_process -def generate_test_certificates(): +def generate_test_certificates() -> None: """Generate self-signed certificates for testing using cryptography library""" os.makedirs(CERT_DIR, exist_ok=True) @@ -80,7 +82,7 @@ def generate_test_certificates(): print(f"Generated test certificates in {CERT_DIR}") -def create_test_data(): +def create_test_data() -> None: """Generate test data for OCI registry tests (replaces build-test-data.sh)""" print("Creating fake artifacts...") @@ -122,13 +124,13 @@ def create_test_data(): f.write(f"dummy content for {file_path}") -def write_zot_config(config_dict, fd): +def write_zot_config(config_dict: Dict[str, Any], fd: int) -> None: with os.fdopen(fd, "w") as fp: json.dump(config_dict, fp, indent=4) -@pytest.fixture(autouse=False, scope="function") -def zot_session(): +@pytest.fixture(autouse=False, scope="function") # type: ignore[misc] +def zot_session() -> Generator[subprocess.Popen[Any]]: load_dotenv() print("start zot session") @@ -175,7 +177,7 @@ def zot_session(): os.remove(zot_config_file_path) -def pytest_sessionstart(session): +def pytest_sessionstart(session: pytest.Session) -> None: generate_test_certificates() # Replace the bash script call with our Python function @@ -185,7 +187,7 @@ def pytest_sessionstart(session): Parser.set_default_gardenlinux_root_dir(GL_ROOT_DIR) -def pytest_sessionfinish(session): +def pytest_sessionfinish(session: pytest.Session) -> None: if os.path.isfile(CERT_DIR + "/oci-sign.crt"): os.remove(CERT_DIR + "/oci-sign.crt") if os.path.isfile(CERT_DIR + "/oci-sign.key"): diff --git a/tests/features/constants.py b/tests/features/constants.py index d594942b..e99ac286 100644 --- a/tests/features/constants.py +++ b/tests/features/constants.py @@ -7,7 +7,7 @@ ) -def generate_container_amd64_release_metadata(version, commit_hash): +def generate_container_amd64_release_metadata(version: str, commit_hash: str) -> str: return f""" ID={GL_RELEASE_ID} ID_LIKE=debian diff --git a/tests/features/test_cname.py b/tests/features/test_cname.py index de619a55..9d70e2ce 100644 --- a/tests/features/test_cname.py +++ b/tests/features/test_cname.py @@ -3,7 +3,7 @@ from gardenlinux.features import CName -@pytest.mark.parametrize( +@pytest.mark.parametrize( # type: ignore[misc] "input_cname, expected_output", [ ( @@ -24,7 +24,7 @@ ), ], ) -def test_cname_flavor(input_cname: str, expected_output: dict): +def test_cname_flavor(input_cname: str, expected_output: str) -> None: """ Tests if cname returns the dict with expected features. @@ -36,7 +36,7 @@ def test_cname_flavor(input_cname: str, expected_output: dict): assert cname.flavor == expected_output -def test_cname_commit_id_setter(): +def test_cname_commit_id_setter() -> None: """ Tests cname setter for `commit_id` to verify a given ID before overwriting. """ diff --git a/tests/features/test_cname_main.py b/tests/features/test_cname_main.py index 23625420..d831225a 100644 --- a/tests/features/test_cname_main.py +++ b/tests/features/test_cname_main.py @@ -1,14 +1,18 @@ import logging import sys import types +from typing import Any, List, Tuple +import networkx import pytest import gardenlinux.features.cname_main as cname_main from gardenlinux.features import Parser -def test_main_happy(monkeypatch, capsys): +def test_main_happy( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture +) -> None: """ Test the "Happy Path" of the main() function. """ @@ -17,18 +21,20 @@ def test_main_happy(monkeypatch, capsys): monkeypatch.setattr(sys, "argv", argv) class FakeGraph: - in_degree = lambda self: [("f1", 0)] + def in_degree(self) -> List[Tuple[str, int]]: + return [("f1", 0)] + edges = [("f1", "f2")] class FakeParser(Parser): - def __init__(self, *a, **k): + def __init__(self, *a: Any, **k: Any): pass - def filter(self, *a, **k): + def filter(self, *a: Any, **k: Any) -> networkx.Graph: return FakeGraph() @staticmethod - def sort_graph_nodes(graph): + def sort_graph_nodes(graph: networkx.Graph) -> List[str]: return ["f1", "f2"] monkeypatch.setattr(cname_main, "Parser", FakeParser) @@ -42,7 +48,9 @@ def sort_graph_nodes(graph): assert "amd64" in out -def test_main_version_from_file(monkeypatch, capsys): +def test_main_version_from_file( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture +) -> None: """ "Happy Path" test for grabbing the version and commit id from file in main(). """ @@ -57,14 +65,14 @@ def test_main_version_from_file(monkeypatch, capsys): ) class FakeParser(Parser): - def __init__(self, *a, **k): + def __init__(self, *a: Any, **k: Any): pass - def filter(self, *a, **k): + def filter(self, *a: Any, **k: Any) -> networkx.Graph: return types.SimpleNamespace(in_degree=lambda: [("f1", 0)], edges=[]) @staticmethod - def sort_graph_nodes(graph): + def sort_graph_nodes(graph: networkx.Graph) -> List[str]: return ["f1"] monkeypatch.setattr(cname_main, "Parser", FakeParser) @@ -76,7 +84,9 @@ def sort_graph_nodes(graph): assert "2.0-abcdef12" in capsys.readouterr().out -def test_cname_main_version_file_missing_warns(monkeypatch, caplog): +def test_cname_main_version_file_missing_warns( + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture +) -> None: """ Check if a warning is logged when it fails to read version and commit id files. @@ -89,7 +99,7 @@ def test_cname_main_version_file_missing_warns(monkeypatch, caplog): monkeypatch.setattr(sys, "argv", argv) # Patch version fatch function to raise RuntimeError (Simulates missing files) - def raise_runtime(_): + def raise_runtime(*args: Any, **kwargs: Any) -> None: raise RuntimeError("missing") monkeypatch.setattr( @@ -98,15 +108,15 @@ def raise_runtime(_): # Patch Parser for minimal valid graph class FakeParser(Parser): - def __init__(self, *a, **k): + def __init__(self, *a: Any, **k: Any): pass # Return object with in_degree method returning a node with zero dependencies - def filter(self, *a, **k): + def filter(self, *a: Any, **k: Any) -> networkx.Graph: return types.SimpleNamespace(in_degree=lambda: [("f1", 0)], edges=[]) @staticmethod - def sort_graph_nodes(graph): + def sort_graph_nodes(graph: networkx.Graph) -> List[str]: return ["f1"] monkeypatch.setattr(cname_main, "Parser", FakeParser) @@ -121,7 +131,7 @@ def sort_graph_nodes(graph): assert "Failed to parse version information" in caplog.text -def test_cname_main_invalid_cname_raises(monkeypatch): +def test_cname_main_invalid_cname_raises(monkeypatch: pytest.MonkeyPatch) -> None: """ Test if AssertionError is raised with an invalid or malformed cname. """ @@ -134,7 +144,9 @@ def test_cname_main_invalid_cname_raises(monkeypatch): cname_main.main() -def test_cname_main_missing_arch_in_cname_raises(monkeypatch): +def test_cname_main_missing_arch_in_cname_raises( + monkeypatch: pytest.MonkeyPatch, +) -> None: """ Test if an assertion error is raised when the arch argument is missing. """ diff --git a/tests/features/test_main.py b/tests/features/test_main.py index a3483306..dcebd537 100644 --- a/tests/features/test_main.py +++ b/tests/features/test_main.py @@ -2,6 +2,7 @@ import types from pathlib import Path from tempfile import TemporaryDirectory +from typing import Any, List, Tuple import pytest @@ -16,7 +17,7 @@ # ------------------------------- -def test_graph_mermaid(): +def test_graph_mermaid() -> None: # Arrange class FakeGraph: edges = [("a", "b"), ("b", "c")] @@ -32,7 +33,7 @@ class FakeGraph: assert "b-->c" in markup -def test_graph_mermaid_raises_no_flavor(): +def test_graph_mermaid_raises_no_flavor() -> None: # Arrange class MockGraph: edges = [("x", "y"), ("y", "z")] @@ -44,10 +45,10 @@ class MockGraph: fema.graph_as_mermaid_markup(None, MockGraph()) -def test_get_minimal_feature_set_filters(): +def test_get_minimal_feature_set_filters() -> None: # Arrange class FakeGraph: - def in_degree(self): + def in_degree(self) -> List[Tuple[str, int]]: return [("a", 0), ("b", 1), ("c", 0)] graph = FakeGraph() @@ -59,7 +60,7 @@ def in_degree(self): assert result == {"a", "c"} -def test_get_version_and_commit_from_file(tmp_path): +def test_get_version_and_commit_from_file(tmp_path: Path) -> None: # Arrange commit_file = tmp_path / "COMMIT" commit_file.write_text("abcdef12\n") @@ -74,7 +75,7 @@ def test_get_version_and_commit_from_file(tmp_path): assert commit == "abcdef12" -def test_get_version_missing_file_raises(tmp_path): +def test_get_version_missing_file_raises(tmp_path: Path) -> None: # Arrange (one file only) (tmp_path / "COMMIT").write_text("abcdef1234\n") @@ -86,7 +87,9 @@ def test_get_version_missing_file_raises(tmp_path): # ------------------------------- # Tests for main() # ------------------------------- -def test_main_prints_arch(monkeypatch, capsys): +def test_main_prints_arch( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture +) -> None: # Arrange argv = ["prog", "--arch", "amd64", "--cname", "flav", "--version", "1.0", "arch"] monkeypatch.setattr(sys, "argv", argv) @@ -100,7 +103,9 @@ def test_main_prints_arch(monkeypatch, capsys): assert "amd64" in out -def test_main_prints_container_name(monkeypatch, capsys): +def test_main_prints_container_name( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture +) -> None: # Arrange argv = [ "prog", @@ -123,7 +128,9 @@ def test_main_prints_container_name(monkeypatch, capsys): assert "container-python-dev" in out -def test_main_prints_container_tag(monkeypatch, capsys): +def test_main_prints_container_tag( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture +) -> None: # Arrange argv = [ "prog", @@ -148,7 +155,9 @@ def test_main_prints_container_tag(monkeypatch, capsys): assert "1-0-post1" == out -def test_main_prints_commit_id(monkeypatch, capsys): +def test_main_prints_commit_id( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture +) -> None: # Arrange argv = ["prog", "--arch", "amd64", "--cname", "flav", "commit_id"] monkeypatch.setattr(sys, "argv", argv) @@ -169,7 +178,9 @@ def test_main_prints_commit_id(monkeypatch, capsys): assert "abcdef12" == captured.out.strip() -def test_main_prints_flags_elements_platforms(monkeypatch, capsys): +def test_main_prints_flags_elements_platforms( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture +) -> None: # Arrange argv = [ "prog", @@ -184,7 +195,7 @@ def test_main_prints_flags_elements_platforms(monkeypatch, capsys): monkeypatch.setattr(sys, "argv", argv) class FakeCName(CName): - def __init__(self, *a, **k): + def __init__(self, *a: Any, **k: Any): CName.__init__(self, *a, **k) self._feature_flags_cached = ["flag1"] @@ -198,7 +209,9 @@ def __init__(self, *a, **k): assert "flag1" in out -def test_main_prints_version(monkeypatch, capsys): +def test_main_prints_version( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture +) -> None: # Arrange argv = ["prog", "--arch", "amd64", "--cname", "flav", "version"] monkeypatch.setattr(sys, "argv", argv) @@ -219,7 +232,9 @@ def test_main_prints_version(monkeypatch, capsys): assert "1.2.3" == captured.out.strip() -def test_main_prints_version_and_commit_id(monkeypatch, capsys): +def test_main_prints_version_and_commit_id( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture +) -> None: # Arrange argv = ["prog", "--arch", "amd64", "--cname", "flav", "version_and_commit_id"] monkeypatch.setattr(sys, "argv", argv) @@ -240,7 +255,7 @@ def test_main_prints_version_and_commit_id(monkeypatch, capsys): assert "1.2.3-abcdef12" == captured.out.strip() -def test_main_requires_cname(monkeypatch): +def test_main_requires_cname(monkeypatch: pytest.MonkeyPatch) -> None: # Arrange monkeypatch.setattr(sys, "argv", ["prog", "arch"]) monkeypatch.setattr(fema, "Parser", lambda *a, **kw: None) @@ -250,7 +265,7 @@ def test_main_requires_cname(monkeypatch): fema.main() -def test_main_cname_raises_missing_commit_id(monkeypatch): +def test_main_cname_raises_missing_commit_id(monkeypatch: pytest.MonkeyPatch) -> None: # Arrange # args.type == 'cname, arch is None and no default_arch set argv = [ @@ -270,7 +285,7 @@ def test_main_cname_raises_missing_commit_id(monkeypatch): fema.main() -def test_main_raises_no_arch_no_default(monkeypatch): +def test_main_raises_no_arch_no_default(monkeypatch: pytest.MonkeyPatch) -> None: # Arrange # args.type == 'cname, arch is None and no default_arch set argv = ["prog", "--cname", "flav", "cname"] @@ -281,7 +296,9 @@ def test_main_raises_no_arch_no_default(monkeypatch): fema.main() -def test_main_raises_missing_commit_id(monkeypatch, capsys): +def test_main_raises_missing_commit_id( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture +) -> None: # Arrange argv = [ "prog", @@ -301,7 +318,9 @@ def test_main_raises_missing_commit_id(monkeypatch, capsys): fema.main() -def test_main_with_exclude_cname_print_elements(monkeypatch, capsys): +def test_main_with_exclude_cname_print_elements( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture +) -> None: # Arrange monkeypatch.setattr( sys, @@ -333,7 +352,9 @@ def test_main_with_exclude_cname_print_elements(monkeypatch, capsys): assert "log,sap,ssh,base,server,multipath,iscsi,nvme,gardener" == captured -def test_main_with_exclude_cname_print_features(monkeypatch, capsys): +def test_main_with_exclude_cname_print_features( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture +) -> None: # Arrange monkeypatch.setattr( sys, @@ -368,7 +389,9 @@ def test_main_with_exclude_cname_print_features(monkeypatch, capsys): ) -def test_cname_release_file(monkeypatch, capsys): +def test_cname_release_file( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture +) -> None: """ Test validation between release metadata and arguments given """ diff --git a/tests/features/test_metadata_main.py b/tests/features/test_metadata_main.py index 5a622d4a..fb9c3b8f 100644 --- a/tests/features/test_metadata_main.py +++ b/tests/features/test_metadata_main.py @@ -9,7 +9,9 @@ from .constants import generate_container_amd64_release_metadata -def test_main_output(monkeypatch, capsys): +def test_main_output( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture +) -> None: """ Test successful "output-release-metadata" """ @@ -34,7 +36,9 @@ def test_main_output(monkeypatch, capsys): assert expected == capsys.readouterr().out.strip() -def test_main_write(monkeypatch, capsys): +def test_main_write( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture +) -> None: """ Test successful "write" """ @@ -63,7 +67,7 @@ def test_main_write(monkeypatch, capsys): assert expected == os_release_file.open("r").read() -def test_main_validation(monkeypatch): +def test_main_validation(monkeypatch: pytest.MonkeyPatch) -> None: """ Test validation between release metadata and arguments given """ diff --git a/tests/features/test_parser.py b/tests/features/test_parser.py index 9917ad89..67e277ab 100644 --- a/tests/features/test_parser.py +++ b/tests/features/test_parser.py @@ -1,3 +1,5 @@ +from typing import Any, Dict, List + import pytest from gardenlinux.features import Parser @@ -5,7 +7,7 @@ from ..constants import GL_ROOT_DIR -@pytest.mark.parametrize( +@pytest.mark.parametrize( # type: ignore[misc] "input_cname, expected_output", [ ( @@ -103,7 +105,9 @@ ), ], ) -def test_parser_filter_as_dict(input_cname: str, expected_output: dict): +def test_parser_filter_as_dict( + input_cname: str, expected_output: Dict[str, Any] +) -> None: """ Tests if parser_filter_as_dict returns the dict with expected features. @@ -115,7 +119,7 @@ def test_parser_filter_as_dict(input_cname: str, expected_output: dict): assert features_dict == expected_output -def test_parser_return_intersection_subset(): +def test_parser_return_intersection_subset() -> None: # Arrange input_set = {"a", "c"} order_list = ["a", "b", "c", "d"] @@ -127,7 +131,7 @@ def test_parser_return_intersection_subset(): assert result == ["a", "c"] -def test_get_flavor_from_feature_set(): +def test_get_flavor_from_feature_set() -> None: # Arrange sorted_features = ["base", "_hidden", "extra"] @@ -138,13 +142,13 @@ def test_get_flavor_from_feature_set(): assert result == "base_hidden-extra" -def test_gget_flavor_from_feature_set_empty_raises(): +def test_gget_flavor_from_feature_set_empty_raises() -> None: # get_flavor with empty iterable raises TypeError with pytest.raises(TypeError): Parser.get_flavor_from_feature_set([]) -def test_parser_subset_nomatch(): +def test_parser_subset_nomatch() -> None: # Arrange input_set = {"x", "y"} order_list = ["a", "b", "c"] @@ -156,10 +160,10 @@ def test_parser_subset_nomatch(): assert result == [] -def test_parser_subset_with_empty_order_list(): +def test_parser_subset_with_empty_order_list() -> None: # Arrange input_set = {"a", "b"} - order_list = [] + order_list: List[str] = [] result = Parser.subset(input_set, order_list) diff --git a/tests/flavors/test_init.py b/tests/flavors/test_init.py index 8207df9b..9b02d4c3 100644 --- a/tests/flavors/test_init.py +++ b/tests/flavors/test_init.py @@ -1,21 +1,23 @@ import importlib +import pytest + from gardenlinux import flavors -def test_parser_exposed_at_top_level(): +def test_parser_exposed_at_top_level() -> None: """Parser should be importable directly from the package.""" from gardenlinux.flavors import parser assert flavors.Parser is parser.Parser -def test___all___is_correct(): +def test___all___is_correct() -> None: """__all__ should only contain Parser.""" assert flavors.__all__ == ["Parser"] -def test_star_import(monkeypatch): +def test_star_import(monkeypatch: pytest.MonkeyPatch) -> None: """from flavors import * should bring Parser into locals().""" # Arrange namespace = {} @@ -30,6 +32,6 @@ def test_star_import(monkeypatch): assert namespace["Parser"] is flavors.Parser -def test_import_module(): +def test_import_module() -> None: """Importing the package should not raise exceptions.""" importlib.reload(flavors) # Should succeed without errors diff --git a/tests/flavors/test_main.py b/tests/flavors/test_main.py index 8c1ecc0c..b74a5e2b 100644 --- a/tests/flavors/test_main.py +++ b/tests/flavors/test_main.py @@ -1,15 +1,19 @@ import json import sys +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple + +import pytest from gardenlinux.flavors import __main__ as fm -def test_generate_markdown_table(): +def test_generate_markdown_table() -> None: # Arrange combos = [("amd64", "linux-amd64")] # Act - table = fm.generate_markdown_table(combos, no_arch=False) + table = fm.generate_markdown_table(combos) # Assert assert table.startswith("| Platform | Architecture | Flavor") @@ -17,7 +21,7 @@ def test_generate_markdown_table(): assert "| linux" -def test_parse_args(monkeypatch): +def test_parse_args(monkeypatch: pytest.MonkeyPatch) -> None: """simulate CLI invocation and make sure parse_args reads them correctly""" # Arrange argv = [ @@ -55,33 +59,37 @@ def test_parse_args(monkeypatch): assert args.json_by_arch is True -def _make_parser_class(filter_result, group_result=None, remove_result=None): +def _make_parser_class( + filter_result: List[Tuple[Any, str]], + group_result: Optional[Dict[str, List[str]]] = None, + remove_result: Optional[List[str]] = None, +) -> Any: """ Factory to create a fake Parser class Instances ignore the favors_data passed to __init__. """ class DummyParser: - def __init__(self, flavors_data): + def __init__(self, flavors_data: str): self._data = flavors_data - def filter(self, **kwargs): + def filter(self, **kwargs: Any) -> List[Tuple[Any, str]]: # return the prepared combinations list return filter_result @staticmethod - def group_by_arch(combinations): + def group_by_arch(combinations: List[Tuple[Any, str]]) -> Dict[str, List[str]]: # Return a prepared mapping or derive a simple mapping if None if group_result is not None: return group_result # naive default behaviour: group combinations by arch - d = {} + d: Dict[str, List[str]] = {} for arch, comb in combinations: d.setdefault(arch, []).append(comb) return d @staticmethod - def remove_arch(combinations): + def remove_arch(combinations: List[Tuple[Any, str]]) -> List[str]: if remove_result is not None: return remove_result # naive default: remote '-{arch}' suffix if present @@ -97,20 +105,22 @@ def remove_arch(combinations): return DummyParser -def _make_git_repository_class(tmp_path): +def _make_git_repository_class(tmp_path: Path) -> Any: """ Factory to create a fake Parser class Instances ignore the favors_data passed to __init__. """ class DummyRepository: - def __init__(self): + def __init__(self): # type: ignore[no-untyped-def] self.root = tmp_path return DummyRepository -def test_main_json_by_arch_prints_json(tmp_path, monkeypatch, capsys): +def test_main_json_by_arch_prints_json( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: # Arrange # prepare flavors.yaml at tmp path flavors_file = tmp_path / "flavors.yaml" @@ -121,7 +131,7 @@ def test_main_json_by_arch_prints_json(tmp_path, monkeypatch, capsys): grouped = {"x86": ["linux-x86"], "arm": ["android-arm"]} DummyParser = _make_parser_class(filter_result=combinations, group_result=grouped) - DummyRepository = _make_git_repository_class(str(tmp_path)) + DummyRepository = _make_git_repository_class(Path(tmp_path)) monkeypatch.setattr(fm, "Parser", DummyParser) monkeypatch.setattr(fm, "Repository", DummyRepository) @@ -137,8 +147,8 @@ def test_main_json_by_arch_prints_json(tmp_path, monkeypatch, capsys): def test_main_json_by_arch_with_no_arch_strips_arch_suffix( - tmp_path, monkeypatch, capsys -): + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: # Arrange flavors_file = tmp_path / "flavors.yaml" flavors_file.write_text("dummy: content") @@ -148,7 +158,7 @@ def test_main_json_by_arch_with_no_arch_strips_arch_suffix( grouped = {"x86": ["linux-x86"], "arm": ["android-arm"]} DummyParser = _make_parser_class(filter_result=combinations, group_result=grouped) - DummyRepository = _make_git_repository_class(str(tmp_path)) + DummyRepository = _make_git_repository_class(Path(tmp_path)) monkeypatch.setattr(fm, "Parser", DummyParser) monkeypatch.setattr(fm, "Repository", DummyRepository) @@ -164,7 +174,9 @@ def test_main_json_by_arch_with_no_arch_strips_arch_suffix( assert parsed == {"x86": ["linux"], "arm": ["android"]} -def test_main_markdown_table_branch(tmp_path, monkeypatch, capsys): +def test_main_markdown_table_branch( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: # Arrange flavors_file = tmp_path / "flavors.yaml" flavors_file.write_text("dummy: content") @@ -172,7 +184,7 @@ def test_main_markdown_table_branch(tmp_path, monkeypatch, capsys): combinations = [("x86_64", "linux-x86_64"), ("armv7", "android-armv7")] DummyParser = _make_parser_class(filter_result=combinations) - DummyRepository = _make_git_repository_class(str(tmp_path)) + DummyRepository = _make_git_repository_class(Path(tmp_path)) monkeypatch.setattr(fm, "Parser", DummyParser) monkeypatch.setattr(fm, "Repository", DummyRepository) @@ -188,7 +200,9 @@ def test_main_markdown_table_branch(tmp_path, monkeypatch, capsys): assert "| Platform" in out -def test_main_default_prints_flavors_list(tmp_path, monkeypatch, capsys): +def test_main_default_prints_flavors_list( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: # Arrange flavors_file = tmp_path / "flavors.yaml" flavors_file.write_text("dummy: content") @@ -197,7 +211,7 @@ def test_main_default_prints_flavors_list(tmp_path, monkeypatch, capsys): combinations = [("x86", "linux-x86"), ("arm", "android-arm")] DummyParser = _make_parser_class(filter_result=combinations) - DummyRepository = _make_git_repository_class(str(tmp_path)) + DummyRepository = _make_git_repository_class(Path(tmp_path)) monkeypatch.setattr(fm, "Parser", DummyParser) monkeypatch.setattr(fm, "Repository", DummyRepository) @@ -212,7 +226,9 @@ def test_main_default_prints_flavors_list(tmp_path, monkeypatch, capsys): assert sorted(lines) == sorted(["linux-x86", "android-arm"]) -def test_main_default_prints_git_flavors_list(tmp_path, monkeypatch, capsys): +def test_main_default_prints_git_flavors_list( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: # filter returns tuples; main's default branch prints comb[1] values, sorted unique combinations = [("x86", "linux-x86"), ("arm", "android-arm")] diff --git a/tests/flavors/test_parser.py b/tests/flavors/test_parser.py index e4bfb1b5..86e94dd5 100644 --- a/tests/flavors/test_parser.py +++ b/tests/flavors/test_parser.py @@ -1,11 +1,13 @@ +from typing import Any, Dict + import pytest import yaml from gardenlinux.flavors.parser import Parser -@pytest.fixture -def valid_data(): +@pytest.fixture # type: ignore[misc] +def valid_data() -> Dict[str, Any]: """Minimal data for valid GL_FLAVORS_SCHEMA.""" return { "targets": [ @@ -49,14 +51,14 @@ def valid_data(): } -def make_parser(data): +def make_parser(data: str) -> Parser: """ Construct Parser from dict. """ return Parser(data) -def test_init_accepts_yaml_and_dict(valid_data): +def test_init_accepts_yaml_and_dict(valid_data: str) -> None: # Arrange yaml_str = yaml.safe_dump(valid_data) @@ -69,7 +71,7 @@ def test_init_accepts_yaml_and_dict(valid_data): assert p_from_yaml._flavors_data == valid_data -def test_filter_defaults(valid_data): +def test_filter_defaults(valid_data: str) -> None: # Arrange parser = make_parser(valid_data) @@ -82,7 +84,7 @@ def test_filter_defaults(valid_data): assert any("linux-arm64" in name for name in combo_names) -def test_filter_category_and_exclude(valid_data): +def test_filter_category_and_exclude(valid_data: str) -> None: # Arrange parser = make_parser(valid_data) @@ -95,19 +97,19 @@ def test_filter_category_and_exclude(valid_data): assert all("android" in name for _, name in android_combos) -@pytest.mark.parametrize("flag", ["only_build", "only_test", "only_publish"]) -def test_filter_with_flags(valid_data, flag): +@pytest.mark.parametrize("flag", ["only_build", "only_test", "only_publish"]) # type: ignore[misc] +def test_filter_with_flags(valid_data: str, flag: str) -> None: # Arrange parser = make_parser(valid_data) # Act - combos = parser.filter(**{flag: True}) + combos = parser.filter(only_build=True) # Assert assert all("linux-f1-amd64" in name for _, name in combos) -def test_filter_only_test_platform(valid_data): +def test_filter_only_test_platform(valid_data: str) -> None: # Arrange parser = make_parser(valid_data) @@ -118,7 +120,7 @@ def test_filter_only_test_platform(valid_data): assert combos == [("arm64", "android-f2-arm64")] -def test_filter_with_excludes(valid_data): +def test_filter_with_excludes(valid_data: str) -> None: # Arrange parser = make_parser(valid_data) @@ -129,7 +131,7 @@ def test_filter_with_excludes(valid_data): assert all(not name.startswith("linux") for _, name in combos) -def test_group_by_arch_and_remove_arch(): +def test_group_by_arch_and_remove_arch() -> None: # Arrange combos = [ ("amd64", "linux-amd64"), @@ -147,7 +149,7 @@ def test_group_by_arch_and_remove_arch(): assert "linux" in removed and "android" in removed -def test_exclude_include_only(): +def test_exclude_include_only() -> None: # Arrange / Act / Assert assert Parser.should_exclude("abc", ["abc"], []) is True assert Parser.should_exclude("abc", [], ["a*"]) is True diff --git a/tests/github/test_create_github_release.py b/tests/github/test_create_github_release.py index c876729c..23af3492 100644 --- a/tests/github/test_create_github_release.py +++ b/tests/github/test_create_github_release.py @@ -21,9 +21,9 @@ def test_create_github_release_needs_github_token(): False, "", ) - assert ( - str(exn.value) == "GITHUB_TOKEN environment variable not set" - ), "Expected an exception to be raised on missing GITHUB_TOKEN environment variable" + assert str(exn.value) == "GITHUB_TOKEN environment variable not set", ( + "Expected an exception to be raised on missing GITHUB_TOKEN environment variable" + ) def test_create_github_release_raise_on_failure(caplog, github_token): @@ -81,6 +81,6 @@ def test_write_to_release_id_file_broken_file_permissions(release_id_file, caplo with pytest.raises(SystemExit): write_to_release_id_file(TEST_GARDENLINUX_RELEASE) - assert any( - "Could not create" in record.message for record in caplog.records - ), "Expected a failure log record" + assert any("Could not create" in record.message for record in caplog.records), ( + "Expected a failure log record" + ) diff --git a/tests/github/test_create_github_release_notes.py b/tests/github/test_create_github_release_notes.py index 38fc56ba..55b08a73 100644 --- a/tests/github/test_create_github_release_notes.py +++ b/tests/github/test_create_github_release_notes.py @@ -36,9 +36,9 @@ def test_release_notes_changes_section_empty_packagelist(): text='{"packageList": []}', status_code=200, ) - assert ( - release_notes_changes_section(TEST_GARDENLINUX_RELEASE) == "" - ), "Expected an empty result if GLVD returns an empty package list" + assert release_notes_changes_section(TEST_GARDENLINUX_RELEASE) == "", ( + "Expected an empty result if GLVD returns an empty package list" + ) def test_release_notes_changes_section_broken_glvd_response(): @@ -50,7 +50,9 @@ def test_release_notes_changes_section_broken_glvd_response(): ) assert "fill this in" in release_notes_changes_section( TEST_GARDENLINUX_RELEASE - ), "Expected a placeholder message to be generated if GVLD response is not valid" + ), ( + "Expected a placeholder message to be generated if GVLD response is not valid" + ) def test_release_notes_compare_package_versions_section_legacy_versioning_is_recognized(): @@ -121,7 +123,6 @@ def test_default_get_file_extension_for_deployment_platform(): @mock_aws def test_github_release_page(monkeypatch, downloads_dir, release_s3_bucket): - class SubmoduleAsRepo(Repo): """This will fake a git submodule as a git repository object.""" @@ -169,10 +170,12 @@ def __new__(cls, *args, **kwargs): text=glvd_response_fixture_path.read_text(), status_code=200, ) - generated_release_notes = gardenlinux.github.release_notes.create_github_release_notes( # pyright: ignore[reportAttributeAccessIssue] - TEST_GARDENLINUX_RELEASE, - TEST_GARDENLINUX_COMMIT, - release_s3_bucket.name, + generated_release_notes = ( + gardenlinux.github.release_notes.create_github_release_notes( # pyright: ignore[reportAttributeAccessIssue] + TEST_GARDENLINUX_RELEASE, + TEST_GARDENLINUX_COMMIT, + release_s3_bucket.name, + ) ) assert generated_release_notes == release_fixture_path.read_text() diff --git a/tests/github/test_github_script.py b/tests/github/test_github_script.py index 9b0acc43..907e0da5 100644 --- a/tests/github/test_github_script.py +++ b/tests/github/test_github_script.py @@ -15,9 +15,9 @@ def test_script_parse_args_wrong_command(monkeypatch, capfd): gh.main() captured = capfd.readouterr() - assert ( - "argument command: invalid choice: 'rejoice'" in captured.err - ), "Expected help message printed" + assert "argument command: invalid choice: 'rejoice'" in captured.err, ( + "Expected help message printed" + ) def test_script_parse_args_create_command_required_args(monkeypatch, capfd): @@ -29,9 +29,9 @@ def test_script_parse_args_create_command_required_args(monkeypatch, capfd): gh.main() captured = capfd.readouterr() - assert ( - "the following arguments are required: --tag, --commit" in captured.err - ), "Expected help message on missing arguments for 'create' command" + assert "the following arguments are required: --tag, --commit" in captured.err, ( + "Expected help message on missing arguments for 'create' command" + ) def test_script_parse_args_upload_command_required_args(monkeypatch, capfd): @@ -50,7 +50,6 @@ def test_script_parse_args_upload_command_required_args(monkeypatch, capfd): def test_script_create_dry_run(monkeypatch, capfd): - monkeypatch.setattr( sys, "argv", diff --git a/tests/github/test_upload_to_github_release_page.py b/tests/github/test_upload_to_github_release_page.py index 04ce3cb6..a62342f9 100644 --- a/tests/github/test_upload_to_github_release_page.py +++ b/tests/github/test_upload_to_github_release_page.py @@ -39,9 +39,9 @@ def test_upload_to_github_release_page_needs_github_token( artifact_for_upload, dry_run=False, ) - assert ( - str(exn.value) == "GITHUB_TOKEN environment variable not set" - ), "Expected an exception to be raised on missing GITHUB_TOKEN environment variable" + assert str(exn.value) == "GITHUB_TOKEN environment variable not set", ( + "Expected an exception to be raised on missing GITHUB_TOKEN environment variable" + ) def test_upload_to_github_release_page( @@ -78,9 +78,9 @@ def test_upload_to_github_release_page_unreadable_artifact( artifact_for_upload, dry_run=False, ) - assert any( - "Error reading file" in record.message for record in caplog.records - ), "Expected an error message log entry" + assert any("Error reading file" in record.message for record in caplog.records), ( + "Expected an error message log entry" + ) def test_upload_to_github_release_page_failed( @@ -114,9 +114,9 @@ def test_script_parse_args_wrong_command(monkeypatch, capfd): gh.main() captured = capfd.readouterr() - assert ( - "argument command: invalid choice: 'rejoice'" in captured.err - ), "Expected help message printed" + assert "argument command: invalid choice: 'rejoice'" in captured.err, ( + "Expected help message printed" + ) def test_script_parse_args_upload_command_required_args(monkeypatch, capfd): diff --git a/tests/helper.py b/tests/helper.py index 7d9a82c7..ea3ee7fa 100644 --- a/tests/helper.py +++ b/tests/helper.py @@ -1,14 +1,17 @@ import shlex import subprocess +from typing import Any, Optional -def spawn_background_process(cmd, stdout=None, stderr=None): +def spawn_background_process( + cmd: str, stdout: Optional[Any] = None, stderr: Optional[Any] = None +) -> subprocess.Popen[Any]: args = shlex.split(cmd) process = subprocess.Popen(args, shell=False, stdout=stdout, stderr=stderr) return process -def call_command(cmd): +def call_command(cmd: str) -> str: try: args = shlex.split(cmd) result = subprocess.run( diff --git a/tests/oci/test_container.py b/tests/oci/test_container.py index b386438c..55c9b8bf 100644 --- a/tests/oci/test_container.py +++ b/tests/oci/test_container.py @@ -1,4 +1,5 @@ from base64 import b64encode +from typing import Any import pytest from requests import Response @@ -14,11 +15,11 @@ ) -@pytest.fixture(name="Container_login_403") -def patch__Container_login_403(monkeypatch): +@pytest.fixture(name="Container_login_403") # type: ignore[misc] +def patch__Container_login_403(monkeypatch: pytest.MonkeyPatch) -> None: """Patch `login()` to return HTTP 403. `docker.errors.APIError` extends from `requests.exceptions.HTTPError` as well.""" - def login_403(*args, **kwargs): + def login_403(*args: Any, **kwargs: Any) -> None: response = Response() response.status_code = 403 raise HTTPError("403 Forbidden", response=response) @@ -26,11 +27,11 @@ def login_403(*args, **kwargs): monkeypatch.setattr(Container, "login", login_403) -@pytest.fixture(name="Container_read_or_generate_403") -def patch__Container_read_or_generate_403(monkeypatch): +@pytest.fixture(name="Container_read_or_generate_403") # type: ignore[misc] +def patch__Container_read_or_generate_403(monkeypatch: pytest.MonkeyPatch) -> None: """Patch `read_or_generate_manifest()` to return HTTP 403.""" - def read_or_generate_403(*args, **kwargs): + def read_or_generate_403(*args: Any, **kwargs: Any) -> None: response = Response() response.status_code = 403 raise HTTPError("403 Forbidden", response=response) @@ -38,8 +39,8 @@ def read_or_generate_403(*args, **kwargs): monkeypatch.setattr(Container, "read_or_generate_manifest", read_or_generate_403) -@pytest.mark.usefixtures("zot_session") -def test_manifest(): +@pytest.mark.usefixtures("zot_session") # type: ignore[misc] +def test_manifest() -> None: """Verify a newly created manifest returns correct commit value.""" # Arrange container = Container(f"{CONTAINER_NAME_ZOT_EXAMPLE}:{TEST_VERSION}", insecure=True) @@ -52,9 +53,9 @@ def test_manifest(): assert manifest.commit == TEST_COMMIT -@pytest.mark.usefixtures("zot_session") -@pytest.mark.usefixtures("Container_read_or_generate_403") -def test_manifest_403(): +@pytest.mark.usefixtures("zot_session") # type: ignore[misc] +@pytest.mark.usefixtures("Container_read_or_generate_403") # type: ignore[misc] +def test_manifest_403() -> None: """Verify container calls raises exceptions for certain errors.""" # Arrange container = Container(f"{CONTAINER_NAME_ZOT_EXAMPLE}:{TEST_VERSION}", insecure=True) @@ -63,8 +64,10 @@ def test_manifest_403(): container.read_or_generate_manifest(version=TEST_VERSION, commit=TEST_COMMIT) -@pytest.mark.usefixtures("zot_session") -def test_manifest_auth_token(monkeypatch, caplog): +@pytest.mark.usefixtures("zot_session") # type: ignore[misc] +def test_manifest_auth_token( + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture +) -> None: """Verify container calls use login environment variables if defined.""" with monkeypatch.context(): token = "test" @@ -79,9 +82,11 @@ def test_manifest_auth_token(monkeypatch, caplog): assert container.auth.token == b64encode(bytes(token, "utf-8")).decode("utf-8") -@pytest.mark.usefixtures("zot_session") -@pytest.mark.usefixtures("Container_login_403") -def test_manifest_login_username_password(monkeypatch, caplog): +@pytest.mark.usefixtures("zot_session") # type: ignore[misc] +@pytest.mark.usefixtures("Container_login_403") # type: ignore[misc] +def test_manifest_login_username_password( + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture +) -> None: """Verify container calls use login environment variables if defined.""" with monkeypatch.context(): monkeypatch.setenv("GL_CLI_REGISTRY_USERNAME", "test") diff --git a/tests/oci/test_image_manifest.py b/tests/oci/test_image_manifest.py index 1c7799c7..55cdc483 100644 --- a/tests/oci/test_image_manifest.py +++ b/tests/oci/test_image_manifest.py @@ -1,9 +1,11 @@ +from pathlib import Path + import pytest from gardenlinux.oci import ImageManifest, Layer -def test_ImageManifest_arch(): +def test_ImageManifest_arch() -> None: # Arrange empty_manifest = ImageManifest() manifest = ImageManifest(annotations={"architecture": "amd64"}) @@ -18,7 +20,7 @@ def test_ImageManifest_arch(): assert manifest.arch == "amd64" -def test_ImageManifest_cname(): +def test_ImageManifest_cname() -> None: # Arrange cname = "container-amd64-today-local" @@ -35,7 +37,7 @@ def test_ImageManifest_cname(): assert manifest.cname == cname -def test_ImageManifest_feature_set(): +def test_ImageManifest_feature_set() -> None: # Arrange feature_set = "container" @@ -52,7 +54,7 @@ def test_ImageManifest_feature_set(): assert manifest.feature_set == feature_set -def test_ImageManifest_flavor(): +def test_ImageManifest_flavor() -> None: # Arrange flavor = "container" cname = f"{flavor}-amd64-today-local" @@ -70,7 +72,7 @@ def test_ImageManifest_flavor(): assert manifest.flavor == flavor -def test_ImageManifest_layer(tmp_path): +def test_ImageManifest_layer(tmp_path: Path) -> None: # Arrange blob = tmp_path / "blob.txt" blob.write_text("data") @@ -88,10 +90,10 @@ def test_ImageManifest_layer(tmp_path): # Assert with pytest.raises(RuntimeError): - assert manifest.append_layer({"test": "invalid"}) + manifest.append_layer({"test": "invalid"}) -def test_ImageManifest_version(): +def test_ImageManifest_version() -> None: # Arrange version = "today" diff --git a/tests/oci/test_index.py b/tests/oci/test_index.py index 3a4904ed..dbdfe083 100644 --- a/tests/oci/test_index.py +++ b/tests/oci/test_index.py @@ -1,11 +1,12 @@ import json +from typing import Any, Dict import pytest from gardenlinux.oci.index import Index -def test_index_init_and_json(): +def test_index_init_and_json() -> None: """Ensure Index init works correctly""" # Arrange idx = Index() @@ -20,7 +21,7 @@ def test_index_init_and_json(): assert decoded == idx -def test_manifests_as_dict(): +def test_manifests_as_dict() -> None: """Verify manifests_as_dict returns correct keys for cname and digest cases.""" # Arrange idx = Index() @@ -36,7 +37,7 @@ def test_manifests_as_dict(): assert result["sha256:def"] == manifest_no_cname -def test_append_manifest_replace(): +def test_append_manifest_replace() -> None: """Ensure append_manifest replaces existing manifest with same cname.""" # Arrange idx = Index() @@ -55,7 +56,7 @@ def test_append_manifest_replace(): assert any(manifest["digest"] == "sha256:new" for manifest in idx["manifests"]) -def test_append_manifest_cname_not_found(): +def test_append_manifest_cname_not_found() -> None: """Test appending new manifest if cname isn't found.""" # Arrange idx = Index() @@ -76,8 +77,8 @@ def test_append_manifest_cname_not_found(): "not-a-dict", {"annotations": {}}, ], -) -def test_append_invalid_input_raises(bad_manifest): +) # type: ignore[misc] +def test_append_invalid_input_raises(bad_manifest: Dict[str, Any]) -> None: """Test proper error handling for invalid append_manifest input.""" # Arrange idx = Index() diff --git a/tests/oci/test_layer.py b/tests/oci/test_layer.py index 15fca532..717b02d7 100644 --- a/tests/oci/test_layer.py +++ b/tests/oci/test_layer.py @@ -1,3 +1,6 @@ +from pathlib import Path +from typing import Any, Dict, Generator, Optional + import pytest import gardenlinux.oci.layer as gl_layer @@ -6,29 +9,31 @@ class DummyLayer: """Minimal stub for oras.oci.Layer""" - def __init__(self, blob_path, media_type=None, is_dir=False): + def __init__( + self, blob_path: str, media_type: Optional[str] = None, is_dir: bool = False + ): self._init_args = (blob_path, media_type, is_dir) - def to_dict(self): + def to_dict(self) -> Dict[str, Any]: return {"dummy": True} -@pytest.fixture(autouse=True) -def patch__Layer(monkeypatch): +@pytest.fixture(autouse=True) # type: ignore[misc] +def patch__Layer(monkeypatch: pytest.MonkeyPatch) -> Generator[None]: """Replace oras.oci.Layer with DummyLayer in Layer's module.""" monkeypatch.setattr(gl_layer, "_Layer", DummyLayer) yield -def test_dict_property_returns_with_annotations(tmp_path): +def test_dict_property_returns_with_annotations(tmp_path: Path) -> None: """dict property should merge _Layer.to_dict() with annotations.""" # Arrange blob = tmp_path / "blob.txt" blob.write_text("data") # Act - l = gl_layer.Layer(blob) - result = l.dict + layer = gl_layer.Layer(blob) + result = layer.dict # Assert assert result["dummy"] is True @@ -36,80 +41,79 @@ def test_dict_property_returns_with_annotations(tmp_path): assert result["annotations"]["org.opencontainers.image.title"] == "blob.txt" -def test_getitem_and_delitem_annotations(tmp_path): +def test_getitem_and_delitem_annotations(tmp_path: Path) -> None: """getitem should return annotations, delitem should clear them.""" # Arrange blob = tmp_path / "blob.txt" blob.write_text("data") - l = gl_layer.Layer(blob) + layer = gl_layer.Layer(blob) # Act / Assert (__getitem__) - ann = l["annotations"] + ann = layer["annotations"] assert isinstance(ann, dict) assert "org.opencontainers.image.title" in ann # Act / Assert (__delitem__) - l.__delitem__("annotations") - assert l._annotations == {} + layer.__delitem__("annotations") + assert layer._annotations == {} -def test_getitem_invalid_key_raises(tmp_path): +def test_getitem_invalid_key_raises(tmp_path: Path) -> None: """getitem with unsupported key should raise KeyError.""" # Arrange blob = tmp_path / "blob.txt" blob.write_text("data") - l = gl_layer.Layer(blob) + layer = gl_layer.Layer(blob) # Act / Assert with pytest.raises(KeyError): - _ = l["invalid"] + _ = layer["invalid"] -def test_setitem_annotations(tmp_path): +def test_setitem_annotations(tmp_path: Path) -> None: """setitem with supported keys should set annotations""" # Arrange blob = tmp_path / "blob.txt" blob.write_text("data") - l = gl_layer.Layer(blob) + layer = gl_layer.Layer(blob) # Act new_ann = {"x": "y"} - l.__setitem__("annotations", new_ann) + layer.__setitem__("annotations", new_ann) # Assert - assert l._annotations == new_ann + assert layer._annotations == new_ann -def test_setitem_annotations_invalid_raises(tmp_path): +def test_setitem_annotations_invalid_raises(tmp_path: Path) -> None: # Arrange blob = tmp_path / "blob.txt" blob.write_text("data") - l = gl_layer.Layer(blob) + layer = gl_layer.Layer(blob) # Act / Assert with pytest.raises(KeyError): - _ = l["invalid"] + _ = layer["invalid"] -def test_len_iter(tmp_path): +def test_len_iter(tmp_path: Path) -> None: # Arrange blob = tmp_path / "blob.txt" blob.write_text("data") - l = gl_layer.Layer(blob) + layer = gl_layer.Layer(blob) # Act - keys = list(iter(l)) + keys = list(iter(layer)) # Assert assert keys == ["annotations"] assert len(keys) == 1 -def test_gen_metadata_from_file(tmp_path): +def test_gen_metadata_from_file(tmp_path: Path) -> None: # Arrange blob = tmp_path / "blob.tar" blob.write_text("data") - l = gl_layer.Layer(blob) # Act arch = "amd64" @@ -121,7 +125,7 @@ def test_gen_metadata_from_file(tmp_path): assert metadata["annotations"]["io.gardenlinux.image.layer.architecture"] == arch -def test_lookup_media_type_for_file_name(tmp_path): +def test_lookup_media_type_for_file_name(tmp_path: Path) -> None: # Arrange blob = tmp_path / "blob.tar" blob.write_text("data") @@ -133,7 +137,9 @@ def test_lookup_media_type_for_file_name(tmp_path): assert media_type == GL_MEDIA_TYPE_LOOKUP["tar"] -def test_lookup_media_type_for_file_name_invalid_raises(tmp_path): +def test_lookup_media_type_for_file_name_invalid_raises( + tmp_path: Path, +) -> None: # Arrange / Act / Assert with pytest.raises(ValueError): gl_layer.Layer.lookup_media_type_for_file_name(tmp_path / "unknown.xyz") diff --git a/tests/oci/test_oci.py b/tests/oci/test_oci.py index 3aba1a92..00568a0f 100644 --- a/tests/oci/test_oci.py +++ b/tests/oci/test_oci.py @@ -1,10 +1,10 @@ import json import sys +from typing import Any, List, Optional, Tuple import pytest from click.testing import CliRunner - -# Import reggie library correctly +from oras.client import OrasClient from oras.provider import Registry sys.path.append("src") @@ -26,7 +26,13 @@ ) -def push_manifest(runner, version, arch, cname, additional_tags=None): +def push_manifest( + runner: CliRunner, + version: str, + arch: str, + cname: str, + additional_tags: Optional[List[str]] = None, +) -> bool: """Push manifest to registry and return success status""" print(f"Pushing manifest for {cname} {arch}") @@ -67,7 +73,13 @@ def push_manifest(runner, version, arch, cname, additional_tags=None): return False -def push_manifest_tags(runner, version, arch, cname, tags=None): +def push_manifest_tags( + runner: CliRunner, + version: str, + arch: str, + cname: str, + tags: Optional[List[str]] = None, +) -> bool: """Push manifest to registry and return success status""" print(f"Pushing manifest for {cname} {arch}") @@ -102,7 +114,9 @@ def push_manifest_tags(runner, version, arch, cname, tags=None): return False -def update_index(runner, version, additional_tags=None): +def update_index( + runner: CliRunner, version: str, additional_tags: Optional[List[str]] = None +) -> bool: """Update index in registry and return success status""" print("Updating index") @@ -133,31 +147,31 @@ def update_index(runner, version, additional_tags=None): return False -def get_catalog(client): +def get_catalog(client: OrasClient) -> List[Any]: """Get catalog from registry and return repositories list""" catalog_resp = client.do_request(f"{REGISTRY_URL}/v2/_catalog") - assert ( - catalog_resp.status_code == 200 - ), f"Failed to get catalog, status: {catalog_resp.status_code}" + assert catalog_resp.status_code == 200, ( + f"Failed to get catalog, status: {catalog_resp.status_code}" + ) catalog_json = json.loads(catalog_resp.text) - return catalog_json.get("repositories", []) + return catalog_json.get("repositories", []) # type: ignore[no-any-return] -def get_tags(client, repo): +def get_tags(client: OrasClient, repo: str) -> List[str]: """Get tags for a repository""" tags_resp = client.do_request(f"{REGISTRY_URL}/v2/{repo}/tags/list") - assert ( - tags_resp.status_code == 200 - ), f"Failed to get tags for {repo}, status: {tags_resp.status_code}" + assert tags_resp.status_code == 200, ( + f"Failed to get tags for {repo}, status: {tags_resp.status_code}" + ) tags_json = json.loads(tags_resp.text) - return tags_json.get("tags", []) + return tags_json.get("tags", []) # type: ignore[no-any-return] -def get_manifest(client, repo, reference): +def get_manifest(client: OrasClient, repo: str, reference: str) -> Tuple[Any, str]: """Get manifest and digest for a repository reference""" # Create a simple request for the manifest manifest_resp = client.do_request( @@ -167,9 +181,9 @@ def get_manifest(client, repo, reference): }, ) - assert ( - manifest_resp.status_code == 200 - ), f"Failed to get manifest for {repo}:{reference}, status: {manifest_resp.status_code}" + assert manifest_resp.status_code == 200, ( + f"Failed to get manifest for {repo}:{reference}, status: {manifest_resp.status_code}" + ) # Get the digest and content - use headers.get() instead of header.Get() digest = manifest_resp.headers.get("Docker-Content-Digest") @@ -178,7 +192,7 @@ def get_manifest(client, repo, reference): return manifest_json, digest -def verify_index_manifest(manifest, expected_arch): +def verify_index_manifest(manifest: Any, expected_arch: str) -> None: """Verify the index manifest has expected content""" assert manifest.get("schemaVersion") == 2, "Manifest should have schema version 2" assert "manifests" in manifest, "Manifest should contain manifests array" @@ -193,35 +207,46 @@ def verify_index_manifest(manifest, expected_arch): assert found, f"Manifest should contain an entry for architecture {expected_arch}" -def verify_combined_tag_manifest(manifest, arch, cname, version, feature_set, commit): +def verify_combined_tag_manifest( + manifest: Any, + arch: str, + cname: str, + version: str, + feature_set: str, + commit: str, +) -> None: """Verify the combined tag manifest has expected content""" assert manifest.get("schemaVersion") == 2, "Manifest should have schema version 2" assert "layers" in manifest, "Manifest should contain layers array" assert "annotations" in manifest, "Manifest should contain annotations" annotations = manifest.get("annotations", {}) - assert ( - annotations.get("architecture") == arch - ), f"Manifest should have architecture {arch}" + assert annotations.get("architecture") == arch, ( + f"Manifest should have architecture {arch}" + ) assert annotations.get("cname") == cname, f"Manifest should have cname {cname}" - assert ( - annotations.get("version") == version - ), f"Manifest should have version {version}" + assert annotations.get("version") == version, ( + f"Manifest should have version {version}" + ) if feature_set: - assert ( - annotations.get("feature_set") == feature_set - ), f"Manifest should have feature_set {feature_set}" + assert annotations.get("feature_set") == feature_set, ( + f"Manifest should have feature_set {feature_set}" + ) if commit: - assert ( - annotations.get("commit") == commit - ), f"Manifest should have commit {commit}" + assert annotations.get("commit") == commit, ( + f"Manifest should have commit {commit}" + ) def verify_additional_tags( - client, repo, additional_tags, reference_digest=None, fail_on_missing=True -): + client: OrasClient, + repo: str, + additional_tags: List[str], + reference_digest: Optional[str] = None, + fail_on_missing: bool = True, +) -> List[str]: """ Verify that all additional tags exist and match the reference digest if provided. @@ -280,8 +305,8 @@ def verify_additional_tags( return missing_tags -@pytest.mark.usefixtures("zot_session") -@pytest.mark.parametrize( +@pytest.mark.usefixtures("zot_session") # type: ignore[misc] +@pytest.mark.parametrize( # type: ignore[misc] "version, cname, arch, additional_tags_index, additional_tags_manifest", [ ( @@ -293,7 +318,7 @@ def verify_additional_tags( f"{TEST_VERSION}-patch-{TEST_COMMIT}", f"{TEST_VERSION_STABLE}", f"{TEST_VERSION_STABLE}-stable", - f"latest", + "latest", ], [ f"{TEST_VERSION}-patch-{platform}-{feature_string}-{arch}", @@ -307,8 +332,12 @@ def verify_additional_tags( ], ) def test_push_manifest_and_index( - version, arch, cname, additional_tags_index, additional_tags_manifest -): + version: str, + arch: str, + cname: str, + additional_tags_index: List[str], + additional_tags_manifest: List[str], +) -> None: print(f"\n\n=== Starting test for {cname} {arch} {version} ===") runner = CliRunner() repo_name = "gardenlinux-example" diff --git a/tests/s3/conftest.py b/tests/s3/conftest.py index b2a8e646..0dbaac63 100644 --- a/tests/s3/conftest.py +++ b/tests/s3/conftest.py @@ -2,22 +2,23 @@ from dataclasses import dataclass from hashlib import md5, sha256 +from io import BytesIO +from pathlib import Path +from typing import Any, Generator import boto3 import pytest from moto import mock_aws -from gardenlinux.features.cname import CName as RealCName - BUCKET_NAME = "test-bucket" REGION = "us-east-1" @dataclass(frozen=True) class S3Env: - s3: object + s3: boto3.client bucket_name: str - tmp_path: str + tmp_path: Path cname: str @@ -34,7 +35,7 @@ def make_cname( # Helpers to compute digests for fake files -def dummy_digest(data: bytes, algo: str) -> str: +def dummy_digest(data: BytesIO, algo: str) -> Any: """ Dummy for file_digest() to compute hashes for in-memory byte streams """ @@ -42,15 +43,15 @@ def dummy_digest(data: bytes, algo: str) -> str: data.seek(0) # Reset byte cursor to start for multiple uses if algo == "md5": - return md5(content) # nosec B324 + return md5(content) elif algo == "sha256": return sha256(content) else: raise ValueError(f"Unsupported algo: {algo}") -@pytest.fixture(autouse=True) -def s3_setup(tmp_path, monkeypatch): +@pytest.fixture(autouse=True) # type: ignore[misc] +def s3_setup(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Generator[Any]: """ Provides a clean S3 setup for each test. """ diff --git a/tests/s3/test_bucket.py b/tests/s3/test_bucket.py index 451c10e3..062faaf2 100644 --- a/tests/s3/test_bucket.py +++ b/tests/s3/test_bucket.py @@ -11,10 +11,12 @@ from gardenlinux.s3.bucket import Bucket +from .conftest import S3Env + REGION = "us-east-1" -def test_bucket_minimal(s3_setup): +def test_bucket_minimal(s3_setup: S3Env) -> None: """ Ensure Bucket initializes correctly. """ @@ -23,7 +25,7 @@ def test_bucket_minimal(s3_setup): assert bucket.name == env.bucket_name -def test_objects_empty(s3_setup): +def test_objects_empty(s3_setup: S3Env) -> None: """ List objects from empty bucket. """ @@ -37,7 +39,7 @@ def test_objects_empty(s3_setup): assert list(bucket.objects) == [] -def test_upload_file_and_list(s3_setup): +def test_upload_file_and_list(s3_setup: S3Env) -> None: """ Create a fake file in a temporary directory, upload and try to list it @@ -58,7 +60,7 @@ def test_upload_file_and_list(s3_setup): assert "example.txt" in all_keys -def test_download_file(s3_setup): +def test_download_file(s3_setup: S3Env) -> None: """ Try to download a file pre-existing in the bucket """ @@ -75,7 +77,7 @@ def test_download_file(s3_setup): assert target_path.read_text() == "some data" -def test_read_cache_file_or_filter(s3_setup): +def test_read_cache_file_or_filter(s3_setup: S3Env) -> None: """ Try to read with cache """ @@ -98,7 +100,7 @@ def test_read_cache_file_or_filter(s3_setup): assert result == ["file.txt", "file2.txt"] -def test_upload_fileobj(s3_setup): +def test_upload_fileobj(s3_setup: S3Env) -> None: """ Upload a file-like in-memory object to the bucket """ @@ -117,7 +119,7 @@ def test_upload_fileobj(s3_setup): assert obj["Body"].read() == b"Test Data" -def test_download_fileobj(s3_setup): +def test_download_fileobj(s3_setup: S3Env) -> None: """ Download data into a in-memory object """ @@ -138,7 +140,7 @@ def test_download_fileobj(s3_setup): assert output.read() == b"123abc" -def test_getattr_delegates(s3_setup): +def test_getattr_delegates(s3_setup: S3Env) -> None: """ Verify that attribute access is delegated to the underlying boto3 Bucket. diff --git a/tests/s3/test_main.py b/tests/s3/test_main.py index 887de20a..d0c109fd 100644 --- a/tests/s3/test_main.py +++ b/tests/s3/test_main.py @@ -1,4 +1,5 @@ import sys +from typing import Any, Dict, List from unittest.mock import MagicMock, patch import pytest @@ -6,7 +7,7 @@ import gardenlinux.s3.__main__ as s3m -@pytest.mark.parametrize( +@pytest.mark.parametrize( # type: ignore[misc] "argv, expected_method, expected_args, expected_kwargs", [ ( @@ -42,8 +43,11 @@ ], ) def test_main_calls_correct_artifacts( - argv, expected_method, expected_args, expected_kwargs -): + argv: List[str], + expected_method: str, + expected_args: List[Any], + expected_kwargs: Dict[str, Any], +) -> None: with patch.object(sys, "argv", argv): with patch.object(s3m, "S3Artifacts") as mock_s3_cls: mock_instance = MagicMock() diff --git a/tests/s3/test_s3_artifacts.py b/tests/s3/test_s3_artifacts.py index b6765854..8b7e4016 100644 --- a/tests/s3/test_s3_artifacts.py +++ b/tests/s3/test_s3_artifacts.py @@ -8,6 +8,8 @@ from gardenlinux.s3.s3_artifacts import S3Artifacts +from .conftest import S3Env + RELEASE_DATA = """ GARDENLINUX_CNAME="container-amd64-1234.1-abc123" GARDENLINUX_VERSION=1234.1 @@ -20,7 +22,7 @@ """ -def test_s3artifacts_init_success(s3_setup): +def test_s3artifacts_init_success(s3_setup: S3Env) -> None: # Arrange env = s3_setup @@ -31,13 +33,13 @@ def test_s3artifacts_init_success(s3_setup): assert s3_artifacts.bucket.name == env.bucket_name -def tets_s3artifacts_invalid_bucket(): +def tets_s3artifacts_invalid_bucket() -> None: # Act / Assert with pytest.raises(Exception): S3Artifacts("unknown-bucket") -def test_download_to_directory_success(s3_setup): +def test_download_to_directory_success(s3_setup: S3Env) -> None: """ Test download of multiple files to a directory on disk. """ @@ -62,7 +64,7 @@ def test_download_to_directory_success(s3_setup): assert (outdir / "file2").read_bytes() == b"data2" -def test_download_to_directory_invalid_path(s3_setup): +def test_download_to_directory_invalid_path(s3_setup: S3Env) -> None: """ Test proper handling of download attempt to invalid path. """ @@ -72,10 +74,10 @@ def test_download_to_directory_invalid_path(s3_setup): # Act / Assert with pytest.raises(RuntimeError): - artifacts.download_to_directory({env.cname}, "/invalid/path/does/not/exist") + artifacts.download_to_directory(env.cname, "/invalid/path/does/not/exist") -def test_download_to_directory_non_pathlike_raises(s3_setup): +def test_download_to_directory_non_pathlike_raises(s3_setup: S3Env) -> None: """Raise RuntimeError if artifacts_dir is not a dir""" env = s3_setup artifacts = S3Artifacts(env.bucket_name) @@ -83,7 +85,7 @@ def test_download_to_directory_non_pathlike_raises(s3_setup): artifacts.download_to_directory(env.cname, "nopath") -def test_download_to_directory_no_metadata_raises(s3_setup): +def test_download_to_directory_no_metadata_raises(s3_setup: S3Env) -> None: """Should raise IndexError if bucket has no matching metadata object.""" # Arrange env = s3_setup @@ -95,7 +97,7 @@ def test_download_to_directory_no_metadata_raises(s3_setup): artifacts.download_to_directory(env.cname, tmpdir) -def test_upload_from_directory_success(s3_setup): +def test_upload_from_directory_success(s3_setup: S3Env) -> None: """ Test upload of multiple artifacts from disk to bucket """ @@ -131,7 +133,7 @@ def test_upload_from_directory_success(s3_setup): assert tags["platform"] == "container" -def test_upload_from_directory_with_delete(s3_setup): +def test_upload_from_directory_with_delete(s3_setup: S3Env) -> None: """ Test that upload_from_directory deletes existing files before uploading when delete_before_push=True. @@ -164,7 +166,7 @@ def test_upload_from_directory_with_delete(s3_setup): assert f"meta/singles/{env.cname}" in keys -def test_upload_from_directory_invalid_dir_raises(s3_setup): +def test_upload_from_directory_invalid_dir_raises(s3_setup: S3Env) -> None: """Raise RuntimeError if artifacts_dir is invalid""" env = s3_setup artifacts = S3Artifacts(env.bucket_name) @@ -172,7 +174,7 @@ def test_upload_from_directory_invalid_dir_raises(s3_setup): artifacts.upload_from_directory(env.cname, "/invalid/path") -def test_upload_from_directory_version_mismatch_raises(s3_setup): +def test_upload_from_directory_version_mismatch_raises(s3_setup: S3Env) -> None: """ RuntimeError if version in release file does not match cname. """ @@ -188,19 +190,21 @@ def test_upload_from_directory_version_mismatch_raises(s3_setup): artifacts.upload_from_directory(env.cname, env.tmp_path) -def test_upload_from_directory_succeeds_because_of_release_file(monkeypatch, s3_setup): +def test_upload_from_directory_succeeds_because_of_release_file( + monkeypatch: pytest.MonkeyPatch, s3_setup: S3Env +) -> None: """ Raise RuntimeError if CName.version is None. """ # Arrange env = s3_setup - (env.tmp_path / f"container.release").write_text(RELEASE_DATA) + (env.tmp_path / "container.release").write_text(RELEASE_DATA) artifacts = S3Artifacts(env.bucket_name) artifacts.upload_from_directory("container", env.tmp_path) -def test_upload_from_directory_invalid_artifact_name(s3_setup): +def test_upload_from_directory_invalid_artifact_name(s3_setup: S3Env) -> None: """ Raise RuntimeError if artifact file does not start with cname. """ @@ -222,7 +226,7 @@ def test_upload_from_directory_invalid_artifact_name(s3_setup): assert len(list(bucket.objects.filter(Prefix=f"meta/singles/{env.cname}"))) == 1 -def test_upload_from_directory_commit_mismatch_raises(s3_setup): +def test_upload_from_directory_commit_mismatch_raises(s3_setup: S3Env) -> None: """Raise RuntimeError when commit ID is not matching with cname.""" # Arrange env = s3_setup @@ -236,7 +240,7 @@ def test_upload_from_directory_commit_mismatch_raises(s3_setup): artifacts.upload_from_directory(env.cname, env.tmp_path) -def test_upload_directory_with_requirements_override(s3_setup): +def test_upload_directory_with_requirements_override(s3_setup: S3Env) -> None: """Ensure .requirements file values overide feature flag defaults.""" # Arrange env = s3_setup