diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..4f1e90a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "monthly" + groups: + production-dependencies: + dependency-type: "production" + development-dependencies: + dependency-type: "development" diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 2931fcf..3b7d42a 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -9,12 +9,12 @@ jobs: strategy: matrix: python-version: ["3.9", "3.10", "3.11", "3.12"] - dependency-extras: [""] + dependency-extras: ["", "-E all"] steps: - uses: actions/checkout@v3 - name: Install poetry - run: pipx install "poetry==1.5.1" + run: pipx install "poetry==1.8.3" - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 177cbf9..ca81d3f 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Install poetry - run: pipx install 'poetry==1.5.1' + run: pipx install 'poetry==1.8.3' - name: Set up Python uses: actions/setup-python@v4 with: diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 16291be..ae633f9 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -13,7 +13,7 @@ jobs: release-please: runs-on: ubuntu-latest steps: - - uses: google-github-actions/release-please-action@v3 + - uses: googleapis/release-please-action@v4 with: + token: ${{ secrets.ReleasePlease }} release-type: python - package-name: release-please-action diff --git a/.gitignore b/.gitignore index efcc369..78e757d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ - # Created by https://www.toptal.com/developers/gitignore/api/python # Edit at https://www.toptal.com/developers/gitignore?templates=python diff --git a/CHANGELOG.md b/CHANGELOG.md index 87b08cf..9629c86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,6 @@ * **cicd:** Use correct project type in release-please ([2e5f5ce](https://github.com/MaxG87/root-subvol-snapshot/commit/2e5f5cea76d10758e2b3c85c2231206a04268a87)) - ### Documentation * Add initial Changelog file ([8026b67](https://github.com/MaxG87/root-subvol-snapshot/commit/8026b6757a3f5984b679ec09742e356196b79b79)) diff --git a/example.cfg b/example.cfg new file mode 100644 index 0000000..461302a --- /dev/null +++ b/example.cfg @@ -0,0 +1,29 @@ +[ + { + "BackupRepositoryFolder": "ButterBackupRepository", + "Compression": "zstd:15", + "UUID": "UUID der Festplatte wie in /dev/disk/by-uuid/ angegeben.", + "DevicePassCmd": "gpg --decrypt /pfad/zu/mein-kostbares-password", + "Folders": { + "/home/mmustermann/Downloads": "Downloads", + "/home/mmustermann/Videos/Privat": "Private-Videos", + "/shared/Bilder": "Bilder" + }, + "Files": ["/etc/fstab"], + "FilesDest": "Einzeldateien" + }, + { + "BackupRepositoryFolder": "ResticBackupRepository", + "ExcludePatternsFile": "~/.config/backup-exclude-patterns", + "UUID": "Noch eine UUID wie in /dev/disk/by-uuid/.", + "DevicePassCmd": "gpg --decrypt /pfad/zu/supersicheres-pwt", + "RepositoryPassCmd": "gpg --decrypt /pfad/zu/supersicheres-pwt-repo", + "FilesAndFolders": [ + "/home/mmustermann/Downloads", + "/home/mmustermann/Videos/Privat", + "/shared/Bilder", + "/etc/fstab", + "~/.config/butter-backup.cfg" + ] + } +] diff --git a/poetry.lock b/poetry.lock index 0e797e9..edba27e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,15 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] [[package]] name = "attrs" @@ -21,33 +32,33 @@ tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "p [[package]] name = "black" -version = "24.1.1" +version = "24.4.2" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ - {file = "black-24.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2588021038bd5ada078de606f2a804cadd0a3cc6a79cb3e9bb3a8bf581325a4c"}, - {file = "black-24.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a95915c98d6e32ca43809d46d932e2abc5f1f7d582ffbe65a5b4d1588af7445"}, - {file = "black-24.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fa6a0e965779c8f2afb286f9ef798df770ba2b6cee063c650b96adec22c056a"}, - {file = "black-24.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:5242ecd9e990aeb995b6d03dc3b2d112d4a78f2083e5a8e86d566340ae80fec4"}, - {file = "black-24.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fc1ec9aa6f4d98d022101e015261c056ddebe3da6a8ccfc2c792cbe0349d48b7"}, - {file = "black-24.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0269dfdea12442022e88043d2910429bed717b2d04523867a85dacce535916b8"}, - {file = "black-24.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3d64db762eae4a5ce04b6e3dd745dcca0fb9560eb931a5be97472e38652a161"}, - {file = "black-24.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:5d7b06ea8816cbd4becfe5f70accae953c53c0e53aa98730ceccb0395520ee5d"}, - {file = "black-24.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e2c8dfa14677f90d976f68e0c923947ae68fa3961d61ee30976c388adc0b02c8"}, - {file = "black-24.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a21725862d0e855ae05da1dd25e3825ed712eaaccef6b03017fe0853a01aa45e"}, - {file = "black-24.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07204d078e25327aad9ed2c64790d681238686bce254c910de640c7cc4fc3aa6"}, - {file = "black-24.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:a83fe522d9698d8f9a101b860b1ee154c1d25f8a82ceb807d319f085b2627c5b"}, - {file = "black-24.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:08b34e85170d368c37ca7bf81cf67ac863c9d1963b2c1780c39102187ec8dd62"}, - {file = "black-24.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7258c27115c1e3b5de9ac6c4f9957e3ee2c02c0b39222a24dc7aa03ba0e986f5"}, - {file = "black-24.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40657e1b78212d582a0edecafef133cf1dd02e6677f539b669db4746150d38f6"}, - {file = "black-24.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e298d588744efda02379521a19639ebcd314fba7a49be22136204d7ed1782717"}, - {file = "black-24.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:34afe9da5056aa123b8bfda1664bfe6fb4e9c6f311d8e4a6eb089da9a9173bf9"}, - {file = "black-24.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:854c06fb86fd854140f37fb24dbf10621f5dab9e3b0c29a690ba595e3d543024"}, - {file = "black-24.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3897ae5a21ca132efa219c029cce5e6bfc9c3d34ed7e892113d199c0b1b444a2"}, - {file = "black-24.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:ecba2a15dfb2d97105be74bbfe5128bc5e9fa8477d8c46766505c1dda5883aac"}, - {file = "black-24.1.1-py3-none-any.whl", hash = "sha256:5cdc2e2195212208fbcae579b931407c1fa9997584f0a415421748aeafff1168"}, - {file = "black-24.1.1.tar.gz", hash = "sha256:48b5760dcbfe5cf97fd4fba23946681f3a81514c6ab8a45b50da67ac8fbc6c7b"}, + {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, + {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, + {file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"}, + {file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"}, + {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"}, + {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"}, + {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"}, + {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"}, + {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"}, + {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"}, + {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"}, + {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"}, + {file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"}, + {file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"}, + {file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"}, + {file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"}, + {file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"}, + {file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"}, + {file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"}, + {file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"}, + {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"}, + {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"}, ] [package.dependencies] @@ -92,63 +103,63 @@ files = [ [[package]] name = "coverage" -version = "7.4.1" +version = "7.6.0" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7"}, - {file = "coverage-7.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61"}, - {file = "coverage-7.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee"}, - {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3277f5fa7483c927fe3a7b017b39351610265308f5267ac6d4c2b64cc1d8d25"}, - {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dceb61d40cbfcf45f51e59933c784a50846dc03211054bd76b421a713dcdf19"}, - {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630"}, - {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c"}, - {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b"}, - {file = "coverage-7.4.1-cp310-cp310-win32.whl", hash = "sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016"}, - {file = "coverage-7.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018"}, - {file = "coverage-7.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295"}, - {file = "coverage-7.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c"}, - {file = "coverage-7.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676"}, - {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18e961aa13b6d47f758cc5879383d27b5b3f3dcd9ce8cdbfdc2571fe86feb4dd"}, - {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfd1e1b9f0898817babf840b77ce9fe655ecbe8b1b327983df485b30df8cc011"}, - {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74"}, - {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1"}, - {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6"}, - {file = "coverage-7.4.1-cp311-cp311-win32.whl", hash = "sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5"}, - {file = "coverage-7.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968"}, - {file = "coverage-7.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581"}, - {file = "coverage-7.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6"}, - {file = "coverage-7.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66"}, - {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0860a348bf7004c812c8368d1fc7f77fe8e4c095d661a579196a9533778e156"}, - {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3"}, - {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1"}, - {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1"}, - {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc"}, - {file = "coverage-7.4.1-cp312-cp312-win32.whl", hash = "sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74"}, - {file = "coverage-7.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448"}, - {file = "coverage-7.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218"}, - {file = "coverage-7.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45"}, - {file = "coverage-7.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d"}, - {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b094116f0b6155e36a304ff912f89bbb5067157aff5f94060ff20bbabdc8da06"}, - {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2f5968608b1fe2a1d00d01ad1017ee27efd99b3437e08b83ded9b7af3f6f766"}, - {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75"}, - {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60"}, - {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad"}, - {file = "coverage-7.4.1-cp38-cp38-win32.whl", hash = "sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042"}, - {file = "coverage-7.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d"}, - {file = "coverage-7.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54"}, - {file = "coverage-7.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70"}, - {file = "coverage-7.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628"}, - {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb2c2688ed93b027eb0d26aa188ada34acb22dceea256d76390eea135083950"}, - {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12c923757de24e4e2110cf8832d83a886a4cf215c6e61ed506006872b43a6d1"}, - {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7"}, - {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756"}, - {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35"}, - {file = "coverage-7.4.1-cp39-cp39-win32.whl", hash = "sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c"}, - {file = "coverage-7.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a"}, - {file = "coverage-7.4.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166"}, - {file = "coverage-7.4.1.tar.gz", hash = "sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04"}, + {file = "coverage-7.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dff044f661f59dace805eedb4a7404c573b6ff0cdba4a524141bc63d7be5c7fd"}, + {file = "coverage-7.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8659fd33ee9e6ca03950cfdcdf271d645cf681609153f218826dd9805ab585c"}, + {file = "coverage-7.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7792f0ab20df8071d669d929c75c97fecfa6bcab82c10ee4adb91c7a54055463"}, + {file = "coverage-7.6.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4b3cd1ca7cd73d229487fa5caca9e4bc1f0bca96526b922d61053ea751fe791"}, + {file = "coverage-7.6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7e128f85c0b419907d1f38e616c4f1e9f1d1b37a7949f44df9a73d5da5cd53c"}, + {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a94925102c89247530ae1dab7dc02c690942566f22e189cbd53579b0693c0783"}, + {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dcd070b5b585b50e6617e8972f3fbbee786afca71b1936ac06257f7e178f00f6"}, + {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d50a252b23b9b4dfeefc1f663c568a221092cbaded20a05a11665d0dbec9b8fb"}, + {file = "coverage-7.6.0-cp310-cp310-win32.whl", hash = "sha256:0e7b27d04131c46e6894f23a4ae186a6a2207209a05df5b6ad4caee6d54a222c"}, + {file = "coverage-7.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:54dece71673b3187c86226c3ca793c5f891f9fc3d8aa183f2e3653da18566169"}, + {file = "coverage-7.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7b525ab52ce18c57ae232ba6f7010297a87ced82a2383b1afd238849c1ff933"}, + {file = "coverage-7.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bea27c4269234e06f621f3fac3925f56ff34bc14521484b8f66a580aacc2e7d"}, + {file = "coverage-7.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed8d1d1821ba5fc88d4a4f45387b65de52382fa3ef1f0115a4f7a20cdfab0e94"}, + {file = "coverage-7.6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01c322ef2bbe15057bc4bf132b525b7e3f7206f071799eb8aa6ad1940bcf5fb1"}, + {file = "coverage-7.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03cafe82c1b32b770a29fd6de923625ccac3185a54a5e66606da26d105f37dac"}, + {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d1b923fc4a40c5832be4f35a5dab0e5ff89cddf83bb4174499e02ea089daf57"}, + {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4b03741e70fb811d1a9a1d75355cf391f274ed85847f4b78e35459899f57af4d"}, + {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a73d18625f6a8a1cbb11eadc1d03929f9510f4131879288e3f7922097a429f63"}, + {file = "coverage-7.6.0-cp311-cp311-win32.whl", hash = "sha256:65fa405b837060db569a61ec368b74688f429b32fa47a8929a7a2f9b47183713"}, + {file = "coverage-7.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:6379688fb4cfa921ae349c76eb1a9ab26b65f32b03d46bb0eed841fd4cb6afb1"}, + {file = "coverage-7.6.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f7db0b6ae1f96ae41afe626095149ecd1b212b424626175a6633c2999eaad45b"}, + {file = "coverage-7.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bbdf9a72403110a3bdae77948b8011f644571311c2fb35ee15f0f10a8fc082e8"}, + {file = "coverage-7.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc44bf0315268e253bf563f3560e6c004efe38f76db03a1558274a6e04bf5d5"}, + {file = "coverage-7.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da8549d17489cd52f85a9829d0e1d91059359b3c54a26f28bec2c5d369524807"}, + {file = "coverage-7.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0086cd4fc71b7d485ac93ca4239c8f75732c2ae3ba83f6be1c9be59d9e2c6382"}, + {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fad32ee9b27350687035cb5fdf9145bc9cf0a094a9577d43e909948ebcfa27b"}, + {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:044a0985a4f25b335882b0966625270a8d9db3d3409ddc49a4eb00b0ef5e8cee"}, + {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:76d5f82213aa78098b9b964ea89de4617e70e0d43e97900c2778a50856dac605"}, + {file = "coverage-7.6.0-cp312-cp312-win32.whl", hash = "sha256:3c59105f8d58ce500f348c5b56163a4113a440dad6daa2294b5052a10db866da"}, + {file = "coverage-7.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:ca5d79cfdae420a1d52bf177de4bc2289c321d6c961ae321503b2ca59c17ae67"}, + {file = "coverage-7.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d39bd10f0ae453554798b125d2f39884290c480f56e8a02ba7a6ed552005243b"}, + {file = "coverage-7.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:beb08e8508e53a568811016e59f3234d29c2583f6b6e28572f0954a6b4f7e03d"}, + {file = "coverage-7.6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2e16f4cd2bc4d88ba30ca2d3bbf2f21f00f382cf4e1ce3b1ddc96c634bc48ca"}, + {file = "coverage-7.6.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6616d1c9bf1e3faea78711ee42a8b972367d82ceae233ec0ac61cc7fec09fa6b"}, + {file = "coverage-7.6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad4567d6c334c46046d1c4c20024de2a1c3abc626817ae21ae3da600f5779b44"}, + {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d17c6a415d68cfe1091d3296ba5749d3d8696e42c37fca5d4860c5bf7b729f03"}, + {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9146579352d7b5f6412735d0f203bbd8d00113a680b66565e205bc605ef81bc6"}, + {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:cdab02a0a941af190df8782aafc591ef3ad08824f97850b015c8c6a8b3877b0b"}, + {file = "coverage-7.6.0-cp38-cp38-win32.whl", hash = "sha256:df423f351b162a702c053d5dddc0fc0ef9a9e27ea3f449781ace5f906b664428"}, + {file = "coverage-7.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:f2501d60d7497fd55e391f423f965bbe9e650e9ffc3c627d5f0ac516026000b8"}, + {file = "coverage-7.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7221f9ac9dad9492cecab6f676b3eaf9185141539d5c9689d13fd6b0d7de840c"}, + {file = "coverage-7.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ddaaa91bfc4477d2871442bbf30a125e8fe6b05da8a0015507bfbf4718228ab2"}, + {file = "coverage-7.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4cbe651f3904e28f3a55d6f371203049034b4ddbce65a54527a3f189ca3b390"}, + {file = "coverage-7.6.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:831b476d79408ab6ccfadaaf199906c833f02fdb32c9ab907b1d4aa0713cfa3b"}, + {file = "coverage-7.6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46c3d091059ad0b9c59d1034de74a7f36dcfa7f6d3bde782c49deb42438f2450"}, + {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4d5fae0a22dc86259dee66f2cc6c1d3e490c4a1214d7daa2a93d07491c5c04b6"}, + {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:07ed352205574aad067482e53dd606926afebcb5590653121063fbf4e2175166"}, + {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:49c76cdfa13015c4560702574bad67f0e15ca5a2872c6a125f6327ead2b731dd"}, + {file = "coverage-7.6.0-cp39-cp39-win32.whl", hash = "sha256:482855914928c8175735a2a59c8dc5806cf7d8f032e4820d52e845d1f731dca2"}, + {file = "coverage-7.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:543ef9179bc55edfd895154a51792b01c017c87af0ebaae092720152e19e42ca"}, + {file = "coverage-7.6.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:6fe885135c8a479d3e37a7aae61cbd3a0fb2deccb4dda3c25f92a49189f766d6"}, + {file = "coverage-7.6.0.tar.gz", hash = "sha256:289cc803fa1dc901f84701ac10c9ee873619320f2f9aff38794db4a4a0268d51"}, ] [package.dependencies] @@ -169,18 +180,32 @@ files = [ [[package]] name = "exceptiongroup" -version = "1.2.0" +version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, - {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, ] [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "execnet" +version = "2.1.1" +description = "execnet: rapid multi-Python deployment" +optional = false +python-versions = ">=3.8" +files = [ + {file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"}, + {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"}, +] + +[package.extras] +testing = ["hatch", "pre-commit", "pytest", "tox"] + [[package]] name = "fancycompleter" version = "0.9.1" @@ -267,6 +292,39 @@ files = [ docs = ["Sphinx", "furo"] test = ["objgraph", "psutil"] +[[package]] +name = "hypothesis" +version = "6.108.4" +description = "A library for property-based testing" +optional = false +python-versions = ">=3.8" +files = [ + {file = "hypothesis-6.108.4-py3-none-any.whl", hash = "sha256:901b1883b51207c4c3ecbae506bc8b65d66569f9bc34e023df7d8a821eb495c1"}, + {file = "hypothesis-6.108.4.tar.gz", hash = "sha256:bab99a308ea39be53882f1d89ab77db48e0c03b5c37fbedd2f59f9b656ada301"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +exceptiongroup = {version = ">=1.0.0", markers = "python_version < \"3.11\""} +sortedcontainers = ">=2.1.0,<3.0.0" + +[package.extras] +all = ["backports.zoneinfo (>=0.2.1)", "black (>=19.10b0)", "click (>=7.0)", "crosshair-tool (>=0.0.63)", "django (>=3.2)", "dpcontracts (>=0.4)", "hypothesis-crosshair (>=0.0.9)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.17.3)", "pandas (>=1.1)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2024.1)"] +cli = ["black (>=19.10b0)", "click (>=7.0)", "rich (>=9.0.0)"] +codemods = ["libcst (>=0.3.16)"] +crosshair = ["crosshair-tool (>=0.0.63)", "hypothesis-crosshair (>=0.0.9)"] +dateutil = ["python-dateutil (>=1.4)"] +django = ["django (>=3.2)"] +dpcontracts = ["dpcontracts (>=0.4)"] +ghostwriter = ["black (>=19.10b0)"] +lark = ["lark (>=0.10.1)"] +numpy = ["numpy (>=1.17.3)"] +pandas = ["pandas (>=1.1)"] +pytest = ["pytest (>=4.6)"] +pytz = ["pytz (>=2014.1)"] +redis = ["redis (>=3.0.0)"] +zoneinfo = ["backports.zoneinfo (>=0.2.1)", "tzdata (>=2024.1)"] + [[package]] name = "iniconfig" version = "2.0.0" @@ -278,111 +336,164 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "loguru" +version = "0.7.2" +description = "Python logging made (stupidly) simple" +optional = false +python-versions = ">=3.5" +files = [ + {file = "loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb"}, + {file = "loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac"}, +] + +[package.dependencies] +colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} +win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} + +[package.extras] +dev = ["Sphinx (==7.2.5)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.2.2)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.4.1)", "mypy (==v1.5.1)", "pre-commit (==3.4.0)", "pytest (==6.1.2)", "pytest (==7.4.0)", "pytest-cov (==2.12.1)", "pytest-cov (==4.1.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.0.0)", "sphinx-autobuild (==2021.3.14)", "sphinx-rtd-theme (==1.3.0)", "tox (==3.27.1)", "tox (==4.11.0)"] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + [[package]] name = "msgpack" -version = "1.0.7" +version = "1.0.8" description = "MessagePack serializer" optional = false python-versions = ">=3.8" files = [ - {file = "msgpack-1.0.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04ad6069c86e531682f9e1e71b71c1c3937d6014a7c3e9edd2aa81ad58842862"}, - {file = "msgpack-1.0.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cca1b62fe70d761a282496b96a5e51c44c213e410a964bdffe0928e611368329"}, - {file = "msgpack-1.0.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e50ebce52f41370707f1e21a59514e3375e3edd6e1832f5e5235237db933c98b"}, - {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7b4f35de6a304b5533c238bee86b670b75b03d31b7797929caa7a624b5dda6"}, - {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28efb066cde83c479dfe5a48141a53bc7e5f13f785b92ddde336c716663039ee"}, - {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4cb14ce54d9b857be9591ac364cb08dc2d6a5c4318c1182cb1d02274029d590d"}, - {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b573a43ef7c368ba4ea06050a957c2a7550f729c31f11dd616d2ac4aba99888d"}, - {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ccf9a39706b604d884d2cb1e27fe973bc55f2890c52f38df742bc1d79ab9f5e1"}, - {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cb70766519500281815dfd7a87d3a178acf7ce95390544b8c90587d76b227681"}, - {file = "msgpack-1.0.7-cp310-cp310-win32.whl", hash = "sha256:b610ff0f24e9f11c9ae653c67ff8cc03c075131401b3e5ef4b82570d1728f8a9"}, - {file = "msgpack-1.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:a40821a89dc373d6427e2b44b572efc36a2778d3f543299e2f24eb1a5de65415"}, - {file = "msgpack-1.0.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:576eb384292b139821c41995523654ad82d1916da6a60cff129c715a6223ea84"}, - {file = "msgpack-1.0.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:730076207cb816138cf1af7f7237b208340a2c5e749707457d70705715c93b93"}, - {file = "msgpack-1.0.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:85765fdf4b27eb5086f05ac0491090fc76f4f2b28e09d9350c31aac25a5aaff8"}, - {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3476fae43db72bd11f29a5147ae2f3cb22e2f1a91d575ef130d2bf49afd21c46"}, - {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d4c80667de2e36970ebf74f42d1088cc9ee7ef5f4e8c35eee1b40eafd33ca5b"}, - {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b0bf0effb196ed76b7ad883848143427a73c355ae8e569fa538365064188b8e"}, - {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f9a7c509542db4eceed3dcf21ee5267ab565a83555c9b88a8109dcecc4709002"}, - {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:84b0daf226913133f899ea9b30618722d45feffa67e4fe867b0b5ae83a34060c"}, - {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ec79ff6159dffcc30853b2ad612ed572af86c92b5168aa3fc01a67b0fa40665e"}, - {file = "msgpack-1.0.7-cp311-cp311-win32.whl", hash = "sha256:3e7bf4442b310ff154b7bb9d81eb2c016b7d597e364f97d72b1acc3817a0fdc1"}, - {file = "msgpack-1.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:3f0c8c6dfa6605ab8ff0611995ee30d4f9fcff89966cf562733b4008a3d60d82"}, - {file = "msgpack-1.0.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f0936e08e0003f66bfd97e74ee530427707297b0d0361247e9b4f59ab78ddc8b"}, - {file = "msgpack-1.0.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98bbd754a422a0b123c66a4c341de0474cad4a5c10c164ceed6ea090f3563db4"}, - {file = "msgpack-1.0.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b291f0ee7961a597cbbcc77709374087fa2a9afe7bdb6a40dbbd9b127e79afee"}, - {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebbbba226f0a108a7366bf4b59bf0f30a12fd5e75100c630267d94d7f0ad20e5"}, - {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e2d69948e4132813b8d1131f29f9101bc2c915f26089a6d632001a5c1349672"}, - {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdf38ba2d393c7911ae989c3bbba510ebbcdf4ecbdbfec36272abe350c454075"}, - {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:993584fc821c58d5993521bfdcd31a4adf025c7d745bbd4d12ccfecf695af5ba"}, - {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:52700dc63a4676669b341ba33520f4d6e43d3ca58d422e22ba66d1736b0a6e4c"}, - {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e45ae4927759289c30ccba8d9fdce62bb414977ba158286b5ddaf8df2cddb5c5"}, - {file = "msgpack-1.0.7-cp312-cp312-win32.whl", hash = "sha256:27dcd6f46a21c18fa5e5deed92a43d4554e3df8d8ca5a47bf0615d6a5f39dbc9"}, - {file = "msgpack-1.0.7-cp312-cp312-win_amd64.whl", hash = "sha256:7687e22a31e976a0e7fc99c2f4d11ca45eff652a81eb8c8085e9609298916dcf"}, - {file = "msgpack-1.0.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5b6ccc0c85916998d788b295765ea0e9cb9aac7e4a8ed71d12e7d8ac31c23c95"}, - {file = "msgpack-1.0.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:235a31ec7db685f5c82233bddf9858748b89b8119bf4538d514536c485c15fe0"}, - {file = "msgpack-1.0.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cab3db8bab4b7e635c1c97270d7a4b2a90c070b33cbc00c99ef3f9be03d3e1f7"}, - {file = "msgpack-1.0.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bfdd914e55e0d2c9e1526de210f6fe8ffe9705f2b1dfcc4aecc92a4cb4b533d"}, - {file = "msgpack-1.0.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36e17c4592231a7dbd2ed09027823ab295d2791b3b1efb2aee874b10548b7524"}, - {file = "msgpack-1.0.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38949d30b11ae5f95c3c91917ee7a6b239f5ec276f271f28638dec9156f82cfc"}, - {file = "msgpack-1.0.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ff1d0899f104f3921d94579a5638847f783c9b04f2d5f229392ca77fba5b82fc"}, - {file = "msgpack-1.0.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:dc43f1ec66eb8440567186ae2f8c447d91e0372d793dfe8c222aec857b81a8cf"}, - {file = "msgpack-1.0.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dd632777ff3beaaf629f1ab4396caf7ba0bdd075d948a69460d13d44357aca4c"}, - {file = "msgpack-1.0.7-cp38-cp38-win32.whl", hash = "sha256:4e71bc4416de195d6e9b4ee93ad3f2f6b2ce11d042b4d7a7ee00bbe0358bd0c2"}, - {file = "msgpack-1.0.7-cp38-cp38-win_amd64.whl", hash = "sha256:8f5b234f567cf76ee489502ceb7165c2a5cecec081db2b37e35332b537f8157c"}, - {file = "msgpack-1.0.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfef2bb6ef068827bbd021017a107194956918ab43ce4d6dc945ffa13efbc25f"}, - {file = "msgpack-1.0.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:484ae3240666ad34cfa31eea7b8c6cd2f1fdaae21d73ce2974211df099a95d81"}, - {file = "msgpack-1.0.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3967e4ad1aa9da62fd53e346ed17d7b2e922cba5ab93bdd46febcac39be636fc"}, - {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dd178c4c80706546702c59529ffc005681bd6dc2ea234c450661b205445a34d"}, - {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6ffbc252eb0d229aeb2f9ad051200668fc3a9aaa8994e49f0cb2ffe2b7867e7"}, - {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:822ea70dc4018c7e6223f13affd1c5c30c0f5c12ac1f96cd8e9949acddb48a61"}, - {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:384d779f0d6f1b110eae74cb0659d9aa6ff35aaf547b3955abf2ab4c901c4819"}, - {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f64e376cd20d3f030190e8c32e1c64582eba56ac6dc7d5b0b49a9d44021b52fd"}, - {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ed82f5a7af3697b1c4786053736f24a0efd0a1b8a130d4c7bfee4b9ded0f08f"}, - {file = "msgpack-1.0.7-cp39-cp39-win32.whl", hash = "sha256:f26a07a6e877c76a88e3cecac8531908d980d3d5067ff69213653649ec0f60ad"}, - {file = "msgpack-1.0.7-cp39-cp39-win_amd64.whl", hash = "sha256:1dc93e8e4653bdb5910aed79f11e165c85732067614f180f70534f056da97db3"}, - {file = "msgpack-1.0.7.tar.gz", hash = "sha256:572efc93db7a4d27e404501975ca6d2d9775705c2d922390d878fcf768d92c87"}, + {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:505fe3d03856ac7d215dbe005414bc28505d26f0c128906037e66d98c4e95868"}, + {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b7842518a63a9f17107eb176320960ec095a8ee3b4420b5f688e24bf50c53c"}, + {file = "msgpack-1.0.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:376081f471a2ef24828b83a641a02c575d6103a3ad7fd7dade5486cad10ea659"}, + {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e390971d082dba073c05dbd56322427d3280b7cc8b53484c9377adfbae67dc2"}, + {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e073efcba9ea99db5acef3959efa45b52bc67b61b00823d2a1a6944bf45982"}, + {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82d92c773fbc6942a7a8b520d22c11cfc8fd83bba86116bfcf962c2f5c2ecdaa"}, + {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9ee32dcb8e531adae1f1ca568822e9b3a738369b3b686d1477cbc643c4a9c128"}, + {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e3aa7e51d738e0ec0afbed661261513b38b3014754c9459508399baf14ae0c9d"}, + {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69284049d07fce531c17404fcba2bb1df472bc2dcdac642ae71a2d079d950653"}, + {file = "msgpack-1.0.8-cp310-cp310-win32.whl", hash = "sha256:13577ec9e247f8741c84d06b9ece5f654920d8365a4b636ce0e44f15e07ec693"}, + {file = "msgpack-1.0.8-cp310-cp310-win_amd64.whl", hash = "sha256:e532dbd6ddfe13946de050d7474e3f5fb6ec774fbb1a188aaf469b08cf04189a"}, + {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9517004e21664f2b5a5fd6333b0731b9cf0817403a941b393d89a2f1dc2bd836"}, + {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d16a786905034e7e34098634b184a7d81f91d4c3d246edc6bd7aefb2fd8ea6ad"}, + {file = "msgpack-1.0.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2872993e209f7ed04d963e4b4fbae72d034844ec66bc4ca403329db2074377b"}, + {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c330eace3dd100bdb54b5653b966de7f51c26ec4a7d4e87132d9b4f738220ba"}, + {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b5c044f3eff2a6534768ccfd50425939e7a8b5cf9a7261c385de1e20dcfc85"}, + {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1876b0b653a808fcd50123b953af170c535027bf1d053b59790eebb0aeb38950"}, + {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dfe1f0f0ed5785c187144c46a292b8c34c1295c01da12e10ccddfc16def4448a"}, + {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3528807cbbb7f315bb81959d5961855e7ba52aa60a3097151cb21956fbc7502b"}, + {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e2f879ab92ce502a1e65fce390eab619774dda6a6ff719718069ac94084098ce"}, + {file = "msgpack-1.0.8-cp311-cp311-win32.whl", hash = "sha256:26ee97a8261e6e35885c2ecd2fd4a6d38252246f94a2aec23665a4e66d066305"}, + {file = "msgpack-1.0.8-cp311-cp311-win_amd64.whl", hash = "sha256:eadb9f826c138e6cf3c49d6f8de88225a3c0ab181a9b4ba792e006e5292d150e"}, + {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:114be227f5213ef8b215c22dde19532f5da9652e56e8ce969bf0a26d7c419fee"}, + {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d661dc4785affa9d0edfdd1e59ec056a58b3dbb9f196fa43587f3ddac654ac7b"}, + {file = "msgpack-1.0.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d56fd9f1f1cdc8227d7b7918f55091349741904d9520c65f0139a9755952c9e8"}, + {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0726c282d188e204281ebd8de31724b7d749adebc086873a59efb8cf7ae27df3"}, + {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8db8e423192303ed77cff4dce3a4b88dbfaf43979d280181558af5e2c3c71afc"}, + {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99881222f4a8c2f641f25703963a5cefb076adffd959e0558dc9f803a52d6a58"}, + {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b5505774ea2a73a86ea176e8a9a4a7c8bf5d521050f0f6f8426afe798689243f"}, + {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ef254a06bcea461e65ff0373d8a0dd1ed3aa004af48839f002a0c994a6f72d04"}, + {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e1dd7839443592d00e96db831eddb4111a2a81a46b028f0facd60a09ebbdd543"}, + {file = "msgpack-1.0.8-cp312-cp312-win32.whl", hash = "sha256:64d0fcd436c5683fdd7c907eeae5e2cbb5eb872fafbc03a43609d7941840995c"}, + {file = "msgpack-1.0.8-cp312-cp312-win_amd64.whl", hash = "sha256:74398a4cf19de42e1498368c36eed45d9528f5fd0155241e82c4082b7e16cffd"}, + {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0ceea77719d45c839fd73abcb190b8390412a890df2f83fb8cf49b2a4b5c2f40"}, + {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1ab0bbcd4d1f7b6991ee7c753655b481c50084294218de69365f8f1970d4c151"}, + {file = "msgpack-1.0.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1cce488457370ffd1f953846f82323cb6b2ad2190987cd4d70b2713e17268d24"}, + {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3923a1778f7e5ef31865893fdca12a8d7dc03a44b33e2a5f3295416314c09f5d"}, + {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a22e47578b30a3e199ab067a4d43d790249b3c0587d9a771921f86250c8435db"}, + {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd739c9251d01e0279ce729e37b39d49a08c0420d3fee7f2a4968c0576678f77"}, + {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d3420522057ebab1728b21ad473aa950026d07cb09da41103f8e597dfbfaeb13"}, + {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5845fdf5e5d5b78a49b826fcdc0eb2e2aa7191980e3d2cfd2a30303a74f212e2"}, + {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a0e76621f6e1f908ae52860bdcb58e1ca85231a9b0545e64509c931dd34275a"}, + {file = "msgpack-1.0.8-cp38-cp38-win32.whl", hash = "sha256:374a8e88ddab84b9ada695d255679fb99c53513c0a51778796fcf0944d6c789c"}, + {file = "msgpack-1.0.8-cp38-cp38-win_amd64.whl", hash = "sha256:f3709997b228685fe53e8c433e2df9f0cdb5f4542bd5114ed17ac3c0129b0480"}, + {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f51bab98d52739c50c56658cc303f190785f9a2cd97b823357e7aeae54c8f68a"}, + {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:73ee792784d48aa338bba28063e19a27e8d989344f34aad14ea6e1b9bd83f596"}, + {file = "msgpack-1.0.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f9904e24646570539a8950400602d66d2b2c492b9010ea7e965025cb71d0c86d"}, + {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e75753aeda0ddc4c28dce4c32ba2f6ec30b1b02f6c0b14e547841ba5b24f753f"}, + {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5dbf059fb4b7c240c873c1245ee112505be27497e90f7c6591261c7d3c3a8228"}, + {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4916727e31c28be8beaf11cf117d6f6f188dcc36daae4e851fee88646f5b6b18"}, + {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7938111ed1358f536daf311be244f34df7bf3cdedb3ed883787aca97778b28d8"}, + {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:493c5c5e44b06d6c9268ce21b302c9ca055c1fd3484c25ba41d34476c76ee746"}, + {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"}, + {file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"}, + {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"}, + {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, ] [[package]] name = "mypy" -version = "1.8.0" +version = "1.11.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3"}, - {file = "mypy-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4"}, - {file = "mypy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d"}, - {file = "mypy-1.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9"}, - {file = "mypy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410"}, - {file = "mypy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae"}, - {file = "mypy-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3"}, - {file = "mypy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817"}, - {file = "mypy-1.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d"}, - {file = "mypy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835"}, - {file = "mypy-1.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd"}, - {file = "mypy-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55"}, - {file = "mypy-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218"}, - {file = "mypy-1.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3"}, - {file = "mypy-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e"}, - {file = "mypy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6"}, - {file = "mypy-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66"}, - {file = "mypy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6"}, - {file = "mypy-1.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d"}, - {file = "mypy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02"}, - {file = "mypy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8"}, - {file = "mypy-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259"}, - {file = "mypy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b"}, - {file = "mypy-1.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592"}, - {file = "mypy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a"}, - {file = "mypy-1.8.0-py3-none-any.whl", hash = "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d"}, - {file = "mypy-1.8.0.tar.gz", hash = "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07"}, + {file = "mypy-1.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3824187c99b893f90c845bab405a585d1ced4ff55421fdf5c84cb7710995229"}, + {file = "mypy-1.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:96f8dbc2c85046c81bcddc246232d500ad729cb720da4e20fce3b542cab91287"}, + {file = "mypy-1.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a5d8d8dd8613a3e2be3eae829ee891b6b2de6302f24766ff06cb2875f5be9c6"}, + {file = "mypy-1.11.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:72596a79bbfb195fd41405cffa18210af3811beb91ff946dbcb7368240eed6be"}, + {file = "mypy-1.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:35ce88b8ed3a759634cb4eb646d002c4cef0a38f20565ee82b5023558eb90c00"}, + {file = "mypy-1.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:98790025861cb2c3db8c2f5ad10fc8c336ed2a55f4daf1b8b3f877826b6ff2eb"}, + {file = "mypy-1.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:25bcfa75b9b5a5f8d67147a54ea97ed63a653995a82798221cca2a315c0238c1"}, + {file = "mypy-1.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bea2a0e71c2a375c9fa0ede3d98324214d67b3cbbfcbd55ac8f750f85a414e3"}, + {file = "mypy-1.11.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2b3d36baac48e40e3064d2901f2fbd2a2d6880ec6ce6358825c85031d7c0d4d"}, + {file = "mypy-1.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:d8e2e43977f0e09f149ea69fd0556623919f816764e26d74da0c8a7b48f3e18a"}, + {file = "mypy-1.11.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1d44c1e44a8be986b54b09f15f2c1a66368eb43861b4e82573026e04c48a9e20"}, + {file = "mypy-1.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cea3d0fb69637944dd321f41bc896e11d0fb0b0aa531d887a6da70f6e7473aba"}, + {file = "mypy-1.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a83ec98ae12d51c252be61521aa5731f5512231d0b738b4cb2498344f0b840cd"}, + {file = "mypy-1.11.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c7b73a856522417beb78e0fb6d33ef89474e7a622db2653bc1285af36e2e3e3d"}, + {file = "mypy-1.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:f2268d9fcd9686b61ab64f077be7ffbc6fbcdfb4103e5dd0cc5eaab53a8886c2"}, + {file = "mypy-1.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:940bfff7283c267ae6522ef926a7887305945f716a7704d3344d6d07f02df850"}, + {file = "mypy-1.11.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:14f9294528b5f5cf96c721f231c9f5b2733164e02c1c018ed1a0eff8a18005ac"}, + {file = "mypy-1.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7b54c27783991399046837df5c7c9d325d921394757d09dbcbf96aee4649fe9"}, + {file = "mypy-1.11.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:65f190a6349dec29c8d1a1cd4aa71284177aee5949e0502e6379b42873eddbe7"}, + {file = "mypy-1.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:dbe286303241fea8c2ea5466f6e0e6a046a135a7e7609167b07fd4e7baf151bf"}, + {file = "mypy-1.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:104e9c1620c2675420abd1f6c44bab7dd33cc85aea751c985006e83dcd001095"}, + {file = "mypy-1.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f006e955718ecd8d159cee9932b64fba8f86ee6f7728ca3ac66c3a54b0062abe"}, + {file = "mypy-1.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:becc9111ca572b04e7e77131bc708480cc88a911adf3d0239f974c034b78085c"}, + {file = "mypy-1.11.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6801319fe76c3f3a3833f2b5af7bd2c17bb93c00026a2a1b924e6762f5b19e13"}, + {file = "mypy-1.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:c1a184c64521dc549324ec6ef7cbaa6b351912be9cb5edb803c2808a0d7e85ac"}, + {file = "mypy-1.11.0-py3-none-any.whl", hash = "sha256:56913ec8c7638b0091ef4da6fcc9136896914a9d60d54670a75880c3e5b99ace"}, + {file = "mypy-1.11.0.tar.gz", hash = "sha256:93743608c7348772fdc717af4aeee1997293a1ad04bc0ea6efa15bf65385c538"}, ] [package.dependencies] mypy-extensions = ">=1.0.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=4.1.0" +typing-extensions = ">=4.6.0" [package.extras] dmypy = ["psutil (>=4.0)"] @@ -403,13 +514,13 @@ files = [ [[package]] name = "packaging" -version = "23.2" +version = "24.1" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, - {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] [[package]] @@ -445,47 +556,167 @@ testing = ["funcsigs", "pytest"] [[package]] name = "platformdirs" -version = "4.2.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +version = "4.2.2" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, - {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, + {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, + {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, ] [package.extras] docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] +type = ["mypy (>=1.8)"] [[package]] name = "pluggy" -version = "1.4.0" +version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" files = [ - {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, - {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "pydantic" +version = "2.8.2" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, + {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.20.1" +typing-extensions = {version = ">=4.6.1", markers = "python_version < \"3.13\""} + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.20.1" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"}, + {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"}, + {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"}, + {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"}, + {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"}, + {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"}, + {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"}, + {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"}, + {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"}, + {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"}, + {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"}, + {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"}, + {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"}, + {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"}, + {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"}, + {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"}, + {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"}, + {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"}, + {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"}, + {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"}, + {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"}, + {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"}, + {file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"}, + {file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"}, + {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"}, + {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"}, + {file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"}, + {file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"}, + {file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"}, + {file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"}, + {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"}, + {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"}, + {file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"}, + {file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"}, + {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + [[package]] name = "pygments" -version = "2.17.2" +version = "2.18.0" description = "Pygments is a syntax highlighting package written in Python." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, - {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, ] [package.extras] -plugins = ["importlib-metadata"] windows-terminal = ["colorama (>=0.4.6)"] [[package]] @@ -529,13 +760,13 @@ files = [ [[package]] name = "pytest" -version = "8.0.0" +version = "8.3.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.0.0-py3-none-any.whl", hash = "sha256:50fb9cbe836c3f20f0dfa99c565201fb75dc54c8d76373cd1bde06b06657bdb6"}, - {file = "pytest-8.0.0.tar.gz", hash = "sha256:249b1b0864530ba251b7438274c4d251c58d868edaaec8762893ad4a0d71c36c"}, + {file = "pytest-8.3.1-py3-none-any.whl", hash = "sha256:e9600ccf4f563976e2c99fa02c7624ab938296551f280835ee6516df8bc4ae8c"}, + {file = "pytest-8.3.1.tar.gz", hash = "sha256:7e8e5c5abd6e93cb1cc151f23e57adc31fcf8cfd2a3ff2da63e23f732de35db6"}, ] [package.dependencies] @@ -543,21 +774,21 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" -pluggy = ">=1.3.0,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-cov" -version = "4.1.0" +version = "5.0.0" description = "Pytest plugin for measuring coverage." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, - {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, + {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, + {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, ] [package.dependencies] @@ -565,32 +796,71 @@ coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] + +[[package]] +name = "pytest-xdist" +version = "3.6.1" +description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"}, + {file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"}, +] + +[package.dependencies] +execnet = ">=2.1" +pytest = ">=7.0.0" + +[package.extras] +psutil = ["psutil (>=3.0)"] +setproctitle = ["setproctitle"] +testing = ["filelock"] + +[[package]] +name = "rich" +version = "13.7.1" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, + {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.2.0" +version = "0.5.4" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.2.0-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:638ea3294f800d18bae84a492cb5a245c8d29c90d19a91d8e338937a4c27fca0"}, - {file = "ruff-0.2.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3ff35433fcf4dff6d610738712152df6b7d92351a1bde8e00bd405b08b3d5759"}, - {file = "ruff-0.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf9faafbdcf4f53917019f2c230766da437d4fd5caecd12ddb68bb6a17d74399"}, - {file = "ruff-0.2.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8153a3e4128ed770871c47545f1ae7b055023e0c222ff72a759f5a341ee06483"}, - {file = "ruff-0.2.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8a75a98ae989a27090e9c51f763990ad5bbc92d20626d54e9701c7fe597f399"}, - {file = "ruff-0.2.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:87057dd2fdde297130ff99553be8549ca38a2965871462a97394c22ed2dfc19d"}, - {file = "ruff-0.2.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d232f99d3ab00094ebaf88e0fb7a8ccacaa54cc7fa3b8993d9627a11e6aed7a"}, - {file = "ruff-0.2.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d3c641f95f435fc6754b05591774a17df41648f0daf3de0d75ad3d9f099ab92"}, - {file = "ruff-0.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3826fb34c144ef1e171b323ed6ae9146ab76d109960addca730756dc19dc7b22"}, - {file = "ruff-0.2.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:eceab7d85d09321b4de18b62d38710cf296cb49e98979960a59c6b9307c18cfe"}, - {file = "ruff-0.2.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:30ad74687e1f4a9ff8e513b20b82ccadb6bd796fe5697f1e417189c5cde6be3e"}, - {file = "ruff-0.2.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a7e3818698f8460bd0f8d4322bbe99db8327e9bc2c93c789d3159f5b335f47da"}, - {file = "ruff-0.2.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:edf23041242c48b0d8295214783ef543847ef29e8226d9f69bf96592dba82a83"}, - {file = "ruff-0.2.0-py3-none-win32.whl", hash = "sha256:e155147199c2714ff52385b760fe242bb99ea64b240a9ffbd6a5918eb1268843"}, - {file = "ruff-0.2.0-py3-none-win_amd64.whl", hash = "sha256:ba918e01cdd21e81b07555564f40d307b0caafa9a7a65742e98ff244f5035c59"}, - {file = "ruff-0.2.0-py3-none-win_arm64.whl", hash = "sha256:3fbaff1ba9564a2c5943f8f38bc221f04bac687cc7485e45237579fee7ccda79"}, - {file = "ruff-0.2.0.tar.gz", hash = "sha256:63856b91837606c673537d2889989733d7dffde553828d3b0f0bacfa6def54be"}, + {file = "ruff-0.5.4-py3-none-linux_armv6l.whl", hash = "sha256:82acef724fc639699b4d3177ed5cc14c2a5aacd92edd578a9e846d5b5ec18ddf"}, + {file = "ruff-0.5.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:da62e87637c8838b325e65beee485f71eb36202ce8e3cdbc24b9fcb8b99a37be"}, + {file = "ruff-0.5.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e98ad088edfe2f3b85a925ee96da652028f093d6b9b56b76fc242d8abb8e2059"}, + {file = "ruff-0.5.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c55efbecc3152d614cfe6c2247a3054cfe358cefbf794f8c79c8575456efe19"}, + {file = "ruff-0.5.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f9b85eaa1f653abd0a70603b8b7008d9e00c9fa1bbd0bf40dad3f0c0bdd06793"}, + {file = "ruff-0.5.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cf497a47751be8c883059c4613ba2f50dd06ec672692de2811f039432875278"}, + {file = "ruff-0.5.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:09c14ed6a72af9ccc8d2e313d7acf7037f0faff43cde4b507e66f14e812e37f7"}, + {file = "ruff-0.5.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:628f6b8f97b8bad2490240aa84f3e68f390e13fabc9af5c0d3b96b485921cd60"}, + {file = "ruff-0.5.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3520a00c0563d7a7a7c324ad7e2cde2355733dafa9592c671fb2e9e3cd8194c1"}, + {file = "ruff-0.5.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93789f14ca2244fb91ed481456f6d0bb8af1f75a330e133b67d08f06ad85b516"}, + {file = "ruff-0.5.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:029454e2824eafa25b9df46882f7f7844d36fd8ce51c1b7f6d97e2615a57bbcc"}, + {file = "ruff-0.5.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9492320eed573a13a0bc09a2957f17aa733fff9ce5bf00e66e6d4a88ec33813f"}, + {file = "ruff-0.5.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a6e1f62a92c645e2919b65c02e79d1f61e78a58eddaebca6c23659e7c7cb4ac7"}, + {file = "ruff-0.5.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:768fa9208df2bec4b2ce61dbc7c2ddd6b1be9fb48f1f8d3b78b3332c7d71c1ff"}, + {file = "ruff-0.5.4-py3-none-win32.whl", hash = "sha256:e1e7393e9c56128e870b233c82ceb42164966f25b30f68acbb24ed69ce9c3a4e"}, + {file = "ruff-0.5.4-py3-none-win_amd64.whl", hash = "sha256:58b54459221fd3f661a7329f177f091eb35cf7a603f01d9eb3eb11cc348d38c4"}, + {file = "ruff-0.5.4-py3-none-win_arm64.whl", hash = "sha256:bd53da65f1085fb5b307c38fd3c0829e76acf7b2a912d8d79cadcdb4875c1eb7"}, + {file = "ruff-0.5.4.tar.gz", hash = "sha256:2795726d5f71c4f4e70653273d1c23a8182f07dd8e48c12de5d867bfb7557eed"}, ] [[package]] @@ -607,6 +877,28 @@ files = [ [package.extras] logging = ["loguru (>=0.7.2,<0.8.0)"] +[[package]] +name = "shellingham" +version = "1.5.4" +description = "Tool to Detect Surrounding Shell" +optional = false +python-versions = ">=3.7" +files = [ + {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, + {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +optional = false +python-versions = "*" +files = [ + {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, + {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, +] + [[package]] name = "storage-device-managers" version = "0.14.0" @@ -637,36 +929,46 @@ files = [ [[package]] name = "typer" -version = "0.9.0" +version = "0.12.3" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "typer-0.9.0-py3-none-any.whl", hash = "sha256:5d96d986a21493606a358cae4461bd8cdf83cbf33a5aa950ae629ca3b51467ee"}, - {file = "typer-0.9.0.tar.gz", hash = "sha256:50922fd79aea2f4751a8e0408ff10d2662bd0c8bbfa84755a699f3bada2978b2"}, + {file = "typer-0.12.3-py3-none-any.whl", hash = "sha256:070d7ca53f785acbccba8e7d28b08dcd88f79f1fbda035ade0aecec71ca5c914"}, + {file = "typer-0.12.3.tar.gz", hash = "sha256:49e73131481d804288ef62598d97a1ceef3058905aa536a1134f90891ba35482"}, ] [package.dependencies] -click = ">=7.1.1,<9.0.0" +click = ">=8.0.0" +rich = ">=10.11.0" +shellingham = ">=1.3.0" typing-extensions = ">=3.7.4.3" -[package.extras] -all = ["colorama (>=0.4.3,<0.5.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"] -dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"] -doc = ["cairosvg (>=2.5.2,<3.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pillow (>=9.3.0,<10.0.0)"] -test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "pytest (>=4.4.0,<8.0.0)", "pytest-cov (>=2.10.0,<5.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<4.0.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"] - [[package]] name = "typing-extensions" -version = "4.9.0" +version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, - {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "win32-setctime" +version = "1.1.0" +description = "A small Python utility to set file creation time on Windows" +optional = false +python-versions = ">=3.5" +files = [ + {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, + {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, ] +[package.extras] +dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] + [[package]] name = "wmctrl" version = "0.5" @@ -684,7 +986,10 @@ attrs = "*" [package.extras] test = ["pytest"] +[extras] +all = ["rich"] + [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "c9959a799c6c78bd63f406ca8f65667053a18d6caf82f7ea5fed04df24d6646b" +content-hash = "602a1d20967b6caf236af15c59fa8974e9dc54ebeebacd96fe0b148f3a8baab1" diff --git a/pyproject.toml b/pyproject.toml index a6331a2..7085849 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,20 +8,76 @@ readme = "README.md" [tool.poetry.dependencies] python = ">=3.9,<3.13" +loguru = "^0.7.2" +pydantic = "^2.8.2" +rich = {version = "^13.7.1", optional = true} shell-interface = "^0.13.0" storage-device-managers = "^0.14.0" -typer = "^0.9.0" +typer = "^0.12.3" + +[tool.poetry.extras] +# In fact, `typer[all]` would be preferred, but Poetry does not support to have +# optional extras of dependencies. +all = ["rich"] [tool.poetry.group.dev.dependencies] -mypy = "^1.8.0" -ruff = "^0.2.0" -pytest = "^8.0.0" -pytest-cov = "^4.1.0" +black = "^24.4.2" depr = "^0.2.1" -pynvim = "^0.5.0" -black = "^24.1.1" +hypothesis = "^6.108.4" +mypy = "^1.11.0" pdbpp = "^0.10.3" +pynvim = "^0.5.0" +pytest = "^8.3.1" +pytest-cov = "^5.0.0" +pytest-xdist = "^3.6.1" +ruff = "^0.5.4" + +[tool.poetry.scripts] +snapshots = "root_subvol_snapshot.cli:cli" + + +[tool.mypy] +plugins = [ + "pydantic.mypy" +] +allow_any_unimported = false +warn_unreachable = true +enable_error_code = [ + "possibly-undefined" +] +strict = true +allow_incomplete_defs = true +allow_untyped_defs = true + +[[tool.mypy.overrides]] +module = "tests.*" +disable_error_code = [ + "possibly-undefined" +] + +[tool.pydantic-mypy] +init_forbid_extra = true +init_typed = true +warn_required_dynamic_aliases = true +warn_untyped_fields = true + +[tool.pytest.ini_options] +addopts = [ + "--cov", "src", + "--cov-branch", + "--cov-fail-under", "85" +] +testpaths = ["tests"] + +[tool.ruff] +src = [".", "src/"] + +[tool.ruff.lint] +select = ["A", "B", "C", "F", "I", "ISC", "PIE", "PL", "Q", "RUF", "SIM", "TID", "W", "YTT"] +ignore = ["E", "PLC1901", "SIM117"] +mccabe.max-complexity = 6 + [build-system] -requires = ["poetry-core"] +requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" diff --git a/src/root_subvol_snapshot/__init__.py b/src/root_subvol_snapshot/__init__.py index e69de29..3a53ea3 100644 --- a/src/root_subvol_snapshot/__init__.py +++ b/src/root_subvol_snapshot/__init__.py @@ -0,0 +1,3 @@ +from importlib import metadata + +__version__ = metadata.version(__name__) diff --git a/src/root_subvol_snapshot/backup_backends.py b/src/root_subvol_snapshot/backup_backends.py new file mode 100644 index 0000000..b5f7f25 --- /dev/null +++ b/src/root_subvol_snapshot/backup_backends.py @@ -0,0 +1,146 @@ +from __future__ import annotations + +import abc +import datetime as dt +from dataclasses import dataclass +from pathlib import Path +from typing import Optional, Union, overload + +import shell_interface as sh +from loguru import logger + +from . import config_parser as cp + + +class BackupBackend(abc.ABC): + @abc.abstractmethod + def do_backup(self, mount_dir: Path) -> None: ... + + @overload + @staticmethod + def from_config(config: cp.BtrFSRsyncConfig) -> BtrFSRsyncBackend: ... + + @overload + @staticmethod + def from_config(config: cp.ResticConfig) -> ResticBackend: ... + + @staticmethod + def from_config( + config: Union[cp.BtrFSRsyncConfig, cp.ResticConfig] + ) -> Union[BtrFSRsyncBackend, ResticBackend]: + # Getestet durch Tests der Backuplogik + if isinstance(config, cp.BtrFSRsyncConfig): + return BtrFSRsyncBackend(config=config) + return ResticBackend(config=config) + + +@dataclass(frozen=True) +class BtrFSRsyncBackend(BackupBackend): + config: cp.BtrFSRsyncConfig + + def do_backup(self, mount_dir: Path) -> None: + logger.info(f"Beginne mit BtrFS-Backup für Speichermedium {self.config.UUID}.") + backup_repository = mount_dir / self.config.BackupRepositoryFolder + src_snapshot = self.get_source_snapshot(backup_repository) + backup_root = self.snapshot( + src=src_snapshot, backup_repository=backup_repository + ) + for src, dest_name in self.config.Folders.items(): + dest = backup_root / dest_name + self.rsync_folder(src, dest, self.config.ExcludePatternsFile) + + files_dest = backup_root / self.config.FilesDest + for src in self.config.Files: + self.rsync_file(src, files_dest) + + @staticmethod + def get_source_snapshot(root: Path) -> Path: + return max(root.glob("202?-*")) + + def snapshot(self, *, src: Path, backup_repository: Path) -> Path: + timestamp = dt.datetime.now() + date_fmt = self.config.SubvolTimestampFmt + while True: + # In order to get rid of sleeps in the tests, snapshot name + # collisions must be handled gracefully. Incrementing the timestamp + # by a few seconds seems to be acceptable both in test and real + # world scenarios. + # There is still the possibility of race conditions, where another + # process would create the target folder before a snapshot can be + # created. This possibility is believed to be negligible, so + # nothing is done to prevent it. + backup_root = backup_repository / timestamp.strftime(date_fmt) + if backup_root.exists(): + timestamp += dt.timedelta(seconds=1) + else: + break + cmd: sh.StrPathList = [ + "sudo", + "btrfs", + "subvolume", + "snapshot", + src, + backup_root, + ] + sh.run_cmd(cmd=cmd) + return backup_root + + @staticmethod + def rsync_file(src: Path, dest: Path) -> None: + cmd: sh.StrPathList = ["sudo", "rsync", "-ax", "--inplace", src, dest] + sh.run_cmd(cmd=cmd) + + @staticmethod + def rsync_folder( + src: Path, dest: Path, maybe_exclude_patterns: Optional[Path] + ) -> None: + cmd: sh.StrPathList = [ + "sudo", + "rsync", + "-ax", + "--delete", + "--delete-excluded", + "--inplace", + ] + if maybe_exclude_patterns is not None: + cmd.extend(["--exclude-from", maybe_exclude_patterns]) + cmd.extend([f"{src}/", dest]) + sh.run_cmd(cmd=cmd) + + +@dataclass(frozen=True) +class ResticBackend(BackupBackend): + config: cp.ResticConfig + + def do_backup(self, mount_dir: Path) -> None: + logger.info(f"Beginne mit Restic-Backup für Speichermedium {self.config.UUID}.") + backup_repository = mount_dir / self.config.BackupRepositoryFolder + self.copy_files(backup_repository) + self.adapt_ownership(backup_repository) + + @staticmethod + def adapt_ownership(backup_repository: Path) -> None: + user = sh.get_user() + group = sh.get_group(user) + chown_cmd: sh.StrPathList = [ + "sudo", + "chown", + "-R", + f"{user}:{group}", + backup_repository, + ] + sh.run_cmd(cmd=chown_cmd) + + def copy_files(self, backup_repository: Path) -> None: + restic_cmd: sh.StrPathList = [ + "sudo", + "restic", + "backup", + "--one-file-system", + "--repo", + backup_repository, + ] + if self.config.ExcludePatternsFile is not None: + restic_cmd.extend(["--exclude-file", self.config.ExcludePatternsFile]) + restic_cmd.extend(list(self.config.FilesAndFolders)) + sh.pipe_pass_cmd_to_real_cmd(self.config.RepositoryPassCmd, restic_cmd) diff --git a/src/root_subvol_snapshot/cli.py b/src/root_subvol_snapshot/cli.py index 3f7edd1..7df7a93 100644 --- a/src/root_subvol_snapshot/cli.py +++ b/src/root_subvol_snapshot/cli.py @@ -1,39 +1,192 @@ -""" -Provide CLI - -This module provides the CLI to enter the program. -""" - +#!/usr/bin/env python3 +import enum +import json +import os import sys import typing as t from pathlib import Path +from tempfile import mkdtemp +from typing import Any, Callable, Optional +import storage_device_managers as sdm import typer +from loguru import logger + +from . import __version__ +from . import backup_backends as bb +from . import config_parser as cp +from .device_managers import ( + prepare_device_for_butterbackend, + prepare_device_for_resticbackend, +) app = typer.Typer() +DEFAULT_CONFIG_DIR = Path("~/.config/").expanduser() +DEFAULT_CONFIG_NAME = "butter-backup.cfg" -def main() -> None: - if len(sys.argv) == 1: - snapshot() - else: - app() +class ValidBackends(enum.Enum): + restic = "restic" + btrfs_rsync = "btrfs-rsync" + + +def get_default_config_path() -> str: + config_dir = Path(os.getenv("XDG_CONFIG_HOME", DEFAULT_CONFIG_DIR)) + config_file = config_dir / DEFAULT_CONFIG_NAME + return str(config_file) + + +def setup_logging(verbosity: int) -> None: + # If no `-v/--verbose` is given (verbosity == 0 in this case), errors and + # warnings shall appear. The first flag shall let successes appear, so that + # the user can trace the progress of the program. + logger.remove() + available_levels = [ + # "CRITICAL", + # "ERROR", + "WARNING", + "SUCCESS", + "INFO", + "DEBUG", + "TRACE", + ] + level = min(verbosity, len(available_levels) - 1) + logger.add(sys.stderr, level=available_levels[level]) + + +CONFIG_OPTION = typer.Option(get_default_config_path(), exists=True, dir_okay=False) +VERBOSITY_OPTION = typer.Option(0, "--verbose", "-v", count=True) +DEVICE_OPTION = t.Annotated[ + Path, typer.Option(..., exists=True, dir_okay=False, readable=True) +] @app.command() -def open(device: t.Annotated[t.Optional[Path], typer.Argument()] = None) -> None: - typer.echo(f"Opening {device}...") +def open(device: DEVICE_OPTION, verbose: int = VERBOSITY_OPTION) -> None: # noqa: A001 + """ + Open snapshot subvolume of a Btrfs device + + The command `open` mounts the subvolume `@snapshot` of a Btrfs device. If + no device is provided, the one that hosts `/` will be used. + """ + setup_logging(verbose) + mount_dir = Path(mkdtemp()) + sdm.mount_btrfs_device(device, mount_dir=mount_dir) + typer.echo(f"Device {device} was mounted in {mount_dir}.") @app.command() -def close(device: t.Annotated[t.Optional[Path], typer.Argument()] = None) -> None: - typer.echo(f"Closing {device}...") +def close(device: DEVICE_OPTION, verbose: int = VERBOSITY_OPTION) -> None: + """ + Unmounts a Btrfs device + + The command `close` unmounts the provided Btrfs device. If no device is + provided, the one that hosts `/` will be used. It is the counterpart of the + `open` command. Further explanations can be found there. + """ + setup_logging(verbose) + mounted_devices = sdm.get_mounted_devices() + mount_dirs = mounted_devices[str(device)] + if len(mount_dirs) != 1: + # TODO introduce custom exception + raise ValueError("Got several possible mount points. Expected exactly 1!") + mount_dir = mount_dirs.pop() + sdm.unmount_device(mount_dir) + mount_dir.rmdir() @app.command() -def snapshot(device: t.Annotated[t.Optional[Path], typer.Argument()] = None) -> None: - typer.echo("Making a snapshot of the root subvolume...") +def backup(config: Path = CONFIG_OPTION, verbose: int = VERBOSITY_OPTION) -> None: + """ + Führe Sicherheitskopien durch + + Für jedes angeschlossene Speichermedium wird eine Sicherheitskopie gemäß + der Konfiguration durchgeführt. Das Speichermedium wird hierfür + entschlüsselt und gemountet. Nachdem die Sicherungskopie durchgeführt + wurde, wird es wieder vollständig geschlossen. + + Die Sicherheitskopien werden sequentiell durchgeführt. Dadurch wird + sichergestellt, dass auch auf HDDs brauchbare Lesegeschwindigkeiten erzielt + werden können. + + Direkt nach Beendigung der Durchführung der Sicherheitskopien, d.h. nachdem + `butter-backup backup` zurückgekehrt ist oder nachdem die Durchführung der + Sicherheitskopien des nächsten Speichermediums begonnen wurde, kann das + entsprechende Speichermedium physisch entfernt werden. Eine Wartezeit oder + weitere manuelle Schritte sind nicht nötig. + """ + setup_logging(verbose) + configurations = cp.parse_configuration(config.read_text()) + for cfg in configurations: + if not cfg.device().exists(): + logger.info( + f"Speichermedium {cfg.UUID} existiert nicht. Es wird kein Backup angelegt." + ) + continue + backend = bb.BackupBackend.from_config(cfg) + with sdm.decrypted_device(cfg.device(), cfg.DevicePassCmd) as decrypted: + with sdm.mounted_device(decrypted, cfg.Compression) as mount_dir: + backend.do_backup(mount_dir) + + +@app.command() +def format_device( + backend: ValidBackends = typer.Argument(...), # noqa: B008 + device: Path = typer.Argument( # noqa: B008 + ..., exists=True, dir_okay=False, readable=False + ), + config_to: Optional[Path] = typer.Option( # noqa: B008 + None, + help="Datei, in welche die generierte Konfiguration geschrieben werden" + " soll. Die angegebene Datei darf nicht existieren. Wenn nicht" + " angegeben, wird die Konfiguration auf STDOUT ausgegeben.", + ), + verbose: int = VERBOSITY_OPTION, +) -> None: + """ + Richtet Speichermedium für butter-backup ein + + Das angegebene Speichermedium wird vollständig zur Erstellung von + Sicherheitskopien mit `butter-backup` vorbereitet. Es wird eine + Konfiguration ausgegeben, die nur noch um die zu sichernden Ordner bzw. + Dateien ergänzt werden muss. + + Die in der ausgegebenen Konfiguration enthaltenen Passwörter werden mittels + kryptographisch sicheren Methoden erstellt. + + Es wird dringend angeraten, die Passwörter nicht in der Konfiguration zu + belassen, sondern in einen Passwortmanager zu tun. Der Autor verwendet + `butter-backup` zusammen mit dem Passwortmanager `pass`. + """ + setup_logging(verbose) + config_writer: Callable[[str], Any] + if config_to is None: + config_writer = typer.echo + else: + if config_to.exists(): + raise ValueError( + "Zieldatei für ButterBackup-Konfiguration existiert schon!" + ) + config_writer = config_to.write_text + formatter = ( + prepare_device_for_butterbackend + if backend == ValidBackends.btrfs_rsync + else prepare_device_for_resticbackend + ) + config = formatter(device) + json_serialisable = json.loads(config.model_dump_json(exclude_none=True)) + config_writer(json.dumps([json_serialisable], indent=4, sort_keys=True)) + + +@app.command() +def version() -> None: + """Gibt butter-backups aktuelle Version an""" + typer.echo(__version__) + + +def cli() -> None: + app() if __name__ == "__main__": - main() + cli() diff --git a/src/root_subvol_snapshot/config_parser.py b/src/root_subvol_snapshot/config_parser.py new file mode 100644 index 0000000..d9fa542 --- /dev/null +++ b/src/root_subvol_snapshot/config_parser.py @@ -0,0 +1,147 @@ +from __future__ import annotations + +import json +import sys +import uuid +from collections import Counter +from pathlib import Path +from typing import ClassVar, Dict, List, Optional, Set, Union + +from pydantic import ( + BaseModel, + ConfigDict, + DirectoryPath, + FilePath, + TypeAdapter, + field_validator, + model_validator, +) +from storage_device_managers import ValidCompressions + +FoldersT = Dict[DirectoryPath, str] + + +def path_aware_btrfs_json_decoding(folders: FoldersT) -> str: + as_dict = {str(key): val for key, val in folders.items()} + return json.dumps(as_dict) + + +def path_aware_restic_json_decoding( + files_and_folders: Set[Union[FilePath, DirectoryPath]] +) -> str: + as_dict = {str(cur) for cur in files_and_folders} + return json.dumps(as_dict) + + +class BaseConfig(BaseModel): + BackupRepositoryFolder: str + DevicePassCmd: str + ExcludePatternsFile: Optional[FilePath] = None + UUID: uuid.UUID + Compression: Optional[ValidCompressions] = None + + +class BtrFSRsyncConfig(BaseConfig): + model_config = ConfigDict(extra="forbid", frozen=True) + Files: Set[FilePath] + FilesDest: str + Folders: FoldersT + SubvolTimestampFmt: ClassVar[str] = "%F_%H:%M:%S" + + @field_validator("Files") + def source_file_names_must_be_unique(cls, files): + file_names = Counter(f.name for f in files) + cls.raise_with_message_upon_duplicate(file_names, ("Dateinamen", "Dateinamen")) + return files + + @field_validator("Folders") + def folder_destinations_must_be_unique(cls, folders: FoldersT) -> FoldersT: + destinations = Counter(folders.values()) + cls.raise_with_message_upon_duplicate( + destinations, ("Zielverzeichnissen", "Ziele") + ) + return folders + + @field_validator("ExcludePatternsFile", mode="before") + def expand_tilde_in_exclude_patterns_file_name( + cls, maybe_exclude_patterns + ) -> str | None: + if maybe_exclude_patterns is None: + return None + return str(Path(maybe_exclude_patterns).expanduser()) + + @field_validator("Files", mode="before") + def expand_tilde_in_file_sources(cls, files) -> list[str]: + new = [str(Path(cur).expanduser()) for cur in files] + return new + + @field_validator("Folders", mode="before") + def expand_tilde_in_folder_sources(cls, folders) -> Dict[str, str]: + new = {str(Path(src).expanduser()): dest for src, dest in folders.items()} + return new + + @model_validator(mode="after") + def files_dest_is_no_folder_dest(self): + files_dest = self.FilesDest + destinations = self.Folders.values() + if files_dest in destinations: + raise ValueError( + f"Zielverzeichnis {files_dest} ist gleichzeitig Ziel für Ordner und Einzeldateien." + ) + return self + + @staticmethod + def raise_with_message_upon_duplicate( + counts: Union[Counter[Path], Counter[str]], token: tuple[str, str] + ) -> None: + if all(val == 1 for val in counts.values()): + return + errmsg_begin = ( + f"Duplikate in {token[0]} entdeckt. Folgende {token[1]} kommen doppelt vor:" + ) + errmsg_body = " ".join( + str(elem) for (elem, count) in counts.items() if count > 1 + ) + raise ValueError(f"{errmsg_begin} {errmsg_body}") + + def device(self) -> Path: + return Path(f"/dev/disk/by-uuid/{self.UUID}") + + def map_name(self) -> str: + return str(self.UUID) + + +class ResticConfig(BaseConfig): + model_config = ConfigDict(extra="forbid", frozen=True) + FilesAndFolders: Set[Union[FilePath, DirectoryPath]] + RepositoryPassCmd: str + + @field_validator("ExcludePatternsFile", mode="before") + def expand_tilde_in_exclude_patterns_file_name( + cls, maybe_exclude_patterns + ) -> Optional[str]: + if maybe_exclude_patterns is None: + return None + return str(Path(maybe_exclude_patterns).expanduser()) + + @field_validator("FilesAndFolders", mode="before") + def expand_tilde_in_sources(cls, files_and_folders) -> set[str]: + new = {str(Path(src).expanduser()) for src in files_and_folders} + return new + + def device(self) -> Path: + return Path(f"/dev/disk/by-uuid/{self.UUID}") + + def map_name(self) -> str: + return str(self.UUID) + + +Configuration = Union[BtrFSRsyncConfig, ResticConfig] + + +def parse_configuration(content: str) -> list[Configuration]: + ConfigList = TypeAdapter(List[Configuration]) + config_lst = ConfigList.validate_json(content) + if len(config_lst) == 0: + sys.exit("Leere Konfigurationsdateien sind nicht erlaubt.\n") + return config_lst diff --git a/src/root_subvol_snapshot/device_managers.py b/src/root_subvol_snapshot/device_managers.py new file mode 100644 index 0000000..0d1edbd --- /dev/null +++ b/src/root_subvol_snapshot/device_managers.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +from datetime import date +from pathlib import Path + +import shell_interface as sh +import storage_device_managers as sdm + +from . import config_parser as cp + + +def prepare_device_for_butterbackend(device: Path) -> cp.BtrFSRsyncConfig: + password_cmd = sdm.generate_passcmd() + backup_repository_folder = "ButterBackupRepository" + volume_uuid = sdm.encrypt_device(device, password_cmd) + compression = sdm.ValidCompressions.ZSTD + user = sh.get_user() + group = sh.get_group(user) + with sdm.decrypted_device(device, password_cmd) as decrypted: + sdm.mkfs_btrfs(decrypted) + with sdm.mounted_device(decrypted) as mounted: + backup_repository = mounted / backup_repository_folder + mkdir_cmd: sh.StrPathList = ["sudo", "mkdir", backup_repository] + sh.run_cmd(cmd=mkdir_cmd) + + initial_subvol = backup_repository / date.today().strftime( + cp.BtrFSRsyncConfig.SubvolTimestampFmt + ) + subvol_cmd: sh.StrPathList = [ + "sudo", + "btrfs", + "subvolume", + "create", + initial_subvol, + ] + sh.run_cmd(cmd=subvol_cmd) + sdm.chown(mounted, user, group, recursive=True) + + config = cp.BtrFSRsyncConfig( + BackupRepositoryFolder=backup_repository_folder, + Compression=compression, + DevicePassCmd=password_cmd, + Files=set(), + FilesDest="Einzeldateien", + Folders={}, + UUID=volume_uuid, + ) + return config + + +def prepare_device_for_resticbackend(device: Path) -> cp.ResticConfig: + device_passcmd = sdm.generate_passcmd() + repository_passcmd = sdm.generate_passcmd() + backup_repository_folder = "ResticBackupRepository" + compression = None # Restic encrypts and encrypted data are incompressible + volume_uuid = sdm.encrypt_device(device, device_passcmd) + user = sh.get_user() + group = sh.get_group(user) + with sdm.decrypted_device(device, device_passcmd) as decrypted: + sdm.mkfs_btrfs(decrypted) + with sdm.mounted_device(decrypted) as mounted: + backup_repo = mounted / backup_repository_folder + mkdir_repo: sh.StrPathList = ["sudo", "mkdir", backup_repo] + restic_init: sh.StrPathList = [ + "sudo", + "restic", + "init", + "-r", + backup_repo, + ] + sh.run_cmd(cmd=mkdir_repo) + sh.pipe_pass_cmd_to_real_cmd(repository_passcmd, restic_init) + sdm.chown(mounted, user, group, recursive=True) + config = cp.ResticConfig( + BackupRepositoryFolder=backup_repository_folder, + Compression=compression, + DevicePassCmd=device_passcmd, + FilesAndFolders=set(), + RepositoryPassCmd=repository_passcmd, + UUID=volume_uuid, + ) + return config diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/config_parser/test_btrfs_config.py b/tests/config_parser/test_btrfs_config.py new file mode 100644 index 0000000..172418c --- /dev/null +++ b/tests/config_parser/test_btrfs_config.py @@ -0,0 +1,195 @@ +from __future__ import annotations + +import re +from pathlib import Path +from tempfile import NamedTemporaryFile, TemporaryDirectory +from typing import Optional + +import pytest +from hypothesis import assume, given +from hypothesis import strategies as st +from pydantic import ValidationError +from storage_device_managers import ValidCompressions + +from root_subvol_snapshot import config_parser as cp +from tests import hypothesis_utils as hu + +TEST_RESOURCES = Path(__file__).parent.parent / "resources" +EXCLUDE_FILE = TEST_RESOURCES / "exclude-file" + + +@st.composite +def valid_unparsed_empty_btrfs_config(draw): + config = draw( + st.fixed_dictionaries( + { + "BackupRepositoryFolder": st.text(), + "Compression": st.sampled_from( + [cur.value for cur in ValidCompressions] + ), + "ExcludePatternsFile": st.just(str(EXCLUDE_FILE)) | st.none(), + "DevicePassCmd": st.text(), + "Files": st.just([]), + "FilesDest": st.text(), + "Folders": st.just({}), + "UUID": st.uuids().map(str), + } + ) + ) + return config + + +@given(base_config=valid_unparsed_empty_btrfs_config(), dest_dir=hu.filenames()) +def test_btrfs_config_rejects_file_dest_collision(base_config, dest_dir: str): + base_config["Folders"] = { + "/usr/bin": "backup_bins", + "/etc": dest_dir, + "/var/log": "backup_logs", + } + base_config["FilesDest"] = dest_dir + with NamedTemporaryFile() as src: + base_config["Files"] = [src.name] + with pytest.raises(ValidationError, match=re.escape(dest_dir)): + cp.BtrFSRsyncConfig.model_validate(base_config) + + +@given(base_config=valid_unparsed_empty_btrfs_config(), file_name=hu.filenames()) +def test_btrfs_config_rejects_filename_collision(base_config, file_name): + base_config["Folders"] = {} + with TemporaryDirectory() as td1: + with TemporaryDirectory() as td2: + dirs = [td1, td2] + files = [Path(cur_dir) / file_name for cur_dir in dirs] + for f in files: + f.touch() + base_config["Files"] = [str(f) for f in files] + with pytest.raises(ValidationError, match=re.escape(file_name)): + cp.BtrFSRsyncConfig.model_validate(base_config) + + +@given(base_config=valid_unparsed_empty_btrfs_config()) +def test_btrfs_config_expands_user(base_config): + with TemporaryDirectory() as dest: + pass + folders = { + "/usr/bin": "backup_bins", + "~": dest, + "/var/log": "backup_logs", + } + base_config["Folders"] = folders + with NamedTemporaryFile(dir=Path.home()) as src_file: + fname = f"~/{Path(src_file.name).name}" + base_config["Files"] = ["/bin/bash", fname] + with NamedTemporaryFile(dir=Path.home()) as exclude_file: + exclude_file_relative = f"~/{Path(exclude_file.name).name}" + base_config["ExcludePatternsFile"] = exclude_file_relative + cfg = cp.BtrFSRsyncConfig.model_validate(base_config) + assert Path("~").expanduser() in cfg.Folders + assert Path(src_file.name).expanduser() in cfg.Files + assert cfg.ExcludePatternsFile == Path(exclude_file.name).expanduser() + + +@given( + base_config=valid_unparsed_empty_btrfs_config(), + folder_dest=hu.filenames(), +) +def test_btrfs_config_rejects_duplicate_dest(base_config, folder_dest: str): + with TemporaryDirectory() as src1: + with TemporaryDirectory() as src2: + folders = { + "/usr/bin": "backup_bins", + src1: folder_dest, + "/var/log": "backup_logs", + src2: folder_dest, + } + base_config["Folders"] = folders + base_config["Files"] = [] + with pytest.raises(ValidationError, match=re.escape(folder_dest)): + cp.BtrFSRsyncConfig.model_validate(base_config) + + +@given(base_config=valid_unparsed_empty_btrfs_config()) +def test_btrfs_config_uuid_is_mapname(base_config) -> None: + cfg = cp.BtrFSRsyncConfig.model_validate(base_config) + assert base_config["UUID"] == cfg.map_name() + + +@given(base_config=valid_unparsed_empty_btrfs_config()) +def test_btrfs_config_device_ends_in_uuid(base_config) -> None: + cfg = cp.BtrFSRsyncConfig.model_validate(base_config) + uuid = base_config["UUID"] + assert cfg.device() == Path(f"/dev/disk/by-uuid/{uuid}") + + +@given( + base_config=valid_unparsed_empty_btrfs_config(), + compression=st.sampled_from(["zsdd", "zlbi", "xkcd", "invalid-string", ""]), +) +def test_btrfs_config_rejects_invalid_compression( + base_config, compression: str +) -> None: + base_config["Compression"] = compression + with pytest.raises(ValidationError): + cp.BtrFSRsyncConfig.model_validate(base_config) + + +@given( + base_config=valid_unparsed_empty_btrfs_config(), + level=st.integers(max_value=0) | st.integers(min_value=16), + algorithm=st.sampled_from(["lzo", "zstd", "zlib"]), +) +def test_btrfs_config_rejects_out_of_bounds_compression_level( + base_config, level: int, algorithm: str +) -> None: + base_config["Compression"] = f"{algorithm}:{level}" + with pytest.raises(ValidationError): + cp.BtrFSRsyncConfig.model_validate(base_config) + + +@given( + base_config=valid_unparsed_empty_btrfs_config(), + level=st.integers(min_value=1, max_value=9) | st.none(), +) +def test_btrfs_config_accepts_valid_zlib(base_config, level: Optional[int]) -> None: + compression = "zlib" + if level is not None: + compression += f":{level}" + base_config["Compression"] = compression + cfg = cp.BtrFSRsyncConfig.model_validate(base_config) + assert cfg.Compression == ValidCompressions(compression) + + +@given( + base_config=valid_unparsed_empty_btrfs_config(), + level=st.integers(min_value=1, max_value=15) | st.none(), +) +def test_btrfs_config_accepts_valid_zstd(base_config, level: Optional[int]) -> None: + compression = "zstd" + if level is not None: + compression += f":{level}" + base_config["Compression"] = compression + cfg = cp.BtrFSRsyncConfig.model_validate(base_config) + assert cfg.Compression == ValidCompressions(compression) + + +@given( + base_config=valid_unparsed_empty_btrfs_config(), +) +def test_btrfs_config_accepts_valid_lzo(base_config) -> None: + compression = "lzo" + base_config["Compression"] = compression + cfg = cp.BtrFSRsyncConfig.model_validate(base_config) + assert cfg.Compression == ValidCompressions.LZO + + +@given(base_config=valid_unparsed_empty_btrfs_config(), folder_dest=hu.filenames()) +def test_btrfs_config_json_roundtrip(base_config, folder_dest: str): + assume(folder_dest != base_config["FilesDest"]) + with TemporaryDirectory() as src_folder: + with NamedTemporaryFile() as src_file: + base_config["Folders"] = {src_folder: folder_dest} + base_config["Files"] = [src_file.name] + cfg = cp.BtrFSRsyncConfig.model_validate(base_config) + as_json = cfg.model_dump_json() + deserialised = cp.BtrFSRsyncConfig.model_validate_json(as_json) + assert cfg == deserialised diff --git a/tests/config_parser/test_parse_configuration.py b/tests/config_parser/test_parse_configuration.py new file mode 100644 index 0000000..426bd1c --- /dev/null +++ b/tests/config_parser/test_parse_configuration.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +import json +from pathlib import Path +from tempfile import TemporaryDirectory +from uuid import UUID + +import pytest +from hypothesis import given +from hypothesis import strategies as st +from pydantic import ValidationError + +from root_subvol_snapshot import config_parser as cp + +SUCCESS_CODES = {0, None} + + +def test_parse_configuration_rejects_empty_list() -> None: + with pytest.raises(SystemExit) as sysexit: + cp.parse_configuration("[]") + assert sysexit.value.code not in SUCCESS_CODES + + +@given( + non_list=st.one_of( + st.dictionaries(st.text(), st.text(), min_size=1), st.text(min_size=1) + ) +) +def test_parse_configuration_warns_on_non_lists(non_list) -> None: + with pytest.raises(ValidationError): + cp.parse_configuration(json.dumps(non_list)) + + +def test_parse_configuration_warns_on_non_dict_item() -> None: + with pytest.raises(ValidationError): + cp.parse_configuration(json.dumps([{}, 1337])) + + +@given( + backup_dest_dirs=st.lists(st.text(), min_size=2, max_size=2, unique=True), + backup_repository_folder=st.text(), + pass_cmd=st.text(), + uuid=st.uuids(), +) +def test_parse_configuration_parses_btrfs_config( + backup_dest_dirs: list[str], + backup_repository_folder: str, + pass_cmd: str, + uuid: UUID, +) -> None: + with TemporaryDirectory() as source: + btrfs_cfg = cp.BtrFSRsyncConfig( + BackupRepositoryFolder=backup_repository_folder, + DevicePassCmd=pass_cmd, + Files=set(), + FilesDest=backup_dest_dirs[1], + Folders={Path(source): backup_dest_dirs[0]}, + UUID=uuid, + ) + cfg_lst = cp.parse_configuration(f"[{btrfs_cfg.model_dump_json()}]") + assert cfg_lst == [btrfs_cfg] + + +@given( + backup_dest_dirs=st.lists(st.text(), min_size=2, max_size=2, unique=True), + backup_repository_folder=st.text(), + device_pass_cmd=st.text(), + repository_pass_cmd=st.text(), + uuid=st.uuids(), +) +def test_load_configuration_parses_restic_config( + backup_dest_dirs: list[str], + backup_repository_folder: str, + device_pass_cmd: str, + repository_pass_cmd: str, + uuid: UUID, +) -> None: + with TemporaryDirectory() as source: + restic_cfg = cp.ResticConfig( + BackupRepositoryFolder=backup_repository_folder, + DevicePassCmd=device_pass_cmd, + FilesAndFolders={Path(source)}, + RepositoryPassCmd=repository_pass_cmd, + UUID=uuid, + ) + cfg_lst = cp.parse_configuration(f"[{restic_cfg.model_dump_json()}]") + assert cfg_lst == [restic_cfg] diff --git a/tests/config_parser/test_restic_config.py b/tests/config_parser/test_restic_config.py new file mode 100644 index 0000000..eff7d45 --- /dev/null +++ b/tests/config_parser/test_restic_config.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +import json +from pathlib import Path +from tempfile import NamedTemporaryFile, TemporaryDirectory + +import pytest +import storage_device_managers as sdm +from hypothesis import given +from hypothesis import strategies as st +from pydantic import ValidationError + +from root_subvol_snapshot import config_parser as cp + +TEST_RESOURCES = Path(__file__).parent.parent / "resources" +EXCLUDE_FILE = TEST_RESOURCES / "exclude-file" + + +def get_random_filename() -> str: + with NamedTemporaryFile() as named_file: + return named_file.name + + +@st.composite +def valid_unparsed_empty_restic_config(draw): + config = draw( + st.builds( + cp.ResticConfig, + BackupRepositoryFolder=st.text(), + Compression=st.sampled_from([cur.value for cur in sdm.ValidCompressions]), + ExcludePatternsFile=st.just(str(EXCLUDE_FILE)) | st.none(), + DevicePassCmd=st.text(), + FilesAndFolders=st.just([]), + RepositoryPassCmd=st.text(), + UUID=st.uuids(), + ) + ) + return json.loads(config.model_dump_json()) + + +@given(base_config=valid_unparsed_empty_restic_config()) +def test_restic_config_rejects_missing_source(base_config): + fname = get_random_filename() + base_config["FilesAndFolders"] = {fname} + with pytest.raises(ValidationError): + cp.ResticConfig.model_validate(base_config) + + +@given(base_config=valid_unparsed_empty_restic_config()) +def test_restic_config_expands_user(base_config): + with NamedTemporaryFile(dir=Path.home()) as src_file: + fname = f"~/{Path(src_file.name).name}" + base_config["FilesAndFolders"] = {"~", fname} + with NamedTemporaryFile(dir=Path.home()) as exclude_file: + exclude_file_relative = f"~/{Path(exclude_file.name).name}" + base_config["ExcludePatternsFile"] = exclude_file_relative + cfg = cp.ResticConfig.model_validate(base_config) + expected = {Path("~").expanduser(), Path(src_file.name).expanduser()} + assert cfg.FilesAndFolders == expected + assert cfg.ExcludePatternsFile == Path(exclude_file.name).expanduser() + + +@given(base_config=valid_unparsed_empty_restic_config()) +def test_restic_config_uuid_is_mapname(base_config) -> None: + cfg = cp.ResticConfig.model_validate(base_config) + assert base_config["UUID"] == cfg.map_name() + + +@given(base_config=valid_unparsed_empty_restic_config()) +def test_restic_config_device_ends_in_uuid(base_config) -> None: + cfg = cp.ResticConfig.model_validate(base_config) + uuid = base_config["UUID"] + assert cfg.device() == Path(f"/dev/disk/by-uuid/{uuid}") + + +@given(base_config=valid_unparsed_empty_restic_config()) +def test_restic_config_json_roundtrip(base_config): + with TemporaryDirectory() as src_folder: + with NamedTemporaryFile() as src_file: + base_config["FilesAndFolders"] = {src_folder, src_file.name} + cfg = cp.ResticConfig.model_validate(base_config) + as_json = cfg.model_dump_json() + deserialised = cp.ResticConfig.model_validate_json(as_json) + assert cfg == deserialised diff --git a/tests/conftest.py b/tests/conftest.py index e69de29..d19b66b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -0,0 +1,117 @@ +from __future__ import annotations + +import shutil +import uuid +from pathlib import Path +from tempfile import NamedTemporaryFile + +import pytest +import storage_device_managers as sdm + +from root_subvol_snapshot import device_managers as dm + + +@pytest.fixture(scope="session") +def _big_file_persistent(): + """ + Prepare a file of minimum size for BtrFS and return its path + """ + + min_size = 128 * 1024**2 # ~109MiB is the minimum size for BtrFS + with NamedTemporaryFile() as ntf: + file = Path(ntf.name) + file.write_bytes(bytes(min_size)) + yield file + + +@pytest.fixture +def big_file(_big_file_persistent): + """ + Prepare a file of minimum size for BtrFS and return its path + """ + with NamedTemporaryFile() as ntf: + big_file = Path(ntf.name) + shutil.copy(_big_file_persistent, big_file) + yield big_file + + +@pytest.fixture(scope="session") +def _encrypted_btrfs_device_persistent( + _big_file_persistent, +): + with NamedTemporaryFile() as ntf: + big_file = Path(ntf.name) + shutil.copy(_big_file_persistent, big_file) + config = dm.prepare_device_for_butterbackend(big_file) + yield big_file, config + + +@pytest.fixture +def encrypted_btrfs_device(_encrypted_btrfs_device_persistent): + """ + Prepare device for ButterBackup and return its config + + Returns + ------- + config: BtrfsConfig + configuration allowing to interact with the returned device + """ + + # This implementation resets the UUID of the configuration. This causes the + # partition's UUID and the configuration's UUID to differ. Since the only + # relevance of the partition's UUID is to allow Linux to create the device + # node in /dev/disk/by-uuid, this is not a problem. + old_fs_file, old_config = _encrypted_btrfs_device_persistent + with NamedTemporaryFile() as ntf: + big_file = Path(ntf.name) + shutil.copy(old_fs_file, big_file) + config = old_config.model_copy(update={"UUID": uuid.uuid4()}) + with sdm.symbolic_link(big_file, config.device()): + yield config + + +@pytest.fixture(scope="session") +def _encrypted_restic_device_persistent(_big_file_persistent): + with NamedTemporaryFile() as ntf: + big_file = Path(ntf.name) + shutil.copy(_big_file_persistent, big_file) + config = dm.prepare_device_for_resticbackend(big_file) + yield big_file, config + + +@pytest.fixture +def encrypted_restic_device(_encrypted_restic_device_persistent): + """ + Prepare device for Restic on BtrFS and return its config + + Returns + ------- + config: ResticConfig + configuration allowing to interact with the returned device + """ + + # This implementation resets the UUID of the configuration. This causes the + # partition's UUID and the configuration's UUID to differ. Since the only + # relevance of the partition's UUID is to allow Linux to create the device + # node in /dev/disk/by-uuid, this is not a problem. + old_fs_file, old_config = _encrypted_restic_device_persistent + with NamedTemporaryFile() as ntf: + big_file = Path(ntf.name) + shutil.copy(old_fs_file, big_file) + config = old_config.model_copy(update={"UUID": uuid.uuid4()}) + with sdm.symbolic_link(big_file, config.device()): + yield config + + +@pytest.fixture(params=["encrypted_btrfs_device", "encrypted_restic_device"]) +def encrypted_device(request): + config = request.getfixturevalue(request.param) + return config + + +@pytest.fixture +def mounted_device(encrypted_device): + config = encrypted_device + with sdm.decrypted_device(config.device(), config.DevicePassCmd) as decrypted: + with sdm.mounted_device(decrypted, config.Compression) as mounted_device: + yield config, mounted_device diff --git a/tests/hypothesis_utils.py b/tests/hypothesis_utils.py new file mode 100644 index 0000000..f75c906 --- /dev/null +++ b/tests/hypothesis_utils.py @@ -0,0 +1,15 @@ +from hypothesis import strategies as st + + +@st.composite +def filenames(draw, min_size=1) -> str: + alpha = "abcdefghijklmnopqrstuvwxyzäöu" + num = "01234567890" + special = "_-.,() " + permitted_chars = f"{alpha}{alpha.upper()}{num}{special}" + fname: str = draw( + st.text(permitted_chars, min_size=min_size).filter( + lambda fname: fname not in {".", ".."} + ) + ) + return fname diff --git a/tests/resources/exclude-file b/tests/resources/exclude-file new file mode 100644 index 0000000..d42e7ab --- /dev/null +++ b/tests/resources/exclude-file @@ -0,0 +1 @@ +*ignore* diff --git a/tests/resources/first-backup/file-to-ignore b/tests/resources/first-backup/file-to-ignore new file mode 100644 index 0000000..e69de29 diff --git a/tests/resources/first-backup/only-in-first-folder b/tests/resources/first-backup/only-in-first-folder new file mode 100644 index 0000000..85f6f75 --- /dev/null +++ b/tests/resources/first-backup/only-in-first-folder @@ -0,0 +1,5 @@ +Der Inhalt dieser Datei existiert nur, um den Dateiinhalt einzigartig zu +machen. Wäre die Datei 0 Byte groß, müsste in einigen Tests eine Kollision mit +anderen leeren Dateien gesondert behandelt werden. + +8bc319c8-da39-4203-9161-a915f5d56ae4 diff --git a/tests/resources/first-backup/randomfile.bin b/tests/resources/first-backup/randomfile.bin new file mode 100644 index 0000000..0fedf1e Binary files /dev/null and b/tests/resources/first-backup/randomfile.bin differ diff --git a/tests/resources/first-backup/textfile.md b/tests/resources/first-backup/textfile.md new file mode 100644 index 0000000..0e026f5 --- /dev/null +++ b/tests/resources/first-backup/textfile.md @@ -0,0 +1,2 @@ +# Kapitel +Hallo Welt. Dies ist eine Textdatei. diff --git a/tests/resources/second-backup/file-to-ignore b/tests/resources/second-backup/file-to-ignore new file mode 100644 index 0000000..e69de29 diff --git a/tests/resources/second-backup/only-in-second-folder b/tests/resources/second-backup/only-in-second-folder new file mode 100644 index 0000000..97b0e48 --- /dev/null +++ b/tests/resources/second-backup/only-in-second-folder @@ -0,0 +1,5 @@ +Der Inhalt dieser Datei existiert nur, um den Dateiinhalt einzigartig zu +machen. Wäre die Datei 0 Byte groß, müsste in einigen Tests eine Kollision mit +anderen leeren Dateien gesondert behandelt werden. + +25615ed4-5ba2-414c-a1e6-ff0bb965a592 diff --git a/tests/resources/second-backup/randomfile.bin b/tests/resources/second-backup/randomfile.bin new file mode 100644 index 0000000..cb70c0c Binary files /dev/null and b/tests/resources/second-backup/randomfile.bin differ diff --git a/tests/resources/second-backup/textfile.md b/tests/resources/second-backup/textfile.md new file mode 100644 index 0000000..0e88fff --- /dev/null +++ b/tests/resources/second-backup/textfile.md @@ -0,0 +1,7 @@ +# Kapitel +Hallo Welt. Dies ist eine Textdatei. + + +## Unterkapitel + +Hier steht nur Nonsense drin. diff --git a/tests/test_backup_backends.py b/tests/test_backup_backends.py new file mode 100644 index 0000000..ff377cb --- /dev/null +++ b/tests/test_backup_backends.py @@ -0,0 +1,257 @@ +from __future__ import annotations + +import datetime as dt +import os +from collections import Counter +from pathlib import Path +from tempfile import TemporaryDirectory +from typing import Dict, Iterable, Union, overload + +import pytest +import shell_interface as sh + +from root_subvol_snapshot import backup_backends as bb +from root_subvol_snapshot import config_parser as cp + +TEST_RESOURCES = Path(__file__).parent / "resources" +EXCLUDE_FILE = TEST_RESOURCES / "exclude-file" +FIRST_BACKUP = TEST_RESOURCES / "first-backup" +SECOND_BACKUP = TEST_RESOURCES / "second-backup" +USER = os.environ.get("USER", "root") + + +def list_files_recursively(path: Path) -> Iterable[Path]: + for file_or_folder in path.rglob("*"): + if file_or_folder.is_file(): + yield file_or_folder + + +@overload +def complement_configuration( + config: cp.BtrFSRsyncConfig, source_dir: Path +) -> cp.BtrFSRsyncConfig: ... + + +@overload +def complement_configuration( + config: cp.ResticConfig, source_dir: Path +) -> cp.ResticConfig: ... + + +def complement_configuration( + config: cp.Configuration, source_dir: Path +) -> cp.Configuration: + if isinstance(config, cp.BtrFSRsyncConfig): + folder_dest_dir = "some-folder-name" + return config.model_copy(update={"Folders": {source_dir: folder_dest_dir}}) + if isinstance(config, cp.ResticConfig): + return config.model_copy(update={"FilesAndFolders": {source_dir}}) + raise TypeError("Unsupported configuration encountered.") + + +@overload +def get_expected_content( + config: cp.BtrFSRsyncConfig, exclude_to_ignore_file: bool +) -> Dict[Path, bytes]: ... + + +@overload +def get_expected_content( + config: cp.ResticConfig, exclude_to_ignore_file: bool +) -> Counter[bytes]: ... + + +def get_expected_content( + config: cp.Configuration, + exclude_to_ignore_file: bool, +) -> Union[Counter[bytes], Dict[Path, bytes]]: + source_dir: Path + if isinstance(config, cp.BtrFSRsyncConfig): + source_dir = next(iter(config.Folders)) + elif isinstance(config, cp.ResticConfig): + source_dir = next(iter(config.FilesAndFolders)) + else: + raise TypeError("Unsupported configuration encountered.") + expected_content = { + file.relative_to(source_dir): file.read_bytes() + for file in list_files_recursively(source_dir) + if exclude_to_ignore_file is False or "ignore" not in file.name + } + if isinstance(config, cp.ResticConfig): + return Counter(expected_content.values()) + return expected_content + + +@overload +def get_result_content( + config: cp.BtrFSRsyncConfig, mounted: Path +) -> Dict[Path, bytes]: ... + + +@overload +def get_result_content(config: cp.ResticConfig, mounted: Path) -> Counter[bytes]: ... + + +def get_result_content( + config: cp.Configuration, mounted: Path +) -> Union[Counter[bytes], Dict[Path, bytes]]: + if isinstance(config, cp.BtrFSRsyncConfig): + return get_result_content_for_btrfs(config, mounted) + elif isinstance(config, cp.ResticConfig): + return get_result_content_for_restic(config, mounted) + else: + raise TypeError("Unsupported configuration encountered.") + + +def get_result_content_for_btrfs( + config: cp.BtrFSRsyncConfig, mounted: Path +) -> Dict[Path, bytes]: + folder_dest_dir = next(iter(config.Folders.values())) + backup_repository = mounted / config.BackupRepositoryFolder + latest_folder = sorted(backup_repository.iterdir())[-1] + return { + file.relative_to(latest_folder / folder_dest_dir): file.read_bytes() + for file in list_files_recursively(latest_folder) + } + + +def get_result_content_for_restic( + config: cp.ResticConfig, mounted: Path +) -> Counter[bytes]: + with TemporaryDirectory() as restore_dir: + restore_cmd: sh.StrPathList = [ + "restic", + "-r", + mounted / config.BackupRepositoryFolder, + "restore", + "latest", + "--target", + restore_dir, + ] + sh.pipe_pass_cmd_to_real_cmd(config.RepositoryPassCmd, restore_cmd) + return Counter( + file.read_bytes() for file in list_files_recursively(Path(restore_dir)) + ) + + +@pytest.mark.parametrize( + "source_directories", + [[FIRST_BACKUP], [SECOND_BACKUP], [FIRST_BACKUP, SECOND_BACKUP]], +) +def test_do_backup(source_directories, mounted_device) -> None: + empty_config, device = mounted_device + for source_dir in source_directories: + config = complement_configuration(empty_config, source_dir) + backend = bb.BackupBackend.from_config(config) + backend.do_backup(device) + result_content = get_result_content(config, device) + expected_content = get_expected_content(config, exclude_to_ignore_file=False) + assert result_content == expected_content + + +@pytest.mark.parametrize( + "source_directories", + [[FIRST_BACKUP], [SECOND_BACKUP], [FIRST_BACKUP, SECOND_BACKUP]], +) +def test_do_backup_handles_exclude_list(source_directories, mounted_device) -> None: + empty_config, device = mounted_device + for source_dir in source_directories: + config = complement_configuration(empty_config, source_dir).model_copy( + update={"ExcludePatternsFile": EXCLUDE_FILE} + ) + backend = bb.BackupBackend.from_config(config) + backend.do_backup(device) + result_content = get_result_content(config, device) + expected_content = get_expected_content(config, exclude_to_ignore_file=True) + assert result_content == expected_content + + +@pytest.mark.parametrize( + "first_source, second_source", + [(FIRST_BACKUP, SECOND_BACKUP)], +) +def test_do_backup_removes_existing_files_in_exclude_list( + first_source, second_source, mounted_device +) -> None: + # This test ensures that files are removed even if they are matched by the + # exclude patterns. + # Imagine that a user has existing backups. Then she creates an + # ExcludePatternsFile or adds a rule to it. Anyhow, imagine that now the + # ExcludePatternsFile contains a rule that matches files that already exist + # in the existing backups. + # A prior version of BtrFSRsyncBackend would not delete files that would + # match a rule in the ExcludePatternsFile. This lead to plenty of error + # messages when rsync then attempted to remove the folder where the + # not-deleted file was contained, because that folder was not empty. + # However, if the folder is gone in the source, it must be removed in the + # backup too. + # This test explicitly tests this scenario. + + empty_config, device = mounted_device + + first_config = complement_configuration(empty_config, first_source) + first_backend = bb.BackupBackend.from_config(first_config) + first_backend.do_backup(device) + + second_config = complement_configuration(empty_config, second_source).model_copy( + update={"ExcludePatternsFile": EXCLUDE_FILE} + ) + second_backend = bb.BackupBackend.from_config(second_config) + second_backend.do_backup(device) + + result_content = get_result_content(second_config, device) + expected_content = get_expected_content(second_config, exclude_to_ignore_file=True) + assert result_content == expected_content + + +def test_do_backup_for_btrfs_creates_snapshots_with_timestamp_names( + mounted_device, +) -> None: + empty_config, device = mounted_device + if not isinstance(empty_config, cp.BtrFSRsyncConfig): + # This test works for BtrfsConfig only. However, encrypted_device on + # which mounted_device depends on, is parameterised over all backends. + # Since this simplifies many other tests it seemed to be an acceptable + # tradeoff to short-circuit the test here. + return + folder_dest_dir = "some-folder-name" + config = empty_config.model_copy( + update={"Folders": {FIRST_BACKUP: folder_dest_dir}} + ) + backend = bb.BtrFSRsyncBackend(config) + backend.do_backup(device) + backup_repository = device / config.BackupRepositoryFolder + latest_folder = sorted(backup_repository.iterdir())[-1] + expected_date = dt.date.today().isoformat() + assert expected_date in str(latest_folder) + + +@pytest.mark.parametrize( + "source_directories", + [[FIRST_BACKUP], [SECOND_BACKUP], [FIRST_BACKUP, SECOND_BACKUP]], +) +def test_do_backup_for_restic_adapts_ownership( + source_directories, mounted_device +) -> None: + empty_config, device = mounted_device + if not isinstance(empty_config, cp.ResticConfig): + # This test works for ResticConfig only. However, encrypted_device on + # which mounted_device depends on, is parameterised over all backends. + # Since this simplifies many other tests it seemed to be an acceptable + # tradeoff to short-circuit the test here. + return + for source_dir in source_directories: + config = complement_configuration(empty_config, source_dir) + backend = bb.BackupBackend.from_config(config) + backend.do_backup(device) + + expected_user = sh.get_user() + expected_group = sh.get_group(expected_user) + found_user = { + cur_f.owner() for cur_f in (device / config.BackupRepositoryFolder).rglob("*") + } + found_group = { + cur_f.group() for cur_f in (device / config.BackupRepositoryFolder).rglob("*") + } + assert found_user == {expected_user} + assert found_group == {expected_group} diff --git a/tests/test_cli.py b/tests/test_cli.py index c0b4f3f..e47204f 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,49 +1,216 @@ -import sys - -from root_subvol_snapshot.cli import app, main +import re +from pathlib import Path +from tempfile import NamedTemporaryFile, TemporaryDirectory +from unittest import mock + +import pytest +import shell_interface as sh +import storage_device_managers as sdm +from loguru import logger from typer.testing import CliRunner -runner = CliRunner() - - -def test_cli(): - result = runner.invoke(app, ["--help"]) - assert result.exit_code == 0 - assert "Show this message and exit." in result.output - - -def test_cli_without_args_snapshots(capsys, monkeypatch): - monkeypatch.setattr(sys, "argv", [sys.argv[0]]) - main() - captured = capsys.readouterr() - assert captured.out == "Making a snapshot of the root subvolume...\n" - - -def test_open(): - result = runner.invoke(app, ["open"]) - assert result.exit_code == 0 - assert result.output.startswith("Opening") - - -def test_open_with_argument(tmp_path): - result = runner.invoke(app, ["open", str(tmp_path)]) - assert result.exit_code == 0 - assert result.output.endswith(f"{tmp_path}...\n") - - -def test_close(): - result = runner.invoke(app, ["close"]) - assert result.exit_code == 0 - assert result.output.startswith("Closing") - - -def test_close_with_argument(tmp_path): - result = runner.invoke(app, ["close", str(tmp_path)]) - assert result.exit_code == 0 - assert result.output.endswith(f"{tmp_path}...\n") - - -def test_snapshot(): - result = runner.invoke(app, ["snapshot"]) - assert result.exit_code == 0 - assert result.output.startswith("Making a snapshot of the root subvolume...") +from root_subvol_snapshot import cli +from root_subvol_snapshot import config_parser as cp +from root_subvol_snapshot.cli import app + + +def get_random_filename() -> str: + with NamedTemporaryFile() as named_file: + return named_file.name + + +def in_docker_container() -> bool: + return Path("/.dockerenv").exists() + + +@pytest.fixture +def runner(): + return CliRunner(mix_stderr=False) + + +def test_get_default_config_path() -> None: + with TemporaryDirectory() as tempdir: + xdg_config_dir = Path(tempdir) + with mock.patch("os.getenv", {"XDG_CONFIG_HOME": xdg_config_dir}.get): + config_file = cli.get_default_config_path() + expected_cfg = xdg_config_dir / cli.DEFAULT_CONFIG_NAME + assert str(expected_cfg) == config_file + + +@pytest.mark.parametrize( + "logmsg", + [ + "Schläft ein Lied in allen Dingen,", + "Die da träumen fort und fort,", + "Und die Welt hebt an zu singen,", + "Triffst du nur das Zauberwort.", + ], +) +@pytest.mark.parametrize("logfunc", [logger.warning, logger.error]) +def test_setup_logging_logs_errors_and_warnings_by_default( + logmsg: str, logfunc, capsys +) -> None: + cli.setup_logging(verbosity=0) + logfunc(logmsg) + out, err = capsys.readouterr() + err_without_linebreak = err[:-1] + assert out == "" + assert err_without_linebreak.endswith(logmsg) + + +def test_setup_logging_does_not_log_more_than_warnings_by_default(capsys) -> None: + cli.setup_logging(verbosity=0) + logger.success("This line will not appear anywhere.") + out, err = capsys.readouterr() + assert out == "" + assert err == "" + + +def test_setup_logging_logs_success(capsys) -> None: + successmsg = "☕️🤎📰📜⚰️🕰🕯🎻🖋" + infomsg = "🦖🦕🐊" + cli.setup_logging(verbosity=1) + logger.success(successmsg) + logger.info(infomsg) + out, err = capsys.readouterr() + err_without_linebreak = err[:-1] + assert out == "" + assert infomsg not in err + assert err_without_linebreak.endswith(successmsg) + + +def test_setup_logging_clamps_level(capsys) -> None: + successmsg = "√-1 2³ Σ π and it was delicious" + tracemsg = "Trace me if you can!" + cli.setup_logging(verbosity=1337) + logger.success(successmsg) + logger.trace(tracemsg) + out, err = capsys.readouterr() + assert out == "" + assert successmsg in err + assert tracemsg in err + + +@pytest.mark.parametrize("subprogram", ["backup"]) +def test_subprograms_refuse_missing_config(subprogram, runner) -> None: + config_file = Path(get_random_filename()) + result = runner.invoke(app, [subprogram, "--config", str(config_file)]) + assert f"{config_file}" in result.stderr + assert result.exit_code != 0 + + +@pytest.mark.skipif(in_docker_container(), reason="All files are readable for root") +@pytest.mark.parametrize("subprogram", ["backup"]) +def test_subprograms_refuse_unreadable_file(subprogram, runner) -> None: + with NamedTemporaryFile() as fh: + config_file = Path(fh.name) + config_file.chmod(0) + result = runner.invoke(app, [subprogram, "--config", str(config_file)]) + assert f"{config_file}" in result.stderr + assert result.exit_code != 0 + + +@pytest.mark.parametrize("subprogram", ["backup"]) +def test_subprograms_refuse_directories(subprogram, runner) -> None: + with TemporaryDirectory() as tmp_dir: + result = runner.invoke(app, [subprogram, "--config", tmp_dir]) + assert tmp_dir in result.stderr + assert result.exit_code != 0 + + +@pytest.mark.skipif( + in_docker_container(), reason="Test is known to fail in Docker container" +) +def test_close_does_not_close_unopened_device(runner, encrypted_btrfs_device) -> None: + config = encrypted_btrfs_device + with NamedTemporaryFile() as tempf: + config_file = Path(tempf.name) + config_file.write_text(f"[{config.model_dump_json()}]") + close_result = runner.invoke( + app, ["close", "--device", str(encrypted_btrfs_device)] + ) + assert close_result.stdout == "" + assert close_result.exit_code == 0 + + +@pytest.mark.skipif( + in_docker_container(), reason="Test is known to fail in Docker container" +) +def test_open_close_roundtrip(runner, encrypted_device) -> None: + config = encrypted_device + expected_cryptsetup_map = Path(f"/dev/mapper/{config.UUID}") + with NamedTemporaryFile() as tempf: + config_file = Path(tempf.name) + config_file.write_text(f"[{config.model_dump_json()}]") + open_result = runner.invoke(app, ["open", "--config", str(config_file)]) + expected_msg = ( + f"Speichermedium {config.UUID} wurde in (?P/[^ ]+) geöffnet." + ) + match = re.fullmatch(expected_msg, open_result.stdout.strip()) + assert match is not None + mount_dest = Path(match.group("mount_dest")) + assert any( + mount_dest in destinations + for destinations in sdm.get_mounted_devices().values() + ) + assert expected_cryptsetup_map.exists() + runner.invoke(app, ["close", "--config", str(config_file)]) + assert not expected_cryptsetup_map.exists() + assert not sdm.is_mounted(mount_dest) + assert not mount_dest.exists() + + +@pytest.mark.parametrize( + "backend", ["BackupBackend", "fvglxvleaeb", "NotYetImplementedBackend"] +) +def test_format_device_refuses_incorrect_backend(runner, backend: str) -> None: + with NamedTemporaryFile() as tempf: + result = runner.invoke(app, ["format-device", tempf.name, backend]) + assert result.exit_code != 0 + + +@pytest.mark.parametrize("backend", ["restic", "btrfs-rsync"]) +def test_format_device(runner, backend: str, big_file: Path) -> None: + format_result = runner.invoke(app, ["format-device", backend, str(big_file)]) + serialised_config = format_result.stdout + config_lst = list(cp.parse_configuration(serialised_config)) + assert len(config_lst) == 1 + device_uuid = config_lst[0].UUID + with NamedTemporaryFile("w") as fh: + fh.write(serialised_config) + fh.seek(0) + with sdm.symbolic_link(big_file, Path(f"/dev/disk/by-uuid/{device_uuid}")): + open_result = runner.invoke(app, ["open", "--config", fh.name]) + close_result = runner.invoke(app, ["close", "--config", fh.name]) + assert format_result.exit_code == 0 + assert open_result.exit_code == 0 + assert close_result.exit_code == 0 + assert str(device_uuid) in open_result.stdout + + +@pytest.mark.parametrize("backend", ["restic", "btrfs-rsync"]) +def test_format_device_chowns_filesystem_to_user( + runner, backend: str, big_file: Path +) -> None: + format_result = runner.invoke(app, ["format-device", backend, str(big_file)]) + serialised_config = format_result.stdout + config_lst = list(cp.parse_configuration(serialised_config)) + assert len(config_lst) == 1 + config = config_lst[0] + + with sdm.decrypted_device(big_file, config.DevicePassCmd) as decrypted: + with sdm.mounted_device(decrypted, sdm.ValidCompressions.ZLIB1) as mounted: + owner = mounted.owner() + group = mounted.group() + expected_user = sh.get_user() + expected_group = sh.get_group(expected_user) + assert owner == expected_user + assert group == expected_group + + +def test_version(runner) -> None: + result = runner.invoke(app, ["version"]) + lines = result.stdout.splitlines() + assert len(lines) == 1 + parts = lines[0].split(".") + assert len(parts) == 3 # noqa: PLR2004