From bfbe78d383087b55e97ac587ba4fd3a8e156f352 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Fri, 17 Jan 2025 23:32:19 +0100 Subject: [PATCH 01/64] Use uv.lock --- uv.lock | 3555 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 3555 insertions(+) create mode 100644 uv.lock diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..2af8adbd --- /dev/null +++ b/uv.lock @@ -0,0 +1,3555 @@ +version = 1 +requires-python = ">=3.8" +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", + "python_full_version < '3.9'", +] + +[[package]] +name = "aio-pika" +version = "9.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiormq" }, + { name = "yarl", version = "1.15.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "yarl", version = "1.18.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/69/8649bdb97fa1521af3dafe23dbc5debadd4b01abb2850a4d193dae9b0451/aio_pika-9.4.3.tar.gz", hash = "sha256:fd2b1fce25f6ed5203ef1dd554dc03b90c9a46a64aaf758d032d78dc31e5295d", size = 47693 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/66/cad391d83b7266a667c85c826bb6c0d7f68519a0eed7634098c12fb39a4b/aio_pika-9.4.3-py3-none-any.whl", hash = "sha256:f1423d2d5a8b7315d144efe1773763bf687ac17aa1535385982687e9e5ed49bb", size = 53240 }, +] + +[[package]] +name = "aiormq" +version = "6.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pamqp" }, + { name = "yarl", version = "1.15.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "yarl", version = "1.18.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a4/79/5397756a8782bf3d0dce392b48260c3ec81010f16bef8441ff03505dccb4/aiormq-6.8.1.tar.gz", hash = "sha256:a964ab09634be1da1f9298ce225b310859763d5cf83ef3a7eae1a6dc6bd1da1a", size = 30528 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/be/1a613ae1564426f86650ff58c351902895aa969f7e537e74bfd568f5c8bf/aiormq-6.8.1-py3-none-any.whl", hash = "sha256:5da896c8624193708f9409ffad0b20395010e2747f22aa4150593837f40aa017", size = 31174 }, +] + +[[package]] +name = "alabaster" +version = "0.7.13" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/94/71/a8ee96d1fd95ca04a0d2e2d9c4081dac4c2d2b12f7ddb899c8cb9bfd1532/alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2", size = 11454 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/88/c7083fc61120ab661c5d0b82cb77079fc1429d3f913a456c1c82cf4658f7/alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3", size = 13857 }, +] + +[[package]] +name = "alabaster" +version = "0.7.16" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/3e/13dd8e5ed9094e734ac430b5d0eb4f2bb001708a8b7856cbf8e084e001ba/alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", size = 23776 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92", size = 13511 }, +] + +[[package]] +name = "anyio" +version = "4.5.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.9'" }, + { name = "idna", marker = "python_full_version < '3.9'" }, + { name = "sniffio", marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/f9/9a7ce600ebe7804daf90d4d48b1c0510a4561ddce43a596be46676f82343/anyio-4.5.2.tar.gz", hash = "sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b", size = 171293 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/b4/f7e396030e3b11394436358ca258a81d6010106582422f23443c16ca1873/anyio-4.5.2-py3-none-any.whl", hash = "sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f", size = 89766 }, +] + +[[package]] +name = "anyio" +version = "4.8.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, + { name = "idna", marker = "python_full_version >= '3.9'" }, + { name = "sniffio", marker = "python_full_version >= '3.9'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.9' and python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 }, +] + +[[package]] +name = "appnope" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321 }, +] + +[[package]] +name = "argon2-cffi" +version = "23.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argon2-cffi-bindings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/fa/57ec2c6d16ecd2ba0cf15f3c7d1c3c2e7b5fcb83555ff56d7ab10888ec8f/argon2_cffi-23.1.0.tar.gz", hash = "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08", size = 42798 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/6a/e8a041599e78b6b3752da48000b14c8d1e8a04ded09c88c714ba047f34f5/argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea", size = 15124 }, +] + +[[package]] +name = "argon2-cffi-bindings" +version = "21.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/e9/184b8ccce6683b0aa2fbb7ba5683ea4b9c5763f1356347f1312c32e3c66e/argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3", size = 1779911 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/13/838ce2620025e9666aa8f686431f67a29052241692a3dd1ae9d3692a89d3/argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367", size = 29658 }, + { url = "https://files.pythonhosted.org/packages/b3/02/f7f7bb6b6af6031edb11037639c697b912e1dea2db94d436e681aea2f495/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d", size = 80583 }, + { url = "https://files.pythonhosted.org/packages/ec/f7/378254e6dd7ae6f31fe40c8649eea7d4832a42243acaf0f1fff9083b2bed/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae", size = 86168 }, + { url = "https://files.pythonhosted.org/packages/74/f6/4a34a37a98311ed73bb80efe422fed95f2ac25a4cacc5ae1d7ae6a144505/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c", size = 82709 }, + { url = "https://files.pythonhosted.org/packages/74/2b/73d767bfdaab25484f7e7901379d5f8793cccbb86c6e0cbc4c1b96f63896/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86", size = 83613 }, + { url = "https://files.pythonhosted.org/packages/4f/fd/37f86deef67ff57c76f137a67181949c2d408077e2e3dd70c6c42912c9bf/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f", size = 84583 }, + { url = "https://files.pythonhosted.org/packages/6f/52/5a60085a3dae8fded8327a4f564223029f5f54b0cb0455a31131b5363a01/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e", size = 88475 }, + { url = "https://files.pythonhosted.org/packages/8b/95/143cd64feb24a15fa4b189a3e1e7efbaeeb00f39a51e99b26fc62fbacabd/argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082", size = 27698 }, + { url = "https://files.pythonhosted.org/packages/37/2c/e34e47c7dee97ba6f01a6203e0383e15b60fb85d78ac9a15cd066f6fe28b/argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f", size = 30817 }, + { url = "https://files.pythonhosted.org/packages/5a/e4/bf8034d25edaa495da3c8a3405627d2e35758e44ff6eaa7948092646fdcc/argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93", size = 53104 }, + { url = "https://files.pythonhosted.org/packages/34/da/d105a3235ae86c1c1a80c1e9c46953e6e53cc8c4c61fb3c5ac8a39bbca48/argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3b9ef65804859d335dc6b31582cad2c5166f0c3e7975f324d9ffaa34ee7e6583", size = 23689 }, + { url = "https://files.pythonhosted.org/packages/43/f3/20bc53a6e50471dfea16a63dc9b69d2a9ec78fd2b9532cc25f8317e121d9/argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4966ef5848d820776f5f562a7d45fdd70c2f330c961d0d745b784034bd9f48d", size = 28122 }, + { url = "https://files.pythonhosted.org/packages/2e/f1/48888db30b6a4a0c78ab7bc7444058a1135b223b6a2a5f2ac7d6780e7443/argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ef543a89dee4db46a1a6e206cd015360e5a75822f76df533845c3cbaf72670", size = 27882 }, + { url = "https://files.pythonhosted.org/packages/ee/0f/a2260a207f21ce2ff4cad00a417c31597f08eafb547e00615bcbf403d8ea/argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2937d286e2ad0cc79a7087d3c272832865f779430e0cc2b4f3718d3159b0cb", size = 30745 }, + { url = "https://files.pythonhosted.org/packages/ed/55/f8ba268bc9005d0ca57a862e8f1b55bf1775e97a36bd30b0a8fb568c265c/argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5e00316dabdaea0b2dd82d141cc66889ced0cdcbfa599e8b471cf22c620c329a", size = 28587 }, +] + +[[package]] +name = "attrs" +version = "20.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/cb/80a4a274df7da7b8baf083249b0890a0579374c3d74b5ac0ee9291f912dc/attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700", size = 164523 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/aa/cb45262569fcc047bf070b5de61813724d6726db83259222cd7b4c79821a/attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", size = 49337 }, +] + +[[package]] +name = "babel" +version = "2.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytz", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/74/f1bc80f23eeba13393b7222b11d95ca3af2c1e28edca18af487137eefed9/babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316", size = 9348104 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", size = 9587599 }, +] + +[[package]] +name = "backcall" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/40/764a663805d84deee23043e1426a9175567db89c8b3287b5c2ad9f71aa93/backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e", size = 18041 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/1c/ff6546b6c12603d8dd1070aa3c3d273ad4c07f5771689a7b69a550e8c951/backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255", size = 11157 }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.12.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/ca/824b1195773ce6166d388573fc106ce56d4a805bd7427b624e063596ec58/beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051", size = 581181 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/fe/e8c672695b37eecc5cbf43e1d0638d88d66ba3a44c4d321c796f4e59167f/beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed", size = 147925 }, +] + +[[package]] +name = "bleach" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "six", marker = "python_full_version < '3.9'" }, + { name = "webencodings", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/10/77f32b088738f40d4f5be801daa5f327879eadd4562f36a2b5ab975ae571/bleach-6.1.0.tar.gz", hash = "sha256:0a31f1837963c41d46bbf1331b8778e1308ea0791db03cc4e7357b97cf42a8fe", size = 202119 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/63/da7237f805089ecc28a3f36bca6a21c31fcbc2eb380f3b8f1be3312abd14/bleach-6.1.0-py3-none-any.whl", hash = "sha256:3225f354cfc436b9789c66c4ee030194bee0568fbf9cbdad3bc8b5c26c5f12b6", size = 162750 }, +] + +[[package]] +name = "bleach" +version = "6.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "webencodings", marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/9a/0e33f5054c54d349ea62c277191c020c2d6ef1d65ab2cb1993f91ec846d1/bleach-6.2.0.tar.gz", hash = "sha256:123e894118b8a599fd80d3ec1a6d4cc7ce4e5882b1317a7e1ba69b56e95f991f", size = 203083 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/55/96142937f66150805c25c4d0f31ee4132fd33497753400734f9dfdcbdc66/bleach-6.2.0-py3-none-any.whl", hash = "sha256:117d9c6097a7c3d22fd578fcd8d35ff1e125df6736f554da4e432fdd63f31e5e", size = 163406 }, +] + +[[package]] +name = "certifi" +version = "2024.12.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/bd/1d41ee578ce09523c81a15426705dd20969f5abf006d1afe8aeff0dd776a/certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db", size = 166010 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/32/8f6669fc4798494966bf446c8c4a162e0b5d893dff088afddf76414f70e1/certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", size = 164927 }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191 }, + { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592 }, + { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024 }, + { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188 }, + { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571 }, + { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687 }, + { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211 }, + { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325 }, + { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784 }, + { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564 }, + { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804 }, + { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299 }, + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, + { url = "https://files.pythonhosted.org/packages/48/08/15bf6b43ae9bd06f6b00ad8a91f5a8fe1069d4c9fab550a866755402724e/cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b", size = 182457 }, + { url = "https://files.pythonhosted.org/packages/c2/5b/f1523dd545f92f7df468e5f653ffa4df30ac222f3c884e51e139878f1cb5/cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964", size = 425932 }, + { url = "https://files.pythonhosted.org/packages/53/93/7e547ab4105969cc8c93b38a667b82a835dd2cc78f3a7dad6130cfd41e1d/cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9", size = 448585 }, + { url = "https://files.pythonhosted.org/packages/56/c4/a308f2c332006206bb511de219efeff090e9d63529ba0a77aae72e82248b/cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc", size = 456268 }, + { url = "https://files.pythonhosted.org/packages/ca/5b/b63681518265f2f4060d2b60755c1c77ec89e5e045fc3773b72735ddaad5/cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c", size = 436592 }, + { url = "https://files.pythonhosted.org/packages/bb/19/b51af9f4a4faa4a8ac5a0e5d5c2522dcd9703d07fac69da34a36c4d960d3/cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1", size = 446512 }, + { url = "https://files.pythonhosted.org/packages/e2/63/2bed8323890cb613bbecda807688a31ed11a7fe7afe31f8faaae0206a9a3/cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8", size = 171576 }, + { url = "https://files.pythonhosted.org/packages/2f/70/80c33b044ebc79527447fd4fbc5455d514c3bb840dede4455de97da39b4d/cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1", size = 181229 }, + { url = "https://files.pythonhosted.org/packages/b9/ea/8bb50596b8ffbc49ddd7a1ad305035daa770202a6b782fc164647c2673ad/cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", size = 182220 }, + { url = "https://files.pythonhosted.org/packages/ae/11/e77c8cd24f58285a82c23af484cf5b124a376b32644e445960d1a4654c3a/cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", size = 178605 }, + { url = "https://files.pythonhosted.org/packages/ed/65/25a8dc32c53bf5b7b6c2686b42ae2ad58743f7ff644844af7cdb29b49361/cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", size = 424910 }, + { url = "https://files.pythonhosted.org/packages/42/7a/9d086fab7c66bd7c4d0f27c57a1b6b068ced810afc498cc8c49e0088661c/cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", size = 447200 }, + { url = "https://files.pythonhosted.org/packages/da/63/1785ced118ce92a993b0ec9e0d0ac8dc3e5dbfbcaa81135be56c69cabbb6/cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", size = 454565 }, + { url = "https://files.pythonhosted.org/packages/74/06/90b8a44abf3556599cdec107f7290277ae8901a58f75e6fe8f970cd72418/cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", size = 435635 }, + { url = "https://files.pythonhosted.org/packages/bd/62/a1f468e5708a70b1d86ead5bab5520861d9c7eacce4a885ded9faa7729c3/cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", size = 445218 }, + { url = "https://files.pythonhosted.org/packages/5b/95/b34462f3ccb09c2594aa782d90a90b045de4ff1f70148ee79c69d37a0a5a/cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", size = 460486 }, + { url = "https://files.pythonhosted.org/packages/fc/fc/a1e4bebd8d680febd29cf6c8a40067182b64f00c7d105f8f26b5bc54317b/cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", size = 437911 }, + { url = "https://files.pythonhosted.org/packages/e6/c3/21cab7a6154b6a5ea330ae80de386e7665254835b9e98ecc1340b3a7de9a/cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", size = 460632 }, + { url = "https://files.pythonhosted.org/packages/cb/b5/fd9f8b5a84010ca169ee49f4e4ad6f8c05f4e3545b72ee041dbbcb159882/cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", size = 171820 }, + { url = "https://files.pythonhosted.org/packages/8c/52/b08750ce0bce45c143e1b5d7357ee8c55341b52bdef4b0f081af1eb248c2/cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", size = 181290 }, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/58/5580c1716040bc89206c77d8f74418caf82ce519aae06450393ca73475d1/charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", size = 198013 }, + { url = "https://files.pythonhosted.org/packages/d0/11/00341177ae71c6f5159a08168bcb98c6e6d196d372c94511f9f6c9afe0c6/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", size = 141285 }, + { url = "https://files.pythonhosted.org/packages/01/09/11d684ea5819e5a8f5100fb0b38cf8d02b514746607934134d31233e02c8/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", size = 151449 }, + { url = "https://files.pythonhosted.org/packages/08/06/9f5a12939db324d905dc1f70591ae7d7898d030d7662f0d426e2286f68c9/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", size = 143892 }, + { url = "https://files.pythonhosted.org/packages/93/62/5e89cdfe04584cb7f4d36003ffa2936681b03ecc0754f8e969c2becb7e24/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", size = 146123 }, + { url = "https://files.pythonhosted.org/packages/a9/ac/ab729a15c516da2ab70a05f8722ecfccc3f04ed7a18e45c75bbbaa347d61/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", size = 147943 }, + { url = "https://files.pythonhosted.org/packages/03/d2/3f392f23f042615689456e9a274640c1d2e5dd1d52de36ab8f7955f8f050/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", size = 142063 }, + { url = "https://files.pythonhosted.org/packages/f2/e3/e20aae5e1039a2cd9b08d9205f52142329f887f8cf70da3650326670bddf/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", size = 150578 }, + { url = "https://files.pythonhosted.org/packages/8d/af/779ad72a4da0aed925e1139d458adc486e61076d7ecdcc09e610ea8678db/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", size = 153629 }, + { url = "https://files.pythonhosted.org/packages/c2/b6/7aa450b278e7aa92cf7732140bfd8be21f5f29d5bf334ae987c945276639/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", size = 150778 }, + { url = "https://files.pythonhosted.org/packages/39/f4/d9f4f712d0951dcbfd42920d3db81b00dd23b6ab520419626f4023334056/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", size = 146453 }, + { url = "https://files.pythonhosted.org/packages/49/2b/999d0314e4ee0cff3cb83e6bc9aeddd397eeed693edb4facb901eb8fbb69/charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", size = 95479 }, + { url = "https://files.pythonhosted.org/packages/2d/ce/3cbed41cff67e455a386fb5e5dd8906cdda2ed92fbc6297921f2e4419309/charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", size = 102790 }, + { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 }, + { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 }, + { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 }, + { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 }, + { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 }, + { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 }, + { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 }, + { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 }, + { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 }, + { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 }, + { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 }, + { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 }, + { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 }, + { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 }, + { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, + { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, + { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, + { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, + { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, + { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, + { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, + { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, + { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, + { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, + { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 }, + { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 }, + { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, + { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, + { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, + { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, + { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, + { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, + { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, + { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, + { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, + { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, + { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, + { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, + { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, + { url = "https://files.pythonhosted.org/packages/10/bd/6517ea94f2672e801011d50b5d06be2a0deaf566aea27bcdcd47e5195357/charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c", size = 195653 }, + { url = "https://files.pythonhosted.org/packages/e5/0d/815a2ba3f283b4eeaa5ece57acade365c5b4135f65a807a083c818716582/charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9", size = 140701 }, + { url = "https://files.pythonhosted.org/packages/aa/17/c94be7ee0d142687e047fe1de72060f6d6837f40eedc26e87e6e124a3fc6/charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8", size = 150495 }, + { url = "https://files.pythonhosted.org/packages/f7/33/557ac796c47165fc141e4fb71d7b0310f67e05cb420756f3a82e0a0068e0/charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6", size = 142946 }, + { url = "https://files.pythonhosted.org/packages/1e/0d/38ef4ae41e9248d63fc4998d933cae22473b1b2ac4122cf908d0f5eb32aa/charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c", size = 144737 }, + { url = "https://files.pythonhosted.org/packages/43/01/754cdb29dd0560f58290aaaa284d43eea343ad0512e6ad3b8b5c11f08592/charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a", size = 147471 }, + { url = "https://files.pythonhosted.org/packages/ba/cd/861883ba5160c7a9bd242c30b2c71074cda2aefcc0addc91118e0d4e0765/charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd", size = 140801 }, + { url = "https://files.pythonhosted.org/packages/6f/7f/0c0dad447819e90b93f8ed238cc8f11b91353c23c19e70fa80483a155bed/charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd", size = 149312 }, + { url = "https://files.pythonhosted.org/packages/8e/09/9f8abcc6fff60fb727268b63c376c8c79cc37b833c2dfe1f535dfb59523b/charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824", size = 152347 }, + { url = "https://files.pythonhosted.org/packages/be/e5/3f363dad2e24378f88ccf63ecc39e817c29f32e308ef21a7a6d9c1201165/charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca", size = 149888 }, + { url = "https://files.pythonhosted.org/packages/e4/10/a78c0e91f487b4ad0ef7480ac765e15b774f83de2597f1b6ef0eaf7a2f99/charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b", size = 145169 }, + { url = "https://files.pythonhosted.org/packages/d3/81/396e7d7f5d7420da8273c91175d2e9a3f569288e3611d521685e4b9ac9cc/charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e", size = 95094 }, + { url = "https://files.pythonhosted.org/packages/40/bb/20affbbd9ea29c71ea123769dc568a6d42052ff5089c5fe23e21e21084a6/charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4", size = 102139 }, + { url = "https://files.pythonhosted.org/packages/7f/c0/b913f8f02836ed9ab32ea643c6fe4d3325c3d8627cf6e78098671cafff86/charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41", size = 197867 }, + { url = "https://files.pythonhosted.org/packages/0f/6c/2bee440303d705b6fb1e2ec789543edec83d32d258299b16eed28aad48e0/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f", size = 141385 }, + { url = "https://files.pythonhosted.org/packages/3d/04/cb42585f07f6f9fd3219ffb6f37d5a39b4fd2db2355b23683060029c35f7/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2", size = 151367 }, + { url = "https://files.pythonhosted.org/packages/54/54/2412a5b093acb17f0222de007cc129ec0e0df198b5ad2ce5699355269dfe/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770", size = 143928 }, + { url = "https://files.pythonhosted.org/packages/5a/6d/e2773862b043dcf8a221342954f375392bb2ce6487bcd9f2c1b34e1d6781/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4", size = 146203 }, + { url = "https://files.pythonhosted.org/packages/b9/f8/ca440ef60d8f8916022859885f231abb07ada3c347c03d63f283bec32ef5/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537", size = 148082 }, + { url = "https://files.pythonhosted.org/packages/04/d2/42fd330901aaa4b805a1097856c2edf5095e260a597f65def493f4b8c833/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496", size = 142053 }, + { url = "https://files.pythonhosted.org/packages/9e/af/3a97a4fa3c53586f1910dadfc916e9c4f35eeada36de4108f5096cb7215f/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78", size = 150625 }, + { url = "https://files.pythonhosted.org/packages/26/ae/23d6041322a3556e4da139663d02fb1b3c59a23ab2e2b56432bd2ad63ded/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7", size = 153549 }, + { url = "https://files.pythonhosted.org/packages/94/22/b8f2081c6a77cb20d97e57e0b385b481887aa08019d2459dc2858ed64871/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6", size = 150945 }, + { url = "https://files.pythonhosted.org/packages/c7/0b/c5ec5092747f801b8b093cdf5610e732b809d6cb11f4c51e35fc28d1d389/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294", size = 146595 }, + { url = "https://files.pythonhosted.org/packages/0c/5a/0b59704c38470df6768aa154cc87b1ac7c9bb687990a1559dc8765e8627e/charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5", size = 95453 }, + { url = "https://files.pythonhosted.org/packages/85/2d/a9790237cb4d01a6d57afadc8573c8b73c609ade20b80f4cda30802009ee/charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765", size = 102811 }, + { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "comm" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/a8/fb783cb0abe2b5fded9f55e5703015cdf1c9c85b3669087c538dd15a6a86/comm-0.2.2.tar.gz", hash = "sha256:3fd7a84065306e07bea1773df6eb8282de51ba82f77c72f9c85716ab11fe980e", size = 6210 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/75/49e5bfe642f71f272236b5b2d2691cf915a7283cc0ceda56357b61daa538/comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3", size = 7180 }, +] + +[[package]] +name = "coverage" +version = "7.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/08/7e37f82e4d1aead42a7443ff06a1e406aabf7302c4f00a546e4b320b994c/coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d", size = 798791 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/61/eb7ce5ed62bacf21beca4937a90fe32545c91a3c8a42a30c6616d48fc70d/coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16", size = 206690 }, + { url = "https://files.pythonhosted.org/packages/7d/73/041928e434442bd3afde5584bdc3f932fb4562b1597629f537387cec6f3d/coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36", size = 207127 }, + { url = "https://files.pythonhosted.org/packages/c7/c8/6ca52b5147828e45ad0242388477fdb90df2c6cbb9a441701a12b3c71bc8/coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02", size = 235654 }, + { url = "https://files.pythonhosted.org/packages/d5/da/9ac2b62557f4340270942011d6efeab9833648380109e897d48ab7c1035d/coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc", size = 233598 }, + { url = "https://files.pythonhosted.org/packages/53/23/9e2c114d0178abc42b6d8d5281f651a8e6519abfa0ef460a00a91f80879d/coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23", size = 234732 }, + { url = "https://files.pythonhosted.org/packages/0f/7e/a0230756fb133343a52716e8b855045f13342b70e48e8ad41d8a0d60ab98/coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34", size = 233816 }, + { url = "https://files.pythonhosted.org/packages/28/7c/3753c8b40d232b1e5eeaed798c875537cf3cb183fb5041017c1fdb7ec14e/coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c", size = 232325 }, + { url = "https://files.pythonhosted.org/packages/57/e3/818a2b2af5b7573b4b82cf3e9f137ab158c90ea750a8f053716a32f20f06/coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959", size = 233418 }, + { url = "https://files.pythonhosted.org/packages/c8/fb/4532b0b0cefb3f06d201648715e03b0feb822907edab3935112b61b885e2/coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232", size = 209343 }, + { url = "https://files.pythonhosted.org/packages/5a/25/af337cc7421eca1c187cc9c315f0a755d48e755d2853715bfe8c418a45fa/coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0", size = 210136 }, + { url = "https://files.pythonhosted.org/packages/ad/5f/67af7d60d7e8ce61a4e2ddcd1bd5fb787180c8d0ae0fbd073f903b3dd95d/coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93", size = 206796 }, + { url = "https://files.pythonhosted.org/packages/e1/0e/e52332389e057daa2e03be1fbfef25bb4d626b37d12ed42ae6281d0a274c/coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3", size = 207244 }, + { url = "https://files.pythonhosted.org/packages/aa/cd/766b45fb6e090f20f8927d9c7cb34237d41c73a939358bc881883fd3a40d/coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff", size = 239279 }, + { url = "https://files.pythonhosted.org/packages/70/6c/a9ccd6fe50ddaf13442a1e2dd519ca805cbe0f1fcd377fba6d8339b98ccb/coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d", size = 236859 }, + { url = "https://files.pythonhosted.org/packages/14/6f/8351b465febb4dbc1ca9929505202db909c5a635c6fdf33e089bbc3d7d85/coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6", size = 238549 }, + { url = "https://files.pythonhosted.org/packages/68/3c/289b81fa18ad72138e6d78c4c11a82b5378a312c0e467e2f6b495c260907/coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56", size = 237477 }, + { url = "https://files.pythonhosted.org/packages/ed/1c/aa1efa6459d822bd72c4abc0b9418cf268de3f60eeccd65dc4988553bd8d/coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234", size = 236134 }, + { url = "https://files.pythonhosted.org/packages/fb/c8/521c698f2d2796565fe9c789c2ee1ccdae610b3aa20b9b2ef980cc253640/coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133", size = 236910 }, + { url = "https://files.pythonhosted.org/packages/7d/30/033e663399ff17dca90d793ee8a2ea2890e7fdf085da58d82468b4220bf7/coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c", size = 209348 }, + { url = "https://files.pythonhosted.org/packages/20/05/0d1ccbb52727ccdadaa3ff37e4d2dc1cd4d47f0c3df9eb58d9ec8508ca88/coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6", size = 210230 }, + { url = "https://files.pythonhosted.org/packages/7e/d4/300fc921dff243cd518c7db3a4c614b7e4b2431b0d1145c1e274fd99bd70/coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778", size = 206983 }, + { url = "https://files.pythonhosted.org/packages/e1/ab/6bf00de5327ecb8db205f9ae596885417a31535eeda6e7b99463108782e1/coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391", size = 207221 }, + { url = "https://files.pythonhosted.org/packages/92/8f/2ead05e735022d1a7f3a0a683ac7f737de14850395a826192f0288703472/coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8", size = 240342 }, + { url = "https://files.pythonhosted.org/packages/0f/ef/94043e478201ffa85b8ae2d2c79b4081e5a1b73438aafafccf3e9bafb6b5/coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d", size = 237371 }, + { url = "https://files.pythonhosted.org/packages/1f/0f/c890339dd605f3ebc269543247bdd43b703cce6825b5ed42ff5f2d6122c7/coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca", size = 239455 }, + { url = "https://files.pythonhosted.org/packages/d1/04/7fd7b39ec7372a04efb0f70c70e35857a99b6a9188b5205efb4c77d6a57a/coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163", size = 238924 }, + { url = "https://files.pythonhosted.org/packages/ed/bf/73ce346a9d32a09cf369f14d2a06651329c984e106f5992c89579d25b27e/coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a", size = 237252 }, + { url = "https://files.pythonhosted.org/packages/86/74/1dc7a20969725e917b1e07fe71a955eb34bc606b938316bcc799f228374b/coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d", size = 238897 }, + { url = "https://files.pythonhosted.org/packages/b6/e9/d9cc3deceb361c491b81005c668578b0dfa51eed02cd081620e9a62f24ec/coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5", size = 209606 }, + { url = "https://files.pythonhosted.org/packages/47/c8/5a2e41922ea6740f77d555c4d47544acd7dc3f251fe14199c09c0f5958d3/coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb", size = 210373 }, + { url = "https://files.pythonhosted.org/packages/8c/f9/9aa4dfb751cb01c949c990d136a0f92027fbcc5781c6e921df1cb1563f20/coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106", size = 207007 }, + { url = "https://files.pythonhosted.org/packages/b9/67/e1413d5a8591622a46dd04ff80873b04c849268831ed5c304c16433e7e30/coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9", size = 207269 }, + { url = "https://files.pythonhosted.org/packages/14/5b/9dec847b305e44a5634d0fb8498d135ab1d88330482b74065fcec0622224/coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c", size = 239886 }, + { url = "https://files.pythonhosted.org/packages/7b/b7/35760a67c168e29f454928f51f970342d23cf75a2bb0323e0f07334c85f3/coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a", size = 237037 }, + { url = "https://files.pythonhosted.org/packages/f7/95/d2fd31f1d638df806cae59d7daea5abf2b15b5234016a5ebb502c2f3f7ee/coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060", size = 239038 }, + { url = "https://files.pythonhosted.org/packages/6e/bd/110689ff5752b67924efd5e2aedf5190cbbe245fc81b8dec1abaffba619d/coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862", size = 238690 }, + { url = "https://files.pythonhosted.org/packages/d3/a8/08d7b38e6ff8df52331c83130d0ab92d9c9a8b5462f9e99c9f051a4ae206/coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388", size = 236765 }, + { url = "https://files.pythonhosted.org/packages/d6/6a/9cf96839d3147d55ae713eb2d877f4d777e7dc5ba2bce227167d0118dfe8/coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155", size = 238611 }, + { url = "https://files.pythonhosted.org/packages/74/e4/7ff20d6a0b59eeaab40b3140a71e38cf52547ba21dbcf1d79c5a32bba61b/coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a", size = 209671 }, + { url = "https://files.pythonhosted.org/packages/35/59/1812f08a85b57c9fdb6d0b383d779e47b6f643bc278ed682859512517e83/coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129", size = 210368 }, + { url = "https://files.pythonhosted.org/packages/9c/15/08913be1c59d7562a3e39fce20661a98c0a3f59d5754312899acc6cb8a2d/coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e", size = 207758 }, + { url = "https://files.pythonhosted.org/packages/c4/ae/b5d58dff26cade02ada6ca612a76447acd69dccdbb3a478e9e088eb3d4b9/coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962", size = 208035 }, + { url = "https://files.pythonhosted.org/packages/b8/d7/62095e355ec0613b08dfb19206ce3033a0eedb6f4a67af5ed267a8800642/coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb", size = 250839 }, + { url = "https://files.pythonhosted.org/packages/7c/1e/c2967cb7991b112ba3766df0d9c21de46b476d103e32bb401b1b2adf3380/coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704", size = 246569 }, + { url = "https://files.pythonhosted.org/packages/8b/61/a7a6a55dd266007ed3b1df7a3386a0d760d014542d72f7c2c6938483b7bd/coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b", size = 248927 }, + { url = "https://files.pythonhosted.org/packages/c8/fa/13a6f56d72b429f56ef612eb3bc5ce1b75b7ee12864b3bd12526ab794847/coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f", size = 248401 }, + { url = "https://files.pythonhosted.org/packages/75/06/0429c652aa0fb761fc60e8c6b291338c9173c6aa0f4e40e1902345b42830/coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223", size = 246301 }, + { url = "https://files.pythonhosted.org/packages/52/76/1766bb8b803a88f93c3a2d07e30ffa359467810e5cbc68e375ebe6906efb/coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3", size = 247598 }, + { url = "https://files.pythonhosted.org/packages/66/8b/f54f8db2ae17188be9566e8166ac6df105c1c611e25da755738025708d54/coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", size = 210307 }, + { url = "https://files.pythonhosted.org/packages/9f/b0/e0dca6da9170aefc07515cce067b97178cefafb512d00a87a1c717d2efd5/coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", size = 211453 }, + { url = "https://files.pythonhosted.org/packages/81/d0/d9e3d554e38beea5a2e22178ddb16587dbcbe9a1ef3211f55733924bf7fa/coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0", size = 206674 }, + { url = "https://files.pythonhosted.org/packages/38/ea/cab2dc248d9f45b2b7f9f1f596a4d75a435cb364437c61b51d2eb33ceb0e/coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a", size = 207101 }, + { url = "https://files.pythonhosted.org/packages/ca/6f/f82f9a500c7c5722368978a5390c418d2a4d083ef955309a8748ecaa8920/coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b", size = 236554 }, + { url = "https://files.pythonhosted.org/packages/a6/94/d3055aa33d4e7e733d8fa309d9adf147b4b06a82c1346366fc15a2b1d5fa/coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3", size = 234440 }, + { url = "https://files.pythonhosted.org/packages/e4/6e/885bcd787d9dd674de4a7d8ec83faf729534c63d05d51d45d4fa168f7102/coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de", size = 235889 }, + { url = "https://files.pythonhosted.org/packages/f4/63/df50120a7744492710854860783d6819ff23e482dee15462c9a833cc428a/coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6", size = 235142 }, + { url = "https://files.pythonhosted.org/packages/3a/5d/9d0acfcded2b3e9ce1c7923ca52ccc00c78a74e112fc2aee661125b7843b/coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569", size = 233805 }, + { url = "https://files.pythonhosted.org/packages/c4/56/50abf070cb3cd9b1dd32f2c88f083aab561ecbffbcd783275cb51c17f11d/coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989", size = 234655 }, + { url = "https://files.pythonhosted.org/packages/25/ee/b4c246048b8485f85a2426ef4abab88e48c6e80c74e964bea5cd4cd4b115/coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7", size = 209296 }, + { url = "https://files.pythonhosted.org/packages/5c/1c/96cf86b70b69ea2b12924cdf7cabb8ad10e6130eab8d767a1099fbd2a44f/coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8", size = 210137 }, + { url = "https://files.pythonhosted.org/packages/19/d3/d54c5aa83268779d54c86deb39c1c4566e5d45c155369ca152765f8db413/coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255", size = 206688 }, + { url = "https://files.pythonhosted.org/packages/a5/fe/137d5dca72e4a258b1bc17bb04f2e0196898fe495843402ce826a7419fe3/coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8", size = 207120 }, + { url = "https://files.pythonhosted.org/packages/78/5b/a0a796983f3201ff5485323b225d7c8b74ce30c11f456017e23d8e8d1945/coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2", size = 235249 }, + { url = "https://files.pythonhosted.org/packages/4e/e1/76089d6a5ef9d68f018f65411fcdaaeb0141b504587b901d74e8587606ad/coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a", size = 233237 }, + { url = "https://files.pythonhosted.org/packages/9a/6f/eef79b779a540326fee9520e5542a8b428cc3bfa8b7c8f1022c1ee4fc66c/coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc", size = 234311 }, + { url = "https://files.pythonhosted.org/packages/75/e1/656d65fb126c29a494ef964005702b012f3498db1a30dd562958e85a4049/coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004", size = 233453 }, + { url = "https://files.pythonhosted.org/packages/68/6a/45f108f137941a4a1238c85f28fd9d048cc46b5466d6b8dda3aba1bb9d4f/coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb", size = 231958 }, + { url = "https://files.pythonhosted.org/packages/9b/e7/47b809099168b8b8c72ae311efc3e88c8d8a1162b3ba4b8da3cfcdb85743/coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36", size = 232938 }, + { url = "https://files.pythonhosted.org/packages/52/80/052222ba7058071f905435bad0ba392cc12006380731c37afaf3fe749b88/coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c", size = 209352 }, + { url = "https://files.pythonhosted.org/packages/b8/d8/1b92e0b3adcf384e98770a00ca095da1b5f7b483e6563ae4eb5e935d24a1/coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca", size = 210153 }, + { url = "https://files.pythonhosted.org/packages/a5/2b/0354ed096bca64dc8e32a7cbcae28b34cb5ad0b1fe2125d6d99583313ac0/coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df", size = 198926 }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version < '3.9'" }, +] + +[[package]] +name = "coverage" +version = "7.6.10" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/84/ba/ac14d281f80aab516275012e8875991bb06203957aa1e19950139238d658/coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23", size = 803868 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/12/2a2a923edf4ddabdffed7ad6da50d96a5c126dae7b80a33df7310e329a1e/coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78", size = 207982 }, + { url = "https://files.pythonhosted.org/packages/ca/49/6985dbca9c7be3f3cb62a2e6e492a0c88b65bf40579e16c71ae9c33c6b23/coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c", size = 208414 }, + { url = "https://files.pythonhosted.org/packages/35/93/287e8f1d1ed2646f4e0b2605d14616c9a8a2697d0d1b453815eb5c6cebdb/coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a", size = 236860 }, + { url = "https://files.pythonhosted.org/packages/de/e1/cfdb5627a03567a10031acc629b75d45a4ca1616e54f7133ca1fa366050a/coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165", size = 234758 }, + { url = "https://files.pythonhosted.org/packages/6d/85/fc0de2bcda3f97c2ee9fe8568f7d48f7279e91068958e5b2cc19e0e5f600/coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988", size = 235920 }, + { url = "https://files.pythonhosted.org/packages/79/73/ef4ea0105531506a6f4cf4ba571a214b14a884630b567ed65b3d9c1975e1/coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5", size = 234986 }, + { url = "https://files.pythonhosted.org/packages/c6/4d/75afcfe4432e2ad0405c6f27adeb109ff8976c5e636af8604f94f29fa3fc/coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3", size = 233446 }, + { url = "https://files.pythonhosted.org/packages/86/5b/efee56a89c16171288cafff022e8af44f8f94075c2d8da563c3935212871/coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5", size = 234566 }, + { url = "https://files.pythonhosted.org/packages/f2/db/67770cceb4a64d3198bf2aa49946f411b85ec6b0a9b489e61c8467a4253b/coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244", size = 210675 }, + { url = "https://files.pythonhosted.org/packages/8d/27/e8bfc43f5345ec2c27bc8a1fa77cdc5ce9dcf954445e11f14bb70b889d14/coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e", size = 211518 }, + { url = "https://files.pythonhosted.org/packages/85/d2/5e175fcf6766cf7501a8541d81778fd2f52f4870100e791f5327fd23270b/coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3", size = 208088 }, + { url = "https://files.pythonhosted.org/packages/4b/6f/06db4dc8fca33c13b673986e20e466fd936235a6ec1f0045c3853ac1b593/coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43", size = 208536 }, + { url = "https://files.pythonhosted.org/packages/0d/62/c6a0cf80318c1c1af376d52df444da3608eafc913b82c84a4600d8349472/coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132", size = 240474 }, + { url = "https://files.pythonhosted.org/packages/a3/59/750adafc2e57786d2e8739a46b680d4fb0fbc2d57fbcb161290a9f1ecf23/coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f", size = 237880 }, + { url = "https://files.pythonhosted.org/packages/2c/f8/ef009b3b98e9f7033c19deb40d629354aab1d8b2d7f9cfec284dbedf5096/coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994", size = 239750 }, + { url = "https://files.pythonhosted.org/packages/a6/e2/6622f3b70f5f5b59f705e680dae6db64421af05a5d1e389afd24dae62e5b/coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99", size = 238642 }, + { url = "https://files.pythonhosted.org/packages/2d/10/57ac3f191a3c95c67844099514ff44e6e19b2915cd1c22269fb27f9b17b6/coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd", size = 237266 }, + { url = "https://files.pythonhosted.org/packages/ee/2d/7016f4ad9d553cabcb7333ed78ff9d27248ec4eba8dd21fa488254dff894/coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377", size = 238045 }, + { url = "https://files.pythonhosted.org/packages/a7/fe/45af5c82389a71e0cae4546413266d2195c3744849669b0bab4b5f2c75da/coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8", size = 210647 }, + { url = "https://files.pythonhosted.org/packages/db/11/3f8e803a43b79bc534c6a506674da9d614e990e37118b4506faf70d46ed6/coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609", size = 211508 }, + { url = "https://files.pythonhosted.org/packages/86/77/19d09ea06f92fdf0487499283b1b7af06bc422ea94534c8fe3a4cd023641/coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853", size = 208281 }, + { url = "https://files.pythonhosted.org/packages/b6/67/5479b9f2f99fcfb49c0d5cf61912a5255ef80b6e80a3cddba39c38146cf4/coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078", size = 208514 }, + { url = "https://files.pythonhosted.org/packages/15/d1/febf59030ce1c83b7331c3546d7317e5120c5966471727aa7ac157729c4b/coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0", size = 241537 }, + { url = "https://files.pythonhosted.org/packages/4b/7e/5ac4c90192130e7cf8b63153fe620c8bfd9068f89a6d9b5f26f1550f7a26/coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50", size = 238572 }, + { url = "https://files.pythonhosted.org/packages/dc/03/0334a79b26ecf59958f2fe9dd1f5ab3e2f88db876f5071933de39af09647/coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022", size = 240639 }, + { url = "https://files.pythonhosted.org/packages/d7/45/8a707f23c202208d7b286d78ad6233f50dcf929319b664b6cc18a03c1aae/coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b", size = 240072 }, + { url = "https://files.pythonhosted.org/packages/66/02/603ce0ac2d02bc7b393279ef618940b4a0535b0868ee791140bda9ecfa40/coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0", size = 238386 }, + { url = "https://files.pythonhosted.org/packages/04/62/4e6887e9be060f5d18f1dd58c2838b2d9646faf353232dec4e2d4b1c8644/coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852", size = 240054 }, + { url = "https://files.pythonhosted.org/packages/5c/74/83ae4151c170d8bd071924f212add22a0e62a7fe2b149edf016aeecad17c/coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359", size = 210904 }, + { url = "https://files.pythonhosted.org/packages/c3/54/de0893186a221478f5880283119fc40483bc460b27c4c71d1b8bba3474b9/coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247", size = 211692 }, + { url = "https://files.pythonhosted.org/packages/25/6d/31883d78865529257bf847df5789e2ae80e99de8a460c3453dbfbe0db069/coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9", size = 208308 }, + { url = "https://files.pythonhosted.org/packages/70/22/3f2b129cc08de00c83b0ad6252e034320946abfc3e4235c009e57cfeee05/coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b", size = 208565 }, + { url = "https://files.pythonhosted.org/packages/97/0a/d89bc2d1cc61d3a8dfe9e9d75217b2be85f6c73ebf1b9e3c2f4e797f4531/coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690", size = 241083 }, + { url = "https://files.pythonhosted.org/packages/4c/81/6d64b88a00c7a7aaed3a657b8eaa0931f37a6395fcef61e53ff742b49c97/coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18", size = 238235 }, + { url = "https://files.pythonhosted.org/packages/9a/0b/7797d4193f5adb4b837207ed87fecf5fc38f7cc612b369a8e8e12d9fa114/coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c", size = 240220 }, + { url = "https://files.pythonhosted.org/packages/65/4d/6f83ca1bddcf8e51bf8ff71572f39a1c73c34cf50e752a952c34f24d0a60/coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd", size = 239847 }, + { url = "https://files.pythonhosted.org/packages/30/9d/2470df6aa146aff4c65fee0f87f58d2164a67533c771c9cc12ffcdb865d5/coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e", size = 237922 }, + { url = "https://files.pythonhosted.org/packages/08/dd/723fef5d901e6a89f2507094db66c091449c8ba03272861eaefa773ad95c/coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694", size = 239783 }, + { url = "https://files.pythonhosted.org/packages/3d/f7/64d3298b2baf261cb35466000628706ce20a82d42faf9b771af447cd2b76/coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6", size = 210965 }, + { url = "https://files.pythonhosted.org/packages/d5/58/ec43499a7fc681212fe7742fe90b2bc361cdb72e3181ace1604247a5b24d/coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e", size = 211719 }, + { url = "https://files.pythonhosted.org/packages/ab/c9/f2857a135bcff4330c1e90e7d03446b036b2363d4ad37eb5e3a47bbac8a6/coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe", size = 209050 }, + { url = "https://files.pythonhosted.org/packages/aa/b3/f840e5bd777d8433caa9e4a1eb20503495709f697341ac1a8ee6a3c906ad/coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273", size = 209321 }, + { url = "https://files.pythonhosted.org/packages/85/7d/125a5362180fcc1c03d91850fc020f3831d5cda09319522bcfa6b2b70be7/coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8", size = 252039 }, + { url = "https://files.pythonhosted.org/packages/a9/9c/4358bf3c74baf1f9bddd2baf3756b54c07f2cfd2535f0a47f1e7757e54b3/coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098", size = 247758 }, + { url = "https://files.pythonhosted.org/packages/cf/c7/de3eb6fc5263b26fab5cda3de7a0f80e317597a4bad4781859f72885f300/coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb", size = 250119 }, + { url = "https://files.pythonhosted.org/packages/3e/e6/43de91f8ba2ec9140c6a4af1102141712949903dc732cf739167cfa7a3bc/coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0", size = 249597 }, + { url = "https://files.pythonhosted.org/packages/08/40/61158b5499aa2adf9e37bc6d0117e8f6788625b283d51e7e0c53cf340530/coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf", size = 247473 }, + { url = "https://files.pythonhosted.org/packages/50/69/b3f2416725621e9f112e74e8470793d5b5995f146f596f133678a633b77e/coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2", size = 248737 }, + { url = "https://files.pythonhosted.org/packages/3c/6e/fe899fb937657db6df31cc3e61c6968cb56d36d7326361847440a430152e/coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312", size = 211611 }, + { url = "https://files.pythonhosted.org/packages/1c/55/52f5e66142a9d7bc93a15192eba7a78513d2abf6b3558d77b4ca32f5f424/coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d", size = 212781 }, + { url = "https://files.pythonhosted.org/packages/40/41/473617aadf9a1c15bc2d56be65d90d7c29bfa50a957a67ef96462f7ebf8e/coverage-7.6.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a", size = 207978 }, + { url = "https://files.pythonhosted.org/packages/10/f6/480586607768b39a30e6910a3c4522139094ac0f1677028e1f4823688957/coverage-7.6.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27", size = 208415 }, + { url = "https://files.pythonhosted.org/packages/f1/af/439bb760f817deff6f4d38fe7da08d9dd7874a560241f1945bc3b4446550/coverage-7.6.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4", size = 236452 }, + { url = "https://files.pythonhosted.org/packages/d0/13/481f4ceffcabe29ee2332e60efb52e4694f54a402f3ada2bcec10bb32e43/coverage-7.6.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f", size = 234374 }, + { url = "https://files.pythonhosted.org/packages/c5/59/4607ea9d6b1b73e905c7656da08d0b00cdf6e59f2293ec259e8914160025/coverage-7.6.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25", size = 235505 }, + { url = "https://files.pythonhosted.org/packages/85/60/d66365723b9b7f29464b11d024248ed3523ce5aab958e4ad8c43f3f4148b/coverage-7.6.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315", size = 234616 }, + { url = "https://files.pythonhosted.org/packages/74/f8/2cf7a38e7d81b266f47dfcf137fecd8fa66c7bdbd4228d611628d8ca3437/coverage-7.6.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90", size = 233099 }, + { url = "https://files.pythonhosted.org/packages/50/2b/bff6c1c6b63c4396ea7ecdbf8db1788b46046c681b8fcc6ec77db9f4ea49/coverage-7.6.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d", size = 234089 }, + { url = "https://files.pythonhosted.org/packages/bf/b5/baace1c754d546a67779358341aa8d2f7118baf58cac235db457e1001d1b/coverage-7.6.10-cp39-cp39-win32.whl", hash = "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18", size = 210701 }, + { url = "https://files.pythonhosted.org/packages/b1/bf/9e1e95b8b20817398ecc5a1e8d3e05ff404e1b9fb2185cd71561698fe2a2/coverage-7.6.10-cp39-cp39-win_amd64.whl", hash = "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59", size = 211482 }, + { url = "https://files.pythonhosted.org/packages/a1/70/de81bfec9ed38a64fc44a77c7665e20ca507fc3265597c28b0d989e4082e/coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f", size = 200223 }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version <= '3.11'" }, +] + +[[package]] +name = "debugpy" +version = "1.8.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/25/c74e337134edf55c4dfc9af579eccb45af2393c40960e2795a94351e8140/debugpy-1.8.12.tar.gz", hash = "sha256:646530b04f45c830ceae8e491ca1c9320a2d2f0efea3141487c82130aba70dce", size = 1641122 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/19/dd58334c0a1ec07babf80bf29fb8daf1a7ca4c1a3bbe61548e40616ac087/debugpy-1.8.12-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:a2ba7ffe58efeae5b8fad1165357edfe01464f9aef25e814e891ec690e7dd82a", size = 2076091 }, + { url = "https://files.pythonhosted.org/packages/4c/37/bde1737da15f9617d11ab7b8d5267165f1b7dae116b2585a6643e89e1fa2/debugpy-1.8.12-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbbd4149c4fc5e7d508ece083e78c17442ee13b0e69bfa6bd63003e486770f45", size = 3560717 }, + { url = "https://files.pythonhosted.org/packages/d9/ca/bc67f5a36a7de072908bc9e1156c0f0b272a9a2224cf21540ab1ffd71a1f/debugpy-1.8.12-cp310-cp310-win32.whl", hash = "sha256:b202f591204023b3ce62ff9a47baa555dc00bb092219abf5caf0e3718ac20e7c", size = 5180672 }, + { url = "https://files.pythonhosted.org/packages/c1/b9/e899c0a80dfa674dbc992f36f2b1453cd1ee879143cdb455bc04fce999da/debugpy-1.8.12-cp310-cp310-win_amd64.whl", hash = "sha256:9649eced17a98ce816756ce50433b2dd85dfa7bc92ceb60579d68c053f98dff9", size = 5212702 }, + { url = "https://files.pythonhosted.org/packages/af/9f/5b8af282253615296264d4ef62d14a8686f0dcdebb31a669374e22fff0a4/debugpy-1.8.12-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:36f4829839ef0afdfdd208bb54f4c3d0eea86106d719811681a8627ae2e53dd5", size = 2174643 }, + { url = "https://files.pythonhosted.org/packages/ef/31/f9274dcd3b0f9f7d1e60373c3fa4696a585c55acb30729d313bb9d3bcbd1/debugpy-1.8.12-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a28ed481d530e3138553be60991d2d61103ce6da254e51547b79549675f539b7", size = 3133457 }, + { url = "https://files.pythonhosted.org/packages/ab/ca/6ee59e9892e424477e0c76e3798046f1fd1288040b927319c7a7b0baa484/debugpy-1.8.12-cp311-cp311-win32.whl", hash = "sha256:4ad9a94d8f5c9b954e0e3b137cc64ef3f579d0df3c3698fe9c3734ee397e4abb", size = 5106220 }, + { url = "https://files.pythonhosted.org/packages/d5/1a/8ab508ab05ede8a4eae3b139bbc06ea3ca6234f9e8c02713a044f253be5e/debugpy-1.8.12-cp311-cp311-win_amd64.whl", hash = "sha256:4703575b78dd697b294f8c65588dc86874ed787b7348c65da70cfc885efdf1e1", size = 5130481 }, + { url = "https://files.pythonhosted.org/packages/ba/e6/0f876ecfe5831ebe4762b19214364753c8bc2b357d28c5d739a1e88325c7/debugpy-1.8.12-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:7e94b643b19e8feb5215fa508aee531387494bf668b2eca27fa769ea11d9f498", size = 2500846 }, + { url = "https://files.pythonhosted.org/packages/19/64/33f41653a701f3cd2cbff8b41ebaad59885b3428b5afd0d93d16012ecf17/debugpy-1.8.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:086b32e233e89a2740c1615c2f775c34ae951508b28b308681dbbb87bba97d06", size = 4222181 }, + { url = "https://files.pythonhosted.org/packages/32/a6/02646cfe50bfacc9b71321c47dc19a46e35f4e0aceea227b6d205e900e34/debugpy-1.8.12-cp312-cp312-win32.whl", hash = "sha256:2ae5df899732a6051b49ea2632a9ea67f929604fd2b036613a9f12bc3163b92d", size = 5227017 }, + { url = "https://files.pythonhosted.org/packages/da/a6/10056431b5c47103474312cf4a2ec1001f73e0b63b1216706d5fef2531eb/debugpy-1.8.12-cp312-cp312-win_amd64.whl", hash = "sha256:39dfbb6fa09f12fae32639e3286112fc35ae976114f1f3d37375f3130a820969", size = 5267555 }, + { url = "https://files.pythonhosted.org/packages/cf/4d/7c3896619a8791effd5d8c31f0834471fc8f8fb3047ec4f5fc69dd1393dd/debugpy-1.8.12-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:696d8ae4dff4cbd06bf6b10d671e088b66669f110c7c4e18a44c43cf75ce966f", size = 2485246 }, + { url = "https://files.pythonhosted.org/packages/99/46/bc6dcfd7eb8cc969a5716d858e32485eb40c72c6a8dc88d1e3a4d5e95813/debugpy-1.8.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:898fba72b81a654e74412a67c7e0a81e89723cfe2a3ea6fcd3feaa3395138ca9", size = 4218616 }, + { url = "https://files.pythonhosted.org/packages/03/dd/d7fcdf0381a9b8094da1f6a1c9f19fed493a4f8576a2682349b3a8b20ec7/debugpy-1.8.12-cp313-cp313-win32.whl", hash = "sha256:22a11c493c70413a01ed03f01c3c3a2fc4478fc6ee186e340487b2edcd6f4180", size = 5226540 }, + { url = "https://files.pythonhosted.org/packages/25/bd/ecb98f5b5fc7ea0bfbb3c355bc1dd57c198a28780beadd1e19915bf7b4d9/debugpy-1.8.12-cp313-cp313-win_amd64.whl", hash = "sha256:fdb3c6d342825ea10b90e43d7f20f01535a72b3a1997850c0c3cefa5c27a4a2c", size = 5267134 }, + { url = "https://files.pythonhosted.org/packages/6f/56/6c7ddb4dfd2feca7ea3a580a32c7694f6c77183fa08932ee8ba37a0e703c/debugpy-1.8.12-cp38-cp38-macosx_14_0_x86_64.whl", hash = "sha256:b0232cd42506d0c94f9328aaf0d1d0785f90f87ae72d9759df7e5051be039738", size = 2076797 }, + { url = "https://files.pythonhosted.org/packages/73/25/a58e149ddcd609c8212ca733999251022e53508906e2c9f67252e4516de6/debugpy-1.8.12-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9af40506a59450f1315168d47a970db1a65aaab5df3833ac389d2899a5d63b3f", size = 3632547 }, + { url = "https://files.pythonhosted.org/packages/91/c7/17c09b9d8332d09b7b0aa430085010945d92d90945748948cd38865c0b93/debugpy-1.8.12-cp38-cp38-win32.whl", hash = "sha256:5cc45235fefac57f52680902b7d197fb2f3650112379a6fa9aa1b1c1d3ed3f02", size = 5185270 }, + { url = "https://files.pythonhosted.org/packages/3b/d1/afdbb99f95f54c2768fa2511bf38ec8805b4cde319725e318e5016b252ec/debugpy-1.8.12-cp38-cp38-win_amd64.whl", hash = "sha256:557cc55b51ab2f3371e238804ffc8510b6ef087673303890f57a24195d096e61", size = 5217697 }, + { url = "https://files.pythonhosted.org/packages/89/37/a3333c5b69c086465ea3c073424ef2775e52a6c17276f642f64209c4a082/debugpy-1.8.12-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:b5c6c967d02fee30e157ab5227706f965d5c37679c687b1e7bbc5d9e7128bd41", size = 2077275 }, + { url = "https://files.pythonhosted.org/packages/50/1d/99f6a0a78b4b513ff2b0d0e44c1e705f7ee34e3aba0e8add617d339d97dc/debugpy-1.8.12-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88a77f422f31f170c4b7e9ca58eae2a6c8e04da54121900651dfa8e66c29901a", size = 3555956 }, + { url = "https://files.pythonhosted.org/packages/b8/86/c624665aaa807d065da2016b05e9f2fb4fa56872d67a5fbd7751e77f7f88/debugpy-1.8.12-cp39-cp39-win32.whl", hash = "sha256:a4042edef80364239f5b7b5764e55fd3ffd40c32cf6753da9bda4ff0ac466018", size = 5181535 }, + { url = "https://files.pythonhosted.org/packages/72/c7/d59a0f845ce1677b5c2bb170f08cc1cc3531625a5fdce9c67bd31116540a/debugpy-1.8.12-cp39-cp39-win_amd64.whl", hash = "sha256:f30b03b0f27608a0b26c75f0bb8a880c752c0e0b01090551b9d87c7d783e2069", size = 5213601 }, + { url = "https://files.pythonhosted.org/packages/38/c4/5120ad36405c3008f451f94b8f92ef1805b1e516f6ff870f331ccb3c4cc0/debugpy-1.8.12-py2.py3-none-any.whl", hash = "sha256:274b6a2040349b5c9864e475284bce5bb062e63dce368a394b8cc865ae3b00c6", size = 5229490 }, +] + +[[package]] +name = "decorator" +version = "5.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/0c/8d907af351aa16b42caae42f9d6aa37b900c67308052d10fdce809f8d952/decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330", size = 35016 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/50/83c593b07763e1161326b3b8c6686f0f4b0f24d5526546bee538c89837d6/decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186", size = 9073 }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604 }, +] + +[[package]] +name = "deprecation" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/d3/8ae2869247df154b64c1884d7346d412fed0c49df84db635aab2d1c40e62/deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff", size = 173788 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178 }, +] + +[[package]] +name = "distlib" +version = "0.3.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, +] + +[[package]] +name = "docutils" +version = "0.17.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/17/559b4d020f4b46e0287a2eddf2d8ebf76318fd3bd495f1625414b052fdc9/docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125", size = 2016138 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/5e/6003a0d1f37725ec2ebd4046b657abb9372202655f96e76795dca8c0063c/docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61", size = 575533 }, +] + +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408 }, +] + +[[package]] +name = "entrypoints" +version = "0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/8d/a7121ffe5f402dc015277d2d31eb82d2187334503a011c18f2e78ecbb9b2/entrypoints-0.4.tar.gz", hash = "sha256:b706eddaa9218a19ebcd67b56818f05bb27589b1ca9e8d797b74affad4ccacd4", size = 13974 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/a8/365059bbcd4572cbc41de17fd5b682be5868b218c3c5479071865cab9078/entrypoints-0.4-py3-none-any.whl", hash = "sha256:f174b5ff827504fd3cd97cc3f8649f3693f51538c7e4bdf3ef002c8429d42f9f", size = 5294 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, +] + +[[package]] +name = "fastjsonschema" +version = "2.21.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/50/4b769ce1ac4071a1ef6d86b1a3fb56cdc3a37615e8c5519e1af96cdac366/fastjsonschema-2.21.1.tar.gz", hash = "sha256:794d4f0a58f848961ba16af7b9c85a3e88cd360df008c59aac6fc5ae9323b5d4", size = 373939 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/2b/0817a2b257fe88725c25589d89aec060581aabf668707a8d03b2e9e0cb2a/fastjsonschema-2.21.1-py3-none-any.whl", hash = "sha256:c9e5b7e908310918cf494a434eeb31384dd84a98b57a30bcb1f535015b554667", size = 23924 }, +] + +[[package]] +name = "filelock" +version = "3.16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/db/3ef5bb276dae18d6ec2124224403d1d67bccdbefc17af4cc8f553e341ab1/filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435", size = 18037 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 }, +] + +[[package]] +name = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794 }, +] + +[[package]] +name = "gitpython" +version = "3.1.44" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/89/37df0b71473153574a5cdef8f242de422a0f5d26d7a9e231e6f169b4ad14/gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269", size = 214196 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", size = 207599 }, +] + +[[package]] +name = "greenlet" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/90/5234a78dc0ef6496a6eb97b67a42a8e96742a56f7dc808cb954a85390448/greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563", size = 271235 }, + { url = "https://files.pythonhosted.org/packages/7c/16/cd631fa0ab7d06ef06387135b7549fdcc77d8d859ed770a0d28e47b20972/greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83", size = 637168 }, + { url = "https://files.pythonhosted.org/packages/2f/b1/aed39043a6fec33c284a2c9abd63ce191f4f1a07319340ffc04d2ed3256f/greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0", size = 648826 }, + { url = "https://files.pythonhosted.org/packages/76/25/40e0112f7f3ebe54e8e8ed91b2b9f970805143efef16d043dfc15e70f44b/greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120", size = 644443 }, + { url = "https://files.pythonhosted.org/packages/fb/2f/3850b867a9af519794784a7eeed1dd5bc68ffbcc5b28cef703711025fd0a/greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc", size = 643295 }, + { url = "https://files.pythonhosted.org/packages/cf/69/79e4d63b9387b48939096e25115b8af7cd8a90397a304f92436bcb21f5b2/greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617", size = 599544 }, + { url = "https://files.pythonhosted.org/packages/46/1d/44dbcb0e6c323bd6f71b8c2f4233766a5faf4b8948873225d34a0b7efa71/greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7", size = 1125456 }, + { url = "https://files.pythonhosted.org/packages/e0/1d/a305dce121838d0278cee39d5bb268c657f10a5363ae4b726848f833f1bb/greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6", size = 1149111 }, + { url = "https://files.pythonhosted.org/packages/96/28/d62835fb33fb5652f2e98d34c44ad1a0feacc8b1d3f1aecab035f51f267d/greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80", size = 298392 }, + { url = "https://files.pythonhosted.org/packages/28/62/1c2665558618553c42922ed47a4e6d6527e2fa3516a8256c2f431c5d0441/greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70", size = 272479 }, + { url = "https://files.pythonhosted.org/packages/76/9d/421e2d5f07285b6e4e3a676b016ca781f63cfe4a0cd8eaecf3fd6f7a71ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159", size = 640404 }, + { url = "https://files.pythonhosted.org/packages/e5/de/6e05f5c59262a584e502dd3d261bbdd2c97ab5416cc9c0b91ea38932a901/greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e", size = 652813 }, + { url = "https://files.pythonhosted.org/packages/49/93/d5f93c84241acdea15a8fd329362c2c71c79e1a507c3f142a5d67ea435ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1", size = 648517 }, + { url = "https://files.pythonhosted.org/packages/15/85/72f77fc02d00470c86a5c982b8daafdf65d38aefbbe441cebff3bf7037fc/greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383", size = 647831 }, + { url = "https://files.pythonhosted.org/packages/f7/4b/1c9695aa24f808e156c8f4813f685d975ca73c000c2a5056c514c64980f6/greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a", size = 602413 }, + { url = "https://files.pythonhosted.org/packages/76/70/ad6e5b31ef330f03b12559d19fda2606a522d3849cde46b24f223d6d1619/greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511", size = 1129619 }, + { url = "https://files.pythonhosted.org/packages/f4/fb/201e1b932e584066e0f0658b538e73c459b34d44b4bd4034f682423bc801/greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395", size = 1155198 }, + { url = "https://files.pythonhosted.org/packages/12/da/b9ed5e310bb8b89661b80cbcd4db5a067903bbcd7fc854923f5ebb4144f0/greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39", size = 298930 }, + { url = "https://files.pythonhosted.org/packages/7d/ec/bad1ac26764d26aa1353216fcbfa4670050f66d445448aafa227f8b16e80/greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", size = 274260 }, + { url = "https://files.pythonhosted.org/packages/66/d4/c8c04958870f482459ab5956c2942c4ec35cac7fe245527f1039837c17a9/greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", size = 649064 }, + { url = "https://files.pythonhosted.org/packages/51/41/467b12a8c7c1303d20abcca145db2be4e6cd50a951fa30af48b6ec607581/greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", size = 663420 }, + { url = "https://files.pythonhosted.org/packages/27/8f/2a93cd9b1e7107d5c7b3b7816eeadcac2ebcaf6d6513df9abaf0334777f6/greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441", size = 658035 }, + { url = "https://files.pythonhosted.org/packages/57/5c/7c6f50cb12be092e1dccb2599be5a942c3416dbcfb76efcf54b3f8be4d8d/greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36", size = 660105 }, + { url = "https://files.pythonhosted.org/packages/f1/66/033e58a50fd9ec9df00a8671c74f1f3a320564c6415a4ed82a1c651654ba/greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9", size = 613077 }, + { url = "https://files.pythonhosted.org/packages/19/c5/36384a06f748044d06bdd8776e231fadf92fc896bd12cb1c9f5a1bda9578/greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0", size = 1135975 }, + { url = "https://files.pythonhosted.org/packages/38/f9/c0a0eb61bdf808d23266ecf1d63309f0e1471f284300ce6dac0ae1231881/greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942", size = 1163955 }, + { url = "https://files.pythonhosted.org/packages/43/21/a5d9df1d21514883333fc86584c07c2b49ba7c602e670b174bd73cfc9c7f/greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01", size = 299655 }, + { url = "https://files.pythonhosted.org/packages/f3/57/0db4940cd7bb461365ca8d6fd53e68254c9dbbcc2b452e69d0d41f10a85e/greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", size = 272990 }, + { url = "https://files.pythonhosted.org/packages/1c/ec/423d113c9f74e5e402e175b157203e9102feeb7088cee844d735b28ef963/greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", size = 649175 }, + { url = "https://files.pythonhosted.org/packages/a9/46/ddbd2db9ff209186b7b7c621d1432e2f21714adc988703dbdd0e65155c77/greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", size = 663425 }, + { url = "https://files.pythonhosted.org/packages/bc/f9/9c82d6b2b04aa37e38e74f0c429aece5eeb02bab6e3b98e7db89b23d94c6/greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e", size = 657736 }, + { url = "https://files.pythonhosted.org/packages/d9/42/b87bc2a81e3a62c3de2b0d550bf91a86939442b7ff85abb94eec3fc0e6aa/greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4", size = 660347 }, + { url = "https://files.pythonhosted.org/packages/37/fa/71599c3fd06336cdc3eac52e6871cfebab4d9d70674a9a9e7a482c318e99/greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e", size = 615583 }, + { url = "https://files.pythonhosted.org/packages/4e/96/e9ef85de031703ee7a4483489b40cf307f93c1824a02e903106f2ea315fe/greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1", size = 1133039 }, + { url = "https://files.pythonhosted.org/packages/87/76/b2b6362accd69f2d1889db61a18c94bc743e961e3cab344c2effaa4b4a25/greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c", size = 1160716 }, + { url = "https://files.pythonhosted.org/packages/1f/1b/54336d876186920e185066d8c3024ad55f21d7cc3683c856127ddb7b13ce/greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761", size = 299490 }, + { url = "https://files.pythonhosted.org/packages/5f/17/bea55bf36990e1638a2af5ba10c1640273ef20f627962cf97107f1e5d637/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011", size = 643731 }, + { url = "https://files.pythonhosted.org/packages/78/d2/aa3d2157f9ab742a08e0fd8f77d4699f37c22adfbfeb0c610a186b5f75e0/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13", size = 649304 }, + { url = "https://files.pythonhosted.org/packages/f1/8e/d0aeffe69e53ccff5a28fa86f07ad1d2d2d6537a9506229431a2a02e2f15/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475", size = 646537 }, + { url = "https://files.pythonhosted.org/packages/05/79/e15408220bbb989469c8871062c97c6c9136770657ba779711b90870d867/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b", size = 642506 }, + { url = "https://files.pythonhosted.org/packages/18/87/470e01a940307796f1d25f8167b551a968540fbe0551c0ebb853cb527dd6/greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822", size = 602753 }, + { url = "https://files.pythonhosted.org/packages/e2/72/576815ba674eddc3c25028238f74d7b8068902b3968cbe456771b166455e/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", size = 1122731 }, + { url = "https://files.pythonhosted.org/packages/ac/38/08cc303ddddc4b3d7c628c3039a61a3aae36c241ed01393d00c2fd663473/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", size = 1142112 }, + { url = "https://files.pythonhosted.org/packages/97/83/bdf5f69fcf304065ec7cf8fc7c08248479cfed9bcca02bf0001c07e000aa/greenlet-3.1.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:346bed03fe47414091be4ad44786d1bd8bef0c3fcad6ed3dee074a032ab408a9", size = 271017 }, + { url = "https://files.pythonhosted.org/packages/31/4a/2d4443adcb38e1e90e50c653a26b2be39998ea78ca1a4cf414dfdeb2e98b/greenlet-3.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfc59d69fc48664bc693842bd57acfdd490acafda1ab52c7836e3fc75c90a111", size = 642888 }, + { url = "https://files.pythonhosted.org/packages/5a/c9/b5d9ac1b932aa772dd1eb90a8a2b30dbd7ad5569dcb7fdac543810d206b4/greenlet-3.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21e10da6ec19b457b82636209cbe2331ff4306b54d06fa04b7c138ba18c8a81", size = 655451 }, + { url = "https://files.pythonhosted.org/packages/a8/18/218e21caf7caba5b2236370196eaebc00987d4a2b2d3bf63cc4d4dd5a69f/greenlet-3.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37b9de5a96111fc15418819ab4c4432e4f3c2ede61e660b1e33971eba26ef9ba", size = 651409 }, + { url = "https://files.pythonhosted.org/packages/a7/25/de419a2b22fa6e18ce3b2a5adb01d33ec7b2784530f76fa36ba43d8f0fac/greenlet-3.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef9ea3f137e5711f0dbe5f9263e8c009b7069d8a1acea822bd5e9dae0ae49c8", size = 650661 }, + { url = "https://files.pythonhosted.org/packages/d8/88/0ce16c0afb2d71d85562a7bcd9b092fec80a7767ab5b5f7e1bbbca8200f8/greenlet-3.1.1-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85f3ff71e2e60bd4b4932a043fbbe0f499e263c628390b285cb599154a3b03b1", size = 605959 }, + { url = "https://files.pythonhosted.org/packages/5a/10/39a417ad0afb0b7e5b150f1582cdeb9416f41f2e1df76018434dfac4a6cc/greenlet-3.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:95ffcf719966dd7c453f908e208e14cde192e09fde6c7186c8f1896ef778d8cd", size = 1132341 }, + { url = "https://files.pythonhosted.org/packages/9f/f5/e9b151ddd2ed0508b7a47bef7857e46218dbc3fd10e564617a3865abfaac/greenlet-3.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:03a088b9de532cbfe2ba2034b2b85e82df37874681e8c470d6fb2f8c04d7e4b7", size = 1159409 }, + { url = "https://files.pythonhosted.org/packages/86/97/2c86989ca4e0f089fbcdc9229c972a01ef53abdafd5ae89e0f3dcdcd4adb/greenlet-3.1.1-cp38-cp38-win32.whl", hash = "sha256:8b8b36671f10ba80e159378df9c4f15c14098c4fd73a36b9ad715f057272fbef", size = 281126 }, + { url = "https://files.pythonhosted.org/packages/d3/50/7b7a3e10ed82c760c1fd8d3167a7c95508e9fdfc0b0604f05ed1a9a9efdc/greenlet-3.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:7017b2be767b9d43cc31416aba48aab0d2309ee31b4dbf10a1d38fb7972bdf9d", size = 298285 }, + { url = "https://files.pythonhosted.org/packages/8c/82/8051e82af6d6b5150aacb6789a657a8afd48f0a44d8e91cb72aaaf28553a/greenlet-3.1.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3", size = 270027 }, + { url = "https://files.pythonhosted.org/packages/f9/74/f66de2785880293780eebd18a2958aeea7cbe7814af1ccef634f4701f846/greenlet-3.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42", size = 634822 }, + { url = "https://files.pythonhosted.org/packages/68/23/acd9ca6bc412b02b8aa755e47b16aafbe642dde0ad2f929f836e57a7949c/greenlet-3.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f", size = 646866 }, + { url = "https://files.pythonhosted.org/packages/a9/ab/562beaf8a53dc9f6b2459f200e7bc226bb07e51862a66351d8b7817e3efd/greenlet-3.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437", size = 641985 }, + { url = "https://files.pythonhosted.org/packages/03/d3/1006543621f16689f6dc75f6bcf06e3c23e044c26fe391c16c253623313e/greenlet-3.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145", size = 641268 }, + { url = "https://files.pythonhosted.org/packages/2f/c1/ad71ce1b5f61f900593377b3f77b39408bce5dc96754790311b49869e146/greenlet-3.1.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c", size = 597376 }, + { url = "https://files.pythonhosted.org/packages/f7/ff/183226685b478544d61d74804445589e069d00deb8ddef042699733950c7/greenlet-3.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e", size = 1123359 }, + { url = "https://files.pythonhosted.org/packages/c0/8b/9b3b85a89c22f55f315908b94cd75ab5fed5973f7393bbef000ca8b2c5c1/greenlet-3.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e", size = 1147458 }, + { url = "https://files.pythonhosted.org/packages/b8/1c/248fadcecd1790b0ba793ff81fa2375c9ad6442f4c748bf2cc2e6563346a/greenlet-3.1.1-cp39-cp39-win32.whl", hash = "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c", size = 281131 }, + { url = "https://files.pythonhosted.org/packages/ae/02/e7d0aef2354a38709b764df50b2b83608f0621493e47f47694eb80922822/greenlet-3.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22", size = 298306 }, +] + +[[package]] +name = "h11" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, +] + +[[package]] +name = "identify" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/29/bb/25024dbcc93516c492b75919e76f389bac754a3e4248682fba32b250c880/identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98", size = 99097 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/0c/4ef72754c050979fdcc06c744715ae70ea37e734816bb6514f79df77a42f/identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0", size = 98972 }, +] + +[[package]] +name = "identify" +version = "2.6.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/92/69934b9ef3c31ca2470980423fda3d00f0460ddefdf30a67adf7f17e2e00/identify-2.6.5.tar.gz", hash = "sha256:c10b33f250e5bba374fae86fb57f3adcebf1161bce7cdf92031915fd480c13bc", size = 99213 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/fa/dce098f4cdf7621aa8f7b4f919ce545891f489482f0bfa5102f3eca8608b/identify-2.6.5-py2.py3-none-any.whl", hash = "sha256:14181a47091eb75b337af4c23078c9d09225cd4c48929f521f3bf16b09d02566", size = 99078 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "imagesize" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769 }, +] + +[[package]] +name = "importlib-metadata" +version = "4.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", version = "3.20.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "zipp", version = "3.21.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/16/441080c907df829016729e71d8bdd42d99b9bdde48b01492ed08912c0aa9/importlib_metadata-4.12.0.tar.gz", hash = "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670", size = 48153 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/a2/8c239dc898138f208dd14b441b196e7b3032b94d3137d9d8453e186967fc/importlib_metadata-4.12.0-py3-none-any.whl", hash = "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23", size = 21704 }, +] + +[[package]] +name = "importlib-resources" +version = "5.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", version = "3.20.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "zipp", version = "3.21.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/f1/8711c49ffd121083007a24c1bff0d324c9ff621d4fdf8b4ffcb8d9e60330/importlib_resources-5.13.0.tar.gz", hash = "sha256:82d5c6cca930697dbbd86c93333bb2c2e72861d4789a11c2662b933e5ad2b528", size = 36550 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/68/bd9dd6bbf06772c7accce77d0354d783333fbe712a60b08fc13540c05422/importlib_resources-5.13.0-py3-none-any.whl", hash = "sha256:9f7bd0c97b79972a6cce36a366356d16d5e13b09679c11a58f1014bfdf8e64b2", size = 32912 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "ipykernel" +version = "6.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appnope", marker = "sys_platform == 'darwin'" }, + { name = "debugpy" }, + { name = "ipython" }, + { name = "jupyter-client" }, + { name = "matplotlib-inline" }, + { name = "nest-asyncio" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/23/374fc7d075dd0683ba0e3c95490c72856cf13e809f9957d57bab5f946097/ipykernel-6.12.1.tar.gz", hash = "sha256:0868f5561729ade444011f8ca7d3502dc9f27f7f44e20f1d5fee7e1f2b7183a1", size = 129979 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/43/2ab8ba68d5e2c19e16d3c85685be1b28c2e57f1e2908732e9a76bf1bb5d9/ipykernel-6.12.1-py3-none-any.whl", hash = "sha256:d840e3bf1c4b23bf6939f78dcdae639c9f6962e41d17e1c084a18c3c7f972d3a", size = 130866 }, +] + +[[package]] +name = "ipython" +version = "7.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appnope", marker = "sys_platform == 'darwin'" }, + { name = "backcall" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'win32'" }, + { name = "pickleshare" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "setuptools", version = "75.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "setuptools", version = "75.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/6c/3fcf0b8ee46656796099ac4b7b72497af5f090da3e43fd305f2a24c73915/ipython-7.34.0.tar.gz", hash = "sha256:af3bdb46aa292bce5615b1b2ebc76c2080c5f77f54bda2ec72461317273e7cd6", size = 5158632 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/6a/1f1365f4bf9fcb349fcaa5b61edfcefa721aa13ff37c5631296b12fab8e5/ipython-7.34.0-py3-none-any.whl", hash = "sha256:c175d2440a1caff76116eb719d40538fbb316e214eda85c5515c303aacbfb23e", size = 793790 }, +] + +[[package]] +name = "ipython-genutils" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/69/fbeffffc05236398ebfcfb512b6d2511c622871dca1746361006da310399/ipython_genutils-0.2.0.tar.gz", hash = "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8", size = 22208 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/bc/9bd3b5c2b4774d5f33b2d544f1460be9df7df2fe42f352135381c347c69a/ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8", size = 26343 }, +] + +[[package]] +name = "ipywidgets" +version = "7.8.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "comm" }, + { name = "ipython" }, + { name = "ipython-genutils" }, + { name = "jupyterlab-widgets" }, + { name = "traitlets" }, + { name = "widgetsnbextension" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/93/69f9fbe1a9fdec370670a2ee49eee3f8fb3d40203653692954a774c4081b/ipywidgets-7.8.5.tar.gz", hash = "sha256:927439399d75f59f43864c13d7e73b05a4de522d3ea09d6048adc5c583b55c3b", size = 4075909 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/22/f48bd3af0ef7c72dce13f9930a4483acbc7c77373834700296495ea0875f/ipywidgets-7.8.5-py2.py3-none-any.whl", hash = "sha256:8055fe314edd4c101a5f1ea230620ef5e315b0ca87f940264b4eac1faf9746ef", size = 124086 }, +] + +[[package]] +name = "jedi" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278 }, +] + +[[package]] +name = "jinja2" +version = "2.11.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4f/e7/65300e6b32e69768ded990494809106f87da1d436418d5f1367ed3966fd7/Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6", size = 257589 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/c2/1eece8c95ddbc9b1aeb64f5783a9e07a286de42191b7204d67b7496ddf35/Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", size = 125699 }, +] + +[[package]] +name = "json5" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/3d/bbe62f3d0c05a689c711cff57b2e3ac3d3e526380adb7c781989f075115c/json5-0.10.0.tar.gz", hash = "sha256:e66941c8f0a02026943c52c2eb34ebeb2a6f819a0be05920a6f5243cd30fd559", size = 48202 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/42/797895b952b682c3dafe23b1834507ee7f02f4d6299b65aaa61425763278/json5-0.10.0-py3-none-any.whl", hash = "sha256:19b23410220a7271e8377f81ba8aacba2fdd56947fbb137ee5977cbe1f5e8dfa", size = 34049 }, +] + +[[package]] +name = "jsonschema" +version = "4.17.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "importlib-resources", marker = "python_full_version < '3.9'" }, + { name = "pkgutil-resolve-name", marker = "python_full_version < '3.9'" }, + { name = "pyrsistent" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/3d/ca032d5ac064dff543aa13c984737795ac81abc9fb130cd2fcff17cfabc7/jsonschema-4.17.3.tar.gz", hash = "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d", size = 297785 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/97/c698bd9350f307daad79dd740806e1a59becd693bd11443a0f531e3229b3/jsonschema-4.17.3-py3-none-any.whl", hash = "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6", size = 90379 }, +] + +[[package]] +name = "jupyter" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipykernel" }, + { name = "ipywidgets" }, + { name = "jupyter-console" }, + { name = "jupyterlab" }, + { name = "nbconvert" }, + { name = "notebook" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/f3/af28ea964ab8bc1e472dba2e82627d36d470c51f5cd38c37502eeffaa25e/jupyter-1.1.1.tar.gz", hash = "sha256:d55467bceabdea49d7e3624af7e33d59c37fff53ed3a350e1ac957bed731de7a", size = 5714959 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/64/285f20a31679bf547b75602702f7800e74dbabae36ef324f716c02804753/jupyter-1.1.1-py2.py3-none-any.whl", hash = "sha256:7a59533c22af65439b24bbe60373a4e95af8f16ac65a6c00820ad378e3f7cc83", size = 2657 }, +] + +[[package]] +name = "jupyter-cache" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "nbclient" }, + { name = "nbdime" }, + { name = "nbformat" }, + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/cd/43a393cd0e5a5019598bf899c3ccfac4b8ac92b6b47d25980a44cc1a3ec3/jupyter-cache-0.4.3.tar.gz", hash = "sha256:4c9b5431b1d320bc68440c21fa0a155bbeb29c5b979bef72222e244a7bcd54fc", size = 29068 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/b1/5be5e126e5afb004a487443b21f5f39642f471323ca80ac17b1edd62696a/jupyter_cache-0.4.3-py3-none-any.whl", hash = "sha256:6d5d662d81f565d18009e8dcfd3a56fb876af47eafead2a19ef0045aba8ffe3b", size = 31668 }, +] + +[[package]] +name = "jupyter-client" +version = "8.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jupyter-core" }, + { name = "python-dateutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/22/bf9f12fdaeae18019a468b68952a60fe6dbab5d67cd2a103cac7659b41ca/jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419", size = 342019 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/85/b0394e0b6fcccd2c1eeefc230978a6f8cb0c5df1e4cd3e7625735a0d7d1e/jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f", size = 106105 }, +] + +[[package]] +name = "jupyter-console" +version = "6.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipykernel" }, + { name = "ipython" }, + { name = "jupyter-client" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/2f/acb5851aa3ed730f8cde5ec9eb0c0d9681681123f32c3b82d1536df1e937/jupyter_console-6.4.4.tar.gz", hash = "sha256:172f5335e31d600df61613a97b7f0352f2c8250bbd1092ef2d658f77249f89fb", size = 35145 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/0c/f9382ca7b7499c8594a5158817a72c95b4c09a6c6f2de10553bfe8905924/jupyter_console-6.4.4-py3-none-any.whl", hash = "sha256:756df7f4f60c986e7bc0172e4493d3830a7e6e75c08750bbe59c0a5403ad6dee", size = 22825 }, +] + +[[package]] +name = "jupyter-core" +version = "5.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "platformdirs" }, + { name = "pywin32", marker = "platform_python_implementation != 'PyPy' and sys_platform == 'win32'" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/11/b56381fa6c3f4cc5d2cf54a7dbf98ad9aa0b339ef7a601d6053538b079a7/jupyter_core-5.7.2.tar.gz", hash = "sha256:aa5f8d32bbf6b431ac830496da7392035d6f61b4f54872f15c4bd2a9c3f536d9", size = 87629 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/fb/108ecd1fe961941959ad0ee4e12ee7b8b1477247f30b1fdfd83ceaf017f0/jupyter_core-5.7.2-py3-none-any.whl", hash = "sha256:4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409", size = 28965 }, +] + +[[package]] +name = "jupyter-server" +version = "1.15.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", version = "4.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "anyio", version = "4.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "argon2-cffi" }, + { name = "jinja2" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "nbconvert" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "prometheus-client" }, + { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "pyzmq" }, + { name = "send2trash" }, + { name = "terminado" }, + { name = "tornado" }, + { name = "traitlets" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/5f/d8bb2cc084ade361e881b1e13fbdcd11be647dff27c23da24f57127b47fe/jupyter_server-1.15.6.tar.gz", hash = "sha256:56bd6f580d1f46b62294990e8e78651025729f5d3fc798f10f2c03f0cdcbf28d", size = 440539 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/24/007feec44ac711b396443d9b2ad07aeb39aa9412450bd9644fae5f0fe440/jupyter_server-1.15.6-py3-none-any.whl", hash = "sha256:e393934c19fcc324a7fca77f811eacd91201440f04c6fbb15c959c463baaa9c5", size = 341521 }, +] + +[[package]] +name = "jupyter-server-mathjax" +version = "0.2.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-server" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/40/9a1b8c2a2e44e8e2392174cd8e52e0c976335f004301f61b66addea3243e/jupyter_server_mathjax-0.2.6.tar.gz", hash = "sha256:bb1e6b6dc0686c1fe386a22b5886163db548893a99c2810c36399e9c4ca23943", size = 2648665 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/77/6a98cc88f1061c0206b427b602efb6fcb9bc369e958aee11676d5cfc4412/jupyter_server_mathjax-0.2.6-py3-none-any.whl", hash = "sha256:416389dde2010df46d5fbbb7adb087a5607111070af65a1445391040f2babb5e", size = 3120990 }, +] + +[[package]] +name = "jupyter-sphinx" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipython" }, + { name = "ipywidgets" }, + { name = "nbconvert" }, + { name = "nbformat" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/b0/cc381afa960b7af1b4abac58abbedc0fd93d8805d422acd5d2b26682744f/jupyter_sphinx-0.3.1.tar.gz", hash = "sha256:c4caf8bbf2be6edfe0319aa76127d17fdbe6927c8189cda2d6ac59c01f38404b", size = 16686 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/0f/3fedf88d1e5ac7b74e26a0f99f3e1c242e45484c5c0a7487b51e151d09f2/jupyter_sphinx-0.3.1-py3-none-any.whl", hash = "sha256:56f4cd319b96c491c61bfa9d11a2ee452d2758beecbd2723b23916aaac4c2bab", size = 19781 }, +] + +[[package]] +name = "jupyterlab" +version = "3.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipython" }, + { name = "jinja2" }, + { name = "jupyter-core" }, + { name = "jupyter-server" }, + { name = "jupyterlab-server" }, + { name = "nbclassic" }, + { name = "packaging" }, + { name = "tornado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/3e/45f639b1f4ec070cc1c30c5cf26890e78c04d9ad9b2a3f02e1f2918152d2/jupyterlab-3.3.4.tar.gz", hash = "sha256:e04355848b3d91ac4d95c2e3846a0429b33e9c2edc79668fb4fc4d212f1e5107", size = 17147292 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/39/b136ca650aad266bca5438765b378dfcf0588d958579c1fd721e0d11cdc0/jupyterlab-3.3.4-py3-none-any.whl", hash = "sha256:87121636963027a0477e50ea8f366acf1ab06bb05d7e581cd2ec8c00f6e741a5", size = 8709619 }, +] + +[[package]] +name = "jupyterlab-server" +version = "2.10.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "entrypoints" }, + { name = "jinja2" }, + { name = "json5" }, + { name = "jsonschema" }, + { name = "jupyter-server" }, + { name = "packaging" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/de/a026fd6391b7b2ab54c105c12076798fe8fcd6ea0b36294c54946cb7662f/jupyterlab_server-2.10.3.tar.gz", hash = "sha256:3fb84a5813d6d836ceda773fb2d4e9ef3c7944dbc1b45a8d59d98641a80de80a", size = 59522 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/22/308fdf317ed12c7f8e6081797bcccc53de3c7a34d89cbf975069194f7c41/jupyterlab_server-2.10.3-py3-none-any.whl", hash = "sha256:62f3c598f1d48dfb9b27729ed17772e38115cbe61e7d60fe68a853791bdf1038", size = 61027 }, +] + +[[package]] +name = "jupyterlab-widgets" +version = "1.1.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/5b/6c143cba68064854667da6d86981ba25c9f1b032e7abfedace5790b747f2/jupyterlab_widgets-1.1.11.tar.gz", hash = "sha256:414cdbcd99db6e8f1174c7e4ed49c6ba368779f4659806fb1d824f3c377218e4", size = 121441 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/73/4f281e0f86bf532baec742da40ededf8b22dc224523e38c638fb6ad91255/jupyterlab_widgets-1.1.11-py3-none-any.whl", hash = "sha256:840e538021d87e020a8e7b786597f088431f4ebd8308655555e126c3950a1b27", size = 246921 }, +] + +[[package]] +name = "kiwipy" +version = "0.8.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecation" }, + { name = "pyyaml" }, + { name = "shortuuid" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/c9/60f4597b2f7ce9f1ce9f202c1ddc70b857716597d828fc5baa123a2fa17e/kiwipy-0.8.5.tar.gz", hash = "sha256:23b746f60577120764d662673335cea40cf34083d15f1ee8ab4fa6155b50d60f", size = 41087 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/50/2d180b54272d467a3e5eb4d7e64df80a8bb11d483e908404d71905a2801b/kiwipy-0.8.5-py3-none-any.whl", hash = "sha256:b6acf17ba69fdfc9ce246673efd35e1db06a27b2c624ba1735d2159f8e665a1b", size = 41820 }, +] + +[package.optional-dependencies] +docs = [ + { name = "docutils", version = "0.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.9.*'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "jupyter" }, + { name = "nbsphinx" }, + { name = "pandoc" }, + { name = "sphinx" }, + { name = "sphinx-autobuild", version = "2021.3.14", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "sphinx-autobuild", version = "2024.10.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +rmq = [ + { name = "aio-pika" }, + { name = "pamqp" }, + { name = "pytray" }, +] + +[[package]] +name = "livereload" +version = "2.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tornado", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/6e/f2748665839812a9bbe5c75d3f983edbf3ab05fa5cd2f7c2f36fffdf65bd/livereload-2.7.1.tar.gz", hash = "sha256:3d9bf7c05673df06e32bea23b494b8d36ca6d10f7d5c3c8a6989608c09c986a9", size = 22255 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/3e/de54dc7f199e85e6ca37e2e5dae2ec3bce2151e9e28f8eb9076d71e83d56/livereload-2.7.1-py3-none-any.whl", hash = "sha256:5201740078c1b9433f4b2ba22cd2729a39b9d0ec0a2cc6b4d3df257df5ad0564", size = 22657 }, +] + +[[package]] +name = "markdown-it-py" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "mdit-py-plugins" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/c0/8b6e358df933d68c7cc7202ed454eab6a411b792796646e4ced4a998a47d/markdown-it-py-0.6.2.tar.gz", hash = "sha256:c3b9f995be0792cbbc8ab2f53d74072eb7ff8a8b622be8d61d38ab879709eca3", size = 55904 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/cb/8493188845d26599170268bb0e0a63e75584d5e7f130488c641e96449cd7/markdown_it_py-0.6.2-py3-none-any.whl", hash = "sha256:30b3e9f8198dc82a5df0dcb73fd31d56cd9a43bf8a747feb10b2ba74f962bcb1", size = 81687 }, +] + +[[package]] +name = "markupsafe" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/10/ff66fea6d1788c458663a84d88787bae15d45daa16f6b3ef33322a51fc7e/MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", size = 18596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/8b/f28eac2790d49dde61f89ae9e007ac65002edc90bb2dd63c3b9e653820d2/MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53", size = 18330 }, + { url = "https://files.pythonhosted.org/packages/21/84/e090d999105fe0f3e1d955725ed2c9aeebc649ee83edab0e73d353d47e5d/MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38", size = 14005 }, + { url = "https://files.pythonhosted.org/packages/e6/57/e9d243b12918f22bc3aa1392db7821dcb643a120e87b3f8c9bc7e1ad33f1/MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad", size = 26830 }, + { url = "https://files.pythonhosted.org/packages/5a/98/3303496a5d19aabba67c443ba1df6ee1bec94549b3f8976f90c06a6942e6/MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d", size = 30410 }, + { url = "https://files.pythonhosted.org/packages/53/e8/601efa63c4058311a8bda7984a2fe554b9da574044967d7aee253661ee46/MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646", size = 30770 }, + { url = "https://files.pythonhosted.org/packages/a4/c8/9d2161b2080cb69c8834d1c34a399685347523acbfc923b203ad27bf1215/MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b", size = 31170 }, + { url = "https://files.pythonhosted.org/packages/51/c3/7154db2b7d5b24875e1f1c42bab87a46af688bd6a5c89a90c60053cb6b33/MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a", size = 30127 }, + { url = "https://files.pythonhosted.org/packages/04/69/c31e837e4bb5532b02d297152464b2cb8a0edeb9bef762c015e9b4e95e16/MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a", size = 30553 }, + { url = "https://files.pythonhosted.org/packages/c1/39/9df65c006a88fce7bbd5ec3195b949b79477b1a325564f486c611c367893/MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28", size = 14331 }, + { url = "https://files.pythonhosted.org/packages/93/28/d42b954fb9189cf4b78b0b0a025cff9b2583f93b37d1a345768ade29e5dd/MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134", size = 15042 }, + { url = "https://files.pythonhosted.org/packages/51/1e/45e25cd867fb79339c49086dad9794e11923dd6325251ae48c341b0a4271/MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9", size = 18323 }, + { url = "https://files.pythonhosted.org/packages/70/56/f81c0cfbc22882df36358ecdedc5474571183e5a5adde1e237079acee437/MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", size = 13982 }, + { url = "https://files.pythonhosted.org/packages/f9/12/b63afcb3bf9f27fd347adef452f9a6e27dfe7107a8f2685afacc8e9c6592/MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", size = 30224 }, + { url = "https://files.pythonhosted.org/packages/1d/c5/1d1b42c65f96ee7b0c81761260878d1a1dc0afdf259e434b7d7af88a80a3/MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", size = 30562 }, + { url = "https://files.pythonhosted.org/packages/92/ac/94771b65ac9f77cf37e43b38516697bbc4e128ee152b68d596ae44c6c896/MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", size = 30226 }, + { url = "https://files.pythonhosted.org/packages/68/ba/7a5ca0f9b4239e6fd846dd54c0b5928187355fa62fbdbd13e1c5942afae7/MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", size = 30563 }, + { url = "https://files.pythonhosted.org/packages/eb/3b/1cddaf0338a031ef5c2e1d9d74f2d607d564748a933b44de6edfe7a2a880/MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", size = 31869 }, + { url = "https://files.pythonhosted.org/packages/e4/9b/c7b55a2f587368d69eb6dc36e285010ab0bbb74323833d501921e08e2728/MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b", size = 27085 }, + { url = "https://files.pythonhosted.org/packages/cc/f2/854d33eee85df681e61e22b52d8e83bef8b7425c0b9826212289f7885710/MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a", size = 30239 }, + { url = "https://files.pythonhosted.org/packages/7a/e8/00c435416c9b0238dca6f883563b01c4cc532b2ba6aaf7268081f6238520/MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6", size = 30603 }, + { url = "https://files.pythonhosted.org/packages/95/18/b7a45c16635acafdf6837a6fd4c71acfe5bad202884c6fcbae4ea0763dde/MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f", size = 30964 }, + { url = "https://files.pythonhosted.org/packages/15/90/b63743e72c9ffc5988c7b1c04d14f9a32ae49574afe8a7fbea0ce538bda4/MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194", size = 29982 }, + { url = "https://files.pythonhosted.org/packages/1f/44/ada8e01854175525e8e139278c3a52fec0ef720307cbd670bca86b473b56/MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee", size = 30372 }, + { url = "https://files.pythonhosted.org/packages/44/e6/4e1f202ec01062c8b4d03af72f1aeb2ca8fc97f9f5d95b9173302ac4e5ad/MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", size = 14303 }, + { url = "https://files.pythonhosted.org/packages/30/9e/4b7116f464a0151b86ce42b5185941eb74c207b38fe033f71f5e5d150356/MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", size = 14910 }, + { url = "https://files.pythonhosted.org/packages/dd/8f/d0c570c851f70377ca6f344531fab4b6b01a99a9d2a801b25d6fd75525e5/MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", size = 18308 }, + { url = "https://files.pythonhosted.org/packages/ce/a7/835a636047f4bb4fea31a682c18affad9795e864d800892bd7248485425e/MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", size = 13982 }, + { url = "https://files.pythonhosted.org/packages/66/66/b5891704372c9f5d97432933bdd7e9b5a0647fad9170c72bb7f486550c43/MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", size = 30216 }, + { url = "https://files.pythonhosted.org/packages/50/99/06eccf68be0bff67ab9a0b90b5382c04769f9ad2e42cae5e5e92f99380cd/MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", size = 30553 }, + { url = "https://files.pythonhosted.org/packages/5a/ff/34bdcd8cc794f692588de0b3f4c1aa7ec0d17716fda9d874836ed68775c1/MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", size = 30220 }, + { url = "https://files.pythonhosted.org/packages/6f/83/eabfb8c6d60b096dc9ada378cf935809289c4d0327b74a60789bde77e1db/MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", size = 30556 }, + { url = "https://files.pythonhosted.org/packages/ae/70/8dd5f2c0aab82431c9c619a2c4fbd1742fc0fb769d8d7b275ae1d03eb3a5/MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", size = 31882 }, + { url = "https://files.pythonhosted.org/packages/a6/d1/a7b97d0e000336c4e06bfce7e08dcb2b47fc5091146ee883dfac6cb4842e/MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1", size = 26719 }, + { url = "https://files.pythonhosted.org/packages/67/e9/579a3ad8d48f7680f887ff1f22cc6330f083de23ce32a8fa35f8acef477a/MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac", size = 30251 }, + { url = "https://files.pythonhosted.org/packages/c2/db/314df69668f582d5173922bded7b58126044bb77cfce6347c5d992074d2e/MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6", size = 30586 }, + { url = "https://files.pythonhosted.org/packages/8f/87/4668ce3963e942a9aa7b13212158e74bf063a2461138b7ed5a043ac6aa79/MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047", size = 30980 }, + { url = "https://files.pythonhosted.org/packages/a7/55/a576835b6b95af21d15f69eaf14c4fb1358fd48475f2b9813abd9654132e/MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e", size = 29902 }, + { url = "https://files.pythonhosted.org/packages/3b/41/f53e2ac439b309d8bb017d12ee6e7d393aa70c508448c1f30a7e5db9d69e/MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1", size = 30340 }, + { url = "https://files.pythonhosted.org/packages/6a/96/7a23b44f742384a866173502e19cc1ec13951085bbb4e24be504dfc6da9f/MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", size = 14331 }, + { url = "https://files.pythonhosted.org/packages/5b/db/49785acd523bd5eef83d0e21594eec1c2d7d45afc473dcc85037243de673/MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", size = 14937 }, +] + +[[package]] +name = "matplotlib-inline" +version = "0.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899 }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.2.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/28/1ba872560eef4bd28873c53e1ecd63fc70ca971054055e18c1c891576901/mdit-py-plugins-0.2.6.tar.gz", hash = "sha256:1e467ca2ea056e8065cbd5d6c61e5052bb50826bde84c40f6a5ed77e82125710", size = 25661 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/31/f0ecaccf7cd2db17332a94852f190840167c3cb7eadf09efe498412f909a/mdit_py_plugins-0.2.6-py3-none-any.whl", hash = "sha256:77fd75dad81109ee91f30eb49146196f79afbbae041f298ae4886c8c2b5e23d7", size = 39287 }, +] + +[[package]] +name = "mistune" +version = "0.8.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/a4/509f6e7783ddd35482feda27bc7f72e65b5e7dc910eca4ab2164daf9c577/mistune-0.8.4.tar.gz", hash = "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e", size = 58322 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/ec/4b43dae793655b7d8a25f76119624350b4d65eb663459eb9603d7f1f0345/mistune-0.8.4-py2.py3-none-any.whl", hash = "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4", size = 16220 }, +] + +[[package]] +name = "multidict" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/be/504b89a5e9ca731cd47487e91c469064f8ae5af93b7259758dcfc2b9c848/multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a", size = 64002 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/68/259dee7fd14cf56a17c554125e534f6274c2860159692a414d0b402b9a6d/multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60", size = 48628 }, + { url = "https://files.pythonhosted.org/packages/50/79/53ba256069fe5386a4a9e80d4e12857ced9de295baf3e20c68cdda746e04/multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1", size = 29327 }, + { url = "https://files.pythonhosted.org/packages/ff/10/71f1379b05b196dae749b5ac062e87273e3f11634f447ebac12a571d90ae/multidict-6.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53", size = 29689 }, + { url = "https://files.pythonhosted.org/packages/71/45/70bac4f87438ded36ad4793793c0095de6572d433d98575a5752629ef549/multidict-6.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5", size = 126639 }, + { url = "https://files.pythonhosted.org/packages/80/cf/17f35b3b9509b4959303c05379c4bfb0d7dd05c3306039fc79cf035bbac0/multidict-6.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581", size = 134315 }, + { url = "https://files.pythonhosted.org/packages/ef/1f/652d70ab5effb33c031510a3503d4d6efc5ec93153562f1ee0acdc895a57/multidict-6.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56", size = 129471 }, + { url = "https://files.pythonhosted.org/packages/a6/64/2dd6c4c681688c0165dea3975a6a4eab4944ea30f35000f8b8af1df3148c/multidict-6.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429", size = 124585 }, + { url = "https://files.pythonhosted.org/packages/87/56/e6ee5459894c7e554b57ba88f7257dc3c3d2d379cb15baaa1e265b8c6165/multidict-6.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748", size = 116957 }, + { url = "https://files.pythonhosted.org/packages/36/9e/616ce5e8d375c24b84f14fc263c7ef1d8d5e8ef529dbc0f1df8ce71bb5b8/multidict-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db", size = 128609 }, + { url = "https://files.pythonhosted.org/packages/8c/4f/4783e48a38495d000f2124020dc96bacc806a4340345211b1ab6175a6cb4/multidict-6.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056", size = 123016 }, + { url = "https://files.pythonhosted.org/packages/3e/b3/4950551ab8fc39862ba5e9907dc821f896aa829b4524b4deefd3e12945ab/multidict-6.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76", size = 133542 }, + { url = "https://files.pythonhosted.org/packages/96/4d/f0ce6ac9914168a2a71df117935bb1f1781916acdecbb43285e225b484b8/multidict-6.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160", size = 130163 }, + { url = "https://files.pythonhosted.org/packages/be/72/17c9f67e7542a49dd252c5ae50248607dfb780bcc03035907dafefb067e3/multidict-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7", size = 126832 }, + { url = "https://files.pythonhosted.org/packages/71/9f/72d719e248cbd755c8736c6d14780533a1606ffb3fbb0fbd77da9f0372da/multidict-6.1.0-cp310-cp310-win32.whl", hash = "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0", size = 26402 }, + { url = "https://files.pythonhosted.org/packages/04/5a/d88cd5d00a184e1ddffc82aa2e6e915164a6d2641ed3606e766b5d2f275a/multidict-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d", size = 28800 }, + { url = "https://files.pythonhosted.org/packages/93/13/df3505a46d0cd08428e4c8169a196131d1b0c4b515c3649829258843dde6/multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6", size = 48570 }, + { url = "https://files.pythonhosted.org/packages/f0/e1/a215908bfae1343cdb72f805366592bdd60487b4232d039c437fe8f5013d/multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156", size = 29316 }, + { url = "https://files.pythonhosted.org/packages/70/0f/6dc70ddf5d442702ed74f298d69977f904960b82368532c88e854b79f72b/multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb", size = 29640 }, + { url = "https://files.pythonhosted.org/packages/d8/6d/9c87b73a13d1cdea30b321ef4b3824449866bd7f7127eceed066ccb9b9ff/multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b", size = 131067 }, + { url = "https://files.pythonhosted.org/packages/cc/1e/1b34154fef373371fd6c65125b3d42ff5f56c7ccc6bfff91b9b3c60ae9e0/multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72", size = 138507 }, + { url = "https://files.pythonhosted.org/packages/fb/e0/0bc6b2bac6e461822b5f575eae85da6aae76d0e2a79b6665d6206b8e2e48/multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304", size = 133905 }, + { url = "https://files.pythonhosted.org/packages/ba/af/73d13b918071ff9b2205fcf773d316e0f8fefb4ec65354bbcf0b10908cc6/multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351", size = 129004 }, + { url = "https://files.pythonhosted.org/packages/74/21/23960627b00ed39643302d81bcda44c9444ebcdc04ee5bedd0757513f259/multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb", size = 121308 }, + { url = "https://files.pythonhosted.org/packages/8b/5c/cf282263ffce4a596ed0bb2aa1a1dddfe1996d6a62d08842a8d4b33dca13/multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3", size = 132608 }, + { url = "https://files.pythonhosted.org/packages/d7/3e/97e778c041c72063f42b290888daff008d3ab1427f5b09b714f5a8eff294/multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399", size = 127029 }, + { url = "https://files.pythonhosted.org/packages/47/ac/3efb7bfe2f3aefcf8d103e9a7162572f01936155ab2f7ebcc7c255a23212/multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423", size = 137594 }, + { url = "https://files.pythonhosted.org/packages/42/9b/6c6e9e8dc4f915fc90a9b7798c44a30773dea2995fdcb619870e705afe2b/multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3", size = 134556 }, + { url = "https://files.pythonhosted.org/packages/1d/10/8e881743b26aaf718379a14ac58572a240e8293a1c9d68e1418fb11c0f90/multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753", size = 130993 }, + { url = "https://files.pythonhosted.org/packages/45/84/3eb91b4b557442802d058a7579e864b329968c8d0ea57d907e7023c677f2/multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80", size = 26405 }, + { url = "https://files.pythonhosted.org/packages/9f/0b/ad879847ecbf6d27e90a6eabb7eff6b62c129eefe617ea45eae7c1f0aead/multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926", size = 28795 }, + { url = "https://files.pythonhosted.org/packages/fd/16/92057c74ba3b96d5e211b553895cd6dc7cc4d1e43d9ab8fafc727681ef71/multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa", size = 48713 }, + { url = "https://files.pythonhosted.org/packages/94/3d/37d1b8893ae79716179540b89fc6a0ee56b4a65fcc0d63535c6f5d96f217/multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436", size = 29516 }, + { url = "https://files.pythonhosted.org/packages/a2/12/adb6b3200c363062f805275b4c1e656be2b3681aada66c80129932ff0bae/multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761", size = 29557 }, + { url = "https://files.pythonhosted.org/packages/47/e9/604bb05e6e5bce1e6a5cf80a474e0f072e80d8ac105f1b994a53e0b28c42/multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e", size = 130170 }, + { url = "https://files.pythonhosted.org/packages/7e/13/9efa50801785eccbf7086b3c83b71a4fb501a4d43549c2f2f80b8787d69f/multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef", size = 134836 }, + { url = "https://files.pythonhosted.org/packages/bf/0f/93808b765192780d117814a6dfcc2e75de6dcc610009ad408b8814dca3ba/multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95", size = 133475 }, + { url = "https://files.pythonhosted.org/packages/d3/c8/529101d7176fe7dfe1d99604e48d69c5dfdcadb4f06561f465c8ef12b4df/multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925", size = 131049 }, + { url = "https://files.pythonhosted.org/packages/ca/0c/fc85b439014d5a58063e19c3a158a889deec399d47b5269a0f3b6a2e28bc/multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966", size = 120370 }, + { url = "https://files.pythonhosted.org/packages/db/46/d4416eb20176492d2258fbd47b4abe729ff3b6e9c829ea4236f93c865089/multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305", size = 125178 }, + { url = "https://files.pythonhosted.org/packages/5b/46/73697ad7ec521df7de5531a32780bbfd908ded0643cbe457f981a701457c/multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2", size = 119567 }, + { url = "https://files.pythonhosted.org/packages/cd/ed/51f060e2cb0e7635329fa6ff930aa5cffa17f4c7f5c6c3ddc3500708e2f2/multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2", size = 129822 }, + { url = "https://files.pythonhosted.org/packages/df/9e/ee7d1954b1331da3eddea0c4e08d9142da5f14b1321c7301f5014f49d492/multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6", size = 128656 }, + { url = "https://files.pythonhosted.org/packages/77/00/8538f11e3356b5d95fa4b024aa566cde7a38aa7a5f08f4912b32a037c5dc/multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3", size = 125360 }, + { url = "https://files.pythonhosted.org/packages/be/05/5d334c1f2462d43fec2363cd00b1c44c93a78c3925d952e9a71caf662e96/multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133", size = 26382 }, + { url = "https://files.pythonhosted.org/packages/a3/bf/f332a13486b1ed0496d624bcc7e8357bb8053823e8cd4b9a18edc1d97e73/multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1", size = 28529 }, + { url = "https://files.pythonhosted.org/packages/22/67/1c7c0f39fe069aa4e5d794f323be24bf4d33d62d2a348acdb7991f8f30db/multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008", size = 48771 }, + { url = "https://files.pythonhosted.org/packages/3c/25/c186ee7b212bdf0df2519eacfb1981a017bda34392c67542c274651daf23/multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f", size = 29533 }, + { url = "https://files.pythonhosted.org/packages/67/5e/04575fd837e0958e324ca035b339cea174554f6f641d3fb2b4f2e7ff44a2/multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28", size = 29595 }, + { url = "https://files.pythonhosted.org/packages/d3/b2/e56388f86663810c07cfe4a3c3d87227f3811eeb2d08450b9e5d19d78876/multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b", size = 130094 }, + { url = "https://files.pythonhosted.org/packages/6c/ee/30ae9b4186a644d284543d55d491fbd4239b015d36b23fea43b4c94f7052/multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c", size = 134876 }, + { url = "https://files.pythonhosted.org/packages/84/c7/70461c13ba8ce3c779503c70ec9d0345ae84de04521c1f45a04d5f48943d/multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3", size = 133500 }, + { url = "https://files.pythonhosted.org/packages/4a/9f/002af221253f10f99959561123fae676148dd730e2daa2cd053846a58507/multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44", size = 131099 }, + { url = "https://files.pythonhosted.org/packages/82/42/d1c7a7301d52af79d88548a97e297f9d99c961ad76bbe6f67442bb77f097/multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2", size = 120403 }, + { url = "https://files.pythonhosted.org/packages/68/f3/471985c2c7ac707547553e8f37cff5158030d36bdec4414cb825fbaa5327/multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3", size = 125348 }, + { url = "https://files.pythonhosted.org/packages/67/2c/e6df05c77e0e433c214ec1d21ddd203d9a4770a1f2866a8ca40a545869a0/multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa", size = 119673 }, + { url = "https://files.pythonhosted.org/packages/c5/cd/bc8608fff06239c9fb333f9db7743a1b2eafe98c2666c9a196e867a3a0a4/multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa", size = 129927 }, + { url = "https://files.pythonhosted.org/packages/44/8e/281b69b7bc84fc963a44dc6e0bbcc7150e517b91df368a27834299a526ac/multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4", size = 128711 }, + { url = "https://files.pythonhosted.org/packages/12/a4/63e7cd38ed29dd9f1881d5119f272c898ca92536cdb53ffe0843197f6c85/multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6", size = 125519 }, + { url = "https://files.pythonhosted.org/packages/38/e0/4f5855037a72cd8a7a2f60a3952d9aa45feedb37ae7831642102604e8a37/multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81", size = 26426 }, + { url = "https://files.pythonhosted.org/packages/7e/a5/17ee3a4db1e310b7405f5d25834460073a8ccd86198ce044dfaf69eac073/multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774", size = 28531 }, + { url = "https://files.pythonhosted.org/packages/3e/6a/af41f3aaf5f00fd86cc7d470a2f5b25299b0c84691163b8757f4a1a205f2/multidict-6.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:db7457bac39421addd0c8449933ac32d8042aae84a14911a757ae6ca3eef1392", size = 48597 }, + { url = "https://files.pythonhosted.org/packages/d9/d6/3d4082760ed11b05734f8bf32a0615b99e7d9d2b3730ad698a4d7377c00a/multidict-6.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d094ddec350a2fb899fec68d8353c78233debde9b7d8b4beeafa70825f1c281a", size = 29338 }, + { url = "https://files.pythonhosted.org/packages/9d/7f/5d1ce7f47d44393d429922910afbe88fcd29ee3069babbb47507a4c3a7ea/multidict-6.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5845c1fd4866bb5dd3125d89b90e57ed3138241540897de748cdf19de8a2fca2", size = 29562 }, + { url = "https://files.pythonhosted.org/packages/ce/ec/c425257671af9308a9b626e2e21f7f43841616e4551de94eb3c92aca75b2/multidict-6.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9079dfc6a70abe341f521f78405b8949f96db48da98aeb43f9907f342f627cdc", size = 130980 }, + { url = "https://files.pythonhosted.org/packages/d8/d7/d4220ad2633a89b314593e9b85b5bc9287a7c563c7f9108a4a68d9da5374/multidict-6.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3914f5aaa0f36d5d60e8ece6a308ee1c9784cd75ec8151062614657a114c4478", size = 136694 }, + { url = "https://files.pythonhosted.org/packages/a1/2a/13e554db5830c8d40185a2e22aa8325516a5de9634c3fb2caf3886a829b3/multidict-6.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c08be4f460903e5a9d0f76818db3250f12e9c344e79314d1d570fc69d7f4eae4", size = 131616 }, + { url = "https://files.pythonhosted.org/packages/2e/a9/83692e37d8152f104333132105b67100aabfb2e96a87f6bed67f566035a7/multidict-6.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d093be959277cb7dee84b801eb1af388b6ad3ca6a6b6bf1ed7585895789d027d", size = 129664 }, + { url = "https://files.pythonhosted.org/packages/cc/1c/1718cd518fb9da7e8890d9d1611c1af0ea5e60f68ff415d026e38401ed36/multidict-6.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3702ea6872c5a2a4eeefa6ffd36b042e9773f05b1f37ae3ef7264b1163c2dcf6", size = 121855 }, + { url = "https://files.pythonhosted.org/packages/2b/92/f6ed67514b0e3894198f0eb42dcde22f0851ea35f4561a1e4acf36c7b1be/multidict-6.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2090f6a85cafc5b2db085124d752757c9d251548cedabe9bd31afe6363e0aff2", size = 127928 }, + { url = "https://files.pythonhosted.org/packages/f7/30/c66954115a4dc4dc3c84e02c8ae11bb35a43d79ef93122c3c3a40c4d459b/multidict-6.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:f67f217af4b1ff66c68a87318012de788dd95fcfeb24cc889011f4e1c7454dfd", size = 122793 }, + { url = "https://files.pythonhosted.org/packages/62/c9/d386d01b43871e8e1631eb7b3695f6af071b7ae1ab716caf371100f0eb24/multidict-6.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:189f652a87e876098bbc67b4da1049afb5f5dfbaa310dd67c594b01c10388db6", size = 132762 }, + { url = "https://files.pythonhosted.org/packages/69/ff/f70cb0a2f7a358acf48e32139ce3a150ff18c961ee9c714cc8c0dc7e3584/multidict-6.1.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:6bb5992037f7a9eff7991ebe4273ea7f51f1c1c511e6a2ce511d0e7bdb754492", size = 127872 }, + { url = "https://files.pythonhosted.org/packages/89/5b/abea7db3ba4cd07752a9b560f9275a11787cd13f86849b5d99c1ceea921d/multidict-6.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f4c2b9e770c4e393876e35a7046879d195cd123b4f116d299d442b335bcd", size = 126161 }, + { url = "https://files.pythonhosted.org/packages/22/03/acc77a4667cca4462ee974fc39990803e58fa573d5a923d6e82b7ef6da7e/multidict-6.1.0-cp38-cp38-win32.whl", hash = "sha256:e27bbb6d14416713a8bd7aaa1313c0fc8d44ee48d74497a0ff4c3a1b6ccb5167", size = 26338 }, + { url = "https://files.pythonhosted.org/packages/90/bf/3d0c1cc9c8163abc24625fae89c0ade1ede9bccb6eceb79edf8cff3cca46/multidict-6.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:22f3105d4fb15c8f57ff3959a58fcab6ce36814486500cd7485651230ad4d4ef", size = 28736 }, + { url = "https://files.pythonhosted.org/packages/e7/c9/9e153a6572b38ac5ff4434113af38acf8d5e9957897cdb1f513b3d6614ed/multidict-6.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c", size = 48550 }, + { url = "https://files.pythonhosted.org/packages/76/f5/79565ddb629eba6c7f704f09a09df085c8dc04643b12506f10f718cee37a/multidict-6.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1", size = 29298 }, + { url = "https://files.pythonhosted.org/packages/60/1b/9851878b704bc98e641a3e0bce49382ae9e05743dac6d97748feb5b7baba/multidict-6.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c", size = 29641 }, + { url = "https://files.pythonhosted.org/packages/89/87/d451d45aab9e422cb0fb2f7720c31a4c1d3012c740483c37f642eba568fb/multidict-6.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c", size = 126202 }, + { url = "https://files.pythonhosted.org/packages/fa/b4/27cbe9f3e2e469359887653f2e45470272eef7295139916cc21107c6b48c/multidict-6.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f", size = 133925 }, + { url = "https://files.pythonhosted.org/packages/4d/a3/afc841899face8adfd004235ce759a37619f6ec99eafd959650c5ce4df57/multidict-6.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875", size = 129039 }, + { url = "https://files.pythonhosted.org/packages/5e/41/0d0fb18c1ad574f807196f5f3d99164edf9de3e169a58c6dc2d6ed5742b9/multidict-6.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255", size = 124072 }, + { url = "https://files.pythonhosted.org/packages/00/22/defd7a2e71a44e6e5b9a5428f972e5b572e7fe28e404dfa6519bbf057c93/multidict-6.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30", size = 116532 }, + { url = "https://files.pythonhosted.org/packages/91/25/f7545102def0b1d456ab6449388eed2dfd822debba1d65af60194904a23a/multidict-6.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057", size = 128173 }, + { url = "https://files.pythonhosted.org/packages/45/79/3dbe8d35fc99f5ea610813a72ab55f426cb9cf482f860fa8496e5409be11/multidict-6.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657", size = 122654 }, + { url = "https://files.pythonhosted.org/packages/97/cb/209e735eeab96e1b160825b5d0b36c56d3862abff828fc43999bb957dcad/multidict-6.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28", size = 133197 }, + { url = "https://files.pythonhosted.org/packages/e4/3a/a13808a7ada62808afccea67837a79d00ad6581440015ef00f726d064c2d/multidict-6.1.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972", size = 129754 }, + { url = "https://files.pythonhosted.org/packages/77/dd/8540e139eafb240079242da8f8ffdf9d3f4b4ad1aac5a786cd4050923783/multidict-6.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43", size = 126402 }, + { url = "https://files.pythonhosted.org/packages/86/99/e82e1a275d8b1ea16d3a251474262258dbbe41c05cce0c01bceda1fc8ea5/multidict-6.1.0-cp39-cp39-win32.whl", hash = "sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada", size = 26421 }, + { url = "https://files.pythonhosted.org/packages/86/1c/9fa630272355af7e4446a2c7550c259f11ee422ab2d30ff90a0a71cf3d9e/multidict-6.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a", size = 28791 }, + { url = "https://files.pythonhosted.org/packages/99/b7/b9e70fde2c0f0c9af4cc5277782a89b66d35948ea3369ec9f598358c3ac5/multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", size = 10051 }, +] + +[[package]] +name = "mypy" +version = "1.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e8/21/7e9e523537991d145ab8a0a2fd98548d67646dc2aaaf6091c31ad883e7c1/mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e", size = 3152532 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/8c/206de95a27722b5b5a8c85ba3100467bd86299d92a4f71c6b9aa448bfa2f/mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a", size = 11020731 }, + { url = "https://files.pythonhosted.org/packages/ab/bb/b31695a29eea76b1569fd28b4ab141a1adc9842edde080d1e8e1776862c7/mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80", size = 10184276 }, + { url = "https://files.pythonhosted.org/packages/a5/2d/4a23849729bb27934a0e079c9c1aad912167d875c7b070382a408d459651/mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7", size = 12587706 }, + { url = "https://files.pythonhosted.org/packages/5c/c3/d318e38ada50255e22e23353a469c791379825240e71b0ad03e76ca07ae6/mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f", size = 13105586 }, + { url = "https://files.pythonhosted.org/packages/4a/25/3918bc64952370c3dbdbd8c82c363804678127815febd2925b7273d9482c/mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372", size = 9632318 }, + { url = "https://files.pythonhosted.org/packages/d0/19/de0822609e5b93d02579075248c7aa6ceaddcea92f00bf4ea8e4c22e3598/mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d", size = 10939027 }, + { url = "https://files.pythonhosted.org/packages/c8/71/6950fcc6ca84179137e4cbf7cf41e6b68b4a339a1f5d3e954f8c34e02d66/mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d", size = 10108699 }, + { url = "https://files.pythonhosted.org/packages/26/50/29d3e7dd166e74dc13d46050b23f7d6d7533acf48f5217663a3719db024e/mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b", size = 12506263 }, + { url = "https://files.pythonhosted.org/packages/3f/1d/676e76f07f7d5ddcd4227af3938a9c9640f293b7d8a44dd4ff41d4db25c1/mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73", size = 12984688 }, + { url = "https://files.pythonhosted.org/packages/9c/03/5a85a30ae5407b1d28fab51bd3e2103e52ad0918d1e68f02a7778669a307/mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca", size = 9626811 }, + { url = "https://files.pythonhosted.org/packages/fb/31/c526a7bd2e5c710ae47717c7a5f53f616db6d9097caf48ad650581e81748/mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5", size = 11077900 }, + { url = "https://files.pythonhosted.org/packages/83/67/b7419c6b503679d10bd26fc67529bc6a1f7a5f220bbb9f292dc10d33352f/mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e", size = 10074818 }, + { url = "https://files.pythonhosted.org/packages/ba/07/37d67048786ae84e6612575e173d713c9a05d0ae495dde1e68d972207d98/mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2", size = 12589275 }, + { url = "https://files.pythonhosted.org/packages/1f/17/b1018c6bb3e9f1ce3956722b3bf91bff86c1cefccca71cec05eae49d6d41/mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0", size = 13037783 }, + { url = "https://files.pythonhosted.org/packages/cb/32/cd540755579e54a88099aee0287086d996f5a24281a673f78a0e14dba150/mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2", size = 9726197 }, + { url = "https://files.pythonhosted.org/packages/11/bb/ab4cfdc562cad80418f077d8be9b4491ee4fb257440da951b85cbb0a639e/mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7", size = 11069721 }, + { url = "https://files.pythonhosted.org/packages/59/3b/a393b1607cb749ea2c621def5ba8c58308ff05e30d9dbdc7c15028bca111/mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62", size = 10063996 }, + { url = "https://files.pythonhosted.org/packages/d1/1f/6b76be289a5a521bb1caedc1f08e76ff17ab59061007f201a8a18cc514d1/mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8", size = 12584043 }, + { url = "https://files.pythonhosted.org/packages/a6/83/5a85c9a5976c6f96e3a5a7591aa28b4a6ca3a07e9e5ba0cec090c8b596d6/mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7", size = 13036996 }, + { url = "https://files.pythonhosted.org/packages/b4/59/c39a6f752f1f893fccbcf1bdd2aca67c79c842402b5283563d006a67cf76/mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc", size = 9737709 }, + { url = "https://files.pythonhosted.org/packages/5e/2a/13e9ad339131c0fba5c70584f639005a47088f5eed77081a3d00479df0ca/mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a", size = 10955147 }, + { url = "https://files.pythonhosted.org/packages/94/39/02929067dc16b72d78109195cfed349ac4ec85f3d52517ac62b9a5263685/mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb", size = 10138373 }, + { url = "https://files.pythonhosted.org/packages/4a/cc/066709bb01734e3dbbd1375749f8789bf9693f8b842344fc0cf52109694f/mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b", size = 12543621 }, + { url = "https://files.pythonhosted.org/packages/f5/a2/124df839025348c7b9877d0ce134832a9249968e3ab36bb826bab0e9a1cf/mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74", size = 13050348 }, + { url = "https://files.pythonhosted.org/packages/45/86/cc94b1e7f7e756a63043cf425c24fb7470013ee1c032180282db75b1b335/mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6", size = 9615311 }, + { url = "https://files.pythonhosted.org/packages/5f/d4/b33ddd40dad230efb317898a2d1c267c04edba73bc5086bf77edeb410fb2/mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc", size = 11013906 }, + { url = "https://files.pythonhosted.org/packages/f4/e6/f414bca465b44d01cd5f4a82761e15044bedd1bf8025c5af3cc64518fac5/mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732", size = 10180657 }, + { url = "https://files.pythonhosted.org/packages/38/e9/fc3865e417722f98d58409770be01afb961e2c1f99930659ff4ae7ca8b7e/mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc", size = 12586394 }, + { url = "https://files.pythonhosted.org/packages/2e/35/f4d8b6d2cb0b3dad63e96caf159419dda023f45a358c6c9ac582ccaee354/mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d", size = 13103591 }, + { url = "https://files.pythonhosted.org/packages/22/1d/80594aef135f921dd52e142fa0acd19df197690bd0cde42cea7b88cf5aa2/mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24", size = 9634690 }, + { url = "https://files.pythonhosted.org/packages/3b/86/72ce7f57431d87a7ff17d442f521146a6585019eb8f4f31b7c02801f78ad/mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a", size = 2647043 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, +] + +[[package]] +name = "myst-nb" +version = "0.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils", version = "0.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.9.*'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "importlib-metadata" }, + { name = "ipython" }, + { name = "ipywidgets" }, + { name = "jupyter-cache" }, + { name = "jupyter-sphinx" }, + { name = "myst-parser", version = "0.13.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "myst-parser", version = "0.13.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.9.*'" }, + { name = "nbconvert" }, + { name = "nbformat" }, + { name = "pyyaml" }, + { name = "sphinx" }, + { name = "sphinx-togglebutton" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/9f/06bc463c1bcbfb7b48b56240d4fd7a8cefccfc510c7cfde77b8c07bb7fe4/myst-nb-0.11.1.tar.gz", hash = "sha256:1ac530645296310c61ccb7e767309c6498fa386ccc41499f5ec1f6b57a4dd1c9", size = 31598 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/b0/350f2e4b9f21a58d87e93457bdcda89a62b1657e7ddf93c11c059caf6cbe/myst_nb-0.11.1-py3-none-any.whl", hash = "sha256:f009fc7552b425be2250476c92a0e07a5c6f12a27755f265fc2bc5be511a47a6", size = 36654 }, +] + +[[package]] +name = "myst-parser" +version = "0.13.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "jinja2", marker = "python_full_version == '3.9.*'" }, + { name = "markdown-it-py", marker = "python_full_version == '3.9.*'" }, + { name = "mdit-py-plugins", marker = "python_full_version == '3.9.*'" }, + { name = "pyyaml", marker = "python_full_version == '3.9.*'" }, + { name = "sphinx", marker = "python_full_version == '3.9.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/c7/644c475014b7e0c1ac625a9412a0a3f9b1dbb354d43ed12000d3ac8073f8/myst-parser-0.13.6.tar.gz", hash = "sha256:bec01ecebe9b9c04322f8aebd6fd8e61d2cb9ab711d531065a374cc3dcb1d7be", size = 43824 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/dc/0a77028b5b7bf8661e1c73569b72f2b822e4d7a570a34d29d01ac789b626/myst_parser-0.13.6-py3-none-any.whl", hash = "sha256:a448b3dcb39bc62a6954f5e18544b83d69ed69d8947cf01f8ebe8b654921b4bf", size = 43785 }, +] + +[[package]] +name = "myst-parser" +version = "0.13.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version < '3.9'", +] +dependencies = [ + { name = "docutils", version = "0.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.9.*'" }, + { name = "jinja2", marker = "python_full_version != '3.9.*'" }, + { name = "markdown-it-py", marker = "python_full_version != '3.9.*'" }, + { name = "mdit-py-plugins", marker = "python_full_version != '3.9.*'" }, + { name = "pyyaml", marker = "python_full_version != '3.9.*'" }, + { name = "sphinx", marker = "python_full_version != '3.9.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/af/e7c4c8634bf90664efa6ab3d550dc6c526b59b2990e9a4bdd192f8edc4aa/myst-parser-0.13.7.tar.gz", hash = "sha256:e4bc99e43e19f70d22e528de8e7cce59f7e8e7c4c34dcba203de92de7a7c7c85", size = 45618 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/40/db9563e8b57710ea9742b74e5228a4bcb8130aceeeab71f8315ca79a7b57/myst_parser-0.13.7-py3-none-any.whl", hash = "sha256:260355b4da8e8865fe080b0638d7f1ab1791dc4bed02a7a48630b6bad4249219", size = 44007 }, +] + +[[package]] +name = "nbclassic" +version = "0.5.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argon2-cffi" }, + { name = "ipykernel" }, + { name = "ipython-genutils" }, + { name = "jinja2" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "jupyter-server" }, + { name = "nbconvert" }, + { name = "nbformat" }, + { name = "nest-asyncio" }, + { name = "notebook-shim" }, + { name = "prometheus-client" }, + { name = "pyzmq" }, + { name = "send2trash" }, + { name = "terminado" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/3d/8f8cbe374c60476e0165bdc18aec6952820a595b70032ad7881d3c5ed7aa/nbclassic-0.5.6.tar.gz", hash = "sha256:aab53fa1bea084fb6ade5c538b011a4f070c69f88d72878a8e8fb356f152509f", size = 20201214 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/24/63cd9d8b11fb3778ac11de109558e5f2bcd2eed5be01a14ab3162bc95b68/nbclassic-0.5.6-py3-none-any.whl", hash = "sha256:e3c8b7de80046c4a36a74662a5e325386d345289906c618366d8154e03dc2322", size = 9995168 }, +] + +[[package]] +name = "nbclient" +version = "0.5.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-client" }, + { name = "nbformat" }, + { name = "nest-asyncio" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/89/cd8621865c68d7570371d137257566651cb259137879d65fb533bd183165/nbclient-0.5.13.tar.gz", hash = "sha256:40c52c9b5e3c31faecaee69f202b3f53e38d7c1c563de0fadde9d7eda0fdafe8", size = 75191 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/f7/436bb1add1814911efec4a4a5a358c7559e9b1fd19f4ef89a2a71d707c2b/nbclient-0.5.13-py3-none-any.whl", hash = "sha256:47ac905af59379913c1f8f541098d2550153cf8dc58553cbe18c702b181518b0", size = 70613 }, +] + +[[package]] +name = "nbconvert" +version = "5.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bleach", version = "6.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "bleach", version = "6.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "defusedxml" }, + { name = "entrypoints" }, + { name = "jinja2" }, + { name = "jupyter-core" }, + { name = "mistune" }, + { name = "nbformat" }, + { name = "pandocfilters" }, + { name = "pygments" }, + { name = "testpath" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/f2/299fa4b15155ecbe2aefe7412249f0dd91f953b7a9b37c336317d564a1ca/nbconvert-5.6.1.tar.gz", hash = "sha256:21fb48e700b43e82ba0e3142421a659d7739b65568cc832a13976a77be16b523", size = 703233 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/6c/05a569e9f703d18aacb89b7ad6075b404e8a4afde2c26b73ca77bb644b14/nbconvert-5.6.1-py2.py3-none-any.whl", hash = "sha256:f0d6ec03875f96df45aa13e21fd9b8450c42d7e1830418cccc008c0df725fcee", size = 455126 }, +] + +[[package]] +name = "nbdime" +version = "4.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, + { name = "gitpython" }, + { name = "jinja2" }, + { name = "jupyter-server" }, + { name = "jupyter-server-mathjax" }, + { name = "nbformat" }, + { name = "pygments" }, + { name = "requests" }, + { name = "tornado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/f1/4be57ecea4d55d322f05a0f89e0b73d7a8d90a16dbf01168eab3e7bf5939/nbdime-4.0.2.tar.gz", hash = "sha256:d8279f8f4b236c0b253b20d60c4831bb67843ed8dbd6e09f234eb011d36f1bf2", size = 9452967 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/23/824b295f4cc53f4437f6917e8f46c519cd3a9be909dd36ca0682fdc7fff1/nbdime-4.0.2-py3-none-any.whl", hash = "sha256:e5a43aca669c576c66e757071c0e882de05ac305311d79aded99bfb5a3e9419e", size = 5917346 }, +] + +[[package]] +name = "nbformat" +version = "5.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastjsonschema" }, + { name = "jsonschema" }, + { name = "jupyter-core" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454 }, +] + +[[package]] +name = "nbsphinx" +version = "0.9.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils", version = "0.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.9.*'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "jinja2" }, + { name = "nbconvert" }, + { name = "nbformat" }, + { name = "sphinx" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/31/85cb6129d22c75722d1e1a8db0cdaf36ab7e1e7a59189bfa275445c8ab2d/nbsphinx-0.9.3.tar.gz", hash = "sha256:ec339c8691b688f8676104a367a4b8cf3ea01fd089dc28d24dec22d563b11562", size = 171956 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/a0/ca4aeb2f7f2608a483459a3bb486da250a7eb23eb76c9a0af154395f0cb2/nbsphinx-0.9.3-py3-none-any.whl", hash = "sha256:6e805e9627f4a358bd5720d5cbf8bf48853989c79af557afd91a5f22e163029f", size = 31039 }, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195 }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, +] + +[[package]] +name = "notebook" +version = "6.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argon2-cffi" }, + { name = "ipykernel" }, + { name = "ipython-genutils" }, + { name = "jinja2" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "nbclassic" }, + { name = "nbconvert" }, + { name = "nbformat" }, + { name = "nest-asyncio" }, + { name = "prometheus-client" }, + { name = "pyzmq" }, + { name = "send2trash" }, + { name = "terminado" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/1e/b555b6e33c962a605e2e85b6014f609d3e1c6a5ff48f7c2480376b430d96/notebook-6.5.4.tar.gz", hash = "sha256:517209568bd47261e2def27a140e97d49070602eea0d226a696f42a7f16c9a4e", size = 5785832 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/21/0e7683e7c4d51b8f6cc5df9bbd33fb2d1e114b9e5dcddeef96ebd8e86348/notebook-6.5.4-py3-none-any.whl", hash = "sha256:dd17e78aefe64c768737b32bf171c1c766666a21cc79a44d37a1700771cab56f", size = 529822 }, +] + +[[package]] +name = "notebook-shim" +version = "0.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-server" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/d2/92fa3243712b9a3e8bafaf60aac366da1cada3639ca767ff4b5b3654ec28/notebook_shim-0.2.4.tar.gz", hash = "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb", size = 13167 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef", size = 13307 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "pamqp" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/62/35bbd3d3021e008606cd0a9532db7850c65741bbf69ac8a3a0d8cfeb7934/pamqp-3.3.0.tar.gz", hash = "sha256:40b8795bd4efcf2b0f8821c1de83d12ca16d5760f4507836267fd7a02b06763b", size = 30993 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/8d/c1e93296e109a320e508e38118cf7d1fc2a4d1c2ec64de78565b3c445eb5/pamqp-3.3.0-py2.py3-none-any.whl", hash = "sha256:c901a684794157ae39b52cbf700db8c9aae7a470f13528b9d7b4e5f7202f8eb0", size = 33848 }, +] + +[[package]] +name = "pandoc" +version = "2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "plumbum" }, + { name = "ply" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/9a/e3186e760c57ee5f1c27ea5cea577a0ff9abfca51eefcb4d9a4cd39aff2e/pandoc-2.4.tar.gz", hash = "sha256:ecd1f8cbb7f4180c6b5db4a17a7c1a74df519995f5f186ef81ce72a9cbd0dd9a", size = 34635 } + +[[package]] +name = "pandocfilters" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663 }, +] + +[[package]] +name = "parso" +version = "0.8.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size = 400609 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650 }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 }, +] + +[[package]] +name = "pickleshare" +version = "0.7.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/b6/df3c1c9b616e9c0edbc4fbab6ddd09df9535849c64ba51fcb6531c32d4d8/pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", size = 6161 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/41/220f49aaea88bc6fa6cba8d05ecf24676326156c23b991e80b3f2fc24c77/pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56", size = 6877 }, +] + +[[package]] +name = "pkgutil-resolve-name" +version = "1.3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/f2/f2891a9dc37398696ddd945012b90ef8d0a034f0012e3f83c3f7a70b0f79/pkgutil_resolve_name-1.3.10.tar.gz", hash = "sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174", size = 5054 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/5c/3d4882ba113fd55bdba9326c1e4c62a15e674a2501de4869e6bd6301f87e/pkgutil_resolve_name-1.3.10-py3-none-any.whl", hash = "sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e", size = 4734 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "plumbum" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-resources", marker = "python_full_version < '3.9'" }, + { name = "pywin32", marker = "platform_python_implementation != 'PyPy' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/5d/49ba324ad4ae5b1a4caefafbce7a1648540129344481f2ed4ef6bb68d451/plumbum-1.9.0.tar.gz", hash = "sha256:e640062b72642c3873bd5bdc3effed75ba4d3c70ef6b6a7b907357a84d909219", size = 319083 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/9d/d03542c93bb3d448406731b80f39c3d5601282f778328c22c77d270f4ed4/plumbum-1.9.0-py3-none-any.whl", hash = "sha256:9fd0d3b0e8d86e4b581af36edf3f3bbe9d1ae15b45b8caab28de1bcb27aaa7f5", size = 127970 }, +] + +[[package]] +name = "plumpy" +source = { editable = "." } +dependencies = [ + { name = "kiwipy", extra = ["rmq"] }, + { name = "nest-asyncio" }, + { name = "pyyaml" }, +] + +[package.optional-dependencies] +docs = [ + { name = "importlib-metadata" }, + { name = "ipython" }, + { name = "jinja2" }, + { name = "kiwipy", extra = ["docs"] }, + { name = "markupsafe" }, + { name = "myst-nb" }, + { name = "sphinx" }, + { name = "sphinx-book-theme" }, +] +pre-commit = [ + { name = "mypy" }, + { name = "pre-commit" }, + { name = "types-pyyaml" }, +] +tests = [ + { name = "importlib-resources" }, + { name = "ipykernel" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "pytest-notebook" }, + { name = "shortuuid" }, +] + +[package.metadata] +requires-dist = [ + { name = "importlib-metadata", marker = "extra == 'docs'", specifier = "~=4.12.0" }, + { name = "importlib-resources", marker = "extra == 'tests'", specifier = "~=5.2" }, + { name = "ipykernel", marker = "extra == 'tests'", specifier = "==6.12.1" }, + { name = "ipython", marker = "extra == 'docs'", specifier = "~=7.0" }, + { name = "jinja2", marker = "extra == 'docs'", specifier = "==2.11.3" }, + { name = "kiwipy", extras = ["docs"], marker = "extra == 'docs'", specifier = "~=0.8.3" }, + { name = "kiwipy", extras = ["rmq"], specifier = "~=0.8.5" }, + { name = "markupsafe", marker = "extra == 'docs'", specifier = "==2.0.1" }, + { name = "mypy", marker = "extra == 'pre-commit'", specifier = "==1.13.0" }, + { name = "myst-nb", marker = "extra == 'docs'", specifier = "~=0.11.0" }, + { name = "nest-asyncio", specifier = "~=1.5,>=1.5.1" }, + { name = "pre-commit", marker = "extra == 'pre-commit'", specifier = "~=2.2" }, + { name = "pytest", marker = "extra == 'tests'", specifier = "~=7.0" }, + { name = "pytest-asyncio", marker = "extra == 'tests'", specifier = "~=0.12,<0.17" }, + { name = "pytest-cov", marker = "extra == 'tests'", specifier = "~=4.1" }, + { name = "pytest-notebook", marker = "extra == 'tests'", specifier = ">=0.8.0" }, + { name = "pyyaml", specifier = "~=6.0" }, + { name = "shortuuid", marker = "extra == 'tests'", specifier = "==1.0.8" }, + { name = "sphinx", marker = "extra == 'docs'", specifier = "~=3.2.0" }, + { name = "sphinx-book-theme", marker = "extra == 'docs'", specifier = "~=0.0.39" }, + { name = "types-pyyaml", marker = "extra == 'pre-commit'" }, +] + +[[package]] +name = "ply" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/69/882ee5c9d017149285cab114ebeab373308ef0f874fcdac9beb90e0ac4da/ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3", size = 159130 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce", size = 49567 }, +] + +[[package]] +name = "pre-commit" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify", version = "2.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "identify", version = "2.6.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6b/00/1637ae945c6e10838ef5c41965f1c864e59301811bb203e979f335608e7c/pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658", size = 174966 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/6b/6cfe3a8b351b54f4b6c6d2ad4286804e3367f628dce379c603d3b96635f4/pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad", size = 201938 }, +] + +[[package]] +name = "prometheus-client" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/62/14/7d0f567991f3a9af8d1cd4f619040c93b68f09a02b6d0b6ab1b2d1ded5fe/prometheus_client-0.21.1.tar.gz", hash = "sha256:252505a722ac04b0456be05c05f75f45d760c2911ffc45f2a06bcaed9f3ae3fb", size = 78551 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/c2/ab7d37426c179ceb9aeb109a85cda8948bb269b7561a0be870cc656eefe4/prometheus_client-0.21.1-py3-none-any.whl", hash = "sha256:594b45c410d6f4f8888940fe80b5cc2521b305a1fafe1c58609ef715a001f301", size = 54682 }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.48" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/4f/feb5e137aff82f7c7f3248267b97451da3644f6cdc218edfe549fb354127/prompt_toolkit-3.0.48.tar.gz", hash = "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90", size = 424684 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/6a/fd08d94654f7e67c52ca30523a178b3f8ccc4237fce4be90d39c938a831a/prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e", size = 386595 }, +] + +[[package]] +name = "propcache" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/4d/5e5a60b78dbc1d464f8a7bbaeb30957257afdc8512cbb9dfd5659304f5cd/propcache-0.2.0.tar.gz", hash = "sha256:df81779732feb9d01e5d513fad0122efb3d53bbc75f61b2a4f29a020bc985e70", size = 40951 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/08/1963dfb932b8d74d5b09098507b37e9b96c835ba89ab8aad35aa330f4ff3/propcache-0.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c5869b8fd70b81835a6f187c5fdbe67917a04d7e52b6e7cc4e5fe39d55c39d58", size = 80712 }, + { url = "https://files.pythonhosted.org/packages/e6/59/49072aba9bf8a8ed958e576182d46f038e595b17ff7408bc7e8807e721e1/propcache-0.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:952e0d9d07609d9c5be361f33b0d6d650cd2bae393aabb11d9b719364521984b", size = 46301 }, + { url = "https://files.pythonhosted.org/packages/33/a2/6b1978c2e0d80a678e2c483f45e5443c15fe5d32c483902e92a073314ef1/propcache-0.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:33ac8f098df0585c0b53009f039dfd913b38c1d2edafed0cedcc0c32a05aa110", size = 45581 }, + { url = "https://files.pythonhosted.org/packages/43/95/55acc9adff8f997c7572f23d41993042290dfb29e404cdadb07039a4386f/propcache-0.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97e48e8875e6c13909c800fa344cd54cc4b2b0db1d5f911f840458a500fde2c2", size = 208659 }, + { url = "https://files.pythonhosted.org/packages/bd/2c/ef7371ff715e6cd19ea03fdd5637ecefbaa0752fee5b0f2fe8ea8407ee01/propcache-0.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:388f3217649d6d59292b722d940d4d2e1e6a7003259eb835724092a1cca0203a", size = 222613 }, + { url = "https://files.pythonhosted.org/packages/5e/1c/fef251f79fd4971a413fa4b1ae369ee07727b4cc2c71e2d90dfcde664fbb/propcache-0.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f571aea50ba5623c308aa146eb650eebf7dbe0fd8c5d946e28343cb3b5aad577", size = 221067 }, + { url = "https://files.pythonhosted.org/packages/8d/e7/22e76ae6fc5a1708bdce92bdb49de5ebe89a173db87e4ef597d6bbe9145a/propcache-0.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3dfafb44f7bb35c0c06eda6b2ab4bfd58f02729e7c4045e179f9a861b07c9850", size = 208920 }, + { url = "https://files.pythonhosted.org/packages/04/3e/f10aa562781bcd8a1e0b37683a23bef32bdbe501d9cc7e76969becaac30d/propcache-0.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3ebe9a75be7ab0b7da2464a77bb27febcb4fab46a34f9288f39d74833db7f61", size = 200050 }, + { url = "https://files.pythonhosted.org/packages/d0/98/8ac69f638358c5f2a0043809c917802f96f86026e86726b65006830f3dc6/propcache-0.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d2f0d0f976985f85dfb5f3d685697ef769faa6b71993b46b295cdbbd6be8cc37", size = 202346 }, + { url = "https://files.pythonhosted.org/packages/ee/78/4acfc5544a5075d8e660af4d4e468d60c418bba93203d1363848444511ad/propcache-0.2.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a3dc1a4b165283bd865e8f8cb5f0c64c05001e0718ed06250d8cac9bec115b48", size = 199750 }, + { url = "https://files.pythonhosted.org/packages/a2/8f/90ada38448ca2e9cf25adc2fe05d08358bda1b9446f54a606ea38f41798b/propcache-0.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9e0f07b42d2a50c7dd2d8675d50f7343d998c64008f1da5fef888396b7f84630", size = 201279 }, + { url = "https://files.pythonhosted.org/packages/08/31/0e299f650f73903da851f50f576ef09bfffc8e1519e6a2f1e5ed2d19c591/propcache-0.2.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e63e3e1e0271f374ed489ff5ee73d4b6e7c60710e1f76af5f0e1a6117cd26394", size = 211035 }, + { url = "https://files.pythonhosted.org/packages/85/3e/e356cc6b09064bff1c06d0b2413593e7c925726f0139bc7acef8a21e87a8/propcache-0.2.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:56bb5c98f058a41bb58eead194b4db8c05b088c93d94d5161728515bd52b052b", size = 215565 }, + { url = "https://files.pythonhosted.org/packages/8b/54/4ef7236cd657e53098bd05aa59cbc3cbf7018fba37b40eaed112c3921e51/propcache-0.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7665f04d0c7f26ff8bb534e1c65068409bf4687aa2534faf7104d7182debb336", size = 207604 }, + { url = "https://files.pythonhosted.org/packages/1f/27/d01d7799c068443ee64002f0655d82fb067496897bf74b632e28ee6a32cf/propcache-0.2.0-cp310-cp310-win32.whl", hash = "sha256:7cf18abf9764746b9c8704774d8b06714bcb0a63641518a3a89c7f85cc02c2ad", size = 40526 }, + { url = "https://files.pythonhosted.org/packages/bb/44/6c2add5eeafb7f31ff0d25fbc005d930bea040a1364cf0f5768750ddf4d1/propcache-0.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:cfac69017ef97db2438efb854edf24f5a29fd09a536ff3a992b75990720cdc99", size = 44958 }, + { url = "https://files.pythonhosted.org/packages/e0/1c/71eec730e12aec6511e702ad0cd73c2872eccb7cad39de8ba3ba9de693ef/propcache-0.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:63f13bf09cc3336eb04a837490b8f332e0db41da66995c9fd1ba04552e516354", size = 80811 }, + { url = "https://files.pythonhosted.org/packages/89/c3/7e94009f9a4934c48a371632197406a8860b9f08e3f7f7d922ab69e57a41/propcache-0.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608cce1da6f2672a56b24a015b42db4ac612ee709f3d29f27a00c943d9e851de", size = 46365 }, + { url = "https://files.pythonhosted.org/packages/c0/1d/c700d16d1d6903aeab28372fe9999762f074b80b96a0ccc953175b858743/propcache-0.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:466c219deee4536fbc83c08d09115249db301550625c7fef1c5563a584c9bc87", size = 45602 }, + { url = "https://files.pythonhosted.org/packages/2e/5e/4a3e96380805bf742712e39a4534689f4cddf5fa2d3a93f22e9fd8001b23/propcache-0.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc2db02409338bf36590aa985a461b2c96fce91f8e7e0f14c50c5fcc4f229016", size = 236161 }, + { url = "https://files.pythonhosted.org/packages/a5/85/90132481183d1436dff6e29f4fa81b891afb6cb89a7306f32ac500a25932/propcache-0.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6ed8db0a556343d566a5c124ee483ae113acc9a557a807d439bcecc44e7dfbb", size = 244938 }, + { url = "https://files.pythonhosted.org/packages/4a/89/c893533cb45c79c970834274e2d0f6d64383ec740be631b6a0a1d2b4ddc0/propcache-0.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91997d9cb4a325b60d4e3f20967f8eb08dfcb32b22554d5ef78e6fd1dda743a2", size = 243576 }, + { url = "https://files.pythonhosted.org/packages/8c/56/98c2054c8526331a05f205bf45cbb2cda4e58e56df70e76d6a509e5d6ec6/propcache-0.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c7dde9e533c0a49d802b4f3f218fa9ad0a1ce21f2c2eb80d5216565202acab4", size = 236011 }, + { url = "https://files.pythonhosted.org/packages/2d/0c/8b8b9f8a6e1abd869c0fa79b907228e7abb966919047d294ef5df0d136cf/propcache-0.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffcad6c564fe6b9b8916c1aefbb37a362deebf9394bd2974e9d84232e3e08504", size = 224834 }, + { url = "https://files.pythonhosted.org/packages/18/bb/397d05a7298b7711b90e13108db697732325cafdcd8484c894885c1bf109/propcache-0.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:97a58a28bcf63284e8b4d7b460cbee1edaab24634e82059c7b8c09e65284f178", size = 224946 }, + { url = "https://files.pythonhosted.org/packages/25/19/4fc08dac19297ac58135c03770b42377be211622fd0147f015f78d47cd31/propcache-0.2.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:945db8ee295d3af9dbdbb698cce9bbc5c59b5c3fe328bbc4387f59a8a35f998d", size = 217280 }, + { url = "https://files.pythonhosted.org/packages/7e/76/c79276a43df2096ce2aba07ce47576832b1174c0c480fe6b04bd70120e59/propcache-0.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39e104da444a34830751715f45ef9fc537475ba21b7f1f5b0f4d71a3b60d7fe2", size = 220088 }, + { url = "https://files.pythonhosted.org/packages/c3/9a/8a8cf428a91b1336b883f09c8b884e1734c87f724d74b917129a24fe2093/propcache-0.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c5ecca8f9bab618340c8e848d340baf68bcd8ad90a8ecd7a4524a81c1764b3db", size = 233008 }, + { url = "https://files.pythonhosted.org/packages/25/7b/768a8969abd447d5f0f3333df85c6a5d94982a1bc9a89c53c154bf7a8b11/propcache-0.2.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c436130cc779806bdf5d5fae0d848713105472b8566b75ff70048c47d3961c5b", size = 237719 }, + { url = "https://files.pythonhosted.org/packages/ed/0d/e5d68ccc7976ef8b57d80613ac07bbaf0614d43f4750cf953f0168ef114f/propcache-0.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:191db28dc6dcd29d1a3e063c3be0b40688ed76434622c53a284e5427565bbd9b", size = 227729 }, + { url = "https://files.pythonhosted.org/packages/05/64/17eb2796e2d1c3d0c431dc5f40078d7282f4645af0bb4da9097fbb628c6c/propcache-0.2.0-cp311-cp311-win32.whl", hash = "sha256:5f2564ec89058ee7c7989a7b719115bdfe2a2fb8e7a4543b8d1c0cc4cf6478c1", size = 40473 }, + { url = "https://files.pythonhosted.org/packages/83/c5/e89fc428ccdc897ade08cd7605f174c69390147526627a7650fb883e0cd0/propcache-0.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e2e54267980349b723cff366d1e29b138b9a60fa376664a157a342689553f71", size = 44921 }, + { url = "https://files.pythonhosted.org/packages/7c/46/a41ca1097769fc548fc9216ec4c1471b772cc39720eb47ed7e38ef0006a9/propcache-0.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ee7606193fb267be4b2e3b32714f2d58cad27217638db98a60f9efb5efeccc2", size = 80800 }, + { url = "https://files.pythonhosted.org/packages/75/4f/93df46aab9cc473498ff56be39b5f6ee1e33529223d7a4d8c0a6101a9ba2/propcache-0.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:91ee8fc02ca52e24bcb77b234f22afc03288e1dafbb1f88fe24db308910c4ac7", size = 46443 }, + { url = "https://files.pythonhosted.org/packages/0b/17/308acc6aee65d0f9a8375e36c4807ac6605d1f38074b1581bd4042b9fb37/propcache-0.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e900bad2a8456d00a113cad8c13343f3b1f327534e3589acc2219729237a2e8", size = 45676 }, + { url = "https://files.pythonhosted.org/packages/65/44/626599d2854d6c1d4530b9a05e7ff2ee22b790358334b475ed7c89f7d625/propcache-0.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f52a68c21363c45297aca15561812d542f8fc683c85201df0bebe209e349f793", size = 246191 }, + { url = "https://files.pythonhosted.org/packages/f2/df/5d996d7cb18df076debae7d76ac3da085c0575a9f2be6b1f707fe227b54c/propcache-0.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e41d67757ff4fbc8ef2af99b338bfb955010444b92929e9e55a6d4dcc3c4f09", size = 251791 }, + { url = "https://files.pythonhosted.org/packages/2e/6d/9f91e5dde8b1f662f6dd4dff36098ed22a1ef4e08e1316f05f4758f1576c/propcache-0.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a64e32f8bd94c105cc27f42d3b658902b5bcc947ece3c8fe7bc1b05982f60e89", size = 253434 }, + { url = "https://files.pythonhosted.org/packages/3c/e9/1b54b7e26f50b3e0497cd13d3483d781d284452c2c50dd2a615a92a087a3/propcache-0.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55346705687dbd7ef0d77883ab4f6fabc48232f587925bdaf95219bae072491e", size = 248150 }, + { url = "https://files.pythonhosted.org/packages/a7/ef/a35bf191c8038fe3ce9a414b907371c81d102384eda5dbafe6f4dce0cf9b/propcache-0.2.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00181262b17e517df2cd85656fcd6b4e70946fe62cd625b9d74ac9977b64d8d9", size = 233568 }, + { url = "https://files.pythonhosted.org/packages/97/d9/d00bb9277a9165a5e6d60f2142cd1a38a750045c9c12e47ae087f686d781/propcache-0.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6994984550eaf25dd7fc7bd1b700ff45c894149341725bb4edc67f0ffa94efa4", size = 229874 }, + { url = "https://files.pythonhosted.org/packages/8e/78/c123cf22469bdc4b18efb78893e69c70a8b16de88e6160b69ca6bdd88b5d/propcache-0.2.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:56295eb1e5f3aecd516d91b00cfd8bf3a13991de5a479df9e27dd569ea23959c", size = 225857 }, + { url = "https://files.pythonhosted.org/packages/31/1b/fd6b2f1f36d028820d35475be78859d8c89c8f091ad30e377ac49fd66359/propcache-0.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:439e76255daa0f8151d3cb325f6dd4a3e93043e6403e6491813bcaaaa8733887", size = 227604 }, + { url = "https://files.pythonhosted.org/packages/99/36/b07be976edf77a07233ba712e53262937625af02154353171716894a86a6/propcache-0.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f6475a1b2ecb310c98c28d271a30df74f9dd436ee46d09236a6b750a7599ce57", size = 238430 }, + { url = "https://files.pythonhosted.org/packages/0d/64/5822f496c9010e3966e934a011ac08cac8734561842bc7c1f65586e0683c/propcache-0.2.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3444cdba6628accf384e349014084b1cacd866fbb88433cd9d279d90a54e0b23", size = 244814 }, + { url = "https://files.pythonhosted.org/packages/fd/bd/8657918a35d50b18a9e4d78a5df7b6c82a637a311ab20851eef4326305c1/propcache-0.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a9d9b4d0a9b38d1c391bb4ad24aa65f306c6f01b512e10a8a34a2dc5675d348", size = 235922 }, + { url = "https://files.pythonhosted.org/packages/a8/6f/ec0095e1647b4727db945213a9f395b1103c442ef65e54c62e92a72a3f75/propcache-0.2.0-cp312-cp312-win32.whl", hash = "sha256:69d3a98eebae99a420d4b28756c8ce6ea5a29291baf2dc9ff9414b42676f61d5", size = 40177 }, + { url = "https://files.pythonhosted.org/packages/20/a2/bd0896fdc4f4c1db46d9bc361c8c79a9bf08ccc08ba054a98e38e7ba1557/propcache-0.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ad9c9b99b05f163109466638bd30ada1722abb01bbb85c739c50b6dc11f92dc3", size = 44446 }, + { url = "https://files.pythonhosted.org/packages/a8/a7/5f37b69197d4f558bfef5b4bceaff7c43cc9b51adf5bd75e9081d7ea80e4/propcache-0.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ecddc221a077a8132cf7c747d5352a15ed763b674c0448d811f408bf803d9ad7", size = 78120 }, + { url = "https://files.pythonhosted.org/packages/c8/cd/48ab2b30a6b353ecb95a244915f85756d74f815862eb2ecc7a518d565b48/propcache-0.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0e53cb83fdd61cbd67202735e6a6687a7b491c8742dfc39c9e01e80354956763", size = 45127 }, + { url = "https://files.pythonhosted.org/packages/a5/ba/0a1ef94a3412aab057bd996ed5f0ac7458be5bf469e85c70fa9ceb43290b/propcache-0.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92fe151145a990c22cbccf9ae15cae8ae9eddabfc949a219c9f667877e40853d", size = 44419 }, + { url = "https://files.pythonhosted.org/packages/b4/6c/ca70bee4f22fa99eacd04f4d2f1699be9d13538ccf22b3169a61c60a27fa/propcache-0.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6a21ef516d36909931a2967621eecb256018aeb11fc48656e3257e73e2e247a", size = 229611 }, + { url = "https://files.pythonhosted.org/packages/19/70/47b872a263e8511ca33718d96a10c17d3c853aefadeb86dc26e8421184b9/propcache-0.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f88a4095e913f98988f5b338c1d4d5d07dbb0b6bad19892fd447484e483ba6b", size = 234005 }, + { url = "https://files.pythonhosted.org/packages/4f/be/3b0ab8c84a22e4a3224719099c1229ddfdd8a6a1558cf75cb55ee1e35c25/propcache-0.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a5b3bb545ead161be780ee85a2b54fdf7092815995661947812dde94a40f6fb", size = 237270 }, + { url = "https://files.pythonhosted.org/packages/04/d8/f071bb000d4b8f851d312c3c75701e586b3f643fe14a2e3409b1b9ab3936/propcache-0.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67aeb72e0f482709991aa91345a831d0b707d16b0257e8ef88a2ad246a7280bf", size = 231877 }, + { url = "https://files.pythonhosted.org/packages/93/e7/57a035a1359e542bbb0a7df95aad6b9871ebee6dce2840cb157a415bd1f3/propcache-0.2.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c997f8c44ec9b9b0bcbf2d422cc00a1d9b9c681f56efa6ca149a941e5560da2", size = 217848 }, + { url = "https://files.pythonhosted.org/packages/f0/93/d1dea40f112ec183398fb6c42fde340edd7bab202411c4aa1a8289f461b6/propcache-0.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a66df3d4992bc1d725b9aa803e8c5a66c010c65c741ad901e260ece77f58d2f", size = 216987 }, + { url = "https://files.pythonhosted.org/packages/62/4c/877340871251145d3522c2b5d25c16a1690ad655fbab7bb9ece6b117e39f/propcache-0.2.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3ebbcf2a07621f29638799828b8d8668c421bfb94c6cb04269130d8de4fb7136", size = 212451 }, + { url = "https://files.pythonhosted.org/packages/7c/bb/a91b72efeeb42906ef58ccf0cdb87947b54d7475fee3c93425d732f16a61/propcache-0.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1235c01ddaa80da8235741e80815ce381c5267f96cc49b1477fdcf8c047ef325", size = 212879 }, + { url = "https://files.pythonhosted.org/packages/9b/7f/ee7fea8faac57b3ec5d91ff47470c6c5d40d7f15d0b1fccac806348fa59e/propcache-0.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3947483a381259c06921612550867b37d22e1df6d6d7e8361264b6d037595f44", size = 222288 }, + { url = "https://files.pythonhosted.org/packages/ff/d7/acd67901c43d2e6b20a7a973d9d5fd543c6e277af29b1eb0e1f7bd7ca7d2/propcache-0.2.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d5bed7f9805cc29c780f3aee05de3262ee7ce1f47083cfe9f77471e9d6777e83", size = 228257 }, + { url = "https://files.pythonhosted.org/packages/8d/6f/6272ecc7a8daad1d0754cfc6c8846076a8cb13f810005c79b15ce0ef0cf2/propcache-0.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4a91d44379f45f5e540971d41e4626dacd7f01004826a18cb048e7da7e96544", size = 221075 }, + { url = "https://files.pythonhosted.org/packages/7c/bd/c7a6a719a6b3dd8b3aeadb3675b5783983529e4a3185946aa444d3e078f6/propcache-0.2.0-cp313-cp313-win32.whl", hash = "sha256:f902804113e032e2cdf8c71015651c97af6418363bea8d78dc0911d56c335032", size = 39654 }, + { url = "https://files.pythonhosted.org/packages/88/e7/0eef39eff84fa3e001b44de0bd41c7c0e3432e7648ffd3d64955910f002d/propcache-0.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:8f188cfcc64fb1266f4684206c9de0e80f54622c3f22a910cbd200478aeae61e", size = 43705 }, + { url = "https://files.pythonhosted.org/packages/b4/94/2c3d64420fd58ed462e2b416386d48e72dec027cf7bb572066cf3866e939/propcache-0.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:53d1bd3f979ed529f0805dd35ddaca330f80a9a6d90bc0121d2ff398f8ed8861", size = 82315 }, + { url = "https://files.pythonhosted.org/packages/73/b7/9e2a17d9a126f2012b22ddc5d0979c28ca75104e24945214790c1d787015/propcache-0.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:83928404adf8fb3d26793665633ea79b7361efa0287dfbd372a7e74311d51ee6", size = 47188 }, + { url = "https://files.pythonhosted.org/packages/80/ef/18af27caaae5589c08bb5a461cfa136b83b7e7983be604f2140d91f92b97/propcache-0.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:77a86c261679ea5f3896ec060be9dc8e365788248cc1e049632a1be682442063", size = 46314 }, + { url = "https://files.pythonhosted.org/packages/fa/df/8dbd3e472baf73251c0fbb571a3f0a4e3a40c52a1c8c2a6c46ab08736ff9/propcache-0.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:218db2a3c297a3768c11a34812e63b3ac1c3234c3a086def9c0fee50d35add1f", size = 212874 }, + { url = "https://files.pythonhosted.org/packages/7c/57/5d4d783ac594bd56434679b8643673ae12de1ce758116fd8912a7f2313ec/propcache-0.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7735e82e3498c27bcb2d17cb65d62c14f1100b71723b68362872bca7d0913d90", size = 224578 }, + { url = "https://files.pythonhosted.org/packages/66/27/072be8ad434c9a3aa1b561f527984ea0ed4ac072fd18dfaaa2aa2d6e6a2b/propcache-0.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:20a617c776f520c3875cf4511e0d1db847a076d720714ae35ffe0df3e440be68", size = 222636 }, + { url = "https://files.pythonhosted.org/packages/c3/f1/69a30ff0928d07f50bdc6f0147fd9a08e80904fd3fdb711785e518de1021/propcache-0.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67b69535c870670c9f9b14a75d28baa32221d06f6b6fa6f77a0a13c5a7b0a5b9", size = 213573 }, + { url = "https://files.pythonhosted.org/packages/a8/2e/c16716ae113fe0a3219978df3665a6fea049d81d50bd28c4ae72a4c77567/propcache-0.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4569158070180c3855e9c0791c56be3ceeb192defa2cdf6a3f39e54319e56b89", size = 205438 }, + { url = "https://files.pythonhosted.org/packages/e1/df/80e2c5cd5ed56a7bfb1aa58cedb79617a152ae43de7c0a7e800944a6b2e2/propcache-0.2.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:db47514ffdbd91ccdc7e6f8407aac4ee94cc871b15b577c1c324236b013ddd04", size = 202352 }, + { url = "https://files.pythonhosted.org/packages/0f/4e/79f665fa04839f30ffb2903211c718b9660fbb938ac7a4df79525af5aeb3/propcache-0.2.0-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:2a60ad3e2553a74168d275a0ef35e8c0a965448ffbc3b300ab3a5bb9956c2162", size = 200476 }, + { url = "https://files.pythonhosted.org/packages/a9/39/b9ea7b011521dd7cfd2f89bb6b8b304f3c789ea6285445bc145bebc83094/propcache-0.2.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:662dd62358bdeaca0aee5761de8727cfd6861432e3bb828dc2a693aa0471a563", size = 201581 }, + { url = "https://files.pythonhosted.org/packages/e4/81/e8e96c97aa0b675a14e37b12ca9c9713b15cfacf0869e64bf3ab389fabf1/propcache-0.2.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:25a1f88b471b3bc911d18b935ecb7115dff3a192b6fef46f0bfaf71ff4f12418", size = 225628 }, + { url = "https://files.pythonhosted.org/packages/eb/99/15f998c502c214f6c7f51462937605d514a8943a9a6c1fa10f40d2710976/propcache-0.2.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:f60f0ac7005b9f5a6091009b09a419ace1610e163fa5deaba5ce3484341840e7", size = 229270 }, + { url = "https://files.pythonhosted.org/packages/ff/3a/a9f1a0c0e5b994b8f1a1c71bea56bb3e9eeec821cb4dd61e14051c4ba00b/propcache-0.2.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:74acd6e291f885678631b7ebc85d2d4aec458dd849b8c841b57ef04047833bed", size = 207771 }, + { url = "https://files.pythonhosted.org/packages/ff/3e/6103906a66d6713f32880cf6a5ba84a1406b4d66e1b9389bb9b8e1789f9e/propcache-0.2.0-cp38-cp38-win32.whl", hash = "sha256:d9b6ddac6408194e934002a69bcaadbc88c10b5f38fb9307779d1c629181815d", size = 41015 }, + { url = "https://files.pythonhosted.org/packages/37/23/a30214b4c1f2bea24cc1197ef48d67824fbc41d5cf5472b17c37fef6002c/propcache-0.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:676135dcf3262c9c5081cc8f19ad55c8a64e3f7282a21266d05544450bffc3a5", size = 45749 }, + { url = "https://files.pythonhosted.org/packages/38/05/797e6738c9f44ab5039e3ff329540c934eabbe8ad7e63c305c75844bc86f/propcache-0.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:25c8d773a62ce0451b020c7b29a35cfbc05de8b291163a7a0f3b7904f27253e6", size = 81903 }, + { url = "https://files.pythonhosted.org/packages/9f/84/8d5edb9a73e1a56b24dd8f2adb6aac223109ff0e8002313d52e5518258ba/propcache-0.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:375a12d7556d462dc64d70475a9ee5982465fbb3d2b364f16b86ba9135793638", size = 46960 }, + { url = "https://files.pythonhosted.org/packages/e7/77/388697bedda984af0d12d68e536b98129b167282da3401965c8450de510e/propcache-0.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1ec43d76b9677637a89d6ab86e1fef70d739217fefa208c65352ecf0282be957", size = 46133 }, + { url = "https://files.pythonhosted.org/packages/e2/dc/60d444610bc5b1d7a758534f58362b1bcee736a785473f8a39c91f05aad1/propcache-0.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f45eec587dafd4b2d41ac189c2156461ebd0c1082d2fe7013571598abb8505d1", size = 211105 }, + { url = "https://files.pythonhosted.org/packages/bc/c6/40eb0dd1de6f8e84f454615ab61f68eb4a58f9d63d6f6eaf04300ac0cc17/propcache-0.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc092ba439d91df90aea38168e11f75c655880c12782facf5cf9c00f3d42b562", size = 226613 }, + { url = "https://files.pythonhosted.org/packages/de/b6/e078b5e9de58e20db12135eb6a206b4b43cb26c6b62ee0fe36ac40763a64/propcache-0.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa1076244f54bb76e65e22cb6910365779d5c3d71d1f18b275f1dfc7b0d71b4d", size = 225587 }, + { url = "https://files.pythonhosted.org/packages/ce/4e/97059dd24494d1c93d1efb98bb24825e1930265b41858dd59c15cb37a975/propcache-0.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:682a7c79a2fbf40f5dbb1eb6bfe2cd865376deeac65acf9beb607505dced9e12", size = 211826 }, + { url = "https://files.pythonhosted.org/packages/fc/23/4dbf726602a989d2280fe130a9b9dd71faa8d3bb8cd23d3261ff3c23f692/propcache-0.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e40876731f99b6f3c897b66b803c9e1c07a989b366c6b5b475fafd1f7ba3fb8", size = 203140 }, + { url = "https://files.pythonhosted.org/packages/5b/ce/f3bff82c885dbd9ae9e43f134d5b02516c3daa52d46f7a50e4f52ef9121f/propcache-0.2.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:363ea8cd3c5cb6679f1c2f5f1f9669587361c062e4899fce56758efa928728f8", size = 208841 }, + { url = "https://files.pythonhosted.org/packages/29/d7/19a4d3b4c7e95d08f216da97035d0b103d0c90411c6f739d47088d2da1f0/propcache-0.2.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:140fbf08ab3588b3468932974a9331aff43c0ab8a2ec2c608b6d7d1756dbb6cb", size = 203315 }, + { url = "https://files.pythonhosted.org/packages/db/87/5748212a18beb8d4ab46315c55ade8960d1e2cdc190764985b2d229dd3f4/propcache-0.2.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e70fac33e8b4ac63dfc4c956fd7d85a0b1139adcfc0d964ce288b7c527537fea", size = 204724 }, + { url = "https://files.pythonhosted.org/packages/84/2a/c3d2f989fc571a5bad0fabcd970669ccb08c8f9b07b037ecddbdab16a040/propcache-0.2.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b33d7a286c0dc1a15f5fc864cc48ae92a846df287ceac2dd499926c3801054a6", size = 215514 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4c44c133b08bc5f776afcb8f0833889c2636b8a83e07ea1d9096c1e401b0/propcache-0.2.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f6d5749fdd33d90e34c2efb174c7e236829147a2713334d708746e94c4bde40d", size = 220063 }, + { url = "https://files.pythonhosted.org/packages/2e/25/280d0a3bdaee68db74c0acd9a472e59e64b516735b59cffd3a326ff9058a/propcache-0.2.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:22aa8f2272d81d9317ff5756bb108021a056805ce63dd3630e27d042c8092798", size = 211620 }, + { url = "https://files.pythonhosted.org/packages/28/8c/266898981b7883c1563c35954f9ce9ced06019fdcc487a9520150c48dc91/propcache-0.2.0-cp39-cp39-win32.whl", hash = "sha256:73e4b40ea0eda421b115248d7e79b59214411109a5bc47d0d48e4c73e3b8fcf9", size = 41049 }, + { url = "https://files.pythonhosted.org/packages/af/53/a3e5b937f58e757a940716b88105ec4c211c42790c1ea17052b46dc16f16/propcache-0.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:9517d5e9e0731957468c29dbfd0f976736a0e55afaea843726e887f36fe017df", size = 45587 }, + { url = "https://files.pythonhosted.org/packages/3d/b6/e6d98278f2d49b22b4d033c9f792eda783b9ab2094b041f013fc69bcde87/propcache-0.2.0-py3-none-any.whl", hash = "sha256:2ccc28197af5313706511fab3a8b66dcd6da067a1331372c82ea1cb74285e036", size = 11603 }, +] + +[[package]] +name = "propcache" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/20/c8/2a13f78d82211490855b2fb303b6721348d0787fdd9a12ac46d99d3acde1/propcache-0.2.1.tar.gz", hash = "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64", size = 41735 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/a5/0ea64c9426959ef145a938e38c832fc551843481d356713ececa9a8a64e8/propcache-0.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6b3f39a85d671436ee3d12c017f8fdea38509e4f25b28eb25877293c98c243f6", size = 79296 }, + { url = "https://files.pythonhosted.org/packages/76/5a/916db1aba735f55e5eca4733eea4d1973845cf77dfe67c2381a2ca3ce52d/propcache-0.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d51fbe4285d5db5d92a929e3e21536ea3dd43732c5b177c7ef03f918dff9f2", size = 45622 }, + { url = "https://files.pythonhosted.org/packages/2d/62/685d3cf268b8401ec12b250b925b21d152b9d193b7bffa5fdc4815c392c2/propcache-0.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6445804cf4ec763dc70de65a3b0d9954e868609e83850a47ca4f0cb64bd79fea", size = 45133 }, + { url = "https://files.pythonhosted.org/packages/4d/3d/31c9c29ee7192defc05aa4d01624fd85a41cf98e5922aaed206017329944/propcache-0.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9479aa06a793c5aeba49ce5c5692ffb51fcd9a7016e017d555d5e2b0045d212", size = 204809 }, + { url = "https://files.pythonhosted.org/packages/10/a1/e4050776f4797fc86140ac9a480d5dc069fbfa9d499fe5c5d2fa1ae71f07/propcache-0.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9631c5e8b5b3a0fda99cb0d29c18133bca1e18aea9effe55adb3da1adef80d3", size = 219109 }, + { url = "https://files.pythonhosted.org/packages/c9/c0/e7ae0df76343d5e107d81e59acc085cea5fd36a48aa53ef09add7503e888/propcache-0.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3156628250f46a0895f1f36e1d4fbe062a1af8718ec3ebeb746f1d23f0c5dc4d", size = 217368 }, + { url = "https://files.pythonhosted.org/packages/fc/e1/e0a2ed6394b5772508868a977d3238f4afb2eebaf9976f0b44a8d347ad63/propcache-0.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b6fb63ae352e13748289f04f37868099e69dba4c2b3e271c46061e82c745634", size = 205124 }, + { url = "https://files.pythonhosted.org/packages/50/c1/e388c232d15ca10f233c778bbdc1034ba53ede14c207a72008de45b2db2e/propcache-0.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:887d9b0a65404929641a9fabb6452b07fe4572b269d901d622d8a34a4e9043b2", size = 195463 }, + { url = "https://files.pythonhosted.org/packages/0a/fd/71b349b9def426cc73813dbd0f33e266de77305e337c8c12bfb0a2a82bfb/propcache-0.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a96dc1fa45bd8c407a0af03b2d5218392729e1822b0c32e62c5bf7eeb5fb3958", size = 198358 }, + { url = "https://files.pythonhosted.org/packages/02/f2/d7c497cd148ebfc5b0ae32808e6c1af5922215fe38c7a06e4e722fe937c8/propcache-0.2.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a7e65eb5c003a303b94aa2c3852ef130230ec79e349632d030e9571b87c4698c", size = 195560 }, + { url = "https://files.pythonhosted.org/packages/bb/57/f37041bbe5e0dfed80a3f6be2612a3a75b9cfe2652abf2c99bef3455bbad/propcache-0.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:999779addc413181912e984b942fbcc951be1f5b3663cd80b2687758f434c583", size = 196895 }, + { url = "https://files.pythonhosted.org/packages/83/36/ae3cc3e4f310bff2f064e3d2ed5558935cc7778d6f827dce74dcfa125304/propcache-0.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:19a0f89a7bb9d8048d9c4370c9c543c396e894c76be5525f5e1ad287f1750ddf", size = 207124 }, + { url = "https://files.pythonhosted.org/packages/8c/c4/811b9f311f10ce9d31a32ff14ce58500458443627e4df4ae9c264defba7f/propcache-0.2.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1ac2f5fe02fa75f56e1ad473f1175e11f475606ec9bd0be2e78e4734ad575034", size = 210442 }, + { url = "https://files.pythonhosted.org/packages/18/dd/a1670d483a61ecac0d7fc4305d91caaac7a8fc1b200ea3965a01cf03bced/propcache-0.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:574faa3b79e8ebac7cb1d7930f51184ba1ccf69adfdec53a12f319a06030a68b", size = 203219 }, + { url = "https://files.pythonhosted.org/packages/f9/2d/30ced5afde41b099b2dc0c6573b66b45d16d73090e85655f1a30c5a24e07/propcache-0.2.1-cp310-cp310-win32.whl", hash = "sha256:03ff9d3f665769b2a85e6157ac8b439644f2d7fd17615a82fa55739bc97863f4", size = 40313 }, + { url = "https://files.pythonhosted.org/packages/23/84/bd9b207ac80da237af77aa6e153b08ffa83264b1c7882495984fcbfcf85c/propcache-0.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2d3af2e79991102678f53e0dbf4c35de99b6b8b58f29a27ca0325816364caaba", size = 44428 }, + { url = "https://files.pythonhosted.org/packages/bc/0f/2913b6791ebefb2b25b4efd4bb2299c985e09786b9f5b19184a88e5778dd/propcache-0.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ffc3cca89bb438fb9c95c13fc874012f7b9466b89328c3c8b1aa93cdcfadd16", size = 79297 }, + { url = "https://files.pythonhosted.org/packages/cf/73/af2053aeccd40b05d6e19058419ac77674daecdd32478088b79375b9ab54/propcache-0.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f174bbd484294ed9fdf09437f889f95807e5f229d5d93588d34e92106fbf6717", size = 45611 }, + { url = "https://files.pythonhosted.org/packages/3c/09/8386115ba7775ea3b9537730e8cf718d83bbf95bffe30757ccf37ec4e5da/propcache-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:70693319e0b8fd35dd863e3e29513875eb15c51945bf32519ef52927ca883bc3", size = 45146 }, + { url = "https://files.pythonhosted.org/packages/03/7a/793aa12f0537b2e520bf09f4c6833706b63170a211ad042ca71cbf79d9cb/propcache-0.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b480c6a4e1138e1aa137c0079b9b6305ec6dcc1098a8ca5196283e8a49df95a9", size = 232136 }, + { url = "https://files.pythonhosted.org/packages/f1/38/b921b3168d72111769f648314100558c2ea1d52eb3d1ba7ea5c4aa6f9848/propcache-0.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d27b84d5880f6d8aa9ae3edb253c59d9f6642ffbb2c889b78b60361eed449787", size = 239706 }, + { url = "https://files.pythonhosted.org/packages/14/29/4636f500c69b5edea7786db3c34eb6166f3384b905665ce312a6e42c720c/propcache-0.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:857112b22acd417c40fa4595db2fe28ab900c8c5fe4670c7989b1c0230955465", size = 238531 }, + { url = "https://files.pythonhosted.org/packages/85/14/01fe53580a8e1734ebb704a3482b7829a0ef4ea68d356141cf0994d9659b/propcache-0.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf6c4150f8c0e32d241436526f3c3f9cbd34429492abddbada2ffcff506c51af", size = 231063 }, + { url = "https://files.pythonhosted.org/packages/33/5c/1d961299f3c3b8438301ccfbff0143b69afcc30c05fa28673cface692305/propcache-0.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66d4cfda1d8ed687daa4bc0274fcfd5267873db9a5bc0418c2da19273040eeb7", size = 220134 }, + { url = "https://files.pythonhosted.org/packages/00/d0/ed735e76db279ba67a7d3b45ba4c654e7b02bc2f8050671ec365d8665e21/propcache-0.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2f992c07c0fca81655066705beae35fc95a2fa7366467366db627d9f2ee097f", size = 220009 }, + { url = "https://files.pythonhosted.org/packages/75/90/ee8fab7304ad6533872fee982cfff5a53b63d095d78140827d93de22e2d4/propcache-0.2.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:4a571d97dbe66ef38e472703067021b1467025ec85707d57e78711c085984e54", size = 212199 }, + { url = "https://files.pythonhosted.org/packages/eb/ec/977ffaf1664f82e90737275873461695d4c9407d52abc2f3c3e24716da13/propcache-0.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bb6178c241278d5fe853b3de743087be7f5f4c6f7d6d22a3b524d323eecec505", size = 214827 }, + { url = "https://files.pythonhosted.org/packages/57/48/031fb87ab6081764054821a71b71942161619549396224cbb242922525e8/propcache-0.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ad1af54a62ffe39cf34db1aa6ed1a1873bd548f6401db39d8e7cd060b9211f82", size = 228009 }, + { url = "https://files.pythonhosted.org/packages/1a/06/ef1390f2524850838f2390421b23a8b298f6ce3396a7cc6d39dedd4047b0/propcache-0.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e7048abd75fe40712005bcfc06bb44b9dfcd8e101dda2ecf2f5aa46115ad07ca", size = 231638 }, + { url = "https://files.pythonhosted.org/packages/38/2a/101e6386d5a93358395da1d41642b79c1ee0f3b12e31727932b069282b1d/propcache-0.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:160291c60081f23ee43d44b08a7e5fb76681221a8e10b3139618c5a9a291b84e", size = 222788 }, + { url = "https://files.pythonhosted.org/packages/db/81/786f687951d0979007e05ad9346cd357e50e3d0b0f1a1d6074df334b1bbb/propcache-0.2.1-cp311-cp311-win32.whl", hash = "sha256:819ce3b883b7576ca28da3861c7e1a88afd08cc8c96908e08a3f4dd64a228034", size = 40170 }, + { url = "https://files.pythonhosted.org/packages/cf/59/7cc7037b295d5772eceb426358bb1b86e6cab4616d971bd74275395d100d/propcache-0.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:edc9fc7051e3350643ad929df55c451899bb9ae6d24998a949d2e4c87fb596d3", size = 44404 }, + { url = "https://files.pythonhosted.org/packages/4c/28/1d205fe49be8b1b4df4c50024e62480a442b1a7b818e734308bb0d17e7fb/propcache-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:081a430aa8d5e8876c6909b67bd2d937bfd531b0382d3fdedb82612c618bc41a", size = 79588 }, + { url = "https://files.pythonhosted.org/packages/21/ee/fc4d893f8d81cd4971affef2a6cb542b36617cd1d8ce56b406112cb80bf7/propcache-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2ccec9ac47cf4e04897619c0e0c1a48c54a71bdf045117d3a26f80d38ab1fb0", size = 45825 }, + { url = "https://files.pythonhosted.org/packages/4a/de/bbe712f94d088da1d237c35d735f675e494a816fd6f54e9db2f61ef4d03f/propcache-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14d86fe14b7e04fa306e0c43cdbeebe6b2c2156a0c9ce56b815faacc193e320d", size = 45357 }, + { url = "https://files.pythonhosted.org/packages/7f/14/7ae06a6cf2a2f1cb382586d5a99efe66b0b3d0c6f9ac2f759e6f7af9d7cf/propcache-0.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:049324ee97bb67285b49632132db351b41e77833678432be52bdd0289c0e05e4", size = 241869 }, + { url = "https://files.pythonhosted.org/packages/cc/59/227a78be960b54a41124e639e2c39e8807ac0c751c735a900e21315f8c2b/propcache-0.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cd9a1d071158de1cc1c71a26014dcdfa7dd3d5f4f88c298c7f90ad6f27bb46d", size = 247884 }, + { url = "https://files.pythonhosted.org/packages/84/58/f62b4ffaedf88dc1b17f04d57d8536601e4e030feb26617228ef930c3279/propcache-0.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98110aa363f1bb4c073e8dcfaefd3a5cea0f0834c2aab23dda657e4dab2f53b5", size = 248486 }, + { url = "https://files.pythonhosted.org/packages/1c/07/ebe102777a830bca91bbb93e3479cd34c2ca5d0361b83be9dbd93104865e/propcache-0.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:647894f5ae99c4cf6bb82a1bb3a796f6e06af3caa3d32e26d2350d0e3e3faf24", size = 243649 }, + { url = "https://files.pythonhosted.org/packages/ed/bc/4f7aba7f08f520376c4bb6a20b9a981a581b7f2e385fa0ec9f789bb2d362/propcache-0.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfd3223c15bebe26518d58ccf9a39b93948d3dcb3e57a20480dfdd315356baff", size = 229103 }, + { url = "https://files.pythonhosted.org/packages/fe/d5/04ac9cd4e51a57a96f78795e03c5a0ddb8f23ec098b86f92de028d7f2a6b/propcache-0.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d71264a80f3fcf512eb4f18f59423fe82d6e346ee97b90625f283df56aee103f", size = 226607 }, + { url = "https://files.pythonhosted.org/packages/e3/f0/24060d959ea41d7a7cc7fdbf68b31852331aabda914a0c63bdb0e22e96d6/propcache-0.2.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e73091191e4280403bde6c9a52a6999d69cdfde498f1fdf629105247599b57ec", size = 221153 }, + { url = "https://files.pythonhosted.org/packages/77/a7/3ac76045a077b3e4de4859a0753010765e45749bdf53bd02bc4d372da1a0/propcache-0.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3935bfa5fede35fb202c4b569bb9c042f337ca4ff7bd540a0aa5e37131659348", size = 222151 }, + { url = "https://files.pythonhosted.org/packages/e7/af/5e29da6f80cebab3f5a4dcd2a3240e7f56f2c4abf51cbfcc99be34e17f0b/propcache-0.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f508b0491767bb1f2b87fdfacaba5f7eddc2f867740ec69ece6d1946d29029a6", size = 233812 }, + { url = "https://files.pythonhosted.org/packages/8c/89/ebe3ad52642cc5509eaa453e9f4b94b374d81bae3265c59d5c2d98efa1b4/propcache-0.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1672137af7c46662a1c2be1e8dc78cb6d224319aaa40271c9257d886be4363a6", size = 238829 }, + { url = "https://files.pythonhosted.org/packages/e9/2f/6b32f273fa02e978b7577159eae7471b3cfb88b48563b1c2578b2d7ca0bb/propcache-0.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b74c261802d3d2b85c9df2dfb2fa81b6f90deeef63c2db9f0e029a3cac50b518", size = 230704 }, + { url = "https://files.pythonhosted.org/packages/5c/2e/f40ae6ff5624a5f77edd7b8359b208b5455ea113f68309e2b00a2e1426b6/propcache-0.2.1-cp312-cp312-win32.whl", hash = "sha256:d09c333d36c1409d56a9d29b3a1b800a42c76a57a5a8907eacdbce3f18768246", size = 40050 }, + { url = "https://files.pythonhosted.org/packages/3b/77/a92c3ef994e47180862b9d7d11e37624fb1c00a16d61faf55115d970628b/propcache-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:c214999039d4f2a5b2073ac506bba279945233da8c786e490d411dfc30f855c1", size = 44117 }, + { url = "https://files.pythonhosted.org/packages/0f/2a/329e0547cf2def8857157f9477669043e75524cc3e6251cef332b3ff256f/propcache-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aca405706e0b0a44cc6bfd41fbe89919a6a56999157f6de7e182a990c36e37bc", size = 77002 }, + { url = "https://files.pythonhosted.org/packages/12/2d/c4df5415e2382f840dc2ecbca0eeb2293024bc28e57a80392f2012b4708c/propcache-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:12d1083f001ace206fe34b6bdc2cb94be66d57a850866f0b908972f90996b3e9", size = 44639 }, + { url = "https://files.pythonhosted.org/packages/d0/5a/21aaa4ea2f326edaa4e240959ac8b8386ea31dedfdaa636a3544d9e7a408/propcache-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d93f3307ad32a27bda2e88ec81134b823c240aa3abb55821a8da553eed8d9439", size = 44049 }, + { url = "https://files.pythonhosted.org/packages/4e/3e/021b6cd86c0acc90d74784ccbb66808b0bd36067a1bf3e2deb0f3845f618/propcache-0.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba278acf14471d36316159c94a802933d10b6a1e117b8554fe0d0d9b75c9d536", size = 224819 }, + { url = "https://files.pythonhosted.org/packages/3c/57/c2fdeed1b3b8918b1770a133ba5c43ad3d78e18285b0c06364861ef5cc38/propcache-0.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e6281aedfca15301c41f74d7005e6e3f4ca143584ba696ac69df4f02f40d629", size = 229625 }, + { url = "https://files.pythonhosted.org/packages/9d/81/70d4ff57bf2877b5780b466471bebf5892f851a7e2ca0ae7ffd728220281/propcache-0.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b750a8e5a1262434fb1517ddf64b5de58327f1adc3524a5e44c2ca43305eb0b", size = 232934 }, + { url = "https://files.pythonhosted.org/packages/3c/b9/bb51ea95d73b3fb4100cb95adbd4e1acaf2cbb1fd1083f5468eeb4a099a8/propcache-0.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf72af5e0fb40e9babf594308911436c8efde3cb5e75b6f206c34ad18be5c052", size = 227361 }, + { url = "https://files.pythonhosted.org/packages/f1/20/3c6d696cd6fd70b29445960cc803b1851a1131e7a2e4ee261ee48e002bcd/propcache-0.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2d0a12018b04f4cb820781ec0dffb5f7c7c1d2a5cd22bff7fb055a2cb19ebce", size = 213904 }, + { url = "https://files.pythonhosted.org/packages/a1/cb/1593bfc5ac6d40c010fa823f128056d6bc25b667f5393781e37d62f12005/propcache-0.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e800776a79a5aabdb17dcc2346a7d66d0777e942e4cd251defeb084762ecd17d", size = 212632 }, + { url = "https://files.pythonhosted.org/packages/6d/5c/e95617e222be14a34c709442a0ec179f3207f8a2b900273720501a70ec5e/propcache-0.2.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4160d9283bd382fa6c0c2b5e017acc95bc183570cd70968b9202ad6d8fc48dce", size = 207897 }, + { url = "https://files.pythonhosted.org/packages/8e/3b/56c5ab3dc00f6375fbcdeefdede5adf9bee94f1fab04adc8db118f0f9e25/propcache-0.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b43e74f1359353341a7adb783c8f1b1c676367b011709f466f42fda2045e95", size = 208118 }, + { url = "https://files.pythonhosted.org/packages/86/25/d7ef738323fbc6ebcbce33eb2a19c5e07a89a3df2fded206065bd5e868a9/propcache-0.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:58791550b27d5488b1bb52bc96328456095d96206a250d28d874fafe11b3dfaf", size = 217851 }, + { url = "https://files.pythonhosted.org/packages/b3/77/763e6cef1852cf1ba740590364ec50309b89d1c818e3256d3929eb92fabf/propcache-0.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0f022d381747f0dfe27e99d928e31bc51a18b65bb9e481ae0af1380a6725dd1f", size = 222630 }, + { url = "https://files.pythonhosted.org/packages/4f/e9/0f86be33602089c701696fbed8d8c4c07b6ee9605c5b7536fd27ed540c5b/propcache-0.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:297878dc9d0a334358f9b608b56d02e72899f3b8499fc6044133f0d319e2ec30", size = 216269 }, + { url = "https://files.pythonhosted.org/packages/cc/02/5ac83217d522394b6a2e81a2e888167e7ca629ef6569a3f09852d6dcb01a/propcache-0.2.1-cp313-cp313-win32.whl", hash = "sha256:ddfab44e4489bd79bda09d84c430677fc7f0a4939a73d2bba3073036f487a0a6", size = 39472 }, + { url = "https://files.pythonhosted.org/packages/f4/33/d6f5420252a36034bc8a3a01171bc55b4bff5df50d1c63d9caa50693662f/propcache-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:556fc6c10989f19a179e4321e5d678db8eb2924131e64652a51fe83e4c3db0e1", size = 43363 }, + { url = "https://files.pythonhosted.org/packages/0a/08/6ab7f65240a16fa01023125e65258acf7e4884f483f267cdd6fcc48f37db/propcache-0.2.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6a9a8c34fb7bb609419a211e59da8887eeca40d300b5ea8e56af98f6fbbb1541", size = 80403 }, + { url = "https://files.pythonhosted.org/packages/34/fe/e7180285e21b4e6dff7d311fdf22490c9146a09a02834b5232d6248c6004/propcache-0.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ae1aa1cd222c6d205853b3013c69cd04515f9d6ab6de4b0603e2e1c33221303e", size = 46152 }, + { url = "https://files.pythonhosted.org/packages/9c/36/aa74d884af826030ba9cee2ac109b0664beb7e9449c315c9c44db99efbb3/propcache-0.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:accb6150ce61c9c4b7738d45550806aa2b71c7668c6942f17b0ac182b6142fd4", size = 45674 }, + { url = "https://files.pythonhosted.org/packages/22/59/6fe80a3fe7720f715f2c0f6df250dacbd7cad42832410dbd84c719c52f78/propcache-0.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5eee736daafa7af6d0a2dc15cc75e05c64f37fc37bafef2e00d77c14171c2097", size = 207792 }, + { url = "https://files.pythonhosted.org/packages/4a/68/584cd51dd8f4d0f5fff5b128ce0cdb257cde903898eecfb92156bbc2c780/propcache-0.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7a31fc1e1bd362874863fdeed71aed92d348f5336fd84f2197ba40c59f061bd", size = 223280 }, + { url = "https://files.pythonhosted.org/packages/85/cb/4c3528460c41e61b06ec3f970c0f89f87fa21f63acac8642ed81a886c164/propcache-0.2.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba4cfa1052819d16699e1d55d18c92b6e094d4517c41dd231a8b9f87b6fa681", size = 221293 }, + { url = "https://files.pythonhosted.org/packages/69/c0/560e050aa6d31eeece3490d1174da508f05ab27536dfc8474af88b97160a/propcache-0.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f089118d584e859c62b3da0892b88a83d611c2033ac410e929cb6754eec0ed16", size = 208259 }, + { url = "https://files.pythonhosted.org/packages/0c/87/d6c86a77632eb1ba86a328e3313159f246e7564cb5951e05ed77555826a0/propcache-0.2.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:781e65134efaf88feb447e8c97a51772aa75e48b794352f94cb7ea717dedda0d", size = 198632 }, + { url = "https://files.pythonhosted.org/packages/3a/2b/3690ea7b662dc762ab7af5f3ef0e2d7513c823d193d7b2a1b4cda472c2be/propcache-0.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31f5af773530fd3c658b32b6bdc2d0838543de70eb9a2156c03e410f7b0d3aae", size = 203516 }, + { url = "https://files.pythonhosted.org/packages/4d/b5/afe716c16c23c77657185c257a41918b83e03993b6ccdfa748e5e7d328e9/propcache-0.2.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:a7a078f5d37bee6690959c813977da5291b24286e7b962e62a94cec31aa5188b", size = 199402 }, + { url = "https://files.pythonhosted.org/packages/a4/c0/2d2df3aa7f8660d0d4cc4f1e00490c48d5958da57082e70dea7af366f876/propcache-0.2.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cea7daf9fc7ae6687cf1e2c049752f19f146fdc37c2cc376e7d0032cf4f25347", size = 200528 }, + { url = "https://files.pythonhosted.org/packages/21/c8/65ac9142f5e40c8497f7176e71d18826b09e06dd4eb401c9a4ee41aa9c74/propcache-0.2.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:8b3489ff1ed1e8315674d0775dc7d2195fb13ca17b3808721b54dbe9fd020faf", size = 211254 }, + { url = "https://files.pythonhosted.org/packages/09/e4/edb70b447a1d8142df51ec7511e84aa64d7f6ce0a0fdf5eb55363cdd0935/propcache-0.2.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9403db39be1393618dd80c746cb22ccda168efce239c73af13c3763ef56ffc04", size = 214589 }, + { url = "https://files.pythonhosted.org/packages/cb/02/817f309ec8d8883287781d6d9390f80b14db6e6de08bc659dfe798a825c2/propcache-0.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5d97151bc92d2b2578ff7ce779cdb9174337390a535953cbb9452fb65164c587", size = 207283 }, + { url = "https://files.pythonhosted.org/packages/d7/fe/2d18612096ed2212cfef821b6fccdba5d52efc1d64511c206c5c16be28fd/propcache-0.2.1-cp39-cp39-win32.whl", hash = "sha256:9caac6b54914bdf41bcc91e7eb9147d331d29235a7c967c150ef5df6464fd1bb", size = 40866 }, + { url = "https://files.pythonhosted.org/packages/24/2e/b5134802e7b57c403c7b73c7a39374e7a6b7f128d1968b4a4b4c0b700250/propcache-0.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:92fc4500fcb33899b05ba73276dfb684a20d31caa567b7cb5252d48f896a91b1", size = 44975 }, + { url = "https://files.pythonhosted.org/packages/41/b6/c5319caea262f4821995dca2107483b94a3345d4607ad797c76cb9c36bcc/propcache-0.2.1-py3-none-any.whl", hash = "sha256:52277518d6aae65536e9cea52d4e7fd2f7a66f4aa2d30ed3f2fcea620ace3c54", size = 11818 }, +] + +[[package]] +name = "psutil" +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/5a/07871137bb752428aa4b659f910b399ba6f291156bdea939be3e96cae7cb/psutil-6.1.1.tar.gz", hash = "sha256:cf8496728c18f2d0b45198f06895be52f36611711746b7f30c464b422b50e2f5", size = 508502 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/99/ca79d302be46f7bdd8321089762dd4476ee725fce16fc2b2e1dbba8cac17/psutil-6.1.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:fc0ed7fe2231a444fc219b9c42d0376e0a9a1a72f16c5cfa0f68d19f1a0663e8", size = 247511 }, + { url = "https://files.pythonhosted.org/packages/0b/6b/73dbde0dd38f3782905d4587049b9be64d76671042fdcaf60e2430c6796d/psutil-6.1.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0bdd4eab935276290ad3cb718e9809412895ca6b5b334f5a9111ee6d9aff9377", size = 248985 }, + { url = "https://files.pythonhosted.org/packages/17/38/c319d31a1d3f88c5b79c68b3116c129e5133f1822157dd6da34043e32ed6/psutil-6.1.1-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6e06c20c05fe95a3d7302d74e7097756d4ba1247975ad6905441ae1b5b66003", size = 284488 }, + { url = "https://files.pythonhosted.org/packages/9c/39/0f88a830a1c8a3aba27fededc642da37613c57cbff143412e3536f89784f/psutil-6.1.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97f7cb9921fbec4904f522d972f0c0e1f4fabbdd4e0287813b21215074a0f160", size = 287477 }, + { url = "https://files.pythonhosted.org/packages/47/da/99f4345d4ddf2845cb5b5bd0d93d554e84542d116934fde07a0c50bd4e9f/psutil-6.1.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33431e84fee02bc84ea36d9e2c4a6d395d479c9dd9bba2376c1f6ee8f3a4e0b3", size = 289017 }, + { url = "https://files.pythonhosted.org/packages/38/53/bd755c2896f4461fd4f36fa6a6dcb66a88a9e4b9fd4e5b66a77cf9d4a584/psutil-6.1.1-cp37-abi3-win32.whl", hash = "sha256:eaa912e0b11848c4d9279a93d7e2783df352b082f40111e078388701fd479e53", size = 250602 }, + { url = "https://files.pythonhosted.org/packages/7b/d7/7831438e6c3ebbfa6e01a927127a6cb42ad3ab844247f3c5b96bea25d73d/psutil-6.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:f35cfccb065fff93529d2afb4a2e89e363fe63ca1e4a5da22b603a85833c2649", size = 254444 }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993 }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, +] + +[[package]] +name = "pydata-sphinx-theme" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e6/4a/01439756d28d0d1b4af1fa347efeff73f6f4e64c8b5132325cc3c0862d03/pydata-sphinx-theme-0.4.3.tar.gz", hash = "sha256:8cf8fbc74c6c47d6ed497a91f3bedf94d57383b52eebb4fa05ae7fc4f50767a2", size = 2141791 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/7f/b11e6bd6d1a8419b29b54b0f2594f879cf766b834acce8df2bcd9fed301b/pydata_sphinx_theme-0.4.3-py3-none-any.whl", hash = "sha256:aa0ae055de5de36a637387941d0e18d8dad35d97d56f0faf25f5219658d49df2", size = 2144730 }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, +] + +[[package]] +name = "pyrsistent" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/3a/5031723c09068e9c8c2f0bc25c3a9245f2b1d1aea8396c787a408f2b95ca/pyrsistent-0.20.0.tar.gz", hash = "sha256:4c48f78f62ab596c679086084d0dd13254ae4f3d6c72a83ffdf5ebdef8f265a4", size = 103642 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/19/c343b14061907b629b765444b6436b160e2bd4184d17d4804bbe6381f6be/pyrsistent-0.20.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8c3aba3e01235221e5b229a6c05f585f344734bd1ad42a8ac51493d74722bbce", size = 83416 }, + { url = "https://files.pythonhosted.org/packages/9f/4f/8342079ea331031ef9ed57edd312a9ad283bcc8adfaf268931ae356a09a6/pyrsistent-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1beb78af5423b879edaf23c5591ff292cf7c33979734c99aa66d5914ead880f", size = 118021 }, + { url = "https://files.pythonhosted.org/packages/d7/b7/64a125c488243965b7c5118352e47c6f89df95b4ac306d31cee409153d57/pyrsistent-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21cc459636983764e692b9eba7144cdd54fdec23ccdb1e8ba392a63666c60c34", size = 117747 }, + { url = "https://files.pythonhosted.org/packages/fe/a5/43c67bd5f80df9e7583042398d12113263ec57f27c0607abe9d78395d18f/pyrsistent-0.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5ac696f02b3fc01a710427585c855f65cd9c640e14f52abe52020722bb4906b", size = 114524 }, + { url = "https://files.pythonhosted.org/packages/8a/98/b382a87e89ca839106d874f7bf78d226b3eedb26735eb6f751f1a3375f21/pyrsistent-0.20.0-cp310-cp310-win32.whl", hash = "sha256:0724c506cd8b63c69c7f883cc233aac948c1ea946ea95996ad8b1380c25e1d3f", size = 60780 }, + { url = "https://files.pythonhosted.org/packages/37/8a/23e2193f7adea6901262e3cf39c7fe18ac0c446176c0ff0e19aeb2e9681e/pyrsistent-0.20.0-cp310-cp310-win_amd64.whl", hash = "sha256:8441cf9616d642c475684d6cf2520dd24812e996ba9af15e606df5f6fd9d04a7", size = 63310 }, + { url = "https://files.pythonhosted.org/packages/df/63/7544dc7d0953294882a5c587fb1b10a26e0c23d9b92281a14c2514bac1f7/pyrsistent-0.20.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0f3b1bcaa1f0629c978b355a7c37acd58907390149b7311b5db1b37648eb6958", size = 83481 }, + { url = "https://files.pythonhosted.org/packages/ae/a0/49249bc14d71b1bf2ffe89703acfa86f2017c25cfdabcaea532b8c8a5810/pyrsistent-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cdd7ef1ea7a491ae70d826b6cc64868de09a1d5ff9ef8d574250d0940e275b8", size = 120222 }, + { url = "https://files.pythonhosted.org/packages/a1/94/9808e8c9271424120289b9028a657da336ad7e43da0647f62e4f6011d19b/pyrsistent-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cae40a9e3ce178415040a0383f00e8d68b569e97f31928a3a8ad37e3fde6df6a", size = 120002 }, + { url = "https://files.pythonhosted.org/packages/3f/f6/9ecfb78b2fc8e2540546db0fe19df1fae0f56664a5958c21ff8861b0f8da/pyrsistent-0.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6288b3fa6622ad8a91e6eb759cfc48ff3089e7c17fb1d4c59a919769314af224", size = 116850 }, + { url = "https://files.pythonhosted.org/packages/83/c8/e6d28bc27a0719f8eaae660357df9757d6e9ca9be2691595721de9e8adfc/pyrsistent-0.20.0-cp311-cp311-win32.whl", hash = "sha256:7d29c23bdf6e5438c755b941cef867ec2a4a172ceb9f50553b6ed70d50dfd656", size = 60775 }, + { url = "https://files.pythonhosted.org/packages/98/87/c6ef52ff30388f357922d08de012abdd3dc61e09311d88967bdae23ab657/pyrsistent-0.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:59a89bccd615551391f3237e00006a26bcf98a4d18623a19909a2c48b8e986ee", size = 63306 }, + { url = "https://files.pythonhosted.org/packages/15/ee/ff2ed52032ac1ce2e7ba19e79bd5b05d152ebfb77956cf08fcd6e8d760ea/pyrsistent-0.20.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:09848306523a3aba463c4b49493a760e7a6ca52e4826aa100ee99d8d39b7ad1e", size = 83537 }, + { url = "https://files.pythonhosted.org/packages/80/f1/338d0050b24c3132bcfc79b68c3a5f54bce3d213ecef74d37e988b971d8a/pyrsistent-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a14798c3005ec892bbada26485c2eea3b54109cb2533713e355c806891f63c5e", size = 122615 }, + { url = "https://files.pythonhosted.org/packages/07/3a/e56d6431b713518094fae6ff833a04a6f49ad0fbe25fb7c0dc7408e19d20/pyrsistent-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b14decb628fac50db5e02ee5a35a9c0772d20277824cfe845c8a8b717c15daa3", size = 122335 }, + { url = "https://files.pythonhosted.org/packages/4a/bb/5f40a4d5e985a43b43f607250e766cdec28904682c3505eb0bd343a4b7db/pyrsistent-0.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e2c116cc804d9b09ce9814d17df5edf1df0c624aba3b43bc1ad90411487036d", size = 118510 }, + { url = "https://files.pythonhosted.org/packages/1c/13/e6a22f40f5800af116c02c28e29f15c06aa41cb2036f6a64ab124647f28b/pyrsistent-0.20.0-cp312-cp312-win32.whl", hash = "sha256:e78d0c7c1e99a4a45c99143900ea0546025e41bb59ebc10182e947cf1ece9174", size = 60865 }, + { url = "https://files.pythonhosted.org/packages/75/ef/2fa3b55023ec07c22682c957808f9a41836da4cd006b5f55ec76bf0fbfa6/pyrsistent-0.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:4021a7f963d88ccd15b523787d18ed5e5269ce57aa4037146a2377ff607ae87d", size = 63239 }, + { url = "https://files.pythonhosted.org/packages/a5/24/3293a2b2bc4b4d645f2f6743e97b329c18dd9d8177f80e52d2b7911bac0f/pyrsistent-0.20.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:79ed12ba79935adaac1664fd7e0e585a22caa539dfc9b7c7c6d5ebf91fb89054", size = 83450 }, + { url = "https://files.pythonhosted.org/packages/5d/ea/5438a78ba00f2a9cdc6836dcdcd8631b9d802b2bd57d5a61ed9d9ad6f24d/pyrsistent-0.20.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f920385a11207dc372a028b3f1e1038bb244b3ec38d448e6d8e43c6b3ba20e98", size = 121792 }, + { url = "https://files.pythonhosted.org/packages/b1/ff/93dea1abc3e2d44cee0f62974a1f133fc5a4c719c0978148726bd4957b52/pyrsistent-0.20.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f5c2d012671b7391803263419e31b5c7c21e7c95c8760d7fc35602353dee714", size = 121754 }, + { url = "https://files.pythonhosted.org/packages/93/29/93ad2089a3317b00c9f5d863a532339aa44dcd2cd5f8d73c569ef2c9cddb/pyrsistent-0.20.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef3992833fbd686ee783590639f4b8343a57f1f75de8633749d984dc0eb16c86", size = 118326 }, + { url = "https://files.pythonhosted.org/packages/60/c8/6ca4e647512d27b8a9ffe0daf75e284d1cb770c073d845d5893808a6951e/pyrsistent-0.20.0-cp38-cp38-win32.whl", hash = "sha256:881bbea27bbd32d37eb24dd320a5e745a2a5b092a17f6debc1349252fac85423", size = 60841 }, + { url = "https://files.pythonhosted.org/packages/09/6a/6a31c1bbffd4880a8825cea2572e8b3082681215464ebec9404c0b74ab4c/pyrsistent-0.20.0-cp38-cp38-win_amd64.whl", hash = "sha256:6d270ec9dd33cdb13f4d62c95c1a5a50e6b7cdd86302b494217137f760495b9d", size = 63281 }, + { url = "https://files.pythonhosted.org/packages/18/0c/289126299fcebf54fd01d385fb5176c328fef2c4233139c23dd48346e992/pyrsistent-0.20.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ca52d1ceae015859d16aded12584c59eb3825f7b50c6cfd621d4231a6cc624ce", size = 83379 }, + { url = "https://files.pythonhosted.org/packages/4e/45/62639d53ac09eaafc00f2e5845565e70d3eddb2d296337a77637186ca03e/pyrsistent-0.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b318ca24db0f0518630e8b6f3831e9cba78f099ed5c1d65ffe3e023003043ba0", size = 117740 }, + { url = "https://files.pythonhosted.org/packages/ab/12/24b9a6ef7b991b6722756e0aa169a39463af2b8ed0fb526f0a00aae34ea4/pyrsistent-0.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fed2c3216a605dc9a6ea50c7e84c82906e3684c4e80d2908208f662a6cbf9022", size = 117457 }, + { url = "https://files.pythonhosted.org/packages/19/3c/ab06510f86bc0934b77ade41948924ff1f33dcd3433f32feca2028218837/pyrsistent-0.20.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e14c95c16211d166f59c6611533d0dacce2e25de0f76e4c140fde250997b3ca", size = 114280 }, + { url = "https://files.pythonhosted.org/packages/ee/b1/1275bbfb929854d20e72aa2bbfb50ea3b1d7d41a95848b353691875e2817/pyrsistent-0.20.0-cp39-cp39-win32.whl", hash = "sha256:f058a615031eea4ef94ead6456f5ec2026c19fb5bd6bfe86e9665c4158cf802f", size = 60764 }, + { url = "https://files.pythonhosted.org/packages/28/77/0d7af973c0e3b1b83d8b45943601f77f85b943007e3a4d8744f7102c652b/pyrsistent-0.20.0-cp39-cp39-win_amd64.whl", hash = "sha256:58b8f6366e152092194ae68fefe18b9f0b4f89227dfd86a07770c3d86097aebf", size = 63289 }, + { url = "https://files.pythonhosted.org/packages/23/88/0acd180010aaed4987c85700b7cc17f9505f3edb4e5873e4dc67f613e338/pyrsistent-0.20.0-py3-none-any.whl", hash = "sha256:c55acc4733aad6560a7f5f818466631f07efc001fd023f34a6c203f8b6df0f0b", size = 58106 }, +] + +[[package]] +name = "pytest" +version = "7.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/1f/9d8e98e4133ffb16c90f3b405c43e38d3abb715bb5d7a63a5a684f7e46a3/pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", size = 1357116 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8", size = 325287 }, +] + +[[package]] +name = "pytest-asyncio" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/53/8844d99d5343eecbb6d740d708581fbf63cefd560c07c7164b12691e54eb/pytest-asyncio-0.16.0.tar.gz", hash = "sha256:7496c5977ce88c34379df64a66459fe395cd05543f0a2f837016e7144391fcfb", size = 12095 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/d0/d9bd672577857bb59004d7a0902abb5f27770c1d234860a08898eb058bd2/pytest_asyncio-0.16.0-py3-none-any.whl", hash = "sha256:5f2a21273c47b331ae6aa5b36087047b4899e40f03f18397c0e65fa5cca54e9b", size = 12119 }, +] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", version = "7.6.1", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.9'" }, + { name = "coverage", version = "7.6.10", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.9'" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/15/da3df99fd551507694a9b01f512a2f6cf1254f33601605843c3775f39460/pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6", size = 63245 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/4b/8b78d126e275efa2379b1c2e09dc52cf70df16fc3b90613ef82531499d73/pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a", size = 21949 }, +] + +[[package]] +name = "pytest-notebook" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "importlib-resources", marker = "python_full_version < '3.9'" }, + { name = "jsonschema" }, + { name = "nbclient" }, + { name = "nbdime" }, + { name = "nbformat" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/19/8f1ed184e268f00b25dedb90d03fa7b0d8595643398607646aa812d3fdeb/pytest_notebook-0.8.1.tar.gz", hash = "sha256:f7bf003049e01f8dfa95a46841d1e40f6f4c78836f1f4119c6a68d96f7c628fe", size = 3414395 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/9b/739eb19ea927c5e6589f2185452a9b5fffbba5276c39e631bfa11c633d96/pytest_notebook-0.8.1-py3-none-any.whl", hash = "sha256:b46b21ab164753d9d6ea52d27d2018b4a7b864214e67b14d63554370918cab91", size = 37505 }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + +[[package]] +name = "pytray" +version = "0.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecation" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a0/d2/dc063fbe32cd6adf7cdd4cb113d8a95f207a22b9e73da57971914f862539/pytray-0.3.4.tar.gz", hash = "sha256:55f9a858da4f4eb9b17f5f8cd3ad844f0d8d45a7c932e940bc28c4ef1da49cbc", size = 11735 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/1e/fdce85e621881efbef407338a54fc712507b72c3a5cd0efc3da65be44cd3/pytray-0.3.4-py2.py3-none-any.whl", hash = "sha256:8e97d20f738bdc5cbede7b1b7fb1ee19b7d4a2bcc798f71581ef3f8875ed5ee4", size = 11091 }, +] + +[[package]] +name = "pytz" +version = "2024.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/31/3c70bf7603cc2dca0f19bdc53b4537a797747a58875b552c8c413d963a3f/pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", size = 319692 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/c3/005fcca25ce078d2cc29fd559379817424e94885510568bc1bc53d7d5846/pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725", size = 508002 }, +] + +[[package]] +name = "pywin32" +version = "308" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/a6/3e9f2c474895c1bb61b11fa9640be00067b5c5b363c501ee9c3fa53aec01/pywin32-308-cp310-cp310-win32.whl", hash = "sha256:796ff4426437896550d2981b9c2ac0ffd75238ad9ea2d3bfa67a1abd546d262e", size = 5927028 }, + { url = "https://files.pythonhosted.org/packages/d9/b4/84e2463422f869b4b718f79eb7530a4c1693e96b8a4e5e968de38be4d2ba/pywin32-308-cp310-cp310-win_amd64.whl", hash = "sha256:4fc888c59b3c0bef905ce7eb7e2106a07712015ea1c8234b703a088d46110e8e", size = 6558484 }, + { url = "https://files.pythonhosted.org/packages/9f/8f/fb84ab789713f7c6feacaa08dad3ec8105b88ade8d1c4f0f0dfcaaa017d6/pywin32-308-cp310-cp310-win_arm64.whl", hash = "sha256:a5ab5381813b40f264fa3495b98af850098f814a25a63589a8e9eb12560f450c", size = 7971454 }, + { url = "https://files.pythonhosted.org/packages/eb/e2/02652007469263fe1466e98439831d65d4ca80ea1a2df29abecedf7e47b7/pywin32-308-cp311-cp311-win32.whl", hash = "sha256:5d8c8015b24a7d6855b1550d8e660d8daa09983c80e5daf89a273e5c6fb5095a", size = 5928156 }, + { url = "https://files.pythonhosted.org/packages/48/ef/f4fb45e2196bc7ffe09cad0542d9aff66b0e33f6c0954b43e49c33cad7bd/pywin32-308-cp311-cp311-win_amd64.whl", hash = "sha256:575621b90f0dc2695fec346b2d6302faebd4f0f45c05ea29404cefe35d89442b", size = 6559559 }, + { url = "https://files.pythonhosted.org/packages/79/ef/68bb6aa865c5c9b11a35771329e95917b5559845bd75b65549407f9fc6b4/pywin32-308-cp311-cp311-win_arm64.whl", hash = "sha256:100a5442b7332070983c4cd03f2e906a5648a5104b8a7f50175f7906efd16bb6", size = 7972495 }, + { url = "https://files.pythonhosted.org/packages/00/7c/d00d6bdd96de4344e06c4afbf218bc86b54436a94c01c71a8701f613aa56/pywin32-308-cp312-cp312-win32.whl", hash = "sha256:587f3e19696f4bf96fde9d8a57cec74a57021ad5f204c9e627e15c33ff568897", size = 5939729 }, + { url = "https://files.pythonhosted.org/packages/21/27/0c8811fbc3ca188f93b5354e7c286eb91f80a53afa4e11007ef661afa746/pywin32-308-cp312-cp312-win_amd64.whl", hash = "sha256:00b3e11ef09ede56c6a43c71f2d31857cf7c54b0ab6e78ac659497abd2834f47", size = 6543015 }, + { url = "https://files.pythonhosted.org/packages/9d/0f/d40f8373608caed2255781a3ad9a51d03a594a1248cd632d6a298daca693/pywin32-308-cp312-cp312-win_arm64.whl", hash = "sha256:9b4de86c8d909aed15b7011182c8cab38c8850de36e6afb1f0db22b8959e3091", size = 7976033 }, + { url = "https://files.pythonhosted.org/packages/a9/a4/aa562d8935e3df5e49c161b427a3a2efad2ed4e9cf81c3de636f1fdddfd0/pywin32-308-cp313-cp313-win32.whl", hash = "sha256:1c44539a37a5b7b21d02ab34e6a4d314e0788f1690d65b48e9b0b89f31abbbed", size = 5938579 }, + { url = "https://files.pythonhosted.org/packages/c7/50/b0efb8bb66210da67a53ab95fd7a98826a97ee21f1d22949863e6d588b22/pywin32-308-cp313-cp313-win_amd64.whl", hash = "sha256:fd380990e792eaf6827fcb7e187b2b4b1cede0585e3d0c9e84201ec27b9905e4", size = 6542056 }, + { url = "https://files.pythonhosted.org/packages/26/df/2b63e3e4f2df0224f8aaf6d131f54fe4e8c96400eb9df563e2aae2e1a1f9/pywin32-308-cp313-cp313-win_arm64.whl", hash = "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd", size = 7974986 }, + { url = "https://files.pythonhosted.org/packages/f3/0d/2c464011689e11ff5d64a32337f37de463a0cb058e45de5ea4027b56601a/pywin32-308-cp38-cp38-win32.whl", hash = "sha256:5794e764ebcabf4ff08c555b31bd348c9025929371763b2183172ff4708152f0", size = 5998793 }, + { url = "https://files.pythonhosted.org/packages/b7/e8/729b049e3c5c5449049d6036edf7a24a6ba785a9a1d5f617b638a9b444eb/pywin32-308-cp38-cp38-win_amd64.whl", hash = "sha256:3b92622e29d651c6b783e368ba7d6722b1634b8e70bd376fd7610fe1992e19de", size = 6647446 }, + { url = "https://files.pythonhosted.org/packages/a8/41/ead05a7657ffdbb1edabb954ab80825c4f87a3de0285d59f8290457f9016/pywin32-308-cp39-cp39-win32.whl", hash = "sha256:7873ca4dc60ab3287919881a7d4f88baee4a6e639aa6962de25a98ba6b193341", size = 5991824 }, + { url = "https://files.pythonhosted.org/packages/e4/cd/0838c9a6063bff2e9bac2388ae36524c26c50288b5d7b6aebb6cdf8d375d/pywin32-308-cp39-cp39-win_amd64.whl", hash = "sha256:71b3322d949b4cc20776436a9c9ba0eeedcbc9c650daa536df63f0ff111bb920", size = 6640327 }, +] + +[[package]] +name = "pywinpty" +version = "2.0.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/82/90f8750423cba4b9b6c842df227609fb60704482d7abf6dd47e2babc055a/pywinpty-2.0.14.tar.gz", hash = "sha256:18bd9529e4a5daf2d9719aa17788ba6013e594ae94c5a0c27e83df3278b0660e", size = 27769 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/09/56376af256eab8cc5f8982a3b138d387136eca27fa1a8a68660e8ed59e4b/pywinpty-2.0.14-cp310-none-win_amd64.whl", hash = "sha256:0b149c2918c7974f575ba79f5a4aad58bd859a52fa9eb1296cc22aa412aa411f", size = 1397115 }, + { url = "https://files.pythonhosted.org/packages/be/e2/af1a99c0432e4e58c9ac8e334ee191790ec9793d33559189b9d2069bdc1d/pywinpty-2.0.14-cp311-none-win_amd64.whl", hash = "sha256:cf2a43ac7065b3e0dc8510f8c1f13a75fb8fde805efa3b8cff7599a1ef497bc7", size = 1397223 }, + { url = "https://files.pythonhosted.org/packages/ad/79/759ae767a3b78d340446efd54dd1fe4f7dafa4fc7be96ed757e44bcdba54/pywinpty-2.0.14-cp312-none-win_amd64.whl", hash = "sha256:55dad362ef3e9408ade68fd173e4f9032b3ce08f68cfe7eacb2c263ea1179737", size = 1397207 }, + { url = "https://files.pythonhosted.org/packages/7d/34/b77b3c209bf2eaa6455390c8d5449241637f5957f41636a2204065d52bfa/pywinpty-2.0.14-cp313-none-win_amd64.whl", hash = "sha256:074fb988a56ec79ca90ed03a896d40707131897cefb8f76f926e3834227f2819", size = 1396698 }, + { url = "https://files.pythonhosted.org/packages/d8/ef/85e1b0ef7864fa2c579b1c1efce92c5f6fa238c8e73cf9f53deee08f8605/pywinpty-2.0.14-cp39-none-win_amd64.whl", hash = "sha256:5725fd56f73c0531ec218663bd8c8ff5acc43c78962fab28564871b5fce053fd", size = 1397396 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, + { url = "https://files.pythonhosted.org/packages/74/d9/323a59d506f12f498c2097488d80d16f4cf965cee1791eab58b56b19f47a/PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", size = 183218 }, + { url = "https://files.pythonhosted.org/packages/74/cc/20c34d00f04d785f2028737e2e2a8254e1425102e730fee1d6396f832577/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", size = 728067 }, + { url = "https://files.pythonhosted.org/packages/20/52/551c69ca1501d21c0de51ddafa8c23a0191ef296ff098e98358f69080577/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", size = 757812 }, + { url = "https://files.pythonhosted.org/packages/fd/7f/2c3697bba5d4aa5cc2afe81826d73dfae5f049458e44732c7a0938baa673/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", size = 746531 }, + { url = "https://files.pythonhosted.org/packages/8c/ab/6226d3df99900e580091bb44258fde77a8433511a86883bd4681ea19a858/PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", size = 800820 }, + { url = "https://files.pythonhosted.org/packages/a0/99/a9eb0f3e710c06c5d922026f6736e920d431812ace24aae38228d0d64b04/PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", size = 145514 }, + { url = "https://files.pythonhosted.org/packages/75/8a/ee831ad5fafa4431099aa4e078d4c8efd43cd5e48fbc774641d233b683a9/PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", size = 162702 }, + { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777 }, + { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318 }, + { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891 }, + { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614 }, + { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360 }, + { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006 }, + { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577 }, + { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593 }, + { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312 }, +] + +[[package]] +name = "pyzmq" +version = "26.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/05/bed626b9f7bb2322cdbbf7b4bd8f54b1b617b0d2ab2d3547d6e39428a48e/pyzmq-26.2.0.tar.gz", hash = "sha256:070672c258581c8e4f640b5159297580a9974b026043bd4ab0470be9ed324f1f", size = 271975 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/a8/9837c39aba390eb7d01924ace49d761c8dbe7bc2d6082346d00c8332e431/pyzmq-26.2.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:ddf33d97d2f52d89f6e6e7ae66ee35a4d9ca6f36eda89c24591b0c40205a3629", size = 1340058 }, + { url = "https://files.pythonhosted.org/packages/a2/1f/a006f2e8e4f7d41d464272012695da17fb95f33b54342612a6890da96ff6/pyzmq-26.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dacd995031a01d16eec825bf30802fceb2c3791ef24bcce48fa98ce40918c27b", size = 1008818 }, + { url = "https://files.pythonhosted.org/packages/b6/09/b51b6683fde5ca04593a57bbe81788b6b43114d8f8ee4e80afc991e14760/pyzmq-26.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89289a5ee32ef6c439086184529ae060c741334b8970a6855ec0b6ad3ff28764", size = 673199 }, + { url = "https://files.pythonhosted.org/packages/c9/78/486f3e2e824f3a645238332bf5a4c4b4477c3063033a27c1e4052358dee2/pyzmq-26.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5506f06d7dc6ecf1efacb4a013b1f05071bb24b76350832c96449f4a2d95091c", size = 911762 }, + { url = "https://files.pythonhosted.org/packages/5e/3b/2eb1667c9b866f53e76ee8b0c301b0469745a23bd5a87b7ee3d5dd9eb6e5/pyzmq-26.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ea039387c10202ce304af74def5021e9adc6297067f3441d348d2b633e8166a", size = 868773 }, + { url = "https://files.pythonhosted.org/packages/16/29/ca99b4598a9dc7e468b5417eda91f372b595be1e3eec9b7cbe8e5d3584e8/pyzmq-26.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a2224fa4a4c2ee872886ed00a571f5e967c85e078e8e8c2530a2fb01b3309b88", size = 868834 }, + { url = "https://files.pythonhosted.org/packages/ad/e5/9efaeb1d2f4f8c50da04144f639b042bc52869d3a206d6bf672ab3522163/pyzmq-26.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:28ad5233e9c3b52d76196c696e362508959741e1a005fb8fa03b51aea156088f", size = 1202861 }, + { url = "https://files.pythonhosted.org/packages/c3/62/c721b5608a8ac0a69bb83cbb7d07a56f3ff00b3991a138e44198a16f94c7/pyzmq-26.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1c17211bc037c7d88e85ed8b7d8f7e52db6dc8eca5590d162717c654550f7282", size = 1515304 }, + { url = "https://files.pythonhosted.org/packages/87/84/e8bd321aa99b72f48d4606fc5a0a920154125bd0a4608c67eab742dab087/pyzmq-26.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b8f86dd868d41bea9a5f873ee13bf5551c94cf6bc51baebc6f85075971fe6eea", size = 1414712 }, + { url = "https://files.pythonhosted.org/packages/cd/cd/420e3fd1ac6977b008b72e7ad2dae6350cc84d4c5027fc390b024e61738f/pyzmq-26.2.0-cp310-cp310-win32.whl", hash = "sha256:46a446c212e58456b23af260f3d9fb785054f3e3653dbf7279d8f2b5546b21c2", size = 578113 }, + { url = "https://files.pythonhosted.org/packages/5c/57/73930d56ed45ae0cb4946f383f985c855c9b3d4063f26416998f07523c0e/pyzmq-26.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:49d34ab71db5a9c292a7644ce74190b1dd5a3475612eefb1f8be1d6961441971", size = 641631 }, + { url = "https://files.pythonhosted.org/packages/61/d2/ae6ac5c397f1ccad59031c64beaafce7a0d6182e0452cc48f1c9c87d2dd0/pyzmq-26.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:bfa832bfa540e5b5c27dcf5de5d82ebc431b82c453a43d141afb1e5d2de025fa", size = 543528 }, + { url = "https://files.pythonhosted.org/packages/12/20/de7442172f77f7c96299a0ac70e7d4fb78cd51eca67aa2cf552b66c14196/pyzmq-26.2.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:8f7e66c7113c684c2b3f1c83cdd3376103ee0ce4c49ff80a648643e57fb22218", size = 1340639 }, + { url = "https://files.pythonhosted.org/packages/98/4d/5000468bd64c7910190ed0a6c76a1ca59a68189ec1f007c451dc181a22f4/pyzmq-26.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3a495b30fc91db2db25120df5847d9833af237546fd59170701acd816ccc01c4", size = 1008710 }, + { url = "https://files.pythonhosted.org/packages/e1/bf/c67fd638c2f9fbbab8090a3ee779370b97c82b84cc12d0c498b285d7b2c0/pyzmq-26.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77eb0968da535cba0470a5165468b2cac7772cfb569977cff92e240f57e31bef", size = 673129 }, + { url = "https://files.pythonhosted.org/packages/86/94/99085a3f492aa538161cbf27246e8886ff850e113e0c294a5b8245f13b52/pyzmq-26.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ace4f71f1900a548f48407fc9be59c6ba9d9aaf658c2eea6cf2779e72f9f317", size = 910107 }, + { url = "https://files.pythonhosted.org/packages/31/1d/346809e8a9b999646d03f21096428453465b1bca5cd5c64ecd048d9ecb01/pyzmq-26.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92a78853d7280bffb93df0a4a6a2498cba10ee793cc8076ef797ef2f74d107cf", size = 867960 }, + { url = "https://files.pythonhosted.org/packages/ab/68/6fb6ae5551846ad5beca295b7bca32bf0a7ce19f135cb30e55fa2314e6b6/pyzmq-26.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:689c5d781014956a4a6de61d74ba97b23547e431e9e7d64f27d4922ba96e9d6e", size = 869204 }, + { url = "https://files.pythonhosted.org/packages/0f/f9/18417771dee223ccf0f48e29adf8b4e25ba6d0e8285e33bcbce078070bc3/pyzmq-26.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0aca98bc423eb7d153214b2df397c6421ba6373d3397b26c057af3c904452e37", size = 1203351 }, + { url = "https://files.pythonhosted.org/packages/e0/46/f13e67fe0d4f8a2315782cbad50493de6203ea0d744610faf4d5f5b16e90/pyzmq-26.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f3496d76b89d9429a656293744ceca4d2ac2a10ae59b84c1da9b5165f429ad3", size = 1514204 }, + { url = "https://files.pythonhosted.org/packages/50/11/ddcf7343b7b7a226e0fc7b68cbf5a5bb56291fac07f5c3023bb4c319ebb4/pyzmq-26.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5c2b3bfd4b9689919db068ac6c9911f3fcb231c39f7dd30e3138be94896d18e6", size = 1414339 }, + { url = "https://files.pythonhosted.org/packages/01/14/1c18d7d5b7be2708f513f37c61bfadfa62161c10624f8733f1c8451b3509/pyzmq-26.2.0-cp311-cp311-win32.whl", hash = "sha256:eac5174677da084abf378739dbf4ad245661635f1600edd1221f150b165343f4", size = 576928 }, + { url = "https://files.pythonhosted.org/packages/3b/1b/0a540edd75a41df14ec416a9a500b9fec66e554aac920d4c58fbd5756776/pyzmq-26.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:5a509df7d0a83a4b178d0f937ef14286659225ef4e8812e05580776c70e155d5", size = 642317 }, + { url = "https://files.pythonhosted.org/packages/98/77/1cbfec0358078a4c5add529d8a70892db1be900980cdb5dd0898b3d6ab9d/pyzmq-26.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:c0e6091b157d48cbe37bd67233318dbb53e1e6327d6fc3bb284afd585d141003", size = 543834 }, + { url = "https://files.pythonhosted.org/packages/28/2f/78a766c8913ad62b28581777ac4ede50c6d9f249d39c2963e279524a1bbe/pyzmq-26.2.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:ded0fc7d90fe93ae0b18059930086c51e640cdd3baebdc783a695c77f123dcd9", size = 1343105 }, + { url = "https://files.pythonhosted.org/packages/b7/9c/4b1e2d3d4065be715e007fe063ec7885978fad285f87eae1436e6c3201f4/pyzmq-26.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:17bf5a931c7f6618023cdacc7081f3f266aecb68ca692adac015c383a134ca52", size = 1008365 }, + { url = "https://files.pythonhosted.org/packages/4f/ef/5a23ec689ff36d7625b38d121ef15abfc3631a9aecb417baf7a4245e4124/pyzmq-26.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55cf66647e49d4621a7e20c8d13511ef1fe1efbbccf670811864452487007e08", size = 665923 }, + { url = "https://files.pythonhosted.org/packages/ae/61/d436461a47437d63c6302c90724cf0981883ec57ceb6073873f32172d676/pyzmq-26.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4661c88db4a9e0f958c8abc2b97472e23061f0bc737f6f6179d7a27024e1faa5", size = 903400 }, + { url = "https://files.pythonhosted.org/packages/47/42/fc6d35ecefe1739a819afaf6f8e686f7f02a4dd241c78972d316f403474c/pyzmq-26.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea7f69de383cb47522c9c208aec6dd17697db7875a4674c4af3f8cfdac0bdeae", size = 860034 }, + { url = "https://files.pythonhosted.org/packages/07/3b/44ea6266a6761e9eefaa37d98fabefa112328808ac41aa87b4bbb668af30/pyzmq-26.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7f98f6dfa8b8ccaf39163ce872bddacca38f6a67289116c8937a02e30bbe9711", size = 860579 }, + { url = "https://files.pythonhosted.org/packages/38/6f/4df2014ab553a6052b0e551b37da55166991510f9e1002c89cab7ce3b3f2/pyzmq-26.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e3e0210287329272539eea617830a6a28161fbbd8a3271bf4150ae3e58c5d0e6", size = 1196246 }, + { url = "https://files.pythonhosted.org/packages/38/9d/ee240fc0c9fe9817f0c9127a43238a3e28048795483c403cc10720ddef22/pyzmq-26.2.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6b274e0762c33c7471f1a7471d1a2085b1a35eba5cdc48d2ae319f28b6fc4de3", size = 1507441 }, + { url = "https://files.pythonhosted.org/packages/85/4f/01711edaa58d535eac4a26c294c617c9a01f09857c0ce191fd574d06f359/pyzmq-26.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:29c6a4635eef69d68a00321e12a7d2559fe2dfccfa8efae3ffb8e91cd0b36a8b", size = 1406498 }, + { url = "https://files.pythonhosted.org/packages/07/18/907134c85c7152f679ed744e73e645b365f3ad571f38bdb62e36f347699a/pyzmq-26.2.0-cp312-cp312-win32.whl", hash = "sha256:989d842dc06dc59feea09e58c74ca3e1678c812a4a8a2a419046d711031f69c7", size = 575533 }, + { url = "https://files.pythonhosted.org/packages/ce/2c/a6f4a20202a4d3c582ad93f95ee78d79bbdc26803495aec2912b17dbbb6c/pyzmq-26.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:2a50625acdc7801bc6f74698c5c583a491c61d73c6b7ea4dee3901bb99adb27a", size = 637768 }, + { url = "https://files.pythonhosted.org/packages/5f/0e/eb16ff731632d30554bf5af4dbba3ffcd04518219d82028aea4ae1b02ca5/pyzmq-26.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:4d29ab8592b6ad12ebbf92ac2ed2bedcfd1cec192d8e559e2e099f648570e19b", size = 540675 }, + { url = "https://files.pythonhosted.org/packages/04/a7/0f7e2f6c126fe6e62dbae0bc93b1bd3f1099cf7fea47a5468defebe3f39d/pyzmq-26.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9dd8cd1aeb00775f527ec60022004d030ddc51d783d056e3e23e74e623e33726", size = 1006564 }, + { url = "https://files.pythonhosted.org/packages/31/b6/a187165c852c5d49f826a690857684333a6a4a065af0a6015572d2284f6a/pyzmq-26.2.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:28c812d9757fe8acecc910c9ac9dafd2ce968c00f9e619db09e9f8f54c3a68a3", size = 1340447 }, + { url = "https://files.pythonhosted.org/packages/68/ba/f4280c58ff71f321602a6e24fd19879b7e79793fb8ab14027027c0fb58ef/pyzmq-26.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d80b1dd99c1942f74ed608ddb38b181b87476c6a966a88a950c7dee118fdf50", size = 665485 }, + { url = "https://files.pythonhosted.org/packages/77/b5/c987a5c53c7d8704216f29fc3d810b32f156bcea488a940e330e1bcbb88d/pyzmq-26.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c997098cc65e3208eca09303630e84d42718620e83b733d0fd69543a9cab9cb", size = 903484 }, + { url = "https://files.pythonhosted.org/packages/29/c9/07da157d2db18c72a7eccef8e684cefc155b712a88e3d479d930aa9eceba/pyzmq-26.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ad1bc8d1b7a18497dda9600b12dc193c577beb391beae5cd2349184db40f187", size = 859981 }, + { url = "https://files.pythonhosted.org/packages/43/09/e12501bd0b8394b7d02c41efd35c537a1988da67fc9c745cae9c6c776d31/pyzmq-26.2.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:bea2acdd8ea4275e1278350ced63da0b166421928276c7c8e3f9729d7402a57b", size = 860334 }, + { url = "https://files.pythonhosted.org/packages/eb/ff/f5ec1d455f8f7385cc0a8b2acd8c807d7fade875c14c44b85c1bddabae21/pyzmq-26.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:23f4aad749d13698f3f7b64aad34f5fc02d6f20f05999eebc96b89b01262fb18", size = 1196179 }, + { url = "https://files.pythonhosted.org/packages/ec/8a/bb2ac43295b1950fe436a81fc5b298be0b96ac76fb029b514d3ed58f7b27/pyzmq-26.2.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:a4f96f0d88accc3dbe4a9025f785ba830f968e21e3e2c6321ccdfc9aef755115", size = 1507668 }, + { url = "https://files.pythonhosted.org/packages/a9/49/dbc284ebcfd2dca23f6349227ff1616a7ee2c4a35fe0a5d6c3deff2b4fed/pyzmq-26.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ced65e5a985398827cc9276b93ef6dfabe0273c23de8c7931339d7e141c2818e", size = 1406539 }, + { url = "https://files.pythonhosted.org/packages/00/68/093cdce3fe31e30a341d8e52a1ad86392e13c57970d722c1f62a1d1a54b6/pyzmq-26.2.0-cp313-cp313-win32.whl", hash = "sha256:31507f7b47cc1ead1f6e86927f8ebb196a0bab043f6345ce070f412a59bf87b5", size = 575567 }, + { url = "https://files.pythonhosted.org/packages/92/ae/6cc4657148143412b5819b05e362ae7dd09fb9fe76e2a539dcff3d0386bc/pyzmq-26.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:70fc7fcf0410d16ebdda9b26cbd8bf8d803d220a7f3522e060a69a9c87bf7bad", size = 637551 }, + { url = "https://files.pythonhosted.org/packages/6c/67/fbff102e201688f97c8092e4c3445d1c1068c2f27bbd45a578df97ed5f94/pyzmq-26.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:c3789bd5768ab5618ebf09cef6ec2b35fed88709b104351748a63045f0ff9797", size = 540378 }, + { url = "https://files.pythonhosted.org/packages/3f/fe/2d998380b6e0122c6c4bdf9b6caf490831e5f5e2d08a203b5adff060c226/pyzmq-26.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:034da5fc55d9f8da09015d368f519478a52675e558c989bfcb5cf6d4e16a7d2a", size = 1007378 }, + { url = "https://files.pythonhosted.org/packages/4a/f4/30d6e7157f12b3a0390bde94d6a8567cdb88846ed068a6e17238a4ccf600/pyzmq-26.2.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:c92d73464b886931308ccc45b2744e5968cbaade0b1d6aeb40d8ab537765f5bc", size = 1329532 }, + { url = "https://files.pythonhosted.org/packages/82/86/3fe917870e15ee1c3ad48229a2a64458e36036e64b4afa9659045d82bfa8/pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:794a4562dcb374f7dbbfb3f51d28fb40123b5a2abadee7b4091f93054909add5", size = 653242 }, + { url = "https://files.pythonhosted.org/packages/50/2d/242e7e6ef6c8c19e6cb52d095834508cd581ffb925699fd3c640cdc758f1/pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aee22939bb6075e7afededabad1a56a905da0b3c4e3e0c45e75810ebe3a52672", size = 888404 }, + { url = "https://files.pythonhosted.org/packages/ac/11/7270566e1f31e4ea73c81ec821a4b1688fd551009a3d2bab11ec66cb1e8f/pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ae90ff9dad33a1cfe947d2c40cb9cb5e600d759ac4f0fd22616ce6540f72797", size = 845858 }, + { url = "https://files.pythonhosted.org/packages/91/d5/72b38fbc69867795c8711bdd735312f9fef1e3d9204e2f63ab57085434b9/pyzmq-26.2.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:43a47408ac52647dfabbc66a25b05b6a61700b5165807e3fbd40063fcaf46386", size = 847375 }, + { url = "https://files.pythonhosted.org/packages/dd/9a/10ed3c7f72b4c24e719c59359fbadd1a27556a28b36cdf1cd9e4fb7845d5/pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:25bf2374a2a8433633c65ccb9553350d5e17e60c8eb4de4d92cc6bd60f01d306", size = 1183489 }, + { url = "https://files.pythonhosted.org/packages/72/2d/8660892543fabf1fe41861efa222455811adac9f3c0818d6c3170a1153e3/pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:007137c9ac9ad5ea21e6ad97d3489af654381324d5d3ba614c323f60dab8fae6", size = 1492932 }, + { url = "https://files.pythonhosted.org/packages/7b/d6/32fd69744afb53995619bc5effa2a405ae0d343cd3e747d0fbc43fe894ee/pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:470d4a4f6d48fb34e92d768b4e8a5cc3780db0d69107abf1cd7ff734b9766eb0", size = 1392485 }, + { url = "https://files.pythonhosted.org/packages/64/e7/d5d59205d446c299001d27bfc18702c5353512c5485b11ec7cf6df9552d7/pyzmq-26.2.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:2eb7735ee73ca1b0d71e0e67c3739c689067f055c764f73aac4cc8ecf958ee3f", size = 1340492 }, + { url = "https://files.pythonhosted.org/packages/59/bb/aa6616a83694ab43cfb3bdb868d194a5ee2fa24b49e6ec7ec4400691ac3b/pyzmq-26.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a534f43bc738181aa7cbbaf48e3eca62c76453a40a746ab95d4b27b1111a7d2", size = 1008257 }, + { url = "https://files.pythonhosted.org/packages/a6/b6/e578e6c08970df0daa08b7c54e82b606211f9a7e61317ef2db79cc334389/pyzmq-26.2.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:aedd5dd8692635813368e558a05266b995d3d020b23e49581ddd5bbe197a8ab6", size = 907602 }, + { url = "https://files.pythonhosted.org/packages/ab/3a/a26b98aebeb7924b24e9973a2f5bf8974201bb5a3f6ed06ddc3bac19372d/pyzmq-26.2.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8be4700cd8bb02cc454f630dcdf7cfa99de96788b80c51b60fe2fe1dac480289", size = 862291 }, + { url = "https://files.pythonhosted.org/packages/c1/b5/7eedb8d63af13c2858beb9c1f58e90e7e00929176b57f45e3592fccd56dc/pyzmq-26.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fcc03fa4997c447dce58264e93b5aa2d57714fbe0f06c07b7785ae131512732", size = 673879 }, + { url = "https://files.pythonhosted.org/packages/af/22/38734f47543e61b4eb97eee476f0f7ae544988533215eea22fc65e1ca1d7/pyzmq-26.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:402b190912935d3db15b03e8f7485812db350d271b284ded2b80d2e5704be780", size = 1207011 }, + { url = "https://files.pythonhosted.org/packages/59/a4/104cc979ae88ed948ef829db5fb49bca4a771891125fa4166bba1598b2ec/pyzmq-26.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8685fa9c25ff00f550c1fec650430c4b71e4e48e8d852f7ddcf2e48308038640", size = 1516183 }, + { url = "https://files.pythonhosted.org/packages/52/8f/73a8e08897f8ed21fe44fc73b5faf3ea4cacb97bfd219a63ee5f3ea203a8/pyzmq-26.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:76589c020680778f06b7e0b193f4b6dd66d470234a16e1df90329f5e14a171cd", size = 1417481 }, + { url = "https://files.pythonhosted.org/packages/67/cf/f418670a83fb3a91e2d6d26f271a828a58e0265199944a76e4ef274f9ba7/pyzmq-26.2.0-cp38-cp38-win32.whl", hash = "sha256:8423c1877d72c041f2c263b1ec6e34360448decfb323fa8b94e85883043ef988", size = 577930 }, + { url = "https://files.pythonhosted.org/packages/f0/51/1f2b47c8d8fb85c07f088e21df6364b8b5e8298e75bb23ea0e65340ebd82/pyzmq-26.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:76589f2cd6b77b5bdea4fca5992dc1c23389d68b18ccc26a53680ba2dc80ff2f", size = 642503 }, + { url = "https://files.pythonhosted.org/packages/ac/9e/ad5fbbe1bcc7a9d1e8c5f4f7de48f2c1dc481e151ef80cc1ce9a7fe67b55/pyzmq-26.2.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:b1d464cb8d72bfc1a3adc53305a63a8e0cac6bc8c5a07e8ca190ab8d3faa43c2", size = 1341256 }, + { url = "https://files.pythonhosted.org/packages/4c/d9/d7a8022108c214803a82b0b69d4885cee00933d21928f1f09dca371cf4bf/pyzmq-26.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4da04c48873a6abdd71811c5e163bd656ee1b957971db7f35140a2d573f6949c", size = 1009385 }, + { url = "https://files.pythonhosted.org/packages/ed/69/0529b59ac667ea8bfe8796ac71796b688fbb42ff78e06525dabfed3bc7ae/pyzmq-26.2.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d049df610ac811dcffdc147153b414147428567fbbc8be43bb8885f04db39d98", size = 908009 }, + { url = "https://files.pythonhosted.org/packages/6e/bd/3ff3e1172f12f55769793a3a334e956ec2886805ebfb2f64756b6b5c6a1a/pyzmq-26.2.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05590cdbc6b902101d0e65d6a4780af14dc22914cc6ab995d99b85af45362cc9", size = 862078 }, + { url = "https://files.pythonhosted.org/packages/c3/ec/ab13585c3a1f48e2874253844c47b194d56eb25c94718691349c646f336f/pyzmq-26.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c811cfcd6a9bf680236c40c6f617187515269ab2912f3d7e8c0174898e2519db", size = 673756 }, + { url = "https://files.pythonhosted.org/packages/1e/be/febcd4b04dd50ee6d514dfbc33a3d5d9cb38ec9516e02bbfc929baa0f141/pyzmq-26.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6835dd60355593de10350394242b5757fbbd88b25287314316f266e24c61d073", size = 1203684 }, + { url = "https://files.pythonhosted.org/packages/16/28/304150e71afd2df3b82f52f66c0d8ab9ac6fe1f1ffdf92bad4c8cc91d557/pyzmq-26.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc6bee759a6bddea5db78d7dcd609397449cb2d2d6587f48f3ca613b19410cfc", size = 1515864 }, + { url = "https://files.pythonhosted.org/packages/18/89/8d48d8cd505c12a1f5edee597cc32ffcedc65fd8d2603aebaaedc38a7041/pyzmq-26.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c530e1eecd036ecc83c3407f77bb86feb79916d4a33d11394b8234f3bd35b940", size = 1415383 }, + { url = "https://files.pythonhosted.org/packages/d4/7e/43a60c3b179f7da0cbc2b649bd2702fd6a39bff5f72aa38d6e1aeb00256d/pyzmq-26.2.0-cp39-cp39-win32.whl", hash = "sha256:367b4f689786fca726ef7a6c5ba606958b145b9340a5e4808132cc65759abd44", size = 578540 }, + { url = "https://files.pythonhosted.org/packages/3a/55/8841dcd28f783ad06674c8fe8d7d72794b548d0bff8829aaafeb72e8b44d/pyzmq-26.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:e6fa2e3e683f34aea77de8112f6483803c96a44fd726d7358b9888ae5bb394ec", size = 642147 }, + { url = "https://files.pythonhosted.org/packages/b4/78/b3c31ccfcfcdd6ea50b6abc8f46a2a7aadb9c3d40531d1b908d834aaa12e/pyzmq-26.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:7445be39143a8aa4faec43b076e06944b8f9d0701b669df4af200531b21e40bb", size = 543903 }, + { url = "https://files.pythonhosted.org/packages/53/fb/36b2b2548286e9444e52fcd198760af99fd89102b5be50f0660fcfe902df/pyzmq-26.2.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:706e794564bec25819d21a41c31d4df2d48e1cc4b061e8d345d7fb4dd3e94072", size = 906955 }, + { url = "https://files.pythonhosted.org/packages/77/8f/6ce54f8979a01656e894946db6299e2273fcee21c8e5fa57c6295ef11f57/pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b435f2753621cd36e7c1762156815e21c985c72b19135dac43a7f4f31d28dd1", size = 565701 }, + { url = "https://files.pythonhosted.org/packages/ee/1c/bf8cd66730a866b16db8483286078892b7f6536f8c389fb46e4beba0a970/pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:160c7e0a5eb178011e72892f99f918c04a131f36056d10d9c1afb223fc952c2d", size = 794312 }, + { url = "https://files.pythonhosted.org/packages/71/43/91fa4ff25bbfdc914ab6bafa0f03241d69370ef31a761d16bb859f346582/pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c4a71d5d6e7b28a47a394c0471b7e77a0661e2d651e7ae91e0cab0a587859ca", size = 752775 }, + { url = "https://files.pythonhosted.org/packages/ec/d2/3b2ab40f455a256cb6672186bea95cd97b459ce4594050132d71e76f0d6f/pyzmq-26.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:90412f2db8c02a3864cbfc67db0e3dcdbda336acf1c469526d3e869394fe001c", size = 550762 }, + { url = "https://files.pythonhosted.org/packages/38/a7/1c80b0c8013befad391b92ba8a8e597de8884605ad5ad8ab943c888eb3ca/pyzmq-26.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:39887ac397ff35b7b775db7201095fc6310a35fdbae85bac4523f7eb3b840e20", size = 906946 }, + { url = "https://files.pythonhosted.org/packages/9c/ac/34a7ee2e7edb07c7222752096650313424eb05f18401ed0a964e996088fb/pyzmq-26.2.0-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fdb5b3e311d4d4b0eb8b3e8b4d1b0a512713ad7e6a68791d0923d1aec433d919", size = 802021 }, + { url = "https://files.pythonhosted.org/packages/cd/70/c65ddccfb88b469b6044f9664c81f0b7f649711e0dc172cba8b2a968ad99/pyzmq-26.2.0-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:226af7dcb51fdb0109f0016449b357e182ea0ceb6b47dfb5999d569e5db161d5", size = 756818 }, + { url = "https://files.pythonhosted.org/packages/07/7a/fc77f6d57f592207403eab2deca4c6f1ffa9c78b0f03b59e69069a12a1a1/pyzmq-26.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bed0e799e6120b9c32756203fb9dfe8ca2fb8467fed830c34c877e25638c3fc", size = 565698 }, + { url = "https://files.pythonhosted.org/packages/dc/13/e8494ba2d161fb471955fadbef7f48076bd29b19a4dd3c5d61d22e500505/pyzmq-26.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:29c7947c594e105cb9e6c466bace8532dc1ca02d498684128b339799f5248277", size = 550757 }, + { url = "https://files.pythonhosted.org/packages/6c/78/3096d72581365dfb0081ac9512a3b53672fa69854aa174d78636510c4db8/pyzmq-26.2.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cdeabcff45d1c219636ee2e54d852262e5c2e085d6cb476d938aee8d921356b3", size = 906945 }, + { url = "https://files.pythonhosted.org/packages/da/f2/8054574d77c269c31d055d4daf3d8407adf61ea384a50c8d14b158551d09/pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35cffef589bcdc587d06f9149f8d5e9e8859920a071df5a2671de2213bef592a", size = 565698 }, + { url = "https://files.pythonhosted.org/packages/77/21/c3ad93236d1d60eea10b67528f55e7db115a9d32e2bf163fcf601f85e9cc/pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18c8dc3b7468d8b4bdf60ce9d7141897da103c7a4690157b32b60acb45e333e6", size = 794307 }, + { url = "https://files.pythonhosted.org/packages/6a/49/e95b491724500fcb760178ce8db39b923429e328e57bcf9162e32c2c187c/pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7133d0a1677aec369d67dd78520d3fa96dd7f3dcec99d66c1762870e5ea1a50a", size = 752769 }, + { url = "https://files.pythonhosted.org/packages/9b/a9/50c9c06762b30792f71aaad8d1886748d39c4bffedc1171fbc6ad2b92d67/pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6a96179a24b14fa6428cbfc08641c779a53f8fcec43644030328f44034c7f1f4", size = 751338 }, + { url = "https://files.pythonhosted.org/packages/ca/63/27e6142b4f67a442ee480986ca5b88edb01462dd2319843057683a5148bd/pyzmq-26.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4f78c88905461a9203eac9faac157a2a0dbba84a0fd09fd29315db27be40af9f", size = 550757 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3", version = "2.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "urllib3", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "send2trash" +version = "1.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/3a/aec9b02217bb79b87bbc1a21bc6abc51e3d5dcf65c30487ac96c0908c722/Send2Trash-1.8.3.tar.gz", hash = "sha256:b18e7a3966d99871aefeb00cfbcfdced55ce4871194810fc71f4aa484b953abf", size = 17394 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/b0/4562db6223154aa4e22f939003cb92514c79f3d4dccca3444253fd17f902/Send2Trash-1.8.3-py3-none-any.whl", hash = "sha256:0c31227e0bd08961c7665474a3d1ef7193929fedda4233843689baa056be46c9", size = 18072 }, +] + +[[package]] +name = "setuptools" +version = "75.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/22/a438e0caa4576f8c383fa4d35f1cc01655a46c75be358960d815bfbb12bd/setuptools-75.3.0.tar.gz", hash = "sha256:fba5dd4d766e97be1b1681d98712680ae8f2f26d7881245f2ce9e40714f1a686", size = 1351577 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/12/282ee9bce8b58130cb762fbc9beabd531549952cac11fc56add11dcb7ea0/setuptools-75.3.0-py3-none-any.whl", hash = "sha256:f2504966861356aa38616760c0f66568e535562374995367b4e69c7143cf6bcd", size = 1251070 }, +] + +[[package]] +name = "setuptools" +version = "75.8.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/92/ec/089608b791d210aec4e7f97488e67ab0d33add3efccb83a056cbafe3a2a6/setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6", size = 1343222 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/8a/b9dc7678803429e4a3bc9ba462fa3dd9066824d3c607490235c6a796be5a/setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3", size = 1228782 }, +] + +[[package]] +name = "shortuuid" +version = "1.0.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/c2/31dc2345d8e06711f3da9d65e3a72a060293057321815bc7f11a930c2529/shortuuid-1.0.8.tar.gz", hash = "sha256:9435e87e5a64f3b92f7110c81f989a3b7bdb9358e22d2359829167da476cfc23", size = 8302 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/1b/dda73524fc8dd5cd3b80adcc585a49b3f43f8889453d2ed96291b2fcc860/shortuuid-1.0.8-py3-none-any.whl", hash = "sha256:44a7a86bcf24dbaba2e626cf80c779926b7c3a0d31a3a013e0d3cd1077707d23", size = 9458 }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "smmap" +version = "5.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/7b/af302bebf22c749c56c9c3e8ae13190b5b5db37a33d9068652e8f73b7089/snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", size = 86699 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a", size = 93002 }, +] + +[[package]] +name = "soupsieve" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/ce/fbaeed4f9fb8b2daa961f90591662df6a86c1abf25c548329a86920aedfb/soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", size = 101569 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9", size = 36186 }, +] + +[[package]] +name = "sphinx" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alabaster", version = "0.7.13", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "alabaster", version = "0.7.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils", version = "0.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.9.*'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "setuptools", version = "75.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "setuptools", version = "75.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp", version = "1.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "sphinxcontrib-applehelp", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "sphinxcontrib-devhelp", version = "1.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "sphinxcontrib-devhelp", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "sphinxcontrib-htmlhelp", version = "2.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "sphinxcontrib-htmlhelp", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp", version = "1.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "sphinxcontrib-qthelp", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "sphinxcontrib-serializinghtml", version = "1.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "sphinxcontrib-serializinghtml", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/58/48268b16bf3e6e8288c4c6f3d500e4dd1ca0210289a5be8366bd6d2e6088/Sphinx-3.2.1.tar.gz", hash = "sha256:321d6d9b16fa381a5306e5a0b76cd48ffbc588e6340059a729c6fdd66087e0e8", size = 5970067 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/b8/34ba32a94cb2b223b941e43b3bcab11281763b95daa8587879eec1eb9a62/Sphinx-3.2.1-py3-none-any.whl", hash = "sha256:ce6fd7ff5b215af39e2fcd44d4a321f6694b4530b6f2b2109b64d120773faea0", size = 2868133 }, +] + +[[package]] +name = "sphinx-autobuild" +version = "2021.3.14" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.9'" }, + { name = "livereload", marker = "python_full_version < '3.9'" }, + { name = "sphinx", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/a5/2ed1b81e398bc14533743be41bf0ceaa49d671675f131c4d9ce74897c9c1/sphinx-autobuild-2021.3.14.tar.gz", hash = "sha256:de1ca3b66e271d2b5b5140c35034c89e47f263f2cd5db302c9217065f7443f05", size = 206402 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/7d/8fb7557b6c9298d2bcda57f4d070de443c6355dfb475582378e2aa16a02c/sphinx_autobuild-2021.3.14-py3-none-any.whl", hash = "sha256:8fe8cbfdb75db04475232f05187c776f46f6e9e04cacf1e49ce81bdac649ccac", size = 9881 }, +] + +[[package]] +name = "sphinx-autobuild" +version = "2024.10.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.9'" }, + { name = "sphinx", marker = "python_full_version >= '3.9'" }, + { name = "starlette", marker = "python_full_version >= '3.9'" }, + { name = "uvicorn", marker = "python_full_version >= '3.9'" }, + { name = "watchfiles", marker = "python_full_version >= '3.9'" }, + { name = "websockets", marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/2c/155e1de2c1ba96a72e5dba152c509a8b41e047ee5c2def9e9f0d812f8be7/sphinx_autobuild-2024.10.3.tar.gz", hash = "sha256:248150f8f333e825107b6d4b86113ab28fa51750e5f9ae63b59dc339be951fb1", size = 14023 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/c0/eba125db38c84d3c74717008fd3cb5000b68cd7e2cbafd1349c6a38c3d3b/sphinx_autobuild-2024.10.3-py3-none-any.whl", hash = "sha256:158e16c36f9d633e613c9aaf81c19b0fc458ca78b112533b20dafcda430d60fa", size = 11908 }, +] + +[[package]] +name = "sphinx-book-theme" +version = "0.0.42" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "click" }, + { name = "docutils", version = "0.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.9.*'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "pydata-sphinx-theme" }, + { name = "pyyaml" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/ac/704a3e5bbf0aab437851ffd6a5ff366e8f5bf797d44d7eb6dd05e9d39975/sphinx-book-theme-0.0.42.tar.gz", hash = "sha256:a67d3ead308eedec048d52d0ef0f958795f432464b9db02d6612a5697bcf9e33", size = 56585 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/da/426f72e1c45f0e1394c21a872d2a610370f3950293b35d2ed0d773284b7f/sphinx_book_theme-0.0.42-py3-none-any.whl", hash = "sha256:ce958d2c6d91573215f0f591bf97c68f722be313e3c0a19983ab571cc642aed3", size = 89459 }, +] + +[[package]] +name = "sphinx-togglebutton" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils", version = "0.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.9.*'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "setuptools", version = "75.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "setuptools", version = "75.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "sphinx" }, + { name = "wheel" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/e7/cfe952ad8de462080eaebb41108994d5c822b4911fbb65ecb1ec79d25446/sphinx-togglebutton-0.2.3.tar.gz", hash = "sha256:41cbe2f87459eade8dc5718bb56146e8e113a05fb97459b90472470f0d357b55", size = 5411 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/42/deaa3b89f7617cee51df70c1bcecaf885ab3d59302a2b96ce89d85da118a/sphinx_togglebutton-0.2.3-py3-none-any.whl", hash = "sha256:8a3707154b1b3480a7918f189f43b7eee0d34ffa552895af77bb273476b8d5e0", size = 6144 }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/32/df/45e827f4d7e7fcc84e853bcef1d836effd762d63ccb86f43ede4e98b478c/sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e", size = 24766 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/c1/5e2cafbd03105ce50d8500f9b4e8a6e8d02e22d0475b574c3b3e9451a15f/sphinxcontrib_applehelp-1.0.4-py3-none-any.whl", hash = "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228", size = 120601 }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300 }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/98/33/dc28393f16385f722c893cb55539c641c9aaec8d1bc1c15b69ce0ac2dbb3/sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4", size = 17398 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/09/5de5ed43a521387f18bdf5f5af31d099605c992fd25372b2b9b825ce48ee/sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e", size = 84690 }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530 }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/47/64cff68ea3aa450c373301e5bebfbb9fce0a3e70aca245fcadd4af06cd75/sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff", size = 27967 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/ee/a1f5e39046cbb5f8bc8fba87d1ddf1c6643fbc9194e58d26e606de4b9074/sphinxcontrib_htmlhelp-2.0.1-py3-none-any.whl", hash = "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903", size = 99833 }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705 }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071 }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/8e/c4846e59f38a5f2b4a0e3b27af38f2fcf904d4bfd82095bf92de0b114ebd/sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72", size = 21658 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/14/05f9206cf4e9cfca1afb5fd224c7cd434dcc3a433d6d9e4e0264d29c6cdb/sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6", size = 90609 }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743 }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "1.1.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/72/835d6fadb9e5d02304cf39b18f93d227cd93abd3c41ebf58e6853eeb1455/sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952", size = 21019 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/77/5464ec50dd0f1c1037e3c93249b040c8fc8078fdda97530eeb02424b6eea/sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd", size = 94021 }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072 }, +] + +[[package]] +name = "sqlalchemy" +version = "1.4.54" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/af/20290b55d469e873cba9d41c0206ab5461ff49d759989b3fe65010f9d265/sqlalchemy-1.4.54.tar.gz", hash = "sha256:4470fbed088c35dc20b78a39aaf4ae54fe81790c783b3264872a0224f437c31a", size = 8470350 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/7f/f7c1e0b65790649bd573f201aa958263a389f336d6e000a569275ff9bd97/SQLAlchemy-1.4.54-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:af00236fe21c4d4f4c227b6ccc19b44c594160cc3ff28d104cdce85855369277", size = 1573472 }, + { url = "https://files.pythonhosted.org/packages/e1/da/ff7f0fe50844496db523613979651f076f44da8625b8ad89c503dcff0a52/SQLAlchemy-1.4.54-cp310-cp310-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1183599e25fa38a1a322294b949da02b4f0da13dbc2688ef9dbe746df573f8a6", size = 1639088 }, + { url = "https://files.pythonhosted.org/packages/04/45/3a35bb156aa2fd87b66a4992bb8d65593efd7e16ca2e0597e68c32c29037/SQLAlchemy-1.4.54-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1990d5a6a5dc358a0894c8ca02043fb9a5ad9538422001fb2826e91c50f1d539", size = 1627447 }, + { url = "https://files.pythonhosted.org/packages/fe/5b/ed36a50e7147d0d090cd8e35de3b18d2c69a3e85df3be5fe42a570d6c331/SQLAlchemy-1.4.54-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:14b3f4783275339170984cadda66e3ec011cce87b405968dc8d51cf0f9997b0d", size = 1639081 }, + { url = "https://files.pythonhosted.org/packages/4b/75/bfbdeb5dece7bc98acb414751a62ee43398b34b10133b1853f4282597757/SQLAlchemy-1.4.54-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b24364150738ce488333b3fb48bfa14c189a66de41cd632796fbcacb26b4585", size = 1638975 }, + { url = "https://files.pythonhosted.org/packages/f7/62/358a9291d2fc3d51ad50557e126ad5f48200f199878437f7cb38817d607b/SQLAlchemy-1.4.54-cp310-cp310-win32.whl", hash = "sha256:a8a72259a1652f192c68377be7011eac3c463e9892ef2948828c7d58e4829988", size = 1591719 }, + { url = "https://files.pythonhosted.org/packages/10/ad/87cd5578efdcef43a08ce4a21448192abf46bf69a5678ac0039e44364914/SQLAlchemy-1.4.54-cp310-cp310-win_amd64.whl", hash = "sha256:b67589f7955924865344e6eacfdcf70675e64f36800a576aa5e961f0008cde2a", size = 1593512 }, + { url = "https://files.pythonhosted.org/packages/da/49/fb98983b5568e93696a25fd5bec1b789095b79a72d5f57c6effddaa81d0a/SQLAlchemy-1.4.54-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b05e0626ec1c391432eabb47a8abd3bf199fb74bfde7cc44a26d2b1b352c2c6e", size = 1589301 }, + { url = "https://files.pythonhosted.org/packages/03/98/5a81430bbd646991346cb088a2bdc84d1bcd3dbe6b0cfc1aaa898370e5c7/SQLAlchemy-1.4.54-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13e91d6892b5fcb94a36ba061fb7a1f03d0185ed9d8a77c84ba389e5bb05e936", size = 1629553 }, + { url = "https://files.pythonhosted.org/packages/f1/17/14e35db2b0d6deaa27691d014addbb0dd6f7e044f7ee465446a3c0c71404/SQLAlchemy-1.4.54-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb59a11689ff3c58e7652260127f9e34f7f45478a2f3ef831ab6db7bcd72108f", size = 1627640 }, + { url = "https://files.pythonhosted.org/packages/98/62/335006a8f2c98f704f391e1a0cc01446d1b1b9c198f579f03599f55bd860/SQLAlchemy-1.4.54-cp311-cp311-win32.whl", hash = "sha256:1390ca2d301a2708fd4425c6d75528d22f26b8f5cbc9faba1ddca136671432bc", size = 1591723 }, + { url = "https://files.pythonhosted.org/packages/e2/a1/6b4b8c07082920f5445ec65c221fa33baab102aced5dcc2d87a15d3f8db4/SQLAlchemy-1.4.54-cp311-cp311-win_amd64.whl", hash = "sha256:2b37931eac4b837c45e2522066bda221ac6d80e78922fb77c75eb12e4dbcdee5", size = 1593511 }, + { url = "https://files.pythonhosted.org/packages/a5/1b/aa9b99be95d1615f058b5827447c18505b7b3f1dfcbd6ce1b331c2107152/SQLAlchemy-1.4.54-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:3f01c2629a7d6b30d8afe0326b8c649b74825a0e1ebdcb01e8ffd1c920deb07d", size = 1589983 }, + { url = "https://files.pythonhosted.org/packages/59/47/cb0fc64e5344f0a3d02216796c342525ab283f8f052d1c31a1d487d08aa0/SQLAlchemy-1.4.54-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c24dd161c06992ed16c5e528a75878edbaeced5660c3db88c820f1f0d3fe1f4", size = 1630158 }, + { url = "https://files.pythonhosted.org/packages/c0/8b/f45dd378f6c97e8ff9332ff3d03ecb0b8c491be5bb7a698783b5a2f358ec/SQLAlchemy-1.4.54-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5e0d47d619c739bdc636bbe007da4519fc953393304a5943e0b5aec96c9877c", size = 1629232 }, + { url = "https://files.pythonhosted.org/packages/0d/3c/884fe389f5bec86a310b81e79abaa1e26e5d78dc10a84d544a6822833e47/SQLAlchemy-1.4.54-cp312-cp312-win32.whl", hash = "sha256:12bc0141b245918b80d9d17eca94663dbd3f5266ac77a0be60750f36102bbb0f", size = 1592027 }, + { url = "https://files.pythonhosted.org/packages/01/c3/c690d037be57efd3a69cde16a2ef1bd2a905dafe869434d33836de0983d0/SQLAlchemy-1.4.54-cp312-cp312-win_amd64.whl", hash = "sha256:f941aaf15f47f316123e1933f9ea91a6efda73a161a6ab6046d1cde37be62c88", size = 1593827 }, + { url = "https://files.pythonhosted.org/packages/f2/04/66ce6c75569f0b9627b470de66f60a0cc6412fe3203b1fb8d433b9df5f84/SQLAlchemy-1.4.54-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:a49730afb716f3f675755afec109895cab95bc9875db7ffe2e42c1b1c6279482", size = 1573468 }, + { url = "https://files.pythonhosted.org/packages/9b/2f/6a2984e9e572996baf84c2595ee6667d239329621979d02e5dbb42964f71/SQLAlchemy-1.4.54-cp38-cp38-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26e78444bc77d089e62874dc74df05a5c71f01ac598010a327881a48408d0064", size = 1640879 }, + { url = "https://files.pythonhosted.org/packages/a0/de/0275a86ee1b8c87823202d11978289a56f44a7ac39623c2ddea91af0a4ee/SQLAlchemy-1.4.54-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02d2ecb9508f16ab9c5af466dfe5a88e26adf2e1a8d1c56eb616396ccae2c186", size = 1629504 }, + { url = "https://files.pythonhosted.org/packages/70/27/b4229d89862a15ccc4834daf24b9182feac686937ff1a2da5f5ea47b3f5d/SQLAlchemy-1.4.54-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:394b0135900b62dbf63e4809cdc8ac923182af2816d06ea61cd6763943c2cc05", size = 1640873 }, + { url = "https://files.pythonhosted.org/packages/02/0c/b2d0a3c6f90ba19c77f4d02e16ddcbb05cf05fe56860be37053907afa31c/SQLAlchemy-1.4.54-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ed3576675c187e3baa80b02c4c9d0edfab78eff4e89dd9da736b921333a2432", size = 1640766 }, + { url = "https://files.pythonhosted.org/packages/e6/72/68fdcbed8ff41db8ec9cc63d6a72bd6e10ae2dee80492a240c726e42bbbc/SQLAlchemy-1.4.54-cp38-cp38-win32.whl", hash = "sha256:fc9ffd9a38e21fad3e8c5a88926d57f94a32546e937e0be46142b2702003eba7", size = 1591725 }, + { url = "https://files.pythonhosted.org/packages/5a/c8/df13167d3825683e0542965dfcfbc3e95b2f31469fd389dbb0390d39ff4c/SQLAlchemy-1.4.54-cp38-cp38-win_amd64.whl", hash = "sha256:a01bc25eb7a5688656c8770f931d5cb4a44c7de1b3cec69b84cc9745d1e4cc10", size = 1593545 }, + { url = "https://files.pythonhosted.org/packages/c0/2c/d29f176e46fb81cdacc30e1cd60bbd2f56e97ce533a603a86fb5755a2812/SQLAlchemy-1.4.54-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:0b76bbb1cbae618d10679be8966f6d66c94f301cfc15cb49e2f2382563fb6efb", size = 1573472 }, + { url = "https://files.pythonhosted.org/packages/66/7c/6c7bae8e5a6ecd4d3cc34a2a5929c0599b954cd00877a50772fa42304d78/SQLAlchemy-1.4.54-cp39-cp39-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdb2886c0be2c6c54d0651d5a61c29ef347e8eec81fd83afebbf7b59b80b7393", size = 1638334 }, + { url = "https://files.pythonhosted.org/packages/9f/84/719fa1c53f044aede7d20c5a0859f8302eadbf1777b054ebc8c46b46bf19/SQLAlchemy-1.4.54-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:954816850777ac234a4e32b8c88ac1f7847088a6e90cfb8f0e127a1bf3feddff", size = 1626761 }, + { url = "https://files.pythonhosted.org/packages/c4/89/7d0ab875d2e6f931617d4a8fff63436b2d05205f15de06ef29f6627759a1/SQLAlchemy-1.4.54-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1d83cd1cc03c22d922ec94d0d5f7b7c96b1332f5e122e81b1a61fb22da77879a", size = 1638328 }, + { url = "https://files.pythonhosted.org/packages/4f/39/0c9186e581f07c2d58ab713490ab242920700ef162453cf6f0719c1661fe/SQLAlchemy-1.4.54-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1576fba3616f79496e2f067262200dbf4aab1bb727cd7e4e006076686413c80c", size = 1638219 }, + { url = "https://files.pythonhosted.org/packages/3a/8b/4676c988e933dccc7f26a8222ad08ccf4cf1697bd2464cdde05f6bf07eb2/SQLAlchemy-1.4.54-cp39-cp39-win32.whl", hash = "sha256:3112de9e11ff1957148c6de1df2bc5cc1440ee36783412e5eedc6f53638a577d", size = 1591716 }, + { url = "https://files.pythonhosted.org/packages/68/24/70f788b22d0799e0a8b4e952d42629e48beca0e5fb30688b9a431b2c4058/SQLAlchemy-1.4.54-cp39-cp39-win_amd64.whl", hash = "sha256:6da60fb24577f989535b8fc8b2ddc4212204aaf02e53c4c7ac94ac364150ed08", size = 1593546 }, +] + +[[package]] +name = "starlette" +version = "0.45.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", version = "4.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "typing-extensions", marker = "python_full_version == '3.9.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/4f/e1c9f4ec3dae67a94c9285ed275355d5f7cf0f3a5c34538c8ae5412af550/starlette-0.45.2.tar.gz", hash = "sha256:bba1831d15ae5212b22feab2f218bab6ed3cd0fc2dc1d4442443bb1ee52260e0", size = 2574026 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/ab/fe4f57c83620b39dfc9e7687ebad59129ff05170b99422105019d9a65eec/starlette-0.45.2-py3-none-any.whl", hash = "sha256:4daec3356fb0cb1e723a5235e5beaf375d2259af27532958e2d79df549dad9da", size = 71505 }, +] + +[[package]] +name = "terminado" +version = "0.18.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess", marker = "os_name != 'nt'" }, + { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "tornado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/11/965c6fd8e5cc254f1fe142d547387da17a8ebfd75a3455f637c663fb38a0/terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e", size = 32701 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0", size = 14154 }, +] + +[[package]] +name = "testpath" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/08/ad/a3e7d580902f57e31d2181563fc4088894692bb6ef79b816344f27719cdc/testpath-0.6.0.tar.gz", hash = "sha256:2f1b97e6442c02681ebe01bd84f531028a7caea1af3825000f52345c30285e0f", size = 93348 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/43/1ebfb29c2ca1df2bdb33dbcb2b526b77ee96873ba7b9e25650ddd4ae7156/testpath-0.6.0-py3-none-any.whl", hash = "sha256:8ada9f80a2ac6fb0391aa7cdb1a7d11cfa8429f693eda83f74dde570fe6fa639", size = 83894 }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +] + +[[package]] +name = "tornado" +version = "6.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/59/45/a0daf161f7d6f36c3ea5fc0c2de619746cc3dd4c76402e9db545bd920f63/tornado-6.4.2.tar.gz", hash = "sha256:92bad5b4746e9879fd7bf1eb21dce4e3fc5128d71601f80005afa39237ad620b", size = 501135 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/7e/71f604d8cea1b58f82ba3590290b66da1e72d840aeb37e0d5f7291bd30db/tornado-6.4.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e828cce1123e9e44ae2a50a9de3055497ab1d0aeb440c5ac23064d9e44880da1", size = 436299 }, + { url = "https://files.pythonhosted.org/packages/96/44/87543a3b99016d0bf54fdaab30d24bf0af2e848f1d13d34a3a5380aabe16/tornado-6.4.2-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:072ce12ada169c5b00b7d92a99ba089447ccc993ea2143c9ede887e0937aa803", size = 434253 }, + { url = "https://files.pythonhosted.org/packages/cb/fb/fdf679b4ce51bcb7210801ef4f11fdac96e9885daa402861751353beea6e/tornado-6.4.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a017d239bd1bb0919f72af256a970624241f070496635784d9bf0db640d3fec", size = 437602 }, + { url = "https://files.pythonhosted.org/packages/4f/3b/e31aeffffc22b475a64dbeb273026a21b5b566f74dee48742817626c47dc/tornado-6.4.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c36e62ce8f63409301537222faffcef7dfc5284f27eec227389f2ad11b09d946", size = 436972 }, + { url = "https://files.pythonhosted.org/packages/22/55/b78a464de78051a30599ceb6983b01d8f732e6f69bf37b4ed07f642ac0fc/tornado-6.4.2-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca9eb02196e789c9cb5c3c7c0f04fb447dc2adffd95265b2c7223a8a615ccbf", size = 437173 }, + { url = "https://files.pythonhosted.org/packages/79/5e/be4fb0d1684eb822c9a62fb18a3e44a06188f78aa466b2ad991d2ee31104/tornado-6.4.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:304463bd0772442ff4d0f5149c6f1c2135a1fae045adf070821c6cdc76980634", size = 437892 }, + { url = "https://files.pythonhosted.org/packages/f5/33/4f91fdd94ea36e1d796147003b490fe60a0215ac5737b6f9c65e160d4fe0/tornado-6.4.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:c82c46813ba483a385ab2a99caeaedf92585a1f90defb5693351fa7e4ea0bf73", size = 437334 }, + { url = "https://files.pythonhosted.org/packages/2b/ae/c1b22d4524b0e10da2f29a176fb2890386f7bd1f63aacf186444873a88a0/tornado-6.4.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:932d195ca9015956fa502c6b56af9eb06106140d844a335590c1ec7f5277d10c", size = 437261 }, + { url = "https://files.pythonhosted.org/packages/b5/25/36dbd49ab6d179bcfc4c6c093a51795a4f3bed380543a8242ac3517a1751/tornado-6.4.2-cp38-abi3-win32.whl", hash = "sha256:2876cef82e6c5978fde1e0d5b1f919d756968d5b4282418f3146b79b58556482", size = 438463 }, + { url = "https://files.pythonhosted.org/packages/61/cc/58b1adeb1bb46228442081e746fcdbc4540905c87e8add7c277540934edb/tornado-6.4.2-cp38-abi3-win_amd64.whl", hash = "sha256:908b71bf3ff37d81073356a5fadcc660eb10c1476ee6e2725588626ce7e5ca38", size = 438907 }, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359 }, +] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.20241230" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/f9/4d566925bcf9396136c0a2e5dc7e230ff08d86fa011a69888dd184469d80/types_pyyaml-6.0.12.20241230.tar.gz", hash = "sha256:7f07622dbd34bb9c8b264fe860a17e0efcad00d50b5f27e93984909d9363498c", size = 17078 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/c1/48474fbead512b70ccdb4f81ba5eb4a58f69d100ba19f17c92c0c4f50ae6/types_PyYAML-6.0.12.20241230-py3-none-any.whl", hash = "sha256:fa4d32565219b68e6dee5f67534c722e53c00d1cfc09c435ef04d7353e1e96e6", size = 20029 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, +] + +[[package]] +name = "urllib3" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, +] + +[[package]] +name = "uvicorn" +version = "0.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", marker = "python_full_version >= '3.9'" }, + { name = "h11", marker = "python_full_version >= '3.9'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, +] + +[[package]] +name = "virtualenv" +version = "20.29.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/ca/f23dcb02e161a9bba141b1c08aa50e8da6ea25e6d780528f1d385a3efe25/virtualenv-20.29.1.tar.gz", hash = "sha256:b8b8970138d32fb606192cb97f6cd4bb644fa486be9308fb9b63f81091b5dc35", size = 7658028 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/9b/599bcfc7064fbe5740919e78c5df18e5dceb0887e676256a1061bb5ae232/virtualenv-20.29.1-py3-none-any.whl", hash = "sha256:4e4cb403c0b0da39e13b46b1b2476e505cb0046b25f242bee80f62bf990b2779", size = 4282379 }, +] + +[[package]] +name = "watchfiles" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", version = "4.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/26/c705fc77d0a9ecdb9b66f1e2976d95b81df3cae518967431e7dbf9b5e219/watchfiles-1.0.4.tar.gz", hash = "sha256:6ba473efd11062d73e4f00c2b730255f9c1bdd73cd5f9fe5b5da8dbd4a717205", size = 94625 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/02/22fcaed0396730b0d362bc8d1ffb3be2658fd473eecbb2ba84243e157f11/watchfiles-1.0.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ba5bb3073d9db37c64520681dd2650f8bd40902d991e7b4cfaeece3e32561d08", size = 395212 }, + { url = "https://files.pythonhosted.org/packages/e9/3d/ec5a2369a46edf3ebe092c39d9ae48e8cb6dacbde51c4b4f98936c524269/watchfiles-1.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f25d0ba0fe2b6d2c921cf587b2bf4c451860086534f40c384329fb96e2044d1", size = 384815 }, + { url = "https://files.pythonhosted.org/packages/df/b4/898991cececbe171e67142c31905510203649569d9817848f47c4177ee42/watchfiles-1.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47eb32ef8c729dbc4f4273baece89398a4d4b5d21a1493efea77a17059f4df8a", size = 450680 }, + { url = "https://files.pythonhosted.org/packages/58/f7/d4aa3000e812cfb5e5c2c6c0a3ec9d0a46a42489a8727edd160631c4e210/watchfiles-1.0.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:076f293100db3b0b634514aa0d294b941daa85fc777f9c698adb1009e5aca0b1", size = 455923 }, + { url = "https://files.pythonhosted.org/packages/dd/95/7e2e4c6aba1b02fb5c76d2f6a450b85215921ec5f8f7ad5efd075369563f/watchfiles-1.0.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1eacd91daeb5158c598fe22d7ce66d60878b6294a86477a4715154990394c9b3", size = 482339 }, + { url = "https://files.pythonhosted.org/packages/bb/67/4265b0fabcc2ef2c9e3e8802ba7908cf718a357ebfb49c72e53787156a48/watchfiles-1.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13c2ce7b72026cfbca120d652f02c7750f33b4c9395d79c9790b27f014c8a5a2", size = 519908 }, + { url = "https://files.pythonhosted.org/packages/0d/96/b57802d5f8164bdf070befb4fd3dec4edba5a364ec0670965a97eb8098ce/watchfiles-1.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:90192cdc15ab7254caa7765a98132a5a41471cf739513cc9bcf7d2ffcc0ec7b2", size = 501410 }, + { url = "https://files.pythonhosted.org/packages/8b/18/6db0de4e8911ba14e31853201b40c0fa9fea5ecf3feb86b0ad58f006dfc3/watchfiles-1.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:278aaa395f405972e9f523bd786ed59dfb61e4b827856be46a42130605fd0899", size = 452876 }, + { url = "https://files.pythonhosted.org/packages/df/df/092a961815edf723a38ba2638c49491365943919c3526cc9cf82c42786a6/watchfiles-1.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a462490e75e466edbb9fc4cd679b62187153b3ba804868452ef0577ec958f5ff", size = 615353 }, + { url = "https://files.pythonhosted.org/packages/f3/cf/b85fe645de4ff82f3f436c5e9032379fce37c303f6396a18f9726cc34519/watchfiles-1.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8d0d0630930f5cd5af929040e0778cf676a46775753e442a3f60511f2409f48f", size = 613187 }, + { url = "https://files.pythonhosted.org/packages/f6/d4/a9fea27aef4dd69689bc3556718c1157a7accb72aa035ece87c1fa8483b5/watchfiles-1.0.4-cp310-cp310-win32.whl", hash = "sha256:cc27a65069bcabac4552f34fd2dce923ce3fcde0721a16e4fb1b466d63ec831f", size = 270799 }, + { url = "https://files.pythonhosted.org/packages/df/02/dbe9d4439f15dd4ad0720b6e039bde9d66d1f830331f34c18eb70fa6608e/watchfiles-1.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:8b1f135238e75d075359cf506b27bf3f4ca12029c47d3e769d8593a2024ce161", size = 284145 }, + { url = "https://files.pythonhosted.org/packages/0f/bb/8461adc4b1fed009546fb797fc0d5698dcfe5e289cb37e1b8f16a93cdc30/watchfiles-1.0.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2a9f93f8439639dc244c4d2902abe35b0279102bca7bbcf119af964f51d53c19", size = 394869 }, + { url = "https://files.pythonhosted.org/packages/55/88/9ebf36b3547176d1709c320de78c1fa3263a46be31b5b1267571d9102686/watchfiles-1.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9eea33ad8c418847dd296e61eb683cae1c63329b6d854aefcd412e12d94ee235", size = 384905 }, + { url = "https://files.pythonhosted.org/packages/03/8a/04335ce23ef78d8c69f0913e8b20cf7d9233e3986543aeef95ef2d6e43d2/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31f1a379c9dcbb3f09cf6be1b7e83b67c0e9faabed0471556d9438a4a4e14202", size = 449944 }, + { url = "https://files.pythonhosted.org/packages/17/4e/c8d5dcd14fe637f4633616dabea8a4af0a10142dccf3b43e0f081ba81ab4/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab594e75644421ae0a2484554832ca5895f8cab5ab62de30a1a57db460ce06c6", size = 456020 }, + { url = "https://files.pythonhosted.org/packages/5e/74/3e91e09e1861dd7fbb1190ce7bd786700dc0fbc2ccd33bb9fff5de039229/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc2eb5d14a8e0d5df7b36288979176fbb39672d45184fc4b1c004d7c3ce29317", size = 482983 }, + { url = "https://files.pythonhosted.org/packages/a1/3d/e64de2d1ce4eb6a574fd78ce3a28c279da263be9ef3cfcab6f708df192f2/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f68d8e9d5a321163ddacebe97091000955a1b74cd43724e346056030b0bacee", size = 520320 }, + { url = "https://files.pythonhosted.org/packages/2c/bd/52235f7063b57240c66a991696ed27e2a18bd6fcec8a1ea5a040b70d0611/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9ce064e81fe79faa925ff03b9f4c1a98b0bbb4a1b8c1b015afa93030cb21a49", size = 500988 }, + { url = "https://files.pythonhosted.org/packages/3a/b0/ff04194141a5fe650c150400dd9e42667916bc0f52426e2e174d779b8a74/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b77d5622ac5cc91d21ae9c2b284b5d5c51085a0bdb7b518dba263d0af006132c", size = 452573 }, + { url = "https://files.pythonhosted.org/packages/3d/9d/966164332c5a178444ae6d165082d4f351bd56afd9c3ec828eecbf190e6a/watchfiles-1.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1941b4e39de9b38b868a69b911df5e89dc43767feeda667b40ae032522b9b5f1", size = 615114 }, + { url = "https://files.pythonhosted.org/packages/94/df/f569ae4c1877f96ad4086c153a8eee5a19a3b519487bf5c9454a3438c341/watchfiles-1.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4f8c4998506241dedf59613082d1c18b836e26ef2a4caecad0ec41e2a15e4226", size = 613076 }, + { url = "https://files.pythonhosted.org/packages/15/ae/8ce5f29e65d5fa5790e3c80c289819c55e12be2e1b9f5b6a0e55e169b97d/watchfiles-1.0.4-cp311-cp311-win32.whl", hash = "sha256:4ebbeca9360c830766b9f0df3640b791be569d988f4be6c06d6fae41f187f105", size = 271013 }, + { url = "https://files.pythonhosted.org/packages/a4/c6/79dc4a7c598a978e5fafa135090aaf7bbb03b8dec7bada437dfbe578e7ed/watchfiles-1.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:05d341c71f3d7098920f8551d4df47f7b57ac5b8dad56558064c3431bdfc0b74", size = 284229 }, + { url = "https://files.pythonhosted.org/packages/37/3d/928633723211753f3500bfb138434f080363b87a1b08ca188b1ce54d1e05/watchfiles-1.0.4-cp311-cp311-win_arm64.whl", hash = "sha256:32b026a6ab64245b584acf4931fe21842374da82372d5c039cba6bf99ef722f3", size = 276824 }, + { url = "https://files.pythonhosted.org/packages/5b/1a/8f4d9a1461709756ace48c98f07772bc6d4519b1e48b5fa24a4061216256/watchfiles-1.0.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:229e6ec880eca20e0ba2f7e2249c85bae1999d330161f45c78d160832e026ee2", size = 391345 }, + { url = "https://files.pythonhosted.org/packages/bc/d2/6750b7b3527b1cdaa33731438432e7238a6c6c40a9924049e4cebfa40805/watchfiles-1.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5717021b199e8353782dce03bd8a8f64438832b84e2885c4a645f9723bf656d9", size = 381515 }, + { url = "https://files.pythonhosted.org/packages/4e/17/80500e42363deef1e4b4818729ed939aaddc56f82f4e72b2508729dd3c6b/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0799ae68dfa95136dde7c472525700bd48777875a4abb2ee454e3ab18e9fc712", size = 449767 }, + { url = "https://files.pythonhosted.org/packages/10/37/1427fa4cfa09adbe04b1e97bced19a29a3462cc64c78630787b613a23f18/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43b168bba889886b62edb0397cab5b6490ffb656ee2fcb22dec8bfeb371a9e12", size = 455677 }, + { url = "https://files.pythonhosted.org/packages/c5/7a/39e9397f3a19cb549a7d380412fd9e507d4854eddc0700bfad10ef6d4dba/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb2c46e275fbb9f0c92e7654b231543c7bbfa1df07cdc4b99fa73bedfde5c844", size = 482219 }, + { url = "https://files.pythonhosted.org/packages/45/2d/7113931a77e2ea4436cad0c1690c09a40a7f31d366f79c6f0a5bc7a4f6d5/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:857f5fc3aa027ff5e57047da93f96e908a35fe602d24f5e5d8ce64bf1f2fc733", size = 518830 }, + { url = "https://files.pythonhosted.org/packages/f9/1b/50733b1980fa81ef3c70388a546481ae5fa4c2080040100cd7bf3bf7b321/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55ccfd27c497b228581e2838d4386301227fc0cb47f5a12923ec2fe4f97b95af", size = 497997 }, + { url = "https://files.pythonhosted.org/packages/2b/b4/9396cc61b948ef18943e7c85ecfa64cf940c88977d882da57147f62b34b1/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c11ea22304d17d4385067588123658e9f23159225a27b983f343fcffc3e796a", size = 452249 }, + { url = "https://files.pythonhosted.org/packages/fb/69/0c65a5a29e057ad0dc691c2fa6c23b2983c7dabaa190ba553b29ac84c3cc/watchfiles-1.0.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:74cb3ca19a740be4caa18f238298b9d472c850f7b2ed89f396c00a4c97e2d9ff", size = 614412 }, + { url = "https://files.pythonhosted.org/packages/7f/b9/319fcba6eba5fad34327d7ce16a6b163b39741016b1996f4a3c96b8dd0e1/watchfiles-1.0.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c7cce76c138a91e720d1df54014a047e680b652336e1b73b8e3ff3158e05061e", size = 611982 }, + { url = "https://files.pythonhosted.org/packages/f1/47/143c92418e30cb9348a4387bfa149c8e0e404a7c5b0585d46d2f7031b4b9/watchfiles-1.0.4-cp312-cp312-win32.whl", hash = "sha256:b045c800d55bc7e2cadd47f45a97c7b29f70f08a7c2fa13241905010a5493f94", size = 271822 }, + { url = "https://files.pythonhosted.org/packages/ea/94/b0165481bff99a64b29e46e07ac2e0df9f7a957ef13bec4ceab8515f44e3/watchfiles-1.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:c2acfa49dd0ad0bf2a9c0bb9a985af02e89345a7189be1efc6baa085e0f72d7c", size = 285441 }, + { url = "https://files.pythonhosted.org/packages/11/de/09fe56317d582742d7ca8c2ca7b52a85927ebb50678d9b0fa8194658f536/watchfiles-1.0.4-cp312-cp312-win_arm64.whl", hash = "sha256:22bb55a7c9e564e763ea06c7acea24fc5d2ee5dfc5dafc5cfbedfe58505e9f90", size = 277141 }, + { url = "https://files.pythonhosted.org/packages/08/98/f03efabec64b5b1fa58c0daab25c68ef815b0f320e54adcacd0d6847c339/watchfiles-1.0.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:8012bd820c380c3d3db8435e8cf7592260257b378b649154a7948a663b5f84e9", size = 390954 }, + { url = "https://files.pythonhosted.org/packages/16/09/4dd49ba0a32a45813debe5fb3897955541351ee8142f586303b271a02b40/watchfiles-1.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa216f87594f951c17511efe5912808dfcc4befa464ab17c98d387830ce07b60", size = 381133 }, + { url = "https://files.pythonhosted.org/packages/76/59/5aa6fc93553cd8d8ee75c6247763d77c02631aed21551a97d94998bf1dae/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c9953cf85529c05b24705639ffa390f78c26449e15ec34d5339e8108c7c407", size = 449516 }, + { url = "https://files.pythonhosted.org/packages/4c/aa/df4b6fe14b6317290b91335b23c96b488d365d65549587434817e06895ea/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cf684aa9bba4cd95ecb62c822a56de54e3ae0598c1a7f2065d51e24637a3c5d", size = 454820 }, + { url = "https://files.pythonhosted.org/packages/5e/71/185f8672f1094ce48af33252c73e39b48be93b761273872d9312087245f6/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f44a39aee3cbb9b825285ff979ab887a25c5d336e5ec3574f1506a4671556a8d", size = 481550 }, + { url = "https://files.pythonhosted.org/packages/85/d7/50ebba2c426ef1a5cb17f02158222911a2e005d401caf5d911bfca58f4c4/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38320582736922be8c865d46520c043bff350956dfc9fbaee3b2df4e1740a4b", size = 518647 }, + { url = "https://files.pythonhosted.org/packages/f0/7a/4c009342e393c545d68987e8010b937f72f47937731225b2b29b7231428f/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39f4914548b818540ef21fd22447a63e7be6e24b43a70f7642d21f1e73371590", size = 497547 }, + { url = "https://files.pythonhosted.org/packages/0f/7c/1cf50b35412d5c72d63b2bf9a4fffee2e1549a245924960dd087eb6a6de4/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f12969a3765909cf5dc1e50b2436eb2c0e676a3c75773ab8cc3aa6175c16e902", size = 452179 }, + { url = "https://files.pythonhosted.org/packages/d6/a9/3db1410e1c1413735a9a472380e4f431ad9a9e81711cda2aaf02b7f62693/watchfiles-1.0.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:0986902677a1a5e6212d0c49b319aad9cc48da4bd967f86a11bde96ad9676ca1", size = 614125 }, + { url = "https://files.pythonhosted.org/packages/f2/e1/0025d365cf6248c4d1ee4c3d2e3d373bdd3f6aff78ba4298f97b4fad2740/watchfiles-1.0.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:308ac265c56f936636e3b0e3f59e059a40003c655228c131e1ad439957592303", size = 611911 }, + { url = "https://files.pythonhosted.org/packages/55/55/035838277d8c98fc8c917ac9beeb0cd6c59d675dc2421df5f9fcf44a0070/watchfiles-1.0.4-cp313-cp313-win32.whl", hash = "sha256:aee397456a29b492c20fda2d8961e1ffb266223625346ace14e4b6d861ba9c80", size = 271152 }, + { url = "https://files.pythonhosted.org/packages/f0/e5/96b8e55271685ddbadc50ce8bc53aa2dff278fb7ac4c2e473df890def2dc/watchfiles-1.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:d6097538b0ae5c1b88c3b55afa245a66793a8fec7ada6755322e465fb1a0e8cc", size = 285216 }, + { url = "https://files.pythonhosted.org/packages/15/81/54484fc2fa715abe79694b975692af963f0878fb9d72b8251aa542bf3f10/watchfiles-1.0.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:d3452c1ec703aa1c61e15dfe9d482543e4145e7c45a6b8566978fbb044265a21", size = 394967 }, + { url = "https://files.pythonhosted.org/packages/14/b3/557f0cd90add86586fe3deeebd11e8299db6bc3452b44a534f844c6ab831/watchfiles-1.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7b75fee5a16826cf5c46fe1c63116e4a156924d668c38b013e6276f2582230f0", size = 384707 }, + { url = "https://files.pythonhosted.org/packages/03/a3/34638e1bffcb85a405e7b005e30bb211fd9be2ab2cb1847f2ceb81bef27b/watchfiles-1.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e997802d78cdb02623b5941830ab06f8860038faf344f0d288d325cc9c5d2ff", size = 450442 }, + { url = "https://files.pythonhosted.org/packages/8f/9f/6a97460dd11a606003d634c7158d9fea8517e98daffc6f56d0f5fde2e86a/watchfiles-1.0.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0611d244ce94d83f5b9aff441ad196c6e21b55f77f3c47608dcf651efe54c4a", size = 455959 }, + { url = "https://files.pythonhosted.org/packages/9d/bb/e0648c6364e4d37ec692bc3f0c77507d17d8bb8f75689148819142010bbf/watchfiles-1.0.4-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9745a4210b59e218ce64c91deb599ae8775c8a9da4e95fb2ee6fe745fc87d01a", size = 483187 }, + { url = "https://files.pythonhosted.org/packages/dd/ad/d9290586a25288a81dfa8ad6329cf1de32aa1a9798ace45259eb95dcfb37/watchfiles-1.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4810ea2ae622add560f4aa50c92fef975e475f7ac4900ce5ff5547b2434642d8", size = 519733 }, + { url = "https://files.pythonhosted.org/packages/4e/a9/150c1666825cc9637093f8cae7fc6f53b3296311ab8bd65f1389acb717cb/watchfiles-1.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:740d103cd01458f22462dedeb5a3382b7f2c57d07ff033fbc9465919e5e1d0f3", size = 502275 }, + { url = "https://files.pythonhosted.org/packages/44/dc/5bfd21e20a330aca1706ac44713bc322838061938edf4b53130f97a7b211/watchfiles-1.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbd912a61543a36aef85e34f212e5d2486e7c53ebfdb70d1e0b060cc50dd0bf", size = 452907 }, + { url = "https://files.pythonhosted.org/packages/50/fe/8f4fc488f1699f564687b697456eb5c0cb8e2b0b8538150511c234c62094/watchfiles-1.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0bc80d91ddaf95f70258cf78c471246846c1986bcc5fd33ccc4a1a67fcb40f9a", size = 615927 }, + { url = "https://files.pythonhosted.org/packages/ad/19/2e45f6f6eec89dd97a4d281635e3d73c17e5f692e7432063bdfdf9562c89/watchfiles-1.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab0311bb2ffcd9f74b6c9de2dda1612c13c84b996d032cd74799adb656af4e8b", size = 613435 }, + { url = "https://files.pythonhosted.org/packages/91/17/dc5ac62ca377827c24321d68050efc2eaee2ebaf3f21d055bbce2206d309/watchfiles-1.0.4-cp39-cp39-win32.whl", hash = "sha256:02a526ee5b5a09e8168314c905fc545c9bc46509896ed282aeb5a8ba9bd6ca27", size = 270810 }, + { url = "https://files.pythonhosted.org/packages/82/2b/dad851342492d538e7ffe72a8c756f747dd147988abb039ac9d6577d2235/watchfiles-1.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:a5ae5706058b27c74bac987d615105da17724172d5aaacc6c362a40599b6de43", size = 284866 }, + { url = "https://files.pythonhosted.org/packages/6f/06/175d5ac6b838fb319008c0cd981d7bf289317c510154d411d3584ca2b67b/watchfiles-1.0.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdcc92daeae268de1acf5b7befcd6cfffd9a047098199056c72e4623f531de18", size = 396269 }, + { url = "https://files.pythonhosted.org/packages/86/ee/5db93b0b57dc0587abdbac4149296ee73275f615d790a82cb5598af0557f/watchfiles-1.0.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d8d3d9203705b5797f0af7e7e5baa17c8588030aaadb7f6a86107b7247303817", size = 386010 }, + { url = "https://files.pythonhosted.org/packages/75/61/fe0dc5fedf152bfc085a53711f740701f6bdb8ab6b5c950402b681d4858b/watchfiles-1.0.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdef5a1be32d0b07dcea3318a0be95d42c98ece24177820226b56276e06b63b0", size = 450913 }, + { url = "https://files.pythonhosted.org/packages/9f/dd/3c7731af3baf1a9957afc643d176f94480921a690ec3237c9f9d11301c08/watchfiles-1.0.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:342622287b5604ddf0ed2d085f3a589099c9ae8b7331df3ae9845571586c4f3d", size = 453474 }, + { url = "https://files.pythonhosted.org/packages/6b/b4/c3998f54c91a35cee60ee6d3a855a069c5dff2bae6865147a46e9090dccd/watchfiles-1.0.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9fe37a2de80aa785d340f2980276b17ef697ab8db6019b07ee4fd28a8359d2f3", size = 395565 }, + { url = "https://files.pythonhosted.org/packages/3f/05/ac1a4d235beb9ddfb8ac26ce93a00ba6bd1b1b43051ef12d7da957b4a9d1/watchfiles-1.0.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:9d1ef56b56ed7e8f312c934436dea93bfa3e7368adfcf3df4c0da6d4de959a1e", size = 385406 }, + { url = "https://files.pythonhosted.org/packages/4c/ea/36532e7d86525f4e52a10efed182abf33efb106a93d49f5fbc994b256bcd/watchfiles-1.0.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b42cac65beae3a362629950c444077d1b44f1790ea2772beaea95451c086bb", size = 450424 }, + { url = "https://files.pythonhosted.org/packages/7a/e9/3cbcf4d70cd0b6d3f30631deae1bf37cc0be39887ca327a44462fe546bf5/watchfiles-1.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e0227b8ed9074c6172cf55d85b5670199c99ab11fd27d2c473aa30aec67ee42", size = 452488 }, +] + +[[package]] +name = "wcwidth" +version = "0.2.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, +] + +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774 }, +] + +[[package]] +name = "websocket-client" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826 }, +] + +[[package]] +name = "websockets" +version = "14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/1b/380b883ce05bb5f45a905b61790319a28958a9ab1e4b6b95ff5464b60ca1/websockets-14.1.tar.gz", hash = "sha256:398b10c77d471c0aab20a845e7a60076b6390bfdaac7a6d2edb0d2c59d75e8d8", size = 162840 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/91/b1b375dbd856fd5fff3f117de0e520542343ecaf4e8fc60f1ac1e9f5822c/websockets-14.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a0adf84bc2e7c86e8a202537b4fd50e6f7f0e4a6b6bf64d7ccb96c4cd3330b29", size = 161950 }, + { url = "https://files.pythonhosted.org/packages/61/8f/4d52f272d3ebcd35e1325c646e98936099a348374d4a6b83b524bded8116/websockets-14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90b5d9dfbb6d07a84ed3e696012610b6da074d97453bd01e0e30744b472c8179", size = 159601 }, + { url = "https://files.pythonhosted.org/packages/c4/b1/29e87b53eb1937992cdee094a0988aadc94f25cf0b37e90c75eed7123d75/websockets-14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2177ee3901075167f01c5e335a6685e71b162a54a89a56001f1c3e9e3d2ad250", size = 159854 }, + { url = "https://files.pythonhosted.org/packages/3f/e6/752a2f5e8321ae2a613062676c08ff2fccfb37dc837a2ee919178a372e8a/websockets-14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f14a96a0034a27f9d47fd9788913924c89612225878f8078bb9d55f859272b0", size = 168835 }, + { url = "https://files.pythonhosted.org/packages/60/27/ca62de7877596926321b99071639275e94bb2401397130b7cf33dbf2106a/websockets-14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f874ba705deea77bcf64a9da42c1f5fc2466d8f14daf410bc7d4ceae0a9fcb0", size = 167844 }, + { url = "https://files.pythonhosted.org/packages/7e/db/f556a1d06635c680ef376be626c632e3f2bbdb1a0189d1d1bffb061c3b70/websockets-14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9607b9a442392e690a57909c362811184ea429585a71061cd5d3c2b98065c199", size = 168157 }, + { url = "https://files.pythonhosted.org/packages/b3/bc/99e5f511838c365ac6ecae19674eb5e94201aa4235bd1af3e6fa92c12905/websockets-14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bea45f19b7ca000380fbd4e02552be86343080120d074b87f25593ce1700ad58", size = 168561 }, + { url = "https://files.pythonhosted.org/packages/c6/e7/251491585bad61c79e525ac60927d96e4e17b18447cc9c3cfab47b2eb1b8/websockets-14.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:219c8187b3ceeadbf2afcf0f25a4918d02da7b944d703b97d12fb01510869078", size = 167979 }, + { url = "https://files.pythonhosted.org/packages/ac/98/7ac2e4eeada19bdbc7a3a66a58e3ebdf33648b9e1c5b3f08c3224df168cf/websockets-14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ad2ab2547761d79926effe63de21479dfaf29834c50f98c4bf5b5480b5838434", size = 167925 }, + { url = "https://files.pythonhosted.org/packages/ab/3d/09e65c47ee2396b7482968068f6e9b516221e1032b12dcf843b9412a5dfb/websockets-14.1-cp310-cp310-win32.whl", hash = "sha256:1288369a6a84e81b90da5dbed48610cd7e5d60af62df9851ed1d1d23a9069f10", size = 162831 }, + { url = "https://files.pythonhosted.org/packages/8a/67/59828a3d09740e6a485acccfbb66600632f2178b6ed1b61388ee96f17d5a/websockets-14.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0744623852f1497d825a49a99bfbec9bea4f3f946df6eb9d8a2f0c37a2fec2e", size = 163266 }, + { url = "https://files.pythonhosted.org/packages/97/ed/c0d03cb607b7fe1f7ff45e2cd4bb5cd0f9e3299ced79c2c303a6fff44524/websockets-14.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:449d77d636f8d9c17952628cc7e3b8faf6e92a17ec581ec0c0256300717e1512", size = 161949 }, + { url = "https://files.pythonhosted.org/packages/06/91/bf0a44e238660d37a2dda1b4896235d20c29a2d0450f3a46cd688f43b239/websockets-14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a35f704be14768cea9790d921c2c1cc4fc52700410b1c10948511039be824aac", size = 159606 }, + { url = "https://files.pythonhosted.org/packages/ff/b8/7185212adad274c2b42b6a24e1ee6b916b7809ed611cbebc33b227e5c215/websockets-14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b1f3628a0510bd58968c0f60447e7a692933589b791a6b572fcef374053ca280", size = 159854 }, + { url = "https://files.pythonhosted.org/packages/5a/8a/0849968d83474be89c183d8ae8dcb7f7ada1a3c24f4d2a0d7333c231a2c3/websockets-14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c3deac3748ec73ef24fc7be0b68220d14d47d6647d2f85b2771cb35ea847aa1", size = 169402 }, + { url = "https://files.pythonhosted.org/packages/bd/4f/ef886e37245ff6b4a736a09b8468dae05d5d5c99de1357f840d54c6f297d/websockets-14.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7048eb4415d46368ef29d32133134c513f507fff7d953c18c91104738a68c3b3", size = 168406 }, + { url = "https://files.pythonhosted.org/packages/11/43/e2dbd4401a63e409cebddedc1b63b9834de42f51b3c84db885469e9bdcef/websockets-14.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6cf0ad281c979306a6a34242b371e90e891bce504509fb6bb5246bbbf31e7b6", size = 168776 }, + { url = "https://files.pythonhosted.org/packages/6d/d6/7063e3f5c1b612e9f70faae20ebaeb2e684ffa36cb959eb0862ee2809b32/websockets-14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cc1fc87428c1d18b643479caa7b15db7d544652e5bf610513d4a3478dbe823d0", size = 169083 }, + { url = "https://files.pythonhosted.org/packages/49/69/e6f3d953f2fa0f8a723cf18cd011d52733bd7f6e045122b24e0e7f49f9b0/websockets-14.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f95ba34d71e2fa0c5d225bde3b3bdb152e957150100e75c86bc7f3964c450d89", size = 168529 }, + { url = "https://files.pythonhosted.org/packages/70/ff/f31fa14561fc1d7b8663b0ed719996cf1f581abee32c8fb2f295a472f268/websockets-14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9481a6de29105d73cf4515f2bef8eb71e17ac184c19d0b9918a3701c6c9c4f23", size = 168475 }, + { url = "https://files.pythonhosted.org/packages/f1/15/b72be0e4bf32ff373aa5baef46a4c7521b8ea93ad8b49ca8c6e8e764c083/websockets-14.1-cp311-cp311-win32.whl", hash = "sha256:368a05465f49c5949e27afd6fbe0a77ce53082185bbb2ac096a3a8afaf4de52e", size = 162833 }, + { url = "https://files.pythonhosted.org/packages/bc/ef/2d81679acbe7057ffe2308d422f744497b52009ea8bab34b6d74a2657d1d/websockets-14.1-cp311-cp311-win_amd64.whl", hash = "sha256:6d24fc337fc055c9e83414c94e1ee0dee902a486d19d2a7f0929e49d7d604b09", size = 163263 }, + { url = "https://files.pythonhosted.org/packages/55/64/55698544ce29e877c9188f1aee9093712411a8fc9732cca14985e49a8e9c/websockets-14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed907449fe5e021933e46a3e65d651f641975a768d0649fee59f10c2985529ed", size = 161957 }, + { url = "https://files.pythonhosted.org/packages/a2/b1/b088f67c2b365f2c86c7b48edb8848ac27e508caf910a9d9d831b2f343cb/websockets-14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:87e31011b5c14a33b29f17eb48932e63e1dcd3fa31d72209848652310d3d1f0d", size = 159620 }, + { url = "https://files.pythonhosted.org/packages/c1/89/2a09db1bbb40ba967a1b8225b07b7df89fea44f06de9365f17f684d0f7e6/websockets-14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bc6ccf7d54c02ae47a48ddf9414c54d48af9c01076a2e1023e3b486b6e72c707", size = 159852 }, + { url = "https://files.pythonhosted.org/packages/ca/c1/f983138cd56e7d3079f1966e81f77ce6643f230cd309f73aa156bb181749/websockets-14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9777564c0a72a1d457f0848977a1cbe15cfa75fa2f67ce267441e465717dcf1a", size = 169675 }, + { url = "https://files.pythonhosted.org/packages/c1/c8/84191455d8660e2a0bdb33878d4ee5dfa4a2cedbcdc88bbd097303b65bfa/websockets-14.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a655bde548ca98f55b43711b0ceefd2a88a71af6350b0c168aa77562104f3f45", size = 168619 }, + { url = "https://files.pythonhosted.org/packages/8d/a7/62e551fdcd7d44ea74a006dc193aba370505278ad76efd938664531ce9d6/websockets-14.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3dfff83ca578cada2d19e665e9c8368e1598d4e787422a460ec70e531dbdd58", size = 169042 }, + { url = "https://files.pythonhosted.org/packages/ad/ed/1532786f55922c1e9c4d329608e36a15fdab186def3ca9eb10d7465bc1cc/websockets-14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6a6c9bcf7cdc0fd41cc7b7944447982e8acfd9f0d560ea6d6845428ed0562058", size = 169345 }, + { url = "https://files.pythonhosted.org/packages/ea/fb/160f66960d495df3de63d9bcff78e1b42545b2a123cc611950ffe6468016/websockets-14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4b6caec8576e760f2c7dd878ba817653144d5f369200b6ddf9771d64385b84d4", size = 168725 }, + { url = "https://files.pythonhosted.org/packages/cf/53/1bf0c06618b5ac35f1d7906444b9958f8485682ab0ea40dee7b17a32da1e/websockets-14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb6d38971c800ff02e4a6afd791bbe3b923a9a57ca9aeab7314c21c84bf9ff05", size = 168712 }, + { url = "https://files.pythonhosted.org/packages/e5/22/5ec2f39fff75f44aa626f86fa7f20594524a447d9c3be94d8482cd5572ef/websockets-14.1-cp312-cp312-win32.whl", hash = "sha256:1d045cbe1358d76b24d5e20e7b1878efe578d9897a25c24e6006eef788c0fdf0", size = 162838 }, + { url = "https://files.pythonhosted.org/packages/74/27/28f07df09f2983178db7bf6c9cccc847205d2b92ced986cd79565d68af4f/websockets-14.1-cp312-cp312-win_amd64.whl", hash = "sha256:90f4c7a069c733d95c308380aae314f2cb45bd8a904fb03eb36d1a4983a4993f", size = 163277 }, + { url = "https://files.pythonhosted.org/packages/34/77/812b3ba5110ed8726eddf9257ab55ce9e85d97d4aa016805fdbecc5e5d48/websockets-14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3630b670d5057cd9e08b9c4dab6493670e8e762a24c2c94ef312783870736ab9", size = 161966 }, + { url = "https://files.pythonhosted.org/packages/8d/24/4fcb7aa6986ae7d9f6d083d9d53d580af1483c5ec24bdec0978307a0f6ac/websockets-14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36ebd71db3b89e1f7b1a5deaa341a654852c3518ea7a8ddfdf69cc66acc2db1b", size = 159625 }, + { url = "https://files.pythonhosted.org/packages/f8/47/2a0a3a2fc4965ff5b9ce9324d63220156bd8bedf7f90824ab92a822e65fd/websockets-14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5b918d288958dc3fa1c5a0b9aa3256cb2b2b84c54407f4813c45d52267600cd3", size = 159857 }, + { url = "https://files.pythonhosted.org/packages/dd/c8/d7b425011a15e35e17757e4df75b25e1d0df64c0c315a44550454eaf88fc/websockets-14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00fe5da3f037041da1ee0cf8e308374e236883f9842c7c465aa65098b1c9af59", size = 169635 }, + { url = "https://files.pythonhosted.org/packages/93/39/6e3b5cffa11036c40bd2f13aba2e8e691ab2e01595532c46437b56575678/websockets-14.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8149a0f5a72ca36720981418eeffeb5c2729ea55fa179091c81a0910a114a5d2", size = 168578 }, + { url = "https://files.pythonhosted.org/packages/cf/03/8faa5c9576299b2adf34dcccf278fc6bbbcda8a3efcc4d817369026be421/websockets-14.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77569d19a13015e840b81550922056acabc25e3f52782625bc6843cfa034e1da", size = 169018 }, + { url = "https://files.pythonhosted.org/packages/8c/05/ea1fec05cc3a60defcdf0bb9f760c3c6bd2dd2710eff7ac7f891864a22ba/websockets-14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf5201a04550136ef870aa60ad3d29d2a59e452a7f96b94193bee6d73b8ad9a9", size = 169383 }, + { url = "https://files.pythonhosted.org/packages/21/1d/eac1d9ed787f80754e51228e78855f879ede1172c8b6185aca8cef494911/websockets-14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:88cf9163ef674b5be5736a584c999e98daf3aabac6e536e43286eb74c126b9c7", size = 168773 }, + { url = "https://files.pythonhosted.org/packages/0e/1b/e808685530185915299740d82b3a4af3f2b44e56ccf4389397c7a5d95d39/websockets-14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:836bef7ae338a072e9d1863502026f01b14027250a4545672673057997d5c05a", size = 168757 }, + { url = "https://files.pythonhosted.org/packages/b6/19/6ab716d02a3b068fbbeb6face8a7423156e12c446975312f1c7c0f4badab/websockets-14.1-cp313-cp313-win32.whl", hash = "sha256:0d4290d559d68288da9f444089fd82490c8d2744309113fc26e2da6e48b65da6", size = 162834 }, + { url = "https://files.pythonhosted.org/packages/6c/fd/ab6b7676ba712f2fc89d1347a4b5bdc6aa130de10404071f2b2606450209/websockets-14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8621a07991add373c3c5c2cf89e1d277e49dc82ed72c75e3afc74bd0acc446f0", size = 163277 }, + { url = "https://files.pythonhosted.org/packages/4d/23/ac9d8c5ec7b90efc3687d60474ef7e698f8b75cb7c9dfedad72701e797c9/websockets-14.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:01bb2d4f0a6d04538d3c5dfd27c0643269656c28045a53439cbf1c004f90897a", size = 161945 }, + { url = "https://files.pythonhosted.org/packages/c5/6b/ffa450e3b736a86ae6b40ce20a758ac9af80c96a18548f6c323ed60329c5/websockets-14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:414ffe86f4d6f434a8c3b7913655a1a5383b617f9bf38720e7c0799fac3ab1c6", size = 159600 }, + { url = "https://files.pythonhosted.org/packages/74/62/f90d1fd57ea7337ecaa99f17c31a544b9dcdb7c7c32a3d3997ccc42d57d3/websockets-14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8fda642151d5affdee8a430bd85496f2e2517be3a2b9d2484d633d5712b15c56", size = 159850 }, + { url = "https://files.pythonhosted.org/packages/35/dd/1e71865de1f3c265e11d02b0b4c76178f84351c6611e515fbe3d2bd1b98c/websockets-14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd7c11968bc3860d5c78577f0dbc535257ccec41750675d58d8dc66aa47fe52c", size = 168616 }, + { url = "https://files.pythonhosted.org/packages/ba/ae/0d069b52e26d48402dbe90c7581eb6a5bed5d7dbe3d9ca3cf1033859d58e/websockets-14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a032855dc7db987dff813583d04f4950d14326665d7e714d584560b140ae6b8b", size = 167619 }, + { url = "https://files.pythonhosted.org/packages/1c/3f/d3f2df62704c53e0296f0ce714921b6a15df10e2e463734c737b1d9e2522/websockets-14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7e7ea2f782408c32d86b87a0d2c1fd8871b0399dd762364c731d86c86069a78", size = 167921 }, + { url = "https://files.pythonhosted.org/packages/e0/e2/2dcb295bdae9393070cea58c790d87d1d36149bb4319b1da6014c8a36d42/websockets-14.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:39450e6215f7d9f6f7bc2a6da21d79374729f5d052333da4d5825af8a97e6735", size = 168343 }, + { url = "https://files.pythonhosted.org/packages/6b/fd/fa48e8b4e10e2c165cbfc16dada7405b4008818be490fc6b99a4928e232a/websockets-14.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ceada5be22fa5a5a4cdeec74e761c2ee7db287208f54c718f2df4b7e200b8d4a", size = 167745 }, + { url = "https://files.pythonhosted.org/packages/42/45/79db33f2b744d2014b40946428e6c37ce944fde8791d82e1c2f4d4a67d96/websockets-14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3fc753451d471cff90b8f467a1fc0ae64031cf2d81b7b34e1811b7e2691bc4bc", size = 167705 }, + { url = "https://files.pythonhosted.org/packages/da/27/f66507db34ca9c79562f28fa5983433f7b9080fd471cc188906006d36ba4/websockets-14.1-cp39-cp39-win32.whl", hash = "sha256:14839f54786987ccd9d03ed7f334baec0f02272e7ec4f6e9d427ff584aeea8b4", size = 162828 }, + { url = "https://files.pythonhosted.org/packages/11/25/bb8f81a4ec94f595adb845608c5ec9549cb6b446945b292fe61807c7c95b/websockets-14.1-cp39-cp39-win_amd64.whl", hash = "sha256:d9fd19ecc3a4d5ae82ddbfb30962cf6d874ff943e56e0c81f5169be2fda62979", size = 163271 }, + { url = "https://files.pythonhosted.org/packages/fb/cd/382a05a1ba2a93bd9fb807716a660751295df72e77204fb130a102fcdd36/websockets-14.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e5dc25a9dbd1a7f61eca4b7cb04e74ae4b963d658f9e4f9aad9cd00b688692c8", size = 159633 }, + { url = "https://files.pythonhosted.org/packages/b7/a0/fa7c62e2952ef028b422fbf420f9353d9dd4dfaa425de3deae36e98c0784/websockets-14.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:04a97aca96ca2acedf0d1f332c861c5a4486fdcba7bcef35873820f940c4231e", size = 159867 }, + { url = "https://files.pythonhosted.org/packages/c1/94/954b4924f868db31d5f0935893c7a8446515ee4b36bb8ad75a929469e453/websockets-14.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df174ece723b228d3e8734a6f2a6febbd413ddec39b3dc592f5a4aa0aff28098", size = 161121 }, + { url = "https://files.pythonhosted.org/packages/7a/2e/f12bbb41a8f2abb76428ba4fdcd9e67b5b364a3e7fa97c88f4d6950aa2d4/websockets-14.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:034feb9f4286476f273b9a245fb15f02c34d9586a5bc936aff108c3ba1b21beb", size = 160731 }, + { url = "https://files.pythonhosted.org/packages/13/97/b76979401f2373af1fe3e08f960b265cecab112e7dac803446fb98351a52/websockets-14.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:660c308dabd2b380807ab64b62985eaccf923a78ebc572bd485375b9ca2b7dc7", size = 160681 }, + { url = "https://files.pythonhosted.org/packages/39/9c/16916d9a436c109a1d7ba78817e8fee357b78968be3f6e6f517f43afa43d/websockets-14.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5a42d3ecbb2db5080fc578314439b1d79eef71d323dc661aa616fb492436af5d", size = 163316 }, + { url = "https://files.pythonhosted.org/packages/0f/57/50fd09848a80a1b63a572c610f230f8a17590ca47daf256eb28a0851df73/websockets-14.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddaa4a390af911da6f680be8be4ff5aaf31c4c834c1a9147bc21cbcbca2d4370", size = 159633 }, + { url = "https://files.pythonhosted.org/packages/d7/2f/db728b0c7962ad6a13ced8286325bf430b59722d943e7f6bdbd8a78e2bfe/websockets-14.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a4c805c6034206143fbabd2d259ec5e757f8b29d0a2f0bf3d2fe5d1f60147a4a", size = 159863 }, + { url = "https://files.pythonhosted.org/packages/fa/e4/21e7481936fbfffee138edb488a6184eb3468b402a8181b95b9e44f6a676/websockets-14.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:205f672a6c2c671a86d33f6d47c9b35781a998728d2c7c2a3e1cf3333fcb62b7", size = 161119 }, + { url = "https://files.pythonhosted.org/packages/64/2d/efb6cf716d4f9da60190756e06f8db2066faf1ae4a4a8657ab136dfcc7a8/websockets-14.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef440054124728cc49b01c33469de06755e5a7a4e83ef61934ad95fc327fbb0", size = 160724 }, + { url = "https://files.pythonhosted.org/packages/40/b0/a70b972d853c3f26040834fcff3dd45c8a0292af9f5f0b36f9fbb82d5d44/websockets-14.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7591d6f440af7f73c4bd9404f3772bfee064e639d2b6cc8c94076e71b2471c1", size = 160676 }, + { url = "https://files.pythonhosted.org/packages/4a/76/f9da7f97476cc7b8c74829bb4851f1faf660455839689ffcc354b52860a7/websockets-14.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:25225cc79cfebc95ba1d24cd3ab86aaa35bcd315d12fa4358939bd55e9bd74a5", size = 163311 }, + { url = "https://files.pythonhosted.org/packages/b0/0b/c7e5d11020242984d9d37990310520ed663b942333b83a033c2f20191113/websockets-14.1-py3-none-any.whl", hash = "sha256:4d4fc827a20abe6d544a119896f6b78ee13fe81cbfef416f3f2ddf09a03f0e2e", size = 156277 }, +] + +[[package]] +name = "wheel" +version = "0.45.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/98/2d9906746cdc6a6ef809ae6338005b3f21bb568bea3165cfc6a243fdc25c/wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729", size = 107545 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248", size = 72494 }, +] + +[[package]] +name = "widgetsnbextension" +version = "3.6.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "notebook" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/8c/2692555c838db37fb56cc0cfd51c27367af7bc13f3489bec427b7475d4e0/widgetsnbextension-3.6.10.tar.gz", hash = "sha256:cc370876baee1d23d4c506c798ab7d08c355133c9a5e81474159ff75877593df", size = 783481 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/1b/25d570ee8dce0f2ddadb967d6242cf6e10516db7897c7d9a6e3853b56bfc/widgetsnbextension-3.6.10-py2.py3-none-any.whl", hash = "sha256:91a283c2bb50b43ae415dfe69fb026ece0c14e0102987fb53127c7a71e82417d", size = 1556860 }, +] + +[[package]] +name = "yarl" +version = "1.15.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "idna", marker = "python_full_version < '3.9'" }, + { name = "multidict", marker = "python_full_version < '3.9'" }, + { name = "propcache", version = "0.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/e1/d5427a061819c9f885f58bb0467d02a523f1aec19f9e5f9c82ce950d90d3/yarl-1.15.2.tar.gz", hash = "sha256:a39c36f4218a5bb668b4f06874d676d35a035ee668e6e7e3538835c703634b84", size = 169318 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/f8/6b1bbc6f597d8937ad8661c042aa6bdbbe46a3a6e38e2c04214b9c82e804/yarl-1.15.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e4ee8b8639070ff246ad3649294336b06db37a94bdea0d09ea491603e0be73b8", size = 136479 }, + { url = "https://files.pythonhosted.org/packages/61/e0/973c0d16b1cb710d318b55bd5d019a1ecd161d28670b07d8d9df9a83f51f/yarl-1.15.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a7cf963a357c5f00cb55b1955df8bbe68d2f2f65de065160a1c26b85a1e44172", size = 88671 }, + { url = "https://files.pythonhosted.org/packages/16/df/241cfa1cf33b96da2c8773b76fe3ee58e04cb09ecfe794986ec436ae97dc/yarl-1.15.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:43ebdcc120e2ca679dba01a779333a8ea76b50547b55e812b8b92818d604662c", size = 86578 }, + { url = "https://files.pythonhosted.org/packages/02/a4/ee2941d1f93600d921954a0850e20581159772304e7de49f60588e9128a2/yarl-1.15.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3433da95b51a75692dcf6cc8117a31410447c75a9a8187888f02ad45c0a86c50", size = 307212 }, + { url = "https://files.pythonhosted.org/packages/08/64/2e6561af430b092b21c7a867ae3079f62e1532d3e51fee765fd7a74cef6c/yarl-1.15.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38d0124fa992dbacd0c48b1b755d3ee0a9f924f427f95b0ef376556a24debf01", size = 321589 }, + { url = "https://files.pythonhosted.org/packages/f8/af/056ab318a7117fa70f6ab502ff880e47af973948d1d123aff397cd68499c/yarl-1.15.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ded1b1803151dd0f20a8945508786d57c2f97a50289b16f2629f85433e546d47", size = 319443 }, + { url = "https://files.pythonhosted.org/packages/99/d1/051b0bc2c90c9a2618bab10a9a9a61a96ddb28c7c54161a5c97f9e625205/yarl-1.15.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ace4cad790f3bf872c082366c9edd7f8f8f77afe3992b134cfc810332206884f", size = 310324 }, + { url = "https://files.pythonhosted.org/packages/23/1b/16df55016f9ac18457afda165031086bce240d8bcf494501fb1164368617/yarl-1.15.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c77494a2f2282d9bbbbcab7c227a4d1b4bb829875c96251f66fb5f3bae4fb053", size = 300428 }, + { url = "https://files.pythonhosted.org/packages/83/a5/5188d1c575139a8dfd90d463d56f831a018f41f833cdf39da6bd8a72ee08/yarl-1.15.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b7f227ca6db5a9fda0a2b935a2ea34a7267589ffc63c8045f0e4edb8d8dcf956", size = 307079 }, + { url = "https://files.pythonhosted.org/packages/ba/4e/2497f8f2b34d1a261bebdbe00066242eacc9a7dccd4f02ddf0995014290a/yarl-1.15.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:31561a5b4d8dbef1559b3600b045607cf804bae040f64b5f5bca77da38084a8a", size = 305835 }, + { url = "https://files.pythonhosted.org/packages/91/db/40a347e1f8086e287a53c72dc333198816885bc770e3ecafcf5eaeb59311/yarl-1.15.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3e52474256a7db9dcf3c5f4ca0b300fdea6c21cca0148c8891d03a025649d935", size = 311033 }, + { url = "https://files.pythonhosted.org/packages/2f/a6/1500e1e694616c25eed6bf8c1aacc0943f124696d2421a07ae5e9ee101a5/yarl-1.15.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0e1af74a9529a1137c67c887ed9cde62cff53aa4d84a3adbec329f9ec47a3936", size = 326317 }, + { url = "https://files.pythonhosted.org/packages/37/db/868d4b59cc76932ce880cc9946cd0ae4ab111a718494a94cb50dd5b67d82/yarl-1.15.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:15c87339490100c63472a76d87fe7097a0835c705eb5ae79fd96e343473629ed", size = 324196 }, + { url = "https://files.pythonhosted.org/packages/bd/41/b6c917c2fde2601ee0b45c82a0c502dc93e746dea469d3a6d1d0a24749e8/yarl-1.15.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:74abb8709ea54cc483c4fb57fb17bb66f8e0f04438cff6ded322074dbd17c7ec", size = 317023 }, + { url = "https://files.pythonhosted.org/packages/b0/85/2cde6b656fd83c474f19606af3f7a3e94add8988760c87a101ee603e7b8f/yarl-1.15.2-cp310-cp310-win32.whl", hash = "sha256:ffd591e22b22f9cb48e472529db6a47203c41c2c5911ff0a52e85723196c0d75", size = 78136 }, + { url = "https://files.pythonhosted.org/packages/ef/3c/4414901b0588427870002b21d790bd1fad142a9a992a22e5037506d0ed9d/yarl-1.15.2-cp310-cp310-win_amd64.whl", hash = "sha256:1695497bb2a02a6de60064c9f077a4ae9c25c73624e0d43e3aa9d16d983073c2", size = 84231 }, + { url = "https://files.pythonhosted.org/packages/4a/59/3ae125c97a2a8571ea16fdf59fcbd288bc169e0005d1af9946a90ea831d9/yarl-1.15.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9fcda20b2de7042cc35cf911702fa3d8311bd40055a14446c1e62403684afdc5", size = 136492 }, + { url = "https://files.pythonhosted.org/packages/f9/2b/efa58f36b582db45b94c15e87803b775eb8a4ca0db558121a272e67f3564/yarl-1.15.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0545de8c688fbbf3088f9e8b801157923be4bf8e7b03e97c2ecd4dfa39e48e0e", size = 88614 }, + { url = "https://files.pythonhosted.org/packages/82/69/eb73c0453a2ff53194df485dc7427d54e6cb8d1180fcef53251a8e24d069/yarl-1.15.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fbda058a9a68bec347962595f50546a8a4a34fd7b0654a7b9697917dc2bf810d", size = 86607 }, + { url = "https://files.pythonhosted.org/packages/48/4e/89beaee3a4da0d1c6af1176d738cff415ff2ad3737785ee25382409fe3e3/yarl-1.15.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1ac2bc069f4a458634c26b101c2341b18da85cb96afe0015990507efec2e417", size = 334077 }, + { url = "https://files.pythonhosted.org/packages/da/e8/8fcaa7552093f94c3f327783e2171da0eaa71db0c267510898a575066b0f/yarl-1.15.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd126498171f752dd85737ab1544329a4520c53eed3997f9b08aefbafb1cc53b", size = 347365 }, + { url = "https://files.pythonhosted.org/packages/be/fa/dc2002f82a89feab13a783d3e6b915a3a2e0e83314d9e3f6d845ee31bfcc/yarl-1.15.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3db817b4e95eb05c362e3b45dafe7144b18603e1211f4a5b36eb9522ecc62bcf", size = 344823 }, + { url = "https://files.pythonhosted.org/packages/ae/c8/c4a00fe7f2aa6970c2651df332a14c88f8baaedb2e32d6c3b8c8a003ea74/yarl-1.15.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:076b1ed2ac819933895b1a000904f62d615fe4533a5cf3e052ff9a1da560575c", size = 337132 }, + { url = "https://files.pythonhosted.org/packages/07/bf/84125f85f44bf2af03f3cf64e87214b42cd59dcc8a04960d610a9825f4d4/yarl-1.15.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f8cfd847e6b9ecf9f2f2531c8427035f291ec286c0a4944b0a9fce58c6446046", size = 326258 }, + { url = "https://files.pythonhosted.org/packages/00/19/73ad8122b2fa73fe22e32c24b82a6c053cf6c73e2f649b73f7ef97bee8d0/yarl-1.15.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:32b66be100ac5739065496c74c4b7f3015cef792c3174982809274d7e51b3e04", size = 336212 }, + { url = "https://files.pythonhosted.org/packages/39/1d/2fa4337d11f6587e9b7565f84eba549f2921494bc8b10bfe811079acaa70/yarl-1.15.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:34a2d76a1984cac04ff8b1bfc939ec9dc0914821264d4a9c8fd0ed6aa8d4cfd2", size = 330397 }, + { url = "https://files.pythonhosted.org/packages/39/ab/dce75e06806bcb4305966471ead03ce639d8230f4f52c32bd614d820c044/yarl-1.15.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0afad2cd484908f472c8fe2e8ef499facee54a0a6978be0e0cff67b1254fd747", size = 334985 }, + { url = "https://files.pythonhosted.org/packages/c1/98/3f679149347a5e34c952bf8f71a387bc96b3488fae81399a49f8b1a01134/yarl-1.15.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c68e820879ff39992c7f148113b46efcd6ec765a4865581f2902b3c43a5f4bbb", size = 356033 }, + { url = "https://files.pythonhosted.org/packages/f7/8c/96546061c19852d0a4b1b07084a58c2e8911db6bcf7838972cff542e09fb/yarl-1.15.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:98f68df80ec6ca3015186b2677c208c096d646ef37bbf8b49764ab4a38183931", size = 357710 }, + { url = "https://files.pythonhosted.org/packages/01/45/ade6fb3daf689816ebaddb3175c962731edf300425c3254c559b6d0dcc27/yarl-1.15.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c56ec1eacd0a5d35b8a29f468659c47f4fe61b2cab948ca756c39b7617f0aa5", size = 345532 }, + { url = "https://files.pythonhosted.org/packages/e7/d7/8de800d3aecda0e64c43e8fc844f7effc8731a6099fa0c055738a2247504/yarl-1.15.2-cp311-cp311-win32.whl", hash = "sha256:eedc3f247ee7b3808ea07205f3e7d7879bc19ad3e6222195cd5fbf9988853e4d", size = 78250 }, + { url = "https://files.pythonhosted.org/packages/3a/6c/69058bbcfb0164f221aa30e0cd1a250f6babb01221e27c95058c51c498ca/yarl-1.15.2-cp311-cp311-win_amd64.whl", hash = "sha256:0ccaa1bc98751fbfcf53dc8dfdb90d96e98838010fc254180dd6707a6e8bb179", size = 84492 }, + { url = "https://files.pythonhosted.org/packages/e0/d1/17ff90e7e5b1a0b4ddad847f9ec6a214b87905e3a59d01bff9207ce2253b/yarl-1.15.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:82d5161e8cb8f36ec778fd7ac4d740415d84030f5b9ef8fe4da54784a1f46c94", size = 136721 }, + { url = "https://files.pythonhosted.org/packages/44/50/a64ca0577aeb9507f4b672f9c833d46cf8f1e042ce2e80c11753b936457d/yarl-1.15.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fa2bea05ff0a8fb4d8124498e00e02398f06d23cdadd0fe027d84a3f7afde31e", size = 88954 }, + { url = "https://files.pythonhosted.org/packages/c9/0a/a30d0b02046d4088c1fd32d85d025bd70ceb55f441213dee14d503694f41/yarl-1.15.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:99e12d2bf587b44deb74e0d6170fec37adb489964dbca656ec41a7cd8f2ff178", size = 86692 }, + { url = "https://files.pythonhosted.org/packages/06/0b/7613decb8baa26cba840d7ea2074bd3c5e27684cbcb6d06e7840d6c5226c/yarl-1.15.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:243fbbbf003754fe41b5bdf10ce1e7f80bcc70732b5b54222c124d6b4c2ab31c", size = 325762 }, + { url = "https://files.pythonhosted.org/packages/97/f5/b8c389a58d1eb08f89341fc1bbcc23a0341f7372185a0a0704dbdadba53a/yarl-1.15.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:856b7f1a7b98a8c31823285786bd566cf06226ac4f38b3ef462f593c608a9bd6", size = 335037 }, + { url = "https://files.pythonhosted.org/packages/cb/f9/d89b93a7bb8b66e01bf722dcc6fec15e11946e649e71414fd532b05c4d5d/yarl-1.15.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:553dad9af802a9ad1a6525e7528152a015b85fb8dbf764ebfc755c695f488367", size = 334221 }, + { url = "https://files.pythonhosted.org/packages/10/77/1db077601998e0831a540a690dcb0f450c31f64c492e993e2eaadfbc7d31/yarl-1.15.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30c3ff305f6e06650a761c4393666f77384f1cc6c5c0251965d6bfa5fbc88f7f", size = 330167 }, + { url = "https://files.pythonhosted.org/packages/3b/c2/e5b7121662fd758656784fffcff2e411c593ec46dc9ec68e0859a2ffaee3/yarl-1.15.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:353665775be69bbfc6d54c8d134bfc533e332149faeddd631b0bc79df0897f46", size = 317472 }, + { url = "https://files.pythonhosted.org/packages/c6/f3/41e366c17e50782651b192ba06a71d53500cc351547816bf1928fb043c4f/yarl-1.15.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f4fe99ce44128c71233d0d72152db31ca119711dfc5f2c82385ad611d8d7f897", size = 330896 }, + { url = "https://files.pythonhosted.org/packages/79/a2/d72e501bc1e33e68a5a31f584fe4556ab71a50a27bfd607d023f097cc9bb/yarl-1.15.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:9c1e3ff4b89cdd2e1a24c214f141e848b9e0451f08d7d4963cb4108d4d798f1f", size = 328787 }, + { url = "https://files.pythonhosted.org/packages/9d/ba/890f7e1ea17f3c247748548eee876528ceb939e44566fa7d53baee57e5aa/yarl-1.15.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:711bdfae4e699a6d4f371137cbe9e740dc958530cb920eb6f43ff9551e17cfbc", size = 332631 }, + { url = "https://files.pythonhosted.org/packages/48/c7/27b34206fd5dfe76b2caa08bf22f9212b2d665d5bb2df8a6dd3af498dcf4/yarl-1.15.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4388c72174868884f76affcdd3656544c426407e0043c89b684d22fb265e04a5", size = 344023 }, + { url = "https://files.pythonhosted.org/packages/88/e7/730b130f4f02bd8b00479baf9a57fdea1dc927436ed1d6ba08fa5c36c68e/yarl-1.15.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f0e1844ad47c7bd5d6fa784f1d4accc5f4168b48999303a868fe0f8597bde715", size = 352290 }, + { url = "https://files.pythonhosted.org/packages/84/9b/e8dda28f91a0af67098cddd455e6b540d3f682dda4c0de224215a57dee4a/yarl-1.15.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a5cafb02cf097a82d74403f7e0b6b9df3ffbfe8edf9415ea816314711764a27b", size = 343742 }, + { url = "https://files.pythonhosted.org/packages/66/47/b1c6bb85f2b66decbe189e27fcc956ab74670a068655df30ef9a2e15c379/yarl-1.15.2-cp312-cp312-win32.whl", hash = "sha256:156ececdf636143f508770bf8a3a0498de64da5abd890c7dbb42ca9e3b6c05b8", size = 78051 }, + { url = "https://files.pythonhosted.org/packages/7d/9e/1a897e5248ec53e96e9f15b3e6928efd5e75d322c6cf666f55c1c063e5c9/yarl-1.15.2-cp312-cp312-win_amd64.whl", hash = "sha256:435aca062444a7f0c884861d2e3ea79883bd1cd19d0a381928b69ae1b85bc51d", size = 84313 }, + { url = "https://files.pythonhosted.org/packages/46/ab/be3229898d7eb1149e6ba7fe44f873cf054d275a00b326f2a858c9ff7175/yarl-1.15.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:416f2e3beaeae81e2f7a45dc711258be5bdc79c940a9a270b266c0bec038fb84", size = 135006 }, + { url = "https://files.pythonhosted.org/packages/10/10/b91c186b1b0e63951f80481b3e6879bb9f7179d471fe7c4440c9e900e2a3/yarl-1.15.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:173563f3696124372831007e3d4b9821746964a95968628f7075d9231ac6bb33", size = 88121 }, + { url = "https://files.pythonhosted.org/packages/bf/1d/4ceaccf836b9591abfde775e84249b847ac4c6c14ee2dd8d15b5b3cede44/yarl-1.15.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9ce2e0f6123a60bd1a7f5ae3b2c49b240c12c132847f17aa990b841a417598a2", size = 85967 }, + { url = "https://files.pythonhosted.org/packages/93/bd/c924f22bdb2c5d0ca03a9e64ecc5e041aace138c2a91afff7e2f01edc3a1/yarl-1.15.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eaea112aed589131f73d50d570a6864728bd7c0c66ef6c9154ed7b59f24da611", size = 325615 }, + { url = "https://files.pythonhosted.org/packages/59/a5/6226accd5c01cafd57af0d249c7cf9dd12569cd9c78fbd93e8198e7a9d84/yarl-1.15.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4ca3b9f370f218cc2a0309542cab8d0acdfd66667e7c37d04d617012485f904", size = 334945 }, + { url = "https://files.pythonhosted.org/packages/4c/c1/cc6ccdd2bcd0ff7291602d5831754595260f8d2754642dfd34fef1791059/yarl-1.15.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23ec1d3c31882b2a8a69c801ef58ebf7bae2553211ebbddf04235be275a38548", size = 336701 }, + { url = "https://files.pythonhosted.org/packages/ef/ff/39a767ee249444e4b26ea998a526838238f8994c8f274befc1f94dacfb43/yarl-1.15.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75119badf45f7183e10e348edff5a76a94dc19ba9287d94001ff05e81475967b", size = 330977 }, + { url = "https://files.pythonhosted.org/packages/dd/ba/b1fed73f9d39e3e7be8f6786be5a2ab4399c21504c9168c3cadf6e441c2e/yarl-1.15.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78e6fdc976ec966b99e4daa3812fac0274cc28cd2b24b0d92462e2e5ef90d368", size = 317402 }, + { url = "https://files.pythonhosted.org/packages/82/e8/03e3ebb7f558374f29c04868b20ca484d7997f80a0a191490790a8c28058/yarl-1.15.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8657d3f37f781d987037f9cc20bbc8b40425fa14380c87da0cb8dfce7c92d0fb", size = 331776 }, + { url = "https://files.pythonhosted.org/packages/1f/83/90b0f4fd1ecf2602ba4ac50ad0bbc463122208f52dd13f152bbc0d8417dd/yarl-1.15.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:93bed8a8084544c6efe8856c362af08a23e959340c87a95687fdbe9c9f280c8b", size = 331585 }, + { url = "https://files.pythonhosted.org/packages/c7/f6/1ed7e7f270ae5f9f1174c1f8597b29658f552fee101c26de8b2eb4ca147a/yarl-1.15.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:69d5856d526802cbda768d3e6246cd0d77450fa2a4bc2ea0ea14f0d972c2894b", size = 336395 }, + { url = "https://files.pythonhosted.org/packages/e0/3a/4354ed8812909d9ec54a92716a53259b09e6b664209231f2ec5e75f4820d/yarl-1.15.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ccad2800dfdff34392448c4bf834be124f10a5bc102f254521d931c1c53c455a", size = 342810 }, + { url = "https://files.pythonhosted.org/packages/de/cc/39e55e16b1415a87f6d300064965d6cfb2ac8571e11339ccb7dada2444d9/yarl-1.15.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a880372e2e5dbb9258a4e8ff43f13888039abb9dd6d515f28611c54361bc5644", size = 351441 }, + { url = "https://files.pythonhosted.org/packages/fb/19/5cd4757079dc9d9f3de3e3831719b695f709a8ce029e70b33350c9d082a7/yarl-1.15.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c998d0558805860503bc3a595994895ca0f7835e00668dadc673bbf7f5fbfcbe", size = 345875 }, + { url = "https://files.pythonhosted.org/packages/83/a0/ef09b54634f73417f1ea4a746456a4372c1b044f07b26e16fa241bd2d94e/yarl-1.15.2-cp313-cp313-win32.whl", hash = "sha256:533a28754e7f7439f217550a497bb026c54072dbe16402b183fdbca2431935a9", size = 302609 }, + { url = "https://files.pythonhosted.org/packages/20/9f/f39c37c17929d3975da84c737b96b606b68c495cc4ee86408f10523a1635/yarl-1.15.2-cp313-cp313-win_amd64.whl", hash = "sha256:5838f2b79dc8f96fdc44077c9e4e2e33d7089b10788464609df788eb97d03aad", size = 308252 }, + { url = "https://files.pythonhosted.org/packages/7b/1f/544439ce6b7a498327d57ff40f0cd4f24bf4b1c1daf76c8c962dca022e71/yarl-1.15.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fbbb63bed5fcd70cd3dd23a087cd78e4675fb5a2963b8af53f945cbbca79ae16", size = 138555 }, + { url = "https://files.pythonhosted.org/packages/e8/b7/d6f33e7a42832f1e8476d0aabe089be0586a9110b5dfc2cef93444dc7c21/yarl-1.15.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e2e93b88ecc8f74074012e18d679fb2e9c746f2a56f79cd5e2b1afcf2a8a786b", size = 89844 }, + { url = "https://files.pythonhosted.org/packages/93/34/ede8d8ed7350b4b21e33fc4eff71e08de31da697034969b41190132d421f/yarl-1.15.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:af8ff8d7dc07ce873f643de6dfbcd45dc3db2c87462e5c387267197f59e6d776", size = 87671 }, + { url = "https://files.pythonhosted.org/packages/fa/51/6d71e92bc54b5788b18f3dc29806f9ce37e12b7c610e8073357717f34b78/yarl-1.15.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66f629632220a4e7858b58e4857927dd01a850a4cef2fb4044c8662787165cf7", size = 314558 }, + { url = "https://files.pythonhosted.org/packages/76/0a/f9ffe503b4ef77cd77c9eefd37717c092e26f2c2dbbdd45700f864831292/yarl-1.15.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:833547179c31f9bec39b49601d282d6f0ea1633620701288934c5f66d88c3e50", size = 327622 }, + { url = "https://files.pythonhosted.org/packages/8b/38/8eb602eeb153de0189d572dce4ed81b9b14f71de7c027d330b601b4fdcdc/yarl-1.15.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2aa738e0282be54eede1e3f36b81f1e46aee7ec7602aa563e81e0e8d7b67963f", size = 324447 }, + { url = "https://files.pythonhosted.org/packages/c2/1e/1c78c695a4c7b957b5665e46a89ea35df48511dbed301a05c0a8beed0cc3/yarl-1.15.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a13a07532e8e1c4a5a3afff0ca4553da23409fad65def1b71186fb867eeae8d", size = 319009 }, + { url = "https://files.pythonhosted.org/packages/06/a0/7ea93de4ca1991e7f92a8901dcd1585165f547d342f7c6f36f1ea58b75de/yarl-1.15.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c45817e3e6972109d1a2c65091504a537e257bc3c885b4e78a95baa96df6a3f8", size = 307760 }, + { url = "https://files.pythonhosted.org/packages/f4/b4/ceaa1f35cfb37fe06af3f7404438abf9a1262dc5df74dba37c90b0615e06/yarl-1.15.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:670eb11325ed3a6209339974b276811867defe52f4188fe18dc49855774fa9cf", size = 315038 }, + { url = "https://files.pythonhosted.org/packages/da/45/a2ca2b547c56550eefc39e45d61e4b42ae6dbb3e913810b5a0eb53e86412/yarl-1.15.2-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:d417a4f6943112fae3924bae2af7112562285848d9bcee737fc4ff7cbd450e6c", size = 312898 }, + { url = "https://files.pythonhosted.org/packages/ea/e0/f692ba36dedc5b0b22084bba558a7ede053841e247b7dd2adbb9d40450be/yarl-1.15.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:bc8936d06cd53fddd4892677d65e98af514c8d78c79864f418bbf78a4a2edde4", size = 319370 }, + { url = "https://files.pythonhosted.org/packages/b1/3f/0e382caf39958be6ae61d4bb0c82a68a3c45a494fc8cdc6f55c29757970e/yarl-1.15.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:954dde77c404084c2544e572f342aef384240b3e434e06cecc71597e95fd1ce7", size = 332429 }, + { url = "https://files.pythonhosted.org/packages/21/6b/c824a4a1c45d67b15b431d4ab83b63462bfcbc710065902e10fa5c2ffd9e/yarl-1.15.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:5bc0df728e4def5e15a754521e8882ba5a5121bd6b5a3a0ff7efda5d6558ab3d", size = 333143 }, + { url = "https://files.pythonhosted.org/packages/20/76/8af2a1d93fe95b04e284b5d55daaad33aae6e2f6254a1bcdb40e2752af6c/yarl-1.15.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b71862a652f50babab4a43a487f157d26b464b1dedbcc0afda02fd64f3809d04", size = 326687 }, + { url = "https://files.pythonhosted.org/packages/1c/53/490830773f907ef8a311cc5d82e5830f75f7692c1adacbdb731d3f1246fd/yarl-1.15.2-cp38-cp38-win32.whl", hash = "sha256:63eab904f8630aed5a68f2d0aeab565dcfc595dc1bf0b91b71d9ddd43dea3aea", size = 78705 }, + { url = "https://files.pythonhosted.org/packages/9c/9d/d944e897abf37f50f4fa2d8d6f5fd0ed9413bc8327d3b4cc25ba9694e1ba/yarl-1.15.2-cp38-cp38-win_amd64.whl", hash = "sha256:2cf441c4b6e538ba0d2591574f95d3fdd33f1efafa864faa077d9636ecc0c4e9", size = 84998 }, + { url = "https://files.pythonhosted.org/packages/91/1c/1c9d08c29b10499348eedc038cf61b6d96d5ba0e0d69438975845939ed3c/yarl-1.15.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a32d58f4b521bb98b2c0aa9da407f8bd57ca81f34362bcb090e4a79e9924fefc", size = 138011 }, + { url = "https://files.pythonhosted.org/packages/d4/33/2d4a1418bae6d7883c1fcc493be7b6d6fe015919835adc9e8eeba472e9f7/yarl-1.15.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:766dcc00b943c089349d4060b935c76281f6be225e39994c2ccec3a2a36ad627", size = 89618 }, + { url = "https://files.pythonhosted.org/packages/78/2e/0024c674a376cfdc722a167a8f308f5779aca615cb7a28d67fbeabf3f697/yarl-1.15.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bed1b5dbf90bad3bfc19439258c97873eab453c71d8b6869c136346acfe497e7", size = 87347 }, + { url = "https://files.pythonhosted.org/packages/c5/08/a01874dabd4ddf475c5c2adc86f7ac329f83a361ee513a97841720ab7b24/yarl-1.15.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed20a4bdc635f36cb19e630bfc644181dd075839b6fc84cac51c0f381ac472e2", size = 310438 }, + { url = "https://files.pythonhosted.org/packages/09/95/691bc6de2c1b0e9c8bbaa5f8f38118d16896ba1a069a09d1fb073d41a093/yarl-1.15.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d538df442c0d9665664ab6dd5fccd0110fa3b364914f9c85b3ef9b7b2e157980", size = 325384 }, + { url = "https://files.pythonhosted.org/packages/95/fd/fee11eb3337f48c62d39c5676e6a0e4e318e318900a901b609a3c45394df/yarl-1.15.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c6cf1d92edf936ceedc7afa61b07e9d78a27b15244aa46bbcd534c7458ee1b", size = 321820 }, + { url = "https://files.pythonhosted.org/packages/7a/ad/4a2c9bbebaefdce4a69899132f4bf086abbddb738dc6e794a31193bc0854/yarl-1.15.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce44217ad99ffad8027d2fde0269ae368c86db66ea0571c62a000798d69401fb", size = 314150 }, + { url = "https://files.pythonhosted.org/packages/38/7d/552c37bc6c4ae8ea900e44b6c05cb16d50dca72d3782ccd66f53e27e353f/yarl-1.15.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47a6000a7e833ebfe5886b56a31cb2ff12120b1efd4578a6fcc38df16cc77bd", size = 304202 }, + { url = "https://files.pythonhosted.org/packages/2e/f8/c22a158f3337f49775775ecef43fc097a98b20cdce37425b68b9c45a6f94/yarl-1.15.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e52f77a0cd246086afde8815039f3e16f8d2be51786c0a39b57104c563c5cbb0", size = 310311 }, + { url = "https://files.pythonhosted.org/packages/ce/e4/ebce06afa25c2a6c8e6c9a5915cbbc7940a37f3ec38e950e8f346ca908da/yarl-1.15.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:f9ca0e6ce7774dc7830dc0cc4bb6b3eec769db667f230e7c770a628c1aa5681b", size = 310645 }, + { url = "https://files.pythonhosted.org/packages/0a/34/5504cc8fbd1be959ec0a1e9e9f471fd438c37cb877b0178ce09085b36b51/yarl-1.15.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:136f9db0f53c0206db38b8cd0c985c78ded5fd596c9a86ce5c0b92afb91c3a19", size = 313328 }, + { url = "https://files.pythonhosted.org/packages/cf/e4/fb3f91a539c6505e347d7d75bc675d291228960ffd6481ced76a15412924/yarl-1.15.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:173866d9f7409c0fb514cf6e78952e65816600cb888c68b37b41147349fe0057", size = 330135 }, + { url = "https://files.pythonhosted.org/packages/e1/08/a0b27db813f0159e1c8a45f48852afded501de2f527e7613c4dcf436ecf7/yarl-1.15.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:6e840553c9c494a35e449a987ca2c4f8372668ee954a03a9a9685075228e5036", size = 327155 }, + { url = "https://files.pythonhosted.org/packages/97/4e/b3414dded12d0e2b52eb1964c21a8d8b68495b320004807de770f7b6b53a/yarl-1.15.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:458c0c65802d816a6b955cf3603186de79e8fdb46d4f19abaec4ef0a906f50a7", size = 320810 }, + { url = "https://files.pythonhosted.org/packages/bb/ca/e5149c55d1c9dcf3d5b48acd7c71ca8622fd2f61322d0386fe63ba106774/yarl-1.15.2-cp39-cp39-win32.whl", hash = "sha256:5b48388ded01f6f2429a8c55012bdbd1c2a0c3735b3e73e221649e524c34a58d", size = 78686 }, + { url = "https://files.pythonhosted.org/packages/b1/87/f56a80a1abaf65dbf138b821357b51b6cc061756bb7d93f08797950b3881/yarl-1.15.2-cp39-cp39-win_amd64.whl", hash = "sha256:81dadafb3aa124f86dc267a2168f71bbd2bfb163663661ab0038f6e4b8edb810", size = 84818 }, + { url = "https://files.pythonhosted.org/packages/46/cf/a28c494decc9c8776b0d7b729c68d26fdafefcedd8d2eab5d9cd767376b2/yarl-1.15.2-py3-none-any.whl", hash = "sha256:0d3105efab7c5c091609abacad33afff33bdff0035bece164c98bcf5a85ef90a", size = 38891 }, +] + +[[package]] +name = "yarl" +version = "1.18.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "idna", marker = "python_full_version >= '3.9'" }, + { name = "multidict", marker = "python_full_version >= '3.9'" }, + { name = "propcache", version = "0.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/9d/4b94a8e6d2b51b599516a5cb88e5bc99b4d8d4583e468057eaa29d5f0918/yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1", size = 181062 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/98/e005bc608765a8a5569f58e650961314873c8469c333616eb40bff19ae97/yarl-1.18.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34", size = 141458 }, + { url = "https://files.pythonhosted.org/packages/df/5d/f8106b263b8ae8a866b46d9be869ac01f9b3fb7f2325f3ecb3df8003f796/yarl-1.18.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7", size = 94365 }, + { url = "https://files.pythonhosted.org/packages/56/3e/d8637ddb9ba69bf851f765a3ee288676f7cf64fb3be13760c18cbc9d10bd/yarl-1.18.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:602d98f2c2d929f8e697ed274fbadc09902c4025c5a9963bf4e9edfc3ab6f7ed", size = 92181 }, + { url = "https://files.pythonhosted.org/packages/76/f9/d616a5c2daae281171de10fba41e1c0e2d8207166fc3547252f7d469b4e1/yarl-1.18.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c654d5207c78e0bd6d749f6dae1dcbbfde3403ad3a4b11f3c5544d9906969dde", size = 315349 }, + { url = "https://files.pythonhosted.org/packages/bb/b4/3ea5e7b6f08f698b3769a06054783e434f6d59857181b5c4e145de83f59b/yarl-1.18.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5094d9206c64181d0f6e76ebd8fb2f8fe274950a63890ee9e0ebfd58bf9d787b", size = 330494 }, + { url = "https://files.pythonhosted.org/packages/55/f1/e0fc810554877b1b67420568afff51b967baed5b53bcc983ab164eebf9c9/yarl-1.18.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35098b24e0327fc4ebdc8ffe336cee0a87a700c24ffed13161af80124b7dc8e5", size = 326927 }, + { url = "https://files.pythonhosted.org/packages/a9/42/b1753949b327b36f210899f2dd0a0947c0c74e42a32de3f8eb5c7d93edca/yarl-1.18.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3236da9272872443f81fedc389bace88408f64f89f75d1bdb2256069a8730ccc", size = 319703 }, + { url = "https://files.pythonhosted.org/packages/f0/6d/e87c62dc9635daefb064b56f5c97df55a2e9cc947a2b3afd4fd2f3b841c7/yarl-1.18.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2c08cc9b16f4f4bc522771d96734c7901e7ebef70c6c5c35dd0f10845270bcd", size = 310246 }, + { url = "https://files.pythonhosted.org/packages/e3/ef/e2e8d1785cdcbd986f7622d7f0098205f3644546da7919c24b95790ec65a/yarl-1.18.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80316a8bd5109320d38eef8833ccf5f89608c9107d02d2a7f985f98ed6876990", size = 319730 }, + { url = "https://files.pythonhosted.org/packages/fc/15/8723e22345bc160dfde68c4b3ae8b236e868f9963c74015f1bc8a614101c/yarl-1.18.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1e1cc06da1491e6734f0ea1e6294ce00792193c463350626571c287c9a704db", size = 321681 }, + { url = "https://files.pythonhosted.org/packages/86/09/bf764e974f1516efa0ae2801494a5951e959f1610dd41edbfc07e5e0f978/yarl-1.18.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fea09ca13323376a2fdfb353a5fa2e59f90cd18d7ca4eaa1fd31f0a8b4f91e62", size = 324812 }, + { url = "https://files.pythonhosted.org/packages/f6/4c/20a0187e3b903c97d857cf0272d687c1b08b03438968ae8ffc50fe78b0d6/yarl-1.18.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e3b9fd71836999aad54084906f8663dffcd2a7fb5cdafd6c37713b2e72be1760", size = 337011 }, + { url = "https://files.pythonhosted.org/packages/c9/71/6244599a6e1cc4c9f73254a627234e0dad3883ece40cc33dce6265977461/yarl-1.18.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:757e81cae69244257d125ff31663249b3013b5dc0a8520d73694aed497fb195b", size = 338132 }, + { url = "https://files.pythonhosted.org/packages/af/f5/e0c3efaf74566c4b4a41cb76d27097df424052a064216beccae8d303c90f/yarl-1.18.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b1771de9944d875f1b98a745bc547e684b863abf8f8287da8466cf470ef52690", size = 331849 }, + { url = "https://files.pythonhosted.org/packages/8a/b8/3d16209c2014c2f98a8f658850a57b716efb97930aebf1ca0d9325933731/yarl-1.18.3-cp310-cp310-win32.whl", hash = "sha256:8874027a53e3aea659a6d62751800cf6e63314c160fd607489ba5c2edd753cf6", size = 84309 }, + { url = "https://files.pythonhosted.org/packages/fd/b7/2e9a5b18eb0fe24c3a0e8bae994e812ed9852ab4fd067c0107fadde0d5f0/yarl-1.18.3-cp310-cp310-win_amd64.whl", hash = "sha256:93b2e109287f93db79210f86deb6b9bbb81ac32fc97236b16f7433db7fc437d8", size = 90484 }, + { url = "https://files.pythonhosted.org/packages/40/93/282b5f4898d8e8efaf0790ba6d10e2245d2c9f30e199d1a85cae9356098c/yarl-1.18.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8503ad47387b8ebd39cbbbdf0bf113e17330ffd339ba1144074da24c545f0069", size = 141555 }, + { url = "https://files.pythonhosted.org/packages/6d/9c/0a49af78df099c283ca3444560f10718fadb8a18dc8b3edf8c7bd9fd7d89/yarl-1.18.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02ddb6756f8f4517a2d5e99d8b2f272488e18dd0bfbc802f31c16c6c20f22193", size = 94351 }, + { url = "https://files.pythonhosted.org/packages/5a/a1/205ab51e148fdcedad189ca8dd587794c6f119882437d04c33c01a75dece/yarl-1.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:67a283dd2882ac98cc6318384f565bffc751ab564605959df4752d42483ad889", size = 92286 }, + { url = "https://files.pythonhosted.org/packages/ed/fe/88b690b30f3f59275fb674f5f93ddd4a3ae796c2b62e5bb9ece8a4914b83/yarl-1.18.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d980e0325b6eddc81331d3f4551e2a333999fb176fd153e075c6d1c2530aa8a8", size = 340649 }, + { url = "https://files.pythonhosted.org/packages/07/eb/3b65499b568e01f36e847cebdc8d7ccb51fff716dbda1ae83c3cbb8ca1c9/yarl-1.18.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b643562c12680b01e17239be267bc306bbc6aac1f34f6444d1bded0c5ce438ca", size = 356623 }, + { url = "https://files.pythonhosted.org/packages/33/46/f559dc184280b745fc76ec6b1954de2c55595f0ec0a7614238b9ebf69618/yarl-1.18.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c017a3b6df3a1bd45b9fa49a0f54005e53fbcad16633870104b66fa1a30a29d8", size = 354007 }, + { url = "https://files.pythonhosted.org/packages/af/ba/1865d85212351ad160f19fb99808acf23aab9a0f8ff31c8c9f1b4d671fc9/yarl-1.18.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae", size = 344145 }, + { url = "https://files.pythonhosted.org/packages/94/cb/5c3e975d77755d7b3d5193e92056b19d83752ea2da7ab394e22260a7b824/yarl-1.18.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccaa3a4b521b780a7e771cc336a2dba389a0861592bbce09a476190bb0c8b4b3", size = 336133 }, + { url = "https://files.pythonhosted.org/packages/19/89/b77d3fd249ab52a5c40859815765d35c91425b6bb82e7427ab2f78f5ff55/yarl-1.18.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d06d3005e668744e11ed80812e61efd77d70bb7f03e33c1598c301eea20efbb", size = 347967 }, + { url = "https://files.pythonhosted.org/packages/35/bd/f6b7630ba2cc06c319c3235634c582a6ab014d52311e7d7c22f9518189b5/yarl-1.18.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9d41beda9dc97ca9ab0b9888cb71f7539124bc05df02c0cff6e5acc5a19dcc6e", size = 346397 }, + { url = "https://files.pythonhosted.org/packages/18/1a/0b4e367d5a72d1f095318344848e93ea70da728118221f84f1bf6c1e39e7/yarl-1.18.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ba23302c0c61a9999784e73809427c9dbedd79f66a13d84ad1b1943802eaaf59", size = 350206 }, + { url = "https://files.pythonhosted.org/packages/b5/cf/320fff4367341fb77809a2d8d7fe75b5d323a8e1b35710aafe41fdbf327b/yarl-1.18.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6748dbf9bfa5ba1afcc7556b71cda0d7ce5f24768043a02a58846e4a443d808d", size = 362089 }, + { url = "https://files.pythonhosted.org/packages/57/cf/aadba261d8b920253204085268bad5e8cdd86b50162fcb1b10c10834885a/yarl-1.18.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0b0cad37311123211dc91eadcb322ef4d4a66008d3e1bdc404808992260e1a0e", size = 366267 }, + { url = "https://files.pythonhosted.org/packages/54/58/fb4cadd81acdee6dafe14abeb258f876e4dd410518099ae9a35c88d8097c/yarl-1.18.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fb2171a4486bb075316ee754c6d8382ea6eb8b399d4ec62fde2b591f879778a", size = 359141 }, + { url = "https://files.pythonhosted.org/packages/9a/7a/4c571597589da4cd5c14ed2a0b17ac56ec9ee7ee615013f74653169e702d/yarl-1.18.3-cp311-cp311-win32.whl", hash = "sha256:61b1a825a13bef4a5f10b1885245377d3cd0bf87cba068e1d9a88c2ae36880e1", size = 84402 }, + { url = "https://files.pythonhosted.org/packages/ae/7b/8600250b3d89b625f1121d897062f629883c2f45339623b69b1747ec65fa/yarl-1.18.3-cp311-cp311-win_amd64.whl", hash = "sha256:b9d60031cf568c627d028239693fd718025719c02c9f55df0a53e587aab951b5", size = 91030 }, + { url = "https://files.pythonhosted.org/packages/33/85/bd2e2729752ff4c77338e0102914897512e92496375e079ce0150a6dc306/yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50", size = 142644 }, + { url = "https://files.pythonhosted.org/packages/ff/74/1178322cc0f10288d7eefa6e4a85d8d2e28187ccab13d5b844e8b5d7c88d/yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576", size = 94962 }, + { url = "https://files.pythonhosted.org/packages/be/75/79c6acc0261e2c2ae8a1c41cf12265e91628c8c58ae91f5ff59e29c0787f/yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640", size = 92795 }, + { url = "https://files.pythonhosted.org/packages/6b/32/927b2d67a412c31199e83fefdce6e645247b4fb164aa1ecb35a0f9eb2058/yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2", size = 332368 }, + { url = "https://files.pythonhosted.org/packages/19/e5/859fca07169d6eceeaa4fde1997c91d8abde4e9a7c018e371640c2da2b71/yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75", size = 342314 }, + { url = "https://files.pythonhosted.org/packages/08/75/76b63ccd91c9e03ab213ef27ae6add2e3400e77e5cdddf8ed2dbc36e3f21/yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512", size = 341987 }, + { url = "https://files.pythonhosted.org/packages/1a/e1/a097d5755d3ea8479a42856f51d97eeff7a3a7160593332d98f2709b3580/yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba", size = 336914 }, + { url = "https://files.pythonhosted.org/packages/0b/42/e1b4d0e396b7987feceebe565286c27bc085bf07d61a59508cdaf2d45e63/yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb", size = 325765 }, + { url = "https://files.pythonhosted.org/packages/7e/18/03a5834ccc9177f97ca1bbb245b93c13e58e8225276f01eedc4cc98ab820/yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272", size = 344444 }, + { url = "https://files.pythonhosted.org/packages/c8/03/a713633bdde0640b0472aa197b5b86e90fbc4c5bc05b727b714cd8a40e6d/yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6", size = 340760 }, + { url = "https://files.pythonhosted.org/packages/eb/99/f6567e3f3bbad8fd101886ea0276c68ecb86a2b58be0f64077396cd4b95e/yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e", size = 346484 }, + { url = "https://files.pythonhosted.org/packages/8e/a9/84717c896b2fc6cb15bd4eecd64e34a2f0a9fd6669e69170c73a8b46795a/yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb", size = 359864 }, + { url = "https://files.pythonhosted.org/packages/1e/2e/d0f5f1bef7ee93ed17e739ec8dbcb47794af891f7d165fa6014517b48169/yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393", size = 364537 }, + { url = "https://files.pythonhosted.org/packages/97/8a/568d07c5d4964da5b02621a517532adb8ec5ba181ad1687191fffeda0ab6/yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285", size = 357861 }, + { url = "https://files.pythonhosted.org/packages/7d/e3/924c3f64b6b3077889df9a1ece1ed8947e7b61b0a933f2ec93041990a677/yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2", size = 84097 }, + { url = "https://files.pythonhosted.org/packages/34/45/0e055320daaabfc169b21ff6174567b2c910c45617b0d79c68d7ab349b02/yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477", size = 90399 }, + { url = "https://files.pythonhosted.org/packages/30/c7/c790513d5328a8390be8f47be5d52e141f78b66c6c48f48d241ca6bd5265/yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb", size = 140789 }, + { url = "https://files.pythonhosted.org/packages/30/aa/a2f84e93554a578463e2edaaf2300faa61c8701f0898725842c704ba5444/yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa", size = 94144 }, + { url = "https://files.pythonhosted.org/packages/c6/fc/d68d8f83714b221a85ce7866832cba36d7c04a68fa6a960b908c2c84f325/yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782", size = 91974 }, + { url = "https://files.pythonhosted.org/packages/56/4e/d2563d8323a7e9a414b5b25341b3942af5902a2263d36d20fb17c40411e2/yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0", size = 333587 }, + { url = "https://files.pythonhosted.org/packages/25/c9/cfec0bc0cac8d054be223e9f2c7909d3e8442a856af9dbce7e3442a8ec8d/yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482", size = 344386 }, + { url = "https://files.pythonhosted.org/packages/ab/5d/4c532190113b25f1364d25f4c319322e86232d69175b91f27e3ebc2caf9a/yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186", size = 345421 }, + { url = "https://files.pythonhosted.org/packages/23/d1/6cdd1632da013aa6ba18cee4d750d953104a5e7aac44e249d9410a972bf5/yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58", size = 339384 }, + { url = "https://files.pythonhosted.org/packages/9a/c4/6b3c39bec352e441bd30f432cda6ba51681ab19bb8abe023f0d19777aad1/yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53", size = 326689 }, + { url = "https://files.pythonhosted.org/packages/23/30/07fb088f2eefdc0aa4fc1af4e3ca4eb1a3aadd1ce7d866d74c0f124e6a85/yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2", size = 345453 }, + { url = "https://files.pythonhosted.org/packages/63/09/d54befb48f9cd8eec43797f624ec37783a0266855f4930a91e3d5c7717f8/yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8", size = 341872 }, + { url = "https://files.pythonhosted.org/packages/91/26/fd0ef9bf29dd906a84b59f0cd1281e65b0c3e08c6aa94b57f7d11f593518/yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1", size = 347497 }, + { url = "https://files.pythonhosted.org/packages/d9/b5/14ac7a256d0511b2ac168d50d4b7d744aea1c1aa20c79f620d1059aab8b2/yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a", size = 359981 }, + { url = "https://files.pythonhosted.org/packages/ca/b3/d493221ad5cbd18bc07e642894030437e405e1413c4236dd5db6e46bcec9/yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10", size = 366229 }, + { url = "https://files.pythonhosted.org/packages/04/56/6a3e2a5d9152c56c346df9b8fb8edd2c8888b1e03f96324d457e5cf06d34/yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8", size = 360383 }, + { url = "https://files.pythonhosted.org/packages/fd/b7/4b3c7c7913a278d445cc6284e59b2e62fa25e72758f888b7a7a39eb8423f/yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d", size = 310152 }, + { url = "https://files.pythonhosted.org/packages/f5/d5/688db678e987c3e0fb17867970700b92603cadf36c56e5fb08f23e822a0c/yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c", size = 315723 }, + { url = "https://files.pythonhosted.org/packages/6a/3b/fec4b08f5e88f68e56ee698a59284a73704df2e0e0b5bdf6536c86e76c76/yarl-1.18.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:61e5e68cb65ac8f547f6b5ef933f510134a6bf31bb178be428994b0cb46c2a04", size = 142780 }, + { url = "https://files.pythonhosted.org/packages/ed/85/796b0d6a22d536ec8e14bdbb86519250bad980cec450b6e299b1c2a9079e/yarl-1.18.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe57328fbc1bfd0bd0514470ac692630f3901c0ee39052ae47acd1d90a436719", size = 94981 }, + { url = "https://files.pythonhosted.org/packages/ee/0e/a830fd2238f7a29050f6dd0de748b3d6f33a7dbb67dbbc081a970b2bbbeb/yarl-1.18.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a440a2a624683108a1b454705ecd7afc1c3438a08e890a1513d468671d90a04e", size = 92789 }, + { url = "https://files.pythonhosted.org/packages/0f/4f/438c9fd668954779e48f08c0688ee25e0673380a21bb1e8ccc56de5b55d7/yarl-1.18.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09c7907c8548bcd6ab860e5f513e727c53b4a714f459b084f6580b49fa1b9cee", size = 317327 }, + { url = "https://files.pythonhosted.org/packages/bd/79/a78066f06179b4ed4581186c136c12fcfb928c475cbeb23743e71a991935/yarl-1.18.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4f6450109834af88cb4cc5ecddfc5380ebb9c228695afc11915a0bf82116789", size = 336999 }, + { url = "https://files.pythonhosted.org/packages/55/02/527963cf65f34a06aed1e766ff9a3b3e7d0eaa1c90736b2948a62e528e1d/yarl-1.18.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9ca04806f3be0ac6d558fffc2fdf8fcef767e0489d2684a21912cc4ed0cd1b8", size = 331693 }, + { url = "https://files.pythonhosted.org/packages/a2/2a/167447ae39252ba624b98b8c13c0ba35994d40d9110e8a724c83dbbb5822/yarl-1.18.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77a6e85b90a7641d2e07184df5557132a337f136250caafc9ccaa4a2a998ca2c", size = 321473 }, + { url = "https://files.pythonhosted.org/packages/55/03/07955fabb20082373be311c91fd78abe458bc7ff9069d34385e8bddad20e/yarl-1.18.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6333c5a377c8e2f5fae35e7b8f145c617b02c939d04110c76f29ee3676b5f9a5", size = 313571 }, + { url = "https://files.pythonhosted.org/packages/95/e2/67c8d3ec58a8cd8ddb1d63bd06eb7e7b91c9f148707a3eeb5a7ed87df0ef/yarl-1.18.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0b3c92fa08759dbf12b3a59579a4096ba9af8dd344d9a813fc7f5070d86bbab1", size = 325004 }, + { url = "https://files.pythonhosted.org/packages/06/43/51ceb3e427368fe6ccd9eccd162be227fd082523e02bad1fd3063daf68da/yarl-1.18.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:4ac515b860c36becb81bb84b667466885096b5fc85596948548b667da3bf9f24", size = 322677 }, + { url = "https://files.pythonhosted.org/packages/e4/0e/7ef286bfb23267739a703f7b967a858e2128c10bea898de8fa027e962521/yarl-1.18.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:045b8482ce9483ada4f3f23b3774f4e1bf4f23a2d5c912ed5170f68efb053318", size = 332806 }, + { url = "https://files.pythonhosted.org/packages/c8/94/2d1f060f4bfa47c8bd0bcb652bfe71fba881564bcac06ebb6d8ced9ac3bc/yarl-1.18.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:a4bb030cf46a434ec0225bddbebd4b89e6471814ca851abb8696170adb163985", size = 339919 }, + { url = "https://files.pythonhosted.org/packages/8e/8d/73b5f9a6ab69acddf1ca1d5e7bc92f50b69124512e6c26b36844531d7f23/yarl-1.18.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:54d6921f07555713b9300bee9c50fb46e57e2e639027089b1d795ecd9f7fa910", size = 340960 }, + { url = "https://files.pythonhosted.org/packages/41/13/ce6bc32be4476b60f4f8694831f49590884b2c975afcffc8d533bf2be7ec/yarl-1.18.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1d407181cfa6e70077df3377938c08012d18893f9f20e92f7d2f314a437c30b1", size = 336592 }, + { url = "https://files.pythonhosted.org/packages/81/d5/6e0460292d6299ac3919945f912b16b104f4e81ab20bf53e0872a1296daf/yarl-1.18.3-cp39-cp39-win32.whl", hash = "sha256:ac36703a585e0929b032fbaab0707b75dc12703766d0b53486eabd5139ebadd5", size = 84833 }, + { url = "https://files.pythonhosted.org/packages/b2/fc/a8aef69156ad5508165d8ae956736d55c3a68890610834bd985540966008/yarl-1.18.3-cp39-cp39-win_amd64.whl", hash = "sha256:ba87babd629f8af77f557b61e49e7c7cac36f22f871156b91e10a6e9d4f829e9", size = 90968 }, + { url = "https://files.pythonhosted.org/packages/f5/4b/a06e0ec3d155924f77835ed2d167ebd3b211a7b0853da1cf8d8414d784ef/yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b", size = 45109 }, +] + +[[package]] +name = "zipp" +version = "3.20.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/54/bf/5c0000c44ebc80123ecbdddba1f5dcd94a5ada602a9c225d84b5aaa55e86/zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29", size = 24199 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/8b/5ba542fa83c90e09eac972fc9baca7a88e7e7ca4b221a89251954019308b/zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350", size = 9200 }, +] + +[[package]] +name = "zipp" +version = "3.21.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630 }, +] From bf15eee9be46bcbf64740b3f144a0105d2294d0b Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Wed, 11 Dec 2024 12:11:33 +0100 Subject: [PATCH 02/64] Defer import of aio_pika --- src/plumpy/exceptions.py | 12 +++++++++--- src/plumpy/processes.py | 5 +++-- src/plumpy/rmq/__init__.py | 4 ++++ src/plumpy/rmq/exceptions.py | 11 +++++++++++ 4 files changed, 27 insertions(+), 5 deletions(-) create mode 100644 src/plumpy/rmq/__init__.py create mode 100644 src/plumpy/rmq/exceptions.py diff --git a/src/plumpy/exceptions.py b/src/plumpy/exceptions.py index 70b5aa2d..2f290e6a 100644 --- a/src/plumpy/exceptions.py +++ b/src/plumpy/exceptions.py @@ -1,7 +1,13 @@ # -*- coding: utf-8 -*- from typing import Optional -__all__ = ['ClosedError', 'InvalidStateError', 'KilledError', 'PersistenceError', 'UnsuccessfulResult'] +__all__ = [ + 'ClosedError', + 'InvalidStateError', + 'KilledError', + 'PersistenceError', + 'UnsuccessfulResult', +] class KilledError(Exception): @@ -9,8 +15,7 @@ class KilledError(Exception): class InvalidStateError(Exception): - """ - Raised when an operation is attempted that requires the process to be in a state + """Raised when an operation is attempted that requires the process to be in a state that is different from the current state """ @@ -33,3 +38,4 @@ class PersistenceError(Exception): class ClosedError(Exception): """Raised when an mutable operation is attempted on a closed process""" + diff --git a/src/plumpy/processes.py b/src/plumpy/processes.py index 409374d0..7c5e08fc 100644 --- a/src/plumpy/processes.py +++ b/src/plumpy/processes.py @@ -38,7 +38,6 @@ import kiwipy import yaml -from aio_pika.exceptions import ChannelInvalidStateError, ConnectionClosed from . import ( events, @@ -735,6 +734,8 @@ def on_entering(self, state: process_states.State) -> None: call_with_super_check(self.on_except, state.get_exc_info()) # type: ignore def on_entered(self, from_state: Optional[process_states.State]) -> None: + from plumpy.rmq.exceptions import CommunicatorChannelInvalidStateError, CommunicatorConnectionClosed + # Map these onto direct functions that the subclass can implement state_label = self._state.LABEL if state_label == process_states.ProcessState.RUNNING: @@ -754,7 +755,7 @@ def on_entered(self, from_state: Optional[process_states.State]) -> None: self.logger.info('Process<%s>: Broadcasting state change: %s', self.pid, subject) try: self._communicator.broadcast_send(body=None, sender=self.pid, subject=subject) - except (ConnectionClosed, ChannelInvalidStateError): + except (CommunicatorConnectionClosed, CommunicatorChannelInvalidStateError): message = 'Process<%s>: no connection available to broadcast state change from %s to %s' self.logger.warning(message, self.pid, from_label, self.state.value) except kiwipy.TimeoutError: diff --git a/src/plumpy/rmq/__init__.py b/src/plumpy/rmq/__init__.py new file mode 100644 index 00000000..31d97783 --- /dev/null +++ b/src/plumpy/rmq/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +from .exceptions import * + +__all__ = exceptions.__all__ diff --git a/src/plumpy/rmq/exceptions.py b/src/plumpy/rmq/exceptions.py new file mode 100644 index 00000000..b15d51c4 --- /dev/null +++ b/src/plumpy/rmq/exceptions.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +from aio_pika.exceptions import ChannelInvalidStateError, ConnectionClosed + +__all__ = [ + 'CommunicatorChannelInvalidStateError', + 'CommunicatorConnectionClosed', +] + +# Alias aio_pika +CommunicatorConnectionClosed = ConnectionClosed +CommunicatorChannelInvalidStateError = ChannelInvalidStateError From 7c1afd35bea1dbab259bf06b1cd8130787374a74 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Wed, 11 Dec 2024 23:50:36 +0100 Subject: [PATCH 03/64] Explicit future implementation: distinguish concurrent.future.Future and asyncio.Future hand write wrap to kiwipy future (concurrent.futures.Future) kiwipy.Future -> concurrent.futures.Future --- .python-version | 1 + src/plumpy/__init__.py | 1 + src/plumpy/communications.py | 28 +-- src/plumpy/exceptions.py | 1 - src/plumpy/futures.py | 64 +++---- src/plumpy/message.py | 299 +++++++++++++++++++++++++++++++++ src/plumpy/process_comms.py | 3 +- src/plumpy/processes.py | 6 +- src/plumpy/rmq/exceptions.py | 3 + src/plumpy/rmq/futures.py | 111 ++++++++++++ src/plumpy/workchains.py | 4 +- tests/rmq/test_communicator.py | 8 +- tests/test_processes.py | 6 +- tests/utils.py | 4 +- 14 files changed, 454 insertions(+), 85 deletions(-) create mode 100644 .python-version create mode 100644 src/plumpy/message.py create mode 100644 src/plumpy/rmq/futures.py diff --git a/.python-version b/.python-version new file mode 100644 index 00000000..413c7e7e --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +aiida-core-dev-3.12 diff --git a/src/plumpy/__init__.py b/src/plumpy/__init__.py index 46cac83a..adc302ef 100644 --- a/src/plumpy/__init__.py +++ b/src/plumpy/__init__.py @@ -16,6 +16,7 @@ from .process_listener import * from .process_states import * from .processes import * +from .rmq import * from .utils import * from .workchains import * diff --git a/src/plumpy/communications.py b/src/plumpy/communications.py index 1d7e775b..04e39d58 100644 --- a/src/plumpy/communications.py +++ b/src/plumpy/communications.py @@ -15,7 +15,6 @@ 'DeliveryFailed', 'RemoteException', 'TaskRejected', - 'plum_to_kiwi_future', 'wrap_communicator', ] @@ -36,31 +35,6 @@ BroadcastSubscriber = Callable[[kiwipy.Communicator, Any, Any, Any, ID_TYPE], Any] -def plum_to_kiwi_future(plum_future: futures.Future) -> kiwipy.Future: - """ - Return a kiwi future that resolves to the outcome of the plum future - - :param plum_future: the plum future - :return: the kiwipy future - - """ - kiwi_future = kiwipy.Future() - - def on_done(_plum_future: futures.Future) -> None: - with kiwipy.capture_exceptions(kiwi_future): - if plum_future.cancelled(): - kiwi_future.cancel() - else: - result = plum_future.result() - # Did we get another future? In which case convert it too - if isinstance(result, futures.Future): - result = plum_to_kiwi_future(result) - kiwi_future.set_result(result) - - plum_future.add_done_callback(on_done) - return kiwi_future - - def convert_to_comm( callback: 'Subscriber', loop: Optional[asyncio.AbstractEventLoop] = None ) -> Callable[..., kiwipy.Future]: @@ -97,7 +71,7 @@ def converted(communicator: kiwipy.Communicator, *args: Any, **kwargs: Any) -> k msg_fn = functools.partial(coro, communicator, *args, **kwargs) task_future = futures.create_task(msg_fn, loop) - return plum_to_kiwi_future(task_future) + return wrap_to_concurrent_future(task_future) return converted diff --git a/src/plumpy/exceptions.py b/src/plumpy/exceptions.py index 2f290e6a..6f0c75a4 100644 --- a/src/plumpy/exceptions.py +++ b/src/plumpy/exceptions.py @@ -38,4 +38,3 @@ class PersistenceError(Exception): class ClosedError(Exception): """Raised when an mutable operation is attempted on a closed process""" - diff --git a/src/plumpy/futures.py b/src/plumpy/futures.py index f52a0d09..a467f5d8 100644 --- a/src/plumpy/futures.py +++ b/src/plumpy/futures.py @@ -4,24 +4,33 @@ """ import asyncio -from typing import Any, Awaitable, Callable, Optional +import contextlib +from typing import Any, Awaitable, Callable, Generator, Optional -import kiwipy +__all__ = ['CancellableAction', 'create_task', 'create_task'] -__all__ = ['CancelledError', 'Future', 'chain', 'copy_future', 'create_task', 'gather'] -CancelledError = kiwipy.CancelledError +class InvalidFutureError(Exception): + """Exception for when a future or action is in an invalid state""" -class InvalidStateError(Exception): - """Exception for when a future or action is in an invalid state""" +Future = asyncio.Future -copy_future = kiwipy.copy_future -chain = kiwipy.chain -gather = asyncio.gather +@contextlib.contextmanager +def capture_exceptions(future: Future[Any], ignore: tuple[type[BaseException], ...] = ()) -> Generator[None, Any, None]: + """ + Capture any exceptions in the context and set them as the result of the given future -Future = asyncio.Future + :param future: The future to the exception on + :param ignore: An optional list of exception types to ignore, these will be raised and not set on the future + """ + try: + yield + except ignore: + raise + except Exception as exception: + future.set_exception(exception) class CancellableAction(Future): @@ -46,10 +55,10 @@ def run(self, *args: Any, **kwargs: Any) -> None: :param kwargs: the keyword arguments to the action """ if self.done(): - raise InvalidStateError('Action has already been ran') + raise InvalidFutureError('Action has already been ran') try: - with kiwipy.capture_exceptions(self): + with capture_exceptions(self): self.set_result(self._action(*args, **kwargs)) finally: self._action = None # type: ignore @@ -70,38 +79,9 @@ def create_task(coro: Callable[[], Awaitable[Any]], loop: Optional[asyncio.Abstr future = loop.create_future() async def run_task() -> None: - with kiwipy.capture_exceptions(future): + with capture_exceptions(future): res = await coro() future.set_result(res) asyncio.run_coroutine_threadsafe(run_task(), loop) return future - - -def unwrap_kiwi_future(future: kiwipy.Future) -> kiwipy.Future: - """ - Create a kiwi future that represents the final results of a nested series of futures, - meaning that if the futures provided itself resolves to a future the returned - future will not resolve to a value until the final chain of futures is not a future - but a concrete value. If at any point in the chain a future resolves to an exception - then the returned future will also resolve to that exception. - - :param future: the future to unwrap - :return: the unwrapping future - - """ - unwrapping = kiwipy.Future() - - def unwrap(fut: kiwipy.Future) -> None: - if fut.cancelled(): - unwrapping.cancel() - else: - with kiwipy.capture_exceptions(unwrapping): - result = fut.result() - if isinstance(result, kiwipy.Future): - result.add_done_callback(unwrap) - else: - unwrapping.set_result(result) - - future.add_done_callback(unwrap) - return unwrapping diff --git a/src/plumpy/message.py b/src/plumpy/message.py new file mode 100644 index 00000000..47586d21 --- /dev/null +++ b/src/plumpy/message.py @@ -0,0 +1,299 @@ +# -*- coding: utf-8 -*- +"""Module for process level communication functions and classes""" + +import asyncio +import logging +from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence, Union, cast + +from plumpy.coordinator import Communicator +from plumpy.exceptions import PersistenceError, TaskRejectedError + +from . import loaders, persistence +from .utils import PID_TYPE + +__all__ = [ + 'KILL_MSG', + 'PAUSE_MSG', + 'PLAY_MSG', + 'STATUS_MSG', + 'ProcessLauncher', + 'create_continue_body', + 'create_launch_body', +] + +if TYPE_CHECKING: + from .processes import Process + +INTENT_KEY = 'intent' +MESSAGE_KEY = 'message' + + +class Intent: + """Intent constants for a process message""" + + PLAY: str = 'play' + PAUSE: str = 'pause' + KILL: str = 'kill' + STATUS: str = 'status' + + +PAUSE_MSG = {INTENT_KEY: Intent.PAUSE} +PLAY_MSG = {INTENT_KEY: Intent.PLAY} +KILL_MSG = {INTENT_KEY: Intent.KILL} +STATUS_MSG = {INTENT_KEY: Intent.STATUS} + +TASK_KEY = 'task' +TASK_ARGS = 'args' +PERSIST_KEY = 'persist' +# Launch +PROCESS_CLASS_KEY = 'process_class' +ARGS_KEY = 'init_args' +KWARGS_KEY = 'init_kwargs' +NOWAIT_KEY = 'nowait' +# Continue +PID_KEY = 'pid' +TAG_KEY = 'tag' +# Task types +LAUNCH_TASK = 'launch' +CONTINUE_TASK = 'continue' +CREATE_TASK = 'create' + +LOGGER = logging.getLogger(__name__) + + +def create_launch_body( + process_class: str, + init_args: Optional[Sequence[Any]] = None, + init_kwargs: Optional[Dict[str, Any]] = None, + persist: bool = False, + loader: Optional[loaders.ObjectLoader] = None, + nowait: bool = True, +) -> Dict[str, Any]: + """ + Create a message body for the launch action + + :param process_class: the class of the process to launch + :param init_args: any initialisation positional arguments + :param init_kwargs: any initialisation keyword arguments + :param persist: persist this process if True, otherwise don't + :param loader: the loader to use to load the persisted process + :param nowait: wait for the process to finish before completing the task, otherwise just return the PID + :return: a dictionary with the body of the message to launch the process + :rtype: dict + """ + if loader is None: + loader = loaders.get_object_loader() + + msg_body = { + TASK_KEY: LAUNCH_TASK, + TASK_ARGS: { + PROCESS_CLASS_KEY: loader.identify_object(process_class), + PERSIST_KEY: persist, + NOWAIT_KEY: nowait, + ARGS_KEY: init_args, + KWARGS_KEY: init_kwargs, + }, + } + return msg_body + + +def create_continue_body(pid: 'PID_TYPE', tag: Optional[str] = None, nowait: bool = False) -> Dict[str, Any]: + """ + Create a message body to continue an existing process + :param pid: the pid of the existing process + :param tag: the optional persistence tag + :param nowait: wait for the process to finish before completing the task, otherwise just return the PID + :return: a dictionary with the body of the message to continue the process + + """ + msg_body = {TASK_KEY: CONTINUE_TASK, TASK_ARGS: {PID_KEY: pid, NOWAIT_KEY: nowait, TAG_KEY: tag}} + return msg_body + + +def create_create_body( + process_class: str, + init_args: Optional[Sequence[Any]] = None, + init_kwargs: Optional[Dict[str, Any]] = None, + persist: bool = False, + loader: Optional[loaders.ObjectLoader] = None, +) -> Dict[str, Any]: + """ + Create a message body to create a new process + :param process_class: the class of the process to launch + :param init_args: any initialisation positional arguments + :param init_kwargs: any initialisation keyword arguments + :param persist: persist this process if True, otherwise don't + :param loader: the loader to use to load the persisted process + :return: a dictionary with the body of the message to launch the process + + """ + if loader is None: + loader = loaders.get_object_loader() + + msg_body = { + TASK_KEY: CREATE_TASK, + TASK_ARGS: { + PROCESS_CLASS_KEY: loader.identify_object(process_class), + PERSIST_KEY: persist, + ARGS_KEY: init_args, + KWARGS_KEY: init_kwargs, + }, + } + return msg_body + + +class ProcessLauncher: + """ + Takes incoming task messages and uses them to launch processes. + + Expected format of task: + + For launch:: + + { + 'task': + 'process_class': + 'args': + 'kwargs': . + 'nowait': True or False + } + + For continue:: + + { + 'task': + 'pid': + 'nowait': True or False + } + """ + + def __init__( + self, + loop: Optional[asyncio.AbstractEventLoop] = None, + persister: Optional[persistence.Persister] = None, + load_context: Optional[persistence.LoadSaveContext] = None, + loader: Optional[loaders.ObjectLoader] = None, + ) -> None: + self._loop = loop + self._persister = persister + self._load_context = load_context if load_context is not None else persistence.LoadSaveContext() + + if loader is not None: + self._loader = loader + self._load_context = self._load_context.copyextend(loader=loader) + else: + self._loader = loaders.get_object_loader() + + async def __call__(self, communicator: Communicator, task: Dict[str, Any]) -> Union[PID_TYPE, Any]: + """ + Receive a task. + :param task: The task message + """ + task_type = task[TASK_KEY] + if task_type == LAUNCH_TASK: + return await self._launch(**task.get(TASK_ARGS, {})) + if task_type == CONTINUE_TASK: + return await self._continue(**task.get(TASK_ARGS, {})) + if task_type == CREATE_TASK: + return await self._create(**task.get(TASK_ARGS, {})) + + raise TaskRejectedError + + async def _launch( + self, + process_class: str, + persist: bool, + nowait: bool, + init_args: Optional[Sequence[Any]] = None, + init_kwargs: Optional[Dict[str, Any]] = None, + ) -> Union[PID_TYPE, Any]: + """ + Launch the process + + :param _communicator: the communicator + :param process_class: the process class to launch + :param persist: should the process be persisted + :param nowait: if True only return when the process finishes + :param init_args: positional arguments to the process constructor + :param init_kwargs: keyword arguments to the process constructor + :return: the pid of the created process or the outputs (if nowait=False) + """ + if persist and not self._persister: + raise PersistenceError('Cannot persist process, no persister') + + if init_args is None: + init_args = () + if init_kwargs is None: + init_kwargs = {} + + proc_class = self._loader.load_object(process_class) + proc = proc_class(*init_args, **init_kwargs) + if persist and self._persister is not None: + self._persister.save_checkpoint(proc) + + if nowait: + # XXX: can return a reference and gracefully use task to cancel itself when the upper call stack fails + asyncio.ensure_future(proc.step_until_terminated()) # noqa: RUF006 + return proc.pid + + await proc.step_until_terminated() + + return proc.future().result() + + async def _continue(self, pid: 'PID_TYPE', nowait: bool, tag: Optional[str] = None) -> Union[PID_TYPE, Any]: + """ + Continue the process + + :param _communicator: the communicator + :param pid: the pid of the process to continue + :param nowait: if True don't wait for the process to complete + :param tag: the checkpoint tag to continue from + """ + if not self._persister: + LOGGER.warning('rejecting task: cannot continue process<%d> because no persister is available', pid) + raise PersistenceError('Cannot continue process, no persister') + + # Do not catch exceptions here, because if these operations fail, the continue task should except and bubble up + saved_state = self._persister.load_checkpoint(pid, tag) + proc = cast('Process', saved_state.unbundle(self._load_context)) + + if nowait: + # XXX: can return a reference and gracefully use task to cancel itself when the upper call stack fails + asyncio.ensure_future(proc.step_until_terminated()) # noqa: RUF006 + return proc.pid + + await proc.step_until_terminated() + + return proc.future().result() + + async def _create( + self, + process_class: str, + persist: bool, + init_args: Optional[Sequence[Any]] = None, + init_kwargs: Optional[Dict[str, Any]] = None, + ) -> 'PID_TYPE': + """ + Create the process + + :param _communicator: the communicator + :param process_class: the process class to create + :param persist: should the process be persisted + :param init_args: positional arguments to the process constructor + :param init_kwargs: keyword arguments to the process constructor + :return: the pid of the created process + """ + if persist and not self._persister: + raise PersistenceError('Cannot persist process, no persister') + + if init_args is None: + init_args = () + if init_kwargs is None: + init_kwargs = {} + + proc_class = self._loader.load_object(process_class) + proc = proc_class(*init_args, **init_kwargs) + if persist and self._persister is not None: + self._persister.save_checkpoint(proc) + + return proc.pid diff --git a/src/plumpy/process_comms.py b/src/plumpy/process_comms.py index 2d6b3bf4..98f7128e 100644 --- a/src/plumpy/process_comms.py +++ b/src/plumpy/process_comms.py @@ -475,11 +475,10 @@ def execute_process( :param no_reply: if True, this call will be fire-and-forget, i.e. no return value :return: the result of executing the process """ - message = create_create_body(process_class, init_args, init_kwargs, persist=True, loader=loader) execute_future = kiwipy.Future() - create_future = futures.unwrap_kiwi_future(self._communicator.task_send(message)) + create_future = self._communicator.task_send(message) def on_created(_: Any) -> None: with kiwipy.capture_exceptions(execute_future): diff --git a/src/plumpy/processes.py b/src/plumpy/processes.py index 7c5e08fc..89b84ef4 100644 --- a/src/plumpy/processes.py +++ b/src/plumpy/processes.py @@ -977,7 +977,7 @@ def message_receive(self, _comm: kiwipy.Communicator, msg: MessageType) -> Any: def broadcast_receive( self, _comm: kiwipy.Communicator, msg: MessageType, sender: Any, subject: Any, correlation_id: Any - ) -> Optional[kiwipy.Future]: + ) -> Optional[concurrent.futures.Future]: """ Coroutine called when the process receives a message from the communicator @@ -1002,7 +1002,7 @@ def broadcast_receive( return self._schedule_rpc(self.kill, msg_text=msg.get(process_comms.MESSAGE_TEXT_KEY, None)) return None - def _schedule_rpc(self, callback: Callable[..., Any], *args: Any, **kwargs: Any) -> kiwipy.Future: + def _schedule_rpc(self, callback: Callable[..., Any], *args: Any, **kwargs: Any) -> concurrent.futures.Future: """ Schedule a call to a callback as a result of an RPC communication call, this will return a future that resolves to the final result (even after one or more layer of futures being @@ -1017,7 +1017,7 @@ def _schedule_rpc(self, callback: Callable[..., Any], *args: Any, **kwargs: Any) :return: a kiwi future that resolves to the outcome of the callback """ - kiwi_future = kiwipy.Future() + kiwi_future = concurrent.futures.Future() async def run_callback() -> None: with kiwipy.capture_exceptions(kiwi_future): diff --git a/src/plumpy/rmq/exceptions.py b/src/plumpy/rmq/exceptions.py index b15d51c4..02eb3c97 100644 --- a/src/plumpy/rmq/exceptions.py +++ b/src/plumpy/rmq/exceptions.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import kiwipy from aio_pika.exceptions import ChannelInvalidStateError, ConnectionClosed __all__ = [ @@ -9,3 +10,5 @@ # Alias aio_pika CommunicatorConnectionClosed = ConnectionClosed CommunicatorChannelInvalidStateError = ChannelInvalidStateError + +CancelledError = kiwipy.CancelledError diff --git a/src/plumpy/rmq/futures.py b/src/plumpy/rmq/futures.py new file mode 100644 index 00000000..897c8147 --- /dev/null +++ b/src/plumpy/rmq/futures.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +# mypy: disable-error-code="no-untyped-def, no-untyped-call" +"""Module containing future related methods and classes""" + +import asyncio +import concurrent.futures +from typing import Any + +import kiwipy + +__all__ = ['wrap_to_concurrent_future'] + + +def _convert_future_exc(exc): + exc_class = type(exc) + if exc_class is concurrent.futures.CancelledError: + return asyncio.exceptions.CancelledError(*exc.args) + elif exc_class is concurrent.futures.TimeoutError: + return asyncio.exceptions.TimeoutError(*exc.args) + elif exc_class is concurrent.futures.InvalidStateError: + return asyncio.exceptions.InvalidStateError(*exc.args) + else: + return exc + + +def _set_concurrent_future_state(concurrent, source): + """Copy state from a future to a concurrent.futures.Future.""" + assert source.done() + if source.cancelled(): + concurrent.cancel() + if not concurrent.set_running_or_notify_cancel(): + return + exception = source.exception() + if exception is not None: + concurrent.set_exception(_convert_future_exc(exception)) + else: + result = source.result() + concurrent.set_result(result) + + +def _copy_future_state(source, dest): + """Internal helper to copy state from another Future. + + The other Future may be a concurrent.futures.Future. + """ + assert source.done() + if dest.cancelled(): + return + assert not dest.done() + if source.cancelled(): + dest.cancel() + else: + exception = source.exception() + if exception is not None: + dest.set_exception(_convert_future_exc(exception)) + else: + result = source.result() + dest.set_result(result) + + +def _chain_future(source, destination): + """Chain two futures so that when one completes, so does the other. + + The result (or exception) of source will be copied to destination. + If destination is cancelled, source gets cancelled too. + Compatible with both asyncio.Future and concurrent.futures.Future. + """ + if not asyncio.isfuture(source) and not isinstance(source, concurrent.futures.Future): + raise TypeError('A future is required for source argument') + if not asyncio.isfuture(destination) and not isinstance(destination, concurrent.futures.Future): + raise TypeError('A future is required for destination argument') + source_loop = asyncio.Future.get_loop(source) if asyncio.isfuture(source) else None + dest_loop = asyncio.Future.get_loop(destination) if asyncio.isfuture(destination) else None + + def _set_state(future, other): + if asyncio.isfuture(future): + _copy_future_state(other, future) + else: + _set_concurrent_future_state(future, other) + + def _call_check_cancel(destination): + if destination.cancelled(): + if source_loop is None or source_loop is dest_loop: + source.cancel() + else: + source_loop.call_soon_threadsafe(source.cancel) + + def _call_set_state(source): + if destination.cancelled() and dest_loop is not None and dest_loop.is_closed(): + return + if dest_loop is None or dest_loop is source_loop: + _set_state(destination, source) + else: + if dest_loop.is_closed(): + return + dest_loop.call_soon_threadsafe(_set_state, destination, source) + + destination.add_done_callback(_call_check_cancel) + source.add_done_callback(_call_set_state) + + +def wrap_to_concurrent_future(future: asyncio.Future[Any]) -> kiwipy.Future: + """Wrap to concurrent.futures.Future object. (the function is adapted from asyncio.future.wrap_future). + The function `_chain_future`, `_copy_future_state` is from asyncio future module.""" + if isinstance(future, concurrent.futures.Future): + return future + assert asyncio.isfuture(future), f'concurrent.futures.Future is expected, got {future!r}' + + new_future = kiwipy.Future() + _chain_future(future, new_future) + return new_future diff --git a/src/plumpy/workchains.py b/src/plumpy/workchains.py index b48b1c6b..7e67253f 100644 --- a/src/plumpy/workchains.py +++ b/src/plumpy/workchains.py @@ -23,7 +23,7 @@ cast, ) -import kiwipy +from plumpy.coordinator import Communicator from . import lang, mixins, persistence, process_states, processes from .utils import PID_TYPE, SAVED_STATE_TYPE @@ -128,7 +128,7 @@ def __init__( pid: Optional[PID_TYPE] = None, logger: Optional[logging.Logger] = None, loop: Optional[asyncio.AbstractEventLoop] = None, - communicator: Optional[kiwipy.Communicator] = None, + communicator: Optional[Communicator] = None, ) -> None: super().__init__(inputs=inputs, pid=pid, logger=logger, loop=loop, communicator=communicator) self._stepper: Optional[Stepper] = None diff --git a/tests/rmq/test_communicator.py b/tests/rmq/test_communicator.py index 3f2570d8..d64fa4a5 100644 --- a/tests/rmq/test_communicator.py +++ b/tests/rmq/test_communicator.py @@ -63,7 +63,7 @@ class TestLoopCommunicator: @pytest.mark.asyncio async def test_broadcast(self, loop_communicator): BROADCAST = {'body': 'present', 'sender': 'Martin', 'subject': 'sup', 'correlation_id': 420} # noqa: N806 - broadcast_future = plumpy.Future() + broadcast_future = asyncio.Future() loop = asyncio.get_event_loop() @@ -82,7 +82,7 @@ def get_broadcast(_comm, body, sender, subject, correlation_id): @pytest.mark.asyncio async def test_broadcast_filter(self, loop_communicator): - broadcast_future = plumpy.Future() + broadcast_future = asyncio.Future() def ignore_broadcast(_comm, body, sender, subject, correlation_id): broadcast_future.set_exception(AssertionError('broadcast received')) @@ -102,7 +102,7 @@ def get_broadcast(_comm, body, sender, subject, correlation_id): @pytest.mark.asyncio async def test_rpc(self, loop_communicator): MSG = 'rpc this' # noqa: N806 - rpc_future = plumpy.Future() + rpc_future = asyncio.Future() loop = asyncio.get_event_loop() @@ -119,7 +119,7 @@ def get_rpc(_comm, msg): @pytest.mark.asyncio async def test_task(self, loop_communicator): TASK = 'task this' # noqa: N806 - task_future = plumpy.Future() + task_future = asyncio.Future() loop = asyncio.get_event_loop() diff --git a/tests/test_processes.py b/tests/test_processes.py index 5d3184f2..3d6b4394 100644 --- a/tests/test_processes.py +++ b/tests/test_processes.py @@ -7,6 +7,8 @@ import kiwipy import pytest +from plumpy.futures import CancellableAction +from tests import utils import plumpy from plumpy import BundleKeys, Process, ProcessState @@ -537,7 +539,7 @@ def test_pause_in_process(self): class TestPausePlay(plumpy.Process): def run(self): fut = self.pause() - test_case.assertIsInstance(fut, plumpy.Future) + assert isinstance(fut, CancellableAction) loop = asyncio.get_event_loop() @@ -561,7 +563,7 @@ def test_pause_play_in_process(self): class TestPausePlay(plumpy.Process): def run(self): fut = self.pause() - test_case.assertIsInstance(fut, plumpy.Future) + test_case.assertIsInstance(fut, CancellableAction) result = self.play() test_case.assertTrue(result) diff --git a/tests/utils.py b/tests/utils.py index 13abc38c..05290990 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -466,7 +466,7 @@ def run_until_waiting(proc): from plumpy import ProcessState listener = plumpy.ProcessListener() - in_waiting = plumpy.Future() + in_waiting = asyncio.Future() if proc.state == ProcessState.WAITING: in_waiting.set_result(True) @@ -486,7 +486,7 @@ def run_until_paused(proc): """Set up a future that will be resolved when the process is paused""" listener = plumpy.ProcessListener() - paused = plumpy.Future() + paused = asyncio.Future() if proc.paused: paused.set_result(True) From 981fa91f754c503412caebb74f922f6d5fdcd630 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Wed, 11 Dec 2024 20:59:07 +0100 Subject: [PATCH 04/64] Move communication into rmq module Move communication to rmq --- docs/source/nitpick-exceptions | 2 +- src/plumpy/__init__.py | 8 +- src/plumpy/futures.py | 2 +- src/plumpy/processes.py | 52 +++-- src/plumpy/rmq/__init__.py | 4 +- src/plumpy/{ => rmq}/communications.py | 4 +- src/plumpy/{ => rmq}/process_comms.py | 182 ++---------------- tests/{ => rmq}/test_communications.py | 4 +- tests/rmq/test_communicator.py | 2 +- tests/rmq/test_process_comms.py | 4 +- ...{test_process_comms.py => test_message.py} | 6 +- 11 files changed, 54 insertions(+), 216 deletions(-) rename src/plumpy/{ => rmq}/communications.py (98%) rename src/plumpy/{ => rmq}/process_comms.py (74%) rename tests/{ => rmq}/test_communications.py (95%) rename tests/{test_process_comms.py => test_message.py} (90%) diff --git a/docs/source/nitpick-exceptions b/docs/source/nitpick-exceptions index 2f354987..6aa8c345 100644 --- a/docs/source/nitpick-exceptions +++ b/docs/source/nitpick-exceptions @@ -23,7 +23,7 @@ py:class plumpy.base.state_machine.State py:class State py:class Process py:class plumpy.futures.CancellableAction -py:class plumpy.communications.LoopCommunicator +py:class plumpy.rmq.communications.LoopCommunicator py:class plumpy.persistence.PersistedPickle py:class plumpy.utils.AttributesFrozendict py:class plumpy.workchains._FunctionCall diff --git a/src/plumpy/__init__.py b/src/plumpy/__init__.py index adc302ef..8f62edb6 100644 --- a/src/plumpy/__init__.py +++ b/src/plumpy/__init__.py @@ -4,21 +4,20 @@ import logging -from .communications import * from .events import * from .exceptions import * from .futures import * from .loaders import * +from .message import * from .mixins import * from .persistence import * from .ports import * -from .process_comms import * from .process_listener import * from .process_states import * from .processes import * -from .rmq import * from .utils import * from .workchains import * +from .rmq import * __all__ = ( events.__all__ @@ -28,8 +27,7 @@ + futures.__all__ + mixins.__all__ + persistence.__all__ - + communications.__all__ - + process_comms.__all__ + + message.__all__ + process_listener.__all__ + workchains.__all__ + loaders.__all__ diff --git a/src/plumpy/futures.py b/src/plumpy/futures.py index a467f5d8..2f861d64 100644 --- a/src/plumpy/futures.py +++ b/src/plumpy/futures.py @@ -7,7 +7,7 @@ import contextlib from typing import Any, Awaitable, Callable, Generator, Optional -__all__ = ['CancellableAction', 'create_task', 'create_task'] +__all__ = ['CancellableAction', 'create_task', 'create_task', 'capture_exceptions'] class InvalidFutureError(Exception): diff --git a/src/plumpy/processes.py b/src/plumpy/processes.py index 89b84ef4..5b7c951d 100644 --- a/src/plumpy/processes.py +++ b/src/plumpy/processes.py @@ -39,16 +39,8 @@ import kiwipy import yaml -from . import ( - events, - exceptions, - futures, - persistence, - ports, - process_comms, - process_states, - utils, -) +from . import events, exceptions, message, persistence, ports, process_states, utils +from .futures import capture_exceptions, CancellableAction from .base import state_machine from .base.state_machine import StateEntryFailed, StateMachine, TransitionFailed, event from .base.utils import call_with_super_check, super_check @@ -153,10 +145,10 @@ class Process(StateMachine, persistence.Savable, metaclass=ProcessStateMachineMe _spec_class = ProcessSpec # Default placeholders, will be populated in init() _stepping = False - _pausing: Optional[futures.CancellableAction] = None + _pausing: Optional[CancellableAction] = None _paused: Optional[persistence.SavableFuture] = None - _killing: Optional[futures.CancellableAction] = None - _interrupt_action: Optional[futures.CancellableAction] = None + _killing: Optional[CancellableAction] = None + _interrupt_action: Optional[CancellableAction] = None _closed = False _cleanups: Optional[List[Callable[[], None]]] = None @@ -341,7 +333,7 @@ def init(self) -> None: if not self._future.done(): - def try_killing(future: futures.Future) -> None: + def try_killing(future: asyncio.Future) -> None: if future.cancelled(): if not self.kill('Killed by future being cancelled'): self.logger.warning( @@ -959,15 +951,15 @@ def message_receive(self, _comm: kiwipy.Communicator, msg: MessageType) -> Any: msg, ) - intent = msg[process_comms.INTENT_KEY] + intent = msg[message.INTENT_KEY] - if intent == process_comms.Intent.PLAY: + if intent == message.Intent.PLAY: return self._schedule_rpc(self.play) - if intent == process_comms.Intent.PAUSE: - return self._schedule_rpc(self.pause, msg_text=msg.get(process_comms.MESSAGE_TEXT_KEY, None)) - if intent == process_comms.Intent.KILL: - return self._schedule_rpc(self.kill, msg_text=msg.get(process_comms.MESSAGE_TEXT_KEY, None)) - if intent == process_comms.Intent.STATUS: + if intent == message.Intent.PAUSE: + return self._schedule_rpc(self.pause, msg_text=msg.get(message.MESSAGE_KEY, None)) + if intent == message.Intent.KILL: + return self._schedule_rpc(self.kill, msg_text=msg.get(message.MESSAGE_KEY, None)) + if intent == message.Intent.STATUS: status_info: Dict[str, Any] = {} self.get_status_info(status_info) return status_info @@ -994,7 +986,7 @@ def broadcast_receive( ) # If we get a message we recognise then action it, otherwise ignore - if subject == process_comms.Intent.PLAY: + if subject == message.Intent.PLAY: return self._schedule_rpc(self.play) if subject == process_comms.Intent.PAUSE: return self._schedule_rpc(self.pause, msg_text=msg.get(process_comms.MESSAGE_TEXT_KEY, None)) @@ -1020,7 +1012,7 @@ def _schedule_rpc(self, callback: Callable[..., Any], *args: Any, **kwargs: Any) kiwi_future = concurrent.futures.Future() async def run_callback() -> None: - with kiwipy.capture_exceptions(kiwi_future): + with capture_exceptions(kiwi_future): try: result = callback(*args, **kwargs) except Exception as exc: @@ -1097,7 +1089,7 @@ def transition_failed( ) self.transition_to(new_state) - def pause(self, msg_text: Optional[str] = None) -> Union[bool, futures.CancellableAction]: + def pause(self, msg_text: Optional[str] = None) -> Union[bool, CancellableAction]: """Pause the process. :param msg: an optional message to set as the status. The current status will be saved in the private @@ -1126,7 +1118,7 @@ def pause(self, msg_text: Optional[str] = None) -> Union[bool, futures.Cancellab self._pausing = self._interrupt_action # Try to interrupt the state self._state.interrupt(interrupt_exception) - return cast(futures.CancellableAction, self._interrupt_action) + return cast(CancellableAction, self._interrupt_action) msg = MessageBuilder.pause(msg_text) return self._do_pause(state_msg=msg) @@ -1149,7 +1141,7 @@ def _do_pause(self, state_msg: Optional[MessageType], next_state: Optional[proce return True - def _create_interrupt_action(self, exception: process_states.Interruption) -> futures.CancellableAction: + def _create_interrupt_action(self, exception: process_states.Interruption) -> CancellableAction: """ Create an interrupt action from the corresponding interrupt exception @@ -1159,7 +1151,7 @@ def _create_interrupt_action(self, exception: process_states.Interruption) -> fu """ if isinstance(exception, process_states.PauseInterruption): do_pause = functools.partial(self._do_pause, exception.msg) - return futures.CancellableAction(do_pause, cookie=exception) + return CancellableAction(do_pause, cookie=exception) if isinstance(exception, process_states.KillInterruption): @@ -1171,11 +1163,11 @@ def do_kill(_next_state: process_states.State) -> Any: finally: self._killing = None - return futures.CancellableAction(do_kill, cookie=exception) + return CancellableAction(do_kill, cookie=exception) raise ValueError(f"Got unknown interruption type '{type(exception)}'") - def _set_interrupt_action(self, new_action: Optional[futures.CancellableAction]) -> None: + def _set_interrupt_action(self, new_action: Optional[CancellableAction]) -> None: """ Set the interrupt action cancelling the current one if it exists :param new_action: The new interrupt action to set @@ -1247,7 +1239,7 @@ def kill(self, msg_text: Optional[str] = None) -> Union[bool, asyncio.Future]: self._set_interrupt_action_from_exception(interrupt_exception) self._killing = self._interrupt_action self._state.interrupt(interrupt_exception) - return cast(futures.CancellableAction, self._interrupt_action) + return cast(CancellableAction, self._interrupt_action) msg = MessageBuilder.kill(msg_text) new_state = self._create_state_instance(process_states.ProcessState.KILLED, msg=msg) diff --git a/src/plumpy/rmq/__init__.py b/src/plumpy/rmq/__init__.py index 31d97783..ad0642ca 100644 --- a/src/plumpy/rmq/__init__.py +++ b/src/plumpy/rmq/__init__.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- from .exceptions import * +from .futures import * +from .process_comms import * -__all__ = exceptions.__all__ +__all__ = exceptions.__all__ + communications.__all__ + futures.__all__ + process_comms.__all__ diff --git a/src/plumpy/communications.py b/src/plumpy/rmq/communications.py similarity index 98% rename from src/plumpy/communications.py rename to src/plumpy/rmq/communications.py index 04e39d58..b66e9694 100644 --- a/src/plumpy/communications.py +++ b/src/plumpy/rmq/communications.py @@ -7,8 +7,8 @@ import kiwipy -from . import futures -from .utils import ensure_coroutine +from plumpy import futures +from plumpy.utils import ensure_coroutine __all__ = [ 'Communicator', diff --git a/src/plumpy/process_comms.py b/src/plumpy/rmq/process_comms.py similarity index 74% rename from src/plumpy/process_comms.py rename to src/plumpy/rmq/process_comms.py index 98f7128e..010fd67d 100644 --- a/src/plumpy/process_comms.py +++ b/src/plumpy/rmq/process_comms.py @@ -9,21 +9,28 @@ import kiwipy -from . import communications, futures, loaders, persistence -from .utils import PID_TYPE +from plumpy.message import ( + MESSAGE_KEY, + PAUSE_MSG, + PLAY_MSG, + STATUS_MSG, + KILL_MSG, + Intent, + create_continue_body, + create_create_body, + create_launch_body, +) + +from plumpy import loaders +from plumpy.utils import PID_TYPE __all__ = [ 'MessageBuilder', 'ProcessLauncher', 'RemoteProcessController', 'RemoteProcessThreadController', - 'create_continue_body', - 'create_launch_body', ] -if TYPE_CHECKING: - from .processes import Process - ProcessResult = Any ProcessStatus = Any @@ -498,164 +505,3 @@ def task_send(self, message: Any, no_reply: bool = False) -> Optional[Any]: :return: the response from the remote side (if no_reply=False) """ return self._communicator.task_send(message, no_reply=no_reply) - - -class ProcessLauncher: - """ - Takes incoming task messages and uses them to launch processes. - - Expected format of task: - - For launch:: - - { - 'task': - 'process_class': - 'args': - 'kwargs': . - 'nowait': True or False - } - - For continue:: - - { - 'task': - 'pid': - 'nowait': True or False - } - """ - - def __init__( - self, - loop: Optional[asyncio.AbstractEventLoop] = None, - persister: Optional[persistence.Persister] = None, - load_context: Optional[persistence.LoadSaveContext] = None, - loader: Optional[loaders.ObjectLoader] = None, - ) -> None: - self._loop = loop - self._persister = persister - self._load_context = load_context if load_context is not None else persistence.LoadSaveContext() - - if loader is not None: - self._loader = loader - self._load_context = self._load_context.copyextend(loader=loader) - else: - self._loader = loaders.get_object_loader() - - async def __call__(self, communicator: kiwipy.Communicator, task: Dict[str, Any]) -> Union[PID_TYPE, ProcessResult]: - """ - Receive a task. - :param task: The task message - """ - task_type = task[TASK_KEY] - if task_type == LAUNCH_TASK: - return await self._launch(communicator, **task.get(TASK_ARGS, {})) - if task_type == CONTINUE_TASK: - return await self._continue(communicator, **task.get(TASK_ARGS, {})) - if task_type == CREATE_TASK: - return await self._create(communicator, **task.get(TASK_ARGS, {})) - - raise communications.TaskRejected - - async def _launch( - self, - _communicator: kiwipy.Communicator, - process_class: str, - persist: bool, - nowait: bool, - init_args: Optional[Sequence[Any]] = None, - init_kwargs: Optional[Dict[str, Any]] = None, - ) -> Union[PID_TYPE, ProcessResult]: - """ - Launch the process - - :param _communicator: the communicator - :param process_class: the process class to launch - :param persist: should the process be persisted - :param nowait: if True only return when the process finishes - :param init_args: positional arguments to the process constructor - :param init_kwargs: keyword arguments to the process constructor - :return: the pid of the created process or the outputs (if nowait=False) - """ - if persist and not self._persister: - raise communications.TaskRejected('Cannot persist process, no persister') - - if init_args is None: - init_args = () - if init_kwargs is None: - init_kwargs = {} - - proc_class = self._loader.load_object(process_class) - proc = proc_class(*init_args, **init_kwargs) - if persist and self._persister is not None: - self._persister.save_checkpoint(proc) - - if nowait: - # XXX: can return a reference and gracefully use task to cancel itself when the upper call stack fails - asyncio.ensure_future(proc.step_until_terminated()) # noqa: RUF006 - return proc.pid - - await proc.step_until_terminated() - - return proc.future().result() - - async def _continue( - self, _communicator: kiwipy.Communicator, pid: 'PID_TYPE', nowait: bool, tag: Optional[str] = None - ) -> Union[PID_TYPE, ProcessResult]: - """ - Continue the process - - :param _communicator: the communicator - :param pid: the pid of the process to continue - :param nowait: if True don't wait for the process to complete - :param tag: the checkpoint tag to continue from - """ - if not self._persister: - LOGGER.warning('rejecting task: cannot continue process<%d> because no persister is available', pid) - raise communications.TaskRejected('Cannot continue process, no persister') - - # Do not catch exceptions here, because if these operations fail, the continue task should except and bubble up - saved_state = self._persister.load_checkpoint(pid, tag) - proc = cast('Process', saved_state.unbundle(self._load_context)) - - if nowait: - # XXX: can return a reference and gracefully use task to cancel itself when the upper call stack fails - asyncio.ensure_future(proc.step_until_terminated()) # noqa: RUF006 - return proc.pid - - await proc.step_until_terminated() - - return proc.future().result() - - async def _create( - self, - _communicator: kiwipy.Communicator, - process_class: str, - persist: bool, - init_args: Optional[Sequence[Any]] = None, - init_kwargs: Optional[Dict[str, Any]] = None, - ) -> 'PID_TYPE': - """ - Create the process - - :param _communicator: the communicator - :param process_class: the process class to create - :param persist: should the process be persisted - :param init_args: positional arguments to the process constructor - :param init_kwargs: keyword arguments to the process constructor - :return: the pid of the created process - """ - if persist and not self._persister: - raise communications.TaskRejected('Cannot persist process, no persister') - - if init_args is None: - init_args = () - if init_kwargs is None: - init_kwargs = {} - - proc_class = self._loader.load_object(process_class) - proc = proc_class(*init_args, **init_kwargs) - if persist and self._persister is not None: - self._persister.save_checkpoint(proc) - - return proc.pid diff --git a/tests/test_communications.py b/tests/rmq/test_communications.py similarity index 95% rename from tests/test_communications.py rename to tests/rmq/test_communications.py index f7e04255..63813bdc 100644 --- a/tests/test_communications.py +++ b/tests/rmq/test_communications.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- -"""Tests for the :mod:`plumpy.communications` module.""" +"""Tests for the :mod:`plumpy.rmq.communications` module.""" import pytest from kiwipy import CommunicatorHelper -from plumpy.communications import LoopCommunicator +from plumpy.rmq.communications import LoopCommunicator class Subscriber: diff --git a/tests/rmq/test_communicator.py b/tests/rmq/test_communicator.py index d64fa4a5..26c9a852 100644 --- a/tests/rmq/test_communicator.py +++ b/tests/rmq/test_communicator.py @@ -13,7 +13,7 @@ from kiwipy import BroadcastFilter, rmq import plumpy -from plumpy import communications, process_comms +from plumpy.rmq import communications, process_comms from .. import utils diff --git a/tests/rmq/test_process_comms.py b/tests/rmq/test_process_comms.py index 7a03fac4..454c0787 100644 --- a/tests/rmq/test_process_comms.py +++ b/tests/rmq/test_process_comms.py @@ -7,8 +7,8 @@ from kiwipy import rmq import plumpy -import plumpy.communications -from plumpy import process_comms +from plumpy.message import KILL_MSG, MESSAGE_KEY +from plumpy.rmq import process_comms from .. import utils diff --git a/tests/test_process_comms.py b/tests/test_message.py similarity index 90% rename from tests/test_process_comms.py rename to tests/test_message.py index 44947230..82951afd 100644 --- a/tests/test_process_comms.py +++ b/tests/test_message.py @@ -2,7 +2,7 @@ import pytest import plumpy -from plumpy import process_comms +from plumpy import message from tests import utils @@ -37,7 +37,7 @@ async def test_continue(): del process process = None - result = await launcher._continue(None, **plumpy.create_continue_body(pid)[process_comms.TASK_ARGS]) + result = await launcher._continue(None, **plumpy.create_continue_body(pid)[message.TASK_ARGS]) assert result == utils.DummyProcess.EXPECTED_OUTPUTS @@ -51,5 +51,5 @@ async def test_loader_is_used(): launcher = plumpy.ProcessLauncher(persister=persister, loader=loader) continue_task = plumpy.create_continue_body(proc.pid) - result = await launcher._continue(None, **continue_task[process_comms.TASK_ARGS]) + result = await launcher._continue(None, **continue_task[message.TASK_ARGS]) assert result == utils.DummyProcess.EXPECTED_OUTPUTS From ffbe2984a9a1e182b111bbc7603976efd0dd7252 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Sat, 14 Dec 2024 14:27:28 +0100 Subject: [PATCH 05/64] Move TaskRejectError as the common exception for task launch --- src/plumpy/exceptions.py | 3 +++ src/plumpy/message.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/src/plumpy/exceptions.py b/src/plumpy/exceptions.py index 6f0c75a4..1e6f3b26 100644 --- a/src/plumpy/exceptions.py +++ b/src/plumpy/exceptions.py @@ -38,3 +38,6 @@ class PersistenceError(Exception): class ClosedError(Exception): """Raised when an mutable operation is attempted on a closed process""" + +class TaskRejectedError(Exception): + """ A task was rejected by the coordinacor""" diff --git a/src/plumpy/message.py b/src/plumpy/message.py index 47586d21..b5e1348a 100644 --- a/src/plumpy/message.py +++ b/src/plumpy/message.py @@ -8,6 +8,8 @@ from plumpy.coordinator import Communicator from plumpy.exceptions import PersistenceError, TaskRejectedError +from plumpy.exceptions import PersistenceError, TaskRejectedError + from . import loaders, persistence from .utils import PID_TYPE From 7e67037a866967dab103a7fc8582485b9d2cb381 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Sat, 14 Dec 2024 14:39:54 +0100 Subject: [PATCH 06/64] Remove useless communicator param passed to ProcessLaunch __call__ --- tests/test_message.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_message.py b/tests/test_message.py index 82951afd..0a6ee96c 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -37,7 +37,7 @@ async def test_continue(): del process process = None - result = await launcher._continue(None, **plumpy.create_continue_body(pid)[message.TASK_ARGS]) + result = await launcher._continue(**plumpy.create_continue_body(pid)[message.TASK_ARGS]) assert result == utils.DummyProcess.EXPECTED_OUTPUTS @@ -51,5 +51,5 @@ async def test_loader_is_used(): launcher = plumpy.ProcessLauncher(persister=persister, loader=loader) continue_task = plumpy.create_continue_body(proc.pid) - result = await launcher._continue(None, **continue_task[message.TASK_ARGS]) + result = await launcher._continue(**continue_task[message.TASK_ARGS]) assert result == utils.DummyProcess.EXPECTED_OUTPUTS From 1a18f9e209cbdfc8d0a7666f5f36d7e37c9dff52 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Sat, 14 Dec 2024 21:06:06 +0100 Subject: [PATCH 07/64] Forming Communicator protocol --- src/plumpy/coordinator.py | 21 ++++++++++++++++++++ src/plumpy/processes.py | 42 ++++++++++++++++++++++++--------------- 2 files changed, 47 insertions(+), 16 deletions(-) create mode 100644 src/plumpy/coordinator.py diff --git a/src/plumpy/coordinator.py b/src/plumpy/coordinator.py new file mode 100644 index 00000000..214fc18f --- /dev/null +++ b/src/plumpy/coordinator.py @@ -0,0 +1,21 @@ +from typing import Any, Callable, Protocol + +RpcSubscriber = Callable[['Communicator', Any], Any] +BroadcastSubscriber = Callable[['Communicator', Any, Any, Any, Any], Any] + +class Communicator(Protocol): + + def add_rpc_subscriber(self, subscriber: RpcSubscriber, identifier=None) -> Any: + ... + + def add_broadcast_subscriber(self, subscriber: BroadcastSubscriber, identifier=None) -> Any: + ... + + def remove_rpc_subscriber(self, identifier): + ... + + def remove_broadcast_subscriber(self, identifier): + ... + + def broadcast_send(self, body, sender=None, subject=None, correlation_id=None) -> bool: + ... diff --git a/src/plumpy/processes.py b/src/plumpy/processes.py index 5b7c951d..f12874ed 100644 --- a/src/plumpy/processes.py +++ b/src/plumpy/processes.py @@ -3,6 +3,7 @@ import abc import asyncio +import concurrent.futures import contextlib import copy import enum @@ -31,6 +32,8 @@ cast, ) +from plumpy.coordinator import Communicator + try: from aiocontextvars import ContextVar except ModuleNotFoundError: @@ -264,7 +267,7 @@ def __init__( pid: Optional[PID_TYPE] = None, logger: Optional[logging.Logger] = None, loop: Optional[asyncio.AbstractEventLoop] = None, - communicator: Optional[kiwipy.Communicator] = None, + communicator: Optional[Communicator] = None, ) -> None: """ The signature of the constructor should not be changed by subclassing processes. @@ -317,19 +320,17 @@ def init(self) -> None: try: identifier = self._communicator.add_rpc_subscriber(self.message_receive, identifier=str(self.pid)) self.add_cleanup(functools.partial(self._communicator.remove_rpc_subscriber, identifier)) - except kiwipy.TimeoutError: + except concurrent.futures.TimeoutError: self.logger.exception('Process<%s>: failed to register as an RPC subscriber', self.pid) try: # filter out state change broadcasts + # TODO: pattern filter should be moved to add_broadcast_subscriber. subscriber = kiwipy.BroadcastFilter(self.broadcast_receive, subject=re.compile(r'^(?!state_changed).*')) identifier = self._communicator.add_broadcast_subscriber(subscriber, identifier=str(self.pid)) self.add_cleanup(functools.partial(self._communicator.remove_broadcast_subscriber, identifier)) - except kiwipy.TimeoutError: - self.logger.exception( - 'Process<%s>: failed to register as a broadcast subscriber', - self.pid, - ) + except concurrent.futures.TimeoutError: + self.logger.exception('Process<%s>: failed to register as a broadcast subscriber', self.pid) if not self._future.done(): @@ -726,8 +727,6 @@ def on_entering(self, state: process_states.State) -> None: call_with_super_check(self.on_except, state.get_exc_info()) # type: ignore def on_entered(self, from_state: Optional[process_states.State]) -> None: - from plumpy.rmq.exceptions import CommunicatorChannelInvalidStateError, CommunicatorConnectionClosed - # Map these onto direct functions that the subclass can implement state_label = self._state.LABEL if state_label == process_states.ProcessState.RUNNING: @@ -742,6 +741,8 @@ def on_entered(self, from_state: Optional[process_states.State]) -> None: call_with_super_check(self.on_killed) if self._communicator and isinstance(self.state, enum.Enum): + from plumpy.rmq.exceptions import CommunicatorChannelInvalidStateError, CommunicatorConnectionClosed + from_label = cast(enum.Enum, from_state.LABEL).value if from_state is not None else None subject = f'state_changed.{from_label}.{self.state.value}' self.logger.info('Process<%s>: Broadcasting state change: %s', self.pid, subject) @@ -750,7 +751,7 @@ def on_entered(self, from_state: Optional[process_states.State]) -> None: except (CommunicatorConnectionClosed, CommunicatorChannelInvalidStateError): message = 'Process<%s>: no connection available to broadcast state change from %s to %s' self.logger.warning(message, self.pid, from_label, self.state.value) - except kiwipy.TimeoutError: + except concurrent.futures.TimeoutError: message = 'Process<%s>: sending broadcast of state change from %s to %s timed out' self.logger.warning(message, self.pid, from_label, self.state.value) @@ -936,7 +937,7 @@ def _fire_event(self, evt: Callable[..., Any], *args: Any, **kwargs: Any) -> Non # region Communication - def message_receive(self, _comm: kiwipy.Communicator, msg: MessageType) -> Any: + def message_receive(self, _comm: Communicator, msg: MessageType) -> Any: """ Coroutine called when the process receives a message from the communicator @@ -984,15 +985,24 @@ def broadcast_receive( _comm, msg, ) - # If we get a message we recognise then action it, otherwise ignore + fn = None if subject == message.Intent.PLAY: - return self._schedule_rpc(self.play) - if subject == process_comms.Intent.PAUSE: + fn = self._schedule_rpc(self.play) + elif subject == message.Intent.PAUSE: return self._schedule_rpc(self.pause, msg_text=msg.get(process_comms.MESSAGE_TEXT_KEY, None)) - if subject == process_comms.Intent.KILL: + elif subject == message.Intent.KILL: return self._schedule_rpc(self.kill, msg_text=msg.get(process_comms.MESSAGE_TEXT_KEY, None)) - return None + + if fn is None: + self.logger.warning( + "Process<%s>: received unsupported broadcast message '%s'.", + self.pid, + subject, + ) + return None + + return fn def _schedule_rpc(self, callback: Callable[..., Any], *args: Any, **kwargs: Any) -> concurrent.futures.Future: """ From b96d52dbf8c8541124109719a685828b545309ea Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Sun, 15 Dec 2024 00:07:55 +0100 Subject: [PATCH 08/64] Remove kiwipy/rmq dependencies of process module --- src/plumpy/coordinator.py | 22 ++-- src/plumpy/exceptions.py | 3 +- src/plumpy/futures.py | 2 +- src/plumpy/message.py | 40 ++++++++ src/plumpy/process_states.py | 2 +- src/plumpy/processes.py | 12 +-- src/plumpy/rmq/communications.py | 4 +- src/plumpy/rmq/process_comms.py | 168 +------------------------------ tests/rmq/test_communications.py | 2 +- tests/rmq/test_communicator.py | 5 +- tests/test_processes.py | 3 +- tests/utils.py | 2 +- 12 files changed, 73 insertions(+), 192 deletions(-) diff --git a/src/plumpy/coordinator.py b/src/plumpy/coordinator.py index 214fc18f..1daaf1f8 100644 --- a/src/plumpy/coordinator.py +++ b/src/plumpy/coordinator.py @@ -1,21 +1,19 @@ -from typing import Any, Callable, Protocol +# -*- coding: utf-8 -*- +from typing import Any, Callable, Pattern, Protocol RpcSubscriber = Callable[['Communicator', Any], Any] BroadcastSubscriber = Callable[['Communicator', Any, Any, Any, Any], Any] -class Communicator(Protocol): - def add_rpc_subscriber(self, subscriber: RpcSubscriber, identifier=None) -> Any: - ... +class Communicator(Protocol): + def add_rpc_subscriber(self, subscriber: RpcSubscriber, identifier=None) -> Any: ... - def add_broadcast_subscriber(self, subscriber: BroadcastSubscriber, identifier=None) -> Any: - ... + def add_broadcast_subscriber( + self, subscriber: BroadcastSubscriber, subject_filter: str | Pattern[str] | None = None, identifier=None + ) -> Any: ... - def remove_rpc_subscriber(self, identifier): - ... + def remove_rpc_subscriber(self, identifier): ... - def remove_broadcast_subscriber(self, identifier): - ... + def remove_broadcast_subscriber(self, identifier): ... - def broadcast_send(self, body, sender=None, subject=None, correlation_id=None) -> bool: - ... + def broadcast_send(self, body, sender=None, subject=None, correlation_id=None) -> bool: ... diff --git a/src/plumpy/exceptions.py b/src/plumpy/exceptions.py index 1e6f3b26..9dca8fdb 100644 --- a/src/plumpy/exceptions.py +++ b/src/plumpy/exceptions.py @@ -39,5 +39,6 @@ class PersistenceError(Exception): class ClosedError(Exception): """Raised when an mutable operation is attempted on a closed process""" + class TaskRejectedError(Exception): - """ A task was rejected by the coordinacor""" + """A task was rejected by the coordinacor""" diff --git a/src/plumpy/futures.py b/src/plumpy/futures.py index 2f861d64..01be3951 100644 --- a/src/plumpy/futures.py +++ b/src/plumpy/futures.py @@ -7,7 +7,7 @@ import contextlib from typing import Any, Awaitable, Callable, Generator, Optional -__all__ = ['CancellableAction', 'create_task', 'create_task', 'capture_exceptions'] +__all__ = ['CancellableAction', 'capture_exceptions', 'create_task', 'create_task'] class InvalidFutureError(Exception): diff --git a/src/plumpy/message.py b/src/plumpy/message.py index b5e1348a..c63748f3 100644 --- a/src/plumpy/message.py +++ b/src/plumpy/message.py @@ -28,6 +28,7 @@ INTENT_KEY = 'intent' MESSAGE_KEY = 'message' +FORCE_KILL_KEY = 'force_kill' class Intent: @@ -62,6 +63,45 @@ class Intent: LOGGER = logging.getLogger(__name__) +MessageType = Dict[str, Any] + + +class MessageBuilder: + """MessageBuilder will construct different messages that can passing over communicator.""" + + @classmethod + def play(cls, text: str | None = None) -> MessageType: + """The play message send over communicator.""" + return { + INTENT_KEY: Intent.PLAY, + MESSAGE_KEY: text, + } + + @classmethod + def pause(cls, text: str | None = None) -> MessageType: + """The pause message send over communicator.""" + return { + INTENT_KEY: Intent.PAUSE, + MESSAGE_KEY: text, + } + + @classmethod + def kill(cls, text: str | None = None, force_kill: bool = False) -> MessageType: + """The kill message send over communicator.""" + return { + INTENT_KEY: Intent.KILL, + MESSAGE_KEY: text, + FORCE_KILL_KEY: force_kill, + } + + @classmethod + def status(cls, text: str | None = None) -> MessageType: + """The status message send over communicator.""" + return { + INTENT_KEY: Intent.STATUS, + MESSAGE_KEY: text, + } + def create_launch_body( process_class: str, diff --git a/src/plumpy/process_states.py b/src/plumpy/process_states.py index 931dbc5e..723292bf 100644 --- a/src/plumpy/process_states.py +++ b/src/plumpy/process_states.py @@ -10,7 +10,7 @@ import yaml from yaml.loader import Loader -from plumpy.process_comms import MessageBuilder, MessageType +from plumpy.message import MessageBuilder, MessageType try: import tblib diff --git a/src/plumpy/processes.py b/src/plumpy/processes.py index f12874ed..ef1f1f58 100644 --- a/src/plumpy/processes.py +++ b/src/plumpy/processes.py @@ -39,16 +39,15 @@ except ModuleNotFoundError: from contextvars import ContextVar -import kiwipy import yaml from . import events, exceptions, message, persistence, ports, process_states, utils -from .futures import capture_exceptions, CancellableAction from .base import state_machine from .base.state_machine import StateEntryFailed, StateMachine, TransitionFailed, event from .base.utils import call_with_super_check, super_check from .event_helper import EventHelper -from .process_comms import MESSAGE_TEXT_KEY, MessageBuilder, MessageType +from .futures import CancellableAction, capture_exceptions +from .message import MESSAGE_TEXT_KEY, MessageBuilder, MessageType from .process_listener import ProcessListener from .process_spec import ProcessSpec from .utils import PID_TYPE, SAVED_STATE_TYPE, protected @@ -325,9 +324,9 @@ def init(self) -> None: try: # filter out state change broadcasts - # TODO: pattern filter should be moved to add_broadcast_subscriber. - subscriber = kiwipy.BroadcastFilter(self.broadcast_receive, subject=re.compile(r'^(?!state_changed).*')) - identifier = self._communicator.add_broadcast_subscriber(subscriber, identifier=str(self.pid)) + identifier = self._communicator.add_broadcast_subscriber( + self.broadcast_receive, subject_filter=re.compile(r'^(?!state_changed).*'), identifier=str(self.pid) + ) self.add_cleanup(functools.partial(self._communicator.remove_broadcast_subscriber, identifier)) except concurrent.futures.TimeoutError: self.logger.exception('Process<%s>: failed to register as a broadcast subscriber', self.pid) @@ -741,6 +740,7 @@ def on_entered(self, from_state: Optional[process_states.State]) -> None: call_with_super_check(self.on_killed) if self._communicator and isinstance(self.state, enum.Enum): + # FIXME: move all to `coordinator.broadcast()` call and in rmq implement coordinator from plumpy.rmq.exceptions import CommunicatorChannelInvalidStateError, CommunicatorConnectionClosed from_label = cast(enum.Enum, from_state.LABEL).value if from_state is not None else None diff --git a/src/plumpy/rmq/communications.py b/src/plumpy/rmq/communications.py index b66e9694..5e526b23 100644 --- a/src/plumpy/rmq/communications.py +++ b/src/plumpy/rmq/communications.py @@ -130,10 +130,10 @@ def remove_task_subscriber(self, identifier: 'ID_TYPE') -> None: return self._communicator.remove_task_subscriber(identifier) def add_broadcast_subscriber( - self, subscriber: 'BroadcastSubscriber', identifier: Optional['ID_TYPE'] = None + self, subscriber: 'BroadcastSubscriber', subject_filter=None, identifier: Optional['ID_TYPE'] = None ) -> 'ID_TYPE': converted = convert_to_comm(subscriber, self._loop) - return self._communicator.add_broadcast_subscriber(converted, identifier) + return self._communicator.add_broadcast_subscriber(converted, subject_filter, identifier) def remove_broadcast_subscriber(self, identifier: 'ID_TYPE') -> None: return self._communicator.remove_broadcast_subscriber(identifier) diff --git a/src/plumpy/rmq/process_comms.py b/src/plumpy/rmq/process_comms.py index 010fd67d..f7903d6e 100644 --- a/src/plumpy/rmq/process_comms.py +++ b/src/plumpy/rmq/process_comms.py @@ -4,29 +4,22 @@ from __future__ import annotations import asyncio -import logging -from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence, Union, cast +from typing import Any, Dict, Optional, Sequence, Union import kiwipy +from plumpy import loaders from plumpy.message import ( - MESSAGE_KEY, - PAUSE_MSG, - PLAY_MSG, - STATUS_MSG, - KILL_MSG, Intent, + MessageBuilder, + MessageType, create_continue_body, create_create_body, create_launch_body, ) - -from plumpy import loaders from plumpy.utils import PID_TYPE __all__ = [ - 'MessageBuilder', - 'ProcessLauncher', 'RemoteProcessController', 'RemoteProcessThreadController', ] @@ -34,159 +27,6 @@ ProcessResult = Any ProcessStatus = Any -INTENT_KEY = 'intent' -MESSAGE_TEXT_KEY = 'message' -FORCE_KILL_KEY = 'force_kill' - - -class Intent: - """Intent constants for a process message""" - - PLAY: str = 'play' - PAUSE: str = 'pause' - KILL: str = 'kill' - STATUS: str = 'status' - - -MessageType = Dict[str, Any] - - -class MessageBuilder: - """MessageBuilder will construct different messages that can passing over communicator.""" - - @classmethod - def play(cls, text: str | None = None) -> MessageType: - """The play message send over communicator.""" - return { - INTENT_KEY: Intent.PLAY, - MESSAGE_TEXT_KEY: text, - } - - @classmethod - def pause(cls, text: str | None = None) -> MessageType: - """The pause message send over communicator.""" - return { - INTENT_KEY: Intent.PAUSE, - MESSAGE_TEXT_KEY: text, - } - - @classmethod - def kill(cls, text: str | None = None, force_kill: bool = False) -> MessageType: - """The kill message send over communicator.""" - return { - INTENT_KEY: Intent.KILL, - MESSAGE_TEXT_KEY: text, - FORCE_KILL_KEY: force_kill, - } - - @classmethod - def status(cls, text: str | None = None) -> MessageType: - """The status message send over communicator.""" - return { - INTENT_KEY: Intent.STATUS, - MESSAGE_TEXT_KEY: text, - } - - -TASK_KEY = 'task' -TASK_ARGS = 'args' -PERSIST_KEY = 'persist' -# Launch -PROCESS_CLASS_KEY = 'process_class' -ARGS_KEY = 'init_args' -KWARGS_KEY = 'init_kwargs' -NOWAIT_KEY = 'nowait' -# Continue -PID_KEY = 'pid' -TAG_KEY = 'tag' -# Task types -LAUNCH_TASK = 'launch' -CONTINUE_TASK = 'continue' -CREATE_TASK = 'create' - -LOGGER = logging.getLogger(__name__) - - -def create_launch_body( - process_class: str, - init_args: Optional[Sequence[Any]] = None, - init_kwargs: Optional[Dict[str, Any]] = None, - persist: bool = False, - loader: Optional[loaders.ObjectLoader] = None, - nowait: bool = True, -) -> Dict[str, Any]: - """ - Create a message body for the launch action - - :param process_class: the class of the process to launch - :param init_args: any initialisation positional arguments - :param init_kwargs: any initialisation keyword arguments - :param persist: persist this process if True, otherwise don't - :param loader: the loader to use to load the persisted process - :param nowait: wait for the process to finish before completing the task, otherwise just return the PID - :return: a dictionary with the body of the message to launch the process - :rtype: dict - """ - if loader is None: - loader = loaders.get_object_loader() - - msg_body = { - TASK_KEY: LAUNCH_TASK, - TASK_ARGS: { - PROCESS_CLASS_KEY: loader.identify_object(process_class), - PERSIST_KEY: persist, - NOWAIT_KEY: nowait, - ARGS_KEY: init_args, - KWARGS_KEY: init_kwargs, - }, - } - return msg_body - - -def create_continue_body(pid: 'PID_TYPE', tag: Optional[str] = None, nowait: bool = False) -> Dict[str, Any]: - """ - Create a message body to continue an existing process - :param pid: the pid of the existing process - :param tag: the optional persistence tag - :param nowait: wait for the process to finish before completing the task, otherwise just return the PID - :return: a dictionary with the body of the message to continue the process - - """ - msg_body = {TASK_KEY: CONTINUE_TASK, TASK_ARGS: {PID_KEY: pid, NOWAIT_KEY: nowait, TAG_KEY: tag}} - return msg_body - - -def create_create_body( - process_class: str, - init_args: Optional[Sequence[Any]] = None, - init_kwargs: Optional[Dict[str, Any]] = None, - persist: bool = False, - loader: Optional[loaders.ObjectLoader] = None, -) -> Dict[str, Any]: - """ - Create a message body to create a new process - :param process_class: the class of the process to launch - :param init_args: any initialisation positional arguments - :param init_kwargs: any initialisation keyword arguments - :param persist: persist this process if True, otherwise don't - :param loader: the loader to use to load the persisted process - :return: a dictionary with the body of the message to launch the process - - """ - if loader is None: - loader = loaders.get_object_loader() - - msg_body = { - TASK_KEY: CREATE_TASK, - TASK_ARGS: { - PROCESS_CLASS_KEY: loader.identify_object(process_class), - PERSIST_KEY: persist, - ARGS_KEY: init_args, - KWARGS_KEY: init_kwargs, - }, - } - return msg_body - class RemoteProcessController: """ diff --git a/tests/rmq/test_communications.py b/tests/rmq/test_communications.py index 63813bdc..00b7f1c6 100644 --- a/tests/rmq/test_communications.py +++ b/tests/rmq/test_communications.py @@ -56,7 +56,7 @@ def test_add_broadcast_subscriber(loop_communicator, subscriber): assert loop_communicator.add_broadcast_subscriber(subscriber) is not None identifier = 'identifier' - assert loop_communicator.add_broadcast_subscriber(subscriber, identifier) == identifier + assert loop_communicator.add_broadcast_subscriber(subscriber, identifier=identifier) == identifier def test_remove_broadcast_subscriber(loop_communicator, subscriber): diff --git a/tests/rmq/test_communicator.py b/tests/rmq/test_communicator.py index 26c9a852..80c1ac71 100644 --- a/tests/rmq/test_communicator.py +++ b/tests/rmq/test_communicator.py @@ -7,6 +7,7 @@ import tempfile import uuid +from kiwipy.rmq.communicator import kiwipy import pytest import shortuuid import yaml @@ -81,7 +82,7 @@ def get_broadcast(_comm, body, sender, subject, correlation_id): assert result == BROADCAST @pytest.mark.asyncio - async def test_broadcast_filter(self, loop_communicator): + async def test_broadcast_filter(self, loop_communicator: kiwipy.Communicator): broadcast_future = asyncio.Future() def ignore_broadcast(_comm, body, sender, subject, correlation_id): @@ -90,7 +91,7 @@ def ignore_broadcast(_comm, body, sender, subject, correlation_id): def get_broadcast(_comm, body, sender, subject, correlation_id): broadcast_future.set_result(True) - loop_communicator.add_broadcast_subscriber(BroadcastFilter(ignore_broadcast, subject='other')) + loop_communicator.add_broadcast_subscriber(ignore_broadcast, subject_filter='other') loop_communicator.add_broadcast_subscriber(get_broadcast) loop_communicator.broadcast_send( **{'body': 'present', 'sender': 'Martin', 'subject': 'sup', 'correlation_id': 420} diff --git a/tests/test_processes.py b/tests/test_processes.py index 3d6b4394..0b38287d 100644 --- a/tests/test_processes.py +++ b/tests/test_processes.py @@ -12,7 +12,7 @@ import plumpy from plumpy import BundleKeys, Process, ProcessState -from plumpy.process_comms import MESSAGE_TEXT_KEY, MessageBuilder +from plumpy.message import MessageBuilder from plumpy.utils import AttributesFrozendict from tests import utils @@ -1066,6 +1066,7 @@ def test_paused(self): self.assertSetEqual(events_tester.called, events_tester.expected_events) def test_broadcast(self): + # FIXME: here I need a mock test communicator = kiwipy.LocalCommunicator() messages = [] diff --git a/tests/utils.py b/tests/utils.py index 05290990..123d6e72 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -8,7 +8,7 @@ import plumpy from plumpy import persistence, process_states, processes, utils -from plumpy.process_comms import MessageBuilder +from plumpy.message import MessageBuilder Snapshot = collections.namedtuple('Snapshot', ['state', 'bundle', 'outputs']) From c02bba4b00b0e029eaed14b9c71878ff0badb7f3 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Tue, 17 Dec 2024 12:17:10 +0100 Subject: [PATCH 09/64] Interface change from communicator -> coordinator --- src/plumpy/coordinator.py | 13 +++++++++++++ src/plumpy/message.py | 4 ++-- src/plumpy/processes.py | 32 ++++++++++++++++---------------- src/plumpy/workchains.py | 6 +++--- tests/rmq/test_process_comms.py | 24 ++++++++++++------------ tests/test_processes.py | 2 +- 6 files changed, 47 insertions(+), 34 deletions(-) diff --git a/src/plumpy/coordinator.py b/src/plumpy/coordinator.py index 1daaf1f8..cd66a883 100644 --- a/src/plumpy/coordinator.py +++ b/src/plumpy/coordinator.py @@ -17,3 +17,16 @@ def remove_rpc_subscriber(self, identifier): ... def remove_broadcast_subscriber(self, identifier): ... def broadcast_send(self, body, sender=None, subject=None, correlation_id=None) -> bool: ... + +class Coordinator(Protocol): + def add_rpc_subscriber(self, subscriber: RpcSubscriber, identifier=None) -> Any: ... + + def add_broadcast_subscriber( + self, subscriber: BroadcastSubscriber, subject_filter: str | Pattern[str] | None = None, identifier=None + ) -> Any: ... + + def remove_rpc_subscriber(self, identifier): ... + + def remove_broadcast_subscriber(self, identifier): ... + + def broadcast_send(self, body, sender=None, subject=None, correlation_id=None) -> bool: ... diff --git a/src/plumpy/message.py b/src/plumpy/message.py index c63748f3..2d52b048 100644 --- a/src/plumpy/message.py +++ b/src/plumpy/message.py @@ -5,7 +5,7 @@ import logging from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence, Union, cast -from plumpy.coordinator import Communicator +from plumpy.coordinator import Coordinator from plumpy.exceptions import PersistenceError, TaskRejectedError from plumpy.exceptions import PersistenceError, TaskRejectedError @@ -226,7 +226,7 @@ def __init__( else: self._loader = loaders.get_object_loader() - async def __call__(self, communicator: Communicator, task: Dict[str, Any]) -> Union[PID_TYPE, Any]: + async def __call__(self, coordinator: Coordinator, task: Dict[str, Any]) -> Union[PID_TYPE, Any]: """ Receive a task. :param task: The task message diff --git a/src/plumpy/processes.py b/src/plumpy/processes.py index ef1f1f58..8753c20b 100644 --- a/src/plumpy/processes.py +++ b/src/plumpy/processes.py @@ -32,7 +32,7 @@ cast, ) -from plumpy.coordinator import Communicator +from plumpy.coordinator import Coordinator try: from aiocontextvars import ContextVar @@ -266,7 +266,7 @@ def __init__( pid: Optional[PID_TYPE] = None, logger: Optional[logging.Logger] = None, loop: Optional[asyncio.AbstractEventLoop] = None, - communicator: Optional[Communicator] = None, + coordinator: Optional[Coordinator] = None, ) -> None: """ The signature of the constructor should not be changed by subclassing processes. @@ -305,7 +305,7 @@ def __init__( self._future = persistence.SavableFuture(loop=self._loop) self._event_helper = EventHelper(ProcessListener) self._logger = logger - self._communicator = communicator + self._coordinator = coordinator @super_check def init(self) -> None: @@ -315,19 +315,19 @@ def init(self) -> None: """ self._cleanups = [] # a list of functions to be ran on terminated - if self._communicator is not None: + if self._coordinator is not None: try: - identifier = self._communicator.add_rpc_subscriber(self.message_receive, identifier=str(self.pid)) - self.add_cleanup(functools.partial(self._communicator.remove_rpc_subscriber, identifier)) + identifier = self._coordinator.add_rpc_subscriber(self.message_receive, identifier=str(self.pid)) + self.add_cleanup(functools.partial(self._coordinator.remove_rpc_subscriber, identifier)) except concurrent.futures.TimeoutError: self.logger.exception('Process<%s>: failed to register as an RPC subscriber', self.pid) try: # filter out state change broadcasts - identifier = self._communicator.add_broadcast_subscriber( + identifier = self._coordinator.add_broadcast_subscriber( self.broadcast_receive, subject_filter=re.compile(r'^(?!state_changed).*'), identifier=str(self.pid) ) - self.add_cleanup(functools.partial(self._communicator.remove_broadcast_subscriber, identifier)) + self.add_cleanup(functools.partial(self._coordinator.remove_broadcast_subscriber, identifier)) except concurrent.futures.TimeoutError: self.logger.exception('Process<%s>: failed to register as a broadcast subscriber', self.pid) @@ -448,7 +448,7 @@ def launch( pid=pid, logger=logger, loop=self.loop, - communicator=self._communicator, + coordinator=self._coordinator, ) self.loop.create_task(process.step_until_terminated()) return process @@ -644,7 +644,7 @@ def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persi self._future = persistence.SavableFuture() self._event_helper = EventHelper(ProcessListener) self._logger = None - self._communicator = None + self._coordinator = None if 'loop' in load_context: self._loop = load_context.loop @@ -653,8 +653,8 @@ def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persi self._state: process_states.State = self.recreate_state(saved_state['_state']) - if 'communicator' in load_context: - self._communicator = load_context.communicator + if 'coordinator' in load_context: + self._coordinator = load_context.coordinator if 'logger' in load_context: self._logger = load_context.logger @@ -739,7 +739,7 @@ def on_entered(self, from_state: Optional[process_states.State]) -> None: elif state_label == process_states.ProcessState.KILLED: call_with_super_check(self.on_killed) - if self._communicator and isinstance(self.state, enum.Enum): + if self._coordinator and isinstance(self.state, enum.Enum): # FIXME: move all to `coordinator.broadcast()` call and in rmq implement coordinator from plumpy.rmq.exceptions import CommunicatorChannelInvalidStateError, CommunicatorConnectionClosed @@ -747,7 +747,7 @@ def on_entered(self, from_state: Optional[process_states.State]) -> None: subject = f'state_changed.{from_label}.{self.state.value}' self.logger.info('Process<%s>: Broadcasting state change: %s', self.pid, subject) try: - self._communicator.broadcast_send(body=None, sender=self.pid, subject=subject) + self._coordinator.broadcast_send(body=None, sender=self.pid, subject=subject) except (CommunicatorConnectionClosed, CommunicatorChannelInvalidStateError): message = 'Process<%s>: no connection available to broadcast state change from %s to %s' self.logger.warning(message, self.pid, from_label, self.state.value) @@ -937,7 +937,7 @@ def _fire_event(self, evt: Callable[..., Any], *args: Any, **kwargs: Any) -> Non # region Communication - def message_receive(self, _comm: Communicator, msg: MessageType) -> Any: + def message_receive(self, _comm: Coordinator, msg: MessageType) -> Any: """ Coroutine called when the process receives a message from the communicator @@ -969,7 +969,7 @@ def message_receive(self, _comm: Communicator, msg: MessageType) -> Any: raise RuntimeError('Unknown intent') def broadcast_receive( - self, _comm: kiwipy.Communicator, msg: MessageType, sender: Any, subject: Any, correlation_id: Any + self, _comm: Coordinator, msg: MessageType, sender: Any, subject: Any, correlation_id: Any ) -> Optional[concurrent.futures.Future]: """ Coroutine called when the process receives a message from the communicator diff --git a/src/plumpy/workchains.py b/src/plumpy/workchains.py index 7e67253f..5df20bf4 100644 --- a/src/plumpy/workchains.py +++ b/src/plumpy/workchains.py @@ -23,7 +23,7 @@ cast, ) -from plumpy.coordinator import Communicator +from plumpy.coordinator import Coordinator from . import lang, mixins, persistence, process_states, processes from .utils import PID_TYPE, SAVED_STATE_TYPE @@ -128,9 +128,9 @@ def __init__( pid: Optional[PID_TYPE] = None, logger: Optional[logging.Logger] = None, loop: Optional[asyncio.AbstractEventLoop] = None, - communicator: Optional[Communicator] = None, + coordinator: Optional[Coordinator] = None, ) -> None: - super().__init__(inputs=inputs, pid=pid, logger=logger, loop=loop, communicator=communicator) + super().__init__(inputs=inputs, pid=pid, logger=logger, loop=loop, coordinator=coordinator) self._stepper: Optional[Stepper] = None self._awaitables: Dict[Union[asyncio.Future, processes.Process], str] = {} diff --git a/tests/rmq/test_process_comms.py b/tests/rmq/test_process_comms.py index 454c0787..4d9bca29 100644 --- a/tests/rmq/test_process_comms.py +++ b/tests/rmq/test_process_comms.py @@ -45,7 +45,7 @@ def sync_controller(thread_communicator: rmq.RmqThreadCommunicator): class TestRemoteProcessController: @pytest.mark.asyncio async def test_pause(self, thread_communicator, async_controller): - proc = utils.WaitForSignalProcess(communicator=thread_communicator) + proc = utils.WaitForSignalProcess(coordinator=thread_communicator) # Run the process in the background asyncio.ensure_future(proc.step_until_terminated()) # Send a pause message @@ -57,7 +57,7 @@ async def test_pause(self, thread_communicator, async_controller): @pytest.mark.asyncio async def test_play(self, thread_communicator, async_controller): - proc = utils.WaitForSignalProcess(communicator=thread_communicator) + proc = utils.WaitForSignalProcess(coordinator=thread_communicator) # Run the process in the background asyncio.ensure_future(proc.step_until_terminated()) assert proc.pause() @@ -75,7 +75,7 @@ async def test_play(self, thread_communicator, async_controller): @pytest.mark.asyncio async def test_kill(self, thread_communicator, async_controller): - proc = utils.WaitForSignalProcess(communicator=thread_communicator) + proc = utils.WaitForSignalProcess(coordinator=thread_communicator) # Run the process in the event loop asyncio.ensure_future(proc.step_until_terminated()) @@ -88,7 +88,7 @@ async def test_kill(self, thread_communicator, async_controller): @pytest.mark.asyncio async def test_status(self, thread_communicator, async_controller): - proc = utils.WaitForSignalProcess(communicator=thread_communicator) + proc = utils.WaitForSignalProcess(coordinator=thread_communicator) # Run the process in the background asyncio.ensure_future(proc.step_until_terminated()) @@ -108,7 +108,7 @@ def on_broadcast_receive(**msg): thread_communicator.add_broadcast_subscriber(on_broadcast_receive) - proc = utils.DummyProcess(communicator=thread_communicator) + proc = utils.DummyProcess(coordinator=thread_communicator) proc.execute() expected_subjects = [] @@ -123,7 +123,7 @@ def on_broadcast_receive(**msg): class TestRemoteProcessThreadController: @pytest.mark.asyncio async def test_pause(self, thread_communicator, sync_controller): - proc = utils.WaitForSignalProcess(communicator=thread_communicator) + proc = utils.WaitForSignalProcess(coordinator=thread_communicator) # Send a pause message pause_future = sync_controller.pause_process(proc.pid) @@ -140,7 +140,7 @@ async def test_pause_all(self, thread_communicator, sync_controller): """Test pausing all processes on a communicator""" procs = [] for _ in range(10): - procs.append(utils.WaitForSignalProcess(communicator=thread_communicator)) + procs.append(utils.WaitForSignalProcess(coordinator=thread_communicator)) sync_controller.pause_all("Slow yo' roll") # Wait until they are all paused @@ -151,7 +151,7 @@ async def test_play_all(self, thread_communicator, sync_controller): """Test pausing all processes on a communicator""" procs = [] for _ in range(10): - proc = utils.WaitForSignalProcess(communicator=thread_communicator) + proc = utils.WaitForSignalProcess(coordinator=thread_communicator) procs.append(proc) proc.pause('hold tight') @@ -162,7 +162,7 @@ async def test_play_all(self, thread_communicator, sync_controller): @pytest.mark.asyncio async def test_play(self, thread_communicator, sync_controller): - proc = utils.WaitForSignalProcess(communicator=thread_communicator) + proc = utils.WaitForSignalProcess(coordinator=thread_communicator) assert proc.pause() # Send a play message @@ -176,7 +176,7 @@ async def test_play(self, thread_communicator, sync_controller): @pytest.mark.asyncio async def test_kill(self, thread_communicator, sync_controller): - proc = utils.WaitForSignalProcess(communicator=thread_communicator) + proc = utils.WaitForSignalProcess(coordinator=thread_communicator) # Send a kill message kill_future = sync_controller.kill_process(proc.pid) @@ -193,7 +193,7 @@ async def test_kill_all(self, thread_communicator, sync_controller): """Test pausing all processes on a communicator""" procs = [] for _ in range(10): - procs.append(utils.WaitForSignalProcess(communicator=thread_communicator)) + procs.append(utils.WaitForSignalProcess(coordinator=thread_communicator)) sync_controller.kill_all(msg_text='bang bang, I shot you down') await utils.wait_util(lambda: all([proc.killed() for proc in procs])) @@ -201,7 +201,7 @@ async def test_kill_all(self, thread_communicator, sync_controller): @pytest.mark.asyncio async def test_status(self, thread_communicator, sync_controller): - proc = utils.WaitForSignalProcess(communicator=thread_communicator) + proc = utils.WaitForSignalProcess(coordinator=thread_communicator) # Run the process in the background asyncio.ensure_future(proc.step_until_terminated()) diff --git a/tests/test_processes.py b/tests/test_processes.py index 0b38287d..99e28de6 100644 --- a/tests/test_processes.py +++ b/tests/test_processes.py @@ -1075,7 +1075,7 @@ def on_broadcast_receive(_comm, body, sender, subject, correlation_id): messages.append({'body': body, 'subject': subject, 'sender': sender, 'correlation_id': correlation_id}) communicator.add_broadcast_subscriber(on_broadcast_receive) - proc = utils.DummyProcess(communicator=communicator) + proc = utils.DummyProcess(coordinator=communicator) proc.execute() expected_subjects = [] From 3fddff08fd8075b487f64b54f5a7710ece99b589 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Tue, 17 Dec 2024 12:30:48 +0100 Subject: [PATCH 10/64] Remove unnecessary task_send ab from RemoteProcessControl interface --- src/plumpy/rmq/process_comms.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/plumpy/rmq/process_comms.py b/src/plumpy/rmq/process_comms.py index f7903d6e..38355b03 100644 --- a/src/plumpy/rmq/process_comms.py +++ b/src/plumpy/rmq/process_comms.py @@ -273,7 +273,7 @@ def continue_process( self, pid: 'PID_TYPE', tag: Optional[str] = None, nowait: bool = False, no_reply: bool = False ) -> Union[None, PID_TYPE, ProcessResult]: message = create_continue_body(pid=pid, tag=tag, nowait=nowait) - return self.task_send(message, no_reply=no_reply) + return self._communicator.task_send(message, no_reply=no_reply) def launch_process( self, @@ -298,7 +298,7 @@ def launch_process( :return: the pid of the created process or the outputs (if nowait=False) """ message = create_launch_body(process_class, init_args, init_kwargs, persist, loader, nowait) - return self.task_send(message, no_reply=no_reply) + return self._communicator.task_send(message, no_reply=no_reply) def execute_process( self, @@ -335,13 +335,3 @@ def on_created(_: Any) -> None: create_future.add_done_callback(on_created) return execute_future - - def task_send(self, message: Any, no_reply: bool = False) -> Optional[Any]: - """ - Send a task to be performed using the communicator - - :param message: the task message - :param no_reply: if True, this call will be fire-and-forget, i.e. no return value - :return: the response from the remote side (if no_reply=False) - """ - return self._communicator.task_send(message, no_reply=no_reply) From 6aecacc87e6fe3ab55e0f693f5e323ae1de54116 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Tue, 17 Dec 2024 13:09:52 +0100 Subject: [PATCH 11/64] Interface for ProcessController --- src/plumpy/__init__.py | 6 +- src/plumpy/controller.py | 113 ++++++++++++++++++++++++++++++++ src/plumpy/coordinator.py | 44 ++++++++++--- src/plumpy/rmq/process_comms.py | 61 ++++++++--------- 4 files changed, 185 insertions(+), 39 deletions(-) create mode 100644 src/plumpy/controller.py diff --git a/src/plumpy/__init__.py b/src/plumpy/__init__.py index 8f62edb6..a63be7d1 100644 --- a/src/plumpy/__init__.py +++ b/src/plumpy/__init__.py @@ -19,6 +19,10 @@ from .workchains import * from .rmq import * +# interfaces +from .controller import ProcessController +from .coordinator import Coordinator + __all__ = ( events.__all__ + exceptions.__all__ @@ -33,7 +37,7 @@ + loaders.__all__ + ports.__all__ + process_states.__all__ -) +) + ['ProcessController', 'Coordinator'] # Do this se we don't get the "No handlers could be found..." warnings that will be produced diff --git a/src/plumpy/controller.py b/src/plumpy/controller.py new file mode 100644 index 00000000..5a411fd1 --- /dev/null +++ b/src/plumpy/controller.py @@ -0,0 +1,113 @@ +from collections.abc import Sequence +from typing import Any, Protocol + +from plumpy import loaders +from plumpy.message import MessageType +from plumpy.utils import PID_TYPE + +ProcessResult = Any +ProcessStatus = Any + + +class ProcessController(Protocol): + """ + Control processes using coroutines that will send messages and wait + (in a non-blocking way) for their response + """ + + def get_status(self, pid: 'PID_TYPE') -> ProcessStatus: + """ + Get the status of a process with the given PID + :param pid: the process id + :return: the status response from the process + """ + ... + + def pause_process(self, pid: 'PID_TYPE', msg: Any | None = None) -> ProcessResult: + """ + Pause the process + + :param pid: the pid of the process to pause + :param msg: optional pause message + :return: True if paused, False otherwise + """ + ... + + def play_process(self, pid: 'PID_TYPE') -> ProcessResult: + """ + Play the process + + :param pid: the pid of the process to play + :return: True if played, False otherwise + """ + ... + + def kill_process(self, pid: 'PID_TYPE', msg: MessageType | None = None) -> ProcessResult: + """ + Kill the process + + :param pid: the pid of the process to kill + :param msg: optional kill message + :return: True if killed, False otherwise + """ + ... + + def continue_process( + self, pid: 'PID_TYPE', tag: str|None = None, nowait: bool = False, no_reply: bool = False + ) -> ProcessResult | None: + """ + Continue the process + + :param _communicator: the communicator + :param pid: the pid of the process to continue + :param tag: the checkpoint tag to continue from + """ + ... + + async def launch_process( + self, + process_class: str, + init_args: Sequence[Any] | None = None, + init_kwargs: dict[str, Any] | None = None, + persist: bool = False, + loader: loaders.ObjectLoader | None = None, + nowait: bool = False, + no_reply: bool = False, + ) -> ProcessResult: + """ + Launch a process given the class and constructor arguments + + :param process_class: the class of the process to launch + :param init_args: the constructor positional arguments + :param init_kwargs: the constructor keyword arguments + :param persist: should the process be persisted + :param loader: the classloader to use + :param nowait: if True, don't wait for the process to send a response, just return the pid + :param no_reply: if True, this call will be fire-and-forget, i.e. no return value + :return: the result of launching the process + """ + ... + + async def execute_process( + self, + process_class: str, + init_args: Sequence[Any] | None = None, + init_kwargs: dict[str, Any] | None = None, + loader: loaders.ObjectLoader | None = None, + nowait: bool = False, + no_reply: bool = False, + ) -> ProcessResult: + """ + Execute a process. This call will first send a create task and then a continue task over + the communicator. This means that if communicator messages are durable then the process + will run until the end even if this interpreter instance ceases to exist. + + :param process_class: the process class to execute + :param init_args: the positional arguments to the class constructor + :param init_kwargs: the keyword arguments to the class constructor + :param loader: the class loader to use + :param nowait: if True, don't wait for the process to send a response + :param no_reply: if True, this call will be fire-and-forget, i.e. no return value + :return: the result of executing the process + """ + ... diff --git a/src/plumpy/coordinator.py b/src/plumpy/coordinator.py index cd66a883..c229a6c4 100644 --- a/src/plumpy/coordinator.py +++ b/src/plumpy/coordinator.py @@ -1,15 +1,25 @@ # -*- coding: utf-8 -*- -from typing import Any, Callable, Pattern, Protocol +import concurrent.futures +from typing import TYPE_CHECKING, Any, Callable, Hashable, Pattern, Protocol -RpcSubscriber = Callable[['Communicator', Any], Any] -BroadcastSubscriber = Callable[['Communicator', Any, Any, Any, Any], Any] + +if TYPE_CHECKING: + # identifiers for subscribers + ID_TYPE = Hashable + Subscriber = Callable[..., Any] + # RPC subscriber params: communicator, msg + RpcSubscriber = Callable[['Coordinator', Any], Any] + # Task subscriber params: communicator, task + TaskSubscriber = Callable[['Coordinator', Any], Any] + # Broadcast subscribers params: communicator, body, sender, subject, correlation id + BroadcastSubscriber = Callable[['Coordinator', Any, Any, Any, ID_TYPE], Any] class Communicator(Protocol): - def add_rpc_subscriber(self, subscriber: RpcSubscriber, identifier=None) -> Any: ... + def add_rpc_subscriber(self, subscriber: 'RpcSubscriber', identifier: 'ID_TYPE | None' = None) -> Any: ... def add_broadcast_subscriber( - self, subscriber: BroadcastSubscriber, subject_filter: str | Pattern[str] | None = None, identifier=None + self, subscriber: 'BroadcastSubscriber', subject_filter: str | Pattern[str] | None = None, identifier=None ) -> Any: ... def remove_rpc_subscriber(self, identifier): ... @@ -18,15 +28,33 @@ def remove_broadcast_subscriber(self, identifier): ... def broadcast_send(self, body, sender=None, subject=None, correlation_id=None) -> bool: ... + class Coordinator(Protocol): - def add_rpc_subscriber(self, subscriber: RpcSubscriber, identifier=None) -> Any: ... + def add_rpc_subscriber(self, subscriber: 'RpcSubscriber', identifier=None) -> Any: ... def add_broadcast_subscriber( - self, subscriber: BroadcastSubscriber, subject_filter: str | Pattern[str] | None = None, identifier=None + self, + subscriber: 'BroadcastSubscriber', + subject_filter: str | Pattern[str] | None = None, + identifier: 'ID_TYPE | None' = None, ) -> Any: ... + def add_task_subscriber(self, subscriber: 'TaskSubscriber', identifier: 'ID_TYPE | None' = None) -> 'ID_TYPE': ... + def remove_rpc_subscriber(self, identifier): ... def remove_broadcast_subscriber(self, identifier): ... - def broadcast_send(self, body, sender=None, subject=None, correlation_id=None) -> bool: ... + def remove_task_subscriber(self, identifier: 'ID_TYPE') -> None: ... + + def rpc_send(self, recipient_id: Hashable, msg: Any) -> Any: ... + + def broadcast_send( + self, + body: Any | None, + sender: str | None = None, + subject: str | None = None, + correlation_id: 'ID_TYPE | None' = None, + ) -> Any: ... + + def task_send(self, task: Any, no_reply: bool = False) -> Any: ... diff --git a/src/plumpy/rmq/process_comms.py b/src/plumpy/rmq/process_comms.py index 38355b03..91484332 100644 --- a/src/plumpy/rmq/process_comms.py +++ b/src/plumpy/rmq/process_comms.py @@ -9,6 +9,7 @@ import kiwipy from plumpy import loaders +from plumpy.coordinator import Coordinator from plumpy.message import ( Intent, MessageBuilder, @@ -34,8 +35,8 @@ class RemoteProcessController: (in a non-blocking way) for their response """ - def __init__(self, communicator: kiwipy.Communicator) -> None: - self._communicator = communicator + def __init__(self, coordinator: Coordinator) -> None: + self._coordinator = coordinator async def get_status(self, pid: 'PID_TYPE') -> 'ProcessStatus': """ @@ -43,7 +44,7 @@ async def get_status(self, pid: 'PID_TYPE') -> 'ProcessStatus': :param pid: the process id :return: the status response from the process """ - future = self._communicator.rpc_send(pid, MessageBuilder.status()) + future = self._coordinator.rpc_send(pid, MessageBuilder.status()) result = await asyncio.wrap_future(future) return result @@ -57,8 +58,8 @@ async def pause_process(self, pid: 'PID_TYPE', msg_text: Optional[str] = None) - """ msg = MessageBuilder.pause(text=msg_text) - pause_future = self._communicator.rpc_send(pid, msg) - # rpc_send return a thread future from communicator + pause_future = self._coordinator.rpc_send(pid, msg) + # rpc_send return a thread future from coordinator future = await asyncio.wrap_future(pause_future) # future is just returned from rpc call which return a kiwipy future result = await asyncio.wrap_future(future) @@ -71,7 +72,7 @@ async def play_process(self, pid: 'PID_TYPE') -> 'ProcessResult': :param pid: the pid of the process to play :return: True if played, False otherwise """ - play_future = self._communicator.rpc_send(pid, MessageBuilder.play()) + play_future = self._coordinator.rpc_send(pid, MessageBuilder.play()) future = await asyncio.wrap_future(play_future) result = await asyncio.wrap_future(future) return result @@ -87,7 +88,7 @@ async def kill_process(self, pid: 'PID_TYPE', msg_text: Optional[str] = None) -> msg = MessageBuilder.kill(text=msg_text) # Wait for the communication to go through - kill_future = self._communicator.rpc_send(pid, msg) + kill_future = self._coordinator.rpc_send(pid, msg) future = await asyncio.wrap_future(kill_future) # Now wait for the kill to be enacted result = await asyncio.wrap_future(future) @@ -99,13 +100,13 @@ async def continue_process( """ Continue the process - :param _communicator: the communicator + :param _coordinator: the coordinator :param pid: the pid of the process to continue :param tag: the checkpoint tag to continue from """ message = create_continue_body(pid=pid, tag=tag, nowait=nowait) # Wait for the communication to go through - continue_future = self._communicator.task_send(message, no_reply=no_reply) + continue_future = self._coordinator.task_send(message, no_reply=no_reply) future = await asyncio.wrap_future(continue_future) if no_reply: @@ -139,7 +140,7 @@ async def launch_process( """ message = create_launch_body(process_class, init_args, init_kwargs, persist, loader, nowait) - launch_future = self._communicator.task_send(message, no_reply=no_reply) + launch_future = self._coordinator.task_send(message, no_reply=no_reply) future = await asyncio.wrap_future(launch_future) if no_reply: @@ -159,7 +160,7 @@ async def execute_process( ) -> 'ProcessResult': """ Execute a process. This call will first send a create task and then a continue task over - the communicator. This means that if communicator messages are durable then the process + the coordinator. This means that if coordinator messages are durable then the process will run until the end even if this interpreter instance ceases to exist. :param process_class: the process class to execute @@ -173,12 +174,12 @@ async def execute_process( message = create_create_body(process_class, init_args, init_kwargs, persist=True, loader=loader) - create_future = self._communicator.task_send(message) + create_future = self._coordinator.task_send(message) future = await asyncio.wrap_future(create_future) pid: 'PID_TYPE' = await asyncio.wrap_future(future) message = create_continue_body(pid, nowait=nowait) - continue_future = self._communicator.task_send(message, no_reply=no_reply) + continue_future = self._coordinator.task_send(message, no_reply=no_reply) future = await asyncio.wrap_future(continue_future) if no_reply: @@ -193,14 +194,14 @@ class RemoteProcessThreadController: A class that can be used to control and launch remote processes """ - def __init__(self, communicator: kiwipy.Communicator): + def __init__(self, coordinator: Coordinator): """ Create a new process controller - :param communicator: the communicator to use + :param coordinator: the coordinator to use """ - self._communicator = communicator + self._coordinator = coordinator def get_status(self, pid: 'PID_TYPE') -> kiwipy.Future: """Get the status of a process with the given PID. @@ -208,7 +209,7 @@ def get_status(self, pid: 'PID_TYPE') -> kiwipy.Future: :param pid: the process id :return: the status response from the process """ - return self._communicator.rpc_send(pid, MessageBuilder.status()) + return self._coordinator.rpc_send(pid, MessageBuilder.status()) def pause_process(self, pid: 'PID_TYPE', msg_text: Optional[str] = None) -> kiwipy.Future: """ @@ -221,16 +222,16 @@ def pause_process(self, pid: 'PID_TYPE', msg_text: Optional[str] = None) -> kiwi """ msg = MessageBuilder.pause(text=msg_text) - return self._communicator.rpc_send(pid, msg) + return self._coordinator.rpc_send(pid, msg) def pause_all(self, msg_text: Optional[str]) -> None: """ - Pause all processes that are subscribed to the same communicator + Pause all processes that are subscribed to the same coordinator :param msg: an optional pause message """ msg = MessageBuilder.pause(text=msg_text) - self._communicator.broadcast_send(msg, subject=Intent.PAUSE) + self._coordinator.broadcast_send(msg, subject=Intent.PAUSE) def play_process(self, pid: 'PID_TYPE') -> kiwipy.Future: """ @@ -240,13 +241,13 @@ def play_process(self, pid: 'PID_TYPE') -> kiwipy.Future: :return: a response future from the process to be played """ - return self._communicator.rpc_send(pid, MessageBuilder.play()) + return self._coordinator.rpc_send(pid, MessageBuilder.play()) def play_all(self) -> None: """ - Play all processes that are subscribed to the same communicator + Play all processes that are subscribed to the same coordinator """ - self._communicator.broadcast_send(None, subject=Intent.PLAY) + self._coordinator.broadcast_send(None, subject=Intent.PLAY) def kill_process(self, pid: 'PID_TYPE', msg_text: Optional[str] = None) -> kiwipy.Future: """ @@ -257,23 +258,23 @@ def kill_process(self, pid: 'PID_TYPE', msg_text: Optional[str] = None) -> kiwip :return: a response future from the process to be killed """ msg = MessageBuilder.kill(text=msg_text) - return self._communicator.rpc_send(pid, msg) + return self._coordinator.rpc_send(pid, msg) def kill_all(self, msg_text: Optional[str]) -> None: """ - Kill all processes that are subscribed to the same communicator + Kill all processes that are subscribed to the same coordinator :param msg: an optional pause message """ msg = MessageBuilder.kill(msg_text) - self._communicator.broadcast_send(msg, subject=Intent.KILL) + self._coordinator.broadcast_send(msg, subject=Intent.KILL) def continue_process( self, pid: 'PID_TYPE', tag: Optional[str] = None, nowait: bool = False, no_reply: bool = False ) -> Union[None, PID_TYPE, ProcessResult]: message = create_continue_body(pid=pid, tag=tag, nowait=nowait) - return self._communicator.task_send(message, no_reply=no_reply) + return self._coordinator.task_send(message, no_reply=no_reply) def launch_process( self, @@ -298,7 +299,7 @@ def launch_process( :return: the pid of the created process or the outputs (if nowait=False) """ message = create_launch_body(process_class, init_args, init_kwargs, persist, loader, nowait) - return self._communicator.task_send(message, no_reply=no_reply) + return self._coordinator.task_send(message, no_reply=no_reply) def execute_process( self, @@ -311,7 +312,7 @@ def execute_process( ) -> Union[None, PID_TYPE, ProcessResult]: """ Execute a process. This call will first send a create task and then a continue task over - the communicator. This means that if communicator messages are durable then the process + the coordinator. This means that if coordinator messages are durable then the process will run until the end even if this interpreter instance ceases to exist. :param process_class: the process class to execute @@ -325,7 +326,7 @@ def execute_process( message = create_create_body(process_class, init_args, init_kwargs, persist=True, loader=loader) execute_future = kiwipy.Future() - create_future = self._communicator.task_send(message) + create_future = self._coordinator.task_send(message) def on_created(_: Any) -> None: with kiwipy.capture_exceptions(execute_future): From 071938d3be39f6104c97b9048bc2c9f2b4951c11 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Tue, 17 Dec 2024 16:30:15 +0100 Subject: [PATCH 12/64] RmqCoordinator example to show how using interface can avoid making change in kiwipy --- docs/source/concepts.md | 2 +- docs/source/tutorial.ipynb | 2 +- src/plumpy/__init__.py | 7 +- src/plumpy/controller.py | 5 +- src/plumpy/coordinator.py | 26 +--- src/plumpy/exceptions.py | 14 ++ src/plumpy/futures.py | 4 +- src/plumpy/message.py | 2 + src/plumpy/processes.py | 18 ++- src/plumpy/rmq/__init__.py | 7 +- src/plumpy/rmq/communications.py | 6 +- src/plumpy/rmq/exceptions.py | 14 -- src/plumpy/rmq/futures.py | 2 + .../{process_comms.py => process_control.py} | 13 +- tests/base/test_statemachine.py | 2 +- tests/rmq/__init__.py | 65 +++++++++ tests/rmq/test_communications.py | 76 ++++++----- tests/rmq/test_communicator.py | 77 ++++++----- ...ocess_comms.py => test_process_control.py} | 73 +++++----- tests/test_processes.py | 14 +- tests/utils.py | 128 ++++++++++++++++++ 21 files changed, 379 insertions(+), 178 deletions(-) delete mode 100644 src/plumpy/rmq/exceptions.py rename src/plumpy/rmq/{process_comms.py => process_control.py} (96%) rename tests/rmq/{test_process_comms.py => test_process_control.py} (71%) diff --git a/docs/source/concepts.md b/docs/source/concepts.md index ba6e8b17..0c39d515 100644 --- a/docs/source/concepts.md +++ b/docs/source/concepts.md @@ -32,7 +32,7 @@ WorkChains support the use of logical constructs such as `If_` and `While_` to c A `Controller` can control processes throughout their lifetime, by sending and receiving messages. It can launch, pause, continue, kill and check status of the process. -The {py:class}`~plumpy.process_comms.RemoteProcessThreadController` can communicate with the process over the thread communicator provided by {{kiwipy}} which can subscribe and send messages over the {{rabbitmq}} message broker. +The {py:class}`~plumpy.rmq.process_control.RemoteProcessThreadController` can communicate with the process over the thread communicator provided by {{kiwipy}} which can subscribe and send messages over the {{rabbitmq}} message broker. The thread communicator runs on a independent thread (event loop) and so will not be blocked by sometimes long waiting times in the process event loop. Using RabbitMQ means that even if the computer is terminated unexpectedly, messages are persisted and can be run once the computer restarts. diff --git a/docs/source/tutorial.ipynb b/docs/source/tutorial.ipynb index b544d38b..ba0dd8ca 100644 --- a/docs/source/tutorial.ipynb +++ b/docs/source/tutorial.ipynb @@ -66,7 +66,7 @@ "The {py:class}`~plumpy.workchains.WorkChain`\n", ": A subclass of `Process` that allows for running a process as a set of discrete steps (also known as instructions), with the ability to save the state of the process after each instruction has completed.\n", "\n", - "The process `Controller` (principally the {py:class}`~plumpy.process_comms.RemoteProcessThreadController`)\n", + "The process `Controller` (principally the {py:class}`~plumpy.rmq.process_control.RemoteProcessThreadController`)\n", ": To control the process or workchain throughout its lifetime." ] }, diff --git a/src/plumpy/__init__.py b/src/plumpy/__init__.py index a63be7d1..cc65ba23 100644 --- a/src/plumpy/__init__.py +++ b/src/plumpy/__init__.py @@ -4,6 +4,9 @@ import logging +# interfaces +from .controller import ProcessController +from .coordinator import Coordinator from .events import * from .exceptions import * from .futures import * @@ -19,10 +22,6 @@ from .workchains import * from .rmq import * -# interfaces -from .controller import ProcessController -from .coordinator import Coordinator - __all__ = ( events.__all__ + exceptions.__all__ diff --git a/src/plumpy/controller.py b/src/plumpy/controller.py index 5a411fd1..dcf203dc 100644 --- a/src/plumpy/controller.py +++ b/src/plumpy/controller.py @@ -1,3 +1,6 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations + from collections.abc import Sequence from typing import Any, Protocol @@ -53,7 +56,7 @@ def kill_process(self, pid: 'PID_TYPE', msg: MessageType | None = None) -> Proce ... def continue_process( - self, pid: 'PID_TYPE', tag: str|None = None, nowait: bool = False, no_reply: bool = False + self, pid: 'PID_TYPE', tag: str | None = None, nowait: bool = False, no_reply: bool = False ) -> ProcessResult | None: """ Continue the process diff --git a/src/plumpy/coordinator.py b/src/plumpy/coordinator.py index c229a6c4..b3dcbec5 100644 --- a/src/plumpy/coordinator.py +++ b/src/plumpy/coordinator.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -import concurrent.futures -from typing import TYPE_CHECKING, Any, Callable, Hashable, Pattern, Protocol +from __future__ import annotations +from typing import TYPE_CHECKING, Any, Callable, Hashable, Pattern, Protocol if TYPE_CHECKING: # identifiers for subscribers @@ -15,22 +15,8 @@ BroadcastSubscriber = Callable[['Coordinator', Any, Any, Any, ID_TYPE], Any] -class Communicator(Protocol): - def add_rpc_subscriber(self, subscriber: 'RpcSubscriber', identifier: 'ID_TYPE | None' = None) -> Any: ... - - def add_broadcast_subscriber( - self, subscriber: 'BroadcastSubscriber', subject_filter: str | Pattern[str] | None = None, identifier=None - ) -> Any: ... - - def remove_rpc_subscriber(self, identifier): ... - - def remove_broadcast_subscriber(self, identifier): ... - - def broadcast_send(self, body, sender=None, subject=None, correlation_id=None) -> bool: ... - - class Coordinator(Protocol): - def add_rpc_subscriber(self, subscriber: 'RpcSubscriber', identifier=None) -> Any: ... + def add_rpc_subscriber(self, subscriber: 'RpcSubscriber', identifier: 'ID_TYPE | None' = None) -> Any: ... def add_broadcast_subscriber( self, @@ -41,9 +27,9 @@ def add_broadcast_subscriber( def add_task_subscriber(self, subscriber: 'TaskSubscriber', identifier: 'ID_TYPE | None' = None) -> 'ID_TYPE': ... - def remove_rpc_subscriber(self, identifier): ... + def remove_rpc_subscriber(self, identifier: 'ID_TYPE | None') -> None: ... - def remove_broadcast_subscriber(self, identifier): ... + def remove_broadcast_subscriber(self, identifier: 'ID_TYPE | None') -> None: ... def remove_task_subscriber(self, identifier: 'ID_TYPE') -> None: ... @@ -52,7 +38,7 @@ def rpc_send(self, recipient_id: Hashable, msg: Any) -> Any: ... def broadcast_send( self, body: Any | None, - sender: str | None = None, + sender: 'ID_TYPE | None' = None, subject: str | None = None, correlation_id: 'ID_TYPE | None' = None, ) -> Any: ... diff --git a/src/plumpy/exceptions.py b/src/plumpy/exceptions.py index 9dca8fdb..5d05ea4b 100644 --- a/src/plumpy/exceptions.py +++ b/src/plumpy/exceptions.py @@ -3,6 +3,8 @@ __all__ = [ 'ClosedError', + 'CoordinatorConnectionError', + 'CoordinatorTimeoutError', 'InvalidStateError', 'KilledError', 'PersistenceError', @@ -42,3 +44,15 @@ class ClosedError(Exception): class TaskRejectedError(Exception): """A task was rejected by the coordinacor""" + + +class CoordinatorCommunicationError(Exception): + """Generic coordinator communication error""" + + +class CoordinatorConnectionError(ConnectionError): + """Raised when coordinator cannot be connected""" + + +class CoordinatorTimeoutError(TimeoutError): + """Raised when communicate with coordinator timeout""" diff --git a/src/plumpy/futures.py b/src/plumpy/futures.py index 01be3951..f3e8a30b 100644 --- a/src/plumpy/futures.py +++ b/src/plumpy/futures.py @@ -3,6 +3,8 @@ Module containing future related methods and classes """ +from __future__ import annotations + import asyncio import contextlib from typing import Any, Awaitable, Callable, Generator, Optional @@ -18,7 +20,7 @@ class InvalidFutureError(Exception): @contextlib.contextmanager -def capture_exceptions(future: Future[Any], ignore: tuple[type[BaseException], ...] = ()) -> Generator[None, Any, None]: +def capture_exceptions(future, ignore: tuple[type[BaseException], ...] = ()) -> Generator[None, Any, None]: # type: ignore[no-untyped-def] """ Capture any exceptions in the context and set them as the result of the given future diff --git a/src/plumpy/message.py b/src/plumpy/message.py index 2d52b048..58c1c6bd 100644 --- a/src/plumpy/message.py +++ b/src/plumpy/message.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- """Module for process level communication functions and classes""" +from __future__ import annotations + import asyncio import logging from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence, Union, cast diff --git a/src/plumpy/processes.py b/src/plumpy/processes.py index 8753c20b..c1381471 100644 --- a/src/plumpy/processes.py +++ b/src/plumpy/processes.py @@ -740,20 +740,18 @@ def on_entered(self, from_state: Optional[process_states.State]) -> None: call_with_super_check(self.on_killed) if self._coordinator and isinstance(self.state, enum.Enum): - # FIXME: move all to `coordinator.broadcast()` call and in rmq implement coordinator - from plumpy.rmq.exceptions import CommunicatorChannelInvalidStateError, CommunicatorConnectionClosed - from_label = cast(enum.Enum, from_state.LABEL).value if from_state is not None else None subject = f'state_changed.{from_label}.{self.state.value}' self.logger.info('Process<%s>: Broadcasting state change: %s', self.pid, subject) try: self._coordinator.broadcast_send(body=None, sender=self.pid, subject=subject) - except (CommunicatorConnectionClosed, CommunicatorChannelInvalidStateError): - message = 'Process<%s>: no connection available to broadcast state change from %s to %s' - self.logger.warning(message, self.pid, from_label, self.state.value) - except concurrent.futures.TimeoutError: - message = 'Process<%s>: sending broadcast of state change from %s to %s timed out' - self.logger.warning(message, self.pid, from_label, self.state.value) + except exceptions.CoordinatorCommunicationError: + message = f'Process<{self.pid}>: cannot broadcast state change from {from_label} to {self.state.value}' + self.logger.warning(message) + self.logger.debug(message, exc_info=True) + except Exception: + # bubble up for unknown exception + raise def on_exiting(self) -> None: state = self.state @@ -1019,7 +1017,7 @@ def _schedule_rpc(self, callback: Callable[..., Any], *args: Any, **kwargs: Any) :return: a kiwi future that resolves to the outcome of the callback """ - kiwi_future = concurrent.futures.Future() + kiwi_future = concurrent.futures.Future() # type: ignore[var-annotated] async def run_callback() -> None: with capture_exceptions(kiwi_future): diff --git a/src/plumpy/rmq/__init__.py b/src/plumpy/rmq/__init__.py index ad0642ca..c44c5a2e 100644 --- a/src/plumpy/rmq/__init__.py +++ b/src/plumpy/rmq/__init__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- -from .exceptions import * +# mypy: disable-error-code=name-defined +from .communications import * from .futures import * -from .process_comms import * +from .process_control import * -__all__ = exceptions.__all__ + communications.__all__ + futures.__all__ + process_comms.__all__ +__all__ = communications.__all__ + futures.__all__ + process_control.__all__ diff --git a/src/plumpy/rmq/communications.py b/src/plumpy/rmq/communications.py index 5e526b23..cb0012c9 100644 --- a/src/plumpy/rmq/communications.py +++ b/src/plumpy/rmq/communications.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- """Module for general kiwipy communication methods""" +from __future__ import annotations + import asyncio import functools from typing import TYPE_CHECKING, Any, Callable, Hashable, Optional @@ -130,10 +132,10 @@ def remove_task_subscriber(self, identifier: 'ID_TYPE') -> None: return self._communicator.remove_task_subscriber(identifier) def add_broadcast_subscriber( - self, subscriber: 'BroadcastSubscriber', subject_filter=None, identifier: Optional['ID_TYPE'] = None + self, subscriber: 'BroadcastSubscriber', identifier: Optional['ID_TYPE'] = None ) -> 'ID_TYPE': converted = convert_to_comm(subscriber, self._loop) - return self._communicator.add_broadcast_subscriber(converted, subject_filter, identifier) + return self._communicator.add_broadcast_subscriber(converted, identifier) def remove_broadcast_subscriber(self, identifier: 'ID_TYPE') -> None: return self._communicator.remove_broadcast_subscriber(identifier) diff --git a/src/plumpy/rmq/exceptions.py b/src/plumpy/rmq/exceptions.py deleted file mode 100644 index 02eb3c97..00000000 --- a/src/plumpy/rmq/exceptions.py +++ /dev/null @@ -1,14 +0,0 @@ -# -*- coding: utf-8 -*- -import kiwipy -from aio_pika.exceptions import ChannelInvalidStateError, ConnectionClosed - -__all__ = [ - 'CommunicatorChannelInvalidStateError', - 'CommunicatorConnectionClosed', -] - -# Alias aio_pika -CommunicatorConnectionClosed = ConnectionClosed -CommunicatorChannelInvalidStateError = ChannelInvalidStateError - -CancelledError = kiwipy.CancelledError diff --git a/src/plumpy/rmq/futures.py b/src/plumpy/rmq/futures.py index 897c8147..73e9e36f 100644 --- a/src/plumpy/rmq/futures.py +++ b/src/plumpy/rmq/futures.py @@ -2,6 +2,8 @@ # mypy: disable-error-code="no-untyped-def, no-untyped-call" """Module containing future related methods and classes""" +from __future__ import annotations + import asyncio import concurrent.futures from typing import Any diff --git a/src/plumpy/rmq/process_comms.py b/src/plumpy/rmq/process_control.py similarity index 96% rename from src/plumpy/rmq/process_comms.py rename to src/plumpy/rmq/process_control.py index 91484332..e9ed3ef8 100644 --- a/src/plumpy/rmq/process_comms.py +++ b/src/plumpy/rmq/process_control.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio -from typing import Any, Dict, Optional, Sequence, Union +from typing import Any, Dict, Hashable, Optional, Sequence, Union import kiwipy @@ -270,6 +270,17 @@ def kill_all(self, msg_text: Optional[str]) -> None: self._coordinator.broadcast_send(msg, subject=Intent.KILL) + def notify_all(self, msg: MessageType | None, sender: Hashable | None = None, subject: str | None = None) -> None: + """ + Notify all processes by broadcasting + + :param msg: an optional pause message + """ + if msg is None: + msg = MessageBuilder.kill() + + self._coordinator.broadcast_send(msg, sender=sender, subject=subject) + def continue_process( self, pid: 'PID_TYPE', tag: Optional[str] = None, nowait: bool = False, no_reply: bool = False ) -> Union[None, PID_TYPE, ProcessResult]: diff --git a/tests/base/test_statemachine.py b/tests/base/test_statemachine.py index 3a1621a2..ddcbb8d9 100644 --- a/tests/base/test_statemachine.py +++ b/tests/base/test_statemachine.py @@ -40,7 +40,7 @@ def exit(self): super().exit() self._update_time() - def play(self, track=None): # pylint: disable=no-self-use, unused-argument + def play(self, track=None): return False def _update_time(self): diff --git a/tests/rmq/__init__.py b/tests/rmq/__init__.py index e69de29b..72078829 100644 --- a/tests/rmq/__init__.py +++ b/tests/rmq/__init__.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +import kiwipy +import concurrent.futures + +from plumpy.exceptions import CoordinatorConnectionError + + +class RmqCoordinator: + def __init__(self, comm: kiwipy.Communicator): + self._comm = comm + + # XXX: naming - `add_receiver_rpc` + def add_rpc_subscriber(self, subscriber, identifier=None): + return self._comm.add_rpc_subscriber(subscriber, identifier) + + # XXX: naming - `add_receiver_broadcast` + def add_broadcast_subscriber( + self, + subscriber, + subject_filter=None, + identifier=None, + ): + subscriber = kiwipy.BroadcastFilter(subscriber, subject=subject_filter) + return self._comm.add_broadcast_subscriber(subscriber, identifier) + + # XXX: naming - `add_reciver_task` (can be combined with two above maybe??) + def add_task_subscriber(self, subscriber, identifier=None): + return self._comm.add_task_subscriber(subscriber, identifier) + + def remove_rpc_subscriber(self, identifier): + return self._comm.remove_rpc_subscriber(identifier) + + def remove_broadcast_subscriber(self, identifier): + return self._comm.remove_broadcast_subscriber(identifier) + + def remove_task_subscriber(self, identifier): + return self._comm.remove_task_subscriber(identifier) + + # XXX: naming - `send_to` + def rpc_send(self, recipient_id, msg): + return self._comm.rpc_send(recipient_id, msg) + + # XXX: naming - `broadcast` + def broadcast_send( + self, + body, + sender=None, + subject=None, + correlation_id=None, + ): + from aio_pika.exceptions import ChannelInvalidStateError, AMQPConnectionError + + try: + rsp = self._comm.broadcast_send(body, sender, subject, correlation_id) + except (ChannelInvalidStateError, AMQPConnectionError, concurrent.futures.TimeoutError) as exc: + raise CoordinatorConnectionError from exc + else: + return rsp + + # XXX: naming - `assign_task` (this may able to be combined with send_to) + def task_send(self, task, no_reply=False): + return self._comm.task_send(task, no_reply) + + def close(self): + self._comm.close() diff --git a/tests/rmq/test_communications.py b/tests/rmq/test_communications.py index 00b7f1c6..e45994b2 100644 --- a/tests/rmq/test_communications.py +++ b/tests/rmq/test_communications.py @@ -2,75 +2,81 @@ """Tests for the :mod:`plumpy.rmq.communications` module.""" import pytest -from kiwipy import CommunicatorHelper +import kiwipy from plumpy.rmq.communications import LoopCommunicator +from . import RmqCoordinator -class Subscriber: - """Test class that mocks a subscriber.""" - - def __call__(self): - pass +@pytest.fixture +def _coordinator(): + """Return an instance of `LoopCommunicator`.""" + class _Communicator(kiwipy.CommunicatorHelper): + def task_send(self, task, no_reply=False): + pass -class Communicator(CommunicatorHelper): - def task_send(self, task, no_reply=False): - pass + def rpc_send(self, recipient_id, msg): + pass - def rpc_send(self, recipient_id, msg): - pass + def broadcast_send(self, body, sender=None, subject=None, correlation_id=None): + pass - def broadcast_send(self, body, sender=None, subject=None, correlation_id=None): - pass + comm = LoopCommunicator(_Communicator()) + coordinator = RmqCoordinator(comm) + yield coordinator -@pytest.fixture -def loop_communicator(): - """Return an instance of `LoopCommunicator`.""" - return LoopCommunicator(Communicator()) + coordinator.close() @pytest.fixture def subscriber(): - """Return an instance of `Subscriber`.""" + """Return an instance of mocked `Subscriber`.""" + + class Subscriber: + """Test class that mocks a subscriber.""" + + def __call__(self): + pass + return Subscriber() -def test_add_rpc_subscriber(loop_communicator, subscriber): +def test_add_rpc_subscriber(_coordinator, subscriber): """Test the `LoopCommunicator.add_rpc_subscriber` method.""" - assert loop_communicator.add_rpc_subscriber(subscriber) is not None + assert _coordinator.add_rpc_subscriber(subscriber) is not None identifier = 'identifier' - assert loop_communicator.add_rpc_subscriber(subscriber, identifier) == identifier + assert _coordinator.add_rpc_subscriber(subscriber, identifier) == identifier -def test_remove_rpc_subscriber(loop_communicator, subscriber): +def test_remove_rpc_subscriber(_coordinator, subscriber): """Test the `LoopCommunicator.remove_rpc_subscriber` method.""" - identifier = loop_communicator.add_rpc_subscriber(subscriber) - loop_communicator.remove_rpc_subscriber(identifier) + identifier = _coordinator.add_rpc_subscriber(subscriber) + _coordinator.remove_rpc_subscriber(identifier) -def test_add_broadcast_subscriber(loop_communicator, subscriber): +def test_add_broadcast_subscriber(_coordinator, subscriber): """Test the `LoopCommunicator.add_broadcast_subscriber` method.""" - assert loop_communicator.add_broadcast_subscriber(subscriber) is not None + assert _coordinator.add_broadcast_subscriber(subscriber) is not None identifier = 'identifier' - assert loop_communicator.add_broadcast_subscriber(subscriber, identifier=identifier) == identifier + assert _coordinator.add_broadcast_subscriber(subscriber, identifier=identifier) == identifier -def test_remove_broadcast_subscriber(loop_communicator, subscriber): +def test_remove_broadcast_subscriber(_coordinator, subscriber): """Test the `LoopCommunicator.remove_broadcast_subscriber` method.""" - identifier = loop_communicator.add_broadcast_subscriber(subscriber) - loop_communicator.remove_broadcast_subscriber(identifier) + identifier = _coordinator.add_broadcast_subscriber(subscriber) + _coordinator.remove_broadcast_subscriber(identifier) -def test_add_task_subscriber(loop_communicator, subscriber): +def test_add_task_subscriber(_coordinator, subscriber): """Test the `LoopCommunicator.add_task_subscriber` method.""" - assert loop_communicator.add_task_subscriber(subscriber) is not None + assert _coordinator.add_task_subscriber(subscriber) is not None -def test_remove_task_subscriber(loop_communicator, subscriber): +def test_remove_task_subscriber(_coordinator, subscriber): """Test the `LoopCommunicator.remove_task_subscriber` method.""" - identifier = loop_communicator.add_task_subscriber(subscriber) - loop_communicator.remove_task_subscriber(identifier) + identifier = _coordinator.add_task_subscriber(subscriber) + _coordinator.remove_task_subscriber(identifier) diff --git a/tests/rmq/test_communicator.py b/tests/rmq/test_communicator.py index 80c1ac71..42c5d748 100644 --- a/tests/rmq/test_communicator.py +++ b/tests/rmq/test_communicator.py @@ -6,16 +6,17 @@ import shutil import tempfile import uuid - -from kiwipy.rmq.communicator import kiwipy import pytest import shortuuid import yaml -from kiwipy import BroadcastFilter, rmq + +from kiwipy.rmq import RmqThreadCommunicator import plumpy -from plumpy.rmq import communications, process_comms +from plumpy.coordinator import Coordinator +from plumpy.rmq import communications, process_control +from . import RmqCoordinator from .. import utils @@ -30,12 +31,12 @@ def persister(): @pytest.fixture -def loop_communicator(): +def _coordinator(): message_exchange = f'{__file__}.{shortuuid.uuid()}' task_exchange = f'{__file__}.{shortuuid.uuid()}' task_queue = f'{__file__}.{shortuuid.uuid()}' - thread_communicator = rmq.RmqThreadCommunicator.connect( + thread_comm = RmqThreadCommunicator.connect( connection_params={'url': 'amqp://guest:guest@localhost:5672/'}, message_exchange=message_exchange, task_exchange=task_exchange, @@ -45,24 +46,24 @@ def loop_communicator(): loop = asyncio.get_event_loop() loop.set_debug(True) + comm = communications.LoopCommunicator(thread_comm, loop=loop) + coordinator = RmqCoordinator(comm) - communicator = communications.LoopCommunicator(thread_communicator, loop=loop) - - yield communicator + yield coordinator - thread_communicator.close() + coordinator.close() @pytest.fixture -def async_controller(loop_communicator: communications.LoopCommunicator): - yield process_comms.RemoteProcessController(loop_communicator) +def async_controller(_coordinator): + yield process_control.RemoteProcessController(_coordinator) class TestLoopCommunicator: """Make sure the loop communicator is working as expected""" @pytest.mark.asyncio - async def test_broadcast(self, loop_communicator): + async def test_broadcast(self, _coordinator): BROADCAST = {'body': 'present', 'sender': 'Martin', 'subject': 'sup', 'correlation_id': 420} # noqa: N806 broadcast_future = asyncio.Future() @@ -75,14 +76,14 @@ def get_broadcast(_comm, body, sender, subject, correlation_id): {'body': body, 'sender': sender, 'subject': subject, 'correlation_id': correlation_id} ) - loop_communicator.add_broadcast_subscriber(get_broadcast) - loop_communicator.broadcast_send(**BROADCAST) + _coordinator.add_broadcast_subscriber(get_broadcast) + _coordinator.broadcast_send(**BROADCAST) result = await broadcast_future assert result == BROADCAST @pytest.mark.asyncio - async def test_broadcast_filter(self, loop_communicator: kiwipy.Communicator): + async def test_broadcast_filter(self, _coordinator: Coordinator): broadcast_future = asyncio.Future() def ignore_broadcast(_comm, body, sender, subject, correlation_id): @@ -91,17 +92,15 @@ def ignore_broadcast(_comm, body, sender, subject, correlation_id): def get_broadcast(_comm, body, sender, subject, correlation_id): broadcast_future.set_result(True) - loop_communicator.add_broadcast_subscriber(ignore_broadcast, subject_filter='other') - loop_communicator.add_broadcast_subscriber(get_broadcast) - loop_communicator.broadcast_send( - **{'body': 'present', 'sender': 'Martin', 'subject': 'sup', 'correlation_id': 420} - ) + _coordinator.add_broadcast_subscriber(ignore_broadcast, subject_filter='other') + _coordinator.add_broadcast_subscriber(get_broadcast) + _coordinator.broadcast_send(**{'body': 'present', 'sender': 'Martin', 'subject': 'sup', 'correlation_id': 420}) result = await broadcast_future assert result is True @pytest.mark.asyncio - async def test_rpc(self, loop_communicator): + async def test_rpc(self, _coordinator): MSG = 'rpc this' # noqa: N806 rpc_future = asyncio.Future() @@ -111,14 +110,14 @@ def get_rpc(_comm, msg): assert loop is asyncio.get_event_loop() rpc_future.set_result(msg) - loop_communicator.add_rpc_subscriber(get_rpc, 'rpc') - loop_communicator.rpc_send('rpc', MSG) + _coordinator.add_rpc_subscriber(get_rpc, 'rpc') + _coordinator.rpc_send('rpc', MSG) result = await rpc_future assert result == MSG @pytest.mark.asyncio - async def test_task(self, loop_communicator): + async def test_task(self, _coordinator): TASK = 'task this' # noqa: N806 task_future = asyncio.Future() @@ -128,8 +127,8 @@ def get_task(_comm, msg): assert loop is asyncio.get_event_loop() task_future.set_result(msg) - loop_communicator.add_task_subscriber(get_task) - loop_communicator.task_send(TASK) + _coordinator.add_task_subscriber(get_task) + _coordinator.task_send(TASK) result = await task_future assert result == TASK @@ -137,43 +136,43 @@ def get_task(_comm, msg): class TestTaskActions: @pytest.mark.asyncio - async def test_launch(self, loop_communicator, async_controller, persister): + async def test_launch(self, _coordinator, async_controller, persister): # Let the process run to the end loop = asyncio.get_event_loop() - loop_communicator.add_task_subscriber(plumpy.ProcessLauncher(loop, persister=persister)) + _coordinator.add_task_subscriber(plumpy.ProcessLauncher(loop, persister=persister)) result = await async_controller.launch_process(utils.DummyProcess) # Check that we got a result assert result == utils.DummyProcess.EXPECTED_OUTPUTS @pytest.mark.asyncio - async def test_launch_nowait(self, loop_communicator, async_controller, persister): + async def test_launch_nowait(self, _coordinator, async_controller, persister): """Testing launching but don't wait, just get the pid""" loop = asyncio.get_event_loop() - loop_communicator.add_task_subscriber(plumpy.ProcessLauncher(loop, persister=persister)) + _coordinator.add_task_subscriber(plumpy.ProcessLauncher(loop, persister=persister)) pid = await async_controller.launch_process(utils.DummyProcess, nowait=True) assert isinstance(pid, uuid.UUID) @pytest.mark.asyncio - async def test_execute_action(self, loop_communicator, async_controller, persister): + async def test_execute_action(self, _coordinator, async_controller, persister): """Test the process execute action""" loop = asyncio.get_event_loop() - loop_communicator.add_task_subscriber(plumpy.ProcessLauncher(loop, persister=persister)) + _coordinator.add_task_subscriber(plumpy.ProcessLauncher(loop, persister=persister)) result = await async_controller.execute_process(utils.DummyProcessWithOutput) assert utils.DummyProcessWithOutput.EXPECTED_OUTPUTS == result @pytest.mark.asyncio - async def test_execute_action_nowait(self, loop_communicator, async_controller, persister): + async def test_execute_action_nowait(self, _coordinator, async_controller, persister): """Test the process execute action""" loop = asyncio.get_event_loop() - loop_communicator.add_task_subscriber(plumpy.ProcessLauncher(loop, persister=persister)) + _coordinator.add_task_subscriber(plumpy.ProcessLauncher(loop, persister=persister)) pid = await async_controller.execute_process(utils.DummyProcessWithOutput, nowait=True) assert isinstance(pid, uuid.UUID) @pytest.mark.asyncio - async def test_launch_many(self, loop_communicator, async_controller, persister): + async def test_launch_many(self, _coordinator, async_controller, persister): """Test launching multiple processes""" loop = asyncio.get_event_loop() - loop_communicator.add_task_subscriber(plumpy.ProcessLauncher(loop, persister=persister)) + _coordinator.add_task_subscriber(plumpy.ProcessLauncher(loop, persister=persister)) num_to_launch = 10 launch_futures = [] @@ -186,10 +185,10 @@ async def test_launch_many(self, loop_communicator, async_controller, persister) assert isinstance(result, uuid.UUID) @pytest.mark.asyncio - async def test_continue(self, loop_communicator, async_controller, persister): + async def test_continue(self, _coordinator, async_controller, persister): """Test continuing a saved process""" loop = asyncio.get_event_loop() - loop_communicator.add_task_subscriber(plumpy.ProcessLauncher(loop, persister=persister)) + _coordinator.add_task_subscriber(plumpy.ProcessLauncher(loop, persister=persister)) process = utils.DummyProcessWithOutput() persister.save_checkpoint(process) pid = process.pid diff --git a/tests/rmq/test_process_comms.py b/tests/rmq/test_process_control.py similarity index 71% rename from tests/rmq/test_process_comms.py rename to tests/rmq/test_process_control.py index 4d9bca29..79a98ba3 100644 --- a/tests/rmq/test_process_comms.py +++ b/tests/rmq/test_process_control.py @@ -7,45 +7,46 @@ from kiwipy import rmq import plumpy -from plumpy.message import KILL_MSG, MESSAGE_KEY -from plumpy.rmq import process_comms +from plumpy.rmq import process_control +from . import RmqCoordinator from .. import utils @pytest.fixture -def thread_communicator(): +def _coordinator(): message_exchange = f'{__file__}.{shortuuid.uuid()}' task_exchange = f'{__file__}.{shortuuid.uuid()}' task_queue = f'{__file__}.{shortuuid.uuid()}' - communicator = rmq.RmqThreadCommunicator.connect( + comm = rmq.RmqThreadCommunicator.connect( connection_params={'url': 'amqp://guest:guest@localhost:5672/'}, message_exchange=message_exchange, task_exchange=task_exchange, task_queue=task_queue, ) - communicator._loop.set_debug(True) + comm._loop.set_debug(True) + coordinator = RmqCoordinator(comm) - yield communicator + yield coordinator - communicator.close() + coordinator.close() @pytest.fixture -def async_controller(thread_communicator: rmq.RmqThreadCommunicator): - yield process_comms.RemoteProcessController(thread_communicator) +def async_controller(_coordinator): + yield process_control.RemoteProcessController(_coordinator) @pytest.fixture -def sync_controller(thread_communicator: rmq.RmqThreadCommunicator): - yield process_comms.RemoteProcessThreadController(thread_communicator) +def sync_controller(_coordinator): + yield process_control.RemoteProcessThreadController(_coordinator) class TestRemoteProcessController: @pytest.mark.asyncio - async def test_pause(self, thread_communicator, async_controller): - proc = utils.WaitForSignalProcess(coordinator=thread_communicator) + async def test_pause(self, _coordinator, async_controller): + proc = utils.WaitForSignalProcess(coordinator=_coordinator) # Run the process in the background asyncio.ensure_future(proc.step_until_terminated()) # Send a pause message @@ -56,8 +57,8 @@ async def test_pause(self, thread_communicator, async_controller): assert proc.paused @pytest.mark.asyncio - async def test_play(self, thread_communicator, async_controller): - proc = utils.WaitForSignalProcess(coordinator=thread_communicator) + async def test_play(self, _coordinator, async_controller): + proc = utils.WaitForSignalProcess(coordinator=_coordinator) # Run the process in the background asyncio.ensure_future(proc.step_until_terminated()) assert proc.pause() @@ -74,8 +75,8 @@ async def test_play(self, thread_communicator, async_controller): await async_controller.kill_process(proc.pid) @pytest.mark.asyncio - async def test_kill(self, thread_communicator, async_controller): - proc = utils.WaitForSignalProcess(coordinator=thread_communicator) + async def test_kill(self, _coordinator, async_controller): + proc = utils.WaitForSignalProcess(coordinator=_coordinator) # Run the process in the event loop asyncio.ensure_future(proc.step_until_terminated()) @@ -87,8 +88,8 @@ async def test_kill(self, thread_communicator, async_controller): assert proc.state == plumpy.ProcessState.KILLED @pytest.mark.asyncio - async def test_status(self, thread_communicator, async_controller): - proc = utils.WaitForSignalProcess(coordinator=thread_communicator) + async def test_status(self, _coordinator, async_controller): + proc = utils.WaitForSignalProcess(coordinator=_coordinator) # Run the process in the background asyncio.ensure_future(proc.step_until_terminated()) @@ -100,15 +101,15 @@ async def test_status(self, thread_communicator, async_controller): # make sure proc reach the final state await async_controller.kill_process(proc.pid) - def test_broadcast(self, thread_communicator): + def test_broadcast(self, _coordinator): messages = [] def on_broadcast_receive(**msg): messages.append(msg) - thread_communicator.add_broadcast_subscriber(on_broadcast_receive) + _coordinator.add_broadcast_subscriber(on_broadcast_receive) - proc = utils.DummyProcess(coordinator=thread_communicator) + proc = utils.DummyProcess(coordinator=_coordinator) proc.execute() expected_subjects = [] @@ -122,8 +123,8 @@ def on_broadcast_receive(**msg): class TestRemoteProcessThreadController: @pytest.mark.asyncio - async def test_pause(self, thread_communicator, sync_controller): - proc = utils.WaitForSignalProcess(coordinator=thread_communicator) + async def test_pause(self, _coordinator, sync_controller): + proc = utils.WaitForSignalProcess(coordinator=_coordinator) # Send a pause message pause_future = sync_controller.pause_process(proc.pid) @@ -136,22 +137,22 @@ async def test_pause(self, thread_communicator, sync_controller): assert proc.paused @pytest.mark.asyncio - async def test_pause_all(self, thread_communicator, sync_controller): + async def test_pause_all(self, _coordinator, sync_controller): """Test pausing all processes on a communicator""" procs = [] for _ in range(10): - procs.append(utils.WaitForSignalProcess(coordinator=thread_communicator)) + procs.append(utils.WaitForSignalProcess(coordinator=_coordinator)) sync_controller.pause_all("Slow yo' roll") # Wait until they are all paused await utils.wait_util(lambda: all([proc.paused for proc in procs])) @pytest.mark.asyncio - async def test_play_all(self, thread_communicator, sync_controller): + async def test_play_all(self, _coordinator, sync_controller): """Test pausing all processes on a communicator""" procs = [] for _ in range(10): - proc = utils.WaitForSignalProcess(coordinator=thread_communicator) + proc = utils.WaitForSignalProcess(coordinator=_coordinator) procs.append(proc) proc.pause('hold tight') @@ -161,8 +162,8 @@ async def test_play_all(self, thread_communicator, sync_controller): await utils.wait_util(lambda: all([not proc.paused for proc in procs])) @pytest.mark.asyncio - async def test_play(self, thread_communicator, sync_controller): - proc = utils.WaitForSignalProcess(coordinator=thread_communicator) + async def test_play(self, _coordinator, sync_controller): + proc = utils.WaitForSignalProcess(coordinator=_coordinator) assert proc.pause() # Send a play message @@ -175,8 +176,8 @@ async def test_play(self, thread_communicator, sync_controller): assert proc.state == plumpy.ProcessState.CREATED @pytest.mark.asyncio - async def test_kill(self, thread_communicator, sync_controller): - proc = utils.WaitForSignalProcess(coordinator=thread_communicator) + async def test_kill(self, _coordinator, sync_controller): + proc = utils.WaitForSignalProcess(coordinator=_coordinator) # Send a kill message kill_future = sync_controller.kill_process(proc.pid) @@ -189,19 +190,19 @@ async def test_kill(self, thread_communicator, sync_controller): assert proc.state == plumpy.ProcessState.KILLED @pytest.mark.asyncio - async def test_kill_all(self, thread_communicator, sync_controller): + async def test_kill_all(self, _coordinator, sync_controller): """Test pausing all processes on a communicator""" procs = [] for _ in range(10): - procs.append(utils.WaitForSignalProcess(coordinator=thread_communicator)) + procs.append(utils.WaitForSignalProcess(coordinator=_coordinator)) sync_controller.kill_all(msg_text='bang bang, I shot you down') await utils.wait_util(lambda: all([proc.killed() for proc in procs])) assert all([proc.state == plumpy.ProcessState.KILLED for proc in procs]) @pytest.mark.asyncio - async def test_status(self, thread_communicator, sync_controller): - proc = utils.WaitForSignalProcess(coordinator=thread_communicator) + async def test_status(self, _coordinator, sync_controller): + proc = utils.WaitForSignalProcess(coordinator=_coordinator) # Run the process in the background asyncio.ensure_future(proc.step_until_terminated()) diff --git a/tests/test_processes.py b/tests/test_processes.py index 99e28de6..62a1e916 100644 --- a/tests/test_processes.py +++ b/tests/test_processes.py @@ -5,16 +5,14 @@ import enum import unittest -import kiwipy import pytest from plumpy.futures import CancellableAction -from tests import utils import plumpy from plumpy import BundleKeys, Process, ProcessState from plumpy.message import MessageBuilder from plumpy.utils import AttributesFrozendict -from tests import utils +from . import utils class ForgetToCallParent(plumpy.Process): @@ -1066,16 +1064,15 @@ def test_paused(self): self.assertSetEqual(events_tester.called, events_tester.expected_events) def test_broadcast(self): - # FIXME: here I need a mock test - communicator = kiwipy.LocalCommunicator() + coordinator = utils.MockCoordinator() messages = [] def on_broadcast_receive(_comm, body, sender, subject, correlation_id): messages.append({'body': body, 'subject': subject, 'sender': sender, 'correlation_id': correlation_id}) - communicator.add_broadcast_subscriber(on_broadcast_receive) - proc = utils.DummyProcess(coordinator=communicator) + coordinator.add_broadcast_subscriber(on_broadcast_receive) + proc = utils.DummyProcess(coordinator=coordinator) proc.execute() expected_subjects = [] @@ -1083,8 +1080,7 @@ def on_broadcast_receive(_comm, body, sender, subject, correlation_id): from_state = utils.DummyProcess.EXPECTED_STATE_SEQUENCE[i - 1].value if i != 0 else None expected_subjects.append(f'state_changed.{from_state}.{state.value}') - for i, message in enumerate(messages): - self.assertEqual(message['subject'], expected_subjects[i]) + assert [msg['subject'] for msg in messages] == expected_subjects class _RestartProcess(utils.WaitForSignalProcess): diff --git a/tests/utils.py b/tests/utils.py index 123d6e72..25936415 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -3,16 +3,144 @@ import asyncio import collections +import sys +from typing import Any import unittest from collections.abc import Mapping +import concurrent.futures import plumpy from plumpy import persistence, process_states, processes, utils +from plumpy.exceptions import CoordinatorConnectionError from plumpy.message import MessageBuilder +from plumpy.rmq import TaskRejected +import shortuuid Snapshot = collections.namedtuple('Snapshot', ['state', 'bundle', 'outputs']) +class MockCoordinator: + def __init__(self): + self._task_subscribers = {} + self._broadcast_subscribers = {} + self._rpc_subscribers = {} + self._closed = False + + def is_closed(self) -> bool: + return self._closed + + def close(self): + if self._closed: + return + self._closed = True + del self._task_subscribers + del self._broadcast_subscribers + del self._rpc_subscribers + + def add_rpc_subscriber(self, subscriber, identifier=None) -> Any: + self._ensure_open() + identifier = identifier or shortuuid.uuid() + if identifier in self._rpc_subscribers: + raise RuntimeError(f"Duplicate RPC subscriber with identifier '{identifier}'") + self._rpc_subscribers[identifier] = subscriber + return identifier + + def remove_rpc_subscriber(self, identifier): + self._ensure_open() + try: + self._rpc_subscribers.pop(identifier) + except KeyError as exc: + raise ValueError(f"Unknown subscriber '{identifier}'") from exc + + def add_task_subscriber(self, subscriber, identifier=None): + """ + Register a task subscriber + + :param subscriber: The task callback function + :param identifier: the subscriber identifier + """ + self._ensure_open() + identifier = identifier or shortuuid.uuid() + if identifier in self._rpc_subscribers: + raise RuntimeError(f"Duplicate RPC subscriber with identifier '{identifier}'") + self._task_subscribers[identifier] = subscriber + return identifier + + def remove_task_subscriber(self, identifier): + """ + Remove a task subscriber + + :param identifier: the subscriber to remove + :raises: ValueError if identifier does not correspond to a known subscriber + """ + self._ensure_open() + try: + self._task_subscribers.pop(identifier) + except KeyError as exception: + raise ValueError(f"Unknown subscriber: '{identifier}'") from exception + + def add_broadcast_subscriber(self, subscriber, subject_filter=None, identifier=None) -> Any: + self._ensure_open() + identifier = identifier or shortuuid.uuid() + if identifier in self._broadcast_subscribers: + raise RuntimeError(f"Duplicate RPC subscriber with identifier '{identifier}'") + + self._broadcast_subscribers[identifier] = subscriber + return identifier + + def remove_broadcast_subscriber(self, identifier): + self._ensure_open() + try: + del self._broadcast_subscribers[identifier] + except KeyError as exception: + raise ValueError(f"Broadcast subscriber '{identifier}' unknown") from exception + + def task_send(self, msg, no_reply=False): + self._ensure_open() + future = concurrent.futures.Future() + + for subscriber in self._task_subscribers.values(): + try: + result = subscriber(self, msg) + future.set_result(result) + break + except TaskRejected: + pass + except Exception: + future.set_exception(RuntimeError(sys.exc_info())) + break + + if no_reply: + return None + + return future + + def rpc_send(self, recipient_id, msg): + self._ensure_open() + try: + subscriber = self._rpc_subscribers[recipient_id] + except KeyError as exception: + raise RuntimeError(f"Unknown rpc recipient '{recipient_id}'") from exception + else: + future = concurrent.futures.Future() + try: + future.set_result(subscriber(self, msg)) + except Exception: + future.set_exception(RuntimeError(sys.exc_info())) + + return future + + def broadcast_send(self, body, sender=None, subject=None, correlation_id=None): + self._ensure_open() + for subscriber in self._broadcast_subscribers.values(): + subscriber(self, body=body, sender=sender, subject=subject, correlation_id=correlation_id) + return True + + def _ensure_open(self): + if self.is_closed(): + raise CoordinatorConnectionError + + class TestCase(unittest.TestCase): pass From bf1275b73a35e5bf71804d5d7c557fe3d808dc03 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Wed, 18 Dec 2024 11:56:22 +0100 Subject: [PATCH 13/64] broadcast subscriber has versatile filters --- src/plumpy/coordinator.py | 3 +- src/plumpy/message.py | 15 +++---- src/plumpy/processes.py | 2 +- src/plumpy/rmq/coordinator.py | 76 ++++++++++++++++++++++++++++++++++ src/plumpy/rmq/futures.py | 31 +++++++++++++- tests/rmq/test_communicator.py | 2 +- tests/utils.py | 2 +- 7 files changed, 117 insertions(+), 14 deletions(-) create mode 100644 src/plumpy/rmq/coordinator.py diff --git a/src/plumpy/coordinator.py b/src/plumpy/coordinator.py index b3dcbec5..29533bf4 100644 --- a/src/plumpy/coordinator.py +++ b/src/plumpy/coordinator.py @@ -21,7 +21,8 @@ def add_rpc_subscriber(self, subscriber: 'RpcSubscriber', identifier: 'ID_TYPE | def add_broadcast_subscriber( self, subscriber: 'BroadcastSubscriber', - subject_filter: str | Pattern[str] | None = None, + subject_filters: list[Hashable | Pattern[str]] | None = None, + sender_filters: list[Hashable | Pattern[str]] | None = None, identifier: 'ID_TYPE | None' = None, ) -> Any: ... diff --git a/src/plumpy/message.py b/src/plumpy/message.py index 58c1c6bd..813402b8 100644 --- a/src/plumpy/message.py +++ b/src/plumpy/message.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Module for process level communication functions and classes""" +"""Module for process level coordination functions and classes""" from __future__ import annotations @@ -69,11 +69,11 @@ class Intent: class MessageBuilder: - """MessageBuilder will construct different messages that can passing over communicator.""" + """MessageBuilder will construct different messages that can passing over coordinator.""" @classmethod def play(cls, text: str | None = None) -> MessageType: - """The play message send over communicator.""" + """The play message send over coordinator.""" return { INTENT_KEY: Intent.PLAY, MESSAGE_KEY: text, @@ -81,7 +81,7 @@ def play(cls, text: str | None = None) -> MessageType: @classmethod def pause(cls, text: str | None = None) -> MessageType: - """The pause message send over communicator.""" + """The pause message send over coordinator.""" return { INTENT_KEY: Intent.PAUSE, MESSAGE_KEY: text, @@ -89,7 +89,7 @@ def pause(cls, text: str | None = None) -> MessageType: @classmethod def kill(cls, text: str | None = None, force_kill: bool = False) -> MessageType: - """The kill message send over communicator.""" + """The kill message send over coordinator.""" return { INTENT_KEY: Intent.KILL, MESSAGE_KEY: text, @@ -98,7 +98,7 @@ def kill(cls, text: str | None = None, force_kill: bool = False) -> MessageType: @classmethod def status(cls, text: str | None = None) -> MessageType: - """The status message send over communicator.""" + """The status message send over coordinator.""" return { INTENT_KEY: Intent.STATUS, MESSAGE_KEY: text, @@ -254,7 +254,6 @@ async def _launch( """ Launch the process - :param _communicator: the communicator :param process_class: the process class to launch :param persist: should the process be persisted :param nowait: if True only return when the process finishes @@ -288,7 +287,6 @@ async def _continue(self, pid: 'PID_TYPE', nowait: bool, tag: Optional[str] = No """ Continue the process - :param _communicator: the communicator :param pid: the pid of the process to continue :param nowait: if True don't wait for the process to complete :param tag: the checkpoint tag to continue from @@ -320,7 +318,6 @@ async def _create( """ Create the process - :param _communicator: the communicator :param process_class: the process class to create :param persist: should the process be persisted :param init_args: positional arguments to the process constructor diff --git a/src/plumpy/processes.py b/src/plumpy/processes.py index c1381471..a7963bc8 100644 --- a/src/plumpy/processes.py +++ b/src/plumpy/processes.py @@ -325,7 +325,7 @@ def init(self) -> None: try: # filter out state change broadcasts identifier = self._coordinator.add_broadcast_subscriber( - self.broadcast_receive, subject_filter=re.compile(r'^(?!state_changed).*'), identifier=str(self.pid) + self.broadcast_receive, subject_filters=[re.compile(r'^(?!state_changed).*')], identifier=str(self.pid) ) self.add_cleanup(functools.partial(self._coordinator.remove_broadcast_subscriber, identifier)) except concurrent.futures.TimeoutError: diff --git a/src/plumpy/rmq/coordinator.py b/src/plumpy/rmq/coordinator.py new file mode 100644 index 00000000..9397d307 --- /dev/null +++ b/src/plumpy/rmq/coordinator.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +import kiwipy +import concurrent.futures + +from plumpy.exceptions import CoordinatorConnectionError + +__all__ = ['RmqCoordinator'] + +class RmqCoordinator: + def __init__(self, comm: kiwipy.Communicator): + self._comm = comm + + # XXX: naming - `add_receiver_rpc` + def add_rpc_subscriber(self, subscriber, identifier=None): + return self._comm.add_rpc_subscriber(subscriber, identifier) + + # XXX: naming - `add_receiver_broadcast` + def add_broadcast_subscriber( + self, + subscriber, + subject_filters=None, + sender_filters=None, + identifier=None, + ): + subscriber = kiwipy.BroadcastFilter(subscriber) + + subject_filters = subject_filters or [] + sender_filters = sender_filters or [] + + for filter in subject_filters: + subscriber.add_subject_filter(filter) + for filter in sender_filters: + subscriber.add_sender_filter(filter) + + return self._comm.add_broadcast_subscriber(subscriber, identifier) + + # XXX: naming - `add_reciver_task` (can be combined with two above maybe??) + def add_task_subscriber(self, subscriber, identifier=None): + return self._comm.add_task_subscriber(subscriber, identifier) + + def remove_rpc_subscriber(self, identifier): + return self._comm.remove_rpc_subscriber(identifier) + + def remove_broadcast_subscriber(self, identifier): + return self._comm.remove_broadcast_subscriber(identifier) + + def remove_task_subscriber(self, identifier): + return self._comm.remove_task_subscriber(identifier) + + # XXX: naming - `send_to` + def rpc_send(self, recipient_id, msg): + return self._comm.rpc_send(recipient_id, msg) + + # XXX: naming - `broadcast` + def broadcast_send( + self, + body, + sender=None, + subject=None, + correlation_id=None, + ): + from aio_pika.exceptions import ChannelInvalidStateError, AMQPConnectionError + + try: + rsp = self._comm.broadcast_send(body, sender, subject, correlation_id) + except (ChannelInvalidStateError, AMQPConnectionError, concurrent.futures.TimeoutError) as exc: + raise CoordinatorConnectionError from exc + else: + return rsp + + # XXX: naming - `assign_task` (this may able to be combined with send_to) + def task_send(self, task, no_reply=False): + return self._comm.task_send(task, no_reply) + + def close(self): + self._comm.close() diff --git a/src/plumpy/rmq/futures.py b/src/plumpy/rmq/futures.py index 73e9e36f..0ebe0d45 100644 --- a/src/plumpy/rmq/futures.py +++ b/src/plumpy/rmq/futures.py @@ -10,7 +10,7 @@ import kiwipy -__all__ = ['wrap_to_concurrent_future'] +__all__ = ['wrap_to_concurrent_future', 'unwrap_kiwi_future'] def _convert_future_exc(exc): @@ -111,3 +111,32 @@ def wrap_to_concurrent_future(future: asyncio.Future[Any]) -> kiwipy.Future: new_future = kiwipy.Future() _chain_future(future, new_future) return new_future + +# XXX: this required in aiida-core, see if really need this unwrap. +def unwrap_kiwi_future(future: kiwipy.Future) -> kiwipy.Future: + """ + Create a kiwi future that represents the final results of a nested series of futures, + meaning that if the futures provided itself resolves to a future the returned + future will not resolve to a value until the final chain of futures is not a future + but a concrete value. If at any point in the chain a future resolves to an exception + then the returned future will also resolve to that exception. + + :param future: the future to unwrap + :return: the unwrapping future + + """ + unwrapping = kiwipy.Future() + + def unwrap(fut: kiwipy.Future) -> None: + if fut.cancelled(): + unwrapping.cancel() + else: + with kiwipy.capture_exceptions(unwrapping): + result = fut.result() + if isinstance(result, kiwipy.Future): + result.add_done_callback(unwrap) + else: + unwrapping.set_result(result) + + future.add_done_callback(unwrap) + return unwrapping diff --git a/tests/rmq/test_communicator.py b/tests/rmq/test_communicator.py index 42c5d748..c5dc156c 100644 --- a/tests/rmq/test_communicator.py +++ b/tests/rmq/test_communicator.py @@ -92,7 +92,7 @@ def ignore_broadcast(_comm, body, sender, subject, correlation_id): def get_broadcast(_comm, body, sender, subject, correlation_id): broadcast_future.set_result(True) - _coordinator.add_broadcast_subscriber(ignore_broadcast, subject_filter='other') + _coordinator.add_broadcast_subscriber(ignore_broadcast, subject_filters=['other']) _coordinator.add_broadcast_subscriber(get_broadcast) _coordinator.broadcast_send(**{'body': 'present', 'sender': 'Martin', 'subject': 'sup', 'correlation_id': 420}) diff --git a/tests/utils.py b/tests/utils.py index 25936415..3d4458f4 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -79,7 +79,7 @@ def remove_task_subscriber(self, identifier): except KeyError as exception: raise ValueError(f"Unknown subscriber: '{identifier}'") from exception - def add_broadcast_subscriber(self, subscriber, subject_filter=None, identifier=None) -> Any: + def add_broadcast_subscriber(self, subscriber, subject_filters=None, sender_filters=None, identifier=None) -> Any: self._ensure_open() identifier = identifier or shortuuid.uuid() if identifier in self._broadcast_subscribers: From 2cfcb1c1f4e9079573ba9688e4355620d62ba1f7 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Thu, 19 Dec 2024 01:06:21 +0100 Subject: [PATCH 14/64] Generic typing for Coordinator --- src/plumpy/rmq/communications.py | 18 ++++++++++++------ src/plumpy/rmq/coordinator.py | 13 +++++++++++-- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/plumpy/rmq/communications.py b/src/plumpy/rmq/communications.py index cb0012c9..61f89fc1 100644 --- a/src/plumpy/rmq/communications.py +++ b/src/plumpy/rmq/communications.py @@ -5,7 +5,7 @@ import asyncio import functools -from typing import TYPE_CHECKING, Any, Callable, Hashable, Optional +from typing import TYPE_CHECKING, Any, Callable, Generic, Hashable, Optional, TypeVar, final import kiwipy @@ -77,10 +77,11 @@ def converted(communicator: kiwipy.Communicator, *args: Any, **kwargs: Any) -> k return converted +T = TypeVar('T', bound=kiwipy.Communicator) def wrap_communicator( - communicator: kiwipy.Communicator, loop: Optional[asyncio.AbstractEventLoop] = None -) -> 'LoopCommunicator': + communicator: T, loop: Optional[asyncio.AbstractEventLoop] = None +) -> 'LoopCommunicator[T]': """ Wrap a communicator such that all callbacks made to any subscribers are scheduled on the given event loop. @@ -100,10 +101,11 @@ def wrap_communicator( return LoopCommunicator(communicator, loop) -class LoopCommunicator(kiwipy.Communicator): # type: ignore +@final +class LoopCommunicator(Generic[T], kiwipy.Communicator): # type: ignore """Wrapper around a `kiwipy.Communicator` that schedules any subscriber messages on a given event loop.""" - def __init__(self, communicator: kiwipy.Communicator, loop: Optional[asyncio.AbstractEventLoop] = None): + def __init__(self, communicator: T, loop: Optional[asyncio.AbstractEventLoop] = None): """ :param communicator: The kiwipy communicator :param loop: The event loop to schedule callbacks on @@ -114,6 +116,10 @@ def __init__(self, communicator: kiwipy.Communicator, loop: Optional[asyncio.Abs self._communicator = communicator self._loop: asyncio.AbstractEventLoop = loop or asyncio.get_event_loop() + @property + def inner(self) -> T: + return self._communicator + def loop(self) -> asyncio.AbstractEventLoop: return self._loop @@ -152,7 +158,7 @@ def broadcast_send( sender: Optional[str] = None, subject: Optional[str] = None, correlation_id: Optional['ID_TYPE'] = None, - ) -> futures.Future: + ) -> kiwipy.Future: return self._communicator.broadcast_send(body, sender, subject, correlation_id) def is_closed(self) -> bool: diff --git a/src/plumpy/rmq/coordinator.py b/src/plumpy/rmq/coordinator.py index 9397d307..c529b61c 100644 --- a/src/plumpy/rmq/coordinator.py +++ b/src/plumpy/rmq/coordinator.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from typing import Generic, TypeVar, final import kiwipy import concurrent.futures @@ -6,10 +7,18 @@ __all__ = ['RmqCoordinator'] -class RmqCoordinator: - def __init__(self, comm: kiwipy.Communicator): +U = TypeVar("U", bound=kiwipy.Communicator) + +@final +class RmqCoordinator(Generic[U]): + def __init__(self, comm: U): self._comm = comm + @property + def communicator(self) -> U: + """The inner communicator.""" + return self._comm + # XXX: naming - `add_receiver_rpc` def add_rpc_subscriber(self, subscriber, identifier=None): return self._comm.add_rpc_subscriber(subscriber, identifier) From d83db648137b8f6fb32e1f151e0c3ec988b8f60f Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Thu, 19 Dec 2024 16:16:46 +0100 Subject: [PATCH 15/64] changes required for aiida-core support --- src/plumpy/processes.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/plumpy/processes.py b/src/plumpy/processes.py index a7963bc8..2d1ad0bb 100644 --- a/src/plumpy/processes.py +++ b/src/plumpy/processes.py @@ -32,6 +32,8 @@ cast, ) +import kiwipy + from plumpy.coordinator import Coordinator try: @@ -324,9 +326,11 @@ def init(self) -> None: try: # filter out state change broadcasts - identifier = self._coordinator.add_broadcast_subscriber( - self.broadcast_receive, subject_filters=[re.compile(r'^(?!state_changed).*')], identifier=str(self.pid) - ) + subscriber = kiwipy.BroadcastFilter(self.broadcast_receive, subject=re.compile(r'^(?!state_changed).*')) + identifier = self._coordinator.add_broadcast_subscriber(subscriber, identifier=str(self.pid)) + # identifier = self._coordinator.add_broadcast_subscriber( + # subscriber, subject_filters=[re.compile(r'^(?!state_changed).*')], identifier=str(self.pid) + # ) self.add_cleanup(functools.partial(self._coordinator.remove_broadcast_subscriber, identifier)) except concurrent.futures.TimeoutError: self.logger.exception('Process<%s>: failed to register as a broadcast subscriber', self.pid) @@ -787,6 +791,8 @@ def recursively_copy_dictionaries(value: Any) -> Any: self._uuid = uuid.uuid4() if self._pid is None: self._pid = self._uuid + # __import__('ipdb').set_trace() + # print("!!!!! ") @super_check def on_exit_running(self) -> None: From d84ad6245a54dd33118165b3b4f5a8fbf0fc6772 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Fri, 20 Dec 2024 15:06:29 +0100 Subject: [PATCH 16/64] Adopt new message protocol --- src/plumpy/coordinator.py | 2 ++ src/plumpy/message.py | 20 ++++++-------------- src/plumpy/processes.py | 11 ++++++----- src/plumpy/rmq/process_control.py | 9 ++++----- tests/test_processes.py | 2 +- 5 files changed, 19 insertions(+), 25 deletions(-) diff --git a/src/plumpy/coordinator.py b/src/plumpy/coordinator.py index 29533bf4..dc501c4e 100644 --- a/src/plumpy/coordinator.py +++ b/src/plumpy/coordinator.py @@ -45,3 +45,5 @@ def broadcast_send( ) -> Any: ... def task_send(self, task: Any, no_reply: bool = False) -> Any: ... + + def close(self) -> None: ... diff --git a/src/plumpy/message.py b/src/plumpy/message.py index 813402b8..14a4e251 100644 --- a/src/plumpy/message.py +++ b/src/plumpy/message.py @@ -16,10 +16,7 @@ from .utils import PID_TYPE __all__ = [ - 'KILL_MSG', - 'PAUSE_MSG', - 'PLAY_MSG', - 'STATUS_MSG', + 'MessageBuilder', 'ProcessLauncher', 'create_continue_body', 'create_launch_body', @@ -29,7 +26,7 @@ from .processes import Process INTENT_KEY = 'intent' -MESSAGE_KEY = 'message' +MESSAGE_TEXT_KEY = 'message' FORCE_KILL_KEY = 'force_kill' @@ -42,11 +39,6 @@ class Intent: STATUS: str = 'status' -PAUSE_MSG = {INTENT_KEY: Intent.PAUSE} -PLAY_MSG = {INTENT_KEY: Intent.PLAY} -KILL_MSG = {INTENT_KEY: Intent.KILL} -STATUS_MSG = {INTENT_KEY: Intent.STATUS} - TASK_KEY = 'task' TASK_ARGS = 'args' PERSIST_KEY = 'persist' @@ -76,7 +68,7 @@ def play(cls, text: str | None = None) -> MessageType: """The play message send over coordinator.""" return { INTENT_KEY: Intent.PLAY, - MESSAGE_KEY: text, + MESSAGE_TEXT_KEY: text, } @classmethod @@ -84,7 +76,7 @@ def pause(cls, text: str | None = None) -> MessageType: """The pause message send over coordinator.""" return { INTENT_KEY: Intent.PAUSE, - MESSAGE_KEY: text, + MESSAGE_TEXT_KEY: text, } @classmethod @@ -92,7 +84,7 @@ def kill(cls, text: str | None = None, force_kill: bool = False) -> MessageType: """The kill message send over coordinator.""" return { INTENT_KEY: Intent.KILL, - MESSAGE_KEY: text, + MESSAGE_TEXT_KEY: text, FORCE_KILL_KEY: force_kill, } @@ -101,7 +93,7 @@ def status(cls, text: str | None = None) -> MessageType: """The status message send over coordinator.""" return { INTENT_KEY: Intent.STATUS, - MESSAGE_KEY: text, + MESSAGE_TEXT_KEY: text, } diff --git a/src/plumpy/processes.py b/src/plumpy/processes.py index 2d1ad0bb..c40347b2 100644 --- a/src/plumpy/processes.py +++ b/src/plumpy/processes.py @@ -323,6 +323,7 @@ def init(self) -> None: self.add_cleanup(functools.partial(self._coordinator.remove_rpc_subscriber, identifier)) except concurrent.futures.TimeoutError: self.logger.exception('Process<%s>: failed to register as an RPC subscriber', self.pid) + # XXX: handle duplicate subscribing here: see aiida-core test_duplicate_subscriber_identifier. try: # filter out state change broadcasts @@ -961,9 +962,9 @@ def message_receive(self, _comm: Coordinator, msg: MessageType) -> Any: if intent == message.Intent.PLAY: return self._schedule_rpc(self.play) if intent == message.Intent.PAUSE: - return self._schedule_rpc(self.pause, msg_text=msg.get(message.MESSAGE_KEY, None)) + return self._schedule_rpc(self.pause, msg_text=msg.get(MESSAGE_TEXT_KEY, None)) if intent == message.Intent.KILL: - return self._schedule_rpc(self.kill, msg_text=msg.get(message.MESSAGE_KEY, None)) + return self._schedule_rpc(self.kill, msg_text=msg.get(MESSAGE_TEXT_KEY, None)) if intent == message.Intent.STATUS: status_info: Dict[str, Any] = {} self.get_status_info(status_info) @@ -994,9 +995,9 @@ def broadcast_receive( if subject == message.Intent.PLAY: fn = self._schedule_rpc(self.play) elif subject == message.Intent.PAUSE: - return self._schedule_rpc(self.pause, msg_text=msg.get(process_comms.MESSAGE_TEXT_KEY, None)) + fn = self._schedule_rpc(self.pause, msg_text=msg.get(MESSAGE_TEXT_KEY, None)) elif subject == message.Intent.KILL: - return self._schedule_rpc(self.kill, msg_text=msg.get(process_comms.MESSAGE_TEXT_KEY, None)) + fn = self._schedule_rpc(self.kill, msg_text=msg.get(MESSAGE_TEXT_KEY, None)) if fn is None: self.logger.warning( @@ -1103,7 +1104,7 @@ def transition_failed( ) self.transition_to(new_state) - def pause(self, msg_text: Optional[str] = None) -> Union[bool, CancellableAction]: + def pause(self, msg_text: str | None = None) -> Union[bool, CancellableAction]: """Pause the process. :param msg: an optional message to set as the status. The current status will be saved in the private diff --git a/src/plumpy/rmq/process_control.py b/src/plumpy/rmq/process_control.py index e9ed3ef8..0caf1d7a 100644 --- a/src/plumpy/rmq/process_control.py +++ b/src/plumpy/rmq/process_control.py @@ -29,6 +29,7 @@ ProcessStatus = Any +# FIXME: the class not fit typing of ProcessController protocol class RemoteProcessController: """ Control remote processes using coroutines that will send messages and wait @@ -189,6 +190,7 @@ async def execute_process( return result +# FIXME: the class not fit typing of ProcessController protocol class RemoteProcessThreadController: """ A class that can be used to control and launch remote processes @@ -270,15 +272,12 @@ def kill_all(self, msg_text: Optional[str]) -> None: self._coordinator.broadcast_send(msg, subject=Intent.KILL) - def notify_all(self, msg: MessageType | None, sender: Hashable | None = None, subject: str | None = None) -> None: + def notify_msg(self, msg: MessageType, sender: Hashable | None = None, subject: str | None = None) -> None: """ - Notify all processes by broadcasting + Notify all processes by broadcasting of a msg :param msg: an optional pause message """ - if msg is None: - msg = MessageBuilder.kill() - self._coordinator.broadcast_send(msg, sender=sender, subject=subject) def continue_process( diff --git a/tests/test_processes.py b/tests/test_processes.py index 62a1e916..a05d09a3 100644 --- a/tests/test_processes.py +++ b/tests/test_processes.py @@ -10,7 +10,7 @@ import plumpy from plumpy import BundleKeys, Process, ProcessState -from plumpy.message import MessageBuilder +from plumpy.message import MESSAGE_TEXT_KEY, MessageBuilder from plumpy.utils import AttributesFrozendict from . import utils From 9065d9ade61d875adf0ae2ff5665f1463af8c110 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Mon, 30 Dec 2024 01:14:52 +0100 Subject: [PATCH 17/64] Simpler create_task_threadsafe implementation --- .python-version | 1 - src/plumpy/coordinator.py | 3 +++ src/plumpy/futures.py | 22 +++++++++++++--------- src/plumpy/rmq/communications.py | 5 +++-- tests/rmq/__init__.py | 4 ++-- 5 files changed, 21 insertions(+), 14 deletions(-) delete mode 100644 .python-version diff --git a/.python-version b/.python-version deleted file mode 100644 index 413c7e7e..00000000 --- a/.python-version +++ /dev/null @@ -1 +0,0 @@ -aiida-core-dev-3.12 diff --git a/src/plumpy/coordinator.py b/src/plumpy/coordinator.py index dc501c4e..702ea5f5 100644 --- a/src/plumpy/coordinator.py +++ b/src/plumpy/coordinator.py @@ -16,8 +16,10 @@ class Coordinator(Protocol): + # XXX: naming - 'add_message_handler' def add_rpc_subscriber(self, subscriber: 'RpcSubscriber', identifier: 'ID_TYPE | None' = None) -> Any: ... + # XXX: naming - 'add_broadcast_handler' def add_broadcast_subscriber( self, subscriber: 'BroadcastSubscriber', @@ -26,6 +28,7 @@ def add_broadcast_subscriber( identifier: 'ID_TYPE | None' = None, ) -> Any: ... + # XXX: naming - absorbed into 'add_message_handler' def add_task_subscriber(self, subscriber: 'TaskSubscriber', identifier: 'ID_TYPE | None' = None) -> 'ID_TYPE': ... def remove_rpc_subscriber(self, identifier: 'ID_TYPE | None') -> None: ... diff --git a/src/plumpy/futures.py b/src/plumpy/futures.py index f3e8a30b..b67c0e80 100644 --- a/src/plumpy/futures.py +++ b/src/plumpy/futures.py @@ -78,12 +78,16 @@ def create_task(coro: Callable[[], Awaitable[Any]], loop: Optional[asyncio.Abstr """ loop = loop or asyncio.get_event_loop() - future = loop.create_future() - - async def run_task() -> None: - with capture_exceptions(future): - res = await coro() - future.set_result(res) - - asyncio.run_coroutine_threadsafe(run_task(), loop) - return future + # future = loop.create_future() + # + # async def run_task() -> None: + # with capture_exceptions(future): + # res = await coro() + # future.set_result(res) + # + # asyncio.run_coroutine_threadsafe(run_task(), loop) + # return future + + return asyncio.wrap_future( + asyncio.run_coroutine_threadsafe(coro(), loop) + ) diff --git a/src/plumpy/rmq/communications.py b/src/plumpy/rmq/communications.py index 61f89fc1..b27b65a1 100644 --- a/src/plumpy/rmq/communications.py +++ b/src/plumpy/rmq/communications.py @@ -9,7 +9,8 @@ import kiwipy -from plumpy import futures +from plumpy.futures import create_task +from plumpy.rmq.futures import wrap_to_concurrent_future from plumpy.utils import ensure_coroutine __all__ = [ @@ -72,7 +73,7 @@ def converted(communicator: kiwipy.Communicator, *args: Any, **kwargs: Any) -> k return kiwi_future msg_fn = functools.partial(coro, communicator, *args, **kwargs) - task_future = futures.create_task(msg_fn, loop) + task_future = create_task(msg_fn, loop) return wrap_to_concurrent_future(task_future) return converted diff --git a/tests/rmq/__init__.py b/tests/rmq/__init__.py index 72078829..2845b1bb 100644 --- a/tests/rmq/__init__.py +++ b/tests/rmq/__init__.py @@ -17,10 +17,10 @@ def add_rpc_subscriber(self, subscriber, identifier=None): def add_broadcast_subscriber( self, subscriber, - subject_filter=None, + subject_filters=None, identifier=None, ): - subscriber = kiwipy.BroadcastFilter(subscriber, subject=subject_filter) + subscriber = kiwipy.BroadcastFilter(subscriber, subject=subject_filters) return self._comm.add_broadcast_subscriber(subscriber, identifier) # XXX: naming - `add_reciver_task` (can be combined with two above maybe??) From 39d363fe2327c7dab3792ff8af0b7a3758730e2a Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Fri, 10 Jan 2025 18:07:32 +0100 Subject: [PATCH 18/64] Remove RmqCoordinator to tests/util only --- src/plumpy/__init__.py | 2 +- src/plumpy/futures.py | 4 +- src/plumpy/message.py | 2 - src/plumpy/processes.py | 2 + src/plumpy/rmq/communications.py | 6 +-- src/plumpy/rmq/coordinator.py | 85 -------------------------------- src/plumpy/rmq/futures.py | 3 +- tests/rmq/__init__.py | 25 ++++++++-- 8 files changed, 31 insertions(+), 98 deletions(-) delete mode 100644 src/plumpy/rmq/coordinator.py diff --git a/src/plumpy/__init__.py b/src/plumpy/__init__.py index cc65ba23..864d2226 100644 --- a/src/plumpy/__init__.py +++ b/src/plumpy/__init__.py @@ -18,9 +18,9 @@ from .process_listener import * from .process_states import * from .processes import * +from .rmq import * from .utils import * from .workchains import * -from .rmq import * __all__ = ( events.__all__ diff --git a/src/plumpy/futures.py b/src/plumpy/futures.py index b67c0e80..ed43389e 100644 --- a/src/plumpy/futures.py +++ b/src/plumpy/futures.py @@ -88,6 +88,4 @@ def create_task(coro: Callable[[], Awaitable[Any]], loop: Optional[asyncio.Abstr # asyncio.run_coroutine_threadsafe(run_task(), loop) # return future - return asyncio.wrap_future( - asyncio.run_coroutine_threadsafe(coro(), loop) - ) + return asyncio.wrap_future(asyncio.run_coroutine_threadsafe(coro(), loop)) diff --git a/src/plumpy/message.py b/src/plumpy/message.py index 14a4e251..009f1b26 100644 --- a/src/plumpy/message.py +++ b/src/plumpy/message.py @@ -10,8 +10,6 @@ from plumpy.coordinator import Coordinator from plumpy.exceptions import PersistenceError, TaskRejectedError -from plumpy.exceptions import PersistenceError, TaskRejectedError - from . import loaders, persistence from .utils import PID_TYPE diff --git a/src/plumpy/processes.py b/src/plumpy/processes.py index c40347b2..7e82a9c3 100644 --- a/src/plumpy/processes.py +++ b/src/plumpy/processes.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- """The main Process module""" +from __future__ import annotations + import abc import asyncio import concurrent.futures diff --git a/src/plumpy/rmq/communications.py b/src/plumpy/rmq/communications.py index b27b65a1..50927557 100644 --- a/src/plumpy/rmq/communications.py +++ b/src/plumpy/rmq/communications.py @@ -78,11 +78,11 @@ def converted(communicator: kiwipy.Communicator, *args: Any, **kwargs: Any) -> k return converted + T = TypeVar('T', bound=kiwipy.Communicator) -def wrap_communicator( - communicator: T, loop: Optional[asyncio.AbstractEventLoop] = None -) -> 'LoopCommunicator[T]': + +def wrap_communicator(communicator: T, loop: Optional[asyncio.AbstractEventLoop] = None) -> 'LoopCommunicator[T]': """ Wrap a communicator such that all callbacks made to any subscribers are scheduled on the given event loop. diff --git a/src/plumpy/rmq/coordinator.py b/src/plumpy/rmq/coordinator.py deleted file mode 100644 index c529b61c..00000000 --- a/src/plumpy/rmq/coordinator.py +++ /dev/null @@ -1,85 +0,0 @@ -# -*- coding: utf-8 -*- -from typing import Generic, TypeVar, final -import kiwipy -import concurrent.futures - -from plumpy.exceptions import CoordinatorConnectionError - -__all__ = ['RmqCoordinator'] - -U = TypeVar("U", bound=kiwipy.Communicator) - -@final -class RmqCoordinator(Generic[U]): - def __init__(self, comm: U): - self._comm = comm - - @property - def communicator(self) -> U: - """The inner communicator.""" - return self._comm - - # XXX: naming - `add_receiver_rpc` - def add_rpc_subscriber(self, subscriber, identifier=None): - return self._comm.add_rpc_subscriber(subscriber, identifier) - - # XXX: naming - `add_receiver_broadcast` - def add_broadcast_subscriber( - self, - subscriber, - subject_filters=None, - sender_filters=None, - identifier=None, - ): - subscriber = kiwipy.BroadcastFilter(subscriber) - - subject_filters = subject_filters or [] - sender_filters = sender_filters or [] - - for filter in subject_filters: - subscriber.add_subject_filter(filter) - for filter in sender_filters: - subscriber.add_sender_filter(filter) - - return self._comm.add_broadcast_subscriber(subscriber, identifier) - - # XXX: naming - `add_reciver_task` (can be combined with two above maybe??) - def add_task_subscriber(self, subscriber, identifier=None): - return self._comm.add_task_subscriber(subscriber, identifier) - - def remove_rpc_subscriber(self, identifier): - return self._comm.remove_rpc_subscriber(identifier) - - def remove_broadcast_subscriber(self, identifier): - return self._comm.remove_broadcast_subscriber(identifier) - - def remove_task_subscriber(self, identifier): - return self._comm.remove_task_subscriber(identifier) - - # XXX: naming - `send_to` - def rpc_send(self, recipient_id, msg): - return self._comm.rpc_send(recipient_id, msg) - - # XXX: naming - `broadcast` - def broadcast_send( - self, - body, - sender=None, - subject=None, - correlation_id=None, - ): - from aio_pika.exceptions import ChannelInvalidStateError, AMQPConnectionError - - try: - rsp = self._comm.broadcast_send(body, sender, subject, correlation_id) - except (ChannelInvalidStateError, AMQPConnectionError, concurrent.futures.TimeoutError) as exc: - raise CoordinatorConnectionError from exc - else: - return rsp - - # XXX: naming - `assign_task` (this may able to be combined with send_to) - def task_send(self, task, no_reply=False): - return self._comm.task_send(task, no_reply) - - def close(self): - self._comm.close() diff --git a/src/plumpy/rmq/futures.py b/src/plumpy/rmq/futures.py index 0ebe0d45..b0da02db 100644 --- a/src/plumpy/rmq/futures.py +++ b/src/plumpy/rmq/futures.py @@ -10,7 +10,7 @@ import kiwipy -__all__ = ['wrap_to_concurrent_future', 'unwrap_kiwi_future'] +__all__ = ['unwrap_kiwi_future', 'wrap_to_concurrent_future'] def _convert_future_exc(exc): @@ -112,6 +112,7 @@ def wrap_to_concurrent_future(future: asyncio.Future[Any]) -> kiwipy.Future: _chain_future(future, new_future) return new_future + # XXX: this required in aiida-core, see if really need this unwrap. def unwrap_kiwi_future(future: kiwipy.Future) -> kiwipy.Future: """ diff --git a/tests/rmq/__init__.py b/tests/rmq/__init__.py index 2845b1bb..3a3b9f67 100644 --- a/tests/rmq/__init__.py +++ b/tests/rmq/__init__.py @@ -1,14 +1,23 @@ # -*- coding: utf-8 -*- +from typing import Generic, TypeVar, final import kiwipy import concurrent.futures from plumpy.exceptions import CoordinatorConnectionError -class RmqCoordinator: - def __init__(self, comm: kiwipy.Communicator): +U = TypeVar('U', bound=kiwipy.Communicator) + +@final +class RmqCoordinator(Generic[U]): + def __init__(self, comm: U): self._comm = comm + @property + def communicator(self) -> U: + """The inner communicator.""" + return self._comm + # XXX: naming - `add_receiver_rpc` def add_rpc_subscriber(self, subscriber, identifier=None): return self._comm.add_rpc_subscriber(subscriber, identifier) @@ -18,9 +27,19 @@ def add_broadcast_subscriber( self, subscriber, subject_filters=None, + sender_filters=None, identifier=None, ): - subscriber = kiwipy.BroadcastFilter(subscriber, subject=subject_filters) + subscriber = kiwipy.BroadcastFilter(subscriber) + + subject_filters = subject_filters or [] + sender_filters = sender_filters or [] + + for filter in subject_filters: + subscriber.add_subject_filter(filter) + for filter in sender_filters: + subscriber.add_sender_filter(filter) + return self._comm.add_broadcast_subscriber(subscriber, identifier) # XXX: naming - `add_reciver_task` (can be combined with two above maybe??) From 6d3101de0472f6ccf67938a61966f38410f22445 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Sun, 12 Jan 2025 00:22:23 +0100 Subject: [PATCH 19/64] Export plumpy.futures.Future --- src/plumpy/futures.py | 12 +----------- src/plumpy/processes.py | 2 ++ src/plumpy/rmq/__init__.py | 21 +++++++++++++++++---- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/plumpy/futures.py b/src/plumpy/futures.py index ed43389e..139c6069 100644 --- a/src/plumpy/futures.py +++ b/src/plumpy/futures.py @@ -9,7 +9,7 @@ import contextlib from typing import Any, Awaitable, Callable, Generator, Optional -__all__ = ['CancellableAction', 'capture_exceptions', 'create_task', 'create_task'] +__all__ = ['CancellableAction', 'Future', 'capture_exceptions', 'create_task', 'create_task'] class InvalidFutureError(Exception): @@ -78,14 +78,4 @@ def create_task(coro: Callable[[], Awaitable[Any]], loop: Optional[asyncio.Abstr """ loop = loop or asyncio.get_event_loop() - # future = loop.create_future() - # - # async def run_task() -> None: - # with capture_exceptions(future): - # res = await coro() - # future.set_result(res) - # - # asyncio.run_coroutine_threadsafe(run_task(), loop) - # return future - return asyncio.wrap_future(asyncio.run_coroutine_threadsafe(coro(), loop)) diff --git a/src/plumpy/processes.py b/src/plumpy/processes.py index 7e82a9c3..4c048d9c 100644 --- a/src/plumpy/processes.py +++ b/src/plumpy/processes.py @@ -329,6 +329,7 @@ def init(self) -> None: try: # filter out state change broadcasts + # XXX: remove dep on kiwipy subscriber = kiwipy.BroadcastFilter(self.broadcast_receive, subject=re.compile(r'^(?!state_changed).*')) identifier = self._coordinator.add_broadcast_subscriber(subscriber, identifier=str(self.pid)) # identifier = self._coordinator.add_broadcast_subscriber( @@ -1332,6 +1333,7 @@ async def step(self) -> None: self._stepping = True next_state = None try: + # XXX: debug log when need to step to next state next_state = await self._run_task(self._state.execute) except process_states.Interruption as exception: # If the interruption was caused by a call to a Process method then there should diff --git a/src/plumpy/rmq/__init__.py b/src/plumpy/rmq/__init__.py index c44c5a2e..a046d229 100644 --- a/src/plumpy/rmq/__init__.py +++ b/src/plumpy/rmq/__init__.py @@ -1,7 +1,20 @@ # -*- coding: utf-8 -*- # mypy: disable-error-code=name-defined -from .communications import * -from .futures import * -from .process_control import * +from .communications import Communicator, DeliveryFailed, RemoteException, TaskRejected, wrap_communicator +from .futures import unwrap_kiwi_future, wrap_to_concurrent_future +from .process_control import RemoteProcessController, RemoteProcessThreadController -__all__ = communications.__all__ + futures.__all__ + process_control.__all__ +__all__ = [ + # communications + 'Communicator', + 'DeliveryFailed', + 'RemoteException', + # process_control + 'RemoteProcessController', + 'RemoteProcessThreadController', + 'TaskRejected', + # futures + 'unwrap_kiwi_future', + 'wrap_communicator', + 'wrap_to_concurrent_future', +] From 19ef5052ab20dd5b5278cfbd93ab7542fa032425 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Sat, 18 Jan 2025 01:18:23 +0100 Subject: [PATCH 20/64] Explict module import list in __init__.py (#8) --- src/plumpy/__init__.py | 173 +++++++++++++++++++++++++++------ src/plumpy/events.py | 10 -- src/plumpy/exceptions.py | 10 -- src/plumpy/futures.py | 2 - src/plumpy/loaders.py | 2 - src/plumpy/message.py | 7 -- src/plumpy/mixins.py | 2 - src/plumpy/persistence.py | 12 --- src/plumpy/ports.py | 2 - src/plumpy/process_listener.py | 2 - src/plumpy/process_states.py | 18 ---- src/plumpy/processes.py | 4 +- src/plumpy/utils.py | 2 - src/plumpy/workchains.py | 6 +- 14 files changed, 147 insertions(+), 105 deletions(-) diff --git a/src/plumpy/__init__.py b/src/plumpy/__init__.py index 864d2226..2c988cd8 100644 --- a/src/plumpy/__init__.py +++ b/src/plumpy/__init__.py @@ -1,42 +1,157 @@ # -*- coding: utf-8 -*- -# mypy: disable-error-code=name-defined __version__ = '0.24.0' import logging +from .base.state_machine import TransitionFailed + # interfaces from .controller import ProcessController from .coordinator import Coordinator -from .events import * -from .exceptions import * -from .futures import * -from .loaders import * -from .message import * -from .mixins import * -from .persistence import * -from .ports import * -from .process_listener import * -from .process_states import * -from .processes import * -from .rmq import * -from .utils import * -from .workchains import * +from .events import ( + PlumpyEventLoopPolicy, + get_event_loop, + new_event_loop, + reset_event_loop_policy, + run_until_complete, + set_event_loop, + set_event_loop_policy, +) +from .exceptions import ( + ClosedError, + CoordinatorConnectionError, + CoordinatorTimeoutError, + InvalidStateError, + KilledError, + PersistenceError, + UnsuccessfulResult, +) +from .futures import CancellableAction, Future, capture_exceptions, create_task +from .loaders import DefaultObjectLoader, ObjectLoader, get_object_loader, set_object_loader +from .message import MessageBuilder, ProcessLauncher, create_continue_body, create_launch_body +from .mixins import ContextMixin +from .persistence import ( + Bundle, + InMemoryPersister, + LoadSaveContext, + PersistedCheckpoint, + Persister, + PicklePersister, + Savable, + SavableFuture, + auto_persist, +) +from .ports import UNSPECIFIED, InputPort, OutputPort, Port, PortNamespace, PortValidationError +from .process_listener import ProcessListener +from .process_spec import ProcessSpec +from .process_states import ( + Continue, + Created, + Excepted, + Finished, + Interruption, + Kill, + Killed, + KillInterruption, + PauseInterruption, + ProcessState, + Running, + Stop, + Wait, + Waiting, +) +from .processes import BundleKeys, Process +from .utils import AttributesDict +from .workchains import ToContext, WorkChain, WorkChainSpec, if_, return_, while_ __all__ = ( - events.__all__ - + exceptions.__all__ - + processes.__all__ - + utils.__all__ - + futures.__all__ - + mixins.__all__ - + persistence.__all__ - + message.__all__ - + process_listener.__all__ - + workchains.__all__ - + loaders.__all__ - + ports.__all__ - + process_states.__all__ -) + ['ProcessController', 'Coordinator'] + # ports + 'UNSPECIFIED', + # utils + 'AttributesDict', + # persistence + 'Bundle', + # processes + 'BundleKeys', + # futures + 'CancellableAction', + # exceptions + 'ClosedError', + # mixins + 'ContextMixin', + # process_states/States + 'Continue', + # coordinator + 'Coordinator', + 'CoordinatorConnectionError', + 'CoordinatorTimeoutError', + 'Created', + # loaders + 'DefaultObjectLoader', + 'Excepted', + 'Finished', + 'Future', + 'InMemoryPersister', + 'InputPort', + 'Interruption', + 'InvalidStateError', + # process_states/Commands + 'Kill', + 'KillInterruption', + 'Killed', + 'KilledError', + 'LoadSaveContext', + # message + 'MessageBuilder', + 'ObjectLoader', + 'OutputPort', + 'PauseInterruption', + 'PersistedCheckpoint', + 'PersistenceError', + 'Persister', + 'PicklePersister', + # event + 'PlumpyEventLoopPolicy', + 'Port', + 'PortNamespace', + 'PortValidationError', + 'Process', + # controller + 'ProcessController', + 'ProcessLauncher', + # process_listener + 'ProcessListener', + 'ProcessSpec', + 'ProcessState', + 'Running', + 'Savable', + 'SavableFuture', + 'Stop', + # workchain + 'ToContext', + 'TransitionFailed', + 'UnsuccessfulResult', + 'Wait', + 'Waiting', + 'WorkChain', + 'WorkChainSpec', + 'auto_persist', + 'capture_exceptions', + 'create_continue_body', + 'create_launch_body', + 'create_task', + 'get_event_loop', + 'get_object_loader', + 'if_', + 'new_event_loop', + 'reset_event_loop_policy', + 'return_', + 'run_until_complete', + 'set_event_loop', + 'set_event_loop_policy', + 'set_object_loader', + 'while_', +) # Do this se we don't get the "No handlers could be found..." warnings that will be produced diff --git a/src/plumpy/events.py b/src/plumpy/events.py index 3de81987..a6e62529 100644 --- a/src/plumpy/events.py +++ b/src/plumpy/events.py @@ -5,16 +5,6 @@ import sys from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Sequence -__all__ = [ - 'PlumpyEventLoopPolicy', - 'get_event_loop', - 'new_event_loop', - 'reset_event_loop_policy', - 'run_until_complete', - 'set_event_loop', - 'set_event_loop_policy', -] - if TYPE_CHECKING: from .processes import Process diff --git a/src/plumpy/exceptions.py b/src/plumpy/exceptions.py index 5d05ea4b..b4358770 100644 --- a/src/plumpy/exceptions.py +++ b/src/plumpy/exceptions.py @@ -1,16 +1,6 @@ # -*- coding: utf-8 -*- from typing import Optional -__all__ = [ - 'ClosedError', - 'CoordinatorConnectionError', - 'CoordinatorTimeoutError', - 'InvalidStateError', - 'KilledError', - 'PersistenceError', - 'UnsuccessfulResult', -] - class KilledError(Exception): """The process was killed.""" diff --git a/src/plumpy/futures.py b/src/plumpy/futures.py index 139c6069..3a59351d 100644 --- a/src/plumpy/futures.py +++ b/src/plumpy/futures.py @@ -9,8 +9,6 @@ import contextlib from typing import Any, Awaitable, Callable, Generator, Optional -__all__ = ['CancellableAction', 'Future', 'capture_exceptions', 'create_task', 'create_task'] - class InvalidFutureError(Exception): """Exception for when a future or action is in an invalid state""" diff --git a/src/plumpy/loaders.py b/src/plumpy/loaders.py index a01f9b60..bb248d6a 100644 --- a/src/plumpy/loaders.py +++ b/src/plumpy/loaders.py @@ -3,8 +3,6 @@ import importlib from typing import Any, Optional -__all__ = ['DefaultObjectLoader', 'ObjectLoader', 'get_object_loader', 'set_object_loader'] - class ObjectLoader(metaclass=abc.ABCMeta): """ diff --git a/src/plumpy/message.py b/src/plumpy/message.py index 009f1b26..04f03bd9 100644 --- a/src/plumpy/message.py +++ b/src/plumpy/message.py @@ -13,13 +13,6 @@ from . import loaders, persistence from .utils import PID_TYPE -__all__ = [ - 'MessageBuilder', - 'ProcessLauncher', - 'create_continue_body', - 'create_launch_body', -] - if TYPE_CHECKING: from .processes import Process diff --git a/src/plumpy/mixins.py b/src/plumpy/mixins.py index 10142eb7..4b993dac 100644 --- a/src/plumpy/mixins.py +++ b/src/plumpy/mixins.py @@ -4,8 +4,6 @@ from . import persistence from .utils import SAVED_STATE_TYPE, AttributesDict -__all__ = ['ContextMixin'] - class ContextMixin(persistence.Savable): """ diff --git a/src/plumpy/persistence.py b/src/plumpy/persistence.py index ba755bc5..f7cbad44 100644 --- a/src/plumpy/persistence.py +++ b/src/plumpy/persistence.py @@ -17,18 +17,6 @@ from .base.utils import call_with_super_check, super_check from .utils import PID_TYPE, SAVED_STATE_TYPE -__all__ = [ - 'Bundle', - 'InMemoryPersister', - 'LoadSaveContext', - 'PersistedCheckpoint', - 'Persister', - 'PicklePersister', - 'Savable', - 'SavableFuture', - 'auto_persist', -] - PersistedCheckpoint = collections.namedtuple('PersistedCheckpoint', ['pid', 'tag']) if TYPE_CHECKING: diff --git a/src/plumpy/ports.py b/src/plumpy/ports.py index cfbd92d5..8522f061 100644 --- a/src/plumpy/ports.py +++ b/src/plumpy/ports.py @@ -11,8 +11,6 @@ from plumpy.utils import AttributesFrozendict, is_mutable_property, type_check -__all__ = ['UNSPECIFIED', 'InputPort', 'OutputPort', 'Port', 'PortNamespace', 'PortValidationError'] - _LOGGER = logging.getLogger(__name__) UNSPECIFIED = () diff --git a/src/plumpy/process_listener.py b/src/plumpy/process_listener.py index 8e1acf94..c3ab8e5a 100644 --- a/src/plumpy/process_listener.py +++ b/src/plumpy/process_listener.py @@ -5,8 +5,6 @@ from . import persistence from .utils import SAVED_STATE_TYPE, protected -__all__ = ['ProcessListener'] - if TYPE_CHECKING: from .processes import Process diff --git a/src/plumpy/process_states.py b/src/plumpy/process_states.py index 723292bf..a4c0788d 100644 --- a/src/plumpy/process_states.py +++ b/src/plumpy/process_states.py @@ -25,24 +25,6 @@ from .persistence import auto_persist from .utils import SAVED_STATE_TYPE, ensure_coroutine -__all__ = [ - 'Continue', - 'Created', - 'Excepted', - 'Finished', - 'Interruption', - # Commands - 'Kill', - 'KillInterruption', - 'Killed', - 'PauseInterruption', - 'ProcessState', - 'Running', - 'Stop', - 'Wait', - 'Waiting', -] - if TYPE_CHECKING: from .processes import Process diff --git a/src/plumpy/processes.py b/src/plumpy/processes.py index 4c048d9c..c328caf0 100644 --- a/src/plumpy/processes.py +++ b/src/plumpy/processes.py @@ -47,7 +47,7 @@ from . import events, exceptions, message, persistence, ports, process_states, utils from .base import state_machine -from .base.state_machine import StateEntryFailed, StateMachine, TransitionFailed, event +from .base.state_machine import StateEntryFailed, StateMachine, event from .base.utils import call_with_super_check, super_check from .event_helper import EventHelper from .futures import CancellableAction, capture_exceptions @@ -58,8 +58,6 @@ T = TypeVar('T') -__all__ = ['BundleKeys', 'Process', 'ProcessSpec', 'TransitionFailed'] - _LOGGER = logging.getLogger(__name__) PROCESS_STACK = ContextVar('process stack', default=[]) diff --git a/src/plumpy/utils.py b/src/plumpy/utils.py index bd1b70a7..3c37ce08 100644 --- a/src/plumpy/utils.py +++ b/src/plumpy/utils.py @@ -23,8 +23,6 @@ from . import lang from .settings import check_override, check_protected -__all__ = ['AttributesDict'] - protected = lang.protected(check=check_protected) override = lang.override(check=check_override) diff --git a/src/plumpy/workchains.py b/src/plumpy/workchains.py index 5df20bf4..ef96b48f 100644 --- a/src/plumpy/workchains.py +++ b/src/plumpy/workchains.py @@ -25,11 +25,9 @@ from plumpy.coordinator import Coordinator -from . import lang, mixins, persistence, process_states, processes +from . import lang, mixins, persistence, process_spec, process_states, processes from .utils import PID_TYPE, SAVED_STATE_TYPE -__all__ = ['ToContext', 'WorkChain', 'WorkChainSpec', 'if_', 'return_', 'while_'] - ToContext = dict PREDICATE_TYPE = Callable[['WorkChain'], bool] @@ -37,7 +35,7 @@ EXIT_CODE_TYPE = int -class WorkChainSpec(processes.ProcessSpec): +class WorkChainSpec(process_spec.ProcessSpec): def __init__(self) -> None: super().__init__() self._outline: Optional[Union['_Instruction', '_FunctionCall']] = None From c19a7d5c13ebc8a4fa00db136ae391f1a698d6fb Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Mon, 2 Dec 2024 15:53:13 +0100 Subject: [PATCH 21/64] Remove the middle layer of statemachine.State + Savable abstraction --- docs/source/nitpick-exceptions | 2 +- src/plumpy/process_states.py | 111 +++++++++++++++++++++++---------- src/plumpy/processes.py | 26 ++++---- src/plumpy/workchains.py | 4 +- 4 files changed, 94 insertions(+), 49 deletions(-) diff --git a/docs/source/nitpick-exceptions b/docs/source/nitpick-exceptions index 6aa8c345..f5265734 100644 --- a/docs/source/nitpick-exceptions +++ b/docs/source/nitpick-exceptions @@ -18,7 +18,7 @@ py:class kiwipy.communications.Communicator # unavailable forward references py:class plumpy.process_states.Command -py:class plumpy.process_states.State +py:class plumpy.state_machine.State py:class plumpy.base.state_machine.State py:class State py:class Process diff --git a/src/plumpy/process_states.py b/src/plumpy/process_states.py index a4c0788d..777933d0 100644 --- a/src/plumpy/process_states.py +++ b/src/plumpy/process_states.py @@ -105,6 +105,7 @@ def save_instance_state(self, out_state: SAVED_STATE_TYPE, save_context: persist def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: super().load_instance_state(saved_state, load_context) + self.state_machine = load_context.process try: self.continue_fn = utils.load_function(saved_state[self.CONTINUE_FN]) except ValueError: @@ -130,25 +131,8 @@ class ProcessState(Enum): KILLED: str = 'killed' -@auto_persist('in_state') -class State(state_machine.State, persistence.Savable): - @property - def process(self) -> state_machine.StateMachine: - """ - :return: The process - """ - return self.state_machine - - def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: - super().load_instance_state(saved_state, load_context) - self.state_machine = load_context.process - - def interrupt(self, reason: Any) -> None: - pass - - -@auto_persist('args', 'kwargs') -class Created(State): +@auto_persist('args', 'kwargs', 'in_state') +class Created(state_machine.State, persistence.Savable): LABEL = ProcessState.CREATED ALLOWED = {ProcessState.RUNNING, ProcessState.KILLED, ProcessState.EXCEPTED} @@ -167,14 +151,23 @@ def save_instance_state(self, out_state: SAVED_STATE_TYPE, save_context: persist def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: super().load_instance_state(saved_state, load_context) + self.state_machine = load_context.process + self.run_fn = getattr(self.process, saved_state[self.RUN_FN]) def execute(self) -> state_machine.State: return self.create_state(ProcessState.RUNNING, self.run_fn, *self.args, **self.kwargs) + @property + def process(self) -> state_machine.StateMachine: + """ + :return: The process + """ + return self.state_machine -@auto_persist('args', 'kwargs') -class Running(State): + +@auto_persist('args', 'kwargs', 'in_state') +class Running(state_machine.State, persistence.Savable): LABEL = ProcessState.RUNNING ALLOWED = { ProcessState.RUNNING, @@ -214,6 +207,8 @@ def save_instance_state(self, out_state: SAVED_STATE_TYPE, save_context: persist def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: super().load_instance_state(saved_state, load_context) + self.state_machine = load_context.process + self.run_fn = ensure_coroutine(getattr(self.process, saved_state[self.RUN_FN])) if self.COMMAND in saved_state: self._command = persistence.Savable.load(saved_state[self.COMMAND], load_context) # type: ignore @@ -221,7 +216,7 @@ def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persi def interrupt(self, reason: Any) -> None: pass - async def execute(self) -> State: # type: ignore + async def execute(self) -> state_machine.State: # type: ignore if self._command is not None: command = self._command else: @@ -236,7 +231,7 @@ async def execute(self) -> State: # type: ignore raise except Exception: excepted = self.create_state(ProcessState.EXCEPTED, *sys.exc_info()[1:]) - return cast(State, excepted) + return cast(state_machine.State, excepted) else: if not isinstance(result, Command): if isinstance(result, exceptions.UnsuccessfulResult): @@ -250,7 +245,7 @@ async def execute(self) -> State: # type: ignore next_state = self._action_command(command) return next_state - def _action_command(self, command: Union[Kill, Stop, Wait, Continue]) -> State: + def _action_command(self, command: Union[Kill, Stop, Wait, Continue]) -> state_machine.State: if isinstance(command, Kill): state = self.create_state(ProcessState.KILLED, command.msg) # elif isinstance(command, Pause): @@ -264,11 +259,18 @@ def _action_command(self, command: Union[Kill, Stop, Wait, Continue]) -> State: else: raise ValueError('Unrecognised command') - return cast(State, state) # casting from base.State to process.State + return cast(state_machine.State, state) # casting from base.State to process.State + @property + def process(self) -> state_machine.StateMachine: + """ + :return: The process + """ + return self.state_machine -@auto_persist('msg', 'data') -class Waiting(State): + +@auto_persist('msg', 'data', 'in_state') +class Waiting(state_machine.State, persistence.Savable): LABEL = ProcessState.WAITING ALLOWED = { ProcessState.RUNNING, @@ -308,6 +310,8 @@ def save_instance_state(self, out_state: SAVED_STATE_TYPE, save_context: persist def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: super().load_instance_state(saved_state, load_context) + self.state_machine = load_context.process + callback_name = saved_state.get(self.DONE_CALLBACK, None) if callback_name is not None: self.done_callback = getattr(self.process, callback_name) @@ -319,7 +323,7 @@ def interrupt(self, reason: Any) -> None: # This will cause the future in execute() to raise the exception self._waiting_future.set_exception(reason) - async def execute(self) -> State: # type: ignore + async def execute(self) -> state_machine.State: # type: ignore try: result = await self._waiting_future except Interruption: @@ -334,7 +338,7 @@ async def execute(self) -> State: # type: ignore else: next_state = self.create_state(ProcessState.RUNNING, self.done_callback, result) - return cast(State, next_state) # casting from base.State to process.State + return cast(state_machine.State, next_state) # casting from base.State to process.State def resume(self, value: Any = NULL) -> None: assert self._waiting_future is not None, 'Not yet waiting' @@ -344,8 +348,16 @@ def resume(self, value: Any = NULL) -> None: self._waiting_future.set_result(value) + @property + def process(self) -> state_machine.StateMachine: + """ + :return: The process + """ + return self.state_machine + -class Excepted(State): +@auto_persist('in_state') +class Excepted(state_machine.State, persistence.Savable): """ Excepted state, can optionally provide exception and trace_back @@ -385,6 +397,8 @@ def save_instance_state(self, out_state: SAVED_STATE_TYPE, save_context: persist def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: super().load_instance_state(saved_state, load_context) + self.state_machine = load_context.process + self.exception = yaml.load(saved_state[self.EXC_VALUE], Loader=Loader) if _HAS_TBLIB: try: @@ -406,9 +420,16 @@ def get_exc_info( self.traceback, ) + @property + def process(self) -> state_machine.StateMachine: + """ + :return: The process + """ + return self.state_machine + -@auto_persist('result', 'successful') -class Finished(State): +@auto_persist('result', 'successful', 'in_state') +class Finished(state_machine.State, persistence.Savable): """State for process is finished. :param result: The result of process @@ -422,9 +443,20 @@ def __init__(self, process: 'Process', result: Any, successful: bool) -> None: self.result = result self.successful = successful + @property + def process(self) -> state_machine.StateMachine: + """ + :return: The process + """ + return self.state_machine + + def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: + super().load_instance_state(saved_state, load_context) + self.state_machine = load_context.process + -@auto_persist('msg') -class Killed(State): +@auto_persist('msg', 'in_state') +class Killed(state_machine.State, persistence.Savable): """ Represents a state where a process has been killed. @@ -444,5 +476,16 @@ def __init__(self, process: 'Process', msg: Optional[MessageType]): super().__init__(process) self.msg = msg + @property + def process(self) -> state_machine.StateMachine: + """ + :return: The process + """ + return self.state_machine + + def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: + super().load_instance_state(saved_state, load_context) + self.state_machine = load_context.process + # endregion diff --git a/src/plumpy/processes.py b/src/plumpy/processes.py index c328caf0..81b32c1f 100644 --- a/src/plumpy/processes.py +++ b/src/plumpy/processes.py @@ -172,7 +172,7 @@ def current(cls) -> Optional['Process']: return None @classmethod - def get_states(cls) -> Sequence[Type[process_states.State]]: + def get_states(cls) -> Sequence[Type[state_machine.State]]: """Return all allowed states of the process.""" state_classes = cls.get_state_classes() return ( @@ -181,7 +181,7 @@ def get_states(cls) -> Sequence[Type[process_states.State]]: ) @classmethod - def get_state_classes(cls) -> Dict[Hashable, Type[process_states.State]]: + def get_state_classes(cls) -> Dict[Hashable, Type[state_machine.State]]: # A mapping of the State constants to the corresponding state class return { process_states.ProcessState.CREATED: process_states.Created, @@ -353,10 +353,10 @@ def _setup_event_hooks(self) -> None: """Set the event hooks to process, when it is created or loaded(recreated).""" event_hooks = { state_machine.StateEventHook.ENTERING_STATE: lambda _s, _h, state: self.on_entering( - cast(process_states.State, state) + cast(state_machine.State, state) ), state_machine.StateEventHook.ENTERED_STATE: lambda _s, _h, from_state: self.on_entered( - cast(Optional[process_states.State], from_state) + cast(Optional[state_machine.State], from_state) ), state_machine.StateEventHook.EXITING_STATE: lambda _s, _h, _state: self.on_exiting(), } @@ -657,7 +657,7 @@ def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persi else: self._loop = asyncio.get_event_loop() - self._state: process_states.State = self.recreate_state(saved_state['_state']) + self._state: state_machine.State = self.recreate_state(saved_state['_state']) if 'coordinator' in load_context: self._coordinator = load_context.coordinator @@ -715,7 +715,7 @@ def log_with_pid(self, level: int, msg: str) -> None: # region Events - def on_entering(self, state: process_states.State) -> None: + def on_entering(self, state: state_machine.State) -> None: # Map these onto direct functions that the subclass can implement state_label = state.LABEL if state_label == process_states.ProcessState.CREATED: @@ -731,7 +731,7 @@ def on_entering(self, state: process_states.State) -> None: elif state_label == process_states.ProcessState.EXCEPTED: call_with_super_check(self.on_except, state.get_exc_info()) # type: ignore - def on_entered(self, from_state: Optional[process_states.State]) -> None: + def on_entered(self, from_state: Optional[state_machine.State]) -> None: # Map these onto direct functions that the subclass can implement state_label = self._state.LABEL if state_label == process_states.ProcessState.RUNNING: @@ -1139,7 +1139,7 @@ def pause(self, msg_text: str | None = None) -> Union[bool, CancellableAction]: msg = MessageBuilder.pause(msg_text) return self._do_pause(state_msg=msg) - def _do_pause(self, state_msg: Optional[MessageType], next_state: Optional[process_states.State] = None) -> bool: + def _do_pause(self, state_msg: Optional[MessageType], next_state: Optional[state_machine.State] = None) -> bool: """Carry out the pause procedure, optionally transitioning to the next state first""" try: if next_state is not None: @@ -1171,7 +1171,7 @@ def _create_interrupt_action(self, exception: process_states.Interruption) -> Ca if isinstance(exception, process_states.KillInterruption): - def do_kill(_next_state: process_states.State) -> Any: + def do_kill(_next_state: state_machine.State) -> Any: try: new_state = self._create_state_instance(process_states.ProcessState.KILLED, msg=exception.msg) self.transition_to(new_state) @@ -1269,7 +1269,7 @@ def is_killing(self) -> bool: # endregion - def create_initial_state(self) -> process_states.State: + def create_initial_state(self) -> state_machine.State: """This method is here to override its superclass. Automatically enter the CREATED state when the process is created. @@ -1277,11 +1277,11 @@ def create_initial_state(self) -> process_states.State: :return: A Created state """ return cast( - process_states.State, + state_machine.State, self.get_state_class(process_states.ProcessState.CREATED)(self, self.run), ) - def recreate_state(self, saved_state: persistence.Bundle) -> process_states.State: + def recreate_state(self, saved_state: persistence.Bundle) -> state_machine.State: """ Create a state object from a saved state @@ -1289,7 +1289,7 @@ def recreate_state(self, saved_state: persistence.Bundle) -> process_states.Stat :return: An instance of the object with its state loaded from the save state. """ load_context = persistence.LoadSaveContext(process=self) - return cast(process_states.State, persistence.Savable.load(saved_state, load_context)) + return cast(state_machine.State, persistence.Savable.load(saved_state, load_context)) # endregion diff --git a/src/plumpy/workchains.py b/src/plumpy/workchains.py index ef96b48f..00a711b5 100644 --- a/src/plumpy/workchains.py +++ b/src/plumpy/workchains.py @@ -25,6 +25,8 @@ from plumpy.coordinator import Coordinator +from plumpy.base import state_machine + from . import lang, mixins, persistence, process_spec, process_states, processes from .utils import PID_TYPE, SAVED_STATE_TYPE @@ -115,7 +117,7 @@ class WorkChain(mixins.ContextMixin, processes.Process): _CONTEXT = 'CONTEXT' @classmethod - def get_state_classes(cls) -> Dict[Hashable, Type[process_states.State]]: + def get_state_classes(cls) -> Dict[Hashable, Type[state_machine.State]]: states_map = super().get_state_classes() states_map[process_states.ProcessState.WAITING] = Waiting return states_map From a88d76da6006d011cf28dedf051139b775eae8fb Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Mon, 2 Dec 2024 16:06:39 +0100 Subject: [PATCH 22/64] Move is_terminal as class attribute required --- src/plumpy/base/state_machine.py | 8 ++------ src/plumpy/process_states.py | 10 ++++++++++ src/plumpy/processes.py | 4 ++-- tests/base/test_statemachine.py | 6 ++++++ 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/plumpy/base/state_machine.py b/src/plumpy/base/state_machine.py index 681858f0..217a7d51 100644 --- a/src/plumpy/base/state_machine.py +++ b/src/plumpy/base/state_machine.py @@ -133,10 +133,6 @@ class State: # from this one ALLOWED: Set[LABEL_TYPE] = set() - @classmethod - def is_terminal(cls) -> bool: - return not cls.ALLOWED - def __init__(self, state_machine: 'StateMachine', *args: Any, **kwargs: Any): """ :param state_machine: The process this state belongs to @@ -165,7 +161,7 @@ def execute(self) -> Optional['State']: @super_check def exit(self) -> None: """Exiting the state""" - if self.is_terminal(): + if self.is_terminal: raise InvalidStateError(f'Cannot exit a terminal state {self.LABEL}') def create_state(self, state_label: Hashable, *args: Any, **kwargs: Any) -> 'State': @@ -347,7 +343,7 @@ def transition_to(self, new_state: State | None, **kwargs: Any) -> None: self._exit_current_state(new_state) self._enter_next_state(new_state) - if self._state is not None and self._state.is_terminal(): + if self._state is not None and self._state.is_terminal: call_with_super_check(self.on_terminated) except Exception: self._transitioning = False diff --git a/src/plumpy/process_states.py b/src/plumpy/process_states.py index 777933d0..75f6c7f8 100644 --- a/src/plumpy/process_states.py +++ b/src/plumpy/process_states.py @@ -137,6 +137,7 @@ class Created(state_machine.State, persistence.Savable): ALLOWED = {ProcessState.RUNNING, ProcessState.KILLED, ProcessState.EXCEPTED} RUN_FN = 'run_fn' + is_terminal = False def __init__(self, process: 'Process', run_fn: Callable[..., Any], *args: Any, **kwargs: Any) -> None: super().__init__(process) @@ -185,6 +186,7 @@ class Running(state_machine.State, persistence.Savable): _running: bool = False _run_handle = None + is_terminal = False def __init__( self, process: 'Process', run_fn: Callable[..., Union[Awaitable[Any], Any]], *args: Any, **kwargs: Any ) -> None: @@ -284,6 +286,8 @@ class Waiting(state_machine.State, persistence.Savable): _interruption = None + is_terminal = False + def __str__(self) -> str: state_info = super().__str__() if self.msg is not None: @@ -370,6 +374,8 @@ class Excepted(state_machine.State, persistence.Savable): EXC_VALUE = 'ex_value' TRACEBACK = 'traceback' + is_terminal = True + def __init__( self, process: 'Process', @@ -438,6 +444,8 @@ class Finished(state_machine.State, persistence.Savable): LABEL = ProcessState.FINISHED + is_terminal = True + def __init__(self, process: 'Process', result: Any, successful: bool) -> None: super().__init__(process) self.result = result @@ -468,6 +476,8 @@ class Killed(state_machine.State, persistence.Savable): LABEL = ProcessState.KILLED + is_terminal = True + def __init__(self, process: 'Process', msg: Optional[MessageType]): """ :param process: The associated process diff --git a/src/plumpy/processes.py b/src/plumpy/processes.py index 81b32c1f..4a8e029c 100644 --- a/src/plumpy/processes.py +++ b/src/plumpy/processes.py @@ -463,7 +463,7 @@ def launch( def has_terminated(self) -> bool: """Return whether the process was terminated.""" - return self._state.is_terminal() + return self._state.is_terminal def result(self) -> Any: """ @@ -536,7 +536,7 @@ def done(self) -> bool: Use the `has_terminated` method instead """ warnings.warn('method is deprecated, use `has_terminated` instead', DeprecationWarning) - return self._state.is_terminal() + return self._state.is_terminal # endregion diff --git a/tests/base/test_statemachine.py b/tests/base/test_statemachine.py index ddcbb8d9..6452be51 100644 --- a/tests/base/test_statemachine.py +++ b/tests/base/test_statemachine.py @@ -20,6 +20,8 @@ class Playing(state_machine.State): ALLOWED = {PAUSED, STOPPED} TRANSITIONS = {STOP: STOPPED} + is_terminal = False + def __init__(self, player, track): assert track is not None, 'Must provide a track name' super().__init__(player) @@ -54,6 +56,8 @@ class Paused(state_machine.State): ALLOWED = {PLAYING, STOPPED} TRANSITIONS = {STOP: STOPPED} + is_terminal = False + def __init__(self, player, playing_state): assert isinstance(playing_state, Playing), 'Must provide the playing state to pause' super().__init__(player) @@ -77,6 +81,8 @@ class Stopped(state_machine.State): } TRANSITIONS = {PLAY: PLAYING} + is_terminal = False + def __str__(self): return '[]' From 95a945b0d8e227888dd1c92531b12012962d5ba2 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Mon, 2 Dec 2024 17:07:29 +0100 Subject: [PATCH 23/64] forming the enter/exit for State protocol --- src/plumpy/base/state_machine.py | 66 ++++--------- src/plumpy/process_states.py | 153 ++++++++++++++++++------------- src/plumpy/workchains.py | 25 +++-- tests/base/test_statemachine.py | 44 +++++++-- tests/test_processes.py | 2 +- 5 files changed, 158 insertions(+), 132 deletions(-) diff --git a/src/plumpy/base/state_machine.py b/src/plumpy/base/state_machine.py index 217a7d51..d224db51 100644 --- a/src/plumpy/base/state_machine.py +++ b/src/plumpy/base/state_machine.py @@ -13,15 +13,17 @@ from typing import ( Any, Callable, + ClassVar, Dict, Hashable, Iterable, List, Optional, + Protocol, Sequence, - Set, Type, Union, + runtime_checkable, ) from plumpy.futures import Future @@ -88,12 +90,12 @@ def event( if from_states != '*': if inspect.isclass(from_states): from_states = (from_states,) - if not all(issubclass(state, State) for state in from_states): # type: ignore + if not all(isinstance(state, State) for state in from_states): # type: ignore raise TypeError(f'from_states: {from_states}') if to_states != '*': if inspect.isclass(to_states): to_states = (to_states,) - if not all(issubclass(state, State) for state in to_states): # type: ignore + if not all(isinstance(state, State) for state in to_states): # type: ignore raise TypeError(f'to_states: {to_states}') def wrapper(wrapped: Callable[..., Any]) -> Callable[..., Any]: @@ -127,53 +129,20 @@ def transition(self: Any, *a: Any, **kw: Any) -> Any: return wrapper -class State: - LABEL: LABEL_TYPE = None - # A set containing the labels of states that can be entered - # from this one - ALLOWED: Set[LABEL_TYPE] = set() +@runtime_checkable +class State(Protocol): + LABEL: ClassVar[LABEL_TYPE] - def __init__(self, state_machine: 'StateMachine', *args: Any, **kwargs: Any): - """ - :param state_machine: The process this state belongs to - """ - self.state_machine = state_machine - self.in_state: bool = False - - def __str__(self) -> str: - return str(self.LABEL) - - @property - def label(self) -> LABEL_TYPE: - """Convenience property to get the state label""" - return self.LABEL - - @super_check - def enter(self) -> None: - """Entering the state""" - - def execute(self) -> Optional['State']: + async def execute(self) -> State | None: """ Execute the state, performing the actions that this state is responsible for. :returns: a state to transition to or None if finished. """ + ... - @super_check - def exit(self) -> None: - """Exiting the state""" - if self.is_terminal: - raise InvalidStateError(f'Cannot exit a terminal state {self.LABEL}') - - def create_state(self, state_label: Hashable, *args: Any, **kwargs: Any) -> 'State': - return self.state_machine.create_state(state_label, *args, **kwargs) - - def do_enter(self) -> None: - call_with_super_check(self.enter) - self.in_state = True + def enter(self) -> None: ... - def do_exit(self) -> None: - call_with_super_check(self.exit) - self.in_state = False + def exit(self) -> None: ... class StateEventHook(enum.Enum): @@ -250,7 +219,7 @@ def __ensure_built(cls) -> None: # Build the states map cls._STATES_MAP = {} for state_cls in cls.STATES: - assert issubclass(state_cls, State) + assert isinstance(state_cls, State) label = state_cls.LABEL assert label not in cls._STATES_MAP, f"Duplicate label '{label}'" cls._STATES_MAP[label] = state_cls @@ -382,7 +351,8 @@ def create_state(self, state_label: Hashable, *args: Any, **kwargs: Any) -> Stat # This method should be replaced by `_create_state_instance`. # aiida-core using this method for its Waiting state override. try: - return self.get_states_map()[state_label](self, *args, **kwargs) + state_cls = self.get_states_map()[state_label] + return state_cls(self, *args, **kwargs) except KeyError: raise ValueError(f'{state_label} is not a valid state') @@ -392,20 +362,20 @@ def _exit_current_state(self, next_state: State) -> None: # If we're just being constructed we may not have a state yet to exit, # in which case check the new state is the initial state if self._state is None: - if next_state.label != self.initial_state_label(): + if next_state.LABEL != self.initial_state_label(): raise RuntimeError(f"Cannot enter state '{next_state}' as the initial state") return # Nothing to exit if next_state.LABEL not in self._state.ALLOWED: raise RuntimeError(f'Cannot transition from {self._state.LABEL} to {next_state.label}') self._fire_state_event(StateEventHook.EXITING_STATE, next_state) - self._state.do_exit() + self._state.exit() def _enter_next_state(self, next_state: State) -> None: last_state = self._state self._fire_state_event(StateEventHook.ENTERING_STATE, next_state) # Enter the new state - next_state.do_enter() + next_state.enter() self._state = next_state self._fire_state_event(StateEventHook.ENTERED_STATE, last_state) diff --git a/src/plumpy/process_states.py b/src/plumpy/process_states.py index 75f6c7f8..a2ba5177 100644 --- a/src/plumpy/process_states.py +++ b/src/plumpy/process_states.py @@ -131,6 +131,7 @@ class ProcessState(Enum): KILLED: str = 'killed' +@final @auto_persist('args', 'kwargs', 'in_state') class Created(state_machine.State, persistence.Savable): LABEL = ProcessState.CREATED @@ -140,11 +141,12 @@ class Created(state_machine.State, persistence.Savable): is_terminal = False def __init__(self, process: 'Process', run_fn: Callable[..., Any], *args: Any, **kwargs: Any) -> None: - super().__init__(process) assert run_fn is not None + self.process = process self.run_fn = run_fn self.args = args self.kwargs = kwargs + self.in_state = True def save_instance_state(self, out_state: SAVED_STATE_TYPE, save_context: persistence.LoadSaveContext) -> None: super().save_instance_state(out_state, save_context) @@ -152,21 +154,24 @@ def save_instance_state(self, out_state: SAVED_STATE_TYPE, save_context: persist def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: super().load_instance_state(saved_state, load_context) - self.state_machine = load_context.process + self.process = load_context.process self.run_fn = getattr(self.process, saved_state[self.RUN_FN]) - def execute(self) -> state_machine.State: - return self.create_state(ProcessState.RUNNING, self.run_fn, *self.args, **self.kwargs) + async def execute(self) -> state_machine.State: + return self.process.create_state(ProcessState.RUNNING, self.run_fn, *self.args, **self.kwargs) - @property - def process(self) -> state_machine.StateMachine: - """ - :return: The process - """ - return self.state_machine + def enter(self) -> None: + self.in_state = True + + def exit(self) -> None: + if self.is_terminal: + raise exceptions.InvalidStateError(f'Cannot exit a terminal state {self.LABEL}') + + self.in_state = False +@final @auto_persist('args', 'kwargs', 'in_state') class Running(state_machine.State, persistence.Savable): LABEL = ProcessState.RUNNING @@ -187,19 +192,17 @@ class Running(state_machine.State, persistence.Savable): _run_handle = None is_terminal = False + def __init__( self, process: 'Process', run_fn: Callable[..., Union[Awaitable[Any], Any]], *args: Any, **kwargs: Any ) -> None: - super().__init__(process) assert run_fn is not None - self.run_fn = ensure_coroutine(run_fn) - # We wrap `run_fn` to a coroutine so we can apply await on it, - # even it if it was not a coroutine in the first place. - # This allows the same usage of async and non-async function - # with the await syntax while not changing the program logic. + self.process = process + self.run_fn = run_fn self.args = args self.kwargs = kwargs self._run_handle = None + self.in_state = False def save_instance_state(self, out_state: SAVED_STATE_TYPE, save_context: persistence.LoadSaveContext) -> None: super().save_instance_state(out_state, save_context) @@ -209,7 +212,7 @@ def save_instance_state(self, out_state: SAVED_STATE_TYPE, save_context: persist def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: super().load_instance_state(saved_state, load_context) - self.state_machine = load_context.process + self.process = load_context.process self.run_fn = ensure_coroutine(getattr(self.process, saved_state[self.RUN_FN])) if self.COMMAND in saved_state: @@ -218,7 +221,7 @@ def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persi def interrupt(self, reason: Any) -> None: pass - async def execute(self) -> state_machine.State: # type: ignore + async def execute(self) -> state_machine.State: if self._command is not None: command = self._command else: @@ -232,7 +235,7 @@ async def execute(self) -> state_machine.State: # type: ignore # Let this bubble up to the caller raise except Exception: - excepted = self.create_state(ProcessState.EXCEPTED, *sys.exc_info()[1:]) + excepted = self.process.create_state(ProcessState.EXCEPTED, *sys.exc_info()[1:]) return cast(state_machine.State, excepted) else: if not isinstance(result, Command): @@ -249,28 +252,30 @@ async def execute(self) -> state_machine.State: # type: ignore def _action_command(self, command: Union[Kill, Stop, Wait, Continue]) -> state_machine.State: if isinstance(command, Kill): - state = self.create_state(ProcessState.KILLED, command.msg) + state = self.process.create_state(ProcessState.KILLED, command.msg) # elif isinstance(command, Pause): # self.pause() elif isinstance(command, Stop): - state = self.create_state(ProcessState.FINISHED, command.result, command.successful) + state = self.process.create_state(ProcessState.FINISHED, command.result, command.successful) elif isinstance(command, Wait): - state = self.create_state(ProcessState.WAITING, command.continue_fn, command.msg, command.data) + state = self.process.create_state(ProcessState.WAITING, command.continue_fn, command.msg, command.data) elif isinstance(command, Continue): - state = self.create_state(ProcessState.RUNNING, command.continue_fn, *command.args) + state = self.process.create_state(ProcessState.RUNNING, command.continue_fn, *command.args) else: raise ValueError('Unrecognised command') return cast(state_machine.State, state) # casting from base.State to process.State - @property - def process(self) -> state_machine.StateMachine: - """ - :return: The process - """ - return self.state_machine + def enter(self) -> None: + self.in_state = True + + def exit(self) -> None: + if self.is_terminal: + raise exceptions.InvalidStateError(f'Cannot exit a terminal state {self.LABEL}') + self.in_state = False +@final @auto_persist('msg', 'data', 'in_state') class Waiting(state_machine.State, persistence.Savable): LABEL = ProcessState.WAITING @@ -301,11 +306,12 @@ def __init__( msg: Optional[str] = None, data: Optional[Any] = None, ) -> None: - super().__init__(process) + self.process = process self.done_callback = done_callback self.msg = msg self.data = data self._waiting_future: futures.Future = futures.Future() + self.in_state = False def save_instance_state(self, out_state: SAVED_STATE_TYPE, save_context: persistence.LoadSaveContext) -> None: super().save_instance_state(out_state, save_context) @@ -314,7 +320,7 @@ def save_instance_state(self, out_state: SAVED_STATE_TYPE, save_context: persist def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: super().load_instance_state(saved_state, load_context) - self.state_machine = load_context.process + self.process = load_context.process callback_name = saved_state.get(self.DONE_CALLBACK, None) if callback_name is not None: @@ -338,9 +344,9 @@ async def execute(self) -> state_machine.State: # type: ignore raise if result == NULL: - next_state = self.create_state(ProcessState.RUNNING, self.done_callback) + next_state = self.process.create_state(ProcessState.RUNNING, self.done_callback) else: - next_state = self.create_state(ProcessState.RUNNING, self.done_callback, result) + next_state = self.process.create_state(ProcessState.RUNNING, self.done_callback, result) return cast(state_machine.State, next_state) # casting from base.State to process.State @@ -352,12 +358,14 @@ def resume(self, value: Any = NULL) -> None: self._waiting_future.set_result(value) - @property - def process(self) -> state_machine.StateMachine: - """ - :return: The process - """ - return self.state_machine + def enter(self) -> None: + self.in_state = True + + def exit(self) -> None: + if self.is_terminal: + raise exceptions.InvalidStateError(f'Cannot exit a terminal state {self.LABEL}') + + self.in_state = False @auto_persist('in_state') @@ -387,9 +395,10 @@ def __init__( :param exception: The exception instance :param trace_back: An optional exception traceback """ - super().__init__(process) + self.process = process self.exception = exception self.traceback = trace_back + self.in_state = False def __str__(self) -> str: exception = traceback.format_exception_only(type(self.exception) if self.exception else None, self.exception)[0] @@ -403,7 +412,7 @@ def save_instance_state(self, out_state: SAVED_STATE_TYPE, save_context: persist def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: super().load_instance_state(saved_state, load_context) - self.state_machine = load_context.process + self.process = load_context.process self.exception = yaml.load(saved_state[self.EXC_VALUE], Loader=Loader) if _HAS_TBLIB: @@ -426,12 +435,17 @@ def get_exc_info( self.traceback, ) - @property - def process(self) -> state_machine.StateMachine: - """ - :return: The process - """ - return self.state_machine + async def execute(self) -> state_machine.State: # type: ignore + ... + + def enter(self) -> None: + self.in_state = True + + def exit(self) -> None: + if self.is_terminal: + raise exceptions.InvalidStateError(f'Cannot exit a terminal state {self.LABEL}') + + self.in_state = False @auto_persist('result', 'successful', 'in_state') @@ -447,20 +461,26 @@ class Finished(state_machine.State, persistence.Savable): is_terminal = True def __init__(self, process: 'Process', result: Any, successful: bool) -> None: - super().__init__(process) + self.process = process self.result = result self.successful = successful - - @property - def process(self) -> state_machine.StateMachine: - """ - :return: The process - """ - return self.state_machine + self.in_state = False def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: super().load_instance_state(saved_state, load_context) - self.state_machine = load_context.process + self.process = load_context.process + + def enter(self) -> None: + self.in_state = True + + async def execute(self) -> state_machine.State: # type: ignore + ... + + def exit(self) -> None: + if self.is_terminal: + raise exceptions.InvalidStateError(f'Cannot exit a terminal state {self.LABEL}') + + self.in_state = False @auto_persist('msg', 'in_state') @@ -483,19 +503,24 @@ def __init__(self, process: 'Process', msg: Optional[MessageType]): :param process: The associated process :param msg: Optional kill message """ - super().__init__(process) + self.process = process self.msg = msg - @property - def process(self) -> state_machine.StateMachine: - """ - :return: The process - """ - return self.state_machine + async def execute(self) -> state_machine.State: # type: ignore + ... def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: super().load_instance_state(saved_state, load_context) - self.state_machine = load_context.process + self.process = load_context.process + + def enter(self) -> None: + self.in_state = True + + def exit(self) -> None: + if self.is_terminal: + raise exceptions.InvalidStateError(f'Cannot exit a terminal state {self.LABEL}') + + self.in_state = False # endregion diff --git a/src/plumpy/workchains.py b/src/plumpy/workchains.py index 00a711b5..f6369c28 100644 --- a/src/plumpy/workchains.py +++ b/src/plumpy/workchains.py @@ -26,6 +26,7 @@ from plumpy.coordinator import Coordinator from plumpy.base import state_machine +from plumpy.exceptions import InvalidStateError from . import lang, mixins, persistence, process_spec, process_states, processes from .utils import PID_TYPE, SAVED_STATE_TYPE @@ -85,16 +86,6 @@ def __init__( resolved_awaitable = awaitable.future() if isinstance(awaitable, processes.Process) else awaitable self._awaiting[resolved_awaitable] = key - def enter(self) -> None: - super().enter() - for awaitable in self._awaiting: - awaitable.add_done_callback(self._awaitable_done) - - def exit(self) -> None: - super().exit() - for awaitable in self._awaiting: - awaitable.remove_done_callback(self._awaitable_done) - def _awaitable_done(self, awaitable: asyncio.Future) -> None: key = self._awaiting.pop(awaitable) try: @@ -105,6 +96,20 @@ def _awaitable_done(self, awaitable: asyncio.Future) -> None: if not self._awaiting: self._waiting_future.set_result(lang.NULL) + def enter(self) -> None: + for awaitable in self._awaiting: + awaitable.add_done_callback(self._awaitable_done) + + self.in_state = True + + def exit(self) -> None: + if self.is_terminal: + raise InvalidStateError(f'Cannot exit a terminal state {self.LABEL}') + self.in_state = False + + for awaitable in self._awaiting: + awaitable.remove_done_callback(self._awaitable_done) + class WorkChain(mixins.ContextMixin, processes.Process): """ diff --git a/tests/base/test_statemachine.py b/tests/base/test_statemachine.py index 6452be51..07a2dc80 100644 --- a/tests/base/test_statemachine.py +++ b/tests/base/test_statemachine.py @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- import time +from typing import final import unittest from plumpy.base import state_machine +from plumpy.exceptions import InvalidStateError # Events PLAY = 'Play' @@ -24,24 +26,16 @@ class Playing(state_machine.State): def __init__(self, player, track): assert track is not None, 'Must provide a track name' - super().__init__(player) self.track = track self._last_time = None self._played = 0.0 + self.in_state = False def __str__(self): if self.in_state: self._update_time() return f'> {self.track} ({self._played}s)' - def enter(self): - super().enter() - self._last_time = time.time() - - def exit(self): - super().exit() - self._update_time() - def play(self, track=None): return False @@ -50,6 +44,17 @@ def _update_time(self): self._played += current_time - self._last_time self._last_time = current_time + def enter(self) -> None: + self._last_time = time.time() + self.in_state = True + + def exit(self) -> None: + if self.is_terminal: + raise InvalidStateError(f'Cannot exit a terminal state {self.LABEL}') + + self._update_time() + self.in_state = False + class Paused(state_machine.State): LABEL = PAUSED @@ -73,6 +78,15 @@ def play(self, track=None): else: self.state_machine.transition_to(self.playing_state) + def enter(self) -> None: + self.in_state = True + + def exit(self) -> None: + if self.is_terminal: + raise InvalidStateError(f'Cannot exit a terminal state {self.LABEL}') + + self.in_state = False + class Stopped(state_machine.State): LABEL = STOPPED @@ -83,12 +97,24 @@ class Stopped(state_machine.State): is_terminal = False + def __init__(self, player): + self.state_machine = player + def __str__(self): return '[]' def play(self, track): self.state_machine.transition_to(Playing(self.state_machine, track=track)) + def enter(self) -> None: + self.in_state = True + + def exit(self) -> None: + if self.is_terminal: + raise InvalidStateError(f'Cannot exit a terminal state {self.LABEL}') + + self.in_state = False + class CdPlayer(state_machine.StateMachine): STATES = (Stopped, Playing, Paused) diff --git a/tests/test_processes.py b/tests/test_processes.py index a05d09a3..c6576b7e 100644 --- a/tests/test_processes.py +++ b/tests/test_processes.py @@ -653,7 +653,7 @@ def test_exception_during_on_entered(self): class RaisingProcess(Process): def on_entered(self, from_state): - if from_state is not None and from_state.label == ProcessState.RUNNING: + if from_state is not None and from_state.LABEL == ProcessState.RUNNING: raise RuntimeError('exception during on_entered') super().on_entered(from_state) From 7000b5ba17bb05eb1e9b800897158fa877ef83a4 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Mon, 2 Dec 2024 23:20:45 +0100 Subject: [PATCH 24/64] Forming Interruptable and Proceedable protocol --- src/plumpy/base/state_machine.py | 20 +++++++++++++++----- src/plumpy/process_states.py | 12 ++---------- src/plumpy/processes.py | 22 +++++++++++++++++++++- 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/src/plumpy/base/state_machine.py b/src/plumpy/base/state_machine.py index d224db51..ee238a83 100644 --- a/src/plumpy/base/state_machine.py +++ b/src/plumpy/base/state_machine.py @@ -132,18 +132,28 @@ def transition(self: Any, *a: Any, **kw: Any) -> Any: @runtime_checkable class State(Protocol): LABEL: ClassVar[LABEL_TYPE] + is_terminal: ClassVar[bool] - async def execute(self) -> State | None: + def enter(self) -> None: ... + + def exit(self) -> None: ... + + +@runtime_checkable +class Interruptable(Protocol): + def interrupt(self, reason: Exception) -> None: ... + + +@runtime_checkable +class Proceedable(Protocol): + + def execute(self) -> State | None: """ Execute the state, performing the actions that this state is responsible for. :returns: a state to transition to or None if finished. """ ... - def enter(self) -> None: ... - - def exit(self) -> None: ... - class StateEventHook(enum.Enum): """ diff --git a/src/plumpy/process_states.py b/src/plumpy/process_states.py index a2ba5177..c82c34a3 100644 --- a/src/plumpy/process_states.py +++ b/src/plumpy/process_states.py @@ -275,6 +275,7 @@ def exit(self) -> None: self.in_state = False + @final @auto_persist('msg', 'data', 'in_state') class Waiting(state_machine.State, persistence.Savable): @@ -329,7 +330,7 @@ def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persi self.done_callback = None self._waiting_future = futures.Future() - def interrupt(self, reason: Any) -> None: + def interrupt(self, reason: Exception) -> None: # This will cause the future in execute() to raise the exception self._waiting_future.set_exception(reason) @@ -435,9 +436,6 @@ def get_exc_info( self.traceback, ) - async def execute(self) -> state_machine.State: # type: ignore - ... - def enter(self) -> None: self.in_state = True @@ -473,9 +471,6 @@ def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persi def enter(self) -> None: self.in_state = True - async def execute(self) -> state_machine.State: # type: ignore - ... - def exit(self) -> None: if self.is_terminal: raise exceptions.InvalidStateError(f'Cannot exit a terminal state {self.LABEL}') @@ -506,9 +501,6 @@ def __init__(self, process: 'Process', msg: Optional[MessageType]): self.process = process self.msg = msg - async def execute(self) -> state_machine.State: # type: ignore - ... - def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: super().load_instance_state(saved_state, load_context) self.process = load_context.process diff --git a/src/plumpy/processes.py b/src/plumpy/processes.py index 4a8e029c..c6f45b03 100644 --- a/src/plumpy/processes.py +++ b/src/plumpy/processes.py @@ -47,7 +47,15 @@ from . import events, exceptions, message, persistence, ports, process_states, utils from .base import state_machine -from .base.state_machine import StateEntryFailed, StateMachine, event +from .base.state_machine import ( + Interruptable, + Proceedable, + StateEntryFailed, + StateMachine, + StateMachineError, + TransitionFailed, + event, +) from .base.utils import call_with_super_check, super_check from .event_helper import EventHelper from .futures import CancellableAction, capture_exceptions @@ -1127,6 +1135,11 @@ def pause(self, msg_text: str | None = None) -> Union[bool, CancellableAction]: return self._pausing if self._stepping: + if not isinstance(self._state, Interruptable): + raise exceptions.InvalidStateError( + f'cannot interrupt {self._state.__class__}, method `interrupt` not implement' + ) + # Ask the step function to pause by setting this flag and giving the # caller back a future interrupt_exception = process_states.PauseInterruption(msg_text) @@ -1139,6 +1152,10 @@ def pause(self, msg_text: str | None = None) -> Union[bool, CancellableAction]: msg = MessageBuilder.pause(msg_text) return self._do_pause(state_msg=msg) + @staticmethod + def _interrupt(state: Interruptable, reason: Exception) -> None: + state.interrupt(reason) + def _do_pause(self, state_msg: Optional[MessageType], next_state: Optional[state_machine.State] = None) -> bool: """Carry out the pause procedure, optionally transitioning to the next state first""" try: @@ -1327,6 +1344,9 @@ async def step(self) -> None: if self.paused and self._paused is not None: await self._paused + if not isinstance(self._state, Proceedable): + raise StateMachineError(f'cannot step from {self._state.__class__}, async method `execute` not implemented') + try: self._stepping = True next_state = None From 7ed200d6ab35f4e065f4ca80ccf902665e7c15ca Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Tue, 3 Dec 2024 00:48:53 +0100 Subject: [PATCH 25/64] Refactoring create_state as static function initialize state from label create_state refact Hashable initialized + parameters passed to Hashable Fix pre-commit errors --- src/plumpy/base/state_machine.py | 47 +++--- src/plumpy/process_states.py | 238 ++++++++++++++++--------------- src/plumpy/processes.py | 41 +++--- src/plumpy/workchains.py | 13 +- tests/base/test_statemachine.py | 15 +- 5 files changed, 174 insertions(+), 180 deletions(-) diff --git a/src/plumpy/base/state_machine.py b/src/plumpy/base/state_machine.py index ee238a83..e3912b6f 100644 --- a/src/plumpy/base/state_machine.py +++ b/src/plumpy/base/state_machine.py @@ -34,7 +34,6 @@ _LOGGER = logging.getLogger(__name__) -LABEL_TYPE = Union[None, enum.Enum, str] EVENT_CALLBACK_TYPE = Callable[['StateMachine', Hashable, Optional['State']], None] @@ -131,9 +130,12 @@ def transition(self: Any, *a: Any, **kw: Any) -> Any: @runtime_checkable class State(Protocol): - LABEL: ClassVar[LABEL_TYPE] + LABEL: ClassVar[Any] + ALLOWED: ClassVar[set[Any]] is_terminal: ClassVar[bool] + def __init__(self, *args: Any, **kwargs: Any): ... + def enter(self) -> None: ... def exit(self) -> None: ... @@ -146,7 +148,6 @@ def interrupt(self, reason: Exception) -> None: ... @runtime_checkable class Proceedable(Protocol): - def execute(self) -> State | None: """ Execute the state, performing the actions that this state is responsible for. @@ -155,6 +156,14 @@ def execute(self) -> State | None: ... +def create_state(st: StateMachine, state_label: Hashable, *args: Any, **kwargs: Any) -> State: + if state_label not in st.get_states_map(): + raise ValueError(f'{state_label} is not a valid state') + + state_cls = st.get_states_map()[state_label] + return state_cls(*args, **kwargs) + + class StateEventHook(enum.Enum): """ Hooks that can be used to register callback at various points in the state transition @@ -203,13 +212,13 @@ def get_states(cls) -> Sequence[Type[State]]: raise RuntimeError('States not defined') @classmethod - def initial_state_label(cls) -> LABEL_TYPE: + def initial_state_label(cls) -> Any: cls.__ensure_built() assert cls.STATES is not None return cls.STATES[0].LABEL @classmethod - def get_state_class(cls, label: LABEL_TYPE) -> Type[State]: + def get_state_class(cls, label: Any) -> Type[State]: cls.__ensure_built() assert cls._STATES_MAP is not None return cls._STATES_MAP[label] @@ -253,11 +262,11 @@ def init(self) -> None: def __str__(self) -> str: return f'<{self.__class__.__name__}> ({self.state})' - def create_initial_state(self) -> State: - return self.get_state_class(self.initial_state_label())(self) + def create_initial_state(self, *args: Any, **kwargs: Any) -> State: + return self.get_state_class(self.initial_state_label())(self, *args, **kwargs) @property - def state(self) -> Optional[LABEL_TYPE]: + def state(self) -> Any: if self._state is None: return None return self._state.LABEL @@ -297,6 +306,7 @@ def transition_to(self, new_state: State | None, **kwargs: Any) -> None: The arguments are passed to the state class to create state instance. (process arg does not need to pass since it will always call with 'self' as process) """ + print(f'try: {self._state} -> {new_state}') assert not self._transitioning, 'Cannot call transition_to when already transitioning state' if new_state is None: @@ -355,17 +365,6 @@ def get_debug(self) -> bool: def set_debug(self, enabled: bool) -> None: self._debug: bool = enabled - def create_state(self, state_label: Hashable, *args: Any, **kwargs: Any) -> State: - # XXX: this method create state from label, which is duplicate as _create_state_instance and less generic - # because the label is defined after the state and required to be know before calling this function. - # This method should be replaced by `_create_state_instance`. - # aiida-core using this method for its Waiting state override. - try: - state_cls = self.get_states_map()[state_label] - return state_cls(self, *args, **kwargs) - except KeyError: - raise ValueError(f'{state_label} is not a valid state') - def _exit_current_state(self, next_state: State) -> None: """Exit the given state""" @@ -377,7 +376,7 @@ def _exit_current_state(self, next_state: State) -> None: return # Nothing to exit if next_state.LABEL not in self._state.ALLOWED: - raise RuntimeError(f'Cannot transition from {self._state.LABEL} to {next_state.label}') + raise RuntimeError(f'Cannot transition from {self._state.LABEL} to {next_state.LABEL}') self._fire_state_event(StateEventHook.EXITING_STATE, next_state) self._state.exit() @@ -388,11 +387,3 @@ def _enter_next_state(self, next_state: State) -> None: next_state.enter() self._state = next_state self._fire_state_event(StateEventHook.ENTERED_STATE, last_state) - - def _create_state_instance(self, state_cls: Hashable, **kwargs: Any) -> State: - if state_cls not in self.get_states_map(): - raise ValueError(f'{state_cls} is not a valid state') - - cls = self.get_states_map()[state_cls] - - return cls(self, **kwargs) diff --git a/src/plumpy/process_states.py b/src/plumpy/process_states.py index c82c34a3..e5065203 100644 --- a/src/plumpy/process_states.py +++ b/src/plumpy/process_states.py @@ -5,7 +5,21 @@ import traceback from enum import Enum from types import TracebackType -from typing import TYPE_CHECKING, Any, Awaitable, Callable, Optional, Tuple, Type, Union, cast +from typing import ( + TYPE_CHECKING, + Any, + Awaitable, + Callable, + ClassVar, + Optional, + Protocol, + Tuple, + Type, + Union, + cast, + final, + runtime_checkable, +) import yaml from yaml.loader import Loader @@ -20,9 +34,9 @@ _HAS_TBLIB = False from . import exceptions, futures, persistence, utils -from .base import state_machine +from .base import state_machine as st from .lang import NULL -from .persistence import auto_persist +from .persistence import LoadSaveContext, auto_persist from .utils import SAVED_STATE_TYPE, ensure_coroutine if TYPE_CHECKING: @@ -123,22 +137,28 @@ class ProcessState(Enum): The possible states that a :class:`~plumpy.processes.Process` can be in. """ - CREATED: str = 'created' - RUNNING: str = 'running' - WAITING: str = 'waiting' - FINISHED: str = 'finished' - EXCEPTED: str = 'excepted' - KILLED: str = 'killed' + # FIXME: see LSP error of return a exception, the type is Literal[str] which is invariant, tricky + CREATED = 'created' + RUNNING = 'running' + WAITING = 'waiting' + FINISHED = 'finished' + EXCEPTED = 'excepted' + KILLED = 'killed' + + +@runtime_checkable +class Savable(Protocol): + def save(self, save_context: LoadSaveContext | None = None) -> SAVED_STATE_TYPE: ... @final -@auto_persist('args', 'kwargs', 'in_state') -class Created(state_machine.State, persistence.Savable): - LABEL = ProcessState.CREATED - ALLOWED = {ProcessState.RUNNING, ProcessState.KILLED, ProcessState.EXCEPTED} +@auto_persist('args', 'kwargs') +class Created(persistence.Savable): + LABEL: ClassVar = ProcessState.CREATED + ALLOWED: ClassVar = {ProcessState.RUNNING, ProcessState.KILLED, ProcessState.EXCEPTED} RUN_FN = 'run_fn' - is_terminal = False + is_terminal: ClassVar[bool] = False def __init__(self, process: 'Process', run_fn: Callable[..., Any], *args: Any, **kwargs: Any) -> None: assert run_fn is not None @@ -146,7 +166,6 @@ def __init__(self, process: 'Process', run_fn: Callable[..., Any], *args: Any, * self.run_fn = run_fn self.args = args self.kwargs = kwargs - self.in_state = True def save_instance_state(self, out_state: SAVED_STATE_TYPE, save_context: persistence.LoadSaveContext) -> None: super().save_instance_state(out_state, save_context) @@ -158,24 +177,21 @@ def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persi self.run_fn = getattr(self.process, saved_state[self.RUN_FN]) - async def execute(self) -> state_machine.State: - return self.process.create_state(ProcessState.RUNNING, self.run_fn, *self.args, **self.kwargs) - - def enter(self) -> None: - self.in_state = True + def execute(self) -> st.State: + return st.create_state( + self.process, ProcessState.RUNNING, process=self.process, run_fn=self.run_fn, *self.args, **self.kwargs + ) - def exit(self) -> None: - if self.is_terminal: - raise exceptions.InvalidStateError(f'Cannot exit a terminal state {self.LABEL}') + def enter(self) -> None: ... - self.in_state = False + def exit(self) -> None: ... @final -@auto_persist('args', 'kwargs', 'in_state') -class Running(state_machine.State, persistence.Savable): - LABEL = ProcessState.RUNNING - ALLOWED = { +@auto_persist('args', 'kwargs') +class Running(persistence.Savable): + LABEL: ClassVar = ProcessState.RUNNING + ALLOWED: ClassVar = { ProcessState.RUNNING, ProcessState.WAITING, ProcessState.FINISHED, @@ -191,18 +207,17 @@ class Running(state_machine.State, persistence.Savable): _running: bool = False _run_handle = None - is_terminal = False + is_terminal: ClassVar[bool] = False def __init__( self, process: 'Process', run_fn: Callable[..., Union[Awaitable[Any], Any]], *args: Any, **kwargs: Any ) -> None: assert run_fn is not None self.process = process - self.run_fn = run_fn + self.run_fn = ensure_coroutine(run_fn) self.args = args self.kwargs = kwargs self._run_handle = None - self.in_state = False def save_instance_state(self, out_state: SAVED_STATE_TYPE, save_context: persistence.LoadSaveContext) -> None: super().save_instance_state(out_state, save_context) @@ -221,7 +236,7 @@ def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persi def interrupt(self, reason: Any) -> None: pass - async def execute(self) -> state_machine.State: + async def execute(self) -> st.State: if self._command is not None: command = self._command else: @@ -235,8 +250,10 @@ async def execute(self) -> state_machine.State: # Let this bubble up to the caller raise except Exception: - excepted = self.process.create_state(ProcessState.EXCEPTED, *sys.exc_info()[1:]) - return cast(state_machine.State, excepted) + _, exception, traceback = sys.exc_info() + # excepted = state_cls(exception=exception, traceback=traceback) + excepted = Excepted(exception=exception, traceback=traceback) + return excepted else: if not isinstance(result, Command): if isinstance(result, exceptions.UnsuccessfulResult): @@ -245,42 +262,52 @@ async def execute(self) -> state_machine.State: # Got passed a basic return type result = Stop(result, True) - command = result + command = cast(Stop, result) next_state = self._action_command(command) return next_state - def _action_command(self, command: Union[Kill, Stop, Wait, Continue]) -> state_machine.State: + def _action_command(self, command: Union[Kill, Stop, Wait, Continue]) -> st.State: if isinstance(command, Kill): - state = self.process.create_state(ProcessState.KILLED, command.msg) + state = st.create_state(self.process, ProcessState.KILLED, msg=command.msg) # elif isinstance(command, Pause): # self.pause() elif isinstance(command, Stop): - state = self.process.create_state(ProcessState.FINISHED, command.result, command.successful) + state = st.create_state( + self.process, ProcessState.FINISHED, result=command.result, successful=command.successful + ) elif isinstance(command, Wait): - state = self.process.create_state(ProcessState.WAITING, command.continue_fn, command.msg, command.data) + state = st.create_state( + self.process, + ProcessState.WAITING, + process=self.process, + done_callback=command.continue_fn, + msg=command.msg, + data=command.data, + ) elif isinstance(command, Continue): - state = self.process.create_state(ProcessState.RUNNING, command.continue_fn, *command.args) + state = st.create_state( + self.process, + ProcessState.RUNNING, + process=self.process, + run_fn=command.continue_fn, + *command.args, + **command.kwargs, + ) else: raise ValueError('Unrecognised command') - return cast(state_machine.State, state) # casting from base.State to process.State + return state - def enter(self) -> None: - self.in_state = True + def enter(self) -> None: ... - def exit(self) -> None: - if self.is_terminal: - raise exceptions.InvalidStateError(f'Cannot exit a terminal state {self.LABEL}') + def exit(self) -> None: ... - self.in_state = False - -@final -@auto_persist('msg', 'data', 'in_state') -class Waiting(state_machine.State, persistence.Savable): - LABEL = ProcessState.WAITING - ALLOWED = { +@auto_persist('msg', 'data') +class Waiting(persistence.Savable): + LABEL: ClassVar = ProcessState.WAITING + ALLOWED: ClassVar = { ProcessState.RUNNING, ProcessState.WAITING, ProcessState.KILLED, @@ -292,7 +319,7 @@ class Waiting(state_machine.State, persistence.Savable): _interruption = None - is_terminal = False + is_terminal: ClassVar[bool] = False def __str__(self) -> str: state_info = super().__str__() @@ -312,7 +339,6 @@ def __init__( self.msg = msg self.data = data self._waiting_future: futures.Future = futures.Future() - self.in_state = False def save_instance_state(self, out_state: SAVED_STATE_TYPE, save_context: persistence.LoadSaveContext) -> None: super().save_instance_state(out_state, save_context) @@ -334,7 +360,7 @@ def interrupt(self, reason: Exception) -> None: # This will cause the future in execute() to raise the exception self._waiting_future.set_exception(reason) - async def execute(self) -> state_machine.State: # type: ignore + async def execute(self) -> st.State: try: result = await self._waiting_future except Interruption: @@ -345,11 +371,15 @@ async def execute(self) -> state_machine.State: # type: ignore raise if result == NULL: - next_state = self.process.create_state(ProcessState.RUNNING, self.done_callback) + next_state = st.create_state( + self.process, ProcessState.RUNNING, process=self.process, run_fn=self.done_callback + ) else: - next_state = self.process.create_state(ProcessState.RUNNING, self.done_callback, result) + next_state = st.create_state( + self.process, ProcessState.RUNNING, process=self.process, done_callback=self.done_callback, *result + ) - return cast(state_machine.State, next_state) # casting from base.State to process.State + return next_state def resume(self, value: Any = NULL) -> None: assert self._waiting_future is not None, 'Not yet waiting' @@ -359,47 +389,39 @@ def resume(self, value: Any = NULL) -> None: self._waiting_future.set_result(value) - def enter(self) -> None: - self.in_state = True + def enter(self) -> None: ... - def exit(self) -> None: - if self.is_terminal: - raise exceptions.InvalidStateError(f'Cannot exit a terminal state {self.LABEL}') + def exit(self) -> None: ... - self.in_state = False - -@auto_persist('in_state') -class Excepted(state_machine.State, persistence.Savable): +@final +class Excepted(persistence.Savable): """ - Excepted state, can optionally provide exception and trace_back + Excepted state, can optionally provide exception and traceback :param exception: The exception instance - :param trace_back: An optional exception traceback + :param traceback: An optional exception traceback """ - LABEL = ProcessState.EXCEPTED + LABEL: ClassVar = ProcessState.EXCEPTED + ALLOWED: ClassVar[set[str]] = set() EXC_VALUE = 'ex_value' TRACEBACK = 'traceback' - is_terminal = True + is_terminal: ClassVar = True def __init__( self, - process: 'Process', exception: Optional[BaseException], - trace_back: Optional[TracebackType] = None, + traceback: Optional[TracebackType] = None, ): """ - :param process: The associated process :param exception: The exception instance - :param trace_back: An optional exception traceback + :param traceback: An optional exception traceback """ - self.process = process self.exception = exception - self.traceback = trace_back - self.in_state = False + self.traceback = traceback def __str__(self) -> str: exception = traceback.format_exception_only(type(self.exception) if self.exception else None, self.exception)[0] @@ -413,7 +435,6 @@ def save_instance_state(self, out_state: SAVED_STATE_TYPE, save_context: persist def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: super().load_instance_state(saved_state, load_context) - self.process = load_context.process self.exception = yaml.load(saved_state[self.EXC_VALUE], Loader=Loader) if _HAS_TBLIB: @@ -436,50 +457,40 @@ def get_exc_info( self.traceback, ) - def enter(self) -> None: - self.in_state = True + def enter(self) -> None: ... - def exit(self) -> None: - if self.is_terminal: - raise exceptions.InvalidStateError(f'Cannot exit a terminal state {self.LABEL}') + def exit(self) -> None: ... - self.in_state = False - -@auto_persist('result', 'successful', 'in_state') -class Finished(state_machine.State, persistence.Savable): +@final +@auto_persist('result', 'successful') +class Finished(persistence.Savable): """State for process is finished. :param result: The result of process :param successful: Boolean for the exit code is ``0`` the process is successful. """ - LABEL = ProcessState.FINISHED + LABEL: ClassVar = ProcessState.FINISHED + ALLOWED: ClassVar[set[str]] = set() - is_terminal = True + is_terminal: ClassVar[bool] = True - def __init__(self, process: 'Process', result: Any, successful: bool) -> None: - self.process = process + def __init__(self, result: Any, successful: bool) -> None: self.result = result self.successful = successful - self.in_state = False def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: super().load_instance_state(saved_state, load_context) - self.process = load_context.process - def enter(self) -> None: - self.in_state = True + def enter(self) -> None: ... - def exit(self) -> None: - if self.is_terminal: - raise exceptions.InvalidStateError(f'Cannot exit a terminal state {self.LABEL}') + def exit(self) -> None: ... - self.in_state = False - -@auto_persist('msg', 'in_state') -class Killed(state_machine.State, persistence.Savable): +@final +@auto_persist('msg') +class Killed(persistence.Savable): """ Represents a state where a process has been killed. @@ -489,30 +500,23 @@ class Killed(state_machine.State, persistence.Savable): :param msg: An optional message explaining the reason for the process termination. """ - LABEL = ProcessState.KILLED + LABEL: ClassVar = ProcessState.KILLED + ALLOWED: ClassVar[set[str]] = set() - is_terminal = True + is_terminal: ClassVar[bool] = True - def __init__(self, process: 'Process', msg: Optional[MessageType]): + def __init__(self, msg: Optional[MessageType]): """ - :param process: The associated process :param msg: Optional kill message """ - self.process = process self.msg = msg def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: super().load_instance_state(saved_state, load_context) - self.process = load_context.process - - def enter(self) -> None: - self.in_state = True - def exit(self) -> None: - if self.is_terminal: - raise exceptions.InvalidStateError(f'Cannot exit a terminal state {self.LABEL}') + def enter(self) -> None: ... - self.in_state = False + def exit(self) -> None: ... # endregion diff --git a/src/plumpy/processes.py b/src/plumpy/processes.py index c6f45b03..45c64024 100644 --- a/src/plumpy/processes.py +++ b/src/plumpy/processes.py @@ -53,7 +53,7 @@ StateEntryFailed, StateMachine, StateMachineError, - TransitionFailed, + create_state, event, ) from .base.utils import call_with_super_check, super_check @@ -189,7 +189,7 @@ def get_states(cls) -> Sequence[Type[state_machine.State]]: ) @classmethod - def get_state_classes(cls) -> Dict[Hashable, Type[state_machine.State]]: + def get_state_classes(cls) -> dict[process_states.ProcessState, Type[state_machine.State]]: # A mapping of the State constants to the corresponding state class return { process_states.ProcessState.CREATED: process_states.Created, @@ -629,7 +629,9 @@ def save_instance_state( """ super().save_instance_state(out_state, save_context) - out_state['_state'] = self._state.save() + # FIXME: the combined ProcessState protocol should cover the case + if isinstance(self._state, process_states.Savable): + out_state['_state'] = self._state.save() # Inputs/outputs if self.raw_inputs is not None: @@ -875,7 +877,7 @@ def on_finish(self, result: Any, successful: bool) -> None: validation_error = self.spec().outputs.validate(self.outputs) if validation_error: state_cls = self.get_states_map()[process_states.ProcessState.FINISHED] - finished_state = state_cls(self, result=result, successful=False) + finished_state = state_cls(result=result, successful=False) raise StateEntryFailed(finished_state) self.future().set_result(self.outputs) @@ -1108,9 +1110,8 @@ def transition_failed( if final_state == process_states.ProcessState.CREATED: raise exception.with_traceback(trace) - new_state = self._create_state_instance( - process_states.ProcessState.EXCEPTED, exception=exception, trace_back=trace - ) + # state_class = self.get_states_map()[process_states.ProcessState.EXCEPTED] + new_state = create_state(self, process_states.ProcessState.EXCEPTED, exception=exception, traceback=trace) self.transition_to(new_state) def pause(self, msg_text: str | None = None) -> Union[bool, CancellableAction]: @@ -1190,9 +1191,11 @@ def _create_interrupt_action(self, exception: process_states.Interruption) -> Ca def do_kill(_next_state: state_machine.State) -> Any: try: - new_state = self._create_state_instance(process_states.ProcessState.KILLED, msg=exception.msg) + new_state = create_state(self, process_states.ProcessState.KILLED, msg=exception.msg) self.transition_to(new_state) return True + # FIXME: if try block except, will hit deadlock in event loop + # need to know how to debug it, and where to set a timeout. finally: self._killing = None @@ -1237,15 +1240,14 @@ def resume(self, *args: Any) -> None: return self._state.resume(*args) # type: ignore @event(to_states=process_states.Excepted) - def fail(self, exception: Optional[BaseException], trace_back: Optional[TracebackType]) -> None: + def fail(self, exception: Optional[BaseException], traceback: Optional[TracebackType]) -> None: """ Fail the process in response to an exception :param exception: The exception that caused the failure - :param trace_back: Optional exception traceback + :param traceback: Optional exception traceback """ - new_state = self._create_state_instance( - process_states.ProcessState.EXCEPTED, exception=exception, trace_back=trace_back - ) + # state_class = self.get_states_map()[process_states.ProcessState.EXCEPTED] + new_state = create_state(self, process_states.ProcessState.EXCEPTED, exception=exception, traceback=traceback) self.transition_to(new_state) def kill(self, msg_text: Optional[str] = None) -> Union[bool, asyncio.Future]: @@ -1265,7 +1267,7 @@ def kill(self, msg_text: Optional[str] = None) -> Union[bool, asyncio.Future]: # Already killing return self._killing - if self._stepping: + if self._stepping and isinstance(self._state, Interruptable): # Ask the step function to pause by setting this flag and giving the # caller back a future interrupt_exception = process_states.KillInterruption(msg_text) @@ -1275,7 +1277,7 @@ def kill(self, msg_text: Optional[str] = None) -> Union[bool, asyncio.Future]: return cast(CancellableAction, self._interrupt_action) msg = MessageBuilder.kill(msg_text) - new_state = self._create_state_instance(process_states.ProcessState.KILLED, msg=msg) + new_state = create_state(self, process_states.ProcessState.KILLED, msg=msg) self.transition_to(new_state) return True @@ -1293,10 +1295,7 @@ def create_initial_state(self) -> state_machine.State: :return: A Created state """ - return cast( - state_machine.State, - self.get_state_class(process_states.ProcessState.CREATED)(self, self.run), - ) + return self.get_state_class(process_states.ProcessState.CREATED)(self, self.run) def recreate_state(self, saved_state: persistence.Bundle) -> state_machine.State: """ @@ -1368,7 +1367,9 @@ async def step(self) -> None: raise except Exception: # Overwrite the next state to go to excepted directly - next_state = self.create_state(process_states.ProcessState.EXCEPTED, *sys.exc_info()[1:]) + next_state = create_state( + self, process_states.ProcessState.EXCEPTED, exception=sys.exc_info()[1], traceback=sys.exc_info()[2] + ) self._set_interrupt_action(None) if self._interrupt_action: diff --git a/src/plumpy/workchains.py b/src/plumpy/workchains.py index f6369c28..cf5108c8 100644 --- a/src/plumpy/workchains.py +++ b/src/plumpy/workchains.py @@ -11,7 +11,6 @@ Any, Callable, Dict, - Hashable, List, Mapping, MutableSequence, @@ -23,9 +22,8 @@ cast, ) -from plumpy.coordinator import Coordinator - from plumpy.base import state_machine +from plumpy.coordinator import Coordinator from plumpy.exceptions import InvalidStateError from . import lang, mixins, persistence, process_spec, process_states, processes @@ -69,6 +67,7 @@ def get_outline(self) -> Union['_Instruction', '_FunctionCall']: return self._outline +# FIXME: better use composition here @persistence.auto_persist('_awaiting') class Waiting(process_states.Waiting): """Overwrite the waiting state""" @@ -78,11 +77,11 @@ def __init__( process: 'WorkChain', done_callback: Optional[Callable[..., Any]], msg: Optional[str] = None, - awaiting: Optional[Dict[Union[asyncio.Future, processes.Process], str]] = None, + data: Optional[Dict[Union[asyncio.Future, processes.Process], str]] = None, ) -> None: - super().__init__(process, done_callback, msg, awaiting) + super().__init__(process, done_callback, msg, data) self._awaiting: Dict[asyncio.Future, str] = {} - for awaitable, key in (awaiting or {}).items(): + for awaitable, key in (data or {}).items(): resolved_awaitable = awaitable.future() if isinstance(awaitable, processes.Process) else awaitable self._awaiting[resolved_awaitable] = key @@ -122,7 +121,7 @@ class WorkChain(mixins.ContextMixin, processes.Process): _CONTEXT = 'CONTEXT' @classmethod - def get_state_classes(cls) -> Dict[Hashable, Type[state_machine.State]]: + def get_state_classes(cls) -> Dict[process_states.ProcessState, Type[state_machine.State]]: states_map = super().get_state_classes() states_map[process_states.ProcessState.WAITING] = Waiting return states_map diff --git a/tests/base/test_statemachine.py b/tests/base/test_statemachine.py index 07a2dc80..f046aaa8 100644 --- a/tests/base/test_statemachine.py +++ b/tests/base/test_statemachine.py @@ -17,7 +17,7 @@ STOPPED = 'Stopped' -class Playing(state_machine.State): +class Playing: LABEL = PLAYING ALLOWED = {PAUSED, STOPPED} TRANSITIONS = {STOP: STOPPED} @@ -56,7 +56,7 @@ def exit(self) -> None: self.in_state = False -class Paused(state_machine.State): +class Paused: LABEL = PAUSED ALLOWED = {PLAYING, STOPPED} TRANSITIONS = {STOP: STOPPED} @@ -65,7 +65,6 @@ class Paused(state_machine.State): def __init__(self, player, playing_state): assert isinstance(playing_state, Playing), 'Must provide the playing state to pause' - super().__init__(player) self._player = player self.playing_state = playing_state @@ -74,9 +73,9 @@ def __str__(self): def play(self, track=None): if track is not None: - self.state_machine.transition_to(Playing(player=self.state_machine, track=track)) + self._player.transition_to(Playing(player=self.state_machine, track=track)) else: - self.state_machine.transition_to(self.playing_state) + self._player.transition_to(self.playing_state) def enter(self) -> None: self.in_state = True @@ -88,7 +87,7 @@ def exit(self) -> None: self.in_state = False -class Stopped(state_machine.State): +class Stopped: LABEL = STOPPED ALLOWED = { PLAYING, @@ -98,13 +97,13 @@ class Stopped(state_machine.State): is_terminal = False def __init__(self, player): - self.state_machine = player + self._player = player def __str__(self): return '[]' def play(self, track): - self.state_machine.transition_to(Playing(self.state_machine, track=track)) + self._player.transition_to(Playing(self._player, track=track)) def enter(self) -> None: self.in_state = True From 98f46bb9300b6bc945dfc983e9b8f6765aff1789 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Wed, 4 Dec 2024 16:28:43 +0100 Subject: [PATCH 26/64] To lenthy for rethinking --- src/plumpy/persistence.py | 82 ++++++++++++++++-------------------- src/plumpy/process_states.py | 1 - 2 files changed, 37 insertions(+), 46 deletions(-) diff --git a/src/plumpy/persistence.py b/src/plumpy/persistence.py index f7cbad44..4bc33158 100644 --- a/src/plumpy/persistence.py +++ b/src/plumpy/persistence.py @@ -420,28 +420,6 @@ class Savable: _auto_persist: Optional[Set[str]] = None _persist_configured = False - @staticmethod - def load(saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> 'Savable': - """ - Load a `Savable` from a saved instance state. The load context is a way of passing - runtime data to the object being loaded. - - :param saved_state: The saved state - :param load_context: Additional runtime state that can be passed into when loading. - The type and content (if any) is completely user defined - :return: The loaded Savable instance - - """ - load_context = _ensure_object_loader(load_context, saved_state) - assert load_context.loader is not None # required for type checking - try: - class_name = Savable._get_class_name(saved_state) - load_cls = load_context.loader.load_object(class_name) - except KeyError: - raise ValueError('Class name not found in saved state') - else: - return load_cls.recreate_from(saved_state, load_context) - @classmethod def auto_persist(cls, *members: str) -> None: if cls._auto_persist is None: @@ -472,13 +450,48 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext]) -> None: self._ensure_persist_configured() if self._auto_persist is not None: - self.load_members(self._auto_persist, saved_state, load_context) + for member in self._auto_persist: + setattr(self, member, self._get_value(saved_state, member, load_context)) + + @staticmethod + def load(saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> 'Savable': + """ + Load a `Savable` from a saved instance state. The load context is a way of passing + runtime data to the object being loaded. + + :param saved_state: The saved state + :param load_context: Additional runtime state that can be passed into when loading. + The type and content (if any) is completely user defined + :return: The loaded Savable instance + + """ + load_context = _ensure_object_loader(load_context, saved_state) + assert load_context.loader is not None # required for type checking + try: + class_name = Savable._get_class_name(saved_state) + load_cls = load_context.loader.load_object(class_name) + except KeyError: + raise ValueError('Class name not found in saved state') + else: + return load_cls.recreate_from(saved_state, load_context) @super_check def save_instance_state(self, out_state: SAVED_STATE_TYPE, save_context: Optional[LoadSaveContext]) -> None: self._ensure_persist_configured() if self._auto_persist is not None: - self.save_members(self._auto_persist, out_state) + for member in self._auto_persist: + value = getattr(self, member) + if inspect.ismethod(value): + if value.__self__ is not self: + raise TypeError('Cannot persist methods of other classes') + Savable._set_meta_type(out_state, member, META__TYPE__METHOD) + value = value.__name__ + elif isinstance(value, Savable): + Savable._set_meta_type(out_state, member, META__TYPE__SAVABLE) + value = value.save() + else: + value = copy.deepcopy(value) + out_state[member] = value def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TYPE: out_state: SAVED_STATE_TYPE = {} @@ -501,27 +514,6 @@ def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TY call_with_super_check(self.save_instance_state, out_state, save_context) return out_state - def save_members(self, members: Iterable[str], out_state: SAVED_STATE_TYPE) -> None: - for member in members: - value = getattr(self, member) - if inspect.ismethod(value): - if value.__self__ is not self: - raise TypeError('Cannot persist methods of other classes') - Savable._set_meta_type(out_state, member, META__TYPE__METHOD) - value = value.__name__ - elif isinstance(value, Savable): - Savable._set_meta_type(out_state, member, META__TYPE__SAVABLE) - value = value.save() - else: - value = copy.deepcopy(value) - out_state[member] = value - - def load_members( - self, members: Iterable[str], saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None - ) -> None: - for member in members: - setattr(self, member, self._get_value(saved_state, member, load_context)) - def _ensure_persist_configured(self) -> None: if not self._persist_configured: self.persist() diff --git a/src/plumpy/process_states.py b/src/plumpy/process_states.py index e5065203..acb83931 100644 --- a/src/plumpy/process_states.py +++ b/src/plumpy/process_states.py @@ -137,7 +137,6 @@ class ProcessState(Enum): The possible states that a :class:`~plumpy.processes.Process` can be in. """ - # FIXME: see LSP error of return a exception, the type is Literal[str] which is invariant, tricky CREATED = 'created' RUNNING = 'running' WAITING = 'waiting' From a6705bdf5fec7ec3eb97d54657088e74c91eb842 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Wed, 4 Dec 2024 23:33:13 +0100 Subject: [PATCH 27/64] Move static method load outside --- src/plumpy/persistence.py | 102 +++++++++++++++++++------------------- src/plumpy/processes.py | 2 +- 2 files changed, 52 insertions(+), 52 deletions(-) diff --git a/src/plumpy/persistence.py b/src/plumpy/persistence.py index 4bc33158..fe761e1d 100644 --- a/src/plumpy/persistence.py +++ b/src/plumpy/persistence.py @@ -23,8 +23,33 @@ from .processes import Process +class LoadSaveContext: + def __init__(self, loader: Optional[loaders.ObjectLoader] = None, **kwargs: Any) -> None: + self._values = dict(**kwargs) + self.loader = loader + + def __getattr__(self, item: str) -> Any: + try: + return self._values[item] + except KeyError: + raise AttributeError(f"item '{item}' not found") + + def __iter__(self) -> Iterable[Any]: + return self._value.__iter__() + + def __contains__(self, item: Any) -> bool: + return self._values.__contains__(item) + + def copyextend(self, **kwargs: Any) -> 'LoadSaveContext': + """Add additional information to the context by making a copy with the new values""" + extended = self._values.copy() + extended.update(kwargs) + loader = extended.pop('loader', self.loader) + return LoadSaveContext(loader=loader, **extended) + + class Bundle(dict): - def __init__(self, savable: 'Savable', save_context: Optional['LoadSaveContext'] = None, dereference: bool = False): + def __init__(self, savable: 'Savable', save_context: LoadSaveContext | None = None, dereference: bool = False): """ Create a bundle from a savable. Optionally keep information about the class loader that can be used to load the classes in the bundle. @@ -40,7 +65,7 @@ class loader that can be used to load the classes in the bundle. else: self.update(savable.save(save_context)) - def unbundle(self, load_context: Optional['LoadSaveContext'] = None) -> 'Savable': + def unbundle(self, load_context: LoadSaveContext | None = None) -> 'Savable': """ This method loads the class of the object and calls its recreate_from method passing the positional and keyword arguments. @@ -49,7 +74,29 @@ def unbundle(self, load_context: Optional['LoadSaveContext'] = None) -> 'Savable :return: An instance of the Savable """ - return Savable.load(self, load_context) + return load(self, load_context) + + +def load(saved_state: SAVED_STATE_TYPE, load_context: LoadSaveContext | None = None) -> 'Savable': + """ + Load a `Savable` from a saved instance state. The load context is a way of passing + runtime data to the object being loaded. + + :param saved_state: The saved state + :param load_context: Additional runtime state that can be passed into when loading. + The type and content (if any) is completely user defined + :return: The loaded Savable instance + + """ + load_context = _ensure_object_loader(load_context, saved_state) + assert load_context.loader is not None # required for type checking + try: + class_name = Savable._get_class_name(saved_state) + load_cls = load_context.loader.load_object(class_name) + except KeyError: + raise ValueError('Class name not found in saved state') + else: + return load_cls.recreate_from(saved_state, load_context) _BUNDLE_TAG = '!plumpy:Bundle' @@ -380,31 +427,6 @@ def _ensure_object_loader(context: Optional['LoadSaveContext'], saved_state: SAV return context.copyextend(loader=loader) -class LoadSaveContext: - def __init__(self, loader: Optional[loaders.ObjectLoader] = None, **kwargs: Any) -> None: - self._values = dict(**kwargs) - self.loader = loader - - def __getattr__(self, item: str) -> Any: - try: - return self._values[item] - except KeyError: - raise AttributeError(f"item '{item}' not found") - - def __iter__(self) -> Iterable[Any]: - return self._value.__iter__() - - def __contains__(self, item: Any) -> bool: - return self._values.__contains__(item) - - def copyextend(self, **kwargs: Any) -> 'LoadSaveContext': - """Add additional information to the context by making a copy with the new values""" - extended = self._values.copy() - extended.update(kwargs) - loader = extended.pop('loader', self.loader) - return LoadSaveContext(loader=loader, **extended) - - META: str = '!!meta' META__CLASS_NAME: str = 'class_name' META__OBJECT_LOADER: str = 'object_loader' @@ -453,28 +475,6 @@ def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: Optio for member in self._auto_persist: setattr(self, member, self._get_value(saved_state, member, load_context)) - @staticmethod - def load(saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> 'Savable': - """ - Load a `Savable` from a saved instance state. The load context is a way of passing - runtime data to the object being loaded. - - :param saved_state: The saved state - :param load_context: Additional runtime state that can be passed into when loading. - The type and content (if any) is completely user defined - :return: The loaded Savable instance - - """ - load_context = _ensure_object_loader(load_context, saved_state) - assert load_context.loader is not None # required for type checking - try: - class_name = Savable._get_class_name(saved_state) - load_cls = load_context.loader.load_object(class_name) - except KeyError: - raise ValueError('Class name not found in saved state') - else: - return load_cls.recreate_from(saved_state, load_context) - @super_check def save_instance_state(self, out_state: SAVED_STATE_TYPE, save_context: Optional[LoadSaveContext]) -> None: self._ensure_persist_configured() @@ -568,7 +568,7 @@ def _get_value( if typ == META__TYPE__METHOD: value = getattr(self, value) elif typ == META__TYPE__SAVABLE: - value = Savable.load(value, load_context) + value = load(value, load_context) return value diff --git a/src/plumpy/processes.py b/src/plumpy/processes.py index 45c64024..d088a5f8 100644 --- a/src/plumpy/processes.py +++ b/src/plumpy/processes.py @@ -1305,7 +1305,7 @@ def recreate_state(self, saved_state: persistence.Bundle) -> state_machine.State :return: An instance of the object with its state loaded from the save state. """ load_context = persistence.LoadSaveContext(process=self) - return cast(state_machine.State, persistence.Savable.load(saved_state, load_context)) + return cast(state_machine.State, persistence.load(saved_state, load_context)) # endregion From eefd7b77dede80af512d339c7d841c9f1adbfeb9 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Mon, 9 Dec 2024 11:01:32 +0100 Subject: [PATCH 28/64] save_instance_state simplify to only has save interface For the auto_persist attributes, the fn auto_save will take care of save the state --- src/plumpy/mixins.py | 13 ------ src/plumpy/persistence.py | 82 ++++++++++++++++++--------------- src/plumpy/process_states.py | 44 ++++++++++++------ src/plumpy/processes.py | 13 +++--- src/plumpy/workchains.py | 89 +++++++++++++++++++++++++++++++----- tests/test_processes.py | 2 +- 6 files changed, 157 insertions(+), 86 deletions(-) diff --git a/src/plumpy/mixins.py b/src/plumpy/mixins.py index 4b993dac..0e3bb0c0 100644 --- a/src/plumpy/mixins.py +++ b/src/plumpy/mixins.py @@ -21,19 +21,6 @@ def __init__(self, *args: Any, **kwargs: Any): def ctx(self) -> Optional[AttributesDict]: return self._context - def save_instance_state( - self, out_state: SAVED_STATE_TYPE, save_context: Optional[persistence.LoadSaveContext] - ) -> None: - """Add the instance state to ``out_state``. - .. important:: - - The instance state will contain a pointer to the ``ctx``, - and so should be deep copied or serialised before persisting. - """ - super().save_instance_state(out_state, save_context) - if self._context is not None: - out_state[self.CONTEXT] = self._context.__dict__ - def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: super().load_instance_state(saved_state, load_context) try: diff --git a/src/plumpy/persistence.py b/src/plumpy/persistence.py index fe761e1d..389acc27 100644 --- a/src/plumpy/persistence.py +++ b/src/plumpy/persistence.py @@ -92,7 +92,7 @@ def load(saved_state: SAVED_STATE_TYPE, load_context: LoadSaveContext | None = N assert load_context.loader is not None # required for type checking try: class_name = Savable._get_class_name(saved_state) - load_cls = load_context.loader.load_object(class_name) + load_cls: Savable = load_context.loader.load_object(class_name) except KeyError: raise ValueError('Class name not found in saved state') else: @@ -475,43 +475,9 @@ def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: Optio for member in self._auto_persist: setattr(self, member, self._get_value(saved_state, member, load_context)) - @super_check - def save_instance_state(self, out_state: SAVED_STATE_TYPE, save_context: Optional[LoadSaveContext]) -> None: - self._ensure_persist_configured() - if self._auto_persist is not None: - for member in self._auto_persist: - value = getattr(self, member) - if inspect.ismethod(value): - if value.__self__ is not self: - raise TypeError('Cannot persist methods of other classes') - Savable._set_meta_type(out_state, member, META__TYPE__METHOD) - value = value.__name__ - elif isinstance(value, Savable): - Savable._set_meta_type(out_state, member, META__TYPE__SAVABLE) - value = value.save() - else: - value = copy.deepcopy(value) - out_state[member] = value - def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TYPE: - out_state: SAVED_STATE_TYPE = {} - - if save_context is None: - save_context = LoadSaveContext() + out_state: SAVED_STATE_TYPE = auto_save(self, save_context) - utils.type_check(save_context, LoadSaveContext) - - default_loader = loaders.get_object_loader() - # If the user has specified a class loader, then save it in the saved state - if save_context.loader is not None: - loader_class = default_loader.identify_object(save_context.loader.__class__) - Savable.set_custom_meta(out_state, META__OBJECT_LOADER, loader_class) - loader = save_context.loader - else: - loader = default_loader - - Savable._set_class_name(out_state, loader.identify_object(self.__class__)) - call_with_super_check(self.save_instance_state, out_state, save_context) return out_state def _ensure_persist_configured(self) -> None: @@ -581,11 +547,13 @@ class SavableFuture(futures.Future, Savable): .. note: This does not save any assigned done callbacks. """ - def save_instance_state(self, out_state: SAVED_STATE_TYPE, save_context: LoadSaveContext) -> None: - super().save_instance_state(out_state, save_context) + def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = auto_save(self, save_context) if self.done() and self.exception() is not None: out_state['exception'] = self.exception() + return out_state + @classmethod def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> 'Savable': """ @@ -631,3 +599,41 @@ def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: LoadS # typing says asyncio.Future._callbacks needs to be called, but in the python 3.7 code it is a simple list for callback in self._callbacks: self.remove_done_callback(callback) # type: ignore[arg-type] + + +def auto_save(obj: Savable, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = {} + + if save_context is None: + save_context = LoadSaveContext() + + utils.type_check(save_context, LoadSaveContext) + + default_loader = loaders.get_object_loader() + # If the user has specified a class loader, then save it in the saved state + if save_context.loader is not None: + loader_class = default_loader.identify_object(save_context.loader.__class__) + Savable.set_custom_meta(out_state, META__OBJECT_LOADER, loader_class) + loader = save_context.loader + else: + loader = default_loader + + Savable._set_class_name(out_state, loader.identify_object(obj.__class__)) + + obj._ensure_persist_configured() + if obj._auto_persist is not None: + for member in obj._auto_persist: + value = getattr(obj, member) + if inspect.ismethod(value): + if value.__self__ is not obj: + raise TypeError('Cannot persist methods of other classes') + Savable._set_meta_type(out_state, member, META__TYPE__METHOD) + value = value.__name__ + elif isinstance(value, Savable): + Savable._set_meta_type(out_state, member, META__TYPE__SAVABLE) + value = value.save() + else: + value = copy.deepcopy(value) + out_state[member] = value + + return out_state diff --git a/src/plumpy/process_states.py b/src/plumpy/process_states.py index acb83931..1b4f610c 100644 --- a/src/plumpy/process_states.py +++ b/src/plumpy/process_states.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- from __future__ import annotations +import copy +import inspect import sys import traceback from enum import Enum @@ -36,7 +38,7 @@ from . import exceptions, futures, persistence, utils from .base import state_machine as st from .lang import NULL -from .persistence import LoadSaveContext, auto_persist +from .persistence import META__OBJECT_LOADER, META__TYPE__METHOD, META__TYPE__SAVABLE, LoadSaveContext, Savable, auto_persist, auto_save from .utils import SAVED_STATE_TYPE, ensure_coroutine if TYPE_CHECKING: @@ -113,10 +115,12 @@ def __init__(self, continue_fn: Callable[..., Any], *args: Any, **kwargs: Any): self.args = args self.kwargs = kwargs - def save_instance_state(self, out_state: SAVED_STATE_TYPE, save_context: persistence.LoadSaveContext) -> None: - super().save_instance_state(out_state, save_context) + def save(self, save_context: Optional[persistence.LoadSaveContext] = None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = persistence.auto_save(self, save_context) out_state[self.CONTINUE_FN] = self.continue_fn.__name__ + return out_state + def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: super().load_instance_state(saved_state, load_context) self.state_machine = load_context.process @@ -145,10 +149,9 @@ class ProcessState(Enum): KILLED = 'killed' -@runtime_checkable -class Savable(Protocol): - def save(self, save_context: LoadSaveContext | None = None) -> SAVED_STATE_TYPE: ... - +# @runtime_checkable +# class Savable(Protocol): +# def save(self, save_context: LoadSaveContext | None = None) -> SAVED_STATE_TYPE: ... @final @auto_persist('args', 'kwargs') @@ -166,10 +169,12 @@ def __init__(self, process: 'Process', run_fn: Callable[..., Any], *args: Any, * self.args = args self.kwargs = kwargs - def save_instance_state(self, out_state: SAVED_STATE_TYPE, save_context: persistence.LoadSaveContext) -> None: - super().save_instance_state(out_state, save_context) + def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = auto_save(self, save_context) out_state[self.RUN_FN] = self.run_fn.__name__ + return out_state + def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: super().load_instance_state(saved_state, load_context) self.process = load_context.process @@ -218,12 +223,15 @@ def __init__( self.kwargs = kwargs self._run_handle = None - def save_instance_state(self, out_state: SAVED_STATE_TYPE, save_context: persistence.LoadSaveContext) -> None: - super().save_instance_state(out_state, save_context) + def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = auto_save(self, save_context) + out_state[self.RUN_FN] = self.run_fn.__name__ if self._command is not None: out_state[self.COMMAND] = self._command.save() + return out_state + def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: super().load_instance_state(saved_state, load_context) self.process = load_context.process @@ -339,11 +347,14 @@ def __init__( self.data = data self._waiting_future: futures.Future = futures.Future() - def save_instance_state(self, out_state: SAVED_STATE_TYPE, save_context: persistence.LoadSaveContext) -> None: - super().save_instance_state(out_state, save_context) + def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = auto_save(self, save_context) + if self.done_callback is not None: out_state[self.DONE_CALLBACK] = self.done_callback.__name__ + return out_state + def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: super().load_instance_state(saved_state, load_context) self.process = load_context.process @@ -426,12 +437,15 @@ def __str__(self) -> str: exception = traceback.format_exception_only(type(self.exception) if self.exception else None, self.exception)[0] return super().__str__() + f'({exception})' - def save_instance_state(self, out_state: SAVED_STATE_TYPE, save_context: persistence.LoadSaveContext) -> None: - super().save_instance_state(out_state, save_context) + def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = auto_save(self, save_context) + out_state[self.EXC_VALUE] = yaml.dump(self.exception) if self.traceback is not None: out_state[self.TRACEBACK] = ''.join(traceback.format_tb(self.traceback)) + return out_state + def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: super().load_instance_state(saved_state, load_context) diff --git a/src/plumpy/processes.py b/src/plumpy/processes.py index d088a5f8..6aba39a3 100644 --- a/src/plumpy/processes.py +++ b/src/plumpy/processes.py @@ -10,6 +10,7 @@ import copy import enum import functools +import inspect import logging import re import sys @@ -74,7 +75,7 @@ class BundleKeys: """ String keys used by the process to save its state in the state bundle. - See :meth:`plumpy.processes.Process.save_instance_state` and :meth:`plumpy.processes.Process.load_instance_state`. + See :meth:`plumpy.processes.Process.save` and :meth:`plumpy.processes.Process.load_instance_state`. """ @@ -616,18 +617,14 @@ async def _run_task(self, callback: Callable[..., T], *args: Any, **kwargs: Any) # region Persistence - def save_instance_state( - self, - out_state: SAVED_STATE_TYPE, - save_context: Optional[persistence.LoadSaveContext], - ) -> None: + def save(self, save_context: Optional[persistence.LoadSaveContext] = None) -> SAVED_STATE_TYPE: """ Ask the process to save its current instance state. :param out_state: A bundle to save the state to :param save_context: The save context """ - super().save_instance_state(out_state, save_context) + out_state: SAVED_STATE_TYPE = persistence.auto_save(self, save_context) # FIXME: the combined ProcessState protocol should cover the case if isinstance(self._state, process_states.Savable): @@ -643,6 +640,8 @@ def save_instance_state( if self.outputs: out_state[BundleKeys.OUTPUTS] = self.encode_input_args(self.outputs) + return out_state + @protected def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: """Load the process from its saved instance state. diff --git a/src/plumpy/workchains.py b/src/plumpy/workchains.py index cf5108c8..a1009acd 100644 --- a/src/plumpy/workchains.py +++ b/src/plumpy/workchains.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import annotations +import copy import abc import asyncio import collections @@ -28,6 +29,7 @@ from . import lang, mixins, persistence, process_spec, process_states, processes from .utils import PID_TYPE, SAVED_STATE_TYPE +from plumpy import loaders, utils ToContext = dict @@ -146,15 +148,69 @@ def on_create(self) -> None: super().on_create() self._stepper = self.spec().get_outline().create_stepper(self) - def save_instance_state( - self, out_state: SAVED_STATE_TYPE, save_context: Optional[persistence.LoadSaveContext] - ) -> None: - super().save_instance_state(out_state, save_context) + def save(self, save_context: Optional[persistence.LoadSaveContext] = None) -> SAVED_STATE_TYPE: + """ + Ask the process to save its current instance state. + + :param out_state: A bundle to save the state to + :param save_context: The save context + """ + out_state: SAVED_STATE_TYPE = {} + + if save_context is None: + save_context = persistence.LoadSaveContext() + + utils.type_check(save_context, persistence.LoadSaveContext) + + default_loader = loaders.get_object_loader() + # If the user has specified a class loader, then save it in the saved state + if save_context.loader is not None: + loader_class = default_loader.identify_object(save_context.loader.__class__) + persistence.Savable.set_custom_meta(out_state, persistence.META__OBJECT_LOADER, loader_class) + loader = save_context.loader + else: + loader = default_loader + + persistence.Savable._set_class_name(out_state, loader.identify_object(self.__class__)) + + self._ensure_persist_configured() + if self._auto_persist is not None: + for member in self._auto_persist: + value = getattr(self, member) + if inspect.ismethod(value): + if value.__self__ is not self: + raise TypeError('Cannot persist methods of other classes') + persistence.Savable._set_meta_type(out_state, member, persistence.META__TYPE__METHOD) + value = value.__name__ + elif isinstance(value, persistence.Savable): + persistence.Savable._set_meta_type(out_state, member, persistence.META__TYPE__SAVABLE) + value = value.save() + else: + value = copy.deepcopy(value) + out_state[member] = value + + if isinstance(self._state, process_states.Savable): + out_state['_state'] = self._state.save() + + # Inputs/outputs + if self.raw_inputs is not None: + out_state[processes.BundleKeys.INPUTS_RAW] = self.encode_input_args(self.raw_inputs) + + if self.inputs is not None: + out_state[processes.BundleKeys.INPUTS_PARSED] = self.encode_input_args(self.inputs) + + if self.outputs: + out_state[processes.BundleKeys.OUTPUTS] = self.encode_input_args(self.outputs) # Ask the stepper to save itself if self._stepper is not None: out_state[self._STEPPER_STATE] = self._stepper.save() + if self._context is not None: + out_state[self.CONTEXT] = self._context.__dict__ + + return out_state + def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: super().load_instance_state(saved_state, load_context) @@ -250,10 +306,12 @@ def __init__(self, workchain: 'WorkChain', fn: WC_COMMAND_TYPE): super().__init__(workchain) self._fn = fn - def save_instance_state(self, out_state: SAVED_STATE_TYPE, save_context: persistence.LoadSaveContext) -> None: - super().save_instance_state(out_state, save_context) + def save(self, save_context: Optional[persistence.LoadSaveContext] = None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = persistence.auto_save(self, save_context) out_state['_fn'] = self._fn.__name__ + return out_state + def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: super().load_instance_state(saved_state, load_context) self._fn = getattr(self._workchain.__class__, saved_state['_fn']) @@ -323,11 +381,13 @@ def next_instruction(self) -> None: def finished(self) -> bool: return self._pos == len(self._block) - def save_instance_state(self, out_state: SAVED_STATE_TYPE, save_context: persistence.LoadSaveContext) -> None: - super().save_instance_state(out_state, save_context) + def save(self, save_context: Optional[persistence.LoadSaveContext] = None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = persistence.auto_save(self, save_context) if self._child_stepper is not None: out_state[STEPPER_STATE] = self._child_stepper.save() + return out_state + def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: super().load_instance_state(saved_state, load_context) self._block = load_context.block_instruction @@ -461,11 +521,13 @@ def step(self) -> Tuple[bool, Any]: def finished(self) -> bool: return self._pos == len(self._if_instruction) - def save_instance_state(self, out_state: SAVED_STATE_TYPE, save_context: persistence.LoadSaveContext) -> None: - super().save_instance_state(out_state, save_context) + def save(self, save_context: Optional[persistence.LoadSaveContext] = None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = persistence.auto_save(self, save_context) if self._child_stepper is not None: out_state[STEPPER_STATE] = self._child_stepper.save() + return out_state + def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: super().load_instance_state(saved_state, load_context) self._if_instruction = load_context.if_instruction @@ -555,11 +617,14 @@ def step(self) -> Tuple[bool, Any]: return False, result - def save_instance_state(self, out_state: SAVED_STATE_TYPE, save_context: persistence.LoadSaveContext) -> None: - super().save_instance_state(out_state, save_context) + def save(self, save_context: Optional[persistence.LoadSaveContext] = None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = persistence.auto_save(self, save_context) + if self._child_stepper is not None: out_state[STEPPER_STATE] = self._child_stepper.save() + return out_state + def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: super().load_instance_state(saved_state, load_context) self._while_instruction = load_context.while_instruction diff --git a/tests/test_processes.py b/tests/test_processes.py index c6576b7e..a634d4e5 100644 --- a/tests/test_processes.py +++ b/tests/test_processes.py @@ -700,7 +700,7 @@ def step2(self): class TestProcessSaving(unittest.TestCase): maxDiff = None - def test_running_save_instance_state(self): + def test_running_save(self): loop = asyncio.get_event_loop() nsync_comeback = SavePauseProc() From 4c4044c16b86fbc3b89dc2f3a0ef4ba8a267dedd Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Mon, 9 Dec 2024 17:07:58 +0100 Subject: [PATCH 29/64] load_instance_state deabstract simplify - stepper de-abstract - remove ContextMixin - Stepper all using recreate_from --- src/plumpy/persistence.py | 17 +-- src/plumpy/process_states.py | 163 ++++++++++++++++++++++------ src/plumpy/processes.py | 15 +-- src/plumpy/workchains.py | 202 +++++++++++++++++++++++++++-------- 4 files changed, 308 insertions(+), 89 deletions(-) diff --git a/src/plumpy/persistence.py b/src/plumpy/persistence.py index 389acc27..0cb7b800 100644 --- a/src/plumpy/persistence.py +++ b/src/plumpy/persistence.py @@ -465,15 +465,11 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa """ load_context = _ensure_object_loader(load_context, saved_state) obj = cls.__new__(cls) - call_with_super_check(obj.load_instance_state, saved_state, load_context) + obj.load_instance_state(saved_state, load_context) return obj - @super_check def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext]) -> None: - self._ensure_persist_configured() - if self._auto_persist is not None: - for member in self._auto_persist: - setattr(self, member, self._get_value(saved_state, member, load_context)) + auto_load(self, saved_state, load_context) def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TYPE: out_state: SAVED_STATE_TYPE = auto_save(self, save_context) @@ -594,7 +590,8 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa return obj def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: LoadSaveContext) -> None: - super().load_instance_state(saved_state, load_context) + auto_load(self, saved_state, load_context) + if self._callbacks: # typing says asyncio.Future._callbacks needs to be called, but in the python 3.7 code it is a simple list for callback in self._callbacks: @@ -637,3 +634,9 @@ def auto_save(obj: Savable, save_context: Optional[LoadSaveContext] = None) -> S out_state[member] = value return out_state + +def auto_load(obj: Savable, saved_state: SAVED_STATE_TYPE, load_context: LoadSaveContext) -> None: + obj._ensure_persist_configured() + if obj._auto_persist is not None: + for member in obj._auto_persist: + setattr(obj, member, obj._get_value(saved_state, member, load_context)) diff --git a/src/plumpy/process_states.py b/src/plumpy/process_states.py index 1b4f610c..c03fd78d 100644 --- a/src/plumpy/process_states.py +++ b/src/plumpy/process_states.py @@ -121,14 +121,28 @@ def save(self, save_context: Optional[persistence.LoadSaveContext] = None) -> SA return out_state - def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: - super().load_instance_state(saved_state, load_context) - self.state_machine = load_context.process + @classmethod + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> 'Savable': + """ + Recreate a :class:`Savable` from a saved state using an optional load context. + + :param saved_state: The saved state + :param load_context: An optional load context + + :return: The recreated instance + + """ + load_context = _ensure_object_loader(load_context, saved_state) + obj = cls.__new__(cls) + auto_load(obj, saved_state, load_context) + + obj.state_machine = load_context.process try: - self.continue_fn = utils.load_function(saved_state[self.CONTINUE_FN]) + obj.continue_fn = utils.load_function(saved_state[obj.CONTINUE_FN]) except ValueError: process = load_context.process - self.continue_fn = getattr(process, saved_state[self.CONTINUE_FN]) + obj.continue_fn = getattr(process, saved_state[obj.CONTINUE_FN]) + return obj # endregion @@ -153,6 +167,7 @@ class ProcessState(Enum): # class Savable(Protocol): # def save(self, save_context: LoadSaveContext | None = None) -> SAVED_STATE_TYPE: ... + @final @auto_persist('args', 'kwargs') class Created(persistence.Savable): @@ -175,11 +190,27 @@ def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TY return out_state - def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: - super().load_instance_state(saved_state, load_context) - self.process = load_context.process + @classmethod + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> 'Savable': + """ + Recreate a :class:`Savable` from a saved state using an optional load context. + + :param saved_state: The saved state + :param load_context: An optional load context - self.run_fn = getattr(self.process, saved_state[self.RUN_FN]) + :return: The recreated instance + + """ + load_context = _ensure_object_loader(load_context, saved_state) + obj = cls.__new__(cls) + + auto_load(obj, saved_state, load_context) + + obj.process = load_context.process + + obj.run_fn = getattr(obj.process, saved_state[obj.RUN_FN]) + + return obj def execute(self) -> st.State: return st.create_state( @@ -232,13 +263,28 @@ def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TY return out_state - def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: - super().load_instance_state(saved_state, load_context) - self.process = load_context.process + @classmethod + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> 'Savable': + """ + Recreate a :class:`Savable` from a saved state using an optional load context. + + :param saved_state: The saved state + :param load_context: An optional load context + + :return: The recreated instance + + """ + load_context = _ensure_object_loader(load_context, saved_state) + obj = cls.__new__(cls) + auto_load(obj, saved_state, load_context) + + obj.process = load_context.process - self.run_fn = ensure_coroutine(getattr(self.process, saved_state[self.RUN_FN])) - if self.COMMAND in saved_state: - self._command = persistence.Savable.load(saved_state[self.COMMAND], load_context) # type: ignore + obj.run_fn = ensure_coroutine(getattr(self.process, saved_state[self.RUN_FN])) + if obj.COMMAND in saved_state: + # FIXME: typing + obj._command = persistence.load(saved_state[obj.COMMAND], load_context) # type: ignore + return obj def interrupt(self, reason: Any) -> None: pass @@ -355,16 +401,30 @@ def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TY return out_state - def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: - super().load_instance_state(saved_state, load_context) - self.process = load_context.process + @classmethod + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> 'Savable': + """ + Recreate a :class:`Savable` from a saved state using an optional load context. + + :param saved_state: The saved state + :param load_context: An optional load context + + :return: The recreated instance + + """ + load_context = _ensure_object_loader(load_context, saved_state) + obj = cls.__new__(cls) + auto_load(obj, saved_state, load_context) + + obj.process = load_context.process - callback_name = saved_state.get(self.DONE_CALLBACK, None) + callback_name = saved_state.get(obj.DONE_CALLBACK, None) if callback_name is not None: - self.done_callback = getattr(self.process, callback_name) + obj.done_callback = getattr(obj.process, callback_name) else: - self.done_callback = None - self._waiting_future = futures.Future() + obj.done_callback = None + obj._waiting_future = futures.Future() + return obj def interrupt(self, reason: Exception) -> None: # This will cause the future in execute() to raise the exception @@ -446,17 +506,30 @@ def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TY return out_state - def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: - super().load_instance_state(saved_state, load_context) + @classmethod + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> 'Savable': + """ + Recreate a :class:`Savable` from a saved state using an optional load context. + + :param saved_state: The saved state + :param load_context: An optional load context + + :return: The recreated instance + + """ + load_context = _ensure_object_loader(load_context, saved_state) + obj = cls.__new__(cls) + auto_load(obj, saved_state, load_context) - self.exception = yaml.load(saved_state[self.EXC_VALUE], Loader=Loader) + obj.exception = yaml.load(saved_state[obj.EXC_VALUE], Loader=Loader) if _HAS_TBLIB: try: - self.traceback = tblib.Traceback.from_string(saved_state[self.TRACEBACK], strict=False) + obj.traceback = tblib.Traceback.from_string(saved_state[obj.TRACEBACK], strict=False) except KeyError: - self.traceback = None + obj.traceback = None else: - self.traceback = None + obj.traceback = None + return obj def get_exc_info( self, @@ -493,8 +566,21 @@ def __init__(self, result: Any, successful: bool) -> None: self.result = result self.successful = successful - def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: - super().load_instance_state(saved_state, load_context) + @classmethod + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> 'Savable': + """ + Recreate a :class:`Savable` from a saved state using an optional load context. + + :param saved_state: The saved state + :param load_context: An optional load context + + :return: The recreated instance + + """ + load_context = _ensure_object_loader(load_context, saved_state) + obj = cls.__new__(cls) + auto_load(obj, saved_state, load_context) + return obj def enter(self) -> None: ... @@ -524,8 +610,21 @@ def __init__(self, msg: Optional[MessageType]): """ self.msg = msg - def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: - super().load_instance_state(saved_state, load_context) + @classmethod + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> 'Savable': + """ + Recreate a :class:`Savable` from a saved state using an optional load context. + + :param saved_state: The saved state + :param load_context: An optional load context + + :return: The recreated instance + + """ + load_context = _ensure_object_loader(load_context, saved_state) + obj = cls.__new__(cls) + auto_load(obj, saved_state, load_context) + return obj def enter(self) -> None: ... diff --git a/src/plumpy/processes.py b/src/plumpy/processes.py index 6aba39a3..fd8c761a 100644 --- a/src/plumpy/processes.py +++ b/src/plumpy/processes.py @@ -10,7 +10,6 @@ import copy import enum import functools -import inspect import logging import re import sys @@ -38,6 +37,7 @@ import kiwipy from plumpy.coordinator import Coordinator +from plumpy.persistence import _ensure_object_loader try: from aiocontextvars import ContextVar @@ -267,9 +267,12 @@ def recreate_from( :return: An instance of the object with its state loaded from the save state. """ - process = cast(Process, super().recreate_from(saved_state, load_context)) - call_with_super_check(process.init) - return process + load_context = _ensure_object_loader(load_context, saved_state) + proc = cls.__new__(cls) + proc.load_instance_state(saved_state, load_context) + + call_with_super_check(proc.init) + return proc def __init__( self, @@ -651,7 +654,7 @@ def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persi """ # First make sure the state machine constructor is called - super().__init__() + state_machine.StateMachine.__init__(self) self._setup_event_hooks() @@ -675,7 +678,7 @@ def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persi self._logger = load_context.logger # Need to call this here as things downstream may rely on us having the runtime variable above - super().load_instance_state(saved_state, load_context) + persistence.auto_load(self, saved_state, load_context) # Inputs/outputs try: diff --git a/src/plumpy/workchains.py b/src/plumpy/workchains.py index a1009acd..66446619 100644 --- a/src/plumpy/workchains.py +++ b/src/plumpy/workchains.py @@ -16,6 +16,7 @@ Mapping, MutableSequence, Optional, + Protocol, Sequence, Tuple, Type, @@ -25,11 +26,14 @@ from plumpy.base import state_machine from plumpy.coordinator import Coordinator +from plumpy.event_helper import EventHelper from plumpy.exceptions import InvalidStateError +from plumpy.process_listener import ProcessListener from . import lang, mixins, persistence, process_spec, process_states, processes -from .utils import PID_TYPE, SAVED_STATE_TYPE +from .utils import PID_TYPE, SAVED_STATE_TYPE, AttributesDict from plumpy import loaders, utils +from plumpy.persistence import _ensure_object_loader ToContext = dict @@ -101,18 +105,15 @@ def enter(self) -> None: for awaitable in self._awaiting: awaitable.add_done_callback(self._awaitable_done) - self.in_state = True - def exit(self) -> None: if self.is_terminal: raise InvalidStateError(f'Cannot exit a terminal state {self.LABEL}') - self.in_state = False for awaitable in self._awaiting: awaitable.remove_done_callback(self._awaitable_done) -class WorkChain(mixins.ContextMixin, processes.Process): +class WorkChain(processes.Process): """ A WorkChain is a series of instructions carried out with the ability to save state in between. @@ -120,7 +121,7 @@ class WorkChain(mixins.ContextMixin, processes.Process): _spec_class = WorkChainSpec _STEPPER_STATE = 'stepper_state' - _CONTEXT = 'CONTEXT' + CONTEXT = 'CONTEXT' @classmethod def get_state_classes(cls) -> Dict[process_states.ProcessState, Type[state_machine.State]]: @@ -137,9 +138,14 @@ def __init__( coordinator: Optional[Coordinator] = None, ) -> None: super().__init__(inputs=inputs, pid=pid, logger=logger, loop=loop, coordinator=coordinator) + self._context: Optional[AttributesDict] = AttributesDict() self._stepper: Optional[Stepper] = None self._awaitables: Dict[Union[asyncio.Future, processes.Process], str] = {} + @property + def ctx(self) -> Optional[AttributesDict]: + return self._context + @classmethod def spec(cls) -> WorkChainSpec: return cast(WorkChainSpec, super().spec()) @@ -212,7 +218,63 @@ def save(self, save_context: Optional[persistence.LoadSaveContext] = None) -> SA return out_state def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: - super().load_instance_state(saved_state, load_context) + ######### + # FIXME: dup of Process.load_instance_state + state_machine.StateMachine.__init__(self) + + self._setup_event_hooks() + + # Runtime variables, set initial states + self._future = persistence.SavableFuture() + self._event_helper = EventHelper(ProcessListener) + self._logger = None + self._communicator = None + + if 'loop' in load_context: + self._loop = load_context.loop + else: + self._loop = asyncio.get_event_loop() + + self._state: state_machine.State = self.recreate_state(saved_state['_state']) + + if 'communicator' in load_context: + self._communicator = load_context.communicator + + if 'logger' in load_context: + self._logger = load_context.logger + + # Need to call this here as things downstream may rely on us having the runtime variable above + persistence.auto_load(self, saved_state, load_context) + + # Inputs/outputs + try: + decoded = self.decode_input_args(saved_state[processes.BundleKeys.INPUTS_RAW]) + self._raw_inputs = utils.AttributesFrozendict(decoded) + except KeyError: + self._raw_inputs = None + + try: + decoded = self.decode_input_args(saved_state[processes.BundleKeys.INPUTS_PARSED]) + self._parsed_inputs = utils.AttributesFrozendict(decoded) + except KeyError: + self._parsed_inputs = None + + try: + decoded = self.decode_input_args(saved_state[processes.BundleKeys.OUTPUTS]) + self._outputs = decoded + except KeyError: + self._outputs = {} + + # + ######### + + # context mixin + try: + self._context = AttributesDict(**saved_state[self.CONTEXT]) + except KeyError: + pass + + # end of context mixin # Recreate the stepper self._stepper = None @@ -255,15 +317,8 @@ def _do_step(self) -> Any: return return_value -class Stepper(persistence.Savable, metaclass=abc.ABCMeta): - def __init__(self, workchain: 'WorkChain') -> None: - self._workchain = workchain - - def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: - super().load_instance_state(saved_state, load_context) - self._workchain = load_context.workchain - - @abc.abstractmethod +# XXX: Stepper is also a Saver with `save` method. +class Stepper(Protocol): def step(self) -> Tuple[bool, Any]: """ Execute on step of the instructions. @@ -272,6 +327,7 @@ def step(self) -> Tuple[bool, Any]: 1. The return value from the executed step """ + ... class _Instruction(metaclass=abc.ABCMeta): @@ -301,9 +357,9 @@ def get_description(self) -> Any: """ -class _FunctionStepper(Stepper): +class _FunctionStepper(persistence.Savable): def __init__(self, workchain: 'WorkChain', fn: WC_COMMAND_TYPE): - super().__init__(workchain) + self._workchain = workchain self._fn = fn def save(self, save_context: Optional[persistence.LoadSaveContext] = None) -> SAVED_STATE_TYPE: @@ -312,9 +368,24 @@ def save(self, save_context: Optional[persistence.LoadSaveContext] = None) -> SA return out_state - def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: - super().load_instance_state(saved_state, load_context) - self._fn = getattr(self._workchain.__class__, saved_state['_fn']) + @classmethod + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> 'Savable': + """ + Recreate a :class:`Savable` from a saved state using an optional load context. + + :param saved_state: The saved state + :param load_context: An optional load context + + :return: The recreated instance + + """ + load_context = _ensure_object_loader(load_context, saved_state) + obj = cls.__new__(cls) + persistence.auto_load(obj, saved_state, load_context) + obj._workchain = load_context.workchain + obj._fn = getattr(obj._workchain.__class__, saved_state['_fn']) + + return obj def step(self) -> Tuple[bool, Any]: return True, self._fn(self._workchain) @@ -354,9 +425,9 @@ def get_description(self) -> str: @persistence.auto_persist('_pos') -class _BlockStepper(Stepper): +class _BlockStepper(persistence.Savable): def __init__(self, block: Sequence[_Instruction], workchain: 'WorkChain') -> None: - super().__init__(workchain) + self._workchain = workchain self._block = block self._pos: int = 0 self._child_stepper: Optional[Stepper] = self._block[0].create_stepper(self._workchain) @@ -388,13 +459,28 @@ def save(self, save_context: Optional[persistence.LoadSaveContext] = None) -> SA return out_state - def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: - super().load_instance_state(saved_state, load_context) - self._block = load_context.block_instruction + @classmethod + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> 'Savable': + """ + Recreate a :class:`Savable` from a saved state using an optional load context. + + :param saved_state: The saved state + :param load_context: An optional load context + + :return: The recreated instance + + """ + load_context = _ensure_object_loader(load_context, saved_state) + obj = cls.__new__(cls) + persistence.auto_load(obj, saved_state, load_context) + obj._workchain = load_context.workchain + obj._block = load_context.block_instruction stepper_state = saved_state.get(STEPPER_STATE, None) - self._child_stepper = None + obj._child_stepper = None if stepper_state is not None: - self._child_stepper = self._block[self._pos].recreate_stepper(stepper_state, self._workchain) + obj._child_stepper = obj._block[obj._pos].recreate_stepper(stepper_state, obj._workchain) + + return obj def __str__(self) -> str: return str(self._pos) + ':' + str(self._child_stepper) @@ -487,9 +573,9 @@ def __str__(self) -> str: @persistence.auto_persist('_pos') -class _IfStepper(Stepper): +class _IfStepper(persistence.Savable): def __init__(self, if_instruction: '_If', workchain: 'WorkChain') -> None: - super().__init__(workchain) + self._workchain = workchain self._if_instruction = if_instruction self._pos = 0 self._child_stepper: Optional[Stepper] = None @@ -528,13 +614,27 @@ def save(self, save_context: Optional[persistence.LoadSaveContext] = None) -> SA return out_state - def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: - super().load_instance_state(saved_state, load_context) - self._if_instruction = load_context.if_instruction + @classmethod + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> 'Savable': + """ + Recreate a :class:`Savable` from a saved state using an optional load context. + + :param saved_state: The saved state + :param load_context: An optional load context + + :return: The recreated instance + + """ + load_context = _ensure_object_loader(load_context, saved_state) + obj = cls.__new__(cls) + persistence.auto_load(obj, saved_state, load_context) + obj._workchain = load_context.workchain + obj._if_instruction = load_context.if_instruction stepper_state = saved_state.get(STEPPER_STATE, None) - self._child_stepper = None + obj._child_stepper = None if stepper_state is not None: - self._child_stepper = self._if_instruction[self._pos].body.recreate_stepper(stepper_state, self._workchain) + obj._child_stepper = obj._if_instruction[obj._pos].body.recreate_stepper(stepper_state, obj._workchain) + return obj def __str__(self) -> str: string = str(self._if_instruction[self._pos]) @@ -596,9 +696,9 @@ def get_description(self) -> Mapping[str, Any]: return description -class _WhileStepper(Stepper): +class _WhileStepper(persistence.Savable): def __init__(self, while_instruction: '_While', workchain: 'WorkChain') -> None: - super().__init__(workchain) + self._workchain = workchain self._while_instruction = while_instruction self._child_stepper: Optional[_BlockStepper] = None @@ -625,13 +725,27 @@ def save(self, save_context: Optional[persistence.LoadSaveContext] = None) -> SA return out_state - def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: - super().load_instance_state(saved_state, load_context) - self._while_instruction = load_context.while_instruction + @classmethod + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> 'Savable': + """ + Recreate a :class:`Savable` from a saved state using an optional load context. + + :param saved_state: The saved state + :param load_context: An optional load context + + :return: The recreated instance + + """ + load_context = _ensure_object_loader(load_context, saved_state) + obj = cls.__new__(cls) + persistence.auto_load(obj, saved_state, load_context) + obj._workchain = load_context.workchain + obj._while_instruction = load_context.while_instruction stepper_state = saved_state.get(STEPPER_STATE, None) - self._child_stepper = None + obj._child_stepper = None if stepper_state is not None: - self._child_stepper = self._while_instruction.body.recreate_stepper(stepper_state, self._workchain) + obj._child_stepper = obj._while_instruction.body.recreate_stepper(stepper_state, obj._workchain) + return obj def __str__(self) -> str: string = str(self._while_instruction) @@ -669,9 +783,9 @@ def __init__(self, exit_code: Optional[EXIT_CODE_TYPE]) -> None: self.exit_code = exit_code -class _ReturnStepper(Stepper): +class _ReturnStepper(persistence.Savable): def __init__(self, return_instruction: '_Return', workchain: 'WorkChain') -> None: - super().__init__(workchain) + self._workchain = workchain self._return_instruction = return_instruction def step(self) -> Tuple[bool, Any]: From 4e95fc37e95cb666207da5690b3bf887d1668a89 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Mon, 9 Dec 2024 22:07:15 +0100 Subject: [PATCH 30/64] ProcessListener recreate_from --- src/plumpy/process_listener.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/plumpy/process_listener.py b/src/plumpy/process_listener.py index c3ab8e5a..e84b504d 100644 --- a/src/plumpy/process_listener.py +++ b/src/plumpy/process_listener.py @@ -3,7 +3,8 @@ from typing import TYPE_CHECKING, Any, Dict, Optional from . import persistence -from .utils import SAVED_STATE_TYPE, protected +from .utils import SAVED_STATE_TYPE +from plumpy.persistence import LoadSaveContext, _ensure_object_loader if TYPE_CHECKING: from .processes import Process @@ -20,12 +21,21 @@ def __init__(self) -> None: def init(self, **kwargs: Any) -> None: self._params = kwargs - @protected - def load_instance_state( - self, saved_state: SAVED_STATE_TYPE, load_context: Optional[persistence.LoadSaveContext] - ) -> None: - super().load_instance_state(saved_state, load_context) - self.init(**saved_state['_params']) + @classmethod + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> 'Savable': + """ + Recreate a :class:`Savable` from a saved state using an optional load context. + + :param saved_state: The saved state + :param load_context: An optional load context + + :return: The recreated instance + + """ + load_context = _ensure_object_loader(load_context, saved_state) + obj = cls.__new__(cls) + obj.init(**saved_state['_params']) + return obj # endregion From 7557a3fc90a6f1759ede416d073d06222bd4da56 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Mon, 9 Dec 2024 22:24:38 +0100 Subject: [PATCH 31/64] Absorb all load_instance_state into recreate_from --- src/plumpy/persistence.py | 23 ++++---- src/plumpy/processes.py | 112 +++++++++++++++++--------------------- src/plumpy/workchains.py | 80 ++++++++++++++++----------- 3 files changed, 110 insertions(+), 105 deletions(-) diff --git a/src/plumpy/persistence.py b/src/plumpy/persistence.py index 0cb7b800..85b2d96d 100644 --- a/src/plumpy/persistence.py +++ b/src/plumpy/persistence.py @@ -465,12 +465,9 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa """ load_context = _ensure_object_loader(load_context, saved_state) obj = cls.__new__(cls) - obj.load_instance_state(saved_state, load_context) + auto_load(obj, saved_state, load_context) return obj - def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext]) -> None: - auto_load(self, saved_state, load_context) - def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TYPE: out_state: SAVED_STATE_TYPE = auto_save(self, save_context) @@ -587,15 +584,16 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa obj = cls(loop=loop) obj.cancel() - return obj - - def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: LoadSaveContext) -> None: - auto_load(self, saved_state, load_context) + # ## XXX: load_instance_state: test not cover + # auto_load(obj, saved_state, load_context) + # + # if obj._callbacks: + # # typing says asyncio.Future._callbacks needs to be called, but in the python 3.7 code it is a simple list + # for callback in obj._callbacks: + # obj.remove_done_callback(callback) # type: ignore[arg-type] + # ## UNTILHERE XXX: - if self._callbacks: - # typing says asyncio.Future._callbacks needs to be called, but in the python 3.7 code it is a simple list - for callback in self._callbacks: - self.remove_done_callback(callback) # type: ignore[arg-type] + return obj def auto_save(obj: Savable, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TYPE: @@ -635,6 +633,7 @@ def auto_save(obj: Savable, save_context: Optional[LoadSaveContext] = None) -> S return out_state + def auto_load(obj: Savable, saved_state: SAVED_STATE_TYPE, load_context: LoadSaveContext) -> None: obj._ensure_persist_configured() if obj._auto_persist is not None: diff --git a/src/plumpy/processes.py b/src/plumpy/processes.py index fd8c761a..f591ba1a 100644 --- a/src/plumpy/processes.py +++ b/src/plumpy/processes.py @@ -75,7 +75,7 @@ class BundleKeys: """ String keys used by the process to save its state in the state bundle. - See :meth:`plumpy.processes.Process.save` and :meth:`plumpy.processes.Process.load_instance_state`. + See :meth:`plumpy.processes.Process.save` and :meth:`plumpy.processes.Process.recreate_from`. """ @@ -257,10 +257,8 @@ def recreate_from( cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[persistence.LoadSaveContext] = None, - ) -> 'Process': - """ - Recreate a process from a saved state, passing any positional and - keyword arguments on to load_instance_state + ) -> Process: + """Recreate a process from a saved state, passing any positional :param saved_state: The saved state to load from :param load_context: The load context to use @@ -269,7 +267,53 @@ def recreate_from( """ load_context = _ensure_object_loader(load_context, saved_state) proc = cls.__new__(cls) - proc.load_instance_state(saved_state, load_context) + + # XXX: load_instance_state + # First make sure the state machine constructor is called + state_machine.StateMachine.__init__(proc) + + proc._setup_event_hooks() + + # Runtime variables, set initial states + proc._future = persistence.SavableFuture() + proc._event_helper = EventHelper(ProcessListener) + proc._logger = None + proc._communicator = None + + if 'loop' in load_context: + proc._loop = load_context.loop + else: + proc._loop = asyncio.get_event_loop() + + proc._state: state_machine.State = proc.recreate_state(saved_state['_state']) + + if 'communicator' in load_context: + proc._communicator = load_context.communicator + + if 'logger' in load_context: + proc._logger = load_context.logger + + # Need to call this here as things downstream may rely on us having the runtime variable above + persistence.auto_load(proc, saved_state, load_context) + + # Inputs/outputs + try: + decoded = proc.decode_input_args(saved_state[BundleKeys.INPUTS_RAW]) + proc._raw_inputs = utils.AttributesFrozendict(decoded) + except KeyError: + proc._raw_inputs = None + + try: + decoded = proc.decode_input_args(saved_state[BundleKeys.INPUTS_PARSED]) + proc._parsed_inputs = utils.AttributesFrozendict(decoded) + except KeyError: + proc._parsed_inputs = None + + try: + decoded = proc.decode_input_args(saved_state[BundleKeys.OUTPUTS]) + proc._outputs = decoded + except KeyError: + proc._outputs = {} call_with_super_check(proc.init) return proc @@ -645,62 +689,6 @@ def save(self, save_context: Optional[persistence.LoadSaveContext] = None) -> SA return out_state - @protected - def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: - """Load the process from its saved instance state. - - :param saved_state: A bundle to load the state from - :param load_context: The load context - - """ - # First make sure the state machine constructor is called - state_machine.StateMachine.__init__(self) - - self._setup_event_hooks() - - # Runtime variables, set initial states - self._future = persistence.SavableFuture() - self._event_helper = EventHelper(ProcessListener) - self._logger = None - self._coordinator = None - - if 'loop' in load_context: - self._loop = load_context.loop - else: - self._loop = asyncio.get_event_loop() - - self._state: state_machine.State = self.recreate_state(saved_state['_state']) - - if 'coordinator' in load_context: - self._coordinator = load_context.coordinator - - if 'logger' in load_context: - self._logger = load_context.logger - - # Need to call this here as things downstream may rely on us having the runtime variable above - persistence.auto_load(self, saved_state, load_context) - - # Inputs/outputs - try: - decoded = self.decode_input_args(saved_state[BundleKeys.INPUTS_RAW]) - self._raw_inputs = utils.AttributesFrozendict(decoded) - except KeyError: - self._raw_inputs = None - - try: - decoded = self.decode_input_args(saved_state[BundleKeys.INPUTS_PARSED]) - self._parsed_inputs = utils.AttributesFrozendict(decoded) - except KeyError: - self._parsed_inputs = None - - try: - decoded = self.decode_input_args(saved_state[BundleKeys.OUTPUTS]) - self._outputs = decoded - except KeyError: - self._outputs = {} - - # endregion - def add_process_listener(self, listener: ProcessListener) -> None: """Add a process listener to the process. diff --git a/src/plumpy/workchains.py b/src/plumpy/workchains.py index 66446619..3df4c7f6 100644 --- a/src/plumpy/workchains.py +++ b/src/plumpy/workchains.py @@ -26,6 +26,7 @@ from plumpy.base import state_machine from plumpy.coordinator import Coordinator +from plumpy.base.utils import call_with_super_check from plumpy.event_helper import EventHelper from plumpy.exceptions import InvalidStateError from plumpy.process_listener import ProcessListener @@ -217,70 +218,87 @@ def save(self, save_context: Optional[persistence.LoadSaveContext] = None) -> SA return out_state - def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: - ######### - # FIXME: dup of Process.load_instance_state - state_machine.StateMachine.__init__(self) + @classmethod + def recreate_from( + cls, + saved_state: SAVED_STATE_TYPE, + load_context: Optional[persistence.LoadSaveContext] = None, + ) -> WorkChain: + """Recreate a workchain from a saved state, passing any positional + + :param saved_state: The saved state to load from + :param load_context: The load context to use + :return: An instance of the object with its state loaded from the save state. + + """ + ### FIXME: dup from process.create_from + load_context = _ensure_object_loader(load_context, saved_state) + proc = cls.__new__(cls) + + # XXX: load_instance_state + # First make sure the state machine constructor is called + state_machine.StateMachine.__init__(proc) - self._setup_event_hooks() + proc._setup_event_hooks() # Runtime variables, set initial states - self._future = persistence.SavableFuture() - self._event_helper = EventHelper(ProcessListener) - self._logger = None - self._communicator = None + proc._future = persistence.SavableFuture() + proc._event_helper = EventHelper(ProcessListener) + proc._logger = None + proc._communicator = None if 'loop' in load_context: - self._loop = load_context.loop + proc._loop = load_context.loop else: - self._loop = asyncio.get_event_loop() + proc._loop = asyncio.get_event_loop() - self._state: state_machine.State = self.recreate_state(saved_state['_state']) + proc._state: state_machine.State = proc.recreate_state(saved_state['_state']) if 'communicator' in load_context: - self._communicator = load_context.communicator + proc._communicator = load_context.communicator if 'logger' in load_context: - self._logger = load_context.logger + proc._logger = load_context.logger # Need to call this here as things downstream may rely on us having the runtime variable above - persistence.auto_load(self, saved_state, load_context) + persistence.auto_load(proc, saved_state, load_context) # Inputs/outputs try: - decoded = self.decode_input_args(saved_state[processes.BundleKeys.INPUTS_RAW]) - self._raw_inputs = utils.AttributesFrozendict(decoded) + decoded = proc.decode_input_args(saved_state[processes.BundleKeys.INPUTS_RAW]) + proc._raw_inputs = utils.AttributesFrozendict(decoded) except KeyError: - self._raw_inputs = None + proc._raw_inputs = None try: - decoded = self.decode_input_args(saved_state[processes.BundleKeys.INPUTS_PARSED]) - self._parsed_inputs = utils.AttributesFrozendict(decoded) + decoded = proc.decode_input_args(saved_state[processes.BundleKeys.INPUTS_PARSED]) + proc._parsed_inputs = utils.AttributesFrozendict(decoded) except KeyError: - self._parsed_inputs = None + proc._parsed_inputs = None try: - decoded = self.decode_input_args(saved_state[processes.BundleKeys.OUTPUTS]) - self._outputs = decoded + decoded = proc.decode_input_args(saved_state[processes.BundleKeys.OUTPUTS]) + proc._outputs = decoded except KeyError: - self._outputs = {} - - # - ######### + proc._outputs = {} + ### UNTILHERE FIXME: dup from process.create_from # context mixin try: - self._context = AttributesDict(**saved_state[self.CONTEXT]) + proc._context = AttributesDict(**saved_state[proc.CONTEXT]) except KeyError: pass # end of context mixin # Recreate the stepper - self._stepper = None - stepper_state = saved_state.get(self._STEPPER_STATE, None) + proc._stepper = None + stepper_state = saved_state.get(proc._STEPPER_STATE, None) if stepper_state is not None: - self._stepper = self.spec().get_outline().recreate_stepper(stepper_state, self) + proc._stepper = proc.spec().get_outline().recreate_stepper(stepper_state, proc) + + call_with_super_check(proc.init) + return proc def to_context(self, **kwargs: Union[asyncio.Future, processes.Process]) -> None: """ From 11dfacbfa463ce571450c3533fb4375b16fdcf4f Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Mon, 9 Dec 2024 23:01:53 +0100 Subject: [PATCH 32/64] Remove useless persist method of Savable class --- src/plumpy/persistence.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/plumpy/persistence.py b/src/plumpy/persistence.py index 85b2d96d..0963445e 100644 --- a/src/plumpy/persistence.py +++ b/src/plumpy/persistence.py @@ -448,10 +448,6 @@ def auto_persist(cls, *members: str) -> None: cls._auto_persist = set() cls._auto_persist.update(members) - @classmethod - def persist(cls) -> None: - pass - @classmethod def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> 'Savable': """ @@ -463,10 +459,7 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa :return: The recreated instance """ - load_context = _ensure_object_loader(load_context, saved_state) - obj = cls.__new__(cls) - auto_load(obj, saved_state, load_context) - return obj + ... def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TYPE: out_state: SAVED_STATE_TYPE = auto_save(self, save_context) @@ -475,7 +468,6 @@ def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TY def _ensure_persist_configured(self) -> None: if not self._persist_configured: - self.persist() self._persist_configured = True # region Metadata getter/setters From 558e2a139f84603fe55c868c55a019867ba66566 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Mon, 9 Dec 2024 23:02:09 +0100 Subject: [PATCH 33/64] Explicity recreate_from implementation --- src/plumpy/event_helper.py | 22 ++++++++++++++++-- tests/test_persistence.py | 46 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/src/plumpy/event_helper.py b/src/plumpy/event_helper.py index 47ad4956..e20dae3f 100644 --- a/src/plumpy/event_helper.py +++ b/src/plumpy/event_helper.py @@ -1,12 +1,14 @@ # -*- coding: utf-8 -*- import logging -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any, Callable, Optional + +from plumpy.utils import SAVED_STATE_TYPE from . import persistence +from plumpy.persistence import Savable, LoadSaveContext, _ensure_object_loader, auto_load if TYPE_CHECKING: from typing import Set, Type - from .process_listener import ProcessListener _LOGGER = logging.getLogger(__name__) @@ -30,6 +32,22 @@ def remove_listener(self, listener: 'ProcessListener') -> None: def remove_all_listeners(self) -> None: self._listeners.clear() + @classmethod + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> Savable: + """ + Recreate a :class:`Savable` from a saved state using an optional load context. + + :param saved_state: The saved state + :param load_context: An optional load context + + :return: The recreated instance + + """ + load_context = _ensure_object_loader(load_context, saved_state) + obj = cls.__new__(cls) + auto_load(obj, saved_state, load_context) + return obj + @property def listeners(self) -> 'Set[ProcessListener]': return self._listeners diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 78724aa0..65ef3226 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -5,6 +5,7 @@ import yaml import plumpy +from plumpy.persistence import auto_load from . import utils @@ -12,6 +13,21 @@ class SaveEmpty(plumpy.Savable): pass + @classmethod + def recreate_from(cls, saved_state, load_context= None): + """ + Recreate a :class:`Savable` from a saved state using an optional load context. + + :param saved_state: The saved state + :param load_context: An optional load context + + :return: The recreated instance + + """ + obj = cls.__new__(cls) + auto_load(obj, saved_state, load_context) + return obj + @plumpy.auto_persist('test', 'test_method') class Save1(plumpy.Savable): @@ -22,12 +38,42 @@ def __init__(self): def m(): pass + @classmethod + def recreate_from(cls, saved_state, load_context= None): + """ + Recreate a :class:`Savable` from a saved state using an optional load context. + + :param saved_state: The saved state + :param load_context: An optional load context + + :return: The recreated instance + + """ + obj = cls.__new__(cls) + auto_load(obj, saved_state, load_context) + return obj + @plumpy.auto_persist('test') class Save(plumpy.Savable): def __init__(self): self.test = Save1() + @classmethod + def recreate_from(cls, saved_state, load_context= None): + """ + Recreate a :class:`Savable` from a saved state using an optional load context. + + :param saved_state: The saved state + :param load_context: An optional load context + + :return: The recreated instance + + """ + obj = cls.__new__(cls) + auto_load(obj, saved_state, load_context) + return obj + class TestSavable(unittest.TestCase): def test_empty_savable(self): From 23abe627f4601787c5903a395b4b1532fa012f14 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Mon, 9 Dec 2024 23:14:28 +0100 Subject: [PATCH 34/64] forming Savable protocol - remove persist_config flag of savable --- src/plumpy/event_helper.py | 12 +- src/plumpy/persistence.py | 245 ++++++++++++++++----------------- src/plumpy/process_listener.py | 16 ++- src/plumpy/process_states.py | 100 ++++++++++---- src/plumpy/processes.py | 9 +- src/plumpy/workchains.py | 77 ++++------- tests/test_persistence.py | 32 +++-- tests/test_processes.py | 4 + tests/test_workchains.py | 2 + 9 files changed, 278 insertions(+), 219 deletions(-) diff --git a/src/plumpy/event_helper.py b/src/plumpy/event_helper.py index e20dae3f..abc2b24b 100644 --- a/src/plumpy/event_helper.py +++ b/src/plumpy/event_helper.py @@ -2,20 +2,21 @@ import logging from typing import TYPE_CHECKING, Any, Callable, Optional +from plumpy.persistence import LoadSaveContext, Savable, auto_load, auto_save, ensure_object_loader from plumpy.utils import SAVED_STATE_TYPE from . import persistence -from plumpy.persistence import Savable, LoadSaveContext, _ensure_object_loader, auto_load if TYPE_CHECKING: from typing import Set, Type + from .process_listener import ProcessListener _LOGGER = logging.getLogger(__name__) @persistence.auto_persist('_listeners', '_listener_type') -class EventHelper(persistence.Savable): +class EventHelper: def __init__(self, listener_type: 'Type[ProcessListener]'): assert listener_type is not None, 'Must provide valid listener type' @@ -43,11 +44,16 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa :return: The recreated instance """ - load_context = _ensure_object_loader(load_context, saved_state) + load_context = ensure_object_loader(load_context, saved_state) obj = cls.__new__(cls) auto_load(obj, saved_state, load_context) return obj + def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = auto_save(self, save_context) + + return out_state + @property def listeners(self) -> 'Set[ProcessListener]': return self._listeners diff --git a/src/plumpy/persistence.py b/src/plumpy/persistence.py index 0963445e..afe82439 100644 --- a/src/plumpy/persistence.py +++ b/src/plumpy/persistence.py @@ -9,12 +9,24 @@ import os import pickle from types import MethodType -from typing import TYPE_CHECKING, Any, Callable, Dict, Generator, Iterable, List, Optional, Set, TypeVar, Union +from typing import ( + TYPE_CHECKING, + Any, + Callable, + ClassVar, + Dict, + Generator, + Iterable, + List, + Optional, + Protocol, + cast, + runtime_checkable, +) import yaml from . import futures, loaders, utils -from .base.utils import call_with_super_check, super_check from .utils import PID_TYPE, SAVED_STATE_TYPE PersistedCheckpoint = collections.namedtuple('PersistedCheckpoint', ['pid', 'tag']) @@ -88,10 +100,10 @@ def load(saved_state: SAVED_STATE_TYPE, load_context: LoadSaveContext | None = N :return: The loaded Savable instance """ - load_context = _ensure_object_loader(load_context, saved_state) + load_context = ensure_object_loader(load_context, saved_state) assert load_context.loader is not None # required for type checking try: - class_name = Savable._get_class_name(saved_state) + class_name = SaveUtil.get_class_name(saved_state) load_cls: Savable = load_context.loader.load_object(class_name) except KeyError: raise ValueError('Class name not found in saved state') @@ -380,22 +392,7 @@ def delete_process_checkpoints(self, pid: PID_TYPE) -> None: del self._checkpoints[pid] -SavableClsType = TypeVar('SavableClsType', bound='type[Savable]') - - -def auto_persist(*members: str) -> Callable[[SavableClsType], SavableClsType]: - def wrapped(savable: SavableClsType) -> SavableClsType: - if savable._auto_persist is None: - savable._auto_persist = set() - else: - savable._auto_persist = set(savable._auto_persist) - savable.auto_persist(*members) - return savable - - return wrapped - - -def _ensure_object_loader(context: Optional['LoadSaveContext'], saved_state: SAVED_STATE_TYPE) -> 'LoadSaveContext': +def ensure_object_loader(context: Optional['LoadSaveContext'], saved_state: SAVED_STATE_TYPE) -> 'LoadSaveContext': """ Given a LoadSaveContext this method will ensure that it has a valid class loader using the following priorities: @@ -417,7 +414,7 @@ def _ensure_object_loader(context: Optional['LoadSaveContext'], saved_state: SAV # 2) Try getting from saved_state default_loader = loaders.get_object_loader() try: - loader_identifier = Savable.get_custom_meta(saved_state, META__OBJECT_LOADER) + loader_identifier = SaveUtil.get_custom_meta(saved_state, META__OBJECT_LOADER) except ValueError: # 3) Fall back to default loader = default_loader @@ -436,45 +433,10 @@ def _ensure_object_loader(context: Optional['LoadSaveContext'], saved_state: SAV META__TYPE__SAVABLE: str = 'S' -class Savable: - CLASS_NAME: str = 'class_name' - - _auto_persist: Optional[Set[str]] = None - _persist_configured = False - - @classmethod - def auto_persist(cls, *members: str) -> None: - if cls._auto_persist is None: - cls._auto_persist = set() - cls._auto_persist.update(members) - - @classmethod - def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> 'Savable': - """ - Recreate a :class:`Savable` from a saved state using an optional load context. - - :param saved_state: The saved state - :param load_context: An optional load context - - :return: The recreated instance - - """ - ... - - def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TYPE: - out_state: SAVED_STATE_TYPE = auto_save(self, save_context) - - return out_state - - def _ensure_persist_configured(self) -> None: - if not self._persist_configured: - self._persist_configured = True - - # region Metadata getter/setters - +class SaveUtil: @staticmethod def set_custom_meta(out_state: SAVED_STATE_TYPE, name: str, value: Any) -> None: - user_dict = Savable._get_create_meta(out_state).setdefault(META__USER, {}) + user_dict = SaveUtil.get_create_meta(out_state).setdefault(META__USER, {}) user_dict[name] = value @staticmethod @@ -485,47 +447,127 @@ def get_custom_meta(saved_state: SAVED_STATE_TYPE, name: str) -> Any: raise ValueError(f"Unknown meta key '{name}'") @staticmethod - def _get_create_meta(out_state: SAVED_STATE_TYPE) -> Dict[str, Any]: + def get_create_meta(out_state: SAVED_STATE_TYPE) -> Dict[str, Any]: return out_state.setdefault(META, {}) @staticmethod - def _set_class_name(out_state: SAVED_STATE_TYPE, name: str) -> None: - Savable._get_create_meta(out_state)[META__CLASS_NAME] = name + def set_class_name(out_state: SAVED_STATE_TYPE, name: str) -> None: + SaveUtil.get_create_meta(out_state)[META__CLASS_NAME] = name @staticmethod - def _get_class_name(saved_state: SAVED_STATE_TYPE) -> str: - return Savable._get_create_meta(saved_state)[META__CLASS_NAME] + def get_class_name(saved_state: SAVED_STATE_TYPE) -> str: + return SaveUtil.get_create_meta(saved_state)[META__CLASS_NAME] @staticmethod - def _set_meta_type(out_state: SAVED_STATE_TYPE, name: str, type_spec: Any) -> None: - type_dict = Savable._get_create_meta(out_state).setdefault(META__TYPES, {}) + def set_meta_type(out_state: SAVED_STATE_TYPE, name: str, type_spec: Any) -> None: + type_dict = SaveUtil.get_create_meta(out_state).setdefault(META__TYPES, {}) type_dict[name] = type_spec @staticmethod - def _get_meta_type(saved_state: SAVED_STATE_TYPE, name: str) -> Any: + def get_meta_type(saved_state: SAVED_STATE_TYPE, name: str) -> Any: try: return saved_state[META][META__TYPES][name] except KeyError: pass - # endregion - def _get_value( - self, saved_state: SAVED_STATE_TYPE, name: str, load_context: Optional[LoadSaveContext] - ) -> Union[MethodType, 'Savable']: - value = saved_state[name] +@runtime_checkable +class Savable(Protocol): + @classmethod + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: LoadSaveContext | None = None) -> 'Savable': + """ + Recreate a :class:`Savable` from a saved state using an optional load context. + + :param saved_state: The saved state + :param load_context: An optional load context + + :return: The recreated instance + + """ + ... + + def save(self, save_context: LoadSaveContext | None = None) -> SAVED_STATE_TYPE: ... + + +@runtime_checkable +class SavableWithAutoPersist(Savable, Protocol): + _auto_persist: ClassVar[set[str]] = set() + + +def auto_save(obj: Savable, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = {} + + if save_context is None: + save_context = LoadSaveContext() + + utils.type_check(save_context, LoadSaveContext) + + default_loader = loaders.get_object_loader() + # If the user has specified a class loader, then save it in the saved state + if save_context.loader is not None: + loader_class = default_loader.identify_object(save_context.loader.__class__) + SaveUtil.set_custom_meta(out_state, META__OBJECT_LOADER, loader_class) + loader = save_context.loader + else: + loader = default_loader + + SaveUtil.set_class_name(out_state, loader.identify_object(obj.__class__)) + + if isinstance(obj, SavableWithAutoPersist): + for member in obj._auto_persist: + value = getattr(obj, member) + if inspect.ismethod(value): + if value.__self__ is not obj: + raise TypeError('Cannot persist methods of other classes') + SaveUtil.set_meta_type(out_state, member, META__TYPE__METHOD) + value = value.__name__ + elif isinstance(value, Savable) and not isinstance(value, type): + # persist for a savable obj, call `save` method of obj. + SaveUtil.set_meta_type(out_state, member, META__TYPE__SAVABLE) + value = value.save() + else: + value = copy.deepcopy(value) + out_state[member] = value + + return out_state + + +def auto_load(obj: SavableWithAutoPersist, saved_state: SAVED_STATE_TYPE, load_context: LoadSaveContext) -> None: + for member in obj._auto_persist: + setattr(obj, member, _get_value(obj, saved_state, member, load_context)) + - typ = Savable._get_meta_type(saved_state, name) - if typ == META__TYPE__METHOD: - value = getattr(self, value) - elif typ == META__TYPE__SAVABLE: - value = load(value, load_context) +def _get_value( + obj: Any, saved_state: SAVED_STATE_TYPE, name: str, load_context: LoadSaveContext | None +) -> MethodType | Savable: + value = saved_state[name] - return value + typ = SaveUtil.get_meta_type(saved_state, name) + if typ == META__TYPE__METHOD: + value = getattr(obj, value) + elif typ == META__TYPE__SAVABLE: + value = load(value, load_context) + + return value + + +def auto_persist(*members: str) -> Callable[..., Savable]: + def wrapped(savable_cls: type) -> Savable: + if not hasattr(savable_cls, '_auto_persist') or savable_cls._auto_persist is None: + savable_cls._auto_persist = set() # type: ignore[attr-defined] + else: + savable_cls._auto_persist = set(savable_cls._auto_persist) + savable_cls._auto_persist.update(members) # type: ignore[attr-defined] + # XXX: validate on `save` and `recreate_from` method?? + return cast(Savable, savable_cls) + return wrapped + + +# FIXME: move me to another module? savablefuture.py? @auto_persist('_state', '_result') -class SavableFuture(futures.Future, Savable): +class SavableFuture(futures.Future): """ A savable future. @@ -550,7 +592,7 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa :return: The recreated instance """ - load_context = _ensure_object_loader(load_context, saved_state) + load_context = ensure_object_loader(load_context, saved_state) try: loop = load_context.loop @@ -586,48 +628,3 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa # ## UNTILHERE XXX: return obj - - -def auto_save(obj: Savable, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TYPE: - out_state: SAVED_STATE_TYPE = {} - - if save_context is None: - save_context = LoadSaveContext() - - utils.type_check(save_context, LoadSaveContext) - - default_loader = loaders.get_object_loader() - # If the user has specified a class loader, then save it in the saved state - if save_context.loader is not None: - loader_class = default_loader.identify_object(save_context.loader.__class__) - Savable.set_custom_meta(out_state, META__OBJECT_LOADER, loader_class) - loader = save_context.loader - else: - loader = default_loader - - Savable._set_class_name(out_state, loader.identify_object(obj.__class__)) - - obj._ensure_persist_configured() - if obj._auto_persist is not None: - for member in obj._auto_persist: - value = getattr(obj, member) - if inspect.ismethod(value): - if value.__self__ is not obj: - raise TypeError('Cannot persist methods of other classes') - Savable._set_meta_type(out_state, member, META__TYPE__METHOD) - value = value.__name__ - elif isinstance(value, Savable): - Savable._set_meta_type(out_state, member, META__TYPE__SAVABLE) - value = value.save() - else: - value = copy.deepcopy(value) - out_state[member] = value - - return out_state - - -def auto_load(obj: Savable, saved_state: SAVED_STATE_TYPE, load_context: LoadSaveContext) -> None: - obj._ensure_persist_configured() - if obj._auto_persist is not None: - for member in obj._auto_persist: - setattr(obj, member, obj._get_value(saved_state, member, load_context)) diff --git a/src/plumpy/process_listener.py b/src/plumpy/process_listener.py index e84b504d..8e9673bb 100644 --- a/src/plumpy/process_listener.py +++ b/src/plumpy/process_listener.py @@ -2,16 +2,21 @@ import abc from typing import TYPE_CHECKING, Any, Dict, Optional +from plumpy.persistence import LoadSaveContext, auto_save, ensure_object_loader + from . import persistence from .utils import SAVED_STATE_TYPE -from plumpy.persistence import LoadSaveContext, _ensure_object_loader if TYPE_CHECKING: + from plumpy.persistence import Savable + from .processes import Process +# FIXME: test any process listener is a savable + @persistence.auto_persist('_params') -class ProcessListener(persistence.Savable, metaclass=abc.ABCMeta): +class ProcessListener(metaclass=abc.ABCMeta): # region Persistence methods def __init__(self) -> None: @@ -32,11 +37,16 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa :return: The recreated instance """ - load_context = _ensure_object_loader(load_context, saved_state) + load_context = ensure_object_loader(load_context, saved_state) obj = cls.__new__(cls) obj.init(**saved_state['_params']) return obj + def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = auto_save(self, save_context) + + return out_state + # endregion def on_process_created(self, process: 'Process') -> None: diff --git a/src/plumpy/process_states.py b/src/plumpy/process_states.py index c03fd78d..08a4d24c 100644 --- a/src/plumpy/process_states.py +++ b/src/plumpy/process_states.py @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- from __future__ import annotations -import copy -import inspect import sys import traceback from enum import Enum @@ -14,19 +12,19 @@ Callable, ClassVar, Optional, - Protocol, Tuple, Type, Union, cast, final, - runtime_checkable, + override, ) import yaml from yaml.loader import Loader from plumpy.message import MessageBuilder, MessageType +from plumpy.persistence import ensure_object_loader try: import tblib @@ -38,8 +36,32 @@ from . import exceptions, futures, persistence, utils from .base import state_machine as st from .lang import NULL -from .persistence import META__OBJECT_LOADER, META__TYPE__METHOD, META__TYPE__SAVABLE, LoadSaveContext, Savable, auto_persist, auto_save -from .utils import SAVED_STATE_TYPE, ensure_coroutine +from .persistence import ( + LoadSaveContext, + Savable, + auto_load, + auto_persist, + auto_save, +) +from .utils import SAVED_STATE_TYPE + +__all__ = [ + 'Continue', + 'Created', + 'Excepted', + 'Finished', + 'Interruption', + # Commands + 'Kill', + 'KillInterruption', + 'Killed', + 'PauseInterruption', + 'ProcessState', + 'Running', + 'Stop', + 'Wait', + 'Waiting', +] if TYPE_CHECKING: from .processes import Process @@ -68,8 +90,26 @@ def __init__(self, msg_text: str | None): # region Commands -class Command(persistence.Savable): - pass +class Command: + @classmethod + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> 'Savable': + """ + Recreate a :class:`Savable` from a saved state using an optional load context. + + :param saved_state: The saved state + :param load_context: An optional load context + + :return: The recreated instance + + """ + obj = cls.__new__(cls) + auto_load(obj, saved_state, load_context) + return obj + + def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = auto_save(self, save_context) + + return out_state @auto_persist('msg') @@ -115,12 +155,14 @@ def __init__(self, continue_fn: Callable[..., Any], *args: Any, **kwargs: Any): self.args = args self.kwargs = kwargs + @override def save(self, save_context: Optional[persistence.LoadSaveContext] = None) -> SAVED_STATE_TYPE: out_state: SAVED_STATE_TYPE = persistence.auto_save(self, save_context) out_state[self.CONTINUE_FN] = self.continue_fn.__name__ return out_state + @override @classmethod def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> 'Savable': """ @@ -132,7 +174,7 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa :return: The recreated instance """ - load_context = _ensure_object_loader(load_context, saved_state) + load_context = ensure_object_loader(load_context, saved_state) obj = cls.__new__(cls) auto_load(obj, saved_state, load_context) @@ -163,14 +205,9 @@ class ProcessState(Enum): KILLED = 'killed' -# @runtime_checkable -# class Savable(Protocol): -# def save(self, save_context: LoadSaveContext | None = None) -> SAVED_STATE_TYPE: ... - - @final @auto_persist('args', 'kwargs') -class Created(persistence.Savable): +class Created: LABEL: ClassVar = ProcessState.CREATED ALLOWED: ClassVar = {ProcessState.RUNNING, ProcessState.KILLED, ProcessState.EXCEPTED} @@ -201,7 +238,7 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa :return: The recreated instance """ - load_context = _ensure_object_loader(load_context, saved_state) + load_context = ensure_object_loader(load_context, saved_state) obj = cls.__new__(cls) auto_load(obj, saved_state, load_context) @@ -224,7 +261,7 @@ def exit(self) -> None: ... @final @auto_persist('args', 'kwargs') -class Running(persistence.Savable): +class Running: LABEL: ClassVar = ProcessState.RUNNING ALLOWED: ClassVar = { ProcessState.RUNNING, @@ -274,7 +311,7 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa :return: The recreated instance """ - load_context = _ensure_object_loader(load_context, saved_state) + load_context = ensure_object_loader(load_context, saved_state) obj = cls.__new__(cls) auto_load(obj, saved_state, load_context) @@ -358,7 +395,7 @@ def exit(self) -> None: ... @auto_persist('msg', 'data') -class Waiting(persistence.Savable): +class Waiting: LABEL: ClassVar = ProcessState.WAITING ALLOWED: ClassVar = { ProcessState.RUNNING, @@ -412,7 +449,7 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa :return: The recreated instance """ - load_context = _ensure_object_loader(load_context, saved_state) + load_context = ensure_object_loader(load_context, saved_state) obj = cls.__new__(cls) auto_load(obj, saved_state, load_context) @@ -465,7 +502,8 @@ def exit(self) -> None: ... @final -class Excepted(persistence.Savable): +@auto_persist() +class Excepted: """ Excepted state, can optionally provide exception and traceback @@ -517,7 +555,7 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa :return: The recreated instance """ - load_context = _ensure_object_loader(load_context, saved_state) + load_context = ensure_object_loader(load_context, saved_state) obj = cls.__new__(cls) auto_load(obj, saved_state, load_context) @@ -550,7 +588,7 @@ def exit(self) -> None: ... @final @auto_persist('result', 'successful') -class Finished(persistence.Savable): +class Finished: """State for process is finished. :param result: The result of process @@ -577,11 +615,16 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa :return: The recreated instance """ - load_context = _ensure_object_loader(load_context, saved_state) + load_context = ensure_object_loader(load_context, saved_state) obj = cls.__new__(cls) auto_load(obj, saved_state, load_context) return obj + def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = auto_save(self, save_context) + + return out_state + def enter(self) -> None: ... def exit(self) -> None: ... @@ -589,7 +632,7 @@ def exit(self) -> None: ... @final @auto_persist('msg') -class Killed(persistence.Savable): +class Killed: """ Represents a state where a process has been killed. @@ -621,11 +664,16 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa :return: The recreated instance """ - load_context = _ensure_object_loader(load_context, saved_state) + load_context = ensure_object_loader(load_context, saved_state) obj = cls.__new__(cls) auto_load(obj, saved_state, load_context) return obj + def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = auto_save(self, save_context) + + return out_state + def enter(self) -> None: ... def exit(self) -> None: ... diff --git a/src/plumpy/processes.py b/src/plumpy/processes.py index f591ba1a..09964ae7 100644 --- a/src/plumpy/processes.py +++ b/src/plumpy/processes.py @@ -37,7 +37,7 @@ import kiwipy from plumpy.coordinator import Coordinator -from plumpy.persistence import _ensure_object_loader +from plumpy.persistence import ensure_object_loader try: from aiocontextvars import ContextVar @@ -116,7 +116,7 @@ def func_wrapper(self: Any, *args: Any, **kwargs: Any) -> Any: '_pre_paused_status', '_event_helper', ) -class Process(StateMachine, persistence.Savable, metaclass=ProcessStateMachineMeta): +class Process(StateMachine, metaclass=ProcessStateMachineMeta): """ The Process class is the base for any unit of work in plumpy. @@ -265,7 +265,7 @@ def recreate_from( :return: An instance of the object with its state loaded from the save state. """ - load_context = _ensure_object_loader(load_context, saved_state) + load_context = ensure_object_loader(load_context, saved_state) proc = cls.__new__(cls) # XXX: load_instance_state @@ -673,8 +673,7 @@ def save(self, save_context: Optional[persistence.LoadSaveContext] = None) -> SA """ out_state: SAVED_STATE_TYPE = persistence.auto_save(self, save_context) - # FIXME: the combined ProcessState protocol should cover the case - if isinstance(self._state, process_states.Savable): + if isinstance(self._state, persistence.Savable): out_state['_state'] = self._state.save() # Inputs/outputs diff --git a/src/plumpy/workchains.py b/src/plumpy/workchains.py index 3df4c7f6..9a241b72 100644 --- a/src/plumpy/workchains.py +++ b/src/plumpy/workchains.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- from __future__ import annotations -import copy import abc import asyncio import collections @@ -9,6 +8,7 @@ import logging import re from typing import ( + TYPE_CHECKING, Any, Callable, Dict, @@ -24,17 +24,20 @@ cast, ) +import kiwipy + +from plumpy import utils from plumpy.base import state_machine from plumpy.coordinator import Coordinator from plumpy.base.utils import call_with_super_check from plumpy.event_helper import EventHelper from plumpy.exceptions import InvalidStateError +from plumpy.persistence import LoadSaveContext, auto_persist, auto_save, ensure_object_loader, Savable from plumpy.process_listener import ProcessListener from . import lang, mixins, persistence, process_spec, process_states, processes from .utils import PID_TYPE, SAVED_STATE_TYPE, AttributesDict -from plumpy import loaders, utils -from plumpy.persistence import _ensure_object_loader + ToContext = dict @@ -162,41 +165,9 @@ def save(self, save_context: Optional[persistence.LoadSaveContext] = None) -> SA :param out_state: A bundle to save the state to :param save_context: The save context """ - out_state: SAVED_STATE_TYPE = {} - - if save_context is None: - save_context = persistence.LoadSaveContext() + out_state: SAVED_STATE_TYPE = auto_save(self, save_context) - utils.type_check(save_context, persistence.LoadSaveContext) - - default_loader = loaders.get_object_loader() - # If the user has specified a class loader, then save it in the saved state - if save_context.loader is not None: - loader_class = default_loader.identify_object(save_context.loader.__class__) - persistence.Savable.set_custom_meta(out_state, persistence.META__OBJECT_LOADER, loader_class) - loader = save_context.loader - else: - loader = default_loader - - persistence.Savable._set_class_name(out_state, loader.identify_object(self.__class__)) - - self._ensure_persist_configured() - if self._auto_persist is not None: - for member in self._auto_persist: - value = getattr(self, member) - if inspect.ismethod(value): - if value.__self__ is not self: - raise TypeError('Cannot persist methods of other classes') - persistence.Savable._set_meta_type(out_state, member, persistence.META__TYPE__METHOD) - value = value.__name__ - elif isinstance(value, persistence.Savable): - persistence.Savable._set_meta_type(out_state, member, persistence.META__TYPE__SAVABLE) - value = value.save() - else: - value = copy.deepcopy(value) - out_state[member] = value - - if isinstance(self._state, process_states.Savable): + if isinstance(self._state, persistence.Savable): out_state['_state'] = self._state.save() # Inputs/outputs @@ -210,7 +181,7 @@ def save(self, save_context: Optional[persistence.LoadSaveContext] = None) -> SA out_state[processes.BundleKeys.OUTPUTS] = self.encode_input_args(self.outputs) # Ask the stepper to save itself - if self._stepper is not None: + if self._stepper is not None and isinstance(self._stepper, Savable): out_state[self._STEPPER_STATE] = self._stepper.save() if self._context is not None: @@ -232,7 +203,7 @@ def recreate_from( """ ### FIXME: dup from process.create_from - load_context = _ensure_object_loader(load_context, saved_state) + load_context = ensure_object_loader(load_context, saved_state) proc = cls.__new__(cls) # XXX: load_instance_state @@ -375,7 +346,8 @@ def get_description(self) -> Any: """ -class _FunctionStepper(persistence.Savable): +@auto_persist() +class _FunctionStepper: def __init__(self, workchain: 'WorkChain', fn: WC_COMMAND_TYPE): self._workchain = workchain self._fn = fn @@ -387,7 +359,9 @@ def save(self, save_context: Optional[persistence.LoadSaveContext] = None) -> SA return out_state @classmethod - def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> 'Savable': + def recreate_from( + cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[persistence.LoadSaveContext] = None + ) -> 'Savable': """ Recreate a :class:`Savable` from a saved state using an optional load context. @@ -397,7 +371,7 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa :return: The recreated instance """ - load_context = _ensure_object_loader(load_context, saved_state) + load_context = ensure_object_loader(load_context, saved_state) obj = cls.__new__(cls) persistence.auto_load(obj, saved_state, load_context) obj._workchain = load_context.workchain @@ -443,7 +417,7 @@ def get_description(self) -> str: @persistence.auto_persist('_pos') -class _BlockStepper(persistence.Savable): +class _BlockStepper: def __init__(self, block: Sequence[_Instruction], workchain: 'WorkChain') -> None: self._workchain = workchain self._block = block @@ -488,7 +462,7 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa :return: The recreated instance """ - load_context = _ensure_object_loader(load_context, saved_state) + load_context = ensure_object_loader(load_context, saved_state) obj = cls.__new__(cls) persistence.auto_load(obj, saved_state, load_context) obj._workchain = load_context.workchain @@ -591,7 +565,7 @@ def __str__(self) -> str: @persistence.auto_persist('_pos') -class _IfStepper(persistence.Savable): +class _IfStepper: def __init__(self, if_instruction: '_If', workchain: 'WorkChain') -> None: self._workchain = workchain self._if_instruction = if_instruction @@ -643,7 +617,7 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa :return: The recreated instance """ - load_context = _ensure_object_loader(load_context, saved_state) + load_context = ensure_object_loader(load_context, saved_state) obj = cls.__new__(cls) persistence.auto_load(obj, saved_state, load_context) obj._workchain = load_context.workchain @@ -714,7 +688,7 @@ def get_description(self) -> Mapping[str, Any]: return description -class _WhileStepper(persistence.Savable): +class _WhileStepper: def __init__(self, while_instruction: '_While', workchain: 'WorkChain') -> None: self._workchain = workchain self._while_instruction = while_instruction @@ -744,7 +718,9 @@ def save(self, save_context: Optional[persistence.LoadSaveContext] = None) -> SA return out_state @classmethod - def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> 'Savable': + def recreate_from( + cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[persistence.LoadSaveContext] = None + ) -> 'Savable': """ Recreate a :class:`Savable` from a saved state using an optional load context. @@ -754,7 +730,7 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa :return: The recreated instance """ - load_context = _ensure_object_loader(load_context, saved_state) + load_context = ensure_object_loader(load_context, saved_state) obj = cls.__new__(cls) persistence.auto_load(obj, saved_state, load_context) obj._workchain = load_context.workchain @@ -801,7 +777,8 @@ def __init__(self, exit_code: Optional[EXIT_CODE_TYPE]) -> None: self.exit_code = exit_code -class _ReturnStepper(persistence.Savable): +@persistence.auto_persist() +class _ReturnStepper: def __init__(self, return_instruction: '_Return', workchain: 'WorkChain') -> None: self._workchain = workchain self._return_instruction = return_instruction diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 65ef3226..4ec4c1a5 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -5,16 +5,17 @@ import yaml import plumpy -from plumpy.persistence import auto_load +from plumpy.persistence import auto_load, auto_persist, auto_save +from plumpy.utils import SAVED_STATE_TYPE from . import utils -class SaveEmpty(plumpy.Savable): - pass +@auto_persist() +class SaveEmpty: @classmethod - def recreate_from(cls, saved_state, load_context= None): + def recreate_from(cls, saved_state, load_context=None): """ Recreate a :class:`Savable` from a saved state using an optional load context. @@ -28,9 +29,14 @@ def recreate_from(cls, saved_state, load_context= None): auto_load(obj, saved_state, load_context) return obj + def save(self, save_context=None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = auto_save(self, save_context) + + return out_state + @plumpy.auto_persist('test', 'test_method') -class Save1(plumpy.Savable): +class Save1: def __init__(self): self.test = 'sup yp' self.test_method = self.m @@ -39,7 +45,7 @@ def m(): pass @classmethod - def recreate_from(cls, saved_state, load_context= None): + def recreate_from(cls, saved_state, load_context=None): """ Recreate a :class:`Savable` from a saved state using an optional load context. @@ -53,14 +59,19 @@ def recreate_from(cls, saved_state, load_context= None): auto_load(obj, saved_state, load_context) return obj + def save(self, save_context=None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = auto_save(self, save_context) + + return out_state + @plumpy.auto_persist('test') -class Save(plumpy.Savable): +class Save: def __init__(self): self.test = Save1() @classmethod - def recreate_from(cls, saved_state, load_context= None): + def recreate_from(cls, saved_state, load_context=None): """ Recreate a :class:`Savable` from a saved state using an optional load context. @@ -74,6 +85,11 @@ def recreate_from(cls, saved_state, load_context= None): auto_load(obj, saved_state, load_context) return obj + def save(self, save_context=None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = auto_save(self, save_context) + + return out_state + class TestSavable(unittest.TestCase): def test_empty_savable(self): diff --git a/tests/test_processes.py b/tests/test_processes.py index a634d4e5..d354508f 100644 --- a/tests/test_processes.py +++ b/tests/test_processes.py @@ -14,6 +14,10 @@ from plumpy.utils import AttributesFrozendict from . import utils +# FIXME: after deabstract on savable into a protocol, test that all state are savable +# FIXME: also that any process is savable +# FIXME: any process listener is savable +# FIXME: any process control commands are savable class ForgetToCallParent(plumpy.Process): def __init__(self, forget_on): diff --git a/tests/test_workchains.py b/tests/test_workchains.py index 08c7317a..4e34d2b4 100644 --- a/tests/test_workchains.py +++ b/tests/test_workchains.py @@ -11,6 +11,8 @@ from . import utils +# FIXME: after deabstract on savable into a protocol, test that all stepper are savable +# FIXME: workchani itself is savable class Wf(WorkChain): # Keep track of which steps were completed by the workflow From 629832c3b12e043b43aa58b838c7e8559c3bc120 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Wed, 11 Dec 2024 00:54:27 +0100 Subject: [PATCH 35/64] Make auto_load symmetry with auto_save and state/state_label distinguish --- src/plumpy/base/state_machine.py | 10 +++- src/plumpy/event_helper.py | 3 +- src/plumpy/persistence.py | 19 ++++++- src/plumpy/process_states.py | 40 ++++++--------- src/plumpy/processes.py | 67 +++++++++++++------------ src/plumpy/workchains.py | 23 ++++----- tests/base/test_statemachine.py | 10 ++-- tests/rmq/test_process_control.py | 10 ++-- tests/test_persistence.py | 14 +++--- tests/test_processes.py | 83 ++++++++++++++++--------------- tests/utils.py | 2 +- 11 files changed, 146 insertions(+), 135 deletions(-) diff --git a/src/plumpy/base/state_machine.py b/src/plumpy/base/state_machine.py index e3912b6f..1eae4789 100644 --- a/src/plumpy/base/state_machine.py +++ b/src/plumpy/base/state_machine.py @@ -266,7 +266,13 @@ def create_initial_state(self, *args: Any, **kwargs: Any) -> State: return self.get_state_class(self.initial_state_label())(self, *args, **kwargs) @property - def state(self) -> Any: + def state(self) -> State | None: + if self._state is None: + return None + return self._state + + @property + def state_label(self) -> Any: if self._state is None: return None return self._state.LABEL @@ -314,7 +320,7 @@ def transition_to(self, new_state: State | None, **kwargs: Any) -> None: # it can happened when transit from terminal state return None - initial_state_label = self._state.LABEL if self._state is not None else None + initial_state_label = self.state_label label = None try: self._transitioning = True diff --git a/src/plumpy/event_helper.py b/src/plumpy/event_helper.py index abc2b24b..9262f856 100644 --- a/src/plumpy/event_helper.py +++ b/src/plumpy/event_helper.py @@ -45,8 +45,7 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa """ load_context = ensure_object_loader(load_context, saved_state) - obj = cls.__new__(cls) - auto_load(obj, saved_state, load_context) + obj = auto_load(cls, saved_state, load_context) return obj def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TYPE: diff --git a/src/plumpy/persistence.py b/src/plumpy/persistence.py index afe82439..31bbc67c 100644 --- a/src/plumpy/persistence.py +++ b/src/plumpy/persistence.py @@ -20,6 +20,7 @@ List, Optional, Protocol, + TypeVar, cast, runtime_checkable, ) @@ -523,6 +524,8 @@ def auto_save(obj: Savable, save_context: Optional[LoadSaveContext] = None) -> S value = value.__name__ elif isinstance(value, Savable) and not isinstance(value, type): # persist for a savable obj, call `save` method of obj. + # the rhs branch is for when value is a Savable class, it is true runtime check + # of lhs condition. SaveUtil.set_meta_type(out_state, member, META__TYPE__SAVABLE) value = value.save() else: @@ -532,11 +535,25 @@ def auto_save(obj: Savable, save_context: Optional[LoadSaveContext] = None) -> S return out_state -def auto_load(obj: SavableWithAutoPersist, saved_state: SAVED_STATE_TYPE, load_context: LoadSaveContext) -> None: +def load_auto_persist_params( + obj: SavableWithAutoPersist, saved_state: SAVED_STATE_TYPE, load_context: LoadSaveContext | None +) -> None: for member in obj._auto_persist: setattr(obj, member, _get_value(obj, saved_state, member, load_context)) +T = TypeVar('T', bound=Savable) + + +def auto_load(cls: type[T], saved_state: SAVED_STATE_TYPE, load_context: LoadSaveContext | None) -> T: + obj = cls.__new__(cls) + + if isinstance(obj, SavableWithAutoPersist): + load_auto_persist_params(obj, saved_state, load_context) + + return obj + + def _get_value( obj: Any, saved_state: SAVED_STATE_TYPE, name: str, load_context: LoadSaveContext | None ) -> MethodType | Savable: diff --git a/src/plumpy/process_states.py b/src/plumpy/process_states.py index 08a4d24c..49d76e46 100644 --- a/src/plumpy/process_states.py +++ b/src/plumpy/process_states.py @@ -42,6 +42,7 @@ auto_load, auto_persist, auto_save, + ensure_object_loader, ) from .utils import SAVED_STATE_TYPE @@ -102,8 +103,8 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa :return: The recreated instance """ - obj = cls.__new__(cls) - auto_load(obj, saved_state, load_context) + load_context = ensure_object_loader(load_context, saved_state) + obj = auto_load(cls, saved_state, load_context) return obj def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TYPE: @@ -175,15 +176,15 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa """ load_context = ensure_object_loader(load_context, saved_state) - obj = cls.__new__(cls) - auto_load(obj, saved_state, load_context) + obj = auto_load(cls, saved_state, load_context) - obj.state_machine = load_context.process try: obj.continue_fn = utils.load_function(saved_state[obj.CONTINUE_FN]) except ValueError: - process = load_context.process - obj.continue_fn = getattr(process, saved_state[obj.CONTINUE_FN]) + if load_context is not None: + obj.continue_fn = getattr(load_context.proc, saved_state[obj.CONTINUE_FN]) + else: + raise return obj @@ -239,12 +240,8 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa """ load_context = ensure_object_loader(load_context, saved_state) - obj = cls.__new__(cls) - - auto_load(obj, saved_state, load_context) - + obj = auto_load(cls, saved_state, load_context) obj.process = load_context.process - obj.run_fn = getattr(obj.process, saved_state[obj.RUN_FN]) return obj @@ -312,15 +309,13 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa """ load_context = ensure_object_loader(load_context, saved_state) - obj = cls.__new__(cls) - auto_load(obj, saved_state, load_context) - + obj = auto_load(cls, saved_state, load_context) obj.process = load_context.process obj.run_fn = ensure_coroutine(getattr(self.process, saved_state[self.RUN_FN])) if obj.COMMAND in saved_state: - # FIXME: typing obj._command = persistence.load(saved_state[obj.COMMAND], load_context) # type: ignore + return obj def interrupt(self, reason: Any) -> None: @@ -450,9 +445,7 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa """ load_context = ensure_object_loader(load_context, saved_state) - obj = cls.__new__(cls) - auto_load(obj, saved_state, load_context) - + obj = auto_load(cls, saved_state, load_context) obj.process = load_context.process callback_name = saved_state.get(obj.DONE_CALLBACK, None) @@ -556,8 +549,7 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa """ load_context = ensure_object_loader(load_context, saved_state) - obj = cls.__new__(cls) - auto_load(obj, saved_state, load_context) + obj = auto_load(cls, saved_state, load_context) obj.exception = yaml.load(saved_state[obj.EXC_VALUE], Loader=Loader) if _HAS_TBLIB: @@ -616,8 +608,7 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa """ load_context = ensure_object_loader(load_context, saved_state) - obj = cls.__new__(cls) - auto_load(obj, saved_state, load_context) + obj = auto_load(cls, saved_state, load_context) return obj def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TYPE: @@ -665,8 +656,7 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa """ load_context = ensure_object_loader(load_context, saved_state) - obj = cls.__new__(cls) - auto_load(obj, saved_state, load_context) + obj = auto_load(cls, saved_state, load_context) return obj def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TYPE: diff --git a/src/plumpy/processes.py b/src/plumpy/processes.py index 09964ae7..aadd9290 100644 --- a/src/plumpy/processes.py +++ b/src/plumpy/processes.py @@ -21,6 +21,7 @@ Any, Awaitable, Callable, + ClassVar, Dict, Generator, Hashable, @@ -166,6 +167,7 @@ class Process(StateMachine, metaclass=ProcessStateMachineMeta): _cleanups: Optional[List[Callable[[], None]]] = None __called: bool = False + _auto_persist: ClassVar[set[str]] @classmethod def current(cls) -> Optional['Process']: @@ -285,7 +287,7 @@ def recreate_from( else: proc._loop = asyncio.get_event_loop() - proc._state: state_machine.State = proc.recreate_state(saved_state['_state']) + proc._state = proc.recreate_state(saved_state['_state']) if 'communicator' in load_context: proc._communicator = load_context.communicator @@ -294,7 +296,7 @@ def recreate_from( proc._logger = load_context.logger # Need to call this here as things downstream may rely on us having the runtime variable above - persistence.auto_load(proc, saved_state, load_context) + persistence.load_auto_persist_params(proc, saved_state, load_context) # Inputs/outputs try: @@ -519,7 +521,9 @@ def launch( def has_terminated(self) -> bool: """Return whether the process was terminated.""" - return self._state.is_terminal + if self.state is None: + raise exceptions.InvalidStateError('process is not in state None that is invalid') + return self.state.is_terminal def result(self) -> Any: """ @@ -529,12 +533,12 @@ def result(self) -> Any: If in any other state this will raise an InvalidStateError. :return: The result of the process """ - if isinstance(self._state, process_states.Finished): - return self._state.result - if isinstance(self._state, process_states.Killed): - raise exceptions.KilledError(self._state.msg) - if isinstance(self._state, process_states.Excepted): - raise (self._state.exception or Exception('process excepted')) + if isinstance(self.state, process_states.Finished): + return self.state.result + if isinstance(self.state, process_states.Killed): + raise exceptions.KilledError(self.state.msg) + if isinstance(self.state, process_states.Excepted): + raise (self.state.exception or Exception('process excepted')) raise exceptions.InvalidStateError @@ -544,7 +548,7 @@ def successful(self) -> bool: Will raise if the process is not in the FINISHED state """ try: - return self._state.successful # type: ignore + return self.state.successful # type: ignore except AttributeError as exception: raise exceptions.InvalidStateError('process is not in the finished state') from exception @@ -555,25 +559,25 @@ def is_successful(self) -> bool: :return: boolean, True if the process is in `Finished` state with `successful` attribute set to `True` """ try: - return self._state.successful # type: ignore + return self.state.successful # type: ignore except AttributeError: return False def killed(self) -> bool: """Return whether the process is killed.""" - return self.state == process_states.ProcessState.KILLED + return self.state_label == process_states.ProcessState.KILLED def killed_msg(self) -> Optional[MessageType]: """Return the killed message.""" - if isinstance(self._state, process_states.Killed): - return self._state.msg + if isinstance(self.state, process_states.Killed): + return self.state.msg raise exceptions.InvalidStateError('Has not been killed') def exception(self) -> Optional[BaseException]: """Return exception, if the process is terminated in excepted state.""" - if isinstance(self._state, process_states.Excepted): - return self._state.exception + if isinstance(self.state, process_states.Excepted): + return self.state.exception return None @@ -583,7 +587,7 @@ def is_excepted(self) -> bool: :return: boolean, True if the process is in ``EXCEPTED`` state. """ - return self.state == process_states.ProcessState.EXCEPTED + return self.state_label == process_states.ProcessState.EXCEPTED def done(self) -> bool: """Return True if the call was successfully killed or finished running. @@ -592,7 +596,7 @@ def done(self) -> bool: Use the `has_terminated` method instead """ warnings.warn('method is deprecated, use `has_terminated` instead', DeprecationWarning) - return self._state.is_terminal + return self.has_terminated() # endregion @@ -620,7 +624,7 @@ def callback_excepted( exception: Optional[BaseException], trace: Optional[TracebackType], ) -> None: - if self.state != process_states.ProcessState.EXCEPTED: + if self.state_label != process_states.ProcessState.EXCEPTED: self.fail(exception, trace) @contextlib.contextmanager @@ -673,8 +677,8 @@ def save(self, save_context: Optional[persistence.LoadSaveContext] = None) -> SA """ out_state: SAVED_STATE_TYPE = persistence.auto_save(self, save_context) - if isinstance(self._state, persistence.Savable): - out_state['_state'] = self._state.save() + if isinstance(self.state, persistence.Savable): + out_state['_state'] = self.state.save() # Inputs/outputs if self.raw_inputs is not None: @@ -732,7 +736,7 @@ def on_entering(self, state: state_machine.State) -> None: def on_entered(self, from_state: Optional[state_machine.State]) -> None: # Map these onto direct functions that the subclass can implement - state_label = self._state.LABEL + state_label = self.state_label if state_label == process_states.ProcessState.RUNNING: call_with_super_check(self.on_running) elif state_label == process_states.ProcessState.WAITING: @@ -746,7 +750,7 @@ def on_entered(self, from_state: Optional[state_machine.State]) -> None: if self._coordinator and isinstance(self.state, enum.Enum): from_label = cast(enum.Enum, from_state.LABEL).value if from_state is not None else None - subject = f'state_changed.{from_label}.{self.state.value}' + subject = f'state_changed.{from_label}.{self.state_label.value}' self.logger.info('Process<%s>: Broadcasting state change: %s', self.pid, subject) try: self._coordinator.broadcast_send(body=None, sender=self.pid, subject=subject) @@ -759,7 +763,7 @@ def on_entered(self, from_state: Optional[state_machine.State]) -> None: raise def on_exiting(self) -> None: - state = self.state + state = self.state_label if state == process_states.ProcessState.WAITING: call_with_super_check(self.on_exit_waiting) elif state == process_states.ProcessState.RUNNING: @@ -1099,7 +1103,6 @@ def transition_failed( if final_state == process_states.ProcessState.CREATED: raise exception.with_traceback(trace) - # state_class = self.get_states_map()[process_states.ProcessState.EXCEPTED] new_state = create_state(self, process_states.ProcessState.EXCEPTED, exception=exception, traceback=trace) self.transition_to(new_state) @@ -1125,9 +1128,9 @@ def pause(self, msg_text: str | None = None) -> Union[bool, CancellableAction]: return self._pausing if self._stepping: - if not isinstance(self._state, Interruptable): + if not isinstance(self.state, Interruptable): raise exceptions.InvalidStateError( - f'cannot interrupt {self._state.__class__}, method `interrupt` not implement' + f'cannot interrupt {self.state.__class__}, method `interrupt` not implement' ) # Ask the step function to pause by setting this flag and giving the @@ -1226,7 +1229,7 @@ def play(self) -> bool: @event(from_states=process_states.Waiting) def resume(self, *args: Any) -> None: """Start running the process again.""" - return self._state.resume(*args) # type: ignore + return self.state.resume(*args) # type: ignore @event(to_states=process_states.Excepted) def fail(self, exception: Optional[BaseException], traceback: Optional[TracebackType]) -> None: @@ -1244,7 +1247,7 @@ def kill(self, msg_text: Optional[str] = None) -> Union[bool, asyncio.Future]: Kill the process :param msg: An optional kill message """ - if self.state == process_states.ProcessState.KILLED: + if self.state_label == process_states.ProcessState.KILLED: # Already killed return True @@ -1256,7 +1259,7 @@ def kill(self, msg_text: Optional[str] = None) -> Union[bool, asyncio.Future]: # Already killing return self._killing - if self._stepping and isinstance(self._state, Interruptable): + if self._stepping and isinstance(self.state, Interruptable): # Ask the step function to pause by setting this flag and giving the # caller back a future interrupt_exception = process_states.KillInterruption(msg_text) @@ -1332,8 +1335,8 @@ async def step(self) -> None: if self.paused and self._paused is not None: await self._paused - if not isinstance(self._state, Proceedable): - raise StateMachineError(f'cannot step from {self._state.__class__}, async method `execute` not implemented') + if not isinstance(self.state, Proceedable): + raise StateMachineError(f'cannot step from {self.state.__class__}, async method `execute` not implemented') try: self._stepping = True diff --git a/src/plumpy/workchains.py b/src/plumpy/workchains.py index 9a241b72..0926273c 100644 --- a/src/plumpy/workchains.py +++ b/src/plumpy/workchains.py @@ -8,7 +8,6 @@ import logging import re from typing import ( - TYPE_CHECKING, Any, Callable, Dict, @@ -32,7 +31,7 @@ from plumpy.base.utils import call_with_super_check from plumpy.event_helper import EventHelper from plumpy.exceptions import InvalidStateError -from plumpy.persistence import LoadSaveContext, auto_persist, auto_save, ensure_object_loader, Savable +from plumpy.persistence import LoadSaveContext, Savable, auto_persist, auto_save, ensure_object_loader from plumpy.process_listener import ProcessListener from . import lang, mixins, persistence, process_spec, process_states, processes @@ -223,7 +222,7 @@ def recreate_from( else: proc._loop = asyncio.get_event_loop() - proc._state: state_machine.State = proc.recreate_state(saved_state['_state']) + proc._state = proc.recreate_state(saved_state['_state']) if 'communicator' in load_context: proc._communicator = load_context.communicator @@ -232,7 +231,7 @@ def recreate_from( proc._logger = load_context.logger # Need to call this here as things downstream may rely on us having the runtime variable above - persistence.auto_load(proc, saved_state, load_context) + persistence.load_auto_persist_params(proc, saved_state, load_context) # Inputs/outputs try: @@ -372,8 +371,7 @@ def recreate_from( """ load_context = ensure_object_loader(load_context, saved_state) - obj = cls.__new__(cls) - persistence.auto_load(obj, saved_state, load_context) + obj = persistence.auto_load(cls, saved_state, load_context) obj._workchain = load_context.workchain obj._fn = getattr(obj._workchain.__class__, saved_state['_fn']) @@ -446,7 +444,7 @@ def finished(self) -> bool: def save(self, save_context: Optional[persistence.LoadSaveContext] = None) -> SAVED_STATE_TYPE: out_state: SAVED_STATE_TYPE = persistence.auto_save(self, save_context) - if self._child_stepper is not None: + if self._child_stepper is not None and isinstance(self._child_stepper, Savable): out_state[STEPPER_STATE] = self._child_stepper.save() return out_state @@ -463,8 +461,7 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa """ load_context = ensure_object_loader(load_context, saved_state) - obj = cls.__new__(cls) - persistence.auto_load(obj, saved_state, load_context) + obj = persistence.auto_load(cls, saved_state, load_context) obj._workchain = load_context.workchain obj._block = load_context.block_instruction stepper_state = saved_state.get(STEPPER_STATE, None) @@ -601,7 +598,7 @@ def finished(self) -> bool: def save(self, save_context: Optional[persistence.LoadSaveContext] = None) -> SAVED_STATE_TYPE: out_state: SAVED_STATE_TYPE = persistence.auto_save(self, save_context) - if self._child_stepper is not None: + if self._child_stepper is not None and isinstance(self._child_stepper, Savable): out_state[STEPPER_STATE] = self._child_stepper.save() return out_state @@ -618,8 +615,7 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa """ load_context = ensure_object_loader(load_context, saved_state) - obj = cls.__new__(cls) - persistence.auto_load(obj, saved_state, load_context) + obj = persistence.auto_load(cls, saved_state, load_context) obj._workchain = load_context.workchain obj._if_instruction = load_context.if_instruction stepper_state = saved_state.get(STEPPER_STATE, None) @@ -731,8 +727,7 @@ def recreate_from( """ load_context = ensure_object_loader(load_context, saved_state) - obj = cls.__new__(cls) - persistence.auto_load(obj, saved_state, load_context) + obj = persistence.auto_load(cls, saved_state, load_context) obj._workchain = load_context.workchain obj._while_instruction = load_context.while_instruction stepper_state = saved_state.get(STEPPER_STATE, None) diff --git a/tests/base/test_statemachine.py b/tests/base/test_statemachine.py index f046aaa8..44a084d4 100644 --- a/tests/base/test_statemachine.py +++ b/tests/base/test_statemachine.py @@ -150,22 +150,22 @@ def stop(self): class TestStateMachine(unittest.TestCase): def test_basic(self): cd_player = CdPlayer() - self.assertEqual(cd_player.state, STOPPED) + self.assertEqual(cd_player.state_label, STOPPED) cd_player.play('Eminem - The Real Slim Shady') - self.assertEqual(cd_player.state, PLAYING) + self.assertEqual(cd_player.state_label, PLAYING) time.sleep(1.0) cd_player.pause() - self.assertEqual(cd_player.state, PAUSED) + self.assertEqual(cd_player.state_label, PAUSED) cd_player.play() - self.assertEqual(cd_player.state, PLAYING) + self.assertEqual(cd_player.state_label, PLAYING) self.assertEqual(cd_player.play(), False) cd_player.stop() - self.assertEqual(cd_player.state, STOPPED) + self.assertEqual(cd_player.state_label, STOPPED) def test_invalid_event(self): cd_player = CdPlayer() diff --git a/tests/rmq/test_process_control.py b/tests/rmq/test_process_control.py index 79a98ba3..7c3b431c 100644 --- a/tests/rmq/test_process_control.py +++ b/tests/rmq/test_process_control.py @@ -68,7 +68,7 @@ async def test_play(self, _coordinator, async_controller): # Check that all is as we expect assert result - assert proc.state == plumpy.ProcessState.WAITING + assert proc.state_label == plumpy.ProcessState.WAITING # if not close the background process will raise exception # make sure proc reach the final state @@ -85,7 +85,7 @@ async def test_kill(self, _coordinator, async_controller): # Check the outcome assert result - assert proc.state == plumpy.ProcessState.KILLED + assert proc.state_label == plumpy.ProcessState.KILLED @pytest.mark.asyncio async def test_status(self, _coordinator, async_controller): @@ -173,7 +173,7 @@ async def test_play(self, _coordinator, sync_controller): # Check that all is as we expect assert result - assert proc.state == plumpy.ProcessState.CREATED + assert proc.state_label == plumpy.ProcessState.CREATED @pytest.mark.asyncio async def test_kill(self, _coordinator, sync_controller): @@ -187,7 +187,7 @@ async def test_kill(self, _coordinator, sync_controller): # Check the outcome assert result # Occasionally fail - assert proc.state == plumpy.ProcessState.KILLED + assert proc.state_label == plumpy.ProcessState.KILLED @pytest.mark.asyncio async def test_kill_all(self, _coordinator, sync_controller): @@ -198,7 +198,7 @@ async def test_kill_all(self, _coordinator, sync_controller): sync_controller.kill_all(msg_text='bang bang, I shot you down') await utils.wait_util(lambda: all([proc.killed() for proc in procs])) - assert all([proc.state == plumpy.ProcessState.KILLED for proc in procs]) + assert all([proc.state_label == plumpy.ProcessState.KILLED for proc in procs]) @pytest.mark.asyncio async def test_status(self, _coordinator, sync_controller): diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 4ec4c1a5..7f616433 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -5,7 +5,7 @@ import yaml import plumpy -from plumpy.persistence import auto_load, auto_persist, auto_save +from plumpy.persistence import auto_load, auto_persist, auto_save, ensure_object_loader from plumpy.utils import SAVED_STATE_TYPE from . import utils @@ -25,8 +25,8 @@ def recreate_from(cls, saved_state, load_context=None): :return: The recreated instance """ - obj = cls.__new__(cls) - auto_load(obj, saved_state, load_context) + load_context = ensure_object_loader(load_context, saved_state) + obj = auto_load(cls, saved_state, load_context) return obj def save(self, save_context=None) -> SAVED_STATE_TYPE: @@ -55,8 +55,8 @@ def recreate_from(cls, saved_state, load_context=None): :return: The recreated instance """ - obj = cls.__new__(cls) - auto_load(obj, saved_state, load_context) + load_context = ensure_object_loader(load_context, saved_state) + obj = auto_load(cls, saved_state, load_context) return obj def save(self, save_context=None) -> SAVED_STATE_TYPE: @@ -81,8 +81,8 @@ def recreate_from(cls, saved_state, load_context=None): :return: The recreated instance """ - obj = cls.__new__(cls) - auto_load(obj, saved_state, load_context) + load_context = ensure_object_loader(load_context, saved_state) + obj = auto_load(cls, saved_state, load_context) return obj def save(self, save_context=None) -> SAVED_STATE_TYPE: diff --git a/tests/test_processes.py b/tests/test_processes.py index d354508f..61d73054 100644 --- a/tests/test_processes.py +++ b/tests/test_processes.py @@ -19,6 +19,7 @@ # FIXME: any process listener is savable # FIXME: any process control commands are savable + class ForgetToCallParent(plumpy.Process): def __init__(self, forget_on): super().__init__() @@ -239,7 +240,7 @@ def test_execute(self): proc.execute() self.assertTrue(proc.has_terminated()) - self.assertEqual(proc.state, ProcessState.FINISHED) + self.assertEqual(proc.state_label, ProcessState.FINISHED) self.assertEqual(proc.outputs, {'default': 5}) def test_run_from_class(self): @@ -277,7 +278,7 @@ def test_exception(self): proc = utils.ExceptionProcess() with self.assertRaises(RuntimeError): proc.execute() - self.assertEqual(proc.state, ProcessState.EXCEPTED) + self.assertEqual(proc.state_label, ProcessState.EXCEPTED) def test_run_kill(self): proc = utils.KillProcess() @@ -344,7 +345,7 @@ def test_wait_continue(self): # Check it's done self.assertTrue(proc.has_terminated()) - self.assertEqual(proc.state, ProcessState.FINISHED) + self.assertEqual(proc.state_label, ProcessState.FINISHED) def test_exc_info(self): proc = utils.ExceptionProcess() @@ -368,7 +369,7 @@ def test_wait_pause_play_resume(self): async def async_test(): await utils.run_until_waiting(proc) - self.assertEqual(proc.state, ProcessState.WAITING) + self.assertEqual(proc.state_label, ProcessState.WAITING) result = await proc.pause() self.assertTrue(result) @@ -384,7 +385,7 @@ async def async_test(): # Check it's done self.assertTrue(proc.has_terminated()) - self.assertEqual(proc.state, ProcessState.FINISHED) + self.assertEqual(proc.state_label, ProcessState.FINISHED) loop.create_task(proc.step_until_terminated()) loop.run_until_complete(async_test()) @@ -405,7 +406,7 @@ def test_pause_play_status_messaging(self): async def async_test(): await utils.run_until_waiting(proc) - self.assertEqual(proc.state, ProcessState.WAITING) + self.assertEqual(proc.state_label, ProcessState.WAITING) result = await proc.pause(PAUSE_STATUS) self.assertTrue(result) @@ -425,7 +426,7 @@ async def async_test(): loop.run_until_complete(async_test()) self.assertTrue(proc.has_terminated()) - self.assertEqual(proc.state, ProcessState.FINISHED) + self.assertEqual(proc.state_label, ProcessState.FINISHED) def test_kill_in_run(self): class KillProcess(Process): @@ -443,7 +444,7 @@ def run(self, **kwargs): proc.execute() self.assertTrue(proc.after_kill) - self.assertEqual(proc.state, ProcessState.KILLED) + self.assertEqual(proc.state_label, ProcessState.KILLED) def test_kill_when_paused_in_run(self): class PauseProcess(Process): @@ -455,7 +456,7 @@ def run(self, **kwargs): with self.assertRaises(plumpy.KilledError): proc.execute() - self.assertEqual(proc.state, ProcessState.KILLED) + self.assertEqual(proc.state_label, ProcessState.KILLED) def test_kill_when_paused(self): loop = asyncio.get_event_loop() @@ -479,7 +480,7 @@ async def async_test(): loop.create_task(proc.step_until_terminated()) loop.run_until_complete(async_test()) - self.assertEqual(proc.state, ProcessState.KILLED) + self.assertEqual(proc.state_label, ProcessState.KILLED) def test_run_multiple(self): # Create and play some processes @@ -555,7 +556,7 @@ def run(self): loop.run_forever() self.assertTrue(proc.paused) - self.assertEqual(plumpy.ProcessState.FINISHED, proc.state) + self.assertEqual(proc.state_label, plumpy.ProcessState.FINISHED) def test_pause_play_in_process(self): """Test that we can pause and play that by playing within the process""" @@ -573,7 +574,7 @@ def run(self): proc.execute() self.assertFalse(proc.paused) - self.assertEqual(plumpy.ProcessState.FINISHED, proc.state) + self.assertEqual(proc.state_label, plumpy.ProcessState.FINISHED) def test_process_stack(self): test_case = self @@ -784,7 +785,7 @@ def test_saving_each_step(self): proc = proc_class() saver = utils.ProcessSaver(proc) saver.capture() - self.assertEqual(proc.state, ProcessState.FINISHED) + self.assertEqual(proc.state_label, ProcessState.FINISHED) self.assertTrue(utils.check_process_against_snapshots(loop, proc_class, saver.snapshots)) def test_restart(self): @@ -799,7 +800,7 @@ async def async_test(): # Load a process from the saved state loaded_proc = saved_state.unbundle() - self.assertEqual(loaded_proc.state, ProcessState.WAITING) + self.assertEqual(loaded_proc.state_label, ProcessState.WAITING) # Now resume it loaded_proc.resume() @@ -822,7 +823,7 @@ async def async_test(): # Load a process from the saved state loaded_proc = saved_state.unbundle() - self.assertEqual(loaded_proc.state, ProcessState.WAITING) + self.assertEqual(loaded_proc.state_label, ProcessState.WAITING) # Now resume it twice in succession loaded_proc.resume() @@ -864,7 +865,7 @@ async def async_test(): def test_killed(self): proc = utils.DummyProcess() proc.kill() - self.assertEqual(proc.state, plumpy.ProcessState.KILLED) + self.assertEqual(proc.state_label, plumpy.ProcessState.KILLED) self._check_round_trip(proc) def _check_round_trip(self, proc1): @@ -987,40 +988,40 @@ def run(self): self.out(namespace_nested + '.two', 2) # Run the process in default mode which should not add any outputs and therefore fail - process = DummyDynamicProcess() - process.execute() + proc = DummyDynamicProcess() + proc.execute() - self.assertEqual(process.state, ProcessState.FINISHED) - self.assertFalse(process.is_successful) - self.assertDictEqual(process.outputs, {}) + self.assertEqual(proc.state_label, ProcessState.FINISHED) + self.assertFalse(proc.is_successful) + self.assertDictEqual(proc.outputs, {}) # Attaching only namespaced ports should fail, because the required port is not added - process = DummyDynamicProcess(inputs={'output_mode': OutputMode.DYNAMIC_PORT_NAMESPACE}) - process.execute() + proc = DummyDynamicProcess(inputs={'output_mode': OutputMode.DYNAMIC_PORT_NAMESPACE}) + proc.execute() - self.assertEqual(process.state, ProcessState.FINISHED) - self.assertFalse(process.is_successful) - self.assertEqual(process.outputs[namespace]['nested']['one'], 1) - self.assertEqual(process.outputs[namespace]['nested']['two'], 2) + self.assertEqual(proc.state_label, ProcessState.FINISHED) + self.assertFalse(proc.is_successful) + self.assertEqual(proc.outputs[namespace]['nested']['one'], 1) + self.assertEqual(proc.outputs[namespace]['nested']['two'], 2) # Attaching only the single required top-level port should be fine - process = DummyDynamicProcess(inputs={'output_mode': OutputMode.SINGLE_REQUIRED_PORT}) - process.execute() + proc = DummyDynamicProcess(inputs={'output_mode': OutputMode.SINGLE_REQUIRED_PORT}) + proc.execute() - self.assertEqual(process.state, ProcessState.FINISHED) - self.assertTrue(process.is_successful) - self.assertEqual(process.outputs['required_bool'], False) + self.assertEqual(proc.state_label, ProcessState.FINISHED) + self.assertTrue(proc.is_successful) + self.assertEqual(proc.outputs['required_bool'], False) # Attaching both the required and namespaced ports should result in a successful termination - process = DummyDynamicProcess(inputs={'output_mode': OutputMode.BOTH_SINGLE_AND_NAMESPACE}) - process.execute() - - self.assertIsNotNone(process.outputs) - self.assertEqual(process.state, ProcessState.FINISHED) - self.assertTrue(process.is_successful) - self.assertEqual(process.outputs['required_bool'], False) - self.assertEqual(process.outputs[namespace]['nested']['one'], 1) - self.assertEqual(process.outputs[namespace]['nested']['two'], 2) + proc = DummyDynamicProcess(inputs={'output_mode': OutputMode.BOTH_SINGLE_AND_NAMESPACE}) + proc.execute() + + self.assertIsNotNone(proc.outputs) + self.assertEqual(proc.state_label, ProcessState.FINISHED) + self.assertTrue(proc.is_successful) + self.assertEqual(proc.outputs['required_bool'], False) + self.assertEqual(proc.outputs[namespace]['nested']['one'], 1) + self.assertEqual(proc.outputs[namespace]['nested']['two'], 2) class TestProcessEvents(unittest.TestCase): diff --git a/tests/utils.py b/tests/utils.py index 3d4458f4..18082fd4 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -596,7 +596,7 @@ def run_until_waiting(proc): listener = plumpy.ProcessListener() in_waiting = asyncio.Future() - if proc.state == ProcessState.WAITING: + if proc.state_label == ProcessState.WAITING: in_waiting.set_result(True) else: From d524e637eb7ad9417b73da3337e10c2a2fc645f1 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Sat, 18 Jan 2025 02:59:20 +0100 Subject: [PATCH 36/64] misc rebase --- src/plumpy/__init__.py | 3 --- src/plumpy/mixins.py | 29 ----------------------------- src/plumpy/process_states.py | 5 ++--- src/plumpy/processes.py | 4 ++-- src/plumpy/workchains.py | 13 +++++-------- 5 files changed, 9 insertions(+), 45 deletions(-) delete mode 100644 src/plumpy/mixins.py diff --git a/src/plumpy/__init__.py b/src/plumpy/__init__.py index 2c988cd8..4cb50820 100644 --- a/src/plumpy/__init__.py +++ b/src/plumpy/__init__.py @@ -29,7 +29,6 @@ from .futures import CancellableAction, Future, capture_exceptions, create_task from .loaders import DefaultObjectLoader, ObjectLoader, get_object_loader, set_object_loader from .message import MessageBuilder, ProcessLauncher, create_continue_body, create_launch_body -from .mixins import ContextMixin from .persistence import ( Bundle, InMemoryPersister, @@ -77,8 +76,6 @@ 'CancellableAction', # exceptions 'ClosedError', - # mixins - 'ContextMixin', # process_states/States 'Continue', # coordinator diff --git a/src/plumpy/mixins.py b/src/plumpy/mixins.py deleted file mode 100644 index 0e3bb0c0..00000000 --- a/src/plumpy/mixins.py +++ /dev/null @@ -1,29 +0,0 @@ -# -*- coding: utf-8 -*- -from typing import Any, Optional - -from . import persistence -from .utils import SAVED_STATE_TYPE, AttributesDict - - -class ContextMixin(persistence.Savable): - """ - Add a context to a Process. The contents of the context will be saved - in the instance state unlike standard instance variables. - """ - - CONTEXT: str = '_context' - - def __init__(self, *args: Any, **kwargs: Any): - super().__init__(*args, **kwargs) - self._context: Optional[AttributesDict] = AttributesDict() - - @property - def ctx(self) -> Optional[AttributesDict]: - return self._context - - def load_instance_state(self, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext) -> None: - super().load_instance_state(saved_state, load_context) - try: - self._context = AttributesDict(**saved_state[self.CONTEXT]) - except KeyError: - pass diff --git a/src/plumpy/process_states.py b/src/plumpy/process_states.py index 49d76e46..704da6eb 100644 --- a/src/plumpy/process_states.py +++ b/src/plumpy/process_states.py @@ -42,9 +42,8 @@ auto_load, auto_persist, auto_save, - ensure_object_loader, ) -from .utils import SAVED_STATE_TYPE +from .utils import SAVED_STATE_TYPE, ensure_coroutine __all__ = [ 'Continue', @@ -312,7 +311,7 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa obj = auto_load(cls, saved_state, load_context) obj.process = load_context.process - obj.run_fn = ensure_coroutine(getattr(self.process, saved_state[self.RUN_FN])) + obj.run_fn = ensure_coroutine(getattr(obj.process, saved_state[obj.RUN_FN])) if obj.COMMAND in saved_state: obj._command = persistence.load(saved_state[obj.COMMAND], load_context) # type: ignore diff --git a/src/plumpy/processes.py b/src/plumpy/processes.py index aadd9290..611004ff 100644 --- a/src/plumpy/processes.py +++ b/src/plumpy/processes.py @@ -280,7 +280,7 @@ def recreate_from( proc._future = persistence.SavableFuture() proc._event_helper = EventHelper(ProcessListener) proc._logger = None - proc._communicator = None + proc._coordinator = None if 'loop' in load_context: proc._loop = load_context.loop @@ -290,7 +290,7 @@ def recreate_from( proc._state = proc.recreate_state(saved_state['_state']) if 'communicator' in load_context: - proc._communicator = load_context.communicator + proc._coordinator = load_context.coordinator if 'logger' in load_context: proc._logger = load_context.logger diff --git a/src/plumpy/workchains.py b/src/plumpy/workchains.py index 0926273c..5c459d0e 100644 --- a/src/plumpy/workchains.py +++ b/src/plumpy/workchains.py @@ -23,21 +23,18 @@ cast, ) -import kiwipy - from plumpy import utils from plumpy.base import state_machine -from plumpy.coordinator import Coordinator from plumpy.base.utils import call_with_super_check +from plumpy.coordinator import Coordinator from plumpy.event_helper import EventHelper from plumpy.exceptions import InvalidStateError from plumpy.persistence import LoadSaveContext, Savable, auto_persist, auto_save, ensure_object_loader from plumpy.process_listener import ProcessListener -from . import lang, mixins, persistence, process_spec, process_states, processes +from . import lang, persistence, process_spec, process_states, processes from .utils import PID_TYPE, SAVED_STATE_TYPE, AttributesDict - ToContext = dict PREDICATE_TYPE = Callable[['WorkChain'], bool] @@ -215,7 +212,7 @@ def recreate_from( proc._future = persistence.SavableFuture() proc._event_helper = EventHelper(ProcessListener) proc._logger = None - proc._communicator = None + proc._coordinator = None if 'loop' in load_context: proc._loop = load_context.loop @@ -224,8 +221,8 @@ def recreate_from( proc._state = proc.recreate_state(saved_state['_state']) - if 'communicator' in load_context: - proc._communicator = load_context.communicator + if 'coordinator' in load_context: + proc._coordinator = load_context.coordinator if 'logger' in load_context: proc._logger = load_context.logger From c3f09951b0821c80e9496ab89fbb813da084f08c Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Sat, 18 Jan 2025 03:59:05 +0100 Subject: [PATCH 37/64] debug logger when state change --- src/plumpy/base/state_machine.py | 1 - src/plumpy/processes.py | 4 ++-- tests/test_processes.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/plumpy/base/state_machine.py b/src/plumpy/base/state_machine.py index 1eae4789..814c1491 100644 --- a/src/plumpy/base/state_machine.py +++ b/src/plumpy/base/state_machine.py @@ -312,7 +312,6 @@ def transition_to(self, new_state: State | None, **kwargs: Any) -> None: The arguments are passed to the state class to create state instance. (process arg does not need to pass since it will always call with 'self' as process) """ - print(f'try: {self._state} -> {new_state}') assert not self._transitioning, 'Cannot call transition_to when already transitioning state' if new_state is None: diff --git a/src/plumpy/processes.py b/src/plumpy/processes.py index 611004ff..9c14fa86 100644 --- a/src/plumpy/processes.py +++ b/src/plumpy/processes.py @@ -748,7 +748,7 @@ def on_entered(self, from_state: Optional[state_machine.State]) -> None: elif state_label == process_states.ProcessState.KILLED: call_with_super_check(self.on_killed) - if self._coordinator and isinstance(self.state, enum.Enum): + if self._coordinator and isinstance(self.state_label, enum.Enum): from_label = cast(enum.Enum, from_state.LABEL).value if from_state is not None else None subject = f'state_changed.{from_label}.{self.state_label.value}' self.logger.info('Process<%s>: Broadcasting state change: %s', self.pid, subject) @@ -1342,7 +1342,6 @@ async def step(self) -> None: self._stepping = True next_state = None try: - # XXX: debug log when need to step to next state next_state = await self._run_task(self._state.execute) except process_states.Interruption as exception: # If the interruption was caused by a call to a Process method then there should @@ -1368,6 +1367,7 @@ async def step(self) -> None: self._interrupt_action.run(next_state) else: # Everything nominal so transition to the next state + self.logger.debug(f'Process<{self.pid}>: transfer from {self._state.LABEL} to {next_state.LABEL}') self.transition_to(next_state) finally: diff --git a/tests/test_processes.py b/tests/test_processes.py index 61d73054..91d98e40 100644 --- a/tests/test_processes.py +++ b/tests/test_processes.py @@ -331,7 +331,7 @@ def test_kill(self): proc.kill(msg_text=msg_text) self.assertTrue(proc.killed()) self.assertEqual(proc.killed_msg()[MESSAGE_TEXT_KEY], msg_text) - self.assertEqual(proc.state, ProcessState.KILLED) + self.assertEqual(proc.state_label, ProcessState.KILLED) def test_wait_continue(self): proc = utils.WaitForSignalProcess() From 75ff4a39fd77fea07c94dcdccf96f8db327e65e1 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Sat, 18 Jan 2025 04:03:11 +0100 Subject: [PATCH 38/64] logger for load process from context --- src/plumpy/processes.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/plumpy/processes.py b/src/plumpy/processes.py index 9c14fa86..2cf20a2a 100644 --- a/src/plumpy/processes.py +++ b/src/plumpy/processes.py @@ -285,15 +285,20 @@ def recreate_from( if 'loop' in load_context: proc._loop = load_context.loop else: + _LOGGER.warning(f'cannot find `loop` store in load_context, use default event loop') proc._loop = asyncio.get_event_loop() proc._state = proc.recreate_state(saved_state['_state']) - if 'communicator' in load_context: + if 'coordinator' in load_context: proc._coordinator = load_context.coordinator + else: + _LOGGER.warning(f'cannot find `coordinator` store in load_context') if 'logger' in load_context: proc._logger = load_context.logger + else: + _LOGGER.warning(f'cannot find `logger` store in load_context') # Need to call this here as things downstream may rely on us having the runtime variable above persistence.load_auto_persist_params(proc, saved_state, load_context) From a90dcff2bb3fd0bdd8e928b57569a5c27bbc10d7 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Wed, 22 Jan 2025 10:59:02 +0100 Subject: [PATCH 39/64] Using typing-extensions for 3.9 support of @override --- pyproject.toml | 1 + src/plumpy/base/state_machine.py | 2 ++ src/plumpy/persistence.py | 2 ++ src/plumpy/process_states.py | 2 +- src/plumpy/processes.py | 10 ++++++---- uv.lock | 2 ++ 6 files changed, 14 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ba563ac8..574173d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ dependencies = [ 'kiwipy[rmq]~=0.8.5', 'nest_asyncio~=1.5,>=1.5.1', 'pyyaml~=6.0', + 'typing-extensions~=4.12' ] [project.urls] diff --git a/src/plumpy/base/state_machine.py b/src/plumpy/base/state_machine.py index 814c1491..a12981a0 100644 --- a/src/plumpy/base/state_machine.py +++ b/src/plumpy/base/state_machine.py @@ -275,6 +275,8 @@ def state(self) -> State | None: def state_label(self) -> Any: if self._state is None: return None + # XXX: should not use `.value` to access the printable output from LABEL + # LABEL as the ClassVar should have __str__ return self._state.LABEL def add_state_event_callback(self, hook: Hashable, callback: EVENT_CALLBACK_TYPE) -> None: diff --git a/src/plumpy/persistence.py b/src/plumpy/persistence.py index 31bbc67c..b2da7eef 100644 --- a/src/plumpy/persistence.py +++ b/src/plumpy/persistence.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +from __future__ import annotations + import abc import asyncio import collections diff --git a/src/plumpy/process_states.py b/src/plumpy/process_states.py index 704da6eb..c245d72a 100644 --- a/src/plumpy/process_states.py +++ b/src/plumpy/process_states.py @@ -17,10 +17,10 @@ Union, cast, final, - override, ) import yaml +from typing_extensions import override from yaml.loader import Loader from plumpy.message import MessageBuilder, MessageType diff --git a/src/plumpy/processes.py b/src/plumpy/processes.py index 2cf20a2a..95f73719 100644 --- a/src/plumpy/processes.py +++ b/src/plumpy/processes.py @@ -285,7 +285,7 @@ def recreate_from( if 'loop' in load_context: proc._loop = load_context.loop else: - _LOGGER.warning(f'cannot find `loop` store in load_context, use default event loop') + _LOGGER.warning('cannot find `loop` store in load_context, use default event loop') proc._loop = asyncio.get_event_loop() proc._state = proc.recreate_state(saved_state['_state']) @@ -293,12 +293,12 @@ def recreate_from( if 'coordinator' in load_context: proc._coordinator = load_context.coordinator else: - _LOGGER.warning(f'cannot find `coordinator` store in load_context') + _LOGGER.warning('cannot find `coordinator` store in load_context') if 'logger' in load_context: proc._logger = load_context.logger else: - _LOGGER.warning(f'cannot find `logger` store in load_context') + _LOGGER.warning('cannot find `logger` store in load_context') # Need to call this here as things downstream may rely on us having the runtime variable above persistence.load_auto_persist_params(proc, saved_state, load_context) @@ -760,7 +760,9 @@ def on_entered(self, from_state: Optional[state_machine.State]) -> None: try: self._coordinator.broadcast_send(body=None, sender=self.pid, subject=subject) except exceptions.CoordinatorCommunicationError: - message = f'Process<{self.pid}>: cannot broadcast state change from {from_label} to {self.state.value}' + message = ( + f'Process<{self.pid}>: cannot broadcast state change from {from_label} to {self.state_label.value}' + ) self.logger.warning(message) self.logger.debug(message, exc_info=True) except Exception: diff --git a/uv.lock b/uv.lock index 2af8adbd..d8fc89f5 100644 --- a/uv.lock +++ b/uv.lock @@ -1862,6 +1862,7 @@ dependencies = [ { name = "kiwipy", extra = ["rmq"] }, { name = "nest-asyncio" }, { name = "pyyaml" }, + { name = "typing-extensions" }, ] [package.optional-dependencies] @@ -1913,6 +1914,7 @@ requires-dist = [ { name = "sphinx", marker = "extra == 'docs'", specifier = "~=3.2.0" }, { name = "sphinx-book-theme", marker = "extra == 'docs'", specifier = "~=0.0.39" }, { name = "types-pyyaml", marker = "extra == 'pre-commit'" }, + { name = "typing-extensions", specifier = "~=4.12" }, ] [[package]] From 83080aca1cbdaac8fa0dee5306920e3db632a1a5 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Wed, 22 Jan 2025 14:07:27 +0100 Subject: [PATCH 40/64] test and typing generic for the state savable types --- src/plumpy/persistence.py | 24 ++++++++++----------- src/plumpy/process_states.py | 24 ++++++++++----------- tests/test_process_states.py | 42 ++++++++++++++++++++++++++++++++++++ tests/test_processes.py | 1 - 4 files changed, 66 insertions(+), 25 deletions(-) create mode 100644 tests/test_process_states.py diff --git a/src/plumpy/persistence.py b/src/plumpy/persistence.py index b2da7eef..9680b04e 100644 --- a/src/plumpy/persistence.py +++ b/src/plumpy/persistence.py @@ -22,6 +22,7 @@ List, Optional, Protocol, + Type, TypeVar, cast, runtime_checkable, @@ -474,10 +475,13 @@ def get_meta_type(saved_state: SAVED_STATE_TYPE, name: str) -> Any: pass +T = TypeVar('T', bound='Savable') + + @runtime_checkable class Savable(Protocol): @classmethod - def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: LoadSaveContext | None = None) -> 'Savable': + def recreate_from(cls: type[T], saved_state: SAVED_STATE_TYPE, load_context: LoadSaveContext | None = None) -> T: """ Recreate a :class:`Savable` from a saved state using an optional load context. @@ -544,9 +548,6 @@ def load_auto_persist_params( setattr(obj, member, _get_value(obj, saved_state, member, load_context)) -T = TypeVar('T', bound=Savable) - - def auto_load(cls: type[T], saved_state: SAVED_STATE_TYPE, load_context: LoadSaveContext | None) -> T: obj = cls.__new__(cls) @@ -570,21 +571,20 @@ def _get_value( return value -def auto_persist(*members: str) -> Callable[..., Savable]: - def wrapped(savable_cls: type) -> Savable: - if not hasattr(savable_cls, '_auto_persist') or savable_cls._auto_persist is None: - savable_cls._auto_persist = set() # type: ignore[attr-defined] +def auto_persist(*members: str) -> Callable[[type[T]], type[T]]: + def wrapped(cls: type[T]) -> type[T]: + if not hasattr(cls, '_auto_persist') or cls._auto_persist is None: + cls._auto_persist = set() # type: ignore[attr-defined] else: - savable_cls._auto_persist = set(savable_cls._auto_persist) + cls._auto_persist = set(cls._auto_persist) - savable_cls._auto_persist.update(members) # type: ignore[attr-defined] + cls._auto_persist.update(members) # type: ignore[attr-defined] # XXX: validate on `save` and `recreate_from` method?? - return cast(Savable, savable_cls) + return cls return wrapped -# FIXME: move me to another module? savablefuture.py? @auto_persist('_state', '_result') class SavableFuture(futures.Future): """ diff --git a/src/plumpy/process_states.py b/src/plumpy/process_states.py index c245d72a..7e20602c 100644 --- a/src/plumpy/process_states.py +++ b/src/plumpy/process_states.py @@ -12,6 +12,7 @@ Callable, ClassVar, Optional, + Self, Tuple, Type, Union, @@ -38,7 +39,6 @@ from .lang import NULL from .persistence import ( LoadSaveContext, - Savable, auto_load, auto_persist, auto_save, @@ -92,7 +92,7 @@ def __init__(self, msg_text: str | None): class Command: @classmethod - def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> 'Savable': + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> Self: """ Recreate a :class:`Savable` from a saved state using an optional load context. @@ -164,7 +164,7 @@ def save(self, save_context: Optional[persistence.LoadSaveContext] = None) -> SA @override @classmethod - def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> 'Savable': + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> Self: """ Recreate a :class:`Savable` from a saved state using an optional load context. @@ -214,7 +214,7 @@ class Created: RUN_FN = 'run_fn' is_terminal: ClassVar[bool] = False - def __init__(self, process: 'Process', run_fn: Callable[..., Any], *args: Any, **kwargs: Any) -> None: + def __init__(self, process: 'st.StateMachine', run_fn: Callable[..., Any], *args: Any, **kwargs: Any) -> None: assert run_fn is not None self.process = process self.run_fn = run_fn @@ -228,7 +228,7 @@ def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TY return out_state @classmethod - def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> 'Savable': + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> Self: """ Recreate a :class:`Savable` from a saved state using an optional load context. @@ -278,7 +278,7 @@ class Running: is_terminal: ClassVar[bool] = False def __init__( - self, process: 'Process', run_fn: Callable[..., Union[Awaitable[Any], Any]], *args: Any, **kwargs: Any + self, process: 'st.StateMachine', run_fn: Callable[..., Union[Awaitable[Any], Any]], *args: Any, **kwargs: Any ) -> None: assert run_fn is not None self.process = process @@ -297,7 +297,7 @@ def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TY return out_state @classmethod - def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> 'Savable': + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> Self: """ Recreate a :class:`Savable` from a saved state using an optional load context. @@ -413,7 +413,7 @@ def __str__(self) -> str: def __init__( self, - process: 'Process', + process: 'st.StateMachine', done_callback: Optional[Callable[..., Any]], msg: Optional[str] = None, data: Optional[Any] = None, @@ -433,7 +433,7 @@ def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TY return out_state @classmethod - def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> 'Savable': + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> Self: """ Recreate a :class:`Savable` from a saved state using an optional load context. @@ -537,7 +537,7 @@ def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TY return out_state @classmethod - def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> 'Savable': + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> Self: """ Recreate a :class:`Savable` from a saved state using an optional load context. @@ -596,7 +596,7 @@ def __init__(self, result: Any, successful: bool) -> None: self.successful = successful @classmethod - def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> 'Savable': + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> Self: """ Recreate a :class:`Savable` from a saved state using an optional load context. @@ -644,7 +644,7 @@ def __init__(self, msg: Optional[MessageType]): self.msg = msg @classmethod - def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> 'Savable': + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> Self: """ Recreate a :class:`Savable` from a saved state using an optional load context. diff --git a/tests/test_process_states.py b/tests/test_process_states.py new file mode 100644 index 00000000..e04adedc --- /dev/null +++ b/tests/test_process_states.py @@ -0,0 +1,42 @@ +# FIXME: after deabstract on savable into a protocol, test that all state are savable + +import pytest +from plumpy.base.state_machine import StateMachine +from plumpy.message import MessageBuilder +from plumpy.persistence import Savable +from plumpy.process_states import Created, Excepted, Finished, Killed, Running, Waiting +from tests.utils import DummyProcess + + +@pytest.fixture(scope='function') +def proc() -> 'StateMachine': + return DummyProcess() + + +def test_create_savable(proc: StateMachine): + state = Created(proc, run_fn=lambda: None) + assert isinstance(state, Savable) + + +def test_running_savable(proc: StateMachine): + state = Running(proc, run_fn=lambda: None) + assert isinstance(state, Savable) + + +def test_waiting_savable(proc: StateMachine): + state = Waiting(proc, done_callback=lambda: None) + assert isinstance(state, Savable) + + +def test_excepted_savable(): + state = Excepted(exception=ValueError('dummy')) + assert isinstance(state, Savable) + + +def test_finished_savable(): + state = Finished(result='done', successful=True) + assert isinstance(state, Savable) + +def test_killed_savable(): + state = Killed(msg=MessageBuilder.kill('kill it')) + assert isinstance(state, Savable) diff --git a/tests/test_processes.py b/tests/test_processes.py index 91d98e40..b7424482 100644 --- a/tests/test_processes.py +++ b/tests/test_processes.py @@ -14,7 +14,6 @@ from plumpy.utils import AttributesFrozendict from . import utils -# FIXME: after deabstract on savable into a protocol, test that all state are savable # FIXME: also that any process is savable # FIXME: any process listener is savable # FIXME: any process control commands are savable From 1769ceecfe76aff008618487747fa885829ae65e Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Wed, 22 Jan 2025 14:12:34 +0100 Subject: [PATCH 41/64] test for command is savable --- src/plumpy/process_states.py | 3 --- tests/test_process_states.py | 9 ++++++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/plumpy/process_states.py b/src/plumpy/process_states.py index 7e20602c..bb4bb9a2 100644 --- a/src/plumpy/process_states.py +++ b/src/plumpy/process_states.py @@ -63,9 +63,6 @@ 'Waiting', ] -if TYPE_CHECKING: - from .processes import Process - class Interruption(Exception): # noqa: N818 pass diff --git a/tests/test_process_states.py b/tests/test_process_states.py index e04adedc..6910972f 100644 --- a/tests/test_process_states.py +++ b/tests/test_process_states.py @@ -4,7 +4,7 @@ from plumpy.base.state_machine import StateMachine from plumpy.message import MessageBuilder from plumpy.persistence import Savable -from plumpy.process_states import Created, Excepted, Finished, Killed, Running, Waiting +from plumpy.process_states import Command, Created, Excepted, Finished, Killed, Running, Waiting from tests.utils import DummyProcess @@ -37,6 +37,13 @@ def test_finished_savable(): state = Finished(result='done', successful=True) assert isinstance(state, Savable) + def test_killed_savable(): state = Killed(msg=MessageBuilder.kill('kill it')) assert isinstance(state, Savable) + +def test_subclass_command_savable(): + class DummyCmd(Command): + pass + + assert isinstance(DummyCmd(), Savable) From 7a1430e2ad9dec82f6abfa371e47838b6e8c1797 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Wed, 22 Jan 2025 15:46:18 +0100 Subject: [PATCH 42/64] absorb _get_value into load_auto_persist_params --- src/plumpy/persistence.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/src/plumpy/persistence.py b/src/plumpy/persistence.py index 9680b04e..9681ced6 100644 --- a/src/plumpy/persistence.py +++ b/src/plumpy/persistence.py @@ -545,7 +545,15 @@ def load_auto_persist_params( obj: SavableWithAutoPersist, saved_state: SAVED_STATE_TYPE, load_context: LoadSaveContext | None ) -> None: for member in obj._auto_persist: - setattr(obj, member, _get_value(obj, saved_state, member, load_context)) + value = saved_state[member] + + typ = SaveUtil.get_meta_type(saved_state, member) + if typ == META__TYPE__METHOD: + value = getattr(obj, value) + elif typ == META__TYPE__SAVABLE: + value = load(value, load_context) + + setattr(obj, member, value) def auto_load(cls: type[T], saved_state: SAVED_STATE_TYPE, load_context: LoadSaveContext | None) -> T: @@ -557,20 +565,6 @@ def auto_load(cls: type[T], saved_state: SAVED_STATE_TYPE, load_context: LoadSav return obj -def _get_value( - obj: Any, saved_state: SAVED_STATE_TYPE, name: str, load_context: LoadSaveContext | None -) -> MethodType | Savable: - value = saved_state[name] - - typ = SaveUtil.get_meta_type(saved_state, name) - if typ == META__TYPE__METHOD: - value = getattr(obj, value) - elif typ == META__TYPE__SAVABLE: - value = load(value, load_context) - - return value - - def auto_persist(*members: str) -> Callable[[type[T]], type[T]]: def wrapped(cls: type[T]) -> type[T]: if not hasattr(cls, '_auto_persist') or cls._auto_persist is None: From 51f621c0103005e91e3faf5523125c1ee39c5389 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Wed, 22 Jan 2025 15:46:59 +0100 Subject: [PATCH 43/64] runtime check state command is loaded as command --- src/plumpy/process_states.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/plumpy/process_states.py b/src/plumpy/process_states.py index bb4bb9a2..29952c9f 100644 --- a/src/plumpy/process_states.py +++ b/src/plumpy/process_states.py @@ -268,7 +268,7 @@ class Running: COMMAND = 'command' # The key used to store an upcoming command # Class level defaults - _command: Union[None, Kill, Stop, Wait, Continue] = None + _command: Kill | Stop | Wait | Continue | None = None _running: bool = False _run_handle = None @@ -310,7 +310,13 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa obj.run_fn = ensure_coroutine(getattr(obj.process, saved_state[obj.RUN_FN])) if obj.COMMAND in saved_state: - obj._command = persistence.load(saved_state[obj.COMMAND], load_context) # type: ignore + loaded_cmd = persistence.load(saved_state[obj.COMMAND], load_context) + if isinstance(loaded_cmd, Command): + # runtime check for loading from persistence + obj._command = loaded_cmd + else: + # XXX: debug log + raise RuntimeError(f'command `{obj.COMMAND}` loaded from Running state not a valid `Command` type') return obj From 34ea7967418f39498218de4f3869743fcc3a5df1 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Wed, 22 Jan 2025 18:05:17 +0100 Subject: [PATCH 44/64] Remove load_context use if not needed --- src/plumpy/persistence.py | 6 +++--- src/plumpy/process_states.py | 14 ++++++------ tests/test_persistence.py | 1 + tests/test_process_states.py | 41 ++++++++++++++++++++++++++++++++---- 4 files changed, 48 insertions(+), 14 deletions(-) diff --git a/src/plumpy/persistence.py b/src/plumpy/persistence.py index 9681ced6..1fadf4e6 100644 --- a/src/plumpy/persistence.py +++ b/src/plumpy/persistence.py @@ -47,8 +47,8 @@ def __init__(self, loader: Optional[loaders.ObjectLoader] = None, **kwargs: Any) def __getattr__(self, item: str) -> Any: try: return self._values[item] - except KeyError: - raise AttributeError(f"item '{item}' not found") + except KeyError as exc: + raise AttributeError(f"item '{item}' not found in the runtime context when load") from exc def __iter__(self) -> Iterable[Any]: return self._value.__iter__() @@ -556,7 +556,7 @@ def load_auto_persist_params( setattr(obj, member, value) -def auto_load(cls: type[T], saved_state: SAVED_STATE_TYPE, load_context: LoadSaveContext | None) -> T: +def auto_load(cls: type[T], saved_state: SAVED_STATE_TYPE, load_context: LoadSaveContext | None = None) -> T: obj = cls.__new__(cls) if isinstance(obj, SavableWithAutoPersist): diff --git a/src/plumpy/process_states.py b/src/plumpy/process_states.py index 29952c9f..48532104 100644 --- a/src/plumpy/process_states.py +++ b/src/plumpy/process_states.py @@ -218,25 +218,25 @@ def __init__(self, process: 'st.StateMachine', run_fn: Callable[..., Any], *args self.args = args self.kwargs = kwargs - def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TYPE: + def save(self, save_context: LoadSaveContext | None = None) -> SAVED_STATE_TYPE: out_state: SAVED_STATE_TYPE = auto_save(self, save_context) out_state[self.RUN_FN] = self.run_fn.__name__ return out_state @classmethod - def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> Self: + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: LoadSaveContext | None = None) -> Self: """ - Recreate a :class:`Savable` from a saved state using an optional load context. + Recreate a :class:`Created` from a saved state using an optional load context. :param saved_state: The saved state - :param load_context: An optional load context + :param load_context: An optional load context for runtime attributes :return: The recreated instance """ load_context = ensure_object_loader(load_context, saved_state) - obj = auto_load(cls, saved_state, load_context) + obj = auto_load(cls, saved_state) obj.process = load_context.process obj.run_fn = getattr(obj.process, saved_state[obj.RUN_FN]) @@ -310,12 +310,12 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa obj.run_fn = ensure_coroutine(getattr(obj.process, saved_state[obj.RUN_FN])) if obj.COMMAND in saved_state: - loaded_cmd = persistence.load(saved_state[obj.COMMAND], load_context) + loaded_cmd = persistence.load(saved_state[obj.COMMAND], load_context) if isinstance(loaded_cmd, Command): # runtime check for loading from persistence obj._command = loaded_cmd else: - # XXX: debug log + # XXX: debug log in principle unreachable raise RuntimeError(f'command `{obj.COMMAND}` loaded from Running state not a valid `Command` type') return obj diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 7f616433..ca37b9b7 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -10,6 +10,7 @@ from . import utils +# FIXME: test auto_load can precisely load auto_persist with nested items @auto_persist() class SaveEmpty: diff --git a/tests/test_process_states.py b/tests/test_process_states.py index 6910972f..38af4d4d 100644 --- a/tests/test_process_states.py +++ b/tests/test_process_states.py @@ -1,11 +1,29 @@ -# FIXME: after deabstract on savable into a protocol, test that all state are savable - +from typing import Any import pytest + +from plumpy import process_states from plumpy.base.state_machine import StateMachine from plumpy.message import MessageBuilder -from plumpy.persistence import Savable +from plumpy.persistence import LoadSaveContext, Savable, load from plumpy.process_states import Command, Created, Excepted, Finished, Killed, Running, Waiting -from tests.utils import DummyProcess +from plumpy.processes import Process + + +class DummyProcess(Process): + """ + Process with no inputs or outputs and does nothing when ran. + """ + + EXPECTED_STATE_SEQUENCE = [ + process_states.ProcessState.CREATED, + process_states.ProcessState.RUNNING, + process_states.ProcessState.FINISHED, + ] + + EXPECTED_OUTPUTS = {} + + def run(self) -> Any: + pass @pytest.fixture(scope='function') @@ -42,8 +60,23 @@ def test_killed_savable(): state = Killed(msg=MessageBuilder.kill('kill it')) assert isinstance(state, Savable) + def test_subclass_command_savable(): class DummyCmd(Command): pass assert isinstance(DummyCmd(), Savable) + + +def test_create_save_load(proc: DummyProcess): + state = Created(proc, run_fn=proc.run) + ctx = LoadSaveContext(process=proc) + saved_state = state.save(ctx) + loaded_state = load(saved_state=saved_state, load_context=ctx) + + # __import__('ipdb').set_trace() + + +def test_running_save_load(proc: StateMachine): + state = Running(proc, run_fn=lambda: None) + assert isinstance(state, Savable) From 51e1245ee83f3a23d4f7965307fc3d41ee4481f4 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Wed, 22 Jan 2025 21:24:19 +0100 Subject: [PATCH 45/64] save(self, loader) --- src/plumpy/event_helper.py | 9 ++++---- src/plumpy/persistence.py | 41 ++++++++++++++++++------------------ src/plumpy/process_states.py | 33 +++++++++++++++-------------- src/plumpy/processes.py | 8 ++++--- src/plumpy/utils.py | 1 + src/plumpy/workchains.py | 32 +++++++++++++++------------- tests/test_persistence.py | 18 ++++++++-------- tests/test_process_states.py | 5 +++-- 8 files changed, 77 insertions(+), 70 deletions(-) diff --git a/src/plumpy/event_helper.py b/src/plumpy/event_helper.py index 9262f856..5110bdee 100644 --- a/src/plumpy/event_helper.py +++ b/src/plumpy/event_helper.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- import logging -from typing import TYPE_CHECKING, Any, Callable, Optional +from typing import TYPE_CHECKING, Any, Callable, Optional, Self +from plumpy.loaders import ObjectLoader from plumpy.persistence import LoadSaveContext, Savable, auto_load, auto_save, ensure_object_loader from plumpy.utils import SAVED_STATE_TYPE @@ -34,7 +35,7 @@ def remove_all_listeners(self) -> None: self._listeners.clear() @classmethod - def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> Savable: + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> Self: """ Recreate a :class:`Savable` from a saved state using an optional load context. @@ -48,8 +49,8 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa obj = auto_load(cls, saved_state, load_context) return obj - def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TYPE: - out_state: SAVED_STATE_TYPE = auto_save(self, save_context) + def save(self, loader: ObjectLoader | None = None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = auto_save(self, loader) return out_state diff --git a/src/plumpy/persistence.py b/src/plumpy/persistence.py index 1fadf4e6..335efb9b 100644 --- a/src/plumpy/persistence.py +++ b/src/plumpy/persistence.py @@ -20,8 +20,10 @@ Generator, Iterable, List, + MutableMapping, Optional, Protocol, + Self, Type, TypeVar, cast, @@ -65,21 +67,21 @@ def copyextend(self, **kwargs: Any) -> 'LoadSaveContext': class Bundle(dict): - def __init__(self, savable: 'Savable', save_context: LoadSaveContext | None = None, dereference: bool = False): + def __init__(self, savable: 'Savable', loader: loaders.ObjectLoader | None = None, dereference: bool = False): """ Create a bundle from a savable. Optionally keep information about the class loader that can be used to load the classes in the bundle. :param savable: The savable object to bundle - :param save_context: The optional save context to use + :param loader: The optional object loader to use :param dereference: Remove refrences from the data, by deep copying """ super().__init__() if dereference: - self.update(copy.deepcopy(savable.save(save_context))) + self.update(copy.deepcopy(savable.save(loader))) else: - self.update(savable.save(save_context)) + self.update(savable.save(loader)) def unbundle(self, load_context: LoadSaveContext | None = None) -> 'Savable': """ @@ -365,7 +367,9 @@ def __init__(self, loader: Optional[loaders.ObjectLoader] = None) -> None: self._save_context = LoadSaveContext(loader=loader) def save_checkpoint(self, process: 'Process', tag: Optional[str] = None) -> None: - self._checkpoints.setdefault(process.pid, {})[tag] = Bundle(process, self._save_context, dereference=True) + self._checkpoints.setdefault(process.pid, {})[tag] = Bundle( + process, self._save_context.loader, dereference=True + ) def load_checkpoint(self, pid: PID_TYPE, tag: Optional[str] = None) -> Bundle: return self._checkpoints[pid][tag] @@ -493,7 +497,7 @@ def recreate_from(cls: type[T], saved_state: SAVED_STATE_TYPE, load_context: Loa """ ... - def save(self, save_context: LoadSaveContext | None = None) -> SAVED_STATE_TYPE: ... + def save(self, loader: loaders.ObjectLoader | None = None) -> SAVED_STATE_TYPE: ... @runtime_checkable @@ -501,23 +505,18 @@ class SavableWithAutoPersist(Savable, Protocol): _auto_persist: ClassVar[set[str]] = set() -def auto_save(obj: Savable, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TYPE: +def auto_save(obj: Savable, loader: loaders.ObjectLoader | None = None) -> SAVED_STATE_TYPE: out_state: SAVED_STATE_TYPE = {} - if save_context is None: - save_context = LoadSaveContext() - - utils.type_check(save_context, LoadSaveContext) - default_loader = loaders.get_object_loader() - # If the user has specified a class loader, then save it in the saved state - if save_context.loader is not None: - loader_class = default_loader.identify_object(save_context.loader.__class__) - SaveUtil.set_custom_meta(out_state, META__OBJECT_LOADER, loader_class) - loader = save_context.loader - else: + if loader is None: loader = default_loader + # If the user has specified a class loader saver the loader in the saved state, or save the default loader + loader_class = default_loader.identify_object(loader.__class__) + SaveUtil.set_custom_meta(out_state, META__OBJECT_LOADER, loader_class) + + # Save object class name SaveUtil.set_class_name(out_state, loader.identify_object(obj.__class__)) if isinstance(obj, SavableWithAutoPersist): @@ -587,15 +586,15 @@ class SavableFuture(futures.Future): .. note: This does not save any assigned done callbacks. """ - def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TYPE: - out_state: SAVED_STATE_TYPE = auto_save(self, save_context) + def save(self, loader: loaders.ObjectLoader | None = None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = auto_save(self, loader) if self.done() and self.exception() is not None: out_state['exception'] = self.exception() return out_state @classmethod - def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> 'Savable': + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> Self: """ Recreate a :class:`Savable` from a saved state using an optional load context. diff --git a/src/plumpy/process_states.py b/src/plumpy/process_states.py index 48532104..eeaf92d8 100644 --- a/src/plumpy/process_states.py +++ b/src/plumpy/process_states.py @@ -24,6 +24,7 @@ from typing_extensions import override from yaml.loader import Loader +from plumpy.loaders import ObjectLoader from plumpy.message import MessageBuilder, MessageType from plumpy.persistence import ensure_object_loader @@ -103,8 +104,8 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa obj = auto_load(cls, saved_state, load_context) return obj - def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TYPE: - out_state: SAVED_STATE_TYPE = auto_save(self, save_context) + def save(self, loader: ObjectLoader | None = None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = auto_save(self, loader) return out_state @@ -153,8 +154,8 @@ def __init__(self, continue_fn: Callable[..., Any], *args: Any, **kwargs: Any): self.kwargs = kwargs @override - def save(self, save_context: Optional[persistence.LoadSaveContext] = None) -> SAVED_STATE_TYPE: - out_state: SAVED_STATE_TYPE = persistence.auto_save(self, save_context) + def save(self, loader: ObjectLoader | None = None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = persistence.auto_save(self, loader) out_state[self.CONTINUE_FN] = self.continue_fn.__name__ return out_state @@ -218,8 +219,8 @@ def __init__(self, process: 'st.StateMachine', run_fn: Callable[..., Any], *args self.args = args self.kwargs = kwargs - def save(self, save_context: LoadSaveContext | None = None) -> SAVED_STATE_TYPE: - out_state: SAVED_STATE_TYPE = auto_save(self, save_context) + def save(self, loader: ObjectLoader | None = None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = auto_save(self, loader) out_state[self.RUN_FN] = self.run_fn.__name__ return out_state @@ -284,8 +285,8 @@ def __init__( self.kwargs = kwargs self._run_handle = None - def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TYPE: - out_state: SAVED_STATE_TYPE = auto_save(self, save_context) + def save(self, loader: ObjectLoader | None = None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = auto_save(self, loader) out_state[self.RUN_FN] = self.run_fn.__name__ if self._command is not None: @@ -427,8 +428,8 @@ def __init__( self.data = data self._waiting_future: futures.Future = futures.Future() - def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TYPE: - out_state: SAVED_STATE_TYPE = auto_save(self, save_context) + def save(self, loader: ObjectLoader | None = None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = auto_save(self, loader) if self.done_callback is not None: out_state[self.DONE_CALLBACK] = self.done_callback.__name__ @@ -530,8 +531,8 @@ def __str__(self) -> str: exception = traceback.format_exception_only(type(self.exception) if self.exception else None, self.exception)[0] return super().__str__() + f'({exception})' - def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TYPE: - out_state: SAVED_STATE_TYPE = auto_save(self, save_context) + def save(self, loader: ObjectLoader | None = None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = auto_save(self, loader) out_state[self.EXC_VALUE] = yaml.dump(self.exception) if self.traceback is not None: @@ -613,8 +614,8 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa obj = auto_load(cls, saved_state, load_context) return obj - def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TYPE: - out_state: SAVED_STATE_TYPE = auto_save(self, save_context) + def save(self, loader: ObjectLoader | None = None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = auto_save(self, loader) return out_state @@ -661,8 +662,8 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa obj = auto_load(cls, saved_state, load_context) return obj - def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TYPE: - out_state: SAVED_STATE_TYPE = auto_save(self, save_context) + def save(self, loader: ObjectLoader | None = None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = auto_save(self, loader) return out_state diff --git a/src/plumpy/processes.py b/src/plumpy/processes.py index 95f73719..24db39a4 100644 --- a/src/plumpy/processes.py +++ b/src/plumpy/processes.py @@ -27,6 +27,7 @@ Hashable, List, Optional, + Self, Sequence, Tuple, Type, @@ -38,6 +39,7 @@ import kiwipy from plumpy.coordinator import Coordinator +from plumpy.loaders import ObjectLoader from plumpy.persistence import ensure_object_loader try: @@ -259,7 +261,7 @@ def recreate_from( cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[persistence.LoadSaveContext] = None, - ) -> Process: + ) -> Self: """Recreate a process from a saved state, passing any positional :param saved_state: The saved state to load from @@ -673,14 +675,14 @@ async def _run_task(self, callback: Callable[..., T], *args: Any, **kwargs: Any) # region Persistence - def save(self, save_context: Optional[persistence.LoadSaveContext] = None) -> SAVED_STATE_TYPE: + def save(self, loader: ObjectLoader | None = None) -> SAVED_STATE_TYPE: """ Ask the process to save its current instance state. :param out_state: A bundle to save the state to :param save_context: The save context """ - out_state: SAVED_STATE_TYPE = persistence.auto_save(self, save_context) + out_state: SAVED_STATE_TYPE = persistence.auto_save(self, loader) if isinstance(self.state, persistence.Savable): out_state['_state'] = self.state.save() diff --git a/src/plumpy/utils.py b/src/plumpy/utils.py index 3c37ce08..cb75f7bd 100644 --- a/src/plumpy/utils.py +++ b/src/plumpy/utils.py @@ -28,6 +28,7 @@ _LOGGER = logging.getLogger(__name__) +# FIXME: should be rename to SAVED_TYPE SAVED_STATE_TYPE = MutableMapping[str, Any] PID_TYPE = Hashable diff --git a/src/plumpy/workchains.py b/src/plumpy/workchains.py index 5c459d0e..9412b490 100644 --- a/src/plumpy/workchains.py +++ b/src/plumpy/workchains.py @@ -16,6 +16,7 @@ MutableSequence, Optional, Protocol, + Self, Sequence, Tuple, Type, @@ -29,6 +30,7 @@ from plumpy.coordinator import Coordinator from plumpy.event_helper import EventHelper from plumpy.exceptions import InvalidStateError +from plumpy.loaders import ObjectLoader from plumpy.persistence import LoadSaveContext, Savable, auto_persist, auto_save, ensure_object_loader from plumpy.process_listener import ProcessListener @@ -154,14 +156,14 @@ def on_create(self) -> None: super().on_create() self._stepper = self.spec().get_outline().create_stepper(self) - def save(self, save_context: Optional[persistence.LoadSaveContext] = None) -> SAVED_STATE_TYPE: + def save(self, loader: ObjectLoader | None = None) -> SAVED_STATE_TYPE: """ Ask the process to save its current instance state. :param out_state: A bundle to save the state to :param save_context: The save context """ - out_state: SAVED_STATE_TYPE = auto_save(self, save_context) + out_state: SAVED_STATE_TYPE = auto_save(self, loader) if isinstance(self._state, persistence.Savable): out_state['_state'] = self._state.save() @@ -190,7 +192,7 @@ def recreate_from( cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[persistence.LoadSaveContext] = None, - ) -> WorkChain: + ) -> Self: """Recreate a workchain from a saved state, passing any positional :param saved_state: The saved state to load from @@ -348,8 +350,8 @@ def __init__(self, workchain: 'WorkChain', fn: WC_COMMAND_TYPE): self._workchain = workchain self._fn = fn - def save(self, save_context: Optional[persistence.LoadSaveContext] = None) -> SAVED_STATE_TYPE: - out_state: SAVED_STATE_TYPE = persistence.auto_save(self, save_context) + def save(self, loader: ObjectLoader | None = None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = persistence.auto_save(self, loader) out_state['_fn'] = self._fn.__name__ return out_state @@ -357,7 +359,7 @@ def save(self, save_context: Optional[persistence.LoadSaveContext] = None) -> SA @classmethod def recreate_from( cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[persistence.LoadSaveContext] = None - ) -> 'Savable': + ) -> Self: """ Recreate a :class:`Savable` from a saved state using an optional load context. @@ -439,15 +441,15 @@ def next_instruction(self) -> None: def finished(self) -> bool: return self._pos == len(self._block) - def save(self, save_context: Optional[persistence.LoadSaveContext] = None) -> SAVED_STATE_TYPE: - out_state: SAVED_STATE_TYPE = persistence.auto_save(self, save_context) + def save(self, loader: ObjectLoader | None = None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = persistence.auto_save(self, loader) if self._child_stepper is not None and isinstance(self._child_stepper, Savable): out_state[STEPPER_STATE] = self._child_stepper.save() return out_state @classmethod - def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> 'Savable': + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> Self: """ Recreate a :class:`Savable` from a saved state using an optional load context. @@ -593,15 +595,15 @@ def step(self) -> Tuple[bool, Any]: def finished(self) -> bool: return self._pos == len(self._if_instruction) - def save(self, save_context: Optional[persistence.LoadSaveContext] = None) -> SAVED_STATE_TYPE: - out_state: SAVED_STATE_TYPE = persistence.auto_save(self, save_context) + def save(self, loader: ObjectLoader | None = None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = persistence.auto_save(self, loader) if self._child_stepper is not None and isinstance(self._child_stepper, Savable): out_state[STEPPER_STATE] = self._child_stepper.save() return out_state @classmethod - def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> 'Savable': + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> Self: """ Recreate a :class:`Savable` from a saved state using an optional load context. @@ -702,8 +704,8 @@ def step(self) -> Tuple[bool, Any]: return False, result - def save(self, save_context: Optional[persistence.LoadSaveContext] = None) -> SAVED_STATE_TYPE: - out_state: SAVED_STATE_TYPE = persistence.auto_save(self, save_context) + def save(self, loader: ObjectLoader | None = None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = persistence.auto_save(self, loader) if self._child_stepper is not None: out_state[STEPPER_STATE] = self._child_stepper.save() @@ -713,7 +715,7 @@ def save(self, save_context: Optional[persistence.LoadSaveContext] = None) -> SA @classmethod def recreate_from( cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[persistence.LoadSaveContext] = None - ) -> 'Savable': + ) -> Self: """ Recreate a :class:`Savable` from a saved state using an optional load context. diff --git a/tests/test_persistence.py b/tests/test_persistence.py index ca37b9b7..679853ce 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -12,9 +12,9 @@ # FIXME: test auto_load can precisely load auto_persist with nested items + @auto_persist() class SaveEmpty: - @classmethod def recreate_from(cls, saved_state, load_context=None): """ @@ -30,8 +30,8 @@ def recreate_from(cls, saved_state, load_context=None): obj = auto_load(cls, saved_state, load_context) return obj - def save(self, save_context=None) -> SAVED_STATE_TYPE: - out_state: SAVED_STATE_TYPE = auto_save(self, save_context) + def save(self, loader=None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = auto_save(self) return out_state @@ -60,8 +60,8 @@ def recreate_from(cls, saved_state, load_context=None): obj = auto_load(cls, saved_state, load_context) return obj - def save(self, save_context=None) -> SAVED_STATE_TYPE: - out_state: SAVED_STATE_TYPE = auto_save(self, save_context) + def save(self, loader=None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = auto_save(self, loader) return out_state @@ -86,8 +86,8 @@ def recreate_from(cls, saved_state, load_context=None): obj = auto_load(cls, saved_state, load_context) return obj - def save(self, save_context=None) -> SAVED_STATE_TYPE: - out_state: SAVED_STATE_TYPE = auto_save(self, save_context) + def save(self, loader=None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = auto_save(self, loader) return out_state @@ -130,9 +130,9 @@ def _save_round_trip_with_loader(self, savable): :type savable: :class:`plumpy.Savable` """ object_loader = plumpy.get_object_loader() - saved_state1 = savable.save(plumpy.LoadSaveContext(object_loader)) + saved_state1 = savable.save(object_loader) loaded = savable.recreate_from(saved_state1) - saved_state2 = loaded.save(plumpy.LoadSaveContext(object_loader)) + saved_state2 = loaded.save(object_loader) saved_state3 = loaded.save() self.assertDictEqual(saved_state1, saved_state2) self.assertNotEqual(saved_state1, saved_state3) diff --git a/tests/test_process_states.py b/tests/test_process_states.py index 38af4d4d..7b18a1be 100644 --- a/tests/test_process_states.py +++ b/tests/test_process_states.py @@ -71,10 +71,11 @@ class DummyCmd(Command): def test_create_save_load(proc: DummyProcess): state = Created(proc, run_fn=proc.run) ctx = LoadSaveContext(process=proc) - saved_state = state.save(ctx) + saved_state = state.save() loaded_state = load(saved_state=saved_state, load_context=ctx) + saved_state2 = loaded_state.save() - # __import__('ipdb').set_trace() + assert saved_state == saved_state2 def test_running_save_load(proc: StateMachine): From 99d00ee6cf5311245015f3efd3caf95a2095ab3c Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Wed, 22 Jan 2025 21:33:00 +0100 Subject: [PATCH 46/64] Update persistence test using custom object loader --- src/plumpy/event_helper.py | 2 +- src/plumpy/persistence.py | 7 ++----- src/plumpy/process_states.py | 1 - tests/test_persistence.py | 14 ++++++++++++-- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/plumpy/event_helper.py b/src/plumpy/event_helper.py index 5110bdee..188ae735 100644 --- a/src/plumpy/event_helper.py +++ b/src/plumpy/event_helper.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Any, Callable, Optional, Self from plumpy.loaders import ObjectLoader -from plumpy.persistence import LoadSaveContext, Savable, auto_load, auto_save, ensure_object_loader +from plumpy.persistence import LoadSaveContext, auto_load, auto_save, ensure_object_loader from plumpy.utils import SAVED_STATE_TYPE from . import persistence diff --git a/src/plumpy/persistence.py b/src/plumpy/persistence.py index 335efb9b..782758cc 100644 --- a/src/plumpy/persistence.py +++ b/src/plumpy/persistence.py @@ -10,7 +10,6 @@ import inspect import os import pickle -from types import MethodType from typing import ( TYPE_CHECKING, Any, @@ -20,19 +19,16 @@ Generator, Iterable, List, - MutableMapping, Optional, Protocol, Self, - Type, TypeVar, - cast, runtime_checkable, ) import yaml -from . import futures, loaders, utils +from . import futures, loaders from .utils import PID_TYPE, SAVED_STATE_TYPE PersistedCheckpoint = collections.namedtuple('PersistedCheckpoint', ['pid', 'tag']) @@ -95,6 +91,7 @@ def unbundle(self, load_context: LoadSaveContext | None = None) -> 'Savable': return load(self, load_context) +# FIXME: saved_state -> saved to not confuse with process state def load(saved_state: SAVED_STATE_TYPE, load_context: LoadSaveContext | None = None) -> 'Savable': """ Load a `Savable` from a saved instance state. The load context is a way of passing diff --git a/src/plumpy/process_states.py b/src/plumpy/process_states.py index eeaf92d8..d2d38d4d 100644 --- a/src/plumpy/process_states.py +++ b/src/plumpy/process_states.py @@ -6,7 +6,6 @@ from enum import Enum from types import TracebackType from typing import ( - TYPE_CHECKING, Any, Awaitable, Callable, diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 679853ce..5998817f 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- import asyncio +from typing import Any import unittest import yaml import plumpy +from plumpy.loaders import DefaultObjectLoader, ObjectLoader from plumpy.persistence import auto_load, auto_persist, auto_save, ensure_object_loader from plumpy.utils import SAVED_STATE_TYPE @@ -92,6 +94,14 @@ def save(self, loader=None) -> SAVED_STATE_TYPE: return out_state +class CustomObjectLoader(DefaultObjectLoader): + def load_object(self, identifier: str) -> Any: + return super().load_object(identifier) + + def identify_object(self, obj: Any) -> str: + return super().identify_object(obj) + + class TestSavable(unittest.TestCase): def test_empty_savable(self): self._save_round_trip(SaveEmpty()) @@ -121,7 +131,7 @@ def _save_round_trip(self, savable): def _save_round_trip_with_loader(self, savable): """ - Do a round trip: + Do a round trip, use a custom loader: 1) Save `savables` state 2) Recreate from the saved state 3) Save the state of the recreated `Savable` @@ -129,7 +139,7 @@ def _save_round_trip_with_loader(self, savable): :type savable: :class:`plumpy.Savable` """ - object_loader = plumpy.get_object_loader() + object_loader = CustomObjectLoader() saved_state1 = savable.save(object_loader) loaded = savable.recreate_from(saved_state1) saved_state2 = loaded.save(object_loader) From e1b68d7694ab43b6f0e7c92b2abc823ead82b30b Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Wed, 22 Jan 2025 21:57:14 +0100 Subject: [PATCH 47/64] Test to show the default loader can only load obj in python path --- tests/test_process_states.py | 80 +++++++++++++++++++++++++++--------- 1 file changed, 61 insertions(+), 19 deletions(-) diff --git a/tests/test_process_states.py b/tests/test_process_states.py index 7b18a1be..df913522 100644 --- a/tests/test_process_states.py +++ b/tests/test_process_states.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from typing import Any import pytest @@ -31,53 +32,94 @@ def proc() -> 'StateMachine': return DummyProcess() -def test_create_savable(proc: StateMachine): - state = Created(proc, run_fn=lambda: None) +def test_create_savable(proc: DummyProcess): + state = Created(proc, run_fn=proc.run) assert isinstance(state, Savable) -def test_running_savable(proc: StateMachine): - state = Running(proc, run_fn=lambda: None) +def test_running_savable(proc: DummyProcess): + state = Running(proc, run_fn=proc.run) assert isinstance(state, Savable) + ctx = LoadSaveContext(process=proc) + saved_state = state.save() + loaded_state = load(saved_state=saved_state, load_context=ctx) + saved_state2 = loaded_state.save() -def test_waiting_savable(proc: StateMachine): - state = Waiting(proc, done_callback=lambda: None) + assert saved_state == saved_state2 + + +def test_waiting_savable(proc: DummyProcess): + state = Waiting(proc, done_callback=proc.run) assert isinstance(state, Savable) + ctx = LoadSaveContext(process=proc) + saved_state = state.save() + loaded_state = load(saved_state=saved_state, load_context=ctx) + saved_state2 = loaded_state.save() + + assert saved_state == saved_state2 + def test_excepted_savable(): state = Excepted(exception=ValueError('dummy')) assert isinstance(state, Savable) + saved_state = state.save() + loaded_state = load(saved_state=saved_state) + saved_state2 = loaded_state.save() + + assert saved_state == saved_state2 + def test_finished_savable(): state = Finished(result='done', successful=True) assert isinstance(state, Savable) + saved_state = state.save() + loaded_state = load(saved_state=saved_state) + saved_state2 = loaded_state.save() + + assert saved_state == saved_state2 + def test_killed_savable(): state = Killed(msg=MessageBuilder.kill('kill it')) assert isinstance(state, Savable) + saved_state = state.save() + loaded_state = load(saved_state=saved_state) + saved_state2 = loaded_state.save() + + assert saved_state == saved_state2 + + +class DummyCmd(Command): + pass + def test_subclass_command_savable(): - class DummyCmd(Command): - pass + cmd = DummyCmd() + assert isinstance(cmd, Savable) - assert isinstance(DummyCmd(), Savable) + saved = cmd.save() + loaded = load(saved_state=saved) + saved2 = loaded.save() + assert saved == saved2 -def test_create_save_load(proc: DummyProcess): - state = Created(proc, run_fn=proc.run) - ctx = LoadSaveContext(process=proc) - saved_state = state.save() - loaded_state = load(saved_state=saved_state, load_context=ctx) - saved_state2 = loaded_state.save() - assert saved_state == saved_state2 +# FIXME: using pickle loader this should be able to be solved +@pytest.mark.xfail(reason='the default loader can only load obj from python path') +def test_subclass_command_savable_xfail(): + class DummyCmdXfail(Command): + pass + cmd = DummyCmdXfail() + assert isinstance(cmd, Savable) -def test_running_save_load(proc: StateMachine): - state = Running(proc, run_fn=lambda: None) - assert isinstance(state, Savable) + saved = cmd.save() + loaded = load(saved_state=saved) + saved2 = loaded.save() + + assert saved == saved2 From 31d9d2a8b079eb57d0a69fc301e29d8f641a0657 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Wed, 22 Jan 2025 22:22:12 +0100 Subject: [PATCH 48/64] Process is savable --- tests/test_processes.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_processes.py b/tests/test_processes.py index b7424482..9fc335dc 100644 --- a/tests/test_processes.py +++ b/tests/test_processes.py @@ -11,13 +11,11 @@ import plumpy from plumpy import BundleKeys, Process, ProcessState from plumpy.message import MESSAGE_TEXT_KEY, MessageBuilder +from plumpy.persistence import Savable from plumpy.utils import AttributesFrozendict from . import utils -# FIXME: also that any process is savable # FIXME: any process listener is savable -# FIXME: any process control commands are savable - class ForgetToCallParent(plumpy.Process): def __init__(self, forget_on): @@ -44,6 +42,9 @@ def on_kill(self, msg): if self.forget_on != 'kill': super().on_kill(msg) +def test_process_is_savable(): + proc = utils.DummyProcess() + assert isinstance(proc, Savable) @pytest.mark.asyncio async def test_process_scope(): From f428eeccea38f6ab437bd5cdb41d8da320e35df4 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Wed, 22 Jan 2025 22:22:32 +0100 Subject: [PATCH 49/64] ProcessListener is savable --- src/plumpy/process_listener.py | 13 +++++-------- tests/test_process_listener.py | 14 ++++++++++++++ tests/test_process_states.py | 2 +- tests/utils.py | 1 + 4 files changed, 21 insertions(+), 9 deletions(-) create mode 100644 tests/test_process_listener.py diff --git a/src/plumpy/process_listener.py b/src/plumpy/process_listener.py index 8e9673bb..280137b0 100644 --- a/src/plumpy/process_listener.py +++ b/src/plumpy/process_listener.py @@ -1,19 +1,16 @@ # -*- coding: utf-8 -*- import abc -from typing import TYPE_CHECKING, Any, Dict, Optional +from typing import TYPE_CHECKING, Any, Dict, Optional, Self +from plumpy.loaders import ObjectLoader from plumpy.persistence import LoadSaveContext, auto_save, ensure_object_loader from . import persistence from .utils import SAVED_STATE_TYPE if TYPE_CHECKING: - from plumpy.persistence import Savable - from .processes import Process -# FIXME: test any process listener is a savable - @persistence.auto_persist('_params') class ProcessListener(metaclass=abc.ABCMeta): @@ -27,7 +24,7 @@ def init(self, **kwargs: Any) -> None: self._params = kwargs @classmethod - def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> 'Savable': + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> Self: """ Recreate a :class:`Savable` from a saved state using an optional load context. @@ -42,8 +39,8 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa obj.init(**saved_state['_params']) return obj - def save(self, save_context: Optional[LoadSaveContext] = None) -> SAVED_STATE_TYPE: - out_state: SAVED_STATE_TYPE = auto_save(self, save_context) + def save(self, loader: ObjectLoader | None = None) -> SAVED_STATE_TYPE: + out_state: SAVED_STATE_TYPE = auto_save(self, loader) return out_state diff --git a/tests/test_process_listener.py b/tests/test_process_listener.py new file mode 100644 index 00000000..37d41593 --- /dev/null +++ b/tests/test_process_listener.py @@ -0,0 +1,14 @@ +from plumpy.persistence import Savable, load +from tests.utils import DummyProcess, ProcessListenerTester + + +def test_process_listener_savable(): + proc = DummyProcess() + pl = ProcessListenerTester(proc, ('killed')) + assert isinstance(pl, Savable) + + saved = pl.save() + loaded = load(saved_state=saved) + saved2 = loaded.save() + + assert saved == saved2 diff --git a/tests/test_process_states.py b/tests/test_process_states.py index df913522..07f41634 100644 --- a/tests/test_process_states.py +++ b/tests/test_process_states.py @@ -109,7 +109,7 @@ def test_subclass_command_savable(): assert saved == saved2 -# FIXME: using pickle loader this should be able to be solved +# TODO: using pickle loader this should be able to be solved @pytest.mark.xfail(reason='the default loader can only load obj from python path') def test_subclass_command_savable_xfail(): class DummyCmdXfail(Command): diff --git a/tests/utils.py b/tests/utils.py index 18082fd4..9a1690e3 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -338,6 +338,7 @@ def last_step(self): class ProcessListenerTester(plumpy.ProcessListener): def __init__(self, process, expected_events): + super().__init__() process.add_process_listener(self) self.expected_events = set(expected_events) self.called = set() From 8f85ea667ba4063592acee4e4bdf3287a3e2aa0d Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Wed, 22 Jan 2025 23:45:48 +0100 Subject: [PATCH 50/64] test workchain is savable --- tests/test_workchains.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/test_workchains.py b/tests/test_workchains.py index 4e34d2b4..6d55c611 100644 --- a/tests/test_workchains.py +++ b/tests/test_workchains.py @@ -11,8 +11,7 @@ from . import utils -# FIXME: after deabstract on savable into a protocol, test that all stepper are savable -# FIXME: workchani itself is savable +# FIXME: test steppers are savable and round trip persistence class Wf(WorkChain): # Keep track of which steps were completed by the workflow @@ -87,6 +86,11 @@ def _set_finished(self, function_name): self.finished_steps[function_name] = True +def test_workchain_is_savable(): + w = Wf(inputs=dict(value='A', n=3)) + assert isinstance(w, Savable) + + class IfTest(WorkChain): @classmethod def define(cls, spec): From 64929b72a7aad199d11bb5a0b4804a484f5fbeb9 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Wed, 22 Jan 2025 23:46:25 +0100 Subject: [PATCH 51/64] support save/load collection data in auto_persist --- src/plumpy/persistence.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/plumpy/persistence.py b/src/plumpy/persistence.py index 782758cc..f1cce161 100644 --- a/src/plumpy/persistence.py +++ b/src/plumpy/persistence.py @@ -436,6 +436,7 @@ def ensure_object_loader(context: Optional['LoadSaveContext'], saved_state: SAVE META__TYPES: str = 'types' META__TYPE__METHOD: str = 'm' META__TYPE__SAVABLE: str = 'S' +META__TYPE__COLLECTION: str = 'coll' class SaveUtil: @@ -516,6 +517,7 @@ def auto_save(obj: Savable, loader: loaders.ObjectLoader | None = None) -> SAVED # Save object class name SaveUtil.set_class_name(out_state, loader.identify_object(obj.__class__)) + # FIXME: it should be an iter call to save until all resolved if isinstance(obj, SavableWithAutoPersist): for member in obj._auto_persist: value = getattr(obj, member) @@ -530,6 +532,16 @@ def auto_save(obj: Savable, loader: loaders.ObjectLoader | None = None) -> SAVED # of lhs condition. SaveUtil.set_meta_type(out_state, member, META__TYPE__SAVABLE) value = value.save() + elif isinstance(value, (set, list)): + SaveUtil.set_meta_type(out_state, member, META__TYPE__COLLECTION) + value = [v.save() for v in value if isinstance(v, Savable)] + value_ = [] + for v in value: + if isinstance(v, Savable): + value_.append(v.save()) + else: + value_.append(copy.deepcopy(v)) + value = value_ else: value = copy.deepcopy(value) out_state[member] = value @@ -548,6 +560,8 @@ def load_auto_persist_params( value = getattr(obj, value) elif typ == META__TYPE__SAVABLE: value = load(value, load_context) + elif typ == META__TYPE__COLLECTION: + value = [load(v, load_context) for v in value] setattr(obj, member, value) From 9846280eeb127efedd89df388fbba24bfa9214e4 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Wed, 22 Jan 2025 23:46:39 +0100 Subject: [PATCH 52/64] test_event_helper_savable --- src/plumpy/event_helper.py | 12 ++++++------ src/plumpy/persistence.py | 2 +- tests/test_event_helper.py | 21 +++++++++++++++++++++ tests/test_processes.py | 3 ++- 4 files changed, 30 insertions(+), 8 deletions(-) create mode 100644 tests/test_event_helper.py diff --git a/src/plumpy/event_helper.py b/src/plumpy/event_helper.py index 188ae735..9d70e1c4 100644 --- a/src/plumpy/event_helper.py +++ b/src/plumpy/event_helper.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- import logging -from typing import TYPE_CHECKING, Any, Callable, Optional, Self +from typing import TYPE_CHECKING, Any, Callable, Optional, Self, final from plumpy.loaders import ObjectLoader from plumpy.persistence import LoadSaveContext, auto_load, auto_save, ensure_object_loader @@ -9,20 +9,20 @@ from . import persistence if TYPE_CHECKING: - from typing import Set, Type - from .process_listener import ProcessListener _LOGGER = logging.getLogger(__name__) +# FIXME: test me +@final @persistence.auto_persist('_listeners', '_listener_type') class EventHelper: - def __init__(self, listener_type: 'Type[ProcessListener]'): + def __init__(self, listener_type: 'type[ProcessListener]'): assert listener_type is not None, 'Must provide valid listener type' self._listener_type = listener_type - self._listeners: 'Set[ProcessListener]' = set() + self._listeners: 'set[ProcessListener]' = set() def add_listener(self, listener: 'ProcessListener') -> None: assert isinstance(listener, self._listener_type), 'Listener is not of right type' @@ -55,7 +55,7 @@ def save(self, loader: ObjectLoader | None = None) -> SAVED_STATE_TYPE: return out_state @property - def listeners(self) -> 'Set[ProcessListener]': + def listeners(self) -> 'set[ProcessListener]': return self._listeners def fire_event(self, event_function: Callable[..., Any], *args: Any, **kwargs: Any) -> None: diff --git a/src/plumpy/persistence.py b/src/plumpy/persistence.py index f1cce161..bcc037ae 100644 --- a/src/plumpy/persistence.py +++ b/src/plumpy/persistence.py @@ -517,7 +517,7 @@ def auto_save(obj: Savable, loader: loaders.ObjectLoader | None = None) -> SAVED # Save object class name SaveUtil.set_class_name(out_state, loader.identify_object(obj.__class__)) - # FIXME: it should be an iter call to save until all resolved + # TODO: it should be an regression call to save until all resolved if isinstance(obj, SavableWithAutoPersist): for member in obj._auto_persist: value = getattr(obj, member) diff --git a/tests/test_event_helper.py b/tests/test_event_helper.py new file mode 100644 index 00000000..fc2310fa --- /dev/null +++ b/tests/test_event_helper.py @@ -0,0 +1,21 @@ +from plumpy.event_helper import EventHelper +from plumpy.persistence import Savable, load +from tests.utils import DummyProcess, ProcessListenerTester + + +def test_event_helper_savable(): + eh = EventHelper(ProcessListenerTester) + + proc = DummyProcess() + pl1 = ProcessListenerTester(proc, ('killed')) + pl2 = ProcessListenerTester(proc, ('paused')) + eh.add_listener(pl1) + eh.add_listener(pl2) + + assert isinstance(eh, Savable) + + saved = eh.save() + loaded = load(saved_state=saved) + saved2 = loaded.save() + + assert saved == saved2 diff --git a/tests/test_processes.py b/tests/test_processes.py index 9fc335dc..308d2fb1 100644 --- a/tests/test_processes.py +++ b/tests/test_processes.py @@ -15,7 +15,6 @@ from plumpy.utils import AttributesFrozendict from . import utils -# FIXME: any process listener is savable class ForgetToCallParent(plumpy.Process): def __init__(self, forget_on): @@ -42,10 +41,12 @@ def on_kill(self, msg): if self.forget_on != 'kill': super().on_kill(msg) + def test_process_is_savable(): proc = utils.DummyProcess() assert isinstance(proc, Savable) + @pytest.mark.asyncio async def test_process_scope(): class ProcessTaskInterleave(plumpy.Process): From 24d73a09b9cc93e6ba6cd55559dbc549ff529434 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Thu, 23 Jan 2025 01:02:29 +0100 Subject: [PATCH 53/64] Move test_workchain and test func_stepper savable --- src/plumpy/event_helper.py | 7 +- src/plumpy/persistence.py | 8 +- src/plumpy/process_listener.py | 6 +- src/plumpy/process_states.py | 13 ++- src/plumpy/processes.py | 2 +- src/plumpy/workchains.py | 16 ++-- tests/test_event_helper.py | 1 + tests/test_process_listener.py | 1 + tests/test_processes.py | 2 + tests/workchain/__init__.py | 0 tests/workchain/test_steppers.py | 105 +++++++++++++++++++++++ tests/{ => workchain}/test_workchains.py | 5 +- 12 files changed, 142 insertions(+), 24 deletions(-) create mode 100644 tests/workchain/__init__.py create mode 100644 tests/workchain/test_steppers.py rename tests/{ => workchain}/test_workchains.py (99%) diff --git a/src/plumpy/event_helper.py b/src/plumpy/event_helper.py index 9d70e1c4..47188031 100644 --- a/src/plumpy/event_helper.py +++ b/src/plumpy/event_helper.py @@ -1,6 +1,10 @@ # -*- coding: utf-8 -*- +from __future__ import annotations + import logging -from typing import TYPE_CHECKING, Any, Callable, Optional, Self, final +from typing import TYPE_CHECKING, Any, Callable, Optional, final + +from typing_extensions import Self from plumpy.loaders import ObjectLoader from plumpy.persistence import LoadSaveContext, auto_load, auto_save, ensure_object_loader @@ -13,7 +17,6 @@ _LOGGER = logging.getLogger(__name__) -# FIXME: test me @final @persistence.auto_persist('_listeners', '_listener_type') diff --git a/src/plumpy/persistence.py b/src/plumpy/persistence.py index bcc037ae..02b5ff76 100644 --- a/src/plumpy/persistence.py +++ b/src/plumpy/persistence.py @@ -21,12 +21,12 @@ List, Optional, Protocol, - Self, TypeVar, runtime_checkable, ) import yaml +from typing_extensions import Self from . import futures, loaders from .utils import PID_TYPE, SAVED_STATE_TYPE @@ -577,18 +577,18 @@ def auto_load(cls: type[T], saved_state: SAVED_STATE_TYPE, load_context: LoadSav def auto_persist(*members: str) -> Callable[[type[T]], type[T]]: def wrapped(cls: type[T]) -> type[T]: - if not hasattr(cls, '_auto_persist') or cls._auto_persist is None: + if not hasattr(cls, '_auto_persist') or cls._auto_persist is None: # type: ignore[attr-defined] cls._auto_persist = set() # type: ignore[attr-defined] else: - cls._auto_persist = set(cls._auto_persist) + cls._auto_persist = set(cls._auto_persist) # type: ignore[attr-defined] cls._auto_persist.update(members) # type: ignore[attr-defined] - # XXX: validate on `save` and `recreate_from` method?? return cls return wrapped +# FIXME: test me after clear event loop management @auto_persist('_state', '_result') class SavableFuture(futures.Future): """ diff --git a/src/plumpy/process_listener.py b/src/plumpy/process_listener.py index 280137b0..8bc7c828 100644 --- a/src/plumpy/process_listener.py +++ b/src/plumpy/process_listener.py @@ -1,6 +1,10 @@ # -*- coding: utf-8 -*- +from __future__ import annotations + import abc -from typing import TYPE_CHECKING, Any, Dict, Optional, Self +from typing import TYPE_CHECKING, Any, Dict, Optional + +from typing_extensions import Self from plumpy.loaders import ObjectLoader from plumpy.persistence import LoadSaveContext, auto_save, ensure_object_loader diff --git a/src/plumpy/process_states.py b/src/plumpy/process_states.py index d2d38d4d..9874e616 100644 --- a/src/plumpy/process_states.py +++ b/src/plumpy/process_states.py @@ -11,7 +11,6 @@ Callable, ClassVar, Optional, - Self, Tuple, Type, Union, @@ -20,7 +19,7 @@ ) import yaml -from typing_extensions import override +from typing_extensions import Self, override from yaml.loader import Loader from plumpy.loaders import ObjectLoader @@ -268,7 +267,7 @@ class Running: COMMAND = 'command' # The key used to store an upcoming command # Class level defaults - _command: Kill | Stop | Wait | Continue | None = None + _command: Command | None = None _running: bool = False _run_handle = None @@ -311,13 +310,13 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa obj.run_fn = ensure_coroutine(getattr(obj.process, saved_state[obj.RUN_FN])) if obj.COMMAND in saved_state: loaded_cmd = persistence.load(saved_state[obj.COMMAND], load_context) - if isinstance(loaded_cmd, Command): + if not isinstance(loaded_cmd, Command): # runtime check for loading from persistence - obj._command = loaded_cmd - else: # XXX: debug log in principle unreachable raise RuntimeError(f'command `{obj.COMMAND}` loaded from Running state not a valid `Command` type') + obj._command = loaded_cmd + return obj def interrupt(self, reason: Any) -> None: @@ -354,7 +353,7 @@ async def execute(self) -> st.State: next_state = self._action_command(command) return next_state - def _action_command(self, command: Union[Kill, Stop, Wait, Continue]) -> st.State: + def _action_command(self, command: Command) -> st.State: if isinstance(command, Kill): state = st.create_state(self.process, ProcessState.KILLED, msg=command.msg) # elif isinstance(command, Pause): diff --git a/src/plumpy/processes.py b/src/plumpy/processes.py index 24db39a4..9d985344 100644 --- a/src/plumpy/processes.py +++ b/src/plumpy/processes.py @@ -27,7 +27,6 @@ Hashable, List, Optional, - Self, Sequence, Tuple, Type, @@ -37,6 +36,7 @@ ) import kiwipy +from typing_extensions import Self from plumpy.coordinator import Coordinator from plumpy.loaders import ObjectLoader diff --git a/src/plumpy/workchains.py b/src/plumpy/workchains.py index 9412b490..e6e527f3 100644 --- a/src/plumpy/workchains.py +++ b/src/plumpy/workchains.py @@ -16,7 +16,6 @@ MutableSequence, Optional, Protocol, - Self, Sequence, Tuple, Type, @@ -24,6 +23,8 @@ cast, ) +from typing_extensions import Self + from plumpy import utils from plumpy.base import state_machine from plumpy.base.utils import call_with_super_check @@ -399,7 +400,7 @@ def create_stepper(self, workchain: 'WorkChain') -> _FunctionStepper: def recreate_stepper(self, saved_state: SAVED_STATE_TYPE, workchain: 'WorkChain') -> _FunctionStepper: load_context = persistence.LoadSaveContext(workchain=workchain, func_spec=self) - return cast(_FunctionStepper, _FunctionStepper.recreate_from(saved_state, load_context)) + return _FunctionStepper.recreate_from(saved_state, load_context) def get_description(self) -> str: desc = self._fn.__name__ @@ -479,6 +480,7 @@ class _Block(_Instruction, collections.abc.Sequence): Represents a block of instructions i.e. a sequential list of instructions. """ + # XXX: swap workchain and instructions def __init__(self, instructions: Sequence[Union[_Instruction, WC_COMMAND_TYPE]]) -> None: # Build up the list of commands comms: MutableSequence[_Instruction | _FunctionCall] = [] @@ -502,7 +504,7 @@ def create_stepper(self, workchain: 'WorkChain') -> _BlockStepper: def recreate_stepper(self, saved_state: SAVED_STATE_TYPE, workchain: 'WorkChain') -> _BlockStepper: load_context = persistence.LoadSaveContext(workchain=workchain, block_instruction=self) - return cast(_BlockStepper, _BlockStepper.recreate_from(saved_state, load_context)) + return _BlockStepper.recreate_from(saved_state, load_context) def get_description(self) -> List[str]: return [instruction.get_description() for instruction in self._instruction] @@ -551,7 +553,7 @@ def is_true(self, workflow: 'WorkChain') -> bool: return result - def __call__(self, *instructions: Union[_Instruction, WC_COMMAND_TYPE]) -> _Instruction: + def __call__(self, *instructions: _Instruction | WC_COMMAND_TYPE) -> _Instruction: assert self._body is None, 'Instructions have already been set' self._body = _Block(instructions) return self._parent @@ -562,6 +564,7 @@ def __str__(self) -> str: @persistence.auto_persist('_pos') class _IfStepper: + # XXX: swap workchain and if_instruction def __init__(self, if_instruction: '_If', workchain: 'WorkChain') -> None: self._workchain = workchain self._if_instruction = if_instruction @@ -671,7 +674,7 @@ def create_stepper(self, workchain: 'WorkChain') -> _IfStepper: def recreate_stepper(self, saved_state: SAVED_STATE_TYPE, workchain: 'WorkChain') -> _IfStepper: load_context = persistence.LoadSaveContext(workchain=workchain, if_instruction=self) - return cast(_IfStepper, _IfStepper.recreate_from(saved_state, load_context)) + return _IfStepper.recreate_from(saved_state, load_context) def get_description(self) -> Mapping[str, Any]: description = collections.OrderedDict() @@ -759,7 +762,7 @@ def create_stepper(self, workchain: 'WorkChain') -> _WhileStepper: def recreate_stepper(self, saved_state: SAVED_STATE_TYPE, workchain: 'WorkChain') -> _WhileStepper: load_context = persistence.LoadSaveContext(workchain=workchain, while_instruction=self) - return cast(_WhileStepper, _WhileStepper.recreate_from(saved_state, load_context)) + return _WhileStepper.recreate_from(saved_state, load_context) def get_description(self) -> Dict[str, Any]: return {f'while({self.predicate.__name__})': self.body.get_description()} @@ -771,7 +774,6 @@ def __init__(self, exit_code: Optional[EXIT_CODE_TYPE]) -> None: self.exit_code = exit_code -@persistence.auto_persist() class _ReturnStepper: def __init__(self, return_instruction: '_Return', workchain: 'WorkChain') -> None: self._workchain = workchain diff --git a/tests/test_event_helper.py b/tests/test_event_helper.py index fc2310fa..a8351dd4 100644 --- a/tests/test_event_helper.py +++ b/tests/test_event_helper.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from plumpy.event_helper import EventHelper from plumpy.persistence import Savable, load from tests.utils import DummyProcess, ProcessListenerTester diff --git a/tests/test_process_listener.py b/tests/test_process_listener.py index 37d41593..4223b4cc 100644 --- a/tests/test_process_listener.py +++ b/tests/test_process_listener.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from plumpy.persistence import Savable, load from tests.utils import DummyProcess, ProcessListenerTester diff --git a/tests/test_processes.py b/tests/test_processes.py index 308d2fb1..7e6b82c8 100644 --- a/tests/test_processes.py +++ b/tests/test_processes.py @@ -46,6 +46,8 @@ def test_process_is_savable(): proc = utils.DummyProcess() assert isinstance(proc, Savable) + # TODO: direct save load round trip regression + @pytest.mark.asyncio async def test_process_scope(): diff --git a/tests/workchain/__init__.py b/tests/workchain/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/workchain/test_steppers.py b/tests/workchain/test_steppers.py new file mode 100644 index 00000000..6bd5a07b --- /dev/null +++ b/tests/workchain/test_steppers.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +import pytest +from plumpy.base.state_machine import StateMachine +from plumpy.persistence import LoadSaveContext, Savable, load +from plumpy.workchains import WorkChain, if_, while_ + + +class DummyWc(WorkChain): + @classmethod + def define(cls, spec): + super().define(spec) + spec.outline( + cls.do_nothing, + if_(cls.cond)(cls.do_cond), + while_(cls.wcond)( + cls.do_wcond, + ), + ) + + @staticmethod + def do_nothing(_wc: WorkChain) -> None: + pass + + @staticmethod + def cond(_wc: WorkChain) -> bool: + return True + + @staticmethod + def do_cond(_wc: WorkChain) -> None: + pass + + @staticmethod + def wcond(_wc: WorkChain) -> bool: + return True + + @staticmethod + def do_wcond(_wc: WorkChain) -> None: + pass + + +@pytest.fixture(scope='function') +def wc() -> StateMachine: + return DummyWc() + + +def test_func_stepper_savable(wc: DummyWc): + from plumpy.workchains import _FunctionStepper + + fs = _FunctionStepper(workchain=wc, fn=wc.do_nothing) + assert isinstance(fs, Savable) + + ctx = LoadSaveContext(workchain=wc) + saved_state = fs.save() + loaded_state = load(saved_state=saved_state, load_context=ctx) + saved_state2 = loaded_state.save() + + assert saved_state == saved_state2 + + +def test_block_stepper_savable(wc: DummyWc): + """block stepper test with a dummy function call""" + from plumpy.workchains import _BlockStepper, _FunctionCall + + block = [_FunctionCall(wc.do_nothing)] + bs = _BlockStepper(block=block, workchain=wc) + assert isinstance(bs, Savable) + + ctx = LoadSaveContext(workchain=wc, block_instruction=block) + saved_state = bs.save() + loaded_state = load(saved_state=saved_state, load_context=ctx) + saved_state2 = loaded_state.save() + + assert saved_state == saved_state2 + + +def test_if_stepper_savable(wc: DummyWc): + """block stepper test with a dummy function call""" + from plumpy.workchains import _If, _IfStepper + + dummy_if = _If(wc.cond) + ifs = _IfStepper(if_instruction=dummy_if, workchain=wc) + assert isinstance(ifs, Savable) + + ctx = LoadSaveContext(workchain=wc, if_instruction=ifs) + saved_state = ifs.save() + loaded_state = load(saved_state=saved_state, load_context=ctx) + saved_state2 = loaded_state.save() + + assert saved_state == saved_state2 + + +def test_while_stepper_savable(wc: DummyWc): + """block stepper test with a dummy function call""" + from plumpy.workchains import _While, _WhileStepper + + dummy_while = _While(wc.cond) + wfs = _WhileStepper(while_instruction=dummy_while, workchain=wc) + assert isinstance(wfs, Savable) + + ctx = LoadSaveContext(workchain=wc, while_instruction=wfs) + saved_state = wfs.save() + loaded_state = load(saved_state=saved_state, load_context=ctx) + saved_state2 = loaded_state.save() + + assert saved_state == saved_state2 diff --git a/tests/test_workchains.py b/tests/workchain/test_workchains.py similarity index 99% rename from tests/test_workchains.py rename to tests/workchain/test_workchains.py index 6d55c611..cb76020c 100644 --- a/tests/test_workchains.py +++ b/tests/workchain/test_workchains.py @@ -9,9 +9,8 @@ from plumpy.process_listener import ProcessListener from plumpy.workchains import * -from . import utils +from .. import utils -# FIXME: test steppers are savable and round trip persistence class Wf(WorkChain): # Keep track of which steps were completed by the workflow @@ -90,6 +89,8 @@ def test_workchain_is_savable(): w = Wf(inputs=dict(value='A', n=3)) assert isinstance(w, Savable) + # TODO: direct regression save load round trip + class IfTest(WorkChain): @classmethod From 718e2b2a25c263fe936e66cb483964d30682f717 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Thu, 23 Jan 2025 12:26:50 +0100 Subject: [PATCH 54/64] Drop support for python 3.8/3.9 --- .github/workflows/cd.yml | 2 +- .github/workflows/ci.yml | 2 +- docs/source/conf.py | 2 +- pyproject.toml | 4 +- uv.lock | 1200 ++------------------------------------ 5 files changed, 50 insertions(+), 1160 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 732caffb..fbbfe3da 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -58,7 +58,7 @@ jobs: strategy: matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.10', '3.11', '3.12'] services: postgres: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8d813780..25678ac2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: strategy: matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.10', '3.11', '3.12'] fail-fast: false services: diff --git a/docs/source/conf.py b/docs/source/conf.py index b1a2a019..d5a0d3f9 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -47,7 +47,7 @@ pygments_style = None intersphinx_mapping = { - 'python': ('https://docs.python.org/3.8', None), + 'python': ('https://docs.python.org/3.10', None), 'kiwipy': ('https://kiwipy.readthedocs.io/en/latest/', None), } diff --git a/pyproject.toml b/pyproject.toml index 574173d3..4989e624 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,14 +20,12 @@ classifiers = [ 'License :: OSI Approved :: MIT License', 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 'Programming Language :: Python', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', ] keywords = ['workflow', 'multithreaded', 'rabbitmq'] -requires-python = '>=3.8' +requires-python = '>=3.10' dependencies = [ 'kiwipy[rmq]~=0.8.5', 'nest_asyncio~=1.5,>=1.5.1', diff --git a/uv.lock b/uv.lock index d8fc89f5..934fa850 100644 --- a/uv.lock +++ b/uv.lock @@ -1,10 +1,5 @@ version = 1 -requires-python = ">=3.8" -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version == '3.9.*'", - "python_full_version < '3.9'", -] +requires-python = ">=3.10" [[package]] name = "aio-pika" @@ -12,8 +7,7 @@ version = "9.4.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiormq" }, - { name = "yarl", version = "1.15.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "yarl", version = "1.18.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "yarl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c9/69/8649bdb97fa1521af3dafe23dbc5debadd4b01abb2850a4d193dae9b0451/aio_pika-9.4.3.tar.gz", hash = "sha256:fd2b1fce25f6ed5203ef1dd554dc03b90c9a46a64aaf758d032d78dc31e5295d", size = 47693 } wheels = [ @@ -26,8 +20,7 @@ version = "6.8.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pamqp" }, - { name = "yarl", version = "1.15.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "yarl", version = "1.18.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "yarl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a4/79/5397756a8782bf3d0dce392b48260c3ec81010f16bef8441ff03505dccb4/aiormq-6.8.1.tar.gz", hash = "sha256:a964ab09634be1da1f9298ce225b310859763d5cf83ef3a7eae1a6dc6bd1da1a", size = 30528 } wheels = [ @@ -38,64 +31,26 @@ wheels = [ name = "alabaster" version = "0.7.13" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] sdist = { url = "https://files.pythonhosted.org/packages/94/71/a8ee96d1fd95ca04a0d2e2d9c4081dac4c2d2b12f7ddb899c8cb9bfd1532/alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2", size = 11454 } wheels = [ { url = "https://files.pythonhosted.org/packages/64/88/c7083fc61120ab661c5d0b82cb77079fc1429d3f913a456c1c82cf4658f7/alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3", size = 13857 }, ] -[[package]] -name = "alabaster" -version = "0.7.16" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version == '3.9.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/c9/3e/13dd8e5ed9094e734ac430b5d0eb4f2bb001708a8b7856cbf8e084e001ba/alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", size = 23776 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92", size = 13511 }, -] - [[package]] name = "anyio" version = "4.5.2" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] dependencies = [ - { name = "exceptiongroup", marker = "python_full_version < '3.9'" }, - { name = "idna", marker = "python_full_version < '3.9'" }, - { name = "sniffio", marker = "python_full_version < '3.9'" }, - { name = "typing-extensions", marker = "python_full_version < '3.9'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4d/f9/9a7ce600ebe7804daf90d4d48b1c0510a4561ddce43a596be46676f82343/anyio-4.5.2.tar.gz", hash = "sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b", size = 171293 } wheels = [ { url = "https://files.pythonhosted.org/packages/1b/b4/f7e396030e3b11394436358ca258a81d6010106582422f23443c16ca1873/anyio-4.5.2-py3-none-any.whl", hash = "sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f", size = 89766 }, ] -[[package]] -name = "anyio" -version = "4.8.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version == '3.9.*'", -] -dependencies = [ - { name = "exceptiongroup", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, - { name = "idna", marker = "python_full_version >= '3.9'" }, - { name = "sniffio", marker = "python_full_version >= '3.9'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.9' and python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 }, -] - [[package]] name = "appnope" version = "0.1.4" @@ -136,11 +91,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8b/95/143cd64feb24a15fa4b189a3e1e7efbaeeb00f39a51e99b26fc62fbacabd/argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082", size = 27698 }, { url = "https://files.pythonhosted.org/packages/37/2c/e34e47c7dee97ba6f01a6203e0383e15b60fb85d78ac9a15cd066f6fe28b/argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f", size = 30817 }, { url = "https://files.pythonhosted.org/packages/5a/e4/bf8034d25edaa495da3c8a3405627d2e35758e44ff6eaa7948092646fdcc/argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93", size = 53104 }, - { url = "https://files.pythonhosted.org/packages/34/da/d105a3235ae86c1c1a80c1e9c46953e6e53cc8c4c61fb3c5ac8a39bbca48/argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3b9ef65804859d335dc6b31582cad2c5166f0c3e7975f324d9ffaa34ee7e6583", size = 23689 }, - { url = "https://files.pythonhosted.org/packages/43/f3/20bc53a6e50471dfea16a63dc9b69d2a9ec78fd2b9532cc25f8317e121d9/argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4966ef5848d820776f5f562a7d45fdd70c2f330c961d0d745b784034bd9f48d", size = 28122 }, - { url = "https://files.pythonhosted.org/packages/2e/f1/48888db30b6a4a0c78ab7bc7444058a1135b223b6a2a5f2ac7d6780e7443/argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ef543a89dee4db46a1a6e206cd015360e5a75822f76df533845c3cbaf72670", size = 27882 }, - { url = "https://files.pythonhosted.org/packages/ee/0f/a2260a207f21ce2ff4cad00a417c31597f08eafb547e00615bcbf403d8ea/argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2937d286e2ad0cc79a7087d3c272832865f779430e0cc2b4f3718d3159b0cb", size = 30745 }, - { url = "https://files.pythonhosted.org/packages/ed/55/f8ba268bc9005d0ca57a862e8f1b55bf1775e97a36bd30b0a8fb568c265c/argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5e00316dabdaea0b2dd82d141cc66889ced0cdcbfa599e8b471cf22c620c329a", size = 28587 }, ] [[package]] @@ -156,9 +106,6 @@ wheels = [ name = "babel" version = "2.16.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytz", marker = "python_full_version < '3.9'" }, -] sdist = { url = "https://files.pythonhosted.org/packages/2a/74/f1bc80f23eeba13393b7222b11d95ca3af2c1e28edca18af487137eefed9/babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316", size = 9348104 } wheels = [ { url = "https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", size = 9587599 }, @@ -189,34 +136,15 @@ wheels = [ name = "bleach" version = "6.1.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] dependencies = [ - { name = "six", marker = "python_full_version < '3.9'" }, - { name = "webencodings", marker = "python_full_version < '3.9'" }, + { name = "six" }, + { name = "webencodings" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6d/10/77f32b088738f40d4f5be801daa5f327879eadd4562f36a2b5ab975ae571/bleach-6.1.0.tar.gz", hash = "sha256:0a31f1837963c41d46bbf1331b8778e1308ea0791db03cc4e7357b97cf42a8fe", size = 202119 } wheels = [ { url = "https://files.pythonhosted.org/packages/ea/63/da7237f805089ecc28a3f36bca6a21c31fcbc2eb380f3b8f1be3312abd14/bleach-6.1.0-py3-none-any.whl", hash = "sha256:3225f354cfc436b9789c66c4ee030194bee0568fbf9cbdad3bc8b5c26c5f12b6", size = 162750 }, ] -[[package]] -name = "bleach" -version = "6.2.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version == '3.9.*'", -] -dependencies = [ - { name = "webencodings", marker = "python_full_version >= '3.9'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/76/9a/0e33f5054c54d349ea62c277191c020c2d6ef1d65ab2cb1993f91ec846d1/bleach-6.2.0.tar.gz", hash = "sha256:123e894118b8a599fd80d3ec1a6d4cc7ce4e5882b1317a7e1ba69b56e95f991f", size = 203083 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/55/96142937f66150805c25c4d0f31ee4132fd33497753400734f9dfdcbdc66/bleach-6.2.0-py3-none-any.whl", hash = "sha256:117d9c6097a7c3d22fd578fcd8d35ff1e125df6736f554da4e432fdd63f31e5e", size = 163406 }, -] - [[package]] name = "certifi" version = "2024.12.14" @@ -281,26 +209,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, - { url = "https://files.pythonhosted.org/packages/48/08/15bf6b43ae9bd06f6b00ad8a91f5a8fe1069d4c9fab550a866755402724e/cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b", size = 182457 }, - { url = "https://files.pythonhosted.org/packages/c2/5b/f1523dd545f92f7df468e5f653ffa4df30ac222f3c884e51e139878f1cb5/cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964", size = 425932 }, - { url = "https://files.pythonhosted.org/packages/53/93/7e547ab4105969cc8c93b38a667b82a835dd2cc78f3a7dad6130cfd41e1d/cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9", size = 448585 }, - { url = "https://files.pythonhosted.org/packages/56/c4/a308f2c332006206bb511de219efeff090e9d63529ba0a77aae72e82248b/cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc", size = 456268 }, - { url = "https://files.pythonhosted.org/packages/ca/5b/b63681518265f2f4060d2b60755c1c77ec89e5e045fc3773b72735ddaad5/cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c", size = 436592 }, - { url = "https://files.pythonhosted.org/packages/bb/19/b51af9f4a4faa4a8ac5a0e5d5c2522dcd9703d07fac69da34a36c4d960d3/cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1", size = 446512 }, - { url = "https://files.pythonhosted.org/packages/e2/63/2bed8323890cb613bbecda807688a31ed11a7fe7afe31f8faaae0206a9a3/cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8", size = 171576 }, - { url = "https://files.pythonhosted.org/packages/2f/70/80c33b044ebc79527447fd4fbc5455d514c3bb840dede4455de97da39b4d/cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1", size = 181229 }, - { url = "https://files.pythonhosted.org/packages/b9/ea/8bb50596b8ffbc49ddd7a1ad305035daa770202a6b782fc164647c2673ad/cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", size = 182220 }, - { url = "https://files.pythonhosted.org/packages/ae/11/e77c8cd24f58285a82c23af484cf5b124a376b32644e445960d1a4654c3a/cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", size = 178605 }, - { url = "https://files.pythonhosted.org/packages/ed/65/25a8dc32c53bf5b7b6c2686b42ae2ad58743f7ff644844af7cdb29b49361/cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", size = 424910 }, - { url = "https://files.pythonhosted.org/packages/42/7a/9d086fab7c66bd7c4d0f27c57a1b6b068ced810afc498cc8c49e0088661c/cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", size = 447200 }, - { url = "https://files.pythonhosted.org/packages/da/63/1785ced118ce92a993b0ec9e0d0ac8dc3e5dbfbcaa81135be56c69cabbb6/cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", size = 454565 }, - { url = "https://files.pythonhosted.org/packages/74/06/90b8a44abf3556599cdec107f7290277ae8901a58f75e6fe8f970cd72418/cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", size = 435635 }, - { url = "https://files.pythonhosted.org/packages/bd/62/a1f468e5708a70b1d86ead5bab5520861d9c7eacce4a885ded9faa7729c3/cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", size = 445218 }, - { url = "https://files.pythonhosted.org/packages/5b/95/b34462f3ccb09c2594aa782d90a90b045de4ff1f70148ee79c69d37a0a5a/cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", size = 460486 }, - { url = "https://files.pythonhosted.org/packages/fc/fc/a1e4bebd8d680febd29cf6c8a40067182b64f00c7d105f8f26b5bc54317b/cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", size = 437911 }, - { url = "https://files.pythonhosted.org/packages/e6/c3/21cab7a6154b6a5ea330ae80de386e7665254835b9e98ecc1340b3a7de9a/cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", size = 460632 }, - { url = "https://files.pythonhosted.org/packages/cb/b5/fd9f8b5a84010ca169ee49f4e4ad6f8c05f4e3545b72ee041dbbcb159882/cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", size = 171820 }, - { url = "https://files.pythonhosted.org/packages/8c/52/b08750ce0bce45c143e1b5d7357ee8c55341b52bdef4b0f081af1eb248c2/cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", size = 181290 }, ] [[package]] @@ -370,32 +278,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, - { url = "https://files.pythonhosted.org/packages/10/bd/6517ea94f2672e801011d50b5d06be2a0deaf566aea27bcdcd47e5195357/charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c", size = 195653 }, - { url = "https://files.pythonhosted.org/packages/e5/0d/815a2ba3f283b4eeaa5ece57acade365c5b4135f65a807a083c818716582/charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9", size = 140701 }, - { url = "https://files.pythonhosted.org/packages/aa/17/c94be7ee0d142687e047fe1de72060f6d6837f40eedc26e87e6e124a3fc6/charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8", size = 150495 }, - { url = "https://files.pythonhosted.org/packages/f7/33/557ac796c47165fc141e4fb71d7b0310f67e05cb420756f3a82e0a0068e0/charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6", size = 142946 }, - { url = "https://files.pythonhosted.org/packages/1e/0d/38ef4ae41e9248d63fc4998d933cae22473b1b2ac4122cf908d0f5eb32aa/charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c", size = 144737 }, - { url = "https://files.pythonhosted.org/packages/43/01/754cdb29dd0560f58290aaaa284d43eea343ad0512e6ad3b8b5c11f08592/charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a", size = 147471 }, - { url = "https://files.pythonhosted.org/packages/ba/cd/861883ba5160c7a9bd242c30b2c71074cda2aefcc0addc91118e0d4e0765/charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd", size = 140801 }, - { url = "https://files.pythonhosted.org/packages/6f/7f/0c0dad447819e90b93f8ed238cc8f11b91353c23c19e70fa80483a155bed/charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd", size = 149312 }, - { url = "https://files.pythonhosted.org/packages/8e/09/9f8abcc6fff60fb727268b63c376c8c79cc37b833c2dfe1f535dfb59523b/charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824", size = 152347 }, - { url = "https://files.pythonhosted.org/packages/be/e5/3f363dad2e24378f88ccf63ecc39e817c29f32e308ef21a7a6d9c1201165/charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca", size = 149888 }, - { url = "https://files.pythonhosted.org/packages/e4/10/a78c0e91f487b4ad0ef7480ac765e15b774f83de2597f1b6ef0eaf7a2f99/charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b", size = 145169 }, - { url = "https://files.pythonhosted.org/packages/d3/81/396e7d7f5d7420da8273c91175d2e9a3f569288e3611d521685e4b9ac9cc/charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e", size = 95094 }, - { url = "https://files.pythonhosted.org/packages/40/bb/20affbbd9ea29c71ea123769dc568a6d42052ff5089c5fe23e21e21084a6/charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4", size = 102139 }, - { url = "https://files.pythonhosted.org/packages/7f/c0/b913f8f02836ed9ab32ea643c6fe4d3325c3d8627cf6e78098671cafff86/charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41", size = 197867 }, - { url = "https://files.pythonhosted.org/packages/0f/6c/2bee440303d705b6fb1e2ec789543edec83d32d258299b16eed28aad48e0/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f", size = 141385 }, - { url = "https://files.pythonhosted.org/packages/3d/04/cb42585f07f6f9fd3219ffb6f37d5a39b4fd2db2355b23683060029c35f7/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2", size = 151367 }, - { url = "https://files.pythonhosted.org/packages/54/54/2412a5b093acb17f0222de007cc129ec0e0df198b5ad2ce5699355269dfe/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770", size = 143928 }, - { url = "https://files.pythonhosted.org/packages/5a/6d/e2773862b043dcf8a221342954f375392bb2ce6487bcd9f2c1b34e1d6781/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4", size = 146203 }, - { url = "https://files.pythonhosted.org/packages/b9/f8/ca440ef60d8f8916022859885f231abb07ada3c347c03d63f283bec32ef5/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537", size = 148082 }, - { url = "https://files.pythonhosted.org/packages/04/d2/42fd330901aaa4b805a1097856c2edf5095e260a597f65def493f4b8c833/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496", size = 142053 }, - { url = "https://files.pythonhosted.org/packages/9e/af/3a97a4fa3c53586f1910dadfc916e9c4f35eeada36de4108f5096cb7215f/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78", size = 150625 }, - { url = "https://files.pythonhosted.org/packages/26/ae/23d6041322a3556e4da139663d02fb1b3c59a23ab2e2b56432bd2ad63ded/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7", size = 153549 }, - { url = "https://files.pythonhosted.org/packages/94/22/b8f2081c6a77cb20d97e57e0b385b481887aa08019d2459dc2858ed64871/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6", size = 150945 }, - { url = "https://files.pythonhosted.org/packages/c7/0b/c5ec5092747f801b8b093cdf5610e732b809d6cb11f4c51e35fc28d1d389/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294", size = 146595 }, - { url = "https://files.pythonhosted.org/packages/0c/5a/0b59704c38470df6768aa154cc87b1ac7c9bb687990a1559dc8765e8627e/charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5", size = 95453 }, - { url = "https://files.pythonhosted.org/packages/85/2d/a9790237cb4d01a6d57afadc8573c8b73c609ade20b80f4cda30802009ee/charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765", size = 102811 }, { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, ] @@ -436,9 +318,6 @@ wheels = [ name = "coverage" version = "7.6.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] sdist = { url = "https://files.pythonhosted.org/packages/f7/08/7e37f82e4d1aead42a7443ff06a1e406aabf7302c4f00a546e4b320b994c/coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d", size = 798791 } wheels = [ { url = "https://files.pythonhosted.org/packages/7e/61/eb7ce5ed62bacf21beca4937a90fe32545c91a3c8a42a30c6616d48fc70d/coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16", size = 206690 }, @@ -491,110 +370,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/52/76/1766bb8b803a88f93c3a2d07e30ffa359467810e5cbc68e375ebe6906efb/coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3", size = 247598 }, { url = "https://files.pythonhosted.org/packages/66/8b/f54f8db2ae17188be9566e8166ac6df105c1c611e25da755738025708d54/coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", size = 210307 }, { url = "https://files.pythonhosted.org/packages/9f/b0/e0dca6da9170aefc07515cce067b97178cefafb512d00a87a1c717d2efd5/coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", size = 211453 }, - { url = "https://files.pythonhosted.org/packages/81/d0/d9e3d554e38beea5a2e22178ddb16587dbcbe9a1ef3211f55733924bf7fa/coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0", size = 206674 }, - { url = "https://files.pythonhosted.org/packages/38/ea/cab2dc248d9f45b2b7f9f1f596a4d75a435cb364437c61b51d2eb33ceb0e/coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a", size = 207101 }, - { url = "https://files.pythonhosted.org/packages/ca/6f/f82f9a500c7c5722368978a5390c418d2a4d083ef955309a8748ecaa8920/coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b", size = 236554 }, - { url = "https://files.pythonhosted.org/packages/a6/94/d3055aa33d4e7e733d8fa309d9adf147b4b06a82c1346366fc15a2b1d5fa/coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3", size = 234440 }, - { url = "https://files.pythonhosted.org/packages/e4/6e/885bcd787d9dd674de4a7d8ec83faf729534c63d05d51d45d4fa168f7102/coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de", size = 235889 }, - { url = "https://files.pythonhosted.org/packages/f4/63/df50120a7744492710854860783d6819ff23e482dee15462c9a833cc428a/coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6", size = 235142 }, - { url = "https://files.pythonhosted.org/packages/3a/5d/9d0acfcded2b3e9ce1c7923ca52ccc00c78a74e112fc2aee661125b7843b/coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569", size = 233805 }, - { url = "https://files.pythonhosted.org/packages/c4/56/50abf070cb3cd9b1dd32f2c88f083aab561ecbffbcd783275cb51c17f11d/coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989", size = 234655 }, - { url = "https://files.pythonhosted.org/packages/25/ee/b4c246048b8485f85a2426ef4abab88e48c6e80c74e964bea5cd4cd4b115/coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7", size = 209296 }, - { url = "https://files.pythonhosted.org/packages/5c/1c/96cf86b70b69ea2b12924cdf7cabb8ad10e6130eab8d767a1099fbd2a44f/coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8", size = 210137 }, - { url = "https://files.pythonhosted.org/packages/19/d3/d54c5aa83268779d54c86deb39c1c4566e5d45c155369ca152765f8db413/coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255", size = 206688 }, - { url = "https://files.pythonhosted.org/packages/a5/fe/137d5dca72e4a258b1bc17bb04f2e0196898fe495843402ce826a7419fe3/coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8", size = 207120 }, - { url = "https://files.pythonhosted.org/packages/78/5b/a0a796983f3201ff5485323b225d7c8b74ce30c11f456017e23d8e8d1945/coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2", size = 235249 }, - { url = "https://files.pythonhosted.org/packages/4e/e1/76089d6a5ef9d68f018f65411fcdaaeb0141b504587b901d74e8587606ad/coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a", size = 233237 }, - { url = "https://files.pythonhosted.org/packages/9a/6f/eef79b779a540326fee9520e5542a8b428cc3bfa8b7c8f1022c1ee4fc66c/coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc", size = 234311 }, - { url = "https://files.pythonhosted.org/packages/75/e1/656d65fb126c29a494ef964005702b012f3498db1a30dd562958e85a4049/coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004", size = 233453 }, - { url = "https://files.pythonhosted.org/packages/68/6a/45f108f137941a4a1238c85f28fd9d048cc46b5466d6b8dda3aba1bb9d4f/coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb", size = 231958 }, - { url = "https://files.pythonhosted.org/packages/9b/e7/47b809099168b8b8c72ae311efc3e88c8d8a1162b3ba4b8da3cfcdb85743/coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36", size = 232938 }, - { url = "https://files.pythonhosted.org/packages/52/80/052222ba7058071f905435bad0ba392cc12006380731c37afaf3fe749b88/coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c", size = 209352 }, - { url = "https://files.pythonhosted.org/packages/b8/d8/1b92e0b3adcf384e98770a00ca095da1b5f7b483e6563ae4eb5e935d24a1/coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca", size = 210153 }, { url = "https://files.pythonhosted.org/packages/a5/2b/0354ed096bca64dc8e32a7cbcae28b34cb5ad0b1fe2125d6d99583313ac0/coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df", size = 198926 }, ] [package.optional-dependencies] toml = [ - { name = "tomli", marker = "python_full_version < '3.9'" }, -] - -[[package]] -name = "coverage" -version = "7.6.10" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version == '3.9.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/84/ba/ac14d281f80aab516275012e8875991bb06203957aa1e19950139238d658/coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23", size = 803868 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/12/2a2a923edf4ddabdffed7ad6da50d96a5c126dae7b80a33df7310e329a1e/coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78", size = 207982 }, - { url = "https://files.pythonhosted.org/packages/ca/49/6985dbca9c7be3f3cb62a2e6e492a0c88b65bf40579e16c71ae9c33c6b23/coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c", size = 208414 }, - { url = "https://files.pythonhosted.org/packages/35/93/287e8f1d1ed2646f4e0b2605d14616c9a8a2697d0d1b453815eb5c6cebdb/coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a", size = 236860 }, - { url = "https://files.pythonhosted.org/packages/de/e1/cfdb5627a03567a10031acc629b75d45a4ca1616e54f7133ca1fa366050a/coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165", size = 234758 }, - { url = "https://files.pythonhosted.org/packages/6d/85/fc0de2bcda3f97c2ee9fe8568f7d48f7279e91068958e5b2cc19e0e5f600/coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988", size = 235920 }, - { url = "https://files.pythonhosted.org/packages/79/73/ef4ea0105531506a6f4cf4ba571a214b14a884630b567ed65b3d9c1975e1/coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5", size = 234986 }, - { url = "https://files.pythonhosted.org/packages/c6/4d/75afcfe4432e2ad0405c6f27adeb109ff8976c5e636af8604f94f29fa3fc/coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3", size = 233446 }, - { url = "https://files.pythonhosted.org/packages/86/5b/efee56a89c16171288cafff022e8af44f8f94075c2d8da563c3935212871/coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5", size = 234566 }, - { url = "https://files.pythonhosted.org/packages/f2/db/67770cceb4a64d3198bf2aa49946f411b85ec6b0a9b489e61c8467a4253b/coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244", size = 210675 }, - { url = "https://files.pythonhosted.org/packages/8d/27/e8bfc43f5345ec2c27bc8a1fa77cdc5ce9dcf954445e11f14bb70b889d14/coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e", size = 211518 }, - { url = "https://files.pythonhosted.org/packages/85/d2/5e175fcf6766cf7501a8541d81778fd2f52f4870100e791f5327fd23270b/coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3", size = 208088 }, - { url = "https://files.pythonhosted.org/packages/4b/6f/06db4dc8fca33c13b673986e20e466fd936235a6ec1f0045c3853ac1b593/coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43", size = 208536 }, - { url = "https://files.pythonhosted.org/packages/0d/62/c6a0cf80318c1c1af376d52df444da3608eafc913b82c84a4600d8349472/coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132", size = 240474 }, - { url = "https://files.pythonhosted.org/packages/a3/59/750adafc2e57786d2e8739a46b680d4fb0fbc2d57fbcb161290a9f1ecf23/coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f", size = 237880 }, - { url = "https://files.pythonhosted.org/packages/2c/f8/ef009b3b98e9f7033c19deb40d629354aab1d8b2d7f9cfec284dbedf5096/coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994", size = 239750 }, - { url = "https://files.pythonhosted.org/packages/a6/e2/6622f3b70f5f5b59f705e680dae6db64421af05a5d1e389afd24dae62e5b/coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99", size = 238642 }, - { url = "https://files.pythonhosted.org/packages/2d/10/57ac3f191a3c95c67844099514ff44e6e19b2915cd1c22269fb27f9b17b6/coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd", size = 237266 }, - { url = "https://files.pythonhosted.org/packages/ee/2d/7016f4ad9d553cabcb7333ed78ff9d27248ec4eba8dd21fa488254dff894/coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377", size = 238045 }, - { url = "https://files.pythonhosted.org/packages/a7/fe/45af5c82389a71e0cae4546413266d2195c3744849669b0bab4b5f2c75da/coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8", size = 210647 }, - { url = "https://files.pythonhosted.org/packages/db/11/3f8e803a43b79bc534c6a506674da9d614e990e37118b4506faf70d46ed6/coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609", size = 211508 }, - { url = "https://files.pythonhosted.org/packages/86/77/19d09ea06f92fdf0487499283b1b7af06bc422ea94534c8fe3a4cd023641/coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853", size = 208281 }, - { url = "https://files.pythonhosted.org/packages/b6/67/5479b9f2f99fcfb49c0d5cf61912a5255ef80b6e80a3cddba39c38146cf4/coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078", size = 208514 }, - { url = "https://files.pythonhosted.org/packages/15/d1/febf59030ce1c83b7331c3546d7317e5120c5966471727aa7ac157729c4b/coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0", size = 241537 }, - { url = "https://files.pythonhosted.org/packages/4b/7e/5ac4c90192130e7cf8b63153fe620c8bfd9068f89a6d9b5f26f1550f7a26/coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50", size = 238572 }, - { url = "https://files.pythonhosted.org/packages/dc/03/0334a79b26ecf59958f2fe9dd1f5ab3e2f88db876f5071933de39af09647/coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022", size = 240639 }, - { url = "https://files.pythonhosted.org/packages/d7/45/8a707f23c202208d7b286d78ad6233f50dcf929319b664b6cc18a03c1aae/coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b", size = 240072 }, - { url = "https://files.pythonhosted.org/packages/66/02/603ce0ac2d02bc7b393279ef618940b4a0535b0868ee791140bda9ecfa40/coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0", size = 238386 }, - { url = "https://files.pythonhosted.org/packages/04/62/4e6887e9be060f5d18f1dd58c2838b2d9646faf353232dec4e2d4b1c8644/coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852", size = 240054 }, - { url = "https://files.pythonhosted.org/packages/5c/74/83ae4151c170d8bd071924f212add22a0e62a7fe2b149edf016aeecad17c/coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359", size = 210904 }, - { url = "https://files.pythonhosted.org/packages/c3/54/de0893186a221478f5880283119fc40483bc460b27c4c71d1b8bba3474b9/coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247", size = 211692 }, - { url = "https://files.pythonhosted.org/packages/25/6d/31883d78865529257bf847df5789e2ae80e99de8a460c3453dbfbe0db069/coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9", size = 208308 }, - { url = "https://files.pythonhosted.org/packages/70/22/3f2b129cc08de00c83b0ad6252e034320946abfc3e4235c009e57cfeee05/coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b", size = 208565 }, - { url = "https://files.pythonhosted.org/packages/97/0a/d89bc2d1cc61d3a8dfe9e9d75217b2be85f6c73ebf1b9e3c2f4e797f4531/coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690", size = 241083 }, - { url = "https://files.pythonhosted.org/packages/4c/81/6d64b88a00c7a7aaed3a657b8eaa0931f37a6395fcef61e53ff742b49c97/coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18", size = 238235 }, - { url = "https://files.pythonhosted.org/packages/9a/0b/7797d4193f5adb4b837207ed87fecf5fc38f7cc612b369a8e8e12d9fa114/coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c", size = 240220 }, - { url = "https://files.pythonhosted.org/packages/65/4d/6f83ca1bddcf8e51bf8ff71572f39a1c73c34cf50e752a952c34f24d0a60/coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd", size = 239847 }, - { url = "https://files.pythonhosted.org/packages/30/9d/2470df6aa146aff4c65fee0f87f58d2164a67533c771c9cc12ffcdb865d5/coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e", size = 237922 }, - { url = "https://files.pythonhosted.org/packages/08/dd/723fef5d901e6a89f2507094db66c091449c8ba03272861eaefa773ad95c/coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694", size = 239783 }, - { url = "https://files.pythonhosted.org/packages/3d/f7/64d3298b2baf261cb35466000628706ce20a82d42faf9b771af447cd2b76/coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6", size = 210965 }, - { url = "https://files.pythonhosted.org/packages/d5/58/ec43499a7fc681212fe7742fe90b2bc361cdb72e3181ace1604247a5b24d/coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e", size = 211719 }, - { url = "https://files.pythonhosted.org/packages/ab/c9/f2857a135bcff4330c1e90e7d03446b036b2363d4ad37eb5e3a47bbac8a6/coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe", size = 209050 }, - { url = "https://files.pythonhosted.org/packages/aa/b3/f840e5bd777d8433caa9e4a1eb20503495709f697341ac1a8ee6a3c906ad/coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273", size = 209321 }, - { url = "https://files.pythonhosted.org/packages/85/7d/125a5362180fcc1c03d91850fc020f3831d5cda09319522bcfa6b2b70be7/coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8", size = 252039 }, - { url = "https://files.pythonhosted.org/packages/a9/9c/4358bf3c74baf1f9bddd2baf3756b54c07f2cfd2535f0a47f1e7757e54b3/coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098", size = 247758 }, - { url = "https://files.pythonhosted.org/packages/cf/c7/de3eb6fc5263b26fab5cda3de7a0f80e317597a4bad4781859f72885f300/coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb", size = 250119 }, - { url = "https://files.pythonhosted.org/packages/3e/e6/43de91f8ba2ec9140c6a4af1102141712949903dc732cf739167cfa7a3bc/coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0", size = 249597 }, - { url = "https://files.pythonhosted.org/packages/08/40/61158b5499aa2adf9e37bc6d0117e8f6788625b283d51e7e0c53cf340530/coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf", size = 247473 }, - { url = "https://files.pythonhosted.org/packages/50/69/b3f2416725621e9f112e74e8470793d5b5995f146f596f133678a633b77e/coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2", size = 248737 }, - { url = "https://files.pythonhosted.org/packages/3c/6e/fe899fb937657db6df31cc3e61c6968cb56d36d7326361847440a430152e/coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312", size = 211611 }, - { url = "https://files.pythonhosted.org/packages/1c/55/52f5e66142a9d7bc93a15192eba7a78513d2abf6b3558d77b4ca32f5f424/coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d", size = 212781 }, - { url = "https://files.pythonhosted.org/packages/40/41/473617aadf9a1c15bc2d56be65d90d7c29bfa50a957a67ef96462f7ebf8e/coverage-7.6.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a", size = 207978 }, - { url = "https://files.pythonhosted.org/packages/10/f6/480586607768b39a30e6910a3c4522139094ac0f1677028e1f4823688957/coverage-7.6.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27", size = 208415 }, - { url = "https://files.pythonhosted.org/packages/f1/af/439bb760f817deff6f4d38fe7da08d9dd7874a560241f1945bc3b4446550/coverage-7.6.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4", size = 236452 }, - { url = "https://files.pythonhosted.org/packages/d0/13/481f4ceffcabe29ee2332e60efb52e4694f54a402f3ada2bcec10bb32e43/coverage-7.6.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f", size = 234374 }, - { url = "https://files.pythonhosted.org/packages/c5/59/4607ea9d6b1b73e905c7656da08d0b00cdf6e59f2293ec259e8914160025/coverage-7.6.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25", size = 235505 }, - { url = "https://files.pythonhosted.org/packages/85/60/d66365723b9b7f29464b11d024248ed3523ce5aab958e4ad8c43f3f4148b/coverage-7.6.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315", size = 234616 }, - { url = "https://files.pythonhosted.org/packages/74/f8/2cf7a38e7d81b266f47dfcf137fecd8fa66c7bdbd4228d611628d8ca3437/coverage-7.6.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90", size = 233099 }, - { url = "https://files.pythonhosted.org/packages/50/2b/bff6c1c6b63c4396ea7ecdbf8db1788b46046c681b8fcc6ec77db9f4ea49/coverage-7.6.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d", size = 234089 }, - { url = "https://files.pythonhosted.org/packages/bf/b5/baace1c754d546a67779358341aa8d2f7118baf58cac235db457e1001d1b/coverage-7.6.10-cp39-cp39-win32.whl", hash = "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18", size = 210701 }, - { url = "https://files.pythonhosted.org/packages/b1/bf/9e1e95b8b20817398ecc5a1e8d3e05ff404e1b9fb2185cd71561698fe2a2/coverage-7.6.10-cp39-cp39-win_amd64.whl", hash = "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59", size = 211482 }, - { url = "https://files.pythonhosted.org/packages/a1/70/de81bfec9ed38a64fc44a77c7665e20ca507fc3265597c28b0d989e4082e/coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f", size = 200223 }, -] - -[package.optional-dependencies] -toml = [ - { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version <= '3.11'" }, + { name = "tomli", marker = "python_full_version <= '3.11'" }, ] [[package]] @@ -619,14 +400,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/99/46/bc6dcfd7eb8cc969a5716d858e32485eb40c72c6a8dc88d1e3a4d5e95813/debugpy-1.8.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:898fba72b81a654e74412a67c7e0a81e89723cfe2a3ea6fcd3feaa3395138ca9", size = 4218616 }, { url = "https://files.pythonhosted.org/packages/03/dd/d7fcdf0381a9b8094da1f6a1c9f19fed493a4f8576a2682349b3a8b20ec7/debugpy-1.8.12-cp313-cp313-win32.whl", hash = "sha256:22a11c493c70413a01ed03f01c3c3a2fc4478fc6ee186e340487b2edcd6f4180", size = 5226540 }, { url = "https://files.pythonhosted.org/packages/25/bd/ecb98f5b5fc7ea0bfbb3c355bc1dd57c198a28780beadd1e19915bf7b4d9/debugpy-1.8.12-cp313-cp313-win_amd64.whl", hash = "sha256:fdb3c6d342825ea10b90e43d7f20f01535a72b3a1997850c0c3cefa5c27a4a2c", size = 5267134 }, - { url = "https://files.pythonhosted.org/packages/6f/56/6c7ddb4dfd2feca7ea3a580a32c7694f6c77183fa08932ee8ba37a0e703c/debugpy-1.8.12-cp38-cp38-macosx_14_0_x86_64.whl", hash = "sha256:b0232cd42506d0c94f9328aaf0d1d0785f90f87ae72d9759df7e5051be039738", size = 2076797 }, - { url = "https://files.pythonhosted.org/packages/73/25/a58e149ddcd609c8212ca733999251022e53508906e2c9f67252e4516de6/debugpy-1.8.12-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9af40506a59450f1315168d47a970db1a65aaab5df3833ac389d2899a5d63b3f", size = 3632547 }, - { url = "https://files.pythonhosted.org/packages/91/c7/17c09b9d8332d09b7b0aa430085010945d92d90945748948cd38865c0b93/debugpy-1.8.12-cp38-cp38-win32.whl", hash = "sha256:5cc45235fefac57f52680902b7d197fb2f3650112379a6fa9aa1b1c1d3ed3f02", size = 5185270 }, - { url = "https://files.pythonhosted.org/packages/3b/d1/afdbb99f95f54c2768fa2511bf38ec8805b4cde319725e318e5016b252ec/debugpy-1.8.12-cp38-cp38-win_amd64.whl", hash = "sha256:557cc55b51ab2f3371e238804ffc8510b6ef087673303890f57a24195d096e61", size = 5217697 }, - { url = "https://files.pythonhosted.org/packages/89/37/a3333c5b69c086465ea3c073424ef2775e52a6c17276f642f64209c4a082/debugpy-1.8.12-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:b5c6c967d02fee30e157ab5227706f965d5c37679c687b1e7bbc5d9e7128bd41", size = 2077275 }, - { url = "https://files.pythonhosted.org/packages/50/1d/99f6a0a78b4b513ff2b0d0e44c1e705f7ee34e3aba0e8add617d339d97dc/debugpy-1.8.12-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88a77f422f31f170c4b7e9ca58eae2a6c8e04da54121900651dfa8e66c29901a", size = 3555956 }, - { url = "https://files.pythonhosted.org/packages/b8/86/c624665aaa807d065da2016b05e9f2fb4fa56872d67a5fbd7751e77f7f88/debugpy-1.8.12-cp39-cp39-win32.whl", hash = "sha256:a4042edef80364239f5b7b5764e55fd3ffd40c32cf6753da9bda4ff0ac466018", size = 5181535 }, - { url = "https://files.pythonhosted.org/packages/72/c7/d59a0f845ce1677b5c2bb170f08cc1cc3531625a5fdce9c67bd31116540a/debugpy-1.8.12-cp39-cp39-win_amd64.whl", hash = "sha256:f30b03b0f27608a0b26c75f0bb8a880c752c0e0b01090551b9d87c7d783e2069", size = 5213601 }, { url = "https://files.pythonhosted.org/packages/38/c4/5120ad36405c3008f451f94b8f92ef1805b1e516f6ff870f331ccb3c4cc0/debugpy-1.8.12-py2.py3-none-any.whl", hash = "sha256:274b6a2040349b5c9864e475284bce5bb062e63dce368a394b8cc865ae3b00c6", size = 5229490 }, ] @@ -673,27 +446,11 @@ wheels = [ name = "docutils" version = "0.17.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version < '3.9'", -] sdist = { url = "https://files.pythonhosted.org/packages/4c/17/559b4d020f4b46e0287a2eddf2d8ebf76318fd3bd495f1625414b052fdc9/docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125", size = 2016138 } wheels = [ { url = "https://files.pythonhosted.org/packages/4c/5e/6003a0d1f37725ec2ebd4046b657abb9372202655f96e76795dca8c0063c/docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61", size = 575533 }, ] -[[package]] -name = "docutils" -version = "0.21.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version == '3.9.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408 }, -] - [[package]] name = "entrypoints" version = "0.4" @@ -803,62 +560,17 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/87/470e01a940307796f1d25f8167b551a968540fbe0551c0ebb853cb527dd6/greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822", size = 602753 }, { url = "https://files.pythonhosted.org/packages/e2/72/576815ba674eddc3c25028238f74d7b8068902b3968cbe456771b166455e/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", size = 1122731 }, { url = "https://files.pythonhosted.org/packages/ac/38/08cc303ddddc4b3d7c628c3039a61a3aae36c241ed01393d00c2fd663473/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", size = 1142112 }, - { url = "https://files.pythonhosted.org/packages/97/83/bdf5f69fcf304065ec7cf8fc7c08248479cfed9bcca02bf0001c07e000aa/greenlet-3.1.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:346bed03fe47414091be4ad44786d1bd8bef0c3fcad6ed3dee074a032ab408a9", size = 271017 }, - { url = "https://files.pythonhosted.org/packages/31/4a/2d4443adcb38e1e90e50c653a26b2be39998ea78ca1a4cf414dfdeb2e98b/greenlet-3.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfc59d69fc48664bc693842bd57acfdd490acafda1ab52c7836e3fc75c90a111", size = 642888 }, - { url = "https://files.pythonhosted.org/packages/5a/c9/b5d9ac1b932aa772dd1eb90a8a2b30dbd7ad5569dcb7fdac543810d206b4/greenlet-3.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21e10da6ec19b457b82636209cbe2331ff4306b54d06fa04b7c138ba18c8a81", size = 655451 }, - { url = "https://files.pythonhosted.org/packages/a8/18/218e21caf7caba5b2236370196eaebc00987d4a2b2d3bf63cc4d4dd5a69f/greenlet-3.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37b9de5a96111fc15418819ab4c4432e4f3c2ede61e660b1e33971eba26ef9ba", size = 651409 }, - { url = "https://files.pythonhosted.org/packages/a7/25/de419a2b22fa6e18ce3b2a5adb01d33ec7b2784530f76fa36ba43d8f0fac/greenlet-3.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef9ea3f137e5711f0dbe5f9263e8c009b7069d8a1acea822bd5e9dae0ae49c8", size = 650661 }, - { url = "https://files.pythonhosted.org/packages/d8/88/0ce16c0afb2d71d85562a7bcd9b092fec80a7767ab5b5f7e1bbbca8200f8/greenlet-3.1.1-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85f3ff71e2e60bd4b4932a043fbbe0f499e263c628390b285cb599154a3b03b1", size = 605959 }, - { url = "https://files.pythonhosted.org/packages/5a/10/39a417ad0afb0b7e5b150f1582cdeb9416f41f2e1df76018434dfac4a6cc/greenlet-3.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:95ffcf719966dd7c453f908e208e14cde192e09fde6c7186c8f1896ef778d8cd", size = 1132341 }, - { url = "https://files.pythonhosted.org/packages/9f/f5/e9b151ddd2ed0508b7a47bef7857e46218dbc3fd10e564617a3865abfaac/greenlet-3.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:03a088b9de532cbfe2ba2034b2b85e82df37874681e8c470d6fb2f8c04d7e4b7", size = 1159409 }, - { url = "https://files.pythonhosted.org/packages/86/97/2c86989ca4e0f089fbcdc9229c972a01ef53abdafd5ae89e0f3dcdcd4adb/greenlet-3.1.1-cp38-cp38-win32.whl", hash = "sha256:8b8b36671f10ba80e159378df9c4f15c14098c4fd73a36b9ad715f057272fbef", size = 281126 }, - { url = "https://files.pythonhosted.org/packages/d3/50/7b7a3e10ed82c760c1fd8d3167a7c95508e9fdfc0b0604f05ed1a9a9efdc/greenlet-3.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:7017b2be767b9d43cc31416aba48aab0d2309ee31b4dbf10a1d38fb7972bdf9d", size = 298285 }, - { url = "https://files.pythonhosted.org/packages/8c/82/8051e82af6d6b5150aacb6789a657a8afd48f0a44d8e91cb72aaaf28553a/greenlet-3.1.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3", size = 270027 }, - { url = "https://files.pythonhosted.org/packages/f9/74/f66de2785880293780eebd18a2958aeea7cbe7814af1ccef634f4701f846/greenlet-3.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42", size = 634822 }, - { url = "https://files.pythonhosted.org/packages/68/23/acd9ca6bc412b02b8aa755e47b16aafbe642dde0ad2f929f836e57a7949c/greenlet-3.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f", size = 646866 }, - { url = "https://files.pythonhosted.org/packages/a9/ab/562beaf8a53dc9f6b2459f200e7bc226bb07e51862a66351d8b7817e3efd/greenlet-3.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437", size = 641985 }, - { url = "https://files.pythonhosted.org/packages/03/d3/1006543621f16689f6dc75f6bcf06e3c23e044c26fe391c16c253623313e/greenlet-3.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145", size = 641268 }, - { url = "https://files.pythonhosted.org/packages/2f/c1/ad71ce1b5f61f900593377b3f77b39408bce5dc96754790311b49869e146/greenlet-3.1.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c", size = 597376 }, - { url = "https://files.pythonhosted.org/packages/f7/ff/183226685b478544d61d74804445589e069d00deb8ddef042699733950c7/greenlet-3.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e", size = 1123359 }, - { url = "https://files.pythonhosted.org/packages/c0/8b/9b3b85a89c22f55f315908b94cd75ab5fed5973f7393bbef000ca8b2c5c1/greenlet-3.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e", size = 1147458 }, - { url = "https://files.pythonhosted.org/packages/b8/1c/248fadcecd1790b0ba793ff81fa2375c9ad6442f4c748bf2cc2e6563346a/greenlet-3.1.1-cp39-cp39-win32.whl", hash = "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c", size = 281131 }, - { url = "https://files.pythonhosted.org/packages/ae/02/e7d0aef2354a38709b764df50b2b83608f0621493e47f47694eb80922822/greenlet-3.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22", size = 298306 }, -] - -[[package]] -name = "h11" -version = "0.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, ] [[package]] name = "identify" version = "2.6.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] sdist = { url = "https://files.pythonhosted.org/packages/29/bb/25024dbcc93516c492b75919e76f389bac754a3e4248682fba32b250c880/identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98", size = 99097 } wheels = [ { url = "https://files.pythonhosted.org/packages/7d/0c/4ef72754c050979fdcc06c744715ae70ea37e734816bb6514f79df77a42f/identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0", size = 98972 }, ] -[[package]] -name = "identify" -version = "2.6.5" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version == '3.9.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/cf/92/69934b9ef3c31ca2470980423fda3d00f0460ddefdf30a67adf7f17e2e00/identify-2.6.5.tar.gz", hash = "sha256:c10b33f250e5bba374fae86fb57f3adcebf1161bce7cdf92031915fd480c13bc", size = 99213 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/fa/dce098f4cdf7621aa8f7b4f919ce545891f489482f0bfa5102f3eca8608b/identify-2.6.5-py2.py3-none-any.whl", hash = "sha256:14181a47091eb75b337af4c23078c9d09225cd4c48929f521f3bf16b09d02566", size = 99078 }, -] - [[package]] name = "idna" version = "3.10" @@ -882,8 +594,7 @@ name = "importlib-metadata" version = "4.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp", version = "3.20.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "zipp", version = "3.21.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "zipp" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1a/16/441080c907df829016729e71d8bdd42d99b9bdde48b01492ed08912c0aa9/importlib_metadata-4.12.0.tar.gz", hash = "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670", size = 48153 } wheels = [ @@ -894,10 +605,6 @@ wheels = [ name = "importlib-resources" version = "5.13.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp", version = "3.20.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "zipp", version = "3.21.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, -] sdist = { url = "https://files.pythonhosted.org/packages/a5/f1/8711c49ffd121083007a24c1bff0d324c9ff621d4fdf8b4ffcb8d9e60330/importlib_resources-5.13.0.tar.gz", hash = "sha256:82d5c6cca930697dbbd86c93333bb2c2e72861d4789a11c2662b933e5ad2b528", size = 36550 } wheels = [ { url = "https://files.pythonhosted.org/packages/7a/68/bd9dd6bbf06772c7accce77d0354d783333fbe712a60b08fc13540c05422/importlib_resources-5.13.0-py3-none-any.whl", hash = "sha256:9f7bd0c97b79972a6cce36a366356d16d5e13b09679c11a58f1014bfdf8e64b2", size = 32912 }, @@ -948,8 +655,7 @@ dependencies = [ { name = "pickleshare" }, { name = "prompt-toolkit" }, { name = "pygments" }, - { name = "setuptools", version = "75.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "setuptools", version = "75.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "setuptools" }, { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/db/6c/3fcf0b8ee46656796099ac4b7b72497af5f090da3e43fd305f2a24c73915/ipython-7.34.0.tar.gz", hash = "sha256:af3bdb46aa292bce5615b1b2ebc76c2080c5f77f54bda2ec72461317273e7cd6", size = 5158632 } @@ -1022,8 +728,6 @@ version = "4.17.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, - { name = "importlib-resources", marker = "python_full_version < '3.9'" }, - { name = "pkgutil-resolve-name", marker = "python_full_version < '3.9'" }, { name = "pyrsistent" }, ] sdist = { url = "https://files.pythonhosted.org/packages/36/3d/ca032d5ac064dff543aa13c984737795ac81abc9fb130cd2fcff17cfabc7/jsonschema-4.17.3.tar.gz", hash = "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d", size = 297785 } @@ -1069,7 +773,6 @@ name = "jupyter-client" version = "8.6.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "jupyter-core" }, { name = "python-dateutil" }, { name = "pyzmq" }, @@ -1116,8 +819,7 @@ name = "jupyter-server" version = "1.15.6" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio", version = "4.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "anyio", version = "4.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "anyio" }, { name = "argon2-cffi" }, { name = "jinja2" }, { name = "jupyter-client" }, @@ -1230,14 +932,12 @@ wheels = [ [package.optional-dependencies] docs = [ - { name = "docutils", version = "0.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.9.*'" }, - { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "docutils" }, { name = "jupyter" }, { name = "nbsphinx" }, { name = "pandoc" }, { name = "sphinx" }, - { name = "sphinx-autobuild", version = "2021.3.14", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "sphinx-autobuild", version = "2024.10.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "sphinx-autobuild" }, ] rmq = [ { name = "aio-pika" }, @@ -1250,7 +950,7 @@ name = "livereload" version = "2.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "tornado", marker = "python_full_version < '3.9'" }, + { name = "tornado" }, ] sdist = { url = "https://files.pythonhosted.org/packages/43/6e/f2748665839812a9bbe5c75d3f983edbf3ab05fa5cd2f7c2f36fffdf65bd/livereload-2.7.1.tar.gz", hash = "sha256:3d9bf7c05673df06e32bea23b494b8d36ca6d10f7d5c3c8a6989608c09c986a9", size = 22255 } wheels = [ @@ -1286,36 +986,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/69/c31e837e4bb5532b02d297152464b2cb8a0edeb9bef762c015e9b4e95e16/MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a", size = 30553 }, { url = "https://files.pythonhosted.org/packages/c1/39/9df65c006a88fce7bbd5ec3195b949b79477b1a325564f486c611c367893/MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28", size = 14331 }, { url = "https://files.pythonhosted.org/packages/93/28/d42b954fb9189cf4b78b0b0a025cff9b2583f93b37d1a345768ade29e5dd/MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134", size = 15042 }, - { url = "https://files.pythonhosted.org/packages/51/1e/45e25cd867fb79339c49086dad9794e11923dd6325251ae48c341b0a4271/MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9", size = 18323 }, - { url = "https://files.pythonhosted.org/packages/70/56/f81c0cfbc22882df36358ecdedc5474571183e5a5adde1e237079acee437/MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", size = 13982 }, - { url = "https://files.pythonhosted.org/packages/f9/12/b63afcb3bf9f27fd347adef452f9a6e27dfe7107a8f2685afacc8e9c6592/MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", size = 30224 }, - { url = "https://files.pythonhosted.org/packages/1d/c5/1d1b42c65f96ee7b0c81761260878d1a1dc0afdf259e434b7d7af88a80a3/MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", size = 30562 }, - { url = "https://files.pythonhosted.org/packages/92/ac/94771b65ac9f77cf37e43b38516697bbc4e128ee152b68d596ae44c6c896/MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", size = 30226 }, - { url = "https://files.pythonhosted.org/packages/68/ba/7a5ca0f9b4239e6fd846dd54c0b5928187355fa62fbdbd13e1c5942afae7/MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", size = 30563 }, - { url = "https://files.pythonhosted.org/packages/eb/3b/1cddaf0338a031ef5c2e1d9d74f2d607d564748a933b44de6edfe7a2a880/MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", size = 31869 }, - { url = "https://files.pythonhosted.org/packages/e4/9b/c7b55a2f587368d69eb6dc36e285010ab0bbb74323833d501921e08e2728/MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b", size = 27085 }, - { url = "https://files.pythonhosted.org/packages/cc/f2/854d33eee85df681e61e22b52d8e83bef8b7425c0b9826212289f7885710/MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a", size = 30239 }, - { url = "https://files.pythonhosted.org/packages/7a/e8/00c435416c9b0238dca6f883563b01c4cc532b2ba6aaf7268081f6238520/MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6", size = 30603 }, - { url = "https://files.pythonhosted.org/packages/95/18/b7a45c16635acafdf6837a6fd4c71acfe5bad202884c6fcbae4ea0763dde/MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f", size = 30964 }, - { url = "https://files.pythonhosted.org/packages/15/90/b63743e72c9ffc5988c7b1c04d14f9a32ae49574afe8a7fbea0ce538bda4/MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194", size = 29982 }, - { url = "https://files.pythonhosted.org/packages/1f/44/ada8e01854175525e8e139278c3a52fec0ef720307cbd670bca86b473b56/MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee", size = 30372 }, - { url = "https://files.pythonhosted.org/packages/44/e6/4e1f202ec01062c8b4d03af72f1aeb2ca8fc97f9f5d95b9173302ac4e5ad/MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", size = 14303 }, - { url = "https://files.pythonhosted.org/packages/30/9e/4b7116f464a0151b86ce42b5185941eb74c207b38fe033f71f5e5d150356/MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", size = 14910 }, - { url = "https://files.pythonhosted.org/packages/dd/8f/d0c570c851f70377ca6f344531fab4b6b01a99a9d2a801b25d6fd75525e5/MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", size = 18308 }, - { url = "https://files.pythonhosted.org/packages/ce/a7/835a636047f4bb4fea31a682c18affad9795e864d800892bd7248485425e/MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", size = 13982 }, - { url = "https://files.pythonhosted.org/packages/66/66/b5891704372c9f5d97432933bdd7e9b5a0647fad9170c72bb7f486550c43/MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", size = 30216 }, - { url = "https://files.pythonhosted.org/packages/50/99/06eccf68be0bff67ab9a0b90b5382c04769f9ad2e42cae5e5e92f99380cd/MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", size = 30553 }, - { url = "https://files.pythonhosted.org/packages/5a/ff/34bdcd8cc794f692588de0b3f4c1aa7ec0d17716fda9d874836ed68775c1/MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", size = 30220 }, - { url = "https://files.pythonhosted.org/packages/6f/83/eabfb8c6d60b096dc9ada378cf935809289c4d0327b74a60789bde77e1db/MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", size = 30556 }, - { url = "https://files.pythonhosted.org/packages/ae/70/8dd5f2c0aab82431c9c619a2c4fbd1742fc0fb769d8d7b275ae1d03eb3a5/MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", size = 31882 }, - { url = "https://files.pythonhosted.org/packages/a6/d1/a7b97d0e000336c4e06bfce7e08dcb2b47fc5091146ee883dfac6cb4842e/MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1", size = 26719 }, - { url = "https://files.pythonhosted.org/packages/67/e9/579a3ad8d48f7680f887ff1f22cc6330f083de23ce32a8fa35f8acef477a/MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac", size = 30251 }, - { url = "https://files.pythonhosted.org/packages/c2/db/314df69668f582d5173922bded7b58126044bb77cfce6347c5d992074d2e/MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6", size = 30586 }, - { url = "https://files.pythonhosted.org/packages/8f/87/4668ce3963e942a9aa7b13212158e74bf063a2461138b7ed5a043ac6aa79/MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047", size = 30980 }, - { url = "https://files.pythonhosted.org/packages/a7/55/a576835b6b95af21d15f69eaf14c4fb1358fd48475f2b9813abd9654132e/MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e", size = 29902 }, - { url = "https://files.pythonhosted.org/packages/3b/41/f53e2ac439b309d8bb017d12ee6e7d393aa70c508448c1f30a7e5db9d69e/MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1", size = 30340 }, - { url = "https://files.pythonhosted.org/packages/6a/96/7a23b44f742384a866173502e19cc1ec13951085bbb4e24be504dfc6da9f/MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", size = 14331 }, - { url = "https://files.pythonhosted.org/packages/5b/db/49785acd523bd5eef83d0e21594eec1c2d7d45afc473dcc85037243de673/MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", size = 14937 }, ] [[package]] @@ -1420,36 +1090,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/a4/63e7cd38ed29dd9f1881d5119f272c898ca92536cdb53ffe0843197f6c85/multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6", size = 125519 }, { url = "https://files.pythonhosted.org/packages/38/e0/4f5855037a72cd8a7a2f60a3952d9aa45feedb37ae7831642102604e8a37/multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81", size = 26426 }, { url = "https://files.pythonhosted.org/packages/7e/a5/17ee3a4db1e310b7405f5d25834460073a8ccd86198ce044dfaf69eac073/multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774", size = 28531 }, - { url = "https://files.pythonhosted.org/packages/3e/6a/af41f3aaf5f00fd86cc7d470a2f5b25299b0c84691163b8757f4a1a205f2/multidict-6.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:db7457bac39421addd0c8449933ac32d8042aae84a14911a757ae6ca3eef1392", size = 48597 }, - { url = "https://files.pythonhosted.org/packages/d9/d6/3d4082760ed11b05734f8bf32a0615b99e7d9d2b3730ad698a4d7377c00a/multidict-6.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d094ddec350a2fb899fec68d8353c78233debde9b7d8b4beeafa70825f1c281a", size = 29338 }, - { url = "https://files.pythonhosted.org/packages/9d/7f/5d1ce7f47d44393d429922910afbe88fcd29ee3069babbb47507a4c3a7ea/multidict-6.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5845c1fd4866bb5dd3125d89b90e57ed3138241540897de748cdf19de8a2fca2", size = 29562 }, - { url = "https://files.pythonhosted.org/packages/ce/ec/c425257671af9308a9b626e2e21f7f43841616e4551de94eb3c92aca75b2/multidict-6.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9079dfc6a70abe341f521f78405b8949f96db48da98aeb43f9907f342f627cdc", size = 130980 }, - { url = "https://files.pythonhosted.org/packages/d8/d7/d4220ad2633a89b314593e9b85b5bc9287a7c563c7f9108a4a68d9da5374/multidict-6.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3914f5aaa0f36d5d60e8ece6a308ee1c9784cd75ec8151062614657a114c4478", size = 136694 }, - { url = "https://files.pythonhosted.org/packages/a1/2a/13e554db5830c8d40185a2e22aa8325516a5de9634c3fb2caf3886a829b3/multidict-6.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c08be4f460903e5a9d0f76818db3250f12e9c344e79314d1d570fc69d7f4eae4", size = 131616 }, - { url = "https://files.pythonhosted.org/packages/2e/a9/83692e37d8152f104333132105b67100aabfb2e96a87f6bed67f566035a7/multidict-6.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d093be959277cb7dee84b801eb1af388b6ad3ca6a6b6bf1ed7585895789d027d", size = 129664 }, - { url = "https://files.pythonhosted.org/packages/cc/1c/1718cd518fb9da7e8890d9d1611c1af0ea5e60f68ff415d026e38401ed36/multidict-6.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3702ea6872c5a2a4eeefa6ffd36b042e9773f05b1f37ae3ef7264b1163c2dcf6", size = 121855 }, - { url = "https://files.pythonhosted.org/packages/2b/92/f6ed67514b0e3894198f0eb42dcde22f0851ea35f4561a1e4acf36c7b1be/multidict-6.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2090f6a85cafc5b2db085124d752757c9d251548cedabe9bd31afe6363e0aff2", size = 127928 }, - { url = "https://files.pythonhosted.org/packages/f7/30/c66954115a4dc4dc3c84e02c8ae11bb35a43d79ef93122c3c3a40c4d459b/multidict-6.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:f67f217af4b1ff66c68a87318012de788dd95fcfeb24cc889011f4e1c7454dfd", size = 122793 }, - { url = "https://files.pythonhosted.org/packages/62/c9/d386d01b43871e8e1631eb7b3695f6af071b7ae1ab716caf371100f0eb24/multidict-6.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:189f652a87e876098bbc67b4da1049afb5f5dfbaa310dd67c594b01c10388db6", size = 132762 }, - { url = "https://files.pythonhosted.org/packages/69/ff/f70cb0a2f7a358acf48e32139ce3a150ff18c961ee9c714cc8c0dc7e3584/multidict-6.1.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:6bb5992037f7a9eff7991ebe4273ea7f51f1c1c511e6a2ce511d0e7bdb754492", size = 127872 }, - { url = "https://files.pythonhosted.org/packages/89/5b/abea7db3ba4cd07752a9b560f9275a11787cd13f86849b5d99c1ceea921d/multidict-6.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f4c2b9e770c4e393876e35a7046879d195cd123b4f116d299d442b335bcd", size = 126161 }, - { url = "https://files.pythonhosted.org/packages/22/03/acc77a4667cca4462ee974fc39990803e58fa573d5a923d6e82b7ef6da7e/multidict-6.1.0-cp38-cp38-win32.whl", hash = "sha256:e27bbb6d14416713a8bd7aaa1313c0fc8d44ee48d74497a0ff4c3a1b6ccb5167", size = 26338 }, - { url = "https://files.pythonhosted.org/packages/90/bf/3d0c1cc9c8163abc24625fae89c0ade1ede9bccb6eceb79edf8cff3cca46/multidict-6.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:22f3105d4fb15c8f57ff3959a58fcab6ce36814486500cd7485651230ad4d4ef", size = 28736 }, - { url = "https://files.pythonhosted.org/packages/e7/c9/9e153a6572b38ac5ff4434113af38acf8d5e9957897cdb1f513b3d6614ed/multidict-6.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c", size = 48550 }, - { url = "https://files.pythonhosted.org/packages/76/f5/79565ddb629eba6c7f704f09a09df085c8dc04643b12506f10f718cee37a/multidict-6.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1", size = 29298 }, - { url = "https://files.pythonhosted.org/packages/60/1b/9851878b704bc98e641a3e0bce49382ae9e05743dac6d97748feb5b7baba/multidict-6.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c", size = 29641 }, - { url = "https://files.pythonhosted.org/packages/89/87/d451d45aab9e422cb0fb2f7720c31a4c1d3012c740483c37f642eba568fb/multidict-6.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c", size = 126202 }, - { url = "https://files.pythonhosted.org/packages/fa/b4/27cbe9f3e2e469359887653f2e45470272eef7295139916cc21107c6b48c/multidict-6.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f", size = 133925 }, - { url = "https://files.pythonhosted.org/packages/4d/a3/afc841899face8adfd004235ce759a37619f6ec99eafd959650c5ce4df57/multidict-6.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875", size = 129039 }, - { url = "https://files.pythonhosted.org/packages/5e/41/0d0fb18c1ad574f807196f5f3d99164edf9de3e169a58c6dc2d6ed5742b9/multidict-6.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255", size = 124072 }, - { url = "https://files.pythonhosted.org/packages/00/22/defd7a2e71a44e6e5b9a5428f972e5b572e7fe28e404dfa6519bbf057c93/multidict-6.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30", size = 116532 }, - { url = "https://files.pythonhosted.org/packages/91/25/f7545102def0b1d456ab6449388eed2dfd822debba1d65af60194904a23a/multidict-6.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057", size = 128173 }, - { url = "https://files.pythonhosted.org/packages/45/79/3dbe8d35fc99f5ea610813a72ab55f426cb9cf482f860fa8496e5409be11/multidict-6.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657", size = 122654 }, - { url = "https://files.pythonhosted.org/packages/97/cb/209e735eeab96e1b160825b5d0b36c56d3862abff828fc43999bb957dcad/multidict-6.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28", size = 133197 }, - { url = "https://files.pythonhosted.org/packages/e4/3a/a13808a7ada62808afccea67837a79d00ad6581440015ef00f726d064c2d/multidict-6.1.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972", size = 129754 }, - { url = "https://files.pythonhosted.org/packages/77/dd/8540e139eafb240079242da8f8ffdf9d3f4b4ad1aac5a786cd4050923783/multidict-6.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43", size = 126402 }, - { url = "https://files.pythonhosted.org/packages/86/99/e82e1a275d8b1ea16d3a251474262258dbbe41c05cce0c01bceda1fc8ea5/multidict-6.1.0-cp39-cp39-win32.whl", hash = "sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada", size = 26421 }, - { url = "https://files.pythonhosted.org/packages/86/1c/9fa630272355af7e4446a2c7550c259f11ee422ab2d30ff90a0a71cf3d9e/multidict-6.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a", size = 28791 }, { url = "https://files.pythonhosted.org/packages/99/b7/b9e70fde2c0f0c9af4cc5277782a89b66d35948ea3369ec9f598358c3ac5/multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", size = 10051 }, ] @@ -1484,16 +1124,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/1f/6b76be289a5a521bb1caedc1f08e76ff17ab59061007f201a8a18cc514d1/mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8", size = 12584043 }, { url = "https://files.pythonhosted.org/packages/a6/83/5a85c9a5976c6f96e3a5a7591aa28b4a6ca3a07e9e5ba0cec090c8b596d6/mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7", size = 13036996 }, { url = "https://files.pythonhosted.org/packages/b4/59/c39a6f752f1f893fccbcf1bdd2aca67c79c842402b5283563d006a67cf76/mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc", size = 9737709 }, - { url = "https://files.pythonhosted.org/packages/5e/2a/13e9ad339131c0fba5c70584f639005a47088f5eed77081a3d00479df0ca/mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a", size = 10955147 }, - { url = "https://files.pythonhosted.org/packages/94/39/02929067dc16b72d78109195cfed349ac4ec85f3d52517ac62b9a5263685/mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb", size = 10138373 }, - { url = "https://files.pythonhosted.org/packages/4a/cc/066709bb01734e3dbbd1375749f8789bf9693f8b842344fc0cf52109694f/mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b", size = 12543621 }, - { url = "https://files.pythonhosted.org/packages/f5/a2/124df839025348c7b9877d0ce134832a9249968e3ab36bb826bab0e9a1cf/mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74", size = 13050348 }, - { url = "https://files.pythonhosted.org/packages/45/86/cc94b1e7f7e756a63043cf425c24fb7470013ee1c032180282db75b1b335/mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6", size = 9615311 }, - { url = "https://files.pythonhosted.org/packages/5f/d4/b33ddd40dad230efb317898a2d1c267c04edba73bc5086bf77edeb410fb2/mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc", size = 11013906 }, - { url = "https://files.pythonhosted.org/packages/f4/e6/f414bca465b44d01cd5f4a82761e15044bedd1bf8025c5af3cc64518fac5/mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732", size = 10180657 }, - { url = "https://files.pythonhosted.org/packages/38/e9/fc3865e417722f98d58409770be01afb961e2c1f99930659ff4ae7ca8b7e/mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc", size = 12586394 }, - { url = "https://files.pythonhosted.org/packages/2e/35/f4d8b6d2cb0b3dad63e96caf159419dda023f45a358c6c9ac582ccaee354/mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d", size = 13103591 }, - { url = "https://files.pythonhosted.org/packages/22/1d/80594aef135f921dd52e142fa0acd19df197690bd0cde42cea7b88cf5aa2/mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24", size = 9634690 }, { url = "https://files.pythonhosted.org/packages/3b/86/72ce7f57431d87a7ff17d442f521146a6585019eb8f4f31b7c02801f78ad/mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a", size = 2647043 }, ] @@ -1511,15 +1141,13 @@ name = "myst-nb" version = "0.11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "docutils", version = "0.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.9.*'" }, - { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "docutils" }, { name = "importlib-metadata" }, { name = "ipython" }, { name = "ipywidgets" }, { name = "jupyter-cache" }, { name = "jupyter-sphinx" }, - { name = "myst-parser", version = "0.13.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, - { name = "myst-parser", version = "0.13.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.9.*'" }, + { name = "myst-parser" }, { name = "nbconvert" }, { name = "nbformat" }, { name = "pyyaml" }, @@ -1535,43 +1163,19 @@ wheels = [ name = "myst-parser" version = "0.13.6" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version == '3.9.*'", -] dependencies = [ - { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, - { name = "jinja2", marker = "python_full_version == '3.9.*'" }, - { name = "markdown-it-py", marker = "python_full_version == '3.9.*'" }, - { name = "mdit-py-plugins", marker = "python_full_version == '3.9.*'" }, - { name = "pyyaml", marker = "python_full_version == '3.9.*'" }, - { name = "sphinx", marker = "python_full_version == '3.9.*'" }, + { name = "docutils" }, + { name = "jinja2" }, + { name = "markdown-it-py" }, + { name = "mdit-py-plugins" }, + { name = "pyyaml" }, + { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/be/c7/644c475014b7e0c1ac625a9412a0a3f9b1dbb354d43ed12000d3ac8073f8/myst-parser-0.13.6.tar.gz", hash = "sha256:bec01ecebe9b9c04322f8aebd6fd8e61d2cb9ab711d531065a374cc3dcb1d7be", size = 43824 } wheels = [ { url = "https://files.pythonhosted.org/packages/a6/dc/0a77028b5b7bf8661e1c73569b72f2b822e4d7a570a34d29d01ac789b626/myst_parser-0.13.6-py3-none-any.whl", hash = "sha256:a448b3dcb39bc62a6954f5e18544b83d69ed69d8947cf01f8ebe8b654921b4bf", size = 43785 }, ] -[[package]] -name = "myst-parser" -version = "0.13.7" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version < '3.9'", -] -dependencies = [ - { name = "docutils", version = "0.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.9.*'" }, - { name = "jinja2", marker = "python_full_version != '3.9.*'" }, - { name = "markdown-it-py", marker = "python_full_version != '3.9.*'" }, - { name = "mdit-py-plugins", marker = "python_full_version != '3.9.*'" }, - { name = "pyyaml", marker = "python_full_version != '3.9.*'" }, - { name = "sphinx", marker = "python_full_version != '3.9.*'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a6/af/e7c4c8634bf90664efa6ab3d550dc6c526b59b2990e9a4bdd192f8edc4aa/myst-parser-0.13.7.tar.gz", hash = "sha256:e4bc99e43e19f70d22e528de8e7cce59f7e8e7c4c34dcba203de92de7a7c7c85", size = 45618 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/40/db9563e8b57710ea9742b74e5228a4bcb8130aceeeab71f8315ca79a7b57/myst_parser-0.13.7-py3-none-any.whl", hash = "sha256:260355b4da8e8865fe080b0638d7f1ab1791dc4bed02a7a48630b6bad4249219", size = 44007 }, -] - [[package]] name = "nbclassic" version = "0.5.6" @@ -1620,8 +1224,7 @@ name = "nbconvert" version = "5.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "bleach", version = "6.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "bleach", version = "6.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "bleach" }, { name = "defusedxml" }, { name = "entrypoints" }, { name = "jinja2" }, @@ -1678,8 +1281,7 @@ name = "nbsphinx" version = "0.9.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "docutils", version = "0.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.9.*'" }, - { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "docutils" }, { name = "jinja2" }, { name = "nbconvert" }, { name = "nbformat" }, @@ -1815,15 +1417,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/41/220f49aaea88bc6fa6cba8d05ecf24676326156c23b991e80b3f2fc24c77/pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56", size = 6877 }, ] -[[package]] -name = "pkgutil-resolve-name" -version = "1.3.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/70/f2/f2891a9dc37398696ddd945012b90ef8d0a034f0012e3f83c3f7a70b0f79/pkgutil_resolve_name-1.3.10.tar.gz", hash = "sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174", size = 5054 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/5c/3d4882ba113fd55bdba9326c1e4c62a15e674a2501de4869e6bd6301f87e/pkgutil_resolve_name-1.3.10-py3-none-any.whl", hash = "sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e", size = 4734 }, -] - [[package]] name = "platformdirs" version = "4.3.6" @@ -1847,7 +1440,6 @@ name = "plumbum" version = "1.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "importlib-resources", marker = "python_full_version < '3.9'" }, { name = "pywin32", marker = "platform_python_implementation != 'PyPy' and sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f0/5d/49ba324ad4ae5b1a4caefafbce7a1648540129344481f2ed4ef6bb68d451/plumbum-1.9.0.tar.gz", hash = "sha256:e640062b72642c3873bd5bdc3effed75ba4d3c70ef6b6a7b907357a84d909219", size = 319083 } @@ -1932,8 +1524,7 @@ version = "2.21.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cfgv" }, - { name = "identify", version = "2.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "identify", version = "2.6.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "identify" }, { name = "nodeenv" }, { name = "pyyaml" }, { name = "virtualenv" }, @@ -1968,9 +1559,6 @@ wheels = [ name = "propcache" version = "0.2.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] sdist = { url = "https://files.pythonhosted.org/packages/a9/4d/5e5a60b78dbc1d464f8a7bbaeb30957257afdc8512cbb9dfd5659304f5cd/propcache-0.2.0.tar.gz", hash = "sha256:df81779732feb9d01e5d513fad0122efb3d53bbc75f61b2a4f29a020bc985e70", size = 40951 } wheels = [ { url = "https://files.pythonhosted.org/packages/3a/08/1963dfb932b8d74d5b09098507b37e9b96c835ba89ab8aad35aa330f4ff3/propcache-0.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c5869b8fd70b81835a6f187c5fdbe67917a04d7e52b6e7cc4e5fe39d55c39d58", size = 80712 }, @@ -2037,134 +1625,9 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8d/6f/6272ecc7a8daad1d0754cfc6c8846076a8cb13f810005c79b15ce0ef0cf2/propcache-0.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4a91d44379f45f5e540971d41e4626dacd7f01004826a18cb048e7da7e96544", size = 221075 }, { url = "https://files.pythonhosted.org/packages/7c/bd/c7a6a719a6b3dd8b3aeadb3675b5783983529e4a3185946aa444d3e078f6/propcache-0.2.0-cp313-cp313-win32.whl", hash = "sha256:f902804113e032e2cdf8c71015651c97af6418363bea8d78dc0911d56c335032", size = 39654 }, { url = "https://files.pythonhosted.org/packages/88/e7/0eef39eff84fa3e001b44de0bd41c7c0e3432e7648ffd3d64955910f002d/propcache-0.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:8f188cfcc64fb1266f4684206c9de0e80f54622c3f22a910cbd200478aeae61e", size = 43705 }, - { url = "https://files.pythonhosted.org/packages/b4/94/2c3d64420fd58ed462e2b416386d48e72dec027cf7bb572066cf3866e939/propcache-0.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:53d1bd3f979ed529f0805dd35ddaca330f80a9a6d90bc0121d2ff398f8ed8861", size = 82315 }, - { url = "https://files.pythonhosted.org/packages/73/b7/9e2a17d9a126f2012b22ddc5d0979c28ca75104e24945214790c1d787015/propcache-0.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:83928404adf8fb3d26793665633ea79b7361efa0287dfbd372a7e74311d51ee6", size = 47188 }, - { url = "https://files.pythonhosted.org/packages/80/ef/18af27caaae5589c08bb5a461cfa136b83b7e7983be604f2140d91f92b97/propcache-0.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:77a86c261679ea5f3896ec060be9dc8e365788248cc1e049632a1be682442063", size = 46314 }, - { url = "https://files.pythonhosted.org/packages/fa/df/8dbd3e472baf73251c0fbb571a3f0a4e3a40c52a1c8c2a6c46ab08736ff9/propcache-0.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:218db2a3c297a3768c11a34812e63b3ac1c3234c3a086def9c0fee50d35add1f", size = 212874 }, - { url = "https://files.pythonhosted.org/packages/7c/57/5d4d783ac594bd56434679b8643673ae12de1ce758116fd8912a7f2313ec/propcache-0.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7735e82e3498c27bcb2d17cb65d62c14f1100b71723b68362872bca7d0913d90", size = 224578 }, - { url = "https://files.pythonhosted.org/packages/66/27/072be8ad434c9a3aa1b561f527984ea0ed4ac072fd18dfaaa2aa2d6e6a2b/propcache-0.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:20a617c776f520c3875cf4511e0d1db847a076d720714ae35ffe0df3e440be68", size = 222636 }, - { url = "https://files.pythonhosted.org/packages/c3/f1/69a30ff0928d07f50bdc6f0147fd9a08e80904fd3fdb711785e518de1021/propcache-0.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67b69535c870670c9f9b14a75d28baa32221d06f6b6fa6f77a0a13c5a7b0a5b9", size = 213573 }, - { url = "https://files.pythonhosted.org/packages/a8/2e/c16716ae113fe0a3219978df3665a6fea049d81d50bd28c4ae72a4c77567/propcache-0.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4569158070180c3855e9c0791c56be3ceeb192defa2cdf6a3f39e54319e56b89", size = 205438 }, - { url = "https://files.pythonhosted.org/packages/e1/df/80e2c5cd5ed56a7bfb1aa58cedb79617a152ae43de7c0a7e800944a6b2e2/propcache-0.2.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:db47514ffdbd91ccdc7e6f8407aac4ee94cc871b15b577c1c324236b013ddd04", size = 202352 }, - { url = "https://files.pythonhosted.org/packages/0f/4e/79f665fa04839f30ffb2903211c718b9660fbb938ac7a4df79525af5aeb3/propcache-0.2.0-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:2a60ad3e2553a74168d275a0ef35e8c0a965448ffbc3b300ab3a5bb9956c2162", size = 200476 }, - { url = "https://files.pythonhosted.org/packages/a9/39/b9ea7b011521dd7cfd2f89bb6b8b304f3c789ea6285445bc145bebc83094/propcache-0.2.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:662dd62358bdeaca0aee5761de8727cfd6861432e3bb828dc2a693aa0471a563", size = 201581 }, - { url = "https://files.pythonhosted.org/packages/e4/81/e8e96c97aa0b675a14e37b12ca9c9713b15cfacf0869e64bf3ab389fabf1/propcache-0.2.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:25a1f88b471b3bc911d18b935ecb7115dff3a192b6fef46f0bfaf71ff4f12418", size = 225628 }, - { url = "https://files.pythonhosted.org/packages/eb/99/15f998c502c214f6c7f51462937605d514a8943a9a6c1fa10f40d2710976/propcache-0.2.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:f60f0ac7005b9f5a6091009b09a419ace1610e163fa5deaba5ce3484341840e7", size = 229270 }, - { url = "https://files.pythonhosted.org/packages/ff/3a/a9f1a0c0e5b994b8f1a1c71bea56bb3e9eeec821cb4dd61e14051c4ba00b/propcache-0.2.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:74acd6e291f885678631b7ebc85d2d4aec458dd849b8c841b57ef04047833bed", size = 207771 }, - { url = "https://files.pythonhosted.org/packages/ff/3e/6103906a66d6713f32880cf6a5ba84a1406b4d66e1b9389bb9b8e1789f9e/propcache-0.2.0-cp38-cp38-win32.whl", hash = "sha256:d9b6ddac6408194e934002a69bcaadbc88c10b5f38fb9307779d1c629181815d", size = 41015 }, - { url = "https://files.pythonhosted.org/packages/37/23/a30214b4c1f2bea24cc1197ef48d67824fbc41d5cf5472b17c37fef6002c/propcache-0.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:676135dcf3262c9c5081cc8f19ad55c8a64e3f7282a21266d05544450bffc3a5", size = 45749 }, - { url = "https://files.pythonhosted.org/packages/38/05/797e6738c9f44ab5039e3ff329540c934eabbe8ad7e63c305c75844bc86f/propcache-0.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:25c8d773a62ce0451b020c7b29a35cfbc05de8b291163a7a0f3b7904f27253e6", size = 81903 }, - { url = "https://files.pythonhosted.org/packages/9f/84/8d5edb9a73e1a56b24dd8f2adb6aac223109ff0e8002313d52e5518258ba/propcache-0.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:375a12d7556d462dc64d70475a9ee5982465fbb3d2b364f16b86ba9135793638", size = 46960 }, - { url = "https://files.pythonhosted.org/packages/e7/77/388697bedda984af0d12d68e536b98129b167282da3401965c8450de510e/propcache-0.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1ec43d76b9677637a89d6ab86e1fef70d739217fefa208c65352ecf0282be957", size = 46133 }, - { url = "https://files.pythonhosted.org/packages/e2/dc/60d444610bc5b1d7a758534f58362b1bcee736a785473f8a39c91f05aad1/propcache-0.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f45eec587dafd4b2d41ac189c2156461ebd0c1082d2fe7013571598abb8505d1", size = 211105 }, - { url = "https://files.pythonhosted.org/packages/bc/c6/40eb0dd1de6f8e84f454615ab61f68eb4a58f9d63d6f6eaf04300ac0cc17/propcache-0.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc092ba439d91df90aea38168e11f75c655880c12782facf5cf9c00f3d42b562", size = 226613 }, - { url = "https://files.pythonhosted.org/packages/de/b6/e078b5e9de58e20db12135eb6a206b4b43cb26c6b62ee0fe36ac40763a64/propcache-0.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa1076244f54bb76e65e22cb6910365779d5c3d71d1f18b275f1dfc7b0d71b4d", size = 225587 }, - { url = "https://files.pythonhosted.org/packages/ce/4e/97059dd24494d1c93d1efb98bb24825e1930265b41858dd59c15cb37a975/propcache-0.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:682a7c79a2fbf40f5dbb1eb6bfe2cd865376deeac65acf9beb607505dced9e12", size = 211826 }, - { url = "https://files.pythonhosted.org/packages/fc/23/4dbf726602a989d2280fe130a9b9dd71faa8d3bb8cd23d3261ff3c23f692/propcache-0.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e40876731f99b6f3c897b66b803c9e1c07a989b366c6b5b475fafd1f7ba3fb8", size = 203140 }, - { url = "https://files.pythonhosted.org/packages/5b/ce/f3bff82c885dbd9ae9e43f134d5b02516c3daa52d46f7a50e4f52ef9121f/propcache-0.2.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:363ea8cd3c5cb6679f1c2f5f1f9669587361c062e4899fce56758efa928728f8", size = 208841 }, - { url = "https://files.pythonhosted.org/packages/29/d7/19a4d3b4c7e95d08f216da97035d0b103d0c90411c6f739d47088d2da1f0/propcache-0.2.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:140fbf08ab3588b3468932974a9331aff43c0ab8a2ec2c608b6d7d1756dbb6cb", size = 203315 }, - { url = "https://files.pythonhosted.org/packages/db/87/5748212a18beb8d4ab46315c55ade8960d1e2cdc190764985b2d229dd3f4/propcache-0.2.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e70fac33e8b4ac63dfc4c956fd7d85a0b1139adcfc0d964ce288b7c527537fea", size = 204724 }, - { url = "https://files.pythonhosted.org/packages/84/2a/c3d2f989fc571a5bad0fabcd970669ccb08c8f9b07b037ecddbdab16a040/propcache-0.2.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b33d7a286c0dc1a15f5fc864cc48ae92a846df287ceac2dd499926c3801054a6", size = 215514 }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4c44c133b08bc5f776afcb8f0833889c2636b8a83e07ea1d9096c1e401b0/propcache-0.2.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f6d5749fdd33d90e34c2efb174c7e236829147a2713334d708746e94c4bde40d", size = 220063 }, - { url = "https://files.pythonhosted.org/packages/2e/25/280d0a3bdaee68db74c0acd9a472e59e64b516735b59cffd3a326ff9058a/propcache-0.2.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:22aa8f2272d81d9317ff5756bb108021a056805ce63dd3630e27d042c8092798", size = 211620 }, - { url = "https://files.pythonhosted.org/packages/28/8c/266898981b7883c1563c35954f9ce9ced06019fdcc487a9520150c48dc91/propcache-0.2.0-cp39-cp39-win32.whl", hash = "sha256:73e4b40ea0eda421b115248d7e79b59214411109a5bc47d0d48e4c73e3b8fcf9", size = 41049 }, - { url = "https://files.pythonhosted.org/packages/af/53/a3e5b937f58e757a940716b88105ec4c211c42790c1ea17052b46dc16f16/propcache-0.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:9517d5e9e0731957468c29dbfd0f976736a0e55afaea843726e887f36fe017df", size = 45587 }, { url = "https://files.pythonhosted.org/packages/3d/b6/e6d98278f2d49b22b4d033c9f792eda783b9ab2094b041f013fc69bcde87/propcache-0.2.0-py3-none-any.whl", hash = "sha256:2ccc28197af5313706511fab3a8b66dcd6da067a1331372c82ea1cb74285e036", size = 11603 }, ] -[[package]] -name = "propcache" -version = "0.2.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version == '3.9.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/20/c8/2a13f78d82211490855b2fb303b6721348d0787fdd9a12ac46d99d3acde1/propcache-0.2.1.tar.gz", hash = "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64", size = 41735 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/a5/0ea64c9426959ef145a938e38c832fc551843481d356713ececa9a8a64e8/propcache-0.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6b3f39a85d671436ee3d12c017f8fdea38509e4f25b28eb25877293c98c243f6", size = 79296 }, - { url = "https://files.pythonhosted.org/packages/76/5a/916db1aba735f55e5eca4733eea4d1973845cf77dfe67c2381a2ca3ce52d/propcache-0.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d51fbe4285d5db5d92a929e3e21536ea3dd43732c5b177c7ef03f918dff9f2", size = 45622 }, - { url = "https://files.pythonhosted.org/packages/2d/62/685d3cf268b8401ec12b250b925b21d152b9d193b7bffa5fdc4815c392c2/propcache-0.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6445804cf4ec763dc70de65a3b0d9954e868609e83850a47ca4f0cb64bd79fea", size = 45133 }, - { url = "https://files.pythonhosted.org/packages/4d/3d/31c9c29ee7192defc05aa4d01624fd85a41cf98e5922aaed206017329944/propcache-0.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9479aa06a793c5aeba49ce5c5692ffb51fcd9a7016e017d555d5e2b0045d212", size = 204809 }, - { url = "https://files.pythonhosted.org/packages/10/a1/e4050776f4797fc86140ac9a480d5dc069fbfa9d499fe5c5d2fa1ae71f07/propcache-0.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9631c5e8b5b3a0fda99cb0d29c18133bca1e18aea9effe55adb3da1adef80d3", size = 219109 }, - { url = "https://files.pythonhosted.org/packages/c9/c0/e7ae0df76343d5e107d81e59acc085cea5fd36a48aa53ef09add7503e888/propcache-0.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3156628250f46a0895f1f36e1d4fbe062a1af8718ec3ebeb746f1d23f0c5dc4d", size = 217368 }, - { url = "https://files.pythonhosted.org/packages/fc/e1/e0a2ed6394b5772508868a977d3238f4afb2eebaf9976f0b44a8d347ad63/propcache-0.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b6fb63ae352e13748289f04f37868099e69dba4c2b3e271c46061e82c745634", size = 205124 }, - { url = "https://files.pythonhosted.org/packages/50/c1/e388c232d15ca10f233c778bbdc1034ba53ede14c207a72008de45b2db2e/propcache-0.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:887d9b0a65404929641a9fabb6452b07fe4572b269d901d622d8a34a4e9043b2", size = 195463 }, - { url = "https://files.pythonhosted.org/packages/0a/fd/71b349b9def426cc73813dbd0f33e266de77305e337c8c12bfb0a2a82bfb/propcache-0.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a96dc1fa45bd8c407a0af03b2d5218392729e1822b0c32e62c5bf7eeb5fb3958", size = 198358 }, - { url = "https://files.pythonhosted.org/packages/02/f2/d7c497cd148ebfc5b0ae32808e6c1af5922215fe38c7a06e4e722fe937c8/propcache-0.2.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a7e65eb5c003a303b94aa2c3852ef130230ec79e349632d030e9571b87c4698c", size = 195560 }, - { url = "https://files.pythonhosted.org/packages/bb/57/f37041bbe5e0dfed80a3f6be2612a3a75b9cfe2652abf2c99bef3455bbad/propcache-0.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:999779addc413181912e984b942fbcc951be1f5b3663cd80b2687758f434c583", size = 196895 }, - { url = "https://files.pythonhosted.org/packages/83/36/ae3cc3e4f310bff2f064e3d2ed5558935cc7778d6f827dce74dcfa125304/propcache-0.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:19a0f89a7bb9d8048d9c4370c9c543c396e894c76be5525f5e1ad287f1750ddf", size = 207124 }, - { url = "https://files.pythonhosted.org/packages/8c/c4/811b9f311f10ce9d31a32ff14ce58500458443627e4df4ae9c264defba7f/propcache-0.2.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1ac2f5fe02fa75f56e1ad473f1175e11f475606ec9bd0be2e78e4734ad575034", size = 210442 }, - { url = "https://files.pythonhosted.org/packages/18/dd/a1670d483a61ecac0d7fc4305d91caaac7a8fc1b200ea3965a01cf03bced/propcache-0.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:574faa3b79e8ebac7cb1d7930f51184ba1ccf69adfdec53a12f319a06030a68b", size = 203219 }, - { url = "https://files.pythonhosted.org/packages/f9/2d/30ced5afde41b099b2dc0c6573b66b45d16d73090e85655f1a30c5a24e07/propcache-0.2.1-cp310-cp310-win32.whl", hash = "sha256:03ff9d3f665769b2a85e6157ac8b439644f2d7fd17615a82fa55739bc97863f4", size = 40313 }, - { url = "https://files.pythonhosted.org/packages/23/84/bd9b207ac80da237af77aa6e153b08ffa83264b1c7882495984fcbfcf85c/propcache-0.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2d3af2e79991102678f53e0dbf4c35de99b6b8b58f29a27ca0325816364caaba", size = 44428 }, - { url = "https://files.pythonhosted.org/packages/bc/0f/2913b6791ebefb2b25b4efd4bb2299c985e09786b9f5b19184a88e5778dd/propcache-0.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ffc3cca89bb438fb9c95c13fc874012f7b9466b89328c3c8b1aa93cdcfadd16", size = 79297 }, - { url = "https://files.pythonhosted.org/packages/cf/73/af2053aeccd40b05d6e19058419ac77674daecdd32478088b79375b9ab54/propcache-0.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f174bbd484294ed9fdf09437f889f95807e5f229d5d93588d34e92106fbf6717", size = 45611 }, - { url = "https://files.pythonhosted.org/packages/3c/09/8386115ba7775ea3b9537730e8cf718d83bbf95bffe30757ccf37ec4e5da/propcache-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:70693319e0b8fd35dd863e3e29513875eb15c51945bf32519ef52927ca883bc3", size = 45146 }, - { url = "https://files.pythonhosted.org/packages/03/7a/793aa12f0537b2e520bf09f4c6833706b63170a211ad042ca71cbf79d9cb/propcache-0.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b480c6a4e1138e1aa137c0079b9b6305ec6dcc1098a8ca5196283e8a49df95a9", size = 232136 }, - { url = "https://files.pythonhosted.org/packages/f1/38/b921b3168d72111769f648314100558c2ea1d52eb3d1ba7ea5c4aa6f9848/propcache-0.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d27b84d5880f6d8aa9ae3edb253c59d9f6642ffbb2c889b78b60361eed449787", size = 239706 }, - { url = "https://files.pythonhosted.org/packages/14/29/4636f500c69b5edea7786db3c34eb6166f3384b905665ce312a6e42c720c/propcache-0.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:857112b22acd417c40fa4595db2fe28ab900c8c5fe4670c7989b1c0230955465", size = 238531 }, - { url = "https://files.pythonhosted.org/packages/85/14/01fe53580a8e1734ebb704a3482b7829a0ef4ea68d356141cf0994d9659b/propcache-0.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf6c4150f8c0e32d241436526f3c3f9cbd34429492abddbada2ffcff506c51af", size = 231063 }, - { url = "https://files.pythonhosted.org/packages/33/5c/1d961299f3c3b8438301ccfbff0143b69afcc30c05fa28673cface692305/propcache-0.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66d4cfda1d8ed687daa4bc0274fcfd5267873db9a5bc0418c2da19273040eeb7", size = 220134 }, - { url = "https://files.pythonhosted.org/packages/00/d0/ed735e76db279ba67a7d3b45ba4c654e7b02bc2f8050671ec365d8665e21/propcache-0.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2f992c07c0fca81655066705beae35fc95a2fa7366467366db627d9f2ee097f", size = 220009 }, - { url = "https://files.pythonhosted.org/packages/75/90/ee8fab7304ad6533872fee982cfff5a53b63d095d78140827d93de22e2d4/propcache-0.2.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:4a571d97dbe66ef38e472703067021b1467025ec85707d57e78711c085984e54", size = 212199 }, - { url = "https://files.pythonhosted.org/packages/eb/ec/977ffaf1664f82e90737275873461695d4c9407d52abc2f3c3e24716da13/propcache-0.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bb6178c241278d5fe853b3de743087be7f5f4c6f7d6d22a3b524d323eecec505", size = 214827 }, - { url = "https://files.pythonhosted.org/packages/57/48/031fb87ab6081764054821a71b71942161619549396224cbb242922525e8/propcache-0.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ad1af54a62ffe39cf34db1aa6ed1a1873bd548f6401db39d8e7cd060b9211f82", size = 228009 }, - { url = "https://files.pythonhosted.org/packages/1a/06/ef1390f2524850838f2390421b23a8b298f6ce3396a7cc6d39dedd4047b0/propcache-0.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e7048abd75fe40712005bcfc06bb44b9dfcd8e101dda2ecf2f5aa46115ad07ca", size = 231638 }, - { url = "https://files.pythonhosted.org/packages/38/2a/101e6386d5a93358395da1d41642b79c1ee0f3b12e31727932b069282b1d/propcache-0.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:160291c60081f23ee43d44b08a7e5fb76681221a8e10b3139618c5a9a291b84e", size = 222788 }, - { url = "https://files.pythonhosted.org/packages/db/81/786f687951d0979007e05ad9346cd357e50e3d0b0f1a1d6074df334b1bbb/propcache-0.2.1-cp311-cp311-win32.whl", hash = "sha256:819ce3b883b7576ca28da3861c7e1a88afd08cc8c96908e08a3f4dd64a228034", size = 40170 }, - { url = "https://files.pythonhosted.org/packages/cf/59/7cc7037b295d5772eceb426358bb1b86e6cab4616d971bd74275395d100d/propcache-0.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:edc9fc7051e3350643ad929df55c451899bb9ae6d24998a949d2e4c87fb596d3", size = 44404 }, - { url = "https://files.pythonhosted.org/packages/4c/28/1d205fe49be8b1b4df4c50024e62480a442b1a7b818e734308bb0d17e7fb/propcache-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:081a430aa8d5e8876c6909b67bd2d937bfd531b0382d3fdedb82612c618bc41a", size = 79588 }, - { url = "https://files.pythonhosted.org/packages/21/ee/fc4d893f8d81cd4971affef2a6cb542b36617cd1d8ce56b406112cb80bf7/propcache-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2ccec9ac47cf4e04897619c0e0c1a48c54a71bdf045117d3a26f80d38ab1fb0", size = 45825 }, - { url = "https://files.pythonhosted.org/packages/4a/de/bbe712f94d088da1d237c35d735f675e494a816fd6f54e9db2f61ef4d03f/propcache-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14d86fe14b7e04fa306e0c43cdbeebe6b2c2156a0c9ce56b815faacc193e320d", size = 45357 }, - { url = "https://files.pythonhosted.org/packages/7f/14/7ae06a6cf2a2f1cb382586d5a99efe66b0b3d0c6f9ac2f759e6f7af9d7cf/propcache-0.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:049324ee97bb67285b49632132db351b41e77833678432be52bdd0289c0e05e4", size = 241869 }, - { url = "https://files.pythonhosted.org/packages/cc/59/227a78be960b54a41124e639e2c39e8807ac0c751c735a900e21315f8c2b/propcache-0.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cd9a1d071158de1cc1c71a26014dcdfa7dd3d5f4f88c298c7f90ad6f27bb46d", size = 247884 }, - { url = "https://files.pythonhosted.org/packages/84/58/f62b4ffaedf88dc1b17f04d57d8536601e4e030feb26617228ef930c3279/propcache-0.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98110aa363f1bb4c073e8dcfaefd3a5cea0f0834c2aab23dda657e4dab2f53b5", size = 248486 }, - { url = "https://files.pythonhosted.org/packages/1c/07/ebe102777a830bca91bbb93e3479cd34c2ca5d0361b83be9dbd93104865e/propcache-0.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:647894f5ae99c4cf6bb82a1bb3a796f6e06af3caa3d32e26d2350d0e3e3faf24", size = 243649 }, - { url = "https://files.pythonhosted.org/packages/ed/bc/4f7aba7f08f520376c4bb6a20b9a981a581b7f2e385fa0ec9f789bb2d362/propcache-0.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfd3223c15bebe26518d58ccf9a39b93948d3dcb3e57a20480dfdd315356baff", size = 229103 }, - { url = "https://files.pythonhosted.org/packages/fe/d5/04ac9cd4e51a57a96f78795e03c5a0ddb8f23ec098b86f92de028d7f2a6b/propcache-0.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d71264a80f3fcf512eb4f18f59423fe82d6e346ee97b90625f283df56aee103f", size = 226607 }, - { url = "https://files.pythonhosted.org/packages/e3/f0/24060d959ea41d7a7cc7fdbf68b31852331aabda914a0c63bdb0e22e96d6/propcache-0.2.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e73091191e4280403bde6c9a52a6999d69cdfde498f1fdf629105247599b57ec", size = 221153 }, - { url = "https://files.pythonhosted.org/packages/77/a7/3ac76045a077b3e4de4859a0753010765e45749bdf53bd02bc4d372da1a0/propcache-0.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3935bfa5fede35fb202c4b569bb9c042f337ca4ff7bd540a0aa5e37131659348", size = 222151 }, - { url = "https://files.pythonhosted.org/packages/e7/af/5e29da6f80cebab3f5a4dcd2a3240e7f56f2c4abf51cbfcc99be34e17f0b/propcache-0.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f508b0491767bb1f2b87fdfacaba5f7eddc2f867740ec69ece6d1946d29029a6", size = 233812 }, - { url = "https://files.pythonhosted.org/packages/8c/89/ebe3ad52642cc5509eaa453e9f4b94b374d81bae3265c59d5c2d98efa1b4/propcache-0.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1672137af7c46662a1c2be1e8dc78cb6d224319aaa40271c9257d886be4363a6", size = 238829 }, - { url = "https://files.pythonhosted.org/packages/e9/2f/6b32f273fa02e978b7577159eae7471b3cfb88b48563b1c2578b2d7ca0bb/propcache-0.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b74c261802d3d2b85c9df2dfb2fa81b6f90deeef63c2db9f0e029a3cac50b518", size = 230704 }, - { url = "https://files.pythonhosted.org/packages/5c/2e/f40ae6ff5624a5f77edd7b8359b208b5455ea113f68309e2b00a2e1426b6/propcache-0.2.1-cp312-cp312-win32.whl", hash = "sha256:d09c333d36c1409d56a9d29b3a1b800a42c76a57a5a8907eacdbce3f18768246", size = 40050 }, - { url = "https://files.pythonhosted.org/packages/3b/77/a92c3ef994e47180862b9d7d11e37624fb1c00a16d61faf55115d970628b/propcache-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:c214999039d4f2a5b2073ac506bba279945233da8c786e490d411dfc30f855c1", size = 44117 }, - { url = "https://files.pythonhosted.org/packages/0f/2a/329e0547cf2def8857157f9477669043e75524cc3e6251cef332b3ff256f/propcache-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aca405706e0b0a44cc6bfd41fbe89919a6a56999157f6de7e182a990c36e37bc", size = 77002 }, - { url = "https://files.pythonhosted.org/packages/12/2d/c4df5415e2382f840dc2ecbca0eeb2293024bc28e57a80392f2012b4708c/propcache-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:12d1083f001ace206fe34b6bdc2cb94be66d57a850866f0b908972f90996b3e9", size = 44639 }, - { url = "https://files.pythonhosted.org/packages/d0/5a/21aaa4ea2f326edaa4e240959ac8b8386ea31dedfdaa636a3544d9e7a408/propcache-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d93f3307ad32a27bda2e88ec81134b823c240aa3abb55821a8da553eed8d9439", size = 44049 }, - { url = "https://files.pythonhosted.org/packages/4e/3e/021b6cd86c0acc90d74784ccbb66808b0bd36067a1bf3e2deb0f3845f618/propcache-0.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba278acf14471d36316159c94a802933d10b6a1e117b8554fe0d0d9b75c9d536", size = 224819 }, - { url = "https://files.pythonhosted.org/packages/3c/57/c2fdeed1b3b8918b1770a133ba5c43ad3d78e18285b0c06364861ef5cc38/propcache-0.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e6281aedfca15301c41f74d7005e6e3f4ca143584ba696ac69df4f02f40d629", size = 229625 }, - { url = "https://files.pythonhosted.org/packages/9d/81/70d4ff57bf2877b5780b466471bebf5892f851a7e2ca0ae7ffd728220281/propcache-0.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b750a8e5a1262434fb1517ddf64b5de58327f1adc3524a5e44c2ca43305eb0b", size = 232934 }, - { url = "https://files.pythonhosted.org/packages/3c/b9/bb51ea95d73b3fb4100cb95adbd4e1acaf2cbb1fd1083f5468eeb4a099a8/propcache-0.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf72af5e0fb40e9babf594308911436c8efde3cb5e75b6f206c34ad18be5c052", size = 227361 }, - { url = "https://files.pythonhosted.org/packages/f1/20/3c6d696cd6fd70b29445960cc803b1851a1131e7a2e4ee261ee48e002bcd/propcache-0.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2d0a12018b04f4cb820781ec0dffb5f7c7c1d2a5cd22bff7fb055a2cb19ebce", size = 213904 }, - { url = "https://files.pythonhosted.org/packages/a1/cb/1593bfc5ac6d40c010fa823f128056d6bc25b667f5393781e37d62f12005/propcache-0.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e800776a79a5aabdb17dcc2346a7d66d0777e942e4cd251defeb084762ecd17d", size = 212632 }, - { url = "https://files.pythonhosted.org/packages/6d/5c/e95617e222be14a34c709442a0ec179f3207f8a2b900273720501a70ec5e/propcache-0.2.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4160d9283bd382fa6c0c2b5e017acc95bc183570cd70968b9202ad6d8fc48dce", size = 207897 }, - { url = "https://files.pythonhosted.org/packages/8e/3b/56c5ab3dc00f6375fbcdeefdede5adf9bee94f1fab04adc8db118f0f9e25/propcache-0.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b43e74f1359353341a7adb783c8f1b1c676367b011709f466f42fda2045e95", size = 208118 }, - { url = "https://files.pythonhosted.org/packages/86/25/d7ef738323fbc6ebcbce33eb2a19c5e07a89a3df2fded206065bd5e868a9/propcache-0.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:58791550b27d5488b1bb52bc96328456095d96206a250d28d874fafe11b3dfaf", size = 217851 }, - { url = "https://files.pythonhosted.org/packages/b3/77/763e6cef1852cf1ba740590364ec50309b89d1c818e3256d3929eb92fabf/propcache-0.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0f022d381747f0dfe27e99d928e31bc51a18b65bb9e481ae0af1380a6725dd1f", size = 222630 }, - { url = "https://files.pythonhosted.org/packages/4f/e9/0f86be33602089c701696fbed8d8c4c07b6ee9605c5b7536fd27ed540c5b/propcache-0.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:297878dc9d0a334358f9b608b56d02e72899f3b8499fc6044133f0d319e2ec30", size = 216269 }, - { url = "https://files.pythonhosted.org/packages/cc/02/5ac83217d522394b6a2e81a2e888167e7ca629ef6569a3f09852d6dcb01a/propcache-0.2.1-cp313-cp313-win32.whl", hash = "sha256:ddfab44e4489bd79bda09d84c430677fc7f0a4939a73d2bba3073036f487a0a6", size = 39472 }, - { url = "https://files.pythonhosted.org/packages/f4/33/d6f5420252a36034bc8a3a01171bc55b4bff5df50d1c63d9caa50693662f/propcache-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:556fc6c10989f19a179e4321e5d678db8eb2924131e64652a51fe83e4c3db0e1", size = 43363 }, - { url = "https://files.pythonhosted.org/packages/0a/08/6ab7f65240a16fa01023125e65258acf7e4884f483f267cdd6fcc48f37db/propcache-0.2.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6a9a8c34fb7bb609419a211e59da8887eeca40d300b5ea8e56af98f6fbbb1541", size = 80403 }, - { url = "https://files.pythonhosted.org/packages/34/fe/e7180285e21b4e6dff7d311fdf22490c9146a09a02834b5232d6248c6004/propcache-0.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ae1aa1cd222c6d205853b3013c69cd04515f9d6ab6de4b0603e2e1c33221303e", size = 46152 }, - { url = "https://files.pythonhosted.org/packages/9c/36/aa74d884af826030ba9cee2ac109b0664beb7e9449c315c9c44db99efbb3/propcache-0.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:accb6150ce61c9c4b7738d45550806aa2b71c7668c6942f17b0ac182b6142fd4", size = 45674 }, - { url = "https://files.pythonhosted.org/packages/22/59/6fe80a3fe7720f715f2c0f6df250dacbd7cad42832410dbd84c719c52f78/propcache-0.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5eee736daafa7af6d0a2dc15cc75e05c64f37fc37bafef2e00d77c14171c2097", size = 207792 }, - { url = "https://files.pythonhosted.org/packages/4a/68/584cd51dd8f4d0f5fff5b128ce0cdb257cde903898eecfb92156bbc2c780/propcache-0.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7a31fc1e1bd362874863fdeed71aed92d348f5336fd84f2197ba40c59f061bd", size = 223280 }, - { url = "https://files.pythonhosted.org/packages/85/cb/4c3528460c41e61b06ec3f970c0f89f87fa21f63acac8642ed81a886c164/propcache-0.2.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba4cfa1052819d16699e1d55d18c92b6e094d4517c41dd231a8b9f87b6fa681", size = 221293 }, - { url = "https://files.pythonhosted.org/packages/69/c0/560e050aa6d31eeece3490d1174da508f05ab27536dfc8474af88b97160a/propcache-0.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f089118d584e859c62b3da0892b88a83d611c2033ac410e929cb6754eec0ed16", size = 208259 }, - { url = "https://files.pythonhosted.org/packages/0c/87/d6c86a77632eb1ba86a328e3313159f246e7564cb5951e05ed77555826a0/propcache-0.2.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:781e65134efaf88feb447e8c97a51772aa75e48b794352f94cb7ea717dedda0d", size = 198632 }, - { url = "https://files.pythonhosted.org/packages/3a/2b/3690ea7b662dc762ab7af5f3ef0e2d7513c823d193d7b2a1b4cda472c2be/propcache-0.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31f5af773530fd3c658b32b6bdc2d0838543de70eb9a2156c03e410f7b0d3aae", size = 203516 }, - { url = "https://files.pythonhosted.org/packages/4d/b5/afe716c16c23c77657185c257a41918b83e03993b6ccdfa748e5e7d328e9/propcache-0.2.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:a7a078f5d37bee6690959c813977da5291b24286e7b962e62a94cec31aa5188b", size = 199402 }, - { url = "https://files.pythonhosted.org/packages/a4/c0/2d2df3aa7f8660d0d4cc4f1e00490c48d5958da57082e70dea7af366f876/propcache-0.2.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cea7daf9fc7ae6687cf1e2c049752f19f146fdc37c2cc376e7d0032cf4f25347", size = 200528 }, - { url = "https://files.pythonhosted.org/packages/21/c8/65ac9142f5e40c8497f7176e71d18826b09e06dd4eb401c9a4ee41aa9c74/propcache-0.2.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:8b3489ff1ed1e8315674d0775dc7d2195fb13ca17b3808721b54dbe9fd020faf", size = 211254 }, - { url = "https://files.pythonhosted.org/packages/09/e4/edb70b447a1d8142df51ec7511e84aa64d7f6ce0a0fdf5eb55363cdd0935/propcache-0.2.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9403db39be1393618dd80c746cb22ccda168efce239c73af13c3763ef56ffc04", size = 214589 }, - { url = "https://files.pythonhosted.org/packages/cb/02/817f309ec8d8883287781d6d9390f80b14db6e6de08bc659dfe798a825c2/propcache-0.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5d97151bc92d2b2578ff7ce779cdb9174337390a535953cbb9452fb65164c587", size = 207283 }, - { url = "https://files.pythonhosted.org/packages/d7/fe/2d18612096ed2212cfef821b6fccdba5d52efc1d64511c206c5c16be28fd/propcache-0.2.1-cp39-cp39-win32.whl", hash = "sha256:9caac6b54914bdf41bcc91e7eb9147d331d29235a7c967c150ef5df6464fd1bb", size = 40866 }, - { url = "https://files.pythonhosted.org/packages/24/2e/b5134802e7b57c403c7b73c7a39374e7a6b7f128d1968b4a4b4c0b700250/propcache-0.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:92fc4500fcb33899b05ba73276dfb684a20d31caa567b7cb5252d48f896a91b1", size = 44975 }, - { url = "https://files.pythonhosted.org/packages/41/b6/c5319caea262f4821995dca2107483b94a3345d4607ad797c76cb9c36bcc/propcache-0.2.1-py3-none-any.whl", hash = "sha256:52277518d6aae65536e9cea52d4e7fd2f7a66f4aa2d30ed3f2fcea620ace3c54", size = 11818 }, -] - [[package]] name = "psutil" version = "6.1.1" @@ -2243,18 +1706,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4a/bb/5f40a4d5e985a43b43f607250e766cdec28904682c3505eb0bd343a4b7db/pyrsistent-0.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e2c116cc804d9b09ce9814d17df5edf1df0c624aba3b43bc1ad90411487036d", size = 118510 }, { url = "https://files.pythonhosted.org/packages/1c/13/e6a22f40f5800af116c02c28e29f15c06aa41cb2036f6a64ab124647f28b/pyrsistent-0.20.0-cp312-cp312-win32.whl", hash = "sha256:e78d0c7c1e99a4a45c99143900ea0546025e41bb59ebc10182e947cf1ece9174", size = 60865 }, { url = "https://files.pythonhosted.org/packages/75/ef/2fa3b55023ec07c22682c957808f9a41836da4cd006b5f55ec76bf0fbfa6/pyrsistent-0.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:4021a7f963d88ccd15b523787d18ed5e5269ce57aa4037146a2377ff607ae87d", size = 63239 }, - { url = "https://files.pythonhosted.org/packages/a5/24/3293a2b2bc4b4d645f2f6743e97b329c18dd9d8177f80e52d2b7911bac0f/pyrsistent-0.20.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:79ed12ba79935adaac1664fd7e0e585a22caa539dfc9b7c7c6d5ebf91fb89054", size = 83450 }, - { url = "https://files.pythonhosted.org/packages/5d/ea/5438a78ba00f2a9cdc6836dcdcd8631b9d802b2bd57d5a61ed9d9ad6f24d/pyrsistent-0.20.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f920385a11207dc372a028b3f1e1038bb244b3ec38d448e6d8e43c6b3ba20e98", size = 121792 }, - { url = "https://files.pythonhosted.org/packages/b1/ff/93dea1abc3e2d44cee0f62974a1f133fc5a4c719c0978148726bd4957b52/pyrsistent-0.20.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f5c2d012671b7391803263419e31b5c7c21e7c95c8760d7fc35602353dee714", size = 121754 }, - { url = "https://files.pythonhosted.org/packages/93/29/93ad2089a3317b00c9f5d863a532339aa44dcd2cd5f8d73c569ef2c9cddb/pyrsistent-0.20.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef3992833fbd686ee783590639f4b8343a57f1f75de8633749d984dc0eb16c86", size = 118326 }, - { url = "https://files.pythonhosted.org/packages/60/c8/6ca4e647512d27b8a9ffe0daf75e284d1cb770c073d845d5893808a6951e/pyrsistent-0.20.0-cp38-cp38-win32.whl", hash = "sha256:881bbea27bbd32d37eb24dd320a5e745a2a5b092a17f6debc1349252fac85423", size = 60841 }, - { url = "https://files.pythonhosted.org/packages/09/6a/6a31c1bbffd4880a8825cea2572e8b3082681215464ebec9404c0b74ab4c/pyrsistent-0.20.0-cp38-cp38-win_amd64.whl", hash = "sha256:6d270ec9dd33cdb13f4d62c95c1a5a50e6b7cdd86302b494217137f760495b9d", size = 63281 }, - { url = "https://files.pythonhosted.org/packages/18/0c/289126299fcebf54fd01d385fb5176c328fef2c4233139c23dd48346e992/pyrsistent-0.20.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ca52d1ceae015859d16aded12584c59eb3825f7b50c6cfd621d4231a6cc624ce", size = 83379 }, - { url = "https://files.pythonhosted.org/packages/4e/45/62639d53ac09eaafc00f2e5845565e70d3eddb2d296337a77637186ca03e/pyrsistent-0.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b318ca24db0f0518630e8b6f3831e9cba78f099ed5c1d65ffe3e023003043ba0", size = 117740 }, - { url = "https://files.pythonhosted.org/packages/ab/12/24b9a6ef7b991b6722756e0aa169a39463af2b8ed0fb526f0a00aae34ea4/pyrsistent-0.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fed2c3216a605dc9a6ea50c7e84c82906e3684c4e80d2908208f662a6cbf9022", size = 117457 }, - { url = "https://files.pythonhosted.org/packages/19/3c/ab06510f86bc0934b77ade41948924ff1f33dcd3433f32feca2028218837/pyrsistent-0.20.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e14c95c16211d166f59c6611533d0dacce2e25de0f76e4c140fde250997b3ca", size = 114280 }, - { url = "https://files.pythonhosted.org/packages/ee/b1/1275bbfb929854d20e72aa2bbfb50ea3b1d7d41a95848b353691875e2817/pyrsistent-0.20.0-cp39-cp39-win32.whl", hash = "sha256:f058a615031eea4ef94ead6456f5ec2026c19fb5bd6bfe86e9665c4158cf802f", size = 60764 }, - { url = "https://files.pythonhosted.org/packages/28/77/0d7af973c0e3b1b83d8b45943601f77f85b943007e3a4d8744f7102c652b/pyrsistent-0.20.0-cp39-cp39-win_amd64.whl", hash = "sha256:58b8f6366e152092194ae68fefe18b9f0b4f89227dfd86a07770c3d86097aebf", size = 63289 }, { url = "https://files.pythonhosted.org/packages/23/88/0acd180010aaed4987c85700b7cc17f9505f3edb4e5873e4dc67f613e338/pyrsistent-0.20.0-py3-none-any.whl", hash = "sha256:c55acc4733aad6560a7f5f818466631f07efc001fd023f34a6c203f8b6df0f0b", size = 58106 }, ] @@ -2292,8 +1743,7 @@ name = "pytest-cov" version = "4.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "coverage", version = "7.6.1", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.9'" }, - { name = "coverage", version = "7.6.10", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.9'" }, + { name = "coverage", extra = ["toml"] }, { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/15/da3df99fd551507694a9b01f512a2f6cf1254f33601605843c3775f39460/pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6", size = 63245 } @@ -2307,7 +1757,6 @@ version = "0.8.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, - { name = "importlib-resources", marker = "python_full_version < '3.9'" }, { name = "jsonschema" }, { name = "nbclient" }, { name = "nbdime" }, @@ -2343,15 +1792,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4b/1e/fdce85e621881efbef407338a54fc712507b72c3a5cd0efc3da65be44cd3/pytray-0.3.4-py2.py3-none-any.whl", hash = "sha256:8e97d20f738bdc5cbede7b1b7fb1ee19b7d4a2bcc798f71581ef3f8875ed5ee4", size = 11091 }, ] -[[package]] -name = "pytz" -version = "2024.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3a/31/3c70bf7603cc2dca0f19bdc53b4537a797747a58875b552c8c413d963a3f/pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", size = 319692 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/11/c3/005fcca25ce078d2cc29fd559379817424e94885510568bc1bc53d7d5846/pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725", size = 508002 }, -] - [[package]] name = "pywin32" version = "308" @@ -2369,10 +1809,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a9/a4/aa562d8935e3df5e49c161b427a3a2efad2ed4e9cf81c3de636f1fdddfd0/pywin32-308-cp313-cp313-win32.whl", hash = "sha256:1c44539a37a5b7b21d02ab34e6a4d314e0788f1690d65b48e9b0b89f31abbbed", size = 5938579 }, { url = "https://files.pythonhosted.org/packages/c7/50/b0efb8bb66210da67a53ab95fd7a98826a97ee21f1d22949863e6d588b22/pywin32-308-cp313-cp313-win_amd64.whl", hash = "sha256:fd380990e792eaf6827fcb7e187b2b4b1cede0585e3d0c9e84201ec27b9905e4", size = 6542056 }, { url = "https://files.pythonhosted.org/packages/26/df/2b63e3e4f2df0224f8aaf6d131f54fe4e8c96400eb9df563e2aae2e1a1f9/pywin32-308-cp313-cp313-win_arm64.whl", hash = "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd", size = 7974986 }, - { url = "https://files.pythonhosted.org/packages/f3/0d/2c464011689e11ff5d64a32337f37de463a0cb058e45de5ea4027b56601a/pywin32-308-cp38-cp38-win32.whl", hash = "sha256:5794e764ebcabf4ff08c555b31bd348c9025929371763b2183172ff4708152f0", size = 5998793 }, - { url = "https://files.pythonhosted.org/packages/b7/e8/729b049e3c5c5449049d6036edf7a24a6ba785a9a1d5f617b638a9b444eb/pywin32-308-cp38-cp38-win_amd64.whl", hash = "sha256:3b92622e29d651c6b783e368ba7d6722b1634b8e70bd376fd7610fe1992e19de", size = 6647446 }, - { url = "https://files.pythonhosted.org/packages/a8/41/ead05a7657ffdbb1edabb954ab80825c4f87a3de0285d59f8290457f9016/pywin32-308-cp39-cp39-win32.whl", hash = "sha256:7873ca4dc60ab3287919881a7d4f88baee4a6e639aa6962de25a98ba6b193341", size = 5991824 }, - { url = "https://files.pythonhosted.org/packages/e4/cd/0838c9a6063bff2e9bac2388ae36524c26c50288b5d7b6aebb6cdf8d375d/pywin32-308-cp39-cp39-win_amd64.whl", hash = "sha256:71b3322d949b4cc20776436a9c9ba0eeedcbc9c650daa536df63f0ff111bb920", size = 6640327 }, ] [[package]] @@ -2385,7 +1821,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/be/e2/af1a99c0432e4e58c9ac8e334ee191790ec9793d33559189b9d2069bdc1d/pywinpty-2.0.14-cp311-none-win_amd64.whl", hash = "sha256:cf2a43ac7065b3e0dc8510f8c1f13a75fb8fde805efa3b8cff7599a1ef497bc7", size = 1397223 }, { url = "https://files.pythonhosted.org/packages/ad/79/759ae767a3b78d340446efd54dd1fe4f7dafa4fc7be96ed757e44bcdba54/pywinpty-2.0.14-cp312-none-win_amd64.whl", hash = "sha256:55dad362ef3e9408ade68fd173e4f9032b3ce08f68cfe7eacb2c263ea1179737", size = 1397207 }, { url = "https://files.pythonhosted.org/packages/7d/34/b77b3c209bf2eaa6455390c8d5449241637f5957f41636a2204065d52bfa/pywinpty-2.0.14-cp313-none-win_amd64.whl", hash = "sha256:074fb988a56ec79ca90ed03a896d40707131897cefb8f76f926e3834227f2819", size = 1396698 }, - { url = "https://files.pythonhosted.org/packages/d8/ef/85e1b0ef7864fa2c579b1c1efce92c5f6fa238c8e73cf9f53deee08f8605/pywinpty-2.0.14-cp39-none-win_amd64.whl", hash = "sha256:5725fd56f73c0531ec218663bd8c8ff5acc43c78962fab28564871b5fce053fd", size = 1397396 }, ] [[package]] @@ -2430,22 +1865,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, - { url = "https://files.pythonhosted.org/packages/74/d9/323a59d506f12f498c2097488d80d16f4cf965cee1791eab58b56b19f47a/PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", size = 183218 }, - { url = "https://files.pythonhosted.org/packages/74/cc/20c34d00f04d785f2028737e2e2a8254e1425102e730fee1d6396f832577/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", size = 728067 }, - { url = "https://files.pythonhosted.org/packages/20/52/551c69ca1501d21c0de51ddafa8c23a0191ef296ff098e98358f69080577/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", size = 757812 }, - { url = "https://files.pythonhosted.org/packages/fd/7f/2c3697bba5d4aa5cc2afe81826d73dfae5f049458e44732c7a0938baa673/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", size = 746531 }, - { url = "https://files.pythonhosted.org/packages/8c/ab/6226d3df99900e580091bb44258fde77a8433511a86883bd4681ea19a858/PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", size = 800820 }, - { url = "https://files.pythonhosted.org/packages/a0/99/a9eb0f3e710c06c5d922026f6736e920d431812ace24aae38228d0d64b04/PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", size = 145514 }, - { url = "https://files.pythonhosted.org/packages/75/8a/ee831ad5fafa4431099aa4e078d4c8efd43cd5e48fbc774641d233b683a9/PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", size = 162702 }, - { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777 }, - { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318 }, - { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891 }, - { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614 }, - { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360 }, - { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006 }, - { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577 }, - { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593 }, - { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312 }, ] [[package]] @@ -2514,43 +1933,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dd/9a/10ed3c7f72b4c24e719c59359fbadd1a27556a28b36cdf1cd9e4fb7845d5/pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:25bf2374a2a8433633c65ccb9553350d5e17e60c8eb4de4d92cc6bd60f01d306", size = 1183489 }, { url = "https://files.pythonhosted.org/packages/72/2d/8660892543fabf1fe41861efa222455811adac9f3c0818d6c3170a1153e3/pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:007137c9ac9ad5ea21e6ad97d3489af654381324d5d3ba614c323f60dab8fae6", size = 1492932 }, { url = "https://files.pythonhosted.org/packages/7b/d6/32fd69744afb53995619bc5effa2a405ae0d343cd3e747d0fbc43fe894ee/pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:470d4a4f6d48fb34e92d768b4e8a5cc3780db0d69107abf1cd7ff734b9766eb0", size = 1392485 }, - { url = "https://files.pythonhosted.org/packages/64/e7/d5d59205d446c299001d27bfc18702c5353512c5485b11ec7cf6df9552d7/pyzmq-26.2.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:2eb7735ee73ca1b0d71e0e67c3739c689067f055c764f73aac4cc8ecf958ee3f", size = 1340492 }, - { url = "https://files.pythonhosted.org/packages/59/bb/aa6616a83694ab43cfb3bdb868d194a5ee2fa24b49e6ec7ec4400691ac3b/pyzmq-26.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a534f43bc738181aa7cbbaf48e3eca62c76453a40a746ab95d4b27b1111a7d2", size = 1008257 }, - { url = "https://files.pythonhosted.org/packages/a6/b6/e578e6c08970df0daa08b7c54e82b606211f9a7e61317ef2db79cc334389/pyzmq-26.2.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:aedd5dd8692635813368e558a05266b995d3d020b23e49581ddd5bbe197a8ab6", size = 907602 }, - { url = "https://files.pythonhosted.org/packages/ab/3a/a26b98aebeb7924b24e9973a2f5bf8974201bb5a3f6ed06ddc3bac19372d/pyzmq-26.2.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8be4700cd8bb02cc454f630dcdf7cfa99de96788b80c51b60fe2fe1dac480289", size = 862291 }, - { url = "https://files.pythonhosted.org/packages/c1/b5/7eedb8d63af13c2858beb9c1f58e90e7e00929176b57f45e3592fccd56dc/pyzmq-26.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fcc03fa4997c447dce58264e93b5aa2d57714fbe0f06c07b7785ae131512732", size = 673879 }, - { url = "https://files.pythonhosted.org/packages/af/22/38734f47543e61b4eb97eee476f0f7ae544988533215eea22fc65e1ca1d7/pyzmq-26.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:402b190912935d3db15b03e8f7485812db350d271b284ded2b80d2e5704be780", size = 1207011 }, - { url = "https://files.pythonhosted.org/packages/59/a4/104cc979ae88ed948ef829db5fb49bca4a771891125fa4166bba1598b2ec/pyzmq-26.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8685fa9c25ff00f550c1fec650430c4b71e4e48e8d852f7ddcf2e48308038640", size = 1516183 }, - { url = "https://files.pythonhosted.org/packages/52/8f/73a8e08897f8ed21fe44fc73b5faf3ea4cacb97bfd219a63ee5f3ea203a8/pyzmq-26.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:76589c020680778f06b7e0b193f4b6dd66d470234a16e1df90329f5e14a171cd", size = 1417481 }, - { url = "https://files.pythonhosted.org/packages/67/cf/f418670a83fb3a91e2d6d26f271a828a58e0265199944a76e4ef274f9ba7/pyzmq-26.2.0-cp38-cp38-win32.whl", hash = "sha256:8423c1877d72c041f2c263b1ec6e34360448decfb323fa8b94e85883043ef988", size = 577930 }, - { url = "https://files.pythonhosted.org/packages/f0/51/1f2b47c8d8fb85c07f088e21df6364b8b5e8298e75bb23ea0e65340ebd82/pyzmq-26.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:76589f2cd6b77b5bdea4fca5992dc1c23389d68b18ccc26a53680ba2dc80ff2f", size = 642503 }, - { url = "https://files.pythonhosted.org/packages/ac/9e/ad5fbbe1bcc7a9d1e8c5f4f7de48f2c1dc481e151ef80cc1ce9a7fe67b55/pyzmq-26.2.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:b1d464cb8d72bfc1a3adc53305a63a8e0cac6bc8c5a07e8ca190ab8d3faa43c2", size = 1341256 }, - { url = "https://files.pythonhosted.org/packages/4c/d9/d7a8022108c214803a82b0b69d4885cee00933d21928f1f09dca371cf4bf/pyzmq-26.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4da04c48873a6abdd71811c5e163bd656ee1b957971db7f35140a2d573f6949c", size = 1009385 }, - { url = "https://files.pythonhosted.org/packages/ed/69/0529b59ac667ea8bfe8796ac71796b688fbb42ff78e06525dabfed3bc7ae/pyzmq-26.2.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d049df610ac811dcffdc147153b414147428567fbbc8be43bb8885f04db39d98", size = 908009 }, - { url = "https://files.pythonhosted.org/packages/6e/bd/3ff3e1172f12f55769793a3a334e956ec2886805ebfb2f64756b6b5c6a1a/pyzmq-26.2.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05590cdbc6b902101d0e65d6a4780af14dc22914cc6ab995d99b85af45362cc9", size = 862078 }, - { url = "https://files.pythonhosted.org/packages/c3/ec/ab13585c3a1f48e2874253844c47b194d56eb25c94718691349c646f336f/pyzmq-26.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c811cfcd6a9bf680236c40c6f617187515269ab2912f3d7e8c0174898e2519db", size = 673756 }, - { url = "https://files.pythonhosted.org/packages/1e/be/febcd4b04dd50ee6d514dfbc33a3d5d9cb38ec9516e02bbfc929baa0f141/pyzmq-26.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6835dd60355593de10350394242b5757fbbd88b25287314316f266e24c61d073", size = 1203684 }, - { url = "https://files.pythonhosted.org/packages/16/28/304150e71afd2df3b82f52f66c0d8ab9ac6fe1f1ffdf92bad4c8cc91d557/pyzmq-26.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc6bee759a6bddea5db78d7dcd609397449cb2d2d6587f48f3ca613b19410cfc", size = 1515864 }, - { url = "https://files.pythonhosted.org/packages/18/89/8d48d8cd505c12a1f5edee597cc32ffcedc65fd8d2603aebaaedc38a7041/pyzmq-26.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c530e1eecd036ecc83c3407f77bb86feb79916d4a33d11394b8234f3bd35b940", size = 1415383 }, - { url = "https://files.pythonhosted.org/packages/d4/7e/43a60c3b179f7da0cbc2b649bd2702fd6a39bff5f72aa38d6e1aeb00256d/pyzmq-26.2.0-cp39-cp39-win32.whl", hash = "sha256:367b4f689786fca726ef7a6c5ba606958b145b9340a5e4808132cc65759abd44", size = 578540 }, - { url = "https://files.pythonhosted.org/packages/3a/55/8841dcd28f783ad06674c8fe8d7d72794b548d0bff8829aaafeb72e8b44d/pyzmq-26.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:e6fa2e3e683f34aea77de8112f6483803c96a44fd726d7358b9888ae5bb394ec", size = 642147 }, - { url = "https://files.pythonhosted.org/packages/b4/78/b3c31ccfcfcdd6ea50b6abc8f46a2a7aadb9c3d40531d1b908d834aaa12e/pyzmq-26.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:7445be39143a8aa4faec43b076e06944b8f9d0701b669df4af200531b21e40bb", size = 543903 }, { url = "https://files.pythonhosted.org/packages/53/fb/36b2b2548286e9444e52fcd198760af99fd89102b5be50f0660fcfe902df/pyzmq-26.2.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:706e794564bec25819d21a41c31d4df2d48e1cc4b061e8d345d7fb4dd3e94072", size = 906955 }, { url = "https://files.pythonhosted.org/packages/77/8f/6ce54f8979a01656e894946db6299e2273fcee21c8e5fa57c6295ef11f57/pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b435f2753621cd36e7c1762156815e21c985c72b19135dac43a7f4f31d28dd1", size = 565701 }, { url = "https://files.pythonhosted.org/packages/ee/1c/bf8cd66730a866b16db8483286078892b7f6536f8c389fb46e4beba0a970/pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:160c7e0a5eb178011e72892f99f918c04a131f36056d10d9c1afb223fc952c2d", size = 794312 }, { url = "https://files.pythonhosted.org/packages/71/43/91fa4ff25bbfdc914ab6bafa0f03241d69370ef31a761d16bb859f346582/pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c4a71d5d6e7b28a47a394c0471b7e77a0661e2d651e7ae91e0cab0a587859ca", size = 752775 }, { url = "https://files.pythonhosted.org/packages/ec/d2/3b2ab40f455a256cb6672186bea95cd97b459ce4594050132d71e76f0d6f/pyzmq-26.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:90412f2db8c02a3864cbfc67db0e3dcdbda336acf1c469526d3e869394fe001c", size = 550762 }, - { url = "https://files.pythonhosted.org/packages/38/a7/1c80b0c8013befad391b92ba8a8e597de8884605ad5ad8ab943c888eb3ca/pyzmq-26.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:39887ac397ff35b7b775db7201095fc6310a35fdbae85bac4523f7eb3b840e20", size = 906946 }, - { url = "https://files.pythonhosted.org/packages/9c/ac/34a7ee2e7edb07c7222752096650313424eb05f18401ed0a964e996088fb/pyzmq-26.2.0-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fdb5b3e311d4d4b0eb8b3e8b4d1b0a512713ad7e6a68791d0923d1aec433d919", size = 802021 }, - { url = "https://files.pythonhosted.org/packages/cd/70/c65ddccfb88b469b6044f9664c81f0b7f649711e0dc172cba8b2a968ad99/pyzmq-26.2.0-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:226af7dcb51fdb0109f0016449b357e182ea0ceb6b47dfb5999d569e5db161d5", size = 756818 }, - { url = "https://files.pythonhosted.org/packages/07/7a/fc77f6d57f592207403eab2deca4c6f1ffa9c78b0f03b59e69069a12a1a1/pyzmq-26.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bed0e799e6120b9c32756203fb9dfe8ca2fb8467fed830c34c877e25638c3fc", size = 565698 }, - { url = "https://files.pythonhosted.org/packages/dc/13/e8494ba2d161fb471955fadbef7f48076bd29b19a4dd3c5d61d22e500505/pyzmq-26.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:29c7947c594e105cb9e6c466bace8532dc1ca02d498684128b339799f5248277", size = 550757 }, - { url = "https://files.pythonhosted.org/packages/6c/78/3096d72581365dfb0081ac9512a3b53672fa69854aa174d78636510c4db8/pyzmq-26.2.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cdeabcff45d1c219636ee2e54d852262e5c2e085d6cb476d938aee8d921356b3", size = 906945 }, - { url = "https://files.pythonhosted.org/packages/da/f2/8054574d77c269c31d055d4daf3d8407adf61ea384a50c8d14b158551d09/pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35cffef589bcdc587d06f9149f8d5e9e8859920a071df5a2671de2213bef592a", size = 565698 }, - { url = "https://files.pythonhosted.org/packages/77/21/c3ad93236d1d60eea10b67528f55e7db115a9d32e2bf163fcf601f85e9cc/pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18c8dc3b7468d8b4bdf60ce9d7141897da103c7a4690157b32b60acb45e333e6", size = 794307 }, - { url = "https://files.pythonhosted.org/packages/6a/49/e95b491724500fcb760178ce8db39b923429e328e57bcf9162e32c2c187c/pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7133d0a1677aec369d67dd78520d3fa96dd7f3dcec99d66c1762870e5ea1a50a", size = 752769 }, - { url = "https://files.pythonhosted.org/packages/9b/a9/50c9c06762b30792f71aaad8d1886748d39c4bffedc1171fbc6ad2b92d67/pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6a96179a24b14fa6428cbfc08641c779a53f8fcec43644030328f44034c7f1f4", size = 751338 }, - { url = "https://files.pythonhosted.org/packages/ca/63/27e6142b4f67a442ee480986ca5b88edb01462dd2319843057683a5148bd/pyzmq-26.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4f78c88905461a9203eac9faac157a2a0dbba84a0fd09fd29315db27be40af9f", size = 550757 }, ] [[package]] @@ -2561,8 +1948,7 @@ dependencies = [ { name = "certifi" }, { name = "charset-normalizer" }, { name = "idna" }, - { name = "urllib3", version = "2.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "urllib3", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } wheels = [ @@ -2582,27 +1968,11 @@ wheels = [ name = "setuptools" version = "75.3.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] sdist = { url = "https://files.pythonhosted.org/packages/ed/22/a438e0caa4576f8c383fa4d35f1cc01655a46c75be358960d815bfbb12bd/setuptools-75.3.0.tar.gz", hash = "sha256:fba5dd4d766e97be1b1681d98712680ae8f2f26d7881245f2ce9e40714f1a686", size = 1351577 } wheels = [ { url = "https://files.pythonhosted.org/packages/90/12/282ee9bce8b58130cb762fbc9beabd531549952cac11fc56add11dcb7ea0/setuptools-75.3.0-py3-none-any.whl", hash = "sha256:f2504966861356aa38616760c0f66568e535562374995367b4e69c7143cf6bcd", size = 1251070 }, ] -[[package]] -name = "setuptools" -version = "75.8.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version == '3.9.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/92/ec/089608b791d210aec4e7f97488e67ab0d33add3efccb83a056cbafe3a2a6/setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6", size = 1343222 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/8a/b9dc7678803429e4a3bc9ba462fa3dd9066824d3c607490235c6a796be5a/setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3", size = 1228782 }, -] - [[package]] name = "shortuuid" version = "1.0.8" @@ -2662,31 +2032,23 @@ name = "sphinx" version = "3.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "alabaster", version = "0.7.13", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "alabaster", version = "0.7.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "alabaster" }, { name = "babel" }, { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "docutils", version = "0.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.9.*'" }, - { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "docutils" }, { name = "imagesize" }, { name = "jinja2" }, { name = "packaging" }, { name = "pygments" }, { name = "requests" }, - { name = "setuptools", version = "75.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "setuptools", version = "75.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "setuptools" }, { name = "snowballstemmer" }, - { name = "sphinxcontrib-applehelp", version = "1.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "sphinxcontrib-applehelp", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, - { name = "sphinxcontrib-devhelp", version = "1.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "sphinxcontrib-devhelp", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, - { name = "sphinxcontrib-htmlhelp", version = "2.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "sphinxcontrib-htmlhelp", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, { name = "sphinxcontrib-jsmath" }, - { name = "sphinxcontrib-qthelp", version = "1.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "sphinxcontrib-qthelp", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, - { name = "sphinxcontrib-serializinghtml", version = "1.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "sphinxcontrib-serializinghtml", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/57/58/48268b16bf3e6e8288c4c6f3d500e4dd1ca0210289a5be8366bd6d2e6088/Sphinx-3.2.1.tar.gz", hash = "sha256:321d6d9b16fa381a5306e5a0b76cd48ffbc588e6340059a729c6fdd66087e0e8", size = 5970067 } wheels = [ @@ -2697,40 +2059,16 @@ wheels = [ name = "sphinx-autobuild" version = "2021.3.14" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] dependencies = [ - { name = "colorama", marker = "python_full_version < '3.9'" }, - { name = "livereload", marker = "python_full_version < '3.9'" }, - { name = "sphinx", marker = "python_full_version < '3.9'" }, + { name = "colorama" }, + { name = "livereload" }, + { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/df/a5/2ed1b81e398bc14533743be41bf0ceaa49d671675f131c4d9ce74897c9c1/sphinx-autobuild-2021.3.14.tar.gz", hash = "sha256:de1ca3b66e271d2b5b5140c35034c89e47f263f2cd5db302c9217065f7443f05", size = 206402 } wheels = [ { url = "https://files.pythonhosted.org/packages/7e/7d/8fb7557b6c9298d2bcda57f4d070de443c6355dfb475582378e2aa16a02c/sphinx_autobuild-2021.3.14-py3-none-any.whl", hash = "sha256:8fe8cbfdb75db04475232f05187c776f46f6e9e04cacf1e49ce81bdac649ccac", size = 9881 }, ] -[[package]] -name = "sphinx-autobuild" -version = "2024.10.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version == '3.9.*'", -] -dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.9'" }, - { name = "sphinx", marker = "python_full_version >= '3.9'" }, - { name = "starlette", marker = "python_full_version >= '3.9'" }, - { name = "uvicorn", marker = "python_full_version >= '3.9'" }, - { name = "watchfiles", marker = "python_full_version >= '3.9'" }, - { name = "websockets", marker = "python_full_version >= '3.9'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a5/2c/155e1de2c1ba96a72e5dba152c509a8b41e047ee5c2def9e9f0d812f8be7/sphinx_autobuild-2024.10.3.tar.gz", hash = "sha256:248150f8f333e825107b6d4b86113ab28fa51750e5f9ae63b59dc339be951fb1", size = 14023 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/c0/eba125db38c84d3c74717008fd3cb5000b68cd7e2cbafd1349c6a38c3d3b/sphinx_autobuild-2024.10.3-py3-none-any.whl", hash = "sha256:158e16c36f9d633e613c9aaf81c19b0fc458ca78b112533b20dafcda430d60fa", size = 11908 }, -] - [[package]] name = "sphinx-book-theme" version = "0.0.42" @@ -2738,8 +2076,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "beautifulsoup4" }, { name = "click" }, - { name = "docutils", version = "0.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.9.*'" }, - { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "docutils" }, { name = "pydata-sphinx-theme" }, { name = "pyyaml" }, { name = "sphinx" }, @@ -2754,10 +2091,8 @@ name = "sphinx-togglebutton" version = "0.2.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "docutils", version = "0.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.9.*'" }, - { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, - { name = "setuptools", version = "75.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "setuptools", version = "75.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "docutils" }, + { name = "setuptools" }, { name = "sphinx" }, { name = "wheel" }, ] @@ -2770,77 +2105,29 @@ wheels = [ name = "sphinxcontrib-applehelp" version = "1.0.4" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] sdist = { url = "https://files.pythonhosted.org/packages/32/df/45e827f4d7e7fcc84e853bcef1d836effd762d63ccb86f43ede4e98b478c/sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e", size = 24766 } wheels = [ { url = "https://files.pythonhosted.org/packages/06/c1/5e2cafbd03105ce50d8500f9b4e8a6e8d02e22d0475b574c3b3e9451a15f/sphinxcontrib_applehelp-1.0.4-py3-none-any.whl", hash = "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228", size = 120601 }, ] -[[package]] -name = "sphinxcontrib-applehelp" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version == '3.9.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300 }, -] - [[package]] name = "sphinxcontrib-devhelp" version = "1.0.2" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] sdist = { url = "https://files.pythonhosted.org/packages/98/33/dc28393f16385f722c893cb55539c641c9aaec8d1bc1c15b69ce0ac2dbb3/sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4", size = 17398 } wheels = [ { url = "https://files.pythonhosted.org/packages/c5/09/5de5ed43a521387f18bdf5f5af31d099605c992fd25372b2b9b825ce48ee/sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e", size = 84690 }, ] -[[package]] -name = "sphinxcontrib-devhelp" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version == '3.9.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530 }, -] - [[package]] name = "sphinxcontrib-htmlhelp" version = "2.0.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] sdist = { url = "https://files.pythonhosted.org/packages/b3/47/64cff68ea3aa450c373301e5bebfbb9fce0a3e70aca245fcadd4af06cd75/sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff", size = 27967 } wheels = [ { url = "https://files.pythonhosted.org/packages/6e/ee/a1f5e39046cbb5f8bc8fba87d1ddf1c6643fbc9194e58d26e606de4b9074/sphinxcontrib_htmlhelp-2.0.1-py3-none-any.whl", hash = "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903", size = 99833 }, ] -[[package]] -name = "sphinxcontrib-htmlhelp" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version == '3.9.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705 }, -] - [[package]] name = "sphinxcontrib-jsmath" version = "1.0.1" @@ -2854,52 +2141,20 @@ wheels = [ name = "sphinxcontrib-qthelp" version = "1.0.3" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] sdist = { url = "https://files.pythonhosted.org/packages/b1/8e/c4846e59f38a5f2b4a0e3b27af38f2fcf904d4bfd82095bf92de0b114ebd/sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72", size = 21658 } wheels = [ { url = "https://files.pythonhosted.org/packages/2b/14/05f9206cf4e9cfca1afb5fd224c7cd434dcc3a433d6d9e4e0264d29c6cdb/sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6", size = 90609 }, ] -[[package]] -name = "sphinxcontrib-qthelp" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version == '3.9.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743 }, -] - [[package]] name = "sphinxcontrib-serializinghtml" version = "1.1.5" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] sdist = { url = "https://files.pythonhosted.org/packages/b5/72/835d6fadb9e5d02304cf39b18f93d227cd93abd3c41ebf58e6853eeb1455/sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952", size = 21019 } wheels = [ { url = "https://files.pythonhosted.org/packages/c6/77/5464ec50dd0f1c1037e3c93249b040c8fc8078fdda97530eeb02424b6eea/sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd", size = 94021 }, ] -[[package]] -name = "sphinxcontrib-serializinghtml" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version == '3.9.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072 }, -] - [[package]] name = "sqlalchemy" version = "1.4.54" @@ -2926,33 +2181,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/8b/f45dd378f6c97e8ff9332ff3d03ecb0b8c491be5bb7a698783b5a2f358ec/SQLAlchemy-1.4.54-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5e0d47d619c739bdc636bbe007da4519fc953393304a5943e0b5aec96c9877c", size = 1629232 }, { url = "https://files.pythonhosted.org/packages/0d/3c/884fe389f5bec86a310b81e79abaa1e26e5d78dc10a84d544a6822833e47/SQLAlchemy-1.4.54-cp312-cp312-win32.whl", hash = "sha256:12bc0141b245918b80d9d17eca94663dbd3f5266ac77a0be60750f36102bbb0f", size = 1592027 }, { url = "https://files.pythonhosted.org/packages/01/c3/c690d037be57efd3a69cde16a2ef1bd2a905dafe869434d33836de0983d0/SQLAlchemy-1.4.54-cp312-cp312-win_amd64.whl", hash = "sha256:f941aaf15f47f316123e1933f9ea91a6efda73a161a6ab6046d1cde37be62c88", size = 1593827 }, - { url = "https://files.pythonhosted.org/packages/f2/04/66ce6c75569f0b9627b470de66f60a0cc6412fe3203b1fb8d433b9df5f84/SQLAlchemy-1.4.54-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:a49730afb716f3f675755afec109895cab95bc9875db7ffe2e42c1b1c6279482", size = 1573468 }, - { url = "https://files.pythonhosted.org/packages/9b/2f/6a2984e9e572996baf84c2595ee6667d239329621979d02e5dbb42964f71/SQLAlchemy-1.4.54-cp38-cp38-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26e78444bc77d089e62874dc74df05a5c71f01ac598010a327881a48408d0064", size = 1640879 }, - { url = "https://files.pythonhosted.org/packages/a0/de/0275a86ee1b8c87823202d11978289a56f44a7ac39623c2ddea91af0a4ee/SQLAlchemy-1.4.54-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02d2ecb9508f16ab9c5af466dfe5a88e26adf2e1a8d1c56eb616396ccae2c186", size = 1629504 }, - { url = "https://files.pythonhosted.org/packages/70/27/b4229d89862a15ccc4834daf24b9182feac686937ff1a2da5f5ea47b3f5d/SQLAlchemy-1.4.54-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:394b0135900b62dbf63e4809cdc8ac923182af2816d06ea61cd6763943c2cc05", size = 1640873 }, - { url = "https://files.pythonhosted.org/packages/02/0c/b2d0a3c6f90ba19c77f4d02e16ddcbb05cf05fe56860be37053907afa31c/SQLAlchemy-1.4.54-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ed3576675c187e3baa80b02c4c9d0edfab78eff4e89dd9da736b921333a2432", size = 1640766 }, - { url = "https://files.pythonhosted.org/packages/e6/72/68fdcbed8ff41db8ec9cc63d6a72bd6e10ae2dee80492a240c726e42bbbc/SQLAlchemy-1.4.54-cp38-cp38-win32.whl", hash = "sha256:fc9ffd9a38e21fad3e8c5a88926d57f94a32546e937e0be46142b2702003eba7", size = 1591725 }, - { url = "https://files.pythonhosted.org/packages/5a/c8/df13167d3825683e0542965dfcfbc3e95b2f31469fd389dbb0390d39ff4c/SQLAlchemy-1.4.54-cp38-cp38-win_amd64.whl", hash = "sha256:a01bc25eb7a5688656c8770f931d5cb4a44c7de1b3cec69b84cc9745d1e4cc10", size = 1593545 }, - { url = "https://files.pythonhosted.org/packages/c0/2c/d29f176e46fb81cdacc30e1cd60bbd2f56e97ce533a603a86fb5755a2812/SQLAlchemy-1.4.54-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:0b76bbb1cbae618d10679be8966f6d66c94f301cfc15cb49e2f2382563fb6efb", size = 1573472 }, - { url = "https://files.pythonhosted.org/packages/66/7c/6c7bae8e5a6ecd4d3cc34a2a5929c0599b954cd00877a50772fa42304d78/SQLAlchemy-1.4.54-cp39-cp39-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdb2886c0be2c6c54d0651d5a61c29ef347e8eec81fd83afebbf7b59b80b7393", size = 1638334 }, - { url = "https://files.pythonhosted.org/packages/9f/84/719fa1c53f044aede7d20c5a0859f8302eadbf1777b054ebc8c46b46bf19/SQLAlchemy-1.4.54-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:954816850777ac234a4e32b8c88ac1f7847088a6e90cfb8f0e127a1bf3feddff", size = 1626761 }, - { url = "https://files.pythonhosted.org/packages/c4/89/7d0ab875d2e6f931617d4a8fff63436b2d05205f15de06ef29f6627759a1/SQLAlchemy-1.4.54-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1d83cd1cc03c22d922ec94d0d5f7b7c96b1332f5e122e81b1a61fb22da77879a", size = 1638328 }, - { url = "https://files.pythonhosted.org/packages/4f/39/0c9186e581f07c2d58ab713490ab242920700ef162453cf6f0719c1661fe/SQLAlchemy-1.4.54-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1576fba3616f79496e2f067262200dbf4aab1bb727cd7e4e006076686413c80c", size = 1638219 }, - { url = "https://files.pythonhosted.org/packages/3a/8b/4676c988e933dccc7f26a8222ad08ccf4cf1697bd2464cdde05f6bf07eb2/SQLAlchemy-1.4.54-cp39-cp39-win32.whl", hash = "sha256:3112de9e11ff1957148c6de1df2bc5cc1440ee36783412e5eedc6f53638a577d", size = 1591716 }, - { url = "https://files.pythonhosted.org/packages/68/24/70f788b22d0799e0a8b4e952d42629e48beca0e5fb30688b9a431b2c4058/SQLAlchemy-1.4.54-cp39-cp39-win_amd64.whl", hash = "sha256:6da60fb24577f989535b8fc8b2ddc4212204aaf02e53c4c7ac94ac364150ed08", size = 1593546 }, -] - -[[package]] -name = "starlette" -version = "0.45.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio", version = "4.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, - { name = "typing-extensions", marker = "python_full_version == '3.9.*'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/90/4f/e1c9f4ec3dae67a94c9285ed275355d5f7cf0f3a5c34538c8ae5412af550/starlette-0.45.2.tar.gz", hash = "sha256:bba1831d15ae5212b22feab2f218bab6ed3cd0fc2dc1d4442443bb1ee52260e0", size = 2574026 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/ab/fe4f57c83620b39dfc9e7687ebad59129ff05170b99422105019d9a65eec/starlette-0.45.2-py3-none-any.whl", hash = "sha256:4daec3356fb0cb1e723a5235e5beaf375d2259af27532958e2d79df549dad9da", size = 71505 }, ] [[package]] @@ -3066,41 +2294,11 @@ wheels = [ name = "urllib3" version = "2.2.3" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } wheels = [ { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, ] -[[package]] -name = "urllib3" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version == '3.9.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, -] - -[[package]] -name = "uvicorn" -version = "0.34.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click", marker = "python_full_version >= '3.9'" }, - { name = "h11", marker = "python_full_version >= '3.9'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, -] - [[package]] name = "virtualenv" version = "20.29.1" @@ -3115,87 +2313,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/89/9b/599bcfc7064fbe5740919e78c5df18e5dceb0887e676256a1061bb5ae232/virtualenv-20.29.1-py3-none-any.whl", hash = "sha256:4e4cb403c0b0da39e13b46b1b2476e505cb0046b25f242bee80f62bf990b2779", size = 4282379 }, ] -[[package]] -name = "watchfiles" -version = "1.0.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio", version = "4.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f5/26/c705fc77d0a9ecdb9b66f1e2976d95b81df3cae518967431e7dbf9b5e219/watchfiles-1.0.4.tar.gz", hash = "sha256:6ba473efd11062d73e4f00c2b730255f9c1bdd73cd5f9fe5b5da8dbd4a717205", size = 94625 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/14/02/22fcaed0396730b0d362bc8d1ffb3be2658fd473eecbb2ba84243e157f11/watchfiles-1.0.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ba5bb3073d9db37c64520681dd2650f8bd40902d991e7b4cfaeece3e32561d08", size = 395212 }, - { url = "https://files.pythonhosted.org/packages/e9/3d/ec5a2369a46edf3ebe092c39d9ae48e8cb6dacbde51c4b4f98936c524269/watchfiles-1.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f25d0ba0fe2b6d2c921cf587b2bf4c451860086534f40c384329fb96e2044d1", size = 384815 }, - { url = "https://files.pythonhosted.org/packages/df/b4/898991cececbe171e67142c31905510203649569d9817848f47c4177ee42/watchfiles-1.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47eb32ef8c729dbc4f4273baece89398a4d4b5d21a1493efea77a17059f4df8a", size = 450680 }, - { url = "https://files.pythonhosted.org/packages/58/f7/d4aa3000e812cfb5e5c2c6c0a3ec9d0a46a42489a8727edd160631c4e210/watchfiles-1.0.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:076f293100db3b0b634514aa0d294b941daa85fc777f9c698adb1009e5aca0b1", size = 455923 }, - { url = "https://files.pythonhosted.org/packages/dd/95/7e2e4c6aba1b02fb5c76d2f6a450b85215921ec5f8f7ad5efd075369563f/watchfiles-1.0.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1eacd91daeb5158c598fe22d7ce66d60878b6294a86477a4715154990394c9b3", size = 482339 }, - { url = "https://files.pythonhosted.org/packages/bb/67/4265b0fabcc2ef2c9e3e8802ba7908cf718a357ebfb49c72e53787156a48/watchfiles-1.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13c2ce7b72026cfbca120d652f02c7750f33b4c9395d79c9790b27f014c8a5a2", size = 519908 }, - { url = "https://files.pythonhosted.org/packages/0d/96/b57802d5f8164bdf070befb4fd3dec4edba5a364ec0670965a97eb8098ce/watchfiles-1.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:90192cdc15ab7254caa7765a98132a5a41471cf739513cc9bcf7d2ffcc0ec7b2", size = 501410 }, - { url = "https://files.pythonhosted.org/packages/8b/18/6db0de4e8911ba14e31853201b40c0fa9fea5ecf3feb86b0ad58f006dfc3/watchfiles-1.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:278aaa395f405972e9f523bd786ed59dfb61e4b827856be46a42130605fd0899", size = 452876 }, - { url = "https://files.pythonhosted.org/packages/df/df/092a961815edf723a38ba2638c49491365943919c3526cc9cf82c42786a6/watchfiles-1.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a462490e75e466edbb9fc4cd679b62187153b3ba804868452ef0577ec958f5ff", size = 615353 }, - { url = "https://files.pythonhosted.org/packages/f3/cf/b85fe645de4ff82f3f436c5e9032379fce37c303f6396a18f9726cc34519/watchfiles-1.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8d0d0630930f5cd5af929040e0778cf676a46775753e442a3f60511f2409f48f", size = 613187 }, - { url = "https://files.pythonhosted.org/packages/f6/d4/a9fea27aef4dd69689bc3556718c1157a7accb72aa035ece87c1fa8483b5/watchfiles-1.0.4-cp310-cp310-win32.whl", hash = "sha256:cc27a65069bcabac4552f34fd2dce923ce3fcde0721a16e4fb1b466d63ec831f", size = 270799 }, - { url = "https://files.pythonhosted.org/packages/df/02/dbe9d4439f15dd4ad0720b6e039bde9d66d1f830331f34c18eb70fa6608e/watchfiles-1.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:8b1f135238e75d075359cf506b27bf3f4ca12029c47d3e769d8593a2024ce161", size = 284145 }, - { url = "https://files.pythonhosted.org/packages/0f/bb/8461adc4b1fed009546fb797fc0d5698dcfe5e289cb37e1b8f16a93cdc30/watchfiles-1.0.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2a9f93f8439639dc244c4d2902abe35b0279102bca7bbcf119af964f51d53c19", size = 394869 }, - { url = "https://files.pythonhosted.org/packages/55/88/9ebf36b3547176d1709c320de78c1fa3263a46be31b5b1267571d9102686/watchfiles-1.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9eea33ad8c418847dd296e61eb683cae1c63329b6d854aefcd412e12d94ee235", size = 384905 }, - { url = "https://files.pythonhosted.org/packages/03/8a/04335ce23ef78d8c69f0913e8b20cf7d9233e3986543aeef95ef2d6e43d2/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31f1a379c9dcbb3f09cf6be1b7e83b67c0e9faabed0471556d9438a4a4e14202", size = 449944 }, - { url = "https://files.pythonhosted.org/packages/17/4e/c8d5dcd14fe637f4633616dabea8a4af0a10142dccf3b43e0f081ba81ab4/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab594e75644421ae0a2484554832ca5895f8cab5ab62de30a1a57db460ce06c6", size = 456020 }, - { url = "https://files.pythonhosted.org/packages/5e/74/3e91e09e1861dd7fbb1190ce7bd786700dc0fbc2ccd33bb9fff5de039229/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc2eb5d14a8e0d5df7b36288979176fbb39672d45184fc4b1c004d7c3ce29317", size = 482983 }, - { url = "https://files.pythonhosted.org/packages/a1/3d/e64de2d1ce4eb6a574fd78ce3a28c279da263be9ef3cfcab6f708df192f2/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f68d8e9d5a321163ddacebe97091000955a1b74cd43724e346056030b0bacee", size = 520320 }, - { url = "https://files.pythonhosted.org/packages/2c/bd/52235f7063b57240c66a991696ed27e2a18bd6fcec8a1ea5a040b70d0611/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9ce064e81fe79faa925ff03b9f4c1a98b0bbb4a1b8c1b015afa93030cb21a49", size = 500988 }, - { url = "https://files.pythonhosted.org/packages/3a/b0/ff04194141a5fe650c150400dd9e42667916bc0f52426e2e174d779b8a74/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b77d5622ac5cc91d21ae9c2b284b5d5c51085a0bdb7b518dba263d0af006132c", size = 452573 }, - { url = "https://files.pythonhosted.org/packages/3d/9d/966164332c5a178444ae6d165082d4f351bd56afd9c3ec828eecbf190e6a/watchfiles-1.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1941b4e39de9b38b868a69b911df5e89dc43767feeda667b40ae032522b9b5f1", size = 615114 }, - { url = "https://files.pythonhosted.org/packages/94/df/f569ae4c1877f96ad4086c153a8eee5a19a3b519487bf5c9454a3438c341/watchfiles-1.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4f8c4998506241dedf59613082d1c18b836e26ef2a4caecad0ec41e2a15e4226", size = 613076 }, - { url = "https://files.pythonhosted.org/packages/15/ae/8ce5f29e65d5fa5790e3c80c289819c55e12be2e1b9f5b6a0e55e169b97d/watchfiles-1.0.4-cp311-cp311-win32.whl", hash = "sha256:4ebbeca9360c830766b9f0df3640b791be569d988f4be6c06d6fae41f187f105", size = 271013 }, - { url = "https://files.pythonhosted.org/packages/a4/c6/79dc4a7c598a978e5fafa135090aaf7bbb03b8dec7bada437dfbe578e7ed/watchfiles-1.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:05d341c71f3d7098920f8551d4df47f7b57ac5b8dad56558064c3431bdfc0b74", size = 284229 }, - { url = "https://files.pythonhosted.org/packages/37/3d/928633723211753f3500bfb138434f080363b87a1b08ca188b1ce54d1e05/watchfiles-1.0.4-cp311-cp311-win_arm64.whl", hash = "sha256:32b026a6ab64245b584acf4931fe21842374da82372d5c039cba6bf99ef722f3", size = 276824 }, - { url = "https://files.pythonhosted.org/packages/5b/1a/8f4d9a1461709756ace48c98f07772bc6d4519b1e48b5fa24a4061216256/watchfiles-1.0.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:229e6ec880eca20e0ba2f7e2249c85bae1999d330161f45c78d160832e026ee2", size = 391345 }, - { url = "https://files.pythonhosted.org/packages/bc/d2/6750b7b3527b1cdaa33731438432e7238a6c6c40a9924049e4cebfa40805/watchfiles-1.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5717021b199e8353782dce03bd8a8f64438832b84e2885c4a645f9723bf656d9", size = 381515 }, - { url = "https://files.pythonhosted.org/packages/4e/17/80500e42363deef1e4b4818729ed939aaddc56f82f4e72b2508729dd3c6b/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0799ae68dfa95136dde7c472525700bd48777875a4abb2ee454e3ab18e9fc712", size = 449767 }, - { url = "https://files.pythonhosted.org/packages/10/37/1427fa4cfa09adbe04b1e97bced19a29a3462cc64c78630787b613a23f18/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43b168bba889886b62edb0397cab5b6490ffb656ee2fcb22dec8bfeb371a9e12", size = 455677 }, - { url = "https://files.pythonhosted.org/packages/c5/7a/39e9397f3a19cb549a7d380412fd9e507d4854eddc0700bfad10ef6d4dba/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb2c46e275fbb9f0c92e7654b231543c7bbfa1df07cdc4b99fa73bedfde5c844", size = 482219 }, - { url = "https://files.pythonhosted.org/packages/45/2d/7113931a77e2ea4436cad0c1690c09a40a7f31d366f79c6f0a5bc7a4f6d5/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:857f5fc3aa027ff5e57047da93f96e908a35fe602d24f5e5d8ce64bf1f2fc733", size = 518830 }, - { url = "https://files.pythonhosted.org/packages/f9/1b/50733b1980fa81ef3c70388a546481ae5fa4c2080040100cd7bf3bf7b321/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55ccfd27c497b228581e2838d4386301227fc0cb47f5a12923ec2fe4f97b95af", size = 497997 }, - { url = "https://files.pythonhosted.org/packages/2b/b4/9396cc61b948ef18943e7c85ecfa64cf940c88977d882da57147f62b34b1/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c11ea22304d17d4385067588123658e9f23159225a27b983f343fcffc3e796a", size = 452249 }, - { url = "https://files.pythonhosted.org/packages/fb/69/0c65a5a29e057ad0dc691c2fa6c23b2983c7dabaa190ba553b29ac84c3cc/watchfiles-1.0.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:74cb3ca19a740be4caa18f238298b9d472c850f7b2ed89f396c00a4c97e2d9ff", size = 614412 }, - { url = "https://files.pythonhosted.org/packages/7f/b9/319fcba6eba5fad34327d7ce16a6b163b39741016b1996f4a3c96b8dd0e1/watchfiles-1.0.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c7cce76c138a91e720d1df54014a047e680b652336e1b73b8e3ff3158e05061e", size = 611982 }, - { url = "https://files.pythonhosted.org/packages/f1/47/143c92418e30cb9348a4387bfa149c8e0e404a7c5b0585d46d2f7031b4b9/watchfiles-1.0.4-cp312-cp312-win32.whl", hash = "sha256:b045c800d55bc7e2cadd47f45a97c7b29f70f08a7c2fa13241905010a5493f94", size = 271822 }, - { url = "https://files.pythonhosted.org/packages/ea/94/b0165481bff99a64b29e46e07ac2e0df9f7a957ef13bec4ceab8515f44e3/watchfiles-1.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:c2acfa49dd0ad0bf2a9c0bb9a985af02e89345a7189be1efc6baa085e0f72d7c", size = 285441 }, - { url = "https://files.pythonhosted.org/packages/11/de/09fe56317d582742d7ca8c2ca7b52a85927ebb50678d9b0fa8194658f536/watchfiles-1.0.4-cp312-cp312-win_arm64.whl", hash = "sha256:22bb55a7c9e564e763ea06c7acea24fc5d2ee5dfc5dafc5cfbedfe58505e9f90", size = 277141 }, - { url = "https://files.pythonhosted.org/packages/08/98/f03efabec64b5b1fa58c0daab25c68ef815b0f320e54adcacd0d6847c339/watchfiles-1.0.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:8012bd820c380c3d3db8435e8cf7592260257b378b649154a7948a663b5f84e9", size = 390954 }, - { url = "https://files.pythonhosted.org/packages/16/09/4dd49ba0a32a45813debe5fb3897955541351ee8142f586303b271a02b40/watchfiles-1.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa216f87594f951c17511efe5912808dfcc4befa464ab17c98d387830ce07b60", size = 381133 }, - { url = "https://files.pythonhosted.org/packages/76/59/5aa6fc93553cd8d8ee75c6247763d77c02631aed21551a97d94998bf1dae/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c9953cf85529c05b24705639ffa390f78c26449e15ec34d5339e8108c7c407", size = 449516 }, - { url = "https://files.pythonhosted.org/packages/4c/aa/df4b6fe14b6317290b91335b23c96b488d365d65549587434817e06895ea/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cf684aa9bba4cd95ecb62c822a56de54e3ae0598c1a7f2065d51e24637a3c5d", size = 454820 }, - { url = "https://files.pythonhosted.org/packages/5e/71/185f8672f1094ce48af33252c73e39b48be93b761273872d9312087245f6/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f44a39aee3cbb9b825285ff979ab887a25c5d336e5ec3574f1506a4671556a8d", size = 481550 }, - { url = "https://files.pythonhosted.org/packages/85/d7/50ebba2c426ef1a5cb17f02158222911a2e005d401caf5d911bfca58f4c4/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38320582736922be8c865d46520c043bff350956dfc9fbaee3b2df4e1740a4b", size = 518647 }, - { url = "https://files.pythonhosted.org/packages/f0/7a/4c009342e393c545d68987e8010b937f72f47937731225b2b29b7231428f/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39f4914548b818540ef21fd22447a63e7be6e24b43a70f7642d21f1e73371590", size = 497547 }, - { url = "https://files.pythonhosted.org/packages/0f/7c/1cf50b35412d5c72d63b2bf9a4fffee2e1549a245924960dd087eb6a6de4/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f12969a3765909cf5dc1e50b2436eb2c0e676a3c75773ab8cc3aa6175c16e902", size = 452179 }, - { url = "https://files.pythonhosted.org/packages/d6/a9/3db1410e1c1413735a9a472380e4f431ad9a9e81711cda2aaf02b7f62693/watchfiles-1.0.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:0986902677a1a5e6212d0c49b319aad9cc48da4bd967f86a11bde96ad9676ca1", size = 614125 }, - { url = "https://files.pythonhosted.org/packages/f2/e1/0025d365cf6248c4d1ee4c3d2e3d373bdd3f6aff78ba4298f97b4fad2740/watchfiles-1.0.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:308ac265c56f936636e3b0e3f59e059a40003c655228c131e1ad439957592303", size = 611911 }, - { url = "https://files.pythonhosted.org/packages/55/55/035838277d8c98fc8c917ac9beeb0cd6c59d675dc2421df5f9fcf44a0070/watchfiles-1.0.4-cp313-cp313-win32.whl", hash = "sha256:aee397456a29b492c20fda2d8961e1ffb266223625346ace14e4b6d861ba9c80", size = 271152 }, - { url = "https://files.pythonhosted.org/packages/f0/e5/96b8e55271685ddbadc50ce8bc53aa2dff278fb7ac4c2e473df890def2dc/watchfiles-1.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:d6097538b0ae5c1b88c3b55afa245a66793a8fec7ada6755322e465fb1a0e8cc", size = 285216 }, - { url = "https://files.pythonhosted.org/packages/15/81/54484fc2fa715abe79694b975692af963f0878fb9d72b8251aa542bf3f10/watchfiles-1.0.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:d3452c1ec703aa1c61e15dfe9d482543e4145e7c45a6b8566978fbb044265a21", size = 394967 }, - { url = "https://files.pythonhosted.org/packages/14/b3/557f0cd90add86586fe3deeebd11e8299db6bc3452b44a534f844c6ab831/watchfiles-1.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7b75fee5a16826cf5c46fe1c63116e4a156924d668c38b013e6276f2582230f0", size = 384707 }, - { url = "https://files.pythonhosted.org/packages/03/a3/34638e1bffcb85a405e7b005e30bb211fd9be2ab2cb1847f2ceb81bef27b/watchfiles-1.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e997802d78cdb02623b5941830ab06f8860038faf344f0d288d325cc9c5d2ff", size = 450442 }, - { url = "https://files.pythonhosted.org/packages/8f/9f/6a97460dd11a606003d634c7158d9fea8517e98daffc6f56d0f5fde2e86a/watchfiles-1.0.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0611d244ce94d83f5b9aff441ad196c6e21b55f77f3c47608dcf651efe54c4a", size = 455959 }, - { url = "https://files.pythonhosted.org/packages/9d/bb/e0648c6364e4d37ec692bc3f0c77507d17d8bb8f75689148819142010bbf/watchfiles-1.0.4-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9745a4210b59e218ce64c91deb599ae8775c8a9da4e95fb2ee6fe745fc87d01a", size = 483187 }, - { url = "https://files.pythonhosted.org/packages/dd/ad/d9290586a25288a81dfa8ad6329cf1de32aa1a9798ace45259eb95dcfb37/watchfiles-1.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4810ea2ae622add560f4aa50c92fef975e475f7ac4900ce5ff5547b2434642d8", size = 519733 }, - { url = "https://files.pythonhosted.org/packages/4e/a9/150c1666825cc9637093f8cae7fc6f53b3296311ab8bd65f1389acb717cb/watchfiles-1.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:740d103cd01458f22462dedeb5a3382b7f2c57d07ff033fbc9465919e5e1d0f3", size = 502275 }, - { url = "https://files.pythonhosted.org/packages/44/dc/5bfd21e20a330aca1706ac44713bc322838061938edf4b53130f97a7b211/watchfiles-1.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbd912a61543a36aef85e34f212e5d2486e7c53ebfdb70d1e0b060cc50dd0bf", size = 452907 }, - { url = "https://files.pythonhosted.org/packages/50/fe/8f4fc488f1699f564687b697456eb5c0cb8e2b0b8538150511c234c62094/watchfiles-1.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0bc80d91ddaf95f70258cf78c471246846c1986bcc5fd33ccc4a1a67fcb40f9a", size = 615927 }, - { url = "https://files.pythonhosted.org/packages/ad/19/2e45f6f6eec89dd97a4d281635e3d73c17e5f692e7432063bdfdf9562c89/watchfiles-1.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab0311bb2ffcd9f74b6c9de2dda1612c13c84b996d032cd74799adb656af4e8b", size = 613435 }, - { url = "https://files.pythonhosted.org/packages/91/17/dc5ac62ca377827c24321d68050efc2eaee2ebaf3f21d055bbce2206d309/watchfiles-1.0.4-cp39-cp39-win32.whl", hash = "sha256:02a526ee5b5a09e8168314c905fc545c9bc46509896ed282aeb5a8ba9bd6ca27", size = 270810 }, - { url = "https://files.pythonhosted.org/packages/82/2b/dad851342492d538e7ffe72a8c756f747dd147988abb039ac9d6577d2235/watchfiles-1.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:a5ae5706058b27c74bac987d615105da17724172d5aaacc6c362a40599b6de43", size = 284866 }, - { url = "https://files.pythonhosted.org/packages/6f/06/175d5ac6b838fb319008c0cd981d7bf289317c510154d411d3584ca2b67b/watchfiles-1.0.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdcc92daeae268de1acf5b7befcd6cfffd9a047098199056c72e4623f531de18", size = 396269 }, - { url = "https://files.pythonhosted.org/packages/86/ee/5db93b0b57dc0587abdbac4149296ee73275f615d790a82cb5598af0557f/watchfiles-1.0.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d8d3d9203705b5797f0af7e7e5baa17c8588030aaadb7f6a86107b7247303817", size = 386010 }, - { url = "https://files.pythonhosted.org/packages/75/61/fe0dc5fedf152bfc085a53711f740701f6bdb8ab6b5c950402b681d4858b/watchfiles-1.0.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdef5a1be32d0b07dcea3318a0be95d42c98ece24177820226b56276e06b63b0", size = 450913 }, - { url = "https://files.pythonhosted.org/packages/9f/dd/3c7731af3baf1a9957afc643d176f94480921a690ec3237c9f9d11301c08/watchfiles-1.0.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:342622287b5604ddf0ed2d085f3a589099c9ae8b7331df3ae9845571586c4f3d", size = 453474 }, - { url = "https://files.pythonhosted.org/packages/6b/b4/c3998f54c91a35cee60ee6d3a855a069c5dff2bae6865147a46e9090dccd/watchfiles-1.0.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9fe37a2de80aa785d340f2980276b17ef697ab8db6019b07ee4fd28a8359d2f3", size = 395565 }, - { url = "https://files.pythonhosted.org/packages/3f/05/ac1a4d235beb9ddfb8ac26ce93a00ba6bd1b1b43051ef12d7da957b4a9d1/watchfiles-1.0.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:9d1ef56b56ed7e8f312c934436dea93bfa3e7368adfcf3df4c0da6d4de959a1e", size = 385406 }, - { url = "https://files.pythonhosted.org/packages/4c/ea/36532e7d86525f4e52a10efed182abf33efb106a93d49f5fbc994b256bcd/watchfiles-1.0.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b42cac65beae3a362629950c444077d1b44f1790ea2772beaea95451c086bb", size = 450424 }, - { url = "https://files.pythonhosted.org/packages/7a/e9/3cbcf4d70cd0b6d3f30631deae1bf37cc0be39887ca327a44462fe546bf5/watchfiles-1.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e0227b8ed9074c6172cf55d85b5670199c99ab11fd27d2c473aa30aec67ee42", size = 452488 }, -] - [[package]] name = "wcwidth" version = "0.2.13" @@ -3223,82 +2340,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826 }, ] -[[package]] -name = "websockets" -version = "14.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f4/1b/380b883ce05bb5f45a905b61790319a28958a9ab1e4b6b95ff5464b60ca1/websockets-14.1.tar.gz", hash = "sha256:398b10c77d471c0aab20a845e7a60076b6390bfdaac7a6d2edb0d2c59d75e8d8", size = 162840 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/af/91/b1b375dbd856fd5fff3f117de0e520542343ecaf4e8fc60f1ac1e9f5822c/websockets-14.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a0adf84bc2e7c86e8a202537b4fd50e6f7f0e4a6b6bf64d7ccb96c4cd3330b29", size = 161950 }, - { url = "https://files.pythonhosted.org/packages/61/8f/4d52f272d3ebcd35e1325c646e98936099a348374d4a6b83b524bded8116/websockets-14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90b5d9dfbb6d07a84ed3e696012610b6da074d97453bd01e0e30744b472c8179", size = 159601 }, - { url = "https://files.pythonhosted.org/packages/c4/b1/29e87b53eb1937992cdee094a0988aadc94f25cf0b37e90c75eed7123d75/websockets-14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2177ee3901075167f01c5e335a6685e71b162a54a89a56001f1c3e9e3d2ad250", size = 159854 }, - { url = "https://files.pythonhosted.org/packages/3f/e6/752a2f5e8321ae2a613062676c08ff2fccfb37dc837a2ee919178a372e8a/websockets-14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f14a96a0034a27f9d47fd9788913924c89612225878f8078bb9d55f859272b0", size = 168835 }, - { url = "https://files.pythonhosted.org/packages/60/27/ca62de7877596926321b99071639275e94bb2401397130b7cf33dbf2106a/websockets-14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f874ba705deea77bcf64a9da42c1f5fc2466d8f14daf410bc7d4ceae0a9fcb0", size = 167844 }, - { url = "https://files.pythonhosted.org/packages/7e/db/f556a1d06635c680ef376be626c632e3f2bbdb1a0189d1d1bffb061c3b70/websockets-14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9607b9a442392e690a57909c362811184ea429585a71061cd5d3c2b98065c199", size = 168157 }, - { url = "https://files.pythonhosted.org/packages/b3/bc/99e5f511838c365ac6ecae19674eb5e94201aa4235bd1af3e6fa92c12905/websockets-14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bea45f19b7ca000380fbd4e02552be86343080120d074b87f25593ce1700ad58", size = 168561 }, - { url = "https://files.pythonhosted.org/packages/c6/e7/251491585bad61c79e525ac60927d96e4e17b18447cc9c3cfab47b2eb1b8/websockets-14.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:219c8187b3ceeadbf2afcf0f25a4918d02da7b944d703b97d12fb01510869078", size = 167979 }, - { url = "https://files.pythonhosted.org/packages/ac/98/7ac2e4eeada19bdbc7a3a66a58e3ebdf33648b9e1c5b3f08c3224df168cf/websockets-14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ad2ab2547761d79926effe63de21479dfaf29834c50f98c4bf5b5480b5838434", size = 167925 }, - { url = "https://files.pythonhosted.org/packages/ab/3d/09e65c47ee2396b7482968068f6e9b516221e1032b12dcf843b9412a5dfb/websockets-14.1-cp310-cp310-win32.whl", hash = "sha256:1288369a6a84e81b90da5dbed48610cd7e5d60af62df9851ed1d1d23a9069f10", size = 162831 }, - { url = "https://files.pythonhosted.org/packages/8a/67/59828a3d09740e6a485acccfbb66600632f2178b6ed1b61388ee96f17d5a/websockets-14.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0744623852f1497d825a49a99bfbec9bea4f3f946df6eb9d8a2f0c37a2fec2e", size = 163266 }, - { url = "https://files.pythonhosted.org/packages/97/ed/c0d03cb607b7fe1f7ff45e2cd4bb5cd0f9e3299ced79c2c303a6fff44524/websockets-14.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:449d77d636f8d9c17952628cc7e3b8faf6e92a17ec581ec0c0256300717e1512", size = 161949 }, - { url = "https://files.pythonhosted.org/packages/06/91/bf0a44e238660d37a2dda1b4896235d20c29a2d0450f3a46cd688f43b239/websockets-14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a35f704be14768cea9790d921c2c1cc4fc52700410b1c10948511039be824aac", size = 159606 }, - { url = "https://files.pythonhosted.org/packages/ff/b8/7185212adad274c2b42b6a24e1ee6b916b7809ed611cbebc33b227e5c215/websockets-14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b1f3628a0510bd58968c0f60447e7a692933589b791a6b572fcef374053ca280", size = 159854 }, - { url = "https://files.pythonhosted.org/packages/5a/8a/0849968d83474be89c183d8ae8dcb7f7ada1a3c24f4d2a0d7333c231a2c3/websockets-14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c3deac3748ec73ef24fc7be0b68220d14d47d6647d2f85b2771cb35ea847aa1", size = 169402 }, - { url = "https://files.pythonhosted.org/packages/bd/4f/ef886e37245ff6b4a736a09b8468dae05d5d5c99de1357f840d54c6f297d/websockets-14.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7048eb4415d46368ef29d32133134c513f507fff7d953c18c91104738a68c3b3", size = 168406 }, - { url = "https://files.pythonhosted.org/packages/11/43/e2dbd4401a63e409cebddedc1b63b9834de42f51b3c84db885469e9bdcef/websockets-14.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6cf0ad281c979306a6a34242b371e90e891bce504509fb6bb5246bbbf31e7b6", size = 168776 }, - { url = "https://files.pythonhosted.org/packages/6d/d6/7063e3f5c1b612e9f70faae20ebaeb2e684ffa36cb959eb0862ee2809b32/websockets-14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cc1fc87428c1d18b643479caa7b15db7d544652e5bf610513d4a3478dbe823d0", size = 169083 }, - { url = "https://files.pythonhosted.org/packages/49/69/e6f3d953f2fa0f8a723cf18cd011d52733bd7f6e045122b24e0e7f49f9b0/websockets-14.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f95ba34d71e2fa0c5d225bde3b3bdb152e957150100e75c86bc7f3964c450d89", size = 168529 }, - { url = "https://files.pythonhosted.org/packages/70/ff/f31fa14561fc1d7b8663b0ed719996cf1f581abee32c8fb2f295a472f268/websockets-14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9481a6de29105d73cf4515f2bef8eb71e17ac184c19d0b9918a3701c6c9c4f23", size = 168475 }, - { url = "https://files.pythonhosted.org/packages/f1/15/b72be0e4bf32ff373aa5baef46a4c7521b8ea93ad8b49ca8c6e8e764c083/websockets-14.1-cp311-cp311-win32.whl", hash = "sha256:368a05465f49c5949e27afd6fbe0a77ce53082185bbb2ac096a3a8afaf4de52e", size = 162833 }, - { url = "https://files.pythonhosted.org/packages/bc/ef/2d81679acbe7057ffe2308d422f744497b52009ea8bab34b6d74a2657d1d/websockets-14.1-cp311-cp311-win_amd64.whl", hash = "sha256:6d24fc337fc055c9e83414c94e1ee0dee902a486d19d2a7f0929e49d7d604b09", size = 163263 }, - { url = "https://files.pythonhosted.org/packages/55/64/55698544ce29e877c9188f1aee9093712411a8fc9732cca14985e49a8e9c/websockets-14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed907449fe5e021933e46a3e65d651f641975a768d0649fee59f10c2985529ed", size = 161957 }, - { url = "https://files.pythonhosted.org/packages/a2/b1/b088f67c2b365f2c86c7b48edb8848ac27e508caf910a9d9d831b2f343cb/websockets-14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:87e31011b5c14a33b29f17eb48932e63e1dcd3fa31d72209848652310d3d1f0d", size = 159620 }, - { url = "https://files.pythonhosted.org/packages/c1/89/2a09db1bbb40ba967a1b8225b07b7df89fea44f06de9365f17f684d0f7e6/websockets-14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bc6ccf7d54c02ae47a48ddf9414c54d48af9c01076a2e1023e3b486b6e72c707", size = 159852 }, - { url = "https://files.pythonhosted.org/packages/ca/c1/f983138cd56e7d3079f1966e81f77ce6643f230cd309f73aa156bb181749/websockets-14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9777564c0a72a1d457f0848977a1cbe15cfa75fa2f67ce267441e465717dcf1a", size = 169675 }, - { url = "https://files.pythonhosted.org/packages/c1/c8/84191455d8660e2a0bdb33878d4ee5dfa4a2cedbcdc88bbd097303b65bfa/websockets-14.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a655bde548ca98f55b43711b0ceefd2a88a71af6350b0c168aa77562104f3f45", size = 168619 }, - { url = "https://files.pythonhosted.org/packages/8d/a7/62e551fdcd7d44ea74a006dc193aba370505278ad76efd938664531ce9d6/websockets-14.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3dfff83ca578cada2d19e665e9c8368e1598d4e787422a460ec70e531dbdd58", size = 169042 }, - { url = "https://files.pythonhosted.org/packages/ad/ed/1532786f55922c1e9c4d329608e36a15fdab186def3ca9eb10d7465bc1cc/websockets-14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6a6c9bcf7cdc0fd41cc7b7944447982e8acfd9f0d560ea6d6845428ed0562058", size = 169345 }, - { url = "https://files.pythonhosted.org/packages/ea/fb/160f66960d495df3de63d9bcff78e1b42545b2a123cc611950ffe6468016/websockets-14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4b6caec8576e760f2c7dd878ba817653144d5f369200b6ddf9771d64385b84d4", size = 168725 }, - { url = "https://files.pythonhosted.org/packages/cf/53/1bf0c06618b5ac35f1d7906444b9958f8485682ab0ea40dee7b17a32da1e/websockets-14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb6d38971c800ff02e4a6afd791bbe3b923a9a57ca9aeab7314c21c84bf9ff05", size = 168712 }, - { url = "https://files.pythonhosted.org/packages/e5/22/5ec2f39fff75f44aa626f86fa7f20594524a447d9c3be94d8482cd5572ef/websockets-14.1-cp312-cp312-win32.whl", hash = "sha256:1d045cbe1358d76b24d5e20e7b1878efe578d9897a25c24e6006eef788c0fdf0", size = 162838 }, - { url = "https://files.pythonhosted.org/packages/74/27/28f07df09f2983178db7bf6c9cccc847205d2b92ced986cd79565d68af4f/websockets-14.1-cp312-cp312-win_amd64.whl", hash = "sha256:90f4c7a069c733d95c308380aae314f2cb45bd8a904fb03eb36d1a4983a4993f", size = 163277 }, - { url = "https://files.pythonhosted.org/packages/34/77/812b3ba5110ed8726eddf9257ab55ce9e85d97d4aa016805fdbecc5e5d48/websockets-14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3630b670d5057cd9e08b9c4dab6493670e8e762a24c2c94ef312783870736ab9", size = 161966 }, - { url = "https://files.pythonhosted.org/packages/8d/24/4fcb7aa6986ae7d9f6d083d9d53d580af1483c5ec24bdec0978307a0f6ac/websockets-14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36ebd71db3b89e1f7b1a5deaa341a654852c3518ea7a8ddfdf69cc66acc2db1b", size = 159625 }, - { url = "https://files.pythonhosted.org/packages/f8/47/2a0a3a2fc4965ff5b9ce9324d63220156bd8bedf7f90824ab92a822e65fd/websockets-14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5b918d288958dc3fa1c5a0b9aa3256cb2b2b84c54407f4813c45d52267600cd3", size = 159857 }, - { url = "https://files.pythonhosted.org/packages/dd/c8/d7b425011a15e35e17757e4df75b25e1d0df64c0c315a44550454eaf88fc/websockets-14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00fe5da3f037041da1ee0cf8e308374e236883f9842c7c465aa65098b1c9af59", size = 169635 }, - { url = "https://files.pythonhosted.org/packages/93/39/6e3b5cffa11036c40bd2f13aba2e8e691ab2e01595532c46437b56575678/websockets-14.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8149a0f5a72ca36720981418eeffeb5c2729ea55fa179091c81a0910a114a5d2", size = 168578 }, - { url = "https://files.pythonhosted.org/packages/cf/03/8faa5c9576299b2adf34dcccf278fc6bbbcda8a3efcc4d817369026be421/websockets-14.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77569d19a13015e840b81550922056acabc25e3f52782625bc6843cfa034e1da", size = 169018 }, - { url = "https://files.pythonhosted.org/packages/8c/05/ea1fec05cc3a60defcdf0bb9f760c3c6bd2dd2710eff7ac7f891864a22ba/websockets-14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf5201a04550136ef870aa60ad3d29d2a59e452a7f96b94193bee6d73b8ad9a9", size = 169383 }, - { url = "https://files.pythonhosted.org/packages/21/1d/eac1d9ed787f80754e51228e78855f879ede1172c8b6185aca8cef494911/websockets-14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:88cf9163ef674b5be5736a584c999e98daf3aabac6e536e43286eb74c126b9c7", size = 168773 }, - { url = "https://files.pythonhosted.org/packages/0e/1b/e808685530185915299740d82b3a4af3f2b44e56ccf4389397c7a5d95d39/websockets-14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:836bef7ae338a072e9d1863502026f01b14027250a4545672673057997d5c05a", size = 168757 }, - { url = "https://files.pythonhosted.org/packages/b6/19/6ab716d02a3b068fbbeb6face8a7423156e12c446975312f1c7c0f4badab/websockets-14.1-cp313-cp313-win32.whl", hash = "sha256:0d4290d559d68288da9f444089fd82490c8d2744309113fc26e2da6e48b65da6", size = 162834 }, - { url = "https://files.pythonhosted.org/packages/6c/fd/ab6b7676ba712f2fc89d1347a4b5bdc6aa130de10404071f2b2606450209/websockets-14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8621a07991add373c3c5c2cf89e1d277e49dc82ed72c75e3afc74bd0acc446f0", size = 163277 }, - { url = "https://files.pythonhosted.org/packages/4d/23/ac9d8c5ec7b90efc3687d60474ef7e698f8b75cb7c9dfedad72701e797c9/websockets-14.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:01bb2d4f0a6d04538d3c5dfd27c0643269656c28045a53439cbf1c004f90897a", size = 161945 }, - { url = "https://files.pythonhosted.org/packages/c5/6b/ffa450e3b736a86ae6b40ce20a758ac9af80c96a18548f6c323ed60329c5/websockets-14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:414ffe86f4d6f434a8c3b7913655a1a5383b617f9bf38720e7c0799fac3ab1c6", size = 159600 }, - { url = "https://files.pythonhosted.org/packages/74/62/f90d1fd57ea7337ecaa99f17c31a544b9dcdb7c7c32a3d3997ccc42d57d3/websockets-14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8fda642151d5affdee8a430bd85496f2e2517be3a2b9d2484d633d5712b15c56", size = 159850 }, - { url = "https://files.pythonhosted.org/packages/35/dd/1e71865de1f3c265e11d02b0b4c76178f84351c6611e515fbe3d2bd1b98c/websockets-14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd7c11968bc3860d5c78577f0dbc535257ccec41750675d58d8dc66aa47fe52c", size = 168616 }, - { url = "https://files.pythonhosted.org/packages/ba/ae/0d069b52e26d48402dbe90c7581eb6a5bed5d7dbe3d9ca3cf1033859d58e/websockets-14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a032855dc7db987dff813583d04f4950d14326665d7e714d584560b140ae6b8b", size = 167619 }, - { url = "https://files.pythonhosted.org/packages/1c/3f/d3f2df62704c53e0296f0ce714921b6a15df10e2e463734c737b1d9e2522/websockets-14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7e7ea2f782408c32d86b87a0d2c1fd8871b0399dd762364c731d86c86069a78", size = 167921 }, - { url = "https://files.pythonhosted.org/packages/e0/e2/2dcb295bdae9393070cea58c790d87d1d36149bb4319b1da6014c8a36d42/websockets-14.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:39450e6215f7d9f6f7bc2a6da21d79374729f5d052333da4d5825af8a97e6735", size = 168343 }, - { url = "https://files.pythonhosted.org/packages/6b/fd/fa48e8b4e10e2c165cbfc16dada7405b4008818be490fc6b99a4928e232a/websockets-14.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ceada5be22fa5a5a4cdeec74e761c2ee7db287208f54c718f2df4b7e200b8d4a", size = 167745 }, - { url = "https://files.pythonhosted.org/packages/42/45/79db33f2b744d2014b40946428e6c37ce944fde8791d82e1c2f4d4a67d96/websockets-14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3fc753451d471cff90b8f467a1fc0ae64031cf2d81b7b34e1811b7e2691bc4bc", size = 167705 }, - { url = "https://files.pythonhosted.org/packages/da/27/f66507db34ca9c79562f28fa5983433f7b9080fd471cc188906006d36ba4/websockets-14.1-cp39-cp39-win32.whl", hash = "sha256:14839f54786987ccd9d03ed7f334baec0f02272e7ec4f6e9d427ff584aeea8b4", size = 162828 }, - { url = "https://files.pythonhosted.org/packages/11/25/bb8f81a4ec94f595adb845608c5ec9549cb6b446945b292fe61807c7c95b/websockets-14.1-cp39-cp39-win_amd64.whl", hash = "sha256:d9fd19ecc3a4d5ae82ddbfb30962cf6d874ff943e56e0c81f5169be2fda62979", size = 163271 }, - { url = "https://files.pythonhosted.org/packages/fb/cd/382a05a1ba2a93bd9fb807716a660751295df72e77204fb130a102fcdd36/websockets-14.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e5dc25a9dbd1a7f61eca4b7cb04e74ae4b963d658f9e4f9aad9cd00b688692c8", size = 159633 }, - { url = "https://files.pythonhosted.org/packages/b7/a0/fa7c62e2952ef028b422fbf420f9353d9dd4dfaa425de3deae36e98c0784/websockets-14.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:04a97aca96ca2acedf0d1f332c861c5a4486fdcba7bcef35873820f940c4231e", size = 159867 }, - { url = "https://files.pythonhosted.org/packages/c1/94/954b4924f868db31d5f0935893c7a8446515ee4b36bb8ad75a929469e453/websockets-14.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df174ece723b228d3e8734a6f2a6febbd413ddec39b3dc592f5a4aa0aff28098", size = 161121 }, - { url = "https://files.pythonhosted.org/packages/7a/2e/f12bbb41a8f2abb76428ba4fdcd9e67b5b364a3e7fa97c88f4d6950aa2d4/websockets-14.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:034feb9f4286476f273b9a245fb15f02c34d9586a5bc936aff108c3ba1b21beb", size = 160731 }, - { url = "https://files.pythonhosted.org/packages/13/97/b76979401f2373af1fe3e08f960b265cecab112e7dac803446fb98351a52/websockets-14.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:660c308dabd2b380807ab64b62985eaccf923a78ebc572bd485375b9ca2b7dc7", size = 160681 }, - { url = "https://files.pythonhosted.org/packages/39/9c/16916d9a436c109a1d7ba78817e8fee357b78968be3f6e6f517f43afa43d/websockets-14.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5a42d3ecbb2db5080fc578314439b1d79eef71d323dc661aa616fb492436af5d", size = 163316 }, - { url = "https://files.pythonhosted.org/packages/0f/57/50fd09848a80a1b63a572c610f230f8a17590ca47daf256eb28a0851df73/websockets-14.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddaa4a390af911da6f680be8be4ff5aaf31c4c834c1a9147bc21cbcbca2d4370", size = 159633 }, - { url = "https://files.pythonhosted.org/packages/d7/2f/db728b0c7962ad6a13ced8286325bf430b59722d943e7f6bdbd8a78e2bfe/websockets-14.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a4c805c6034206143fbabd2d259ec5e757f8b29d0a2f0bf3d2fe5d1f60147a4a", size = 159863 }, - { url = "https://files.pythonhosted.org/packages/fa/e4/21e7481936fbfffee138edb488a6184eb3468b402a8181b95b9e44f6a676/websockets-14.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:205f672a6c2c671a86d33f6d47c9b35781a998728d2c7c2a3e1cf3333fcb62b7", size = 161119 }, - { url = "https://files.pythonhosted.org/packages/64/2d/efb6cf716d4f9da60190756e06f8db2066faf1ae4a4a8657ab136dfcc7a8/websockets-14.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef440054124728cc49b01c33469de06755e5a7a4e83ef61934ad95fc327fbb0", size = 160724 }, - { url = "https://files.pythonhosted.org/packages/40/b0/a70b972d853c3f26040834fcff3dd45c8a0292af9f5f0b36f9fbb82d5d44/websockets-14.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7591d6f440af7f73c4bd9404f3772bfee064e639d2b6cc8c94076e71b2471c1", size = 160676 }, - { url = "https://files.pythonhosted.org/packages/4a/76/f9da7f97476cc7b8c74829bb4851f1faf660455839689ffcc354b52860a7/websockets-14.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:25225cc79cfebc95ba1d24cd3ab86aaa35bcd315d12fa4358939bd55e9bd74a5", size = 163311 }, - { url = "https://files.pythonhosted.org/packages/b0/0b/c7e5d11020242984d9d37990310520ed663b942333b83a033c2f20191113/websockets-14.1-py3-none-any.whl", hash = "sha256:4d4fc827a20abe6d544a119896f6b78ee13fe81cbfef416f3f2ddf09a03f0e2e", size = 156277 }, -] - [[package]] name = "wheel" version = "0.45.1" @@ -3324,13 +2365,10 @@ wheels = [ name = "yarl" version = "1.15.2" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] dependencies = [ - { name = "idna", marker = "python_full_version < '3.9'" }, - { name = "multidict", marker = "python_full_version < '3.9'" }, - { name = "propcache", version = "0.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/e1/d5427a061819c9f885f58bb0467d02a523f1aec19f9e5f9c82ce950d90d3/yarl-1.15.2.tar.gz", hash = "sha256:a39c36f4218a5bb668b4f06874d676d35a035ee668e6e7e3538835c703634b84", size = 169318 } wheels = [ @@ -3398,160 +2436,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fb/19/5cd4757079dc9d9f3de3e3831719b695f709a8ce029e70b33350c9d082a7/yarl-1.15.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c998d0558805860503bc3a595994895ca0f7835e00668dadc673bbf7f5fbfcbe", size = 345875 }, { url = "https://files.pythonhosted.org/packages/83/a0/ef09b54634f73417f1ea4a746456a4372c1b044f07b26e16fa241bd2d94e/yarl-1.15.2-cp313-cp313-win32.whl", hash = "sha256:533a28754e7f7439f217550a497bb026c54072dbe16402b183fdbca2431935a9", size = 302609 }, { url = "https://files.pythonhosted.org/packages/20/9f/f39c37c17929d3975da84c737b96b606b68c495cc4ee86408f10523a1635/yarl-1.15.2-cp313-cp313-win_amd64.whl", hash = "sha256:5838f2b79dc8f96fdc44077c9e4e2e33d7089b10788464609df788eb97d03aad", size = 308252 }, - { url = "https://files.pythonhosted.org/packages/7b/1f/544439ce6b7a498327d57ff40f0cd4f24bf4b1c1daf76c8c962dca022e71/yarl-1.15.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fbbb63bed5fcd70cd3dd23a087cd78e4675fb5a2963b8af53f945cbbca79ae16", size = 138555 }, - { url = "https://files.pythonhosted.org/packages/e8/b7/d6f33e7a42832f1e8476d0aabe089be0586a9110b5dfc2cef93444dc7c21/yarl-1.15.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e2e93b88ecc8f74074012e18d679fb2e9c746f2a56f79cd5e2b1afcf2a8a786b", size = 89844 }, - { url = "https://files.pythonhosted.org/packages/93/34/ede8d8ed7350b4b21e33fc4eff71e08de31da697034969b41190132d421f/yarl-1.15.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:af8ff8d7dc07ce873f643de6dfbcd45dc3db2c87462e5c387267197f59e6d776", size = 87671 }, - { url = "https://files.pythonhosted.org/packages/fa/51/6d71e92bc54b5788b18f3dc29806f9ce37e12b7c610e8073357717f34b78/yarl-1.15.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66f629632220a4e7858b58e4857927dd01a850a4cef2fb4044c8662787165cf7", size = 314558 }, - { url = "https://files.pythonhosted.org/packages/76/0a/f9ffe503b4ef77cd77c9eefd37717c092e26f2c2dbbdd45700f864831292/yarl-1.15.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:833547179c31f9bec39b49601d282d6f0ea1633620701288934c5f66d88c3e50", size = 327622 }, - { url = "https://files.pythonhosted.org/packages/8b/38/8eb602eeb153de0189d572dce4ed81b9b14f71de7c027d330b601b4fdcdc/yarl-1.15.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2aa738e0282be54eede1e3f36b81f1e46aee7ec7602aa563e81e0e8d7b67963f", size = 324447 }, - { url = "https://files.pythonhosted.org/packages/c2/1e/1c78c695a4c7b957b5665e46a89ea35df48511dbed301a05c0a8beed0cc3/yarl-1.15.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a13a07532e8e1c4a5a3afff0ca4553da23409fad65def1b71186fb867eeae8d", size = 319009 }, - { url = "https://files.pythonhosted.org/packages/06/a0/7ea93de4ca1991e7f92a8901dcd1585165f547d342f7c6f36f1ea58b75de/yarl-1.15.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c45817e3e6972109d1a2c65091504a537e257bc3c885b4e78a95baa96df6a3f8", size = 307760 }, - { url = "https://files.pythonhosted.org/packages/f4/b4/ceaa1f35cfb37fe06af3f7404438abf9a1262dc5df74dba37c90b0615e06/yarl-1.15.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:670eb11325ed3a6209339974b276811867defe52f4188fe18dc49855774fa9cf", size = 315038 }, - { url = "https://files.pythonhosted.org/packages/da/45/a2ca2b547c56550eefc39e45d61e4b42ae6dbb3e913810b5a0eb53e86412/yarl-1.15.2-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:d417a4f6943112fae3924bae2af7112562285848d9bcee737fc4ff7cbd450e6c", size = 312898 }, - { url = "https://files.pythonhosted.org/packages/ea/e0/f692ba36dedc5b0b22084bba558a7ede053841e247b7dd2adbb9d40450be/yarl-1.15.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:bc8936d06cd53fddd4892677d65e98af514c8d78c79864f418bbf78a4a2edde4", size = 319370 }, - { url = "https://files.pythonhosted.org/packages/b1/3f/0e382caf39958be6ae61d4bb0c82a68a3c45a494fc8cdc6f55c29757970e/yarl-1.15.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:954dde77c404084c2544e572f342aef384240b3e434e06cecc71597e95fd1ce7", size = 332429 }, - { url = "https://files.pythonhosted.org/packages/21/6b/c824a4a1c45d67b15b431d4ab83b63462bfcbc710065902e10fa5c2ffd9e/yarl-1.15.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:5bc0df728e4def5e15a754521e8882ba5a5121bd6b5a3a0ff7efda5d6558ab3d", size = 333143 }, - { url = "https://files.pythonhosted.org/packages/20/76/8af2a1d93fe95b04e284b5d55daaad33aae6e2f6254a1bcdb40e2752af6c/yarl-1.15.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b71862a652f50babab4a43a487f157d26b464b1dedbcc0afda02fd64f3809d04", size = 326687 }, - { url = "https://files.pythonhosted.org/packages/1c/53/490830773f907ef8a311cc5d82e5830f75f7692c1adacbdb731d3f1246fd/yarl-1.15.2-cp38-cp38-win32.whl", hash = "sha256:63eab904f8630aed5a68f2d0aeab565dcfc595dc1bf0b91b71d9ddd43dea3aea", size = 78705 }, - { url = "https://files.pythonhosted.org/packages/9c/9d/d944e897abf37f50f4fa2d8d6f5fd0ed9413bc8327d3b4cc25ba9694e1ba/yarl-1.15.2-cp38-cp38-win_amd64.whl", hash = "sha256:2cf441c4b6e538ba0d2591574f95d3fdd33f1efafa864faa077d9636ecc0c4e9", size = 84998 }, - { url = "https://files.pythonhosted.org/packages/91/1c/1c9d08c29b10499348eedc038cf61b6d96d5ba0e0d69438975845939ed3c/yarl-1.15.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a32d58f4b521bb98b2c0aa9da407f8bd57ca81f34362bcb090e4a79e9924fefc", size = 138011 }, - { url = "https://files.pythonhosted.org/packages/d4/33/2d4a1418bae6d7883c1fcc493be7b6d6fe015919835adc9e8eeba472e9f7/yarl-1.15.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:766dcc00b943c089349d4060b935c76281f6be225e39994c2ccec3a2a36ad627", size = 89618 }, - { url = "https://files.pythonhosted.org/packages/78/2e/0024c674a376cfdc722a167a8f308f5779aca615cb7a28d67fbeabf3f697/yarl-1.15.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bed1b5dbf90bad3bfc19439258c97873eab453c71d8b6869c136346acfe497e7", size = 87347 }, - { url = "https://files.pythonhosted.org/packages/c5/08/a01874dabd4ddf475c5c2adc86f7ac329f83a361ee513a97841720ab7b24/yarl-1.15.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed20a4bdc635f36cb19e630bfc644181dd075839b6fc84cac51c0f381ac472e2", size = 310438 }, - { url = "https://files.pythonhosted.org/packages/09/95/691bc6de2c1b0e9c8bbaa5f8f38118d16896ba1a069a09d1fb073d41a093/yarl-1.15.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d538df442c0d9665664ab6dd5fccd0110fa3b364914f9c85b3ef9b7b2e157980", size = 325384 }, - { url = "https://files.pythonhosted.org/packages/95/fd/fee11eb3337f48c62d39c5676e6a0e4e318e318900a901b609a3c45394df/yarl-1.15.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c6cf1d92edf936ceedc7afa61b07e9d78a27b15244aa46bbcd534c7458ee1b", size = 321820 }, - { url = "https://files.pythonhosted.org/packages/7a/ad/4a2c9bbebaefdce4a69899132f4bf086abbddb738dc6e794a31193bc0854/yarl-1.15.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce44217ad99ffad8027d2fde0269ae368c86db66ea0571c62a000798d69401fb", size = 314150 }, - { url = "https://files.pythonhosted.org/packages/38/7d/552c37bc6c4ae8ea900e44b6c05cb16d50dca72d3782ccd66f53e27e353f/yarl-1.15.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47a6000a7e833ebfe5886b56a31cb2ff12120b1efd4578a6fcc38df16cc77bd", size = 304202 }, - { url = "https://files.pythonhosted.org/packages/2e/f8/c22a158f3337f49775775ecef43fc097a98b20cdce37425b68b9c45a6f94/yarl-1.15.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e52f77a0cd246086afde8815039f3e16f8d2be51786c0a39b57104c563c5cbb0", size = 310311 }, - { url = "https://files.pythonhosted.org/packages/ce/e4/ebce06afa25c2a6c8e6c9a5915cbbc7940a37f3ec38e950e8f346ca908da/yarl-1.15.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:f9ca0e6ce7774dc7830dc0cc4bb6b3eec769db667f230e7c770a628c1aa5681b", size = 310645 }, - { url = "https://files.pythonhosted.org/packages/0a/34/5504cc8fbd1be959ec0a1e9e9f471fd438c37cb877b0178ce09085b36b51/yarl-1.15.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:136f9db0f53c0206db38b8cd0c985c78ded5fd596c9a86ce5c0b92afb91c3a19", size = 313328 }, - { url = "https://files.pythonhosted.org/packages/cf/e4/fb3f91a539c6505e347d7d75bc675d291228960ffd6481ced76a15412924/yarl-1.15.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:173866d9f7409c0fb514cf6e78952e65816600cb888c68b37b41147349fe0057", size = 330135 }, - { url = "https://files.pythonhosted.org/packages/e1/08/a0b27db813f0159e1c8a45f48852afded501de2f527e7613c4dcf436ecf7/yarl-1.15.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:6e840553c9c494a35e449a987ca2c4f8372668ee954a03a9a9685075228e5036", size = 327155 }, - { url = "https://files.pythonhosted.org/packages/97/4e/b3414dded12d0e2b52eb1964c21a8d8b68495b320004807de770f7b6b53a/yarl-1.15.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:458c0c65802d816a6b955cf3603186de79e8fdb46d4f19abaec4ef0a906f50a7", size = 320810 }, - { url = "https://files.pythonhosted.org/packages/bb/ca/e5149c55d1c9dcf3d5b48acd7c71ca8622fd2f61322d0386fe63ba106774/yarl-1.15.2-cp39-cp39-win32.whl", hash = "sha256:5b48388ded01f6f2429a8c55012bdbd1c2a0c3735b3e73e221649e524c34a58d", size = 78686 }, - { url = "https://files.pythonhosted.org/packages/b1/87/f56a80a1abaf65dbf138b821357b51b6cc061756bb7d93f08797950b3881/yarl-1.15.2-cp39-cp39-win_amd64.whl", hash = "sha256:81dadafb3aa124f86dc267a2168f71bbd2bfb163663661ab0038f6e4b8edb810", size = 84818 }, { url = "https://files.pythonhosted.org/packages/46/cf/a28c494decc9c8776b0d7b729c68d26fdafefcedd8d2eab5d9cd767376b2/yarl-1.15.2-py3-none-any.whl", hash = "sha256:0d3105efab7c5c091609abacad33afff33bdff0035bece164c98bcf5a85ef90a", size = 38891 }, ] -[[package]] -name = "yarl" -version = "1.18.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version == '3.9.*'", -] -dependencies = [ - { name = "idna", marker = "python_full_version >= '3.9'" }, - { name = "multidict", marker = "python_full_version >= '3.9'" }, - { name = "propcache", version = "0.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b7/9d/4b94a8e6d2b51b599516a5cb88e5bc99b4d8d4583e468057eaa29d5f0918/yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1", size = 181062 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/98/e005bc608765a8a5569f58e650961314873c8469c333616eb40bff19ae97/yarl-1.18.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34", size = 141458 }, - { url = "https://files.pythonhosted.org/packages/df/5d/f8106b263b8ae8a866b46d9be869ac01f9b3fb7f2325f3ecb3df8003f796/yarl-1.18.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7", size = 94365 }, - { url = "https://files.pythonhosted.org/packages/56/3e/d8637ddb9ba69bf851f765a3ee288676f7cf64fb3be13760c18cbc9d10bd/yarl-1.18.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:602d98f2c2d929f8e697ed274fbadc09902c4025c5a9963bf4e9edfc3ab6f7ed", size = 92181 }, - { url = "https://files.pythonhosted.org/packages/76/f9/d616a5c2daae281171de10fba41e1c0e2d8207166fc3547252f7d469b4e1/yarl-1.18.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c654d5207c78e0bd6d749f6dae1dcbbfde3403ad3a4b11f3c5544d9906969dde", size = 315349 }, - { url = "https://files.pythonhosted.org/packages/bb/b4/3ea5e7b6f08f698b3769a06054783e434f6d59857181b5c4e145de83f59b/yarl-1.18.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5094d9206c64181d0f6e76ebd8fb2f8fe274950a63890ee9e0ebfd58bf9d787b", size = 330494 }, - { url = "https://files.pythonhosted.org/packages/55/f1/e0fc810554877b1b67420568afff51b967baed5b53bcc983ab164eebf9c9/yarl-1.18.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35098b24e0327fc4ebdc8ffe336cee0a87a700c24ffed13161af80124b7dc8e5", size = 326927 }, - { url = "https://files.pythonhosted.org/packages/a9/42/b1753949b327b36f210899f2dd0a0947c0c74e42a32de3f8eb5c7d93edca/yarl-1.18.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3236da9272872443f81fedc389bace88408f64f89f75d1bdb2256069a8730ccc", size = 319703 }, - { url = "https://files.pythonhosted.org/packages/f0/6d/e87c62dc9635daefb064b56f5c97df55a2e9cc947a2b3afd4fd2f3b841c7/yarl-1.18.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2c08cc9b16f4f4bc522771d96734c7901e7ebef70c6c5c35dd0f10845270bcd", size = 310246 }, - { url = "https://files.pythonhosted.org/packages/e3/ef/e2e8d1785cdcbd986f7622d7f0098205f3644546da7919c24b95790ec65a/yarl-1.18.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80316a8bd5109320d38eef8833ccf5f89608c9107d02d2a7f985f98ed6876990", size = 319730 }, - { url = "https://files.pythonhosted.org/packages/fc/15/8723e22345bc160dfde68c4b3ae8b236e868f9963c74015f1bc8a614101c/yarl-1.18.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1e1cc06da1491e6734f0ea1e6294ce00792193c463350626571c287c9a704db", size = 321681 }, - { url = "https://files.pythonhosted.org/packages/86/09/bf764e974f1516efa0ae2801494a5951e959f1610dd41edbfc07e5e0f978/yarl-1.18.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fea09ca13323376a2fdfb353a5fa2e59f90cd18d7ca4eaa1fd31f0a8b4f91e62", size = 324812 }, - { url = "https://files.pythonhosted.org/packages/f6/4c/20a0187e3b903c97d857cf0272d687c1b08b03438968ae8ffc50fe78b0d6/yarl-1.18.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e3b9fd71836999aad54084906f8663dffcd2a7fb5cdafd6c37713b2e72be1760", size = 337011 }, - { url = "https://files.pythonhosted.org/packages/c9/71/6244599a6e1cc4c9f73254a627234e0dad3883ece40cc33dce6265977461/yarl-1.18.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:757e81cae69244257d125ff31663249b3013b5dc0a8520d73694aed497fb195b", size = 338132 }, - { url = "https://files.pythonhosted.org/packages/af/f5/e0c3efaf74566c4b4a41cb76d27097df424052a064216beccae8d303c90f/yarl-1.18.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b1771de9944d875f1b98a745bc547e684b863abf8f8287da8466cf470ef52690", size = 331849 }, - { url = "https://files.pythonhosted.org/packages/8a/b8/3d16209c2014c2f98a8f658850a57b716efb97930aebf1ca0d9325933731/yarl-1.18.3-cp310-cp310-win32.whl", hash = "sha256:8874027a53e3aea659a6d62751800cf6e63314c160fd607489ba5c2edd753cf6", size = 84309 }, - { url = "https://files.pythonhosted.org/packages/fd/b7/2e9a5b18eb0fe24c3a0e8bae994e812ed9852ab4fd067c0107fadde0d5f0/yarl-1.18.3-cp310-cp310-win_amd64.whl", hash = "sha256:93b2e109287f93db79210f86deb6b9bbb81ac32fc97236b16f7433db7fc437d8", size = 90484 }, - { url = "https://files.pythonhosted.org/packages/40/93/282b5f4898d8e8efaf0790ba6d10e2245d2c9f30e199d1a85cae9356098c/yarl-1.18.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8503ad47387b8ebd39cbbbdf0bf113e17330ffd339ba1144074da24c545f0069", size = 141555 }, - { url = "https://files.pythonhosted.org/packages/6d/9c/0a49af78df099c283ca3444560f10718fadb8a18dc8b3edf8c7bd9fd7d89/yarl-1.18.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02ddb6756f8f4517a2d5e99d8b2f272488e18dd0bfbc802f31c16c6c20f22193", size = 94351 }, - { url = "https://files.pythonhosted.org/packages/5a/a1/205ab51e148fdcedad189ca8dd587794c6f119882437d04c33c01a75dece/yarl-1.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:67a283dd2882ac98cc6318384f565bffc751ab564605959df4752d42483ad889", size = 92286 }, - { url = "https://files.pythonhosted.org/packages/ed/fe/88b690b30f3f59275fb674f5f93ddd4a3ae796c2b62e5bb9ece8a4914b83/yarl-1.18.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d980e0325b6eddc81331d3f4551e2a333999fb176fd153e075c6d1c2530aa8a8", size = 340649 }, - { url = "https://files.pythonhosted.org/packages/07/eb/3b65499b568e01f36e847cebdc8d7ccb51fff716dbda1ae83c3cbb8ca1c9/yarl-1.18.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b643562c12680b01e17239be267bc306bbc6aac1f34f6444d1bded0c5ce438ca", size = 356623 }, - { url = "https://files.pythonhosted.org/packages/33/46/f559dc184280b745fc76ec6b1954de2c55595f0ec0a7614238b9ebf69618/yarl-1.18.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c017a3b6df3a1bd45b9fa49a0f54005e53fbcad16633870104b66fa1a30a29d8", size = 354007 }, - { url = "https://files.pythonhosted.org/packages/af/ba/1865d85212351ad160f19fb99808acf23aab9a0f8ff31c8c9f1b4d671fc9/yarl-1.18.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae", size = 344145 }, - { url = "https://files.pythonhosted.org/packages/94/cb/5c3e975d77755d7b3d5193e92056b19d83752ea2da7ab394e22260a7b824/yarl-1.18.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccaa3a4b521b780a7e771cc336a2dba389a0861592bbce09a476190bb0c8b4b3", size = 336133 }, - { url = "https://files.pythonhosted.org/packages/19/89/b77d3fd249ab52a5c40859815765d35c91425b6bb82e7427ab2f78f5ff55/yarl-1.18.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d06d3005e668744e11ed80812e61efd77d70bb7f03e33c1598c301eea20efbb", size = 347967 }, - { url = "https://files.pythonhosted.org/packages/35/bd/f6b7630ba2cc06c319c3235634c582a6ab014d52311e7d7c22f9518189b5/yarl-1.18.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9d41beda9dc97ca9ab0b9888cb71f7539124bc05df02c0cff6e5acc5a19dcc6e", size = 346397 }, - { url = "https://files.pythonhosted.org/packages/18/1a/0b4e367d5a72d1f095318344848e93ea70da728118221f84f1bf6c1e39e7/yarl-1.18.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ba23302c0c61a9999784e73809427c9dbedd79f66a13d84ad1b1943802eaaf59", size = 350206 }, - { url = "https://files.pythonhosted.org/packages/b5/cf/320fff4367341fb77809a2d8d7fe75b5d323a8e1b35710aafe41fdbf327b/yarl-1.18.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6748dbf9bfa5ba1afcc7556b71cda0d7ce5f24768043a02a58846e4a443d808d", size = 362089 }, - { url = "https://files.pythonhosted.org/packages/57/cf/aadba261d8b920253204085268bad5e8cdd86b50162fcb1b10c10834885a/yarl-1.18.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0b0cad37311123211dc91eadcb322ef4d4a66008d3e1bdc404808992260e1a0e", size = 366267 }, - { url = "https://files.pythonhosted.org/packages/54/58/fb4cadd81acdee6dafe14abeb258f876e4dd410518099ae9a35c88d8097c/yarl-1.18.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fb2171a4486bb075316ee754c6d8382ea6eb8b399d4ec62fde2b591f879778a", size = 359141 }, - { url = "https://files.pythonhosted.org/packages/9a/7a/4c571597589da4cd5c14ed2a0b17ac56ec9ee7ee615013f74653169e702d/yarl-1.18.3-cp311-cp311-win32.whl", hash = "sha256:61b1a825a13bef4a5f10b1885245377d3cd0bf87cba068e1d9a88c2ae36880e1", size = 84402 }, - { url = "https://files.pythonhosted.org/packages/ae/7b/8600250b3d89b625f1121d897062f629883c2f45339623b69b1747ec65fa/yarl-1.18.3-cp311-cp311-win_amd64.whl", hash = "sha256:b9d60031cf568c627d028239693fd718025719c02c9f55df0a53e587aab951b5", size = 91030 }, - { url = "https://files.pythonhosted.org/packages/33/85/bd2e2729752ff4c77338e0102914897512e92496375e079ce0150a6dc306/yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50", size = 142644 }, - { url = "https://files.pythonhosted.org/packages/ff/74/1178322cc0f10288d7eefa6e4a85d8d2e28187ccab13d5b844e8b5d7c88d/yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576", size = 94962 }, - { url = "https://files.pythonhosted.org/packages/be/75/79c6acc0261e2c2ae8a1c41cf12265e91628c8c58ae91f5ff59e29c0787f/yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640", size = 92795 }, - { url = "https://files.pythonhosted.org/packages/6b/32/927b2d67a412c31199e83fefdce6e645247b4fb164aa1ecb35a0f9eb2058/yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2", size = 332368 }, - { url = "https://files.pythonhosted.org/packages/19/e5/859fca07169d6eceeaa4fde1997c91d8abde4e9a7c018e371640c2da2b71/yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75", size = 342314 }, - { url = "https://files.pythonhosted.org/packages/08/75/76b63ccd91c9e03ab213ef27ae6add2e3400e77e5cdddf8ed2dbc36e3f21/yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512", size = 341987 }, - { url = "https://files.pythonhosted.org/packages/1a/e1/a097d5755d3ea8479a42856f51d97eeff7a3a7160593332d98f2709b3580/yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba", size = 336914 }, - { url = "https://files.pythonhosted.org/packages/0b/42/e1b4d0e396b7987feceebe565286c27bc085bf07d61a59508cdaf2d45e63/yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb", size = 325765 }, - { url = "https://files.pythonhosted.org/packages/7e/18/03a5834ccc9177f97ca1bbb245b93c13e58e8225276f01eedc4cc98ab820/yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272", size = 344444 }, - { url = "https://files.pythonhosted.org/packages/c8/03/a713633bdde0640b0472aa197b5b86e90fbc4c5bc05b727b714cd8a40e6d/yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6", size = 340760 }, - { url = "https://files.pythonhosted.org/packages/eb/99/f6567e3f3bbad8fd101886ea0276c68ecb86a2b58be0f64077396cd4b95e/yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e", size = 346484 }, - { url = "https://files.pythonhosted.org/packages/8e/a9/84717c896b2fc6cb15bd4eecd64e34a2f0a9fd6669e69170c73a8b46795a/yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb", size = 359864 }, - { url = "https://files.pythonhosted.org/packages/1e/2e/d0f5f1bef7ee93ed17e739ec8dbcb47794af891f7d165fa6014517b48169/yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393", size = 364537 }, - { url = "https://files.pythonhosted.org/packages/97/8a/568d07c5d4964da5b02621a517532adb8ec5ba181ad1687191fffeda0ab6/yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285", size = 357861 }, - { url = "https://files.pythonhosted.org/packages/7d/e3/924c3f64b6b3077889df9a1ece1ed8947e7b61b0a933f2ec93041990a677/yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2", size = 84097 }, - { url = "https://files.pythonhosted.org/packages/34/45/0e055320daaabfc169b21ff6174567b2c910c45617b0d79c68d7ab349b02/yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477", size = 90399 }, - { url = "https://files.pythonhosted.org/packages/30/c7/c790513d5328a8390be8f47be5d52e141f78b66c6c48f48d241ca6bd5265/yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb", size = 140789 }, - { url = "https://files.pythonhosted.org/packages/30/aa/a2f84e93554a578463e2edaaf2300faa61c8701f0898725842c704ba5444/yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa", size = 94144 }, - { url = "https://files.pythonhosted.org/packages/c6/fc/d68d8f83714b221a85ce7866832cba36d7c04a68fa6a960b908c2c84f325/yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782", size = 91974 }, - { url = "https://files.pythonhosted.org/packages/56/4e/d2563d8323a7e9a414b5b25341b3942af5902a2263d36d20fb17c40411e2/yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0", size = 333587 }, - { url = "https://files.pythonhosted.org/packages/25/c9/cfec0bc0cac8d054be223e9f2c7909d3e8442a856af9dbce7e3442a8ec8d/yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482", size = 344386 }, - { url = "https://files.pythonhosted.org/packages/ab/5d/4c532190113b25f1364d25f4c319322e86232d69175b91f27e3ebc2caf9a/yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186", size = 345421 }, - { url = "https://files.pythonhosted.org/packages/23/d1/6cdd1632da013aa6ba18cee4d750d953104a5e7aac44e249d9410a972bf5/yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58", size = 339384 }, - { url = "https://files.pythonhosted.org/packages/9a/c4/6b3c39bec352e441bd30f432cda6ba51681ab19bb8abe023f0d19777aad1/yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53", size = 326689 }, - { url = "https://files.pythonhosted.org/packages/23/30/07fb088f2eefdc0aa4fc1af4e3ca4eb1a3aadd1ce7d866d74c0f124e6a85/yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2", size = 345453 }, - { url = "https://files.pythonhosted.org/packages/63/09/d54befb48f9cd8eec43797f624ec37783a0266855f4930a91e3d5c7717f8/yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8", size = 341872 }, - { url = "https://files.pythonhosted.org/packages/91/26/fd0ef9bf29dd906a84b59f0cd1281e65b0c3e08c6aa94b57f7d11f593518/yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1", size = 347497 }, - { url = "https://files.pythonhosted.org/packages/d9/b5/14ac7a256d0511b2ac168d50d4b7d744aea1c1aa20c79f620d1059aab8b2/yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a", size = 359981 }, - { url = "https://files.pythonhosted.org/packages/ca/b3/d493221ad5cbd18bc07e642894030437e405e1413c4236dd5db6e46bcec9/yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10", size = 366229 }, - { url = "https://files.pythonhosted.org/packages/04/56/6a3e2a5d9152c56c346df9b8fb8edd2c8888b1e03f96324d457e5cf06d34/yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8", size = 360383 }, - { url = "https://files.pythonhosted.org/packages/fd/b7/4b3c7c7913a278d445cc6284e59b2e62fa25e72758f888b7a7a39eb8423f/yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d", size = 310152 }, - { url = "https://files.pythonhosted.org/packages/f5/d5/688db678e987c3e0fb17867970700b92603cadf36c56e5fb08f23e822a0c/yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c", size = 315723 }, - { url = "https://files.pythonhosted.org/packages/6a/3b/fec4b08f5e88f68e56ee698a59284a73704df2e0e0b5bdf6536c86e76c76/yarl-1.18.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:61e5e68cb65ac8f547f6b5ef933f510134a6bf31bb178be428994b0cb46c2a04", size = 142780 }, - { url = "https://files.pythonhosted.org/packages/ed/85/796b0d6a22d536ec8e14bdbb86519250bad980cec450b6e299b1c2a9079e/yarl-1.18.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe57328fbc1bfd0bd0514470ac692630f3901c0ee39052ae47acd1d90a436719", size = 94981 }, - { url = "https://files.pythonhosted.org/packages/ee/0e/a830fd2238f7a29050f6dd0de748b3d6f33a7dbb67dbbc081a970b2bbbeb/yarl-1.18.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a440a2a624683108a1b454705ecd7afc1c3438a08e890a1513d468671d90a04e", size = 92789 }, - { url = "https://files.pythonhosted.org/packages/0f/4f/438c9fd668954779e48f08c0688ee25e0673380a21bb1e8ccc56de5b55d7/yarl-1.18.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09c7907c8548bcd6ab860e5f513e727c53b4a714f459b084f6580b49fa1b9cee", size = 317327 }, - { url = "https://files.pythonhosted.org/packages/bd/79/a78066f06179b4ed4581186c136c12fcfb928c475cbeb23743e71a991935/yarl-1.18.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4f6450109834af88cb4cc5ecddfc5380ebb9c228695afc11915a0bf82116789", size = 336999 }, - { url = "https://files.pythonhosted.org/packages/55/02/527963cf65f34a06aed1e766ff9a3b3e7d0eaa1c90736b2948a62e528e1d/yarl-1.18.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9ca04806f3be0ac6d558fffc2fdf8fcef767e0489d2684a21912cc4ed0cd1b8", size = 331693 }, - { url = "https://files.pythonhosted.org/packages/a2/2a/167447ae39252ba624b98b8c13c0ba35994d40d9110e8a724c83dbbb5822/yarl-1.18.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77a6e85b90a7641d2e07184df5557132a337f136250caafc9ccaa4a2a998ca2c", size = 321473 }, - { url = "https://files.pythonhosted.org/packages/55/03/07955fabb20082373be311c91fd78abe458bc7ff9069d34385e8bddad20e/yarl-1.18.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6333c5a377c8e2f5fae35e7b8f145c617b02c939d04110c76f29ee3676b5f9a5", size = 313571 }, - { url = "https://files.pythonhosted.org/packages/95/e2/67c8d3ec58a8cd8ddb1d63bd06eb7e7b91c9f148707a3eeb5a7ed87df0ef/yarl-1.18.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0b3c92fa08759dbf12b3a59579a4096ba9af8dd344d9a813fc7f5070d86bbab1", size = 325004 }, - { url = "https://files.pythonhosted.org/packages/06/43/51ceb3e427368fe6ccd9eccd162be227fd082523e02bad1fd3063daf68da/yarl-1.18.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:4ac515b860c36becb81bb84b667466885096b5fc85596948548b667da3bf9f24", size = 322677 }, - { url = "https://files.pythonhosted.org/packages/e4/0e/7ef286bfb23267739a703f7b967a858e2128c10bea898de8fa027e962521/yarl-1.18.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:045b8482ce9483ada4f3f23b3774f4e1bf4f23a2d5c912ed5170f68efb053318", size = 332806 }, - { url = "https://files.pythonhosted.org/packages/c8/94/2d1f060f4bfa47c8bd0bcb652bfe71fba881564bcac06ebb6d8ced9ac3bc/yarl-1.18.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:a4bb030cf46a434ec0225bddbebd4b89e6471814ca851abb8696170adb163985", size = 339919 }, - { url = "https://files.pythonhosted.org/packages/8e/8d/73b5f9a6ab69acddf1ca1d5e7bc92f50b69124512e6c26b36844531d7f23/yarl-1.18.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:54d6921f07555713b9300bee9c50fb46e57e2e639027089b1d795ecd9f7fa910", size = 340960 }, - { url = "https://files.pythonhosted.org/packages/41/13/ce6bc32be4476b60f4f8694831f49590884b2c975afcffc8d533bf2be7ec/yarl-1.18.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1d407181cfa6e70077df3377938c08012d18893f9f20e92f7d2f314a437c30b1", size = 336592 }, - { url = "https://files.pythonhosted.org/packages/81/d5/6e0460292d6299ac3919945f912b16b104f4e81ab20bf53e0872a1296daf/yarl-1.18.3-cp39-cp39-win32.whl", hash = "sha256:ac36703a585e0929b032fbaab0707b75dc12703766d0b53486eabd5139ebadd5", size = 84833 }, - { url = "https://files.pythonhosted.org/packages/b2/fc/a8aef69156ad5508165d8ae956736d55c3a68890610834bd985540966008/yarl-1.18.3-cp39-cp39-win_amd64.whl", hash = "sha256:ba87babd629f8af77f557b61e49e7c7cac36f22f871156b91e10a6e9d4f829e9", size = 90968 }, - { url = "https://files.pythonhosted.org/packages/f5/4b/a06e0ec3d155924f77835ed2d167ebd3b211a7b0853da1cf8d8414d784ef/yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b", size = 45109 }, -] - [[package]] name = "zipp" version = "3.20.2" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] sdist = { url = "https://files.pythonhosted.org/packages/54/bf/5c0000c44ebc80123ecbdddba1f5dcd94a5ada602a9c225d84b5aaa55e86/zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29", size = 24199 } wheels = [ { url = "https://files.pythonhosted.org/packages/62/8b/5ba542fa83c90e09eac972fc9baca7a88e7e7ca4b221a89251954019308b/zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350", size = 9200 }, ] - -[[package]] -name = "zipp" -version = "3.21.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version == '3.9.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630 }, -] From 185f0c2c9427e7fed5d61e218662ffdb39c40260 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Thu, 23 Jan 2025 12:37:38 +0100 Subject: [PATCH 55/64] >3.9 typing - state_machine.py - message.py - persistence.py - port.py - workchain.py - utils.py - process_spec.py - process_States.py - processes.py --- src/plumpy/base/state_machine.py | 103 +++++++++++++--------------- src/plumpy/coordinator.py | 7 +- src/plumpy/event_helper.py | 4 +- src/plumpy/events.py | 6 +- src/plumpy/exceptions.py | 7 +- src/plumpy/futures.py | 6 +- src/plumpy/loaders.py | 6 +- src/plumpy/message.py | 45 ++++++------ src/plumpy/persistence.py | 52 +++++++------- src/plumpy/ports.py | 85 ++++++++++++----------- src/plumpy/process_listener.py | 6 +- src/plumpy/process_spec.py | 39 +++++------ src/plumpy/process_states.py | 44 ++++++------ src/plumpy/processes.py | 114 ++++++++++++++----------------- src/plumpy/utils.py | 19 ++---- src/plumpy/workchains.py | 98 ++++++++++++-------------- 16 files changed, 306 insertions(+), 335 deletions(-) diff --git a/src/plumpy/base/state_machine.py b/src/plumpy/base/state_machine.py index a12981a0..2eaee534 100644 --- a/src/plumpy/base/state_machine.py +++ b/src/plumpy/base/state_machine.py @@ -1,28 +1,21 @@ # -*- coding: utf-8 -*- """The state machine for processes""" -from __future__ import annotations - import enum import functools import inspect import logging import os import sys +from collections.abc import Iterable, Sequence from types import TracebackType from typing import ( Any, Callable, ClassVar, - Dict, Hashable, - Iterable, - List, - Optional, Protocol, - Sequence, - Type, - Union, + final, runtime_checkable, ) @@ -34,19 +27,48 @@ _LOGGER = logging.getLogger(__name__) -EVENT_CALLBACK_TYPE = Callable[['StateMachine', Hashable, Optional['State']], None] +EVENT_CALLBACK_TYPE = Callable[['StateMachine', Hashable, 'State | None'], None] + + +@runtime_checkable +class State(Protocol): + LABEL: ClassVar[Any] + ALLOWED: ClassVar[set[Any]] + is_terminal: ClassVar[bool] + + def __init__(self, *args: Any, **kwargs: Any): ... + + def enter(self) -> None: ... + + def exit(self) -> None: ... + + +@runtime_checkable +class Interruptable(Protocol): + def interrupt(self, reason: Exception) -> None: ... + + +@runtime_checkable +class Proceedable(Protocol): + def execute(self) -> State | None: + """ + Execute the state, performing the actions that this state is responsible for. + :returns: a state to transition to or None if finished. + """ + ... class StateMachineError(Exception): """Base class for state machine errors""" +@final class StateEntryFailed(Exception): # noqa: N818 """ Failed to enter a state, can provide the next state to go to via this exception """ - def __init__(self, state: State, *args: Any, **kwargs: Any) -> None: + def __init__(self, state: 'State', *args: Any, **kwargs: Any) -> None: super().__init__('failed to enter state') self.state = state self.args = args @@ -63,11 +85,12 @@ def __init__(self, evt: str, msg: str): self.event = evt +@final class TransitionFailed(Exception): # noqa: N818 """A state transition failed""" def __init__( - self, initial_state: 'State', final_state: Optional['State'] = None, traceback_str: Optional[str] = None + self, initial_state: 'State', final_state: 'State | None' = None, traceback_str: str | None = None ) -> None: self.initial_state = initial_state self.final_state = final_state @@ -82,8 +105,8 @@ def _format_msg(self) -> str: def event( - from_states: Union[str, Type['State'], Iterable[Type['State']]] = '*', - to_states: Union[str, Type['State'], Iterable[Type['State']]] = '*', + from_states: str | type['State'] | Iterable[type['State']] = '*', + to_states: str | type['State'] | Iterable[type['State']] = '*', ) -> Callable[[Callable[..., Any]], Callable[..., Any]]: """A decorator to check for correct transitions, raising ``EventError`` on invalid transitions.""" if from_states != '*': @@ -115,7 +138,7 @@ def transition(self: Any, *a: Any, **kw: Any) -> Any: raise EventError( evt_label, - 'Event produced invalid state transition from ' f'{initial.LABEL} to {self._state.LABEL}', + f'Event produced invalid state transition from {initial.LABEL} to {self._state.LABEL}', ) return result @@ -128,35 +151,7 @@ def transition(self: Any, *a: Any, **kw: Any) -> Any: return wrapper -@runtime_checkable -class State(Protocol): - LABEL: ClassVar[Any] - ALLOWED: ClassVar[set[Any]] - is_terminal: ClassVar[bool] - - def __init__(self, *args: Any, **kwargs: Any): ... - - def enter(self) -> None: ... - - def exit(self) -> None: ... - - -@runtime_checkable -class Interruptable(Protocol): - def interrupt(self, reason: Exception) -> None: ... - - -@runtime_checkable -class Proceedable(Protocol): - def execute(self) -> State | None: - """ - Execute the state, performing the actions that this state is responsible for. - :returns: a state to transition to or None if finished. - """ - ... - - -def create_state(st: StateMachine, state_label: Hashable, *args: Any, **kwargs: Any) -> State: +def create_state(st: 'StateMachine', state_label: Hashable, *args: Any, **kwargs: Any) -> State: if state_label not in st.get_states_map(): raise ValueError(f'{state_label} is not a valid state') @@ -192,20 +187,20 @@ def __call__(cls, *args: Any, **kwargs: Any) -> 'StateMachine': class StateMachine(metaclass=StateMachineMeta): - STATES: Optional[Sequence[Type[State]]] = None - _STATES_MAP: Optional[Dict[Hashable, Type[State]]] = None + STATES: Sequence[type[State]] | None = None + _STATES_MAP: dict[Hashable, type[State]] | None = None - _transitioning = False - _transition_failing = False + _transitioning: bool = False + _transition_failing: bool = False @classmethod - def get_states_map(cls) -> Dict[Hashable, Type[State]]: + def get_states_map(cls) -> dict[Hashable, type[State]]: cls.__ensure_built() assert cls._STATES_MAP is not None # required for type checking return cls._STATES_MAP @classmethod - def get_states(cls) -> Sequence[Type[State]]: + def get_states(cls) -> Sequence[type[State]]: if cls.STATES is not None: return cls.STATES @@ -218,7 +213,7 @@ def initial_state_label(cls) -> Any: return cls.STATES[0].LABEL @classmethod - def get_state_class(cls, label: Any) -> Type[State]: + def get_state_class(cls, label: Any) -> type[State]: cls.__ensure_built() assert cls._STATES_MAP is not None return cls._STATES_MAP[label] @@ -249,11 +244,11 @@ def __ensure_built(cls) -> None: def __init__(self) -> None: super().__init__() self.__ensure_built() - self._state: Optional[State] = None + self._state: State | None = None self._exception_handler = None # Note this appears to never be used self.set_debug((not sys.flags.ignore_environment and bool(os.environ.get('PYTHONSMDEBUG')))) self._transitioning = False - self._event_callbacks: Dict[Hashable, List[EVENT_CALLBACK_TYPE]] = {} + self._event_callbacks: dict[Hashable, list[EVENT_CALLBACK_TYPE]] = {} @super_check def init(self) -> None: @@ -298,7 +293,7 @@ def remove_state_event_callback(self, hook: Hashable, callback: EVENT_CALLBACK_T except (KeyError, ValueError): raise ValueError(f"Callback not set for hook '{hook}'") - def _fire_state_event(self, hook: Hashable, state: Optional[State]) -> None: + def _fire_state_event(self, hook: Hashable, state: State | None) -> None: for callback in self._event_callbacks.get(hook, []): callback(self, hook, state) diff --git a/src/plumpy/coordinator.py b/src/plumpy/coordinator.py index 702ea5f5..cffd08b5 100644 --- a/src/plumpy/coordinator.py +++ b/src/plumpy/coordinator.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- from __future__ import annotations -from typing import TYPE_CHECKING, Any, Callable, Hashable, Pattern, Protocol +import re +from typing import TYPE_CHECKING, Any, Callable, Hashable, Protocol if TYPE_CHECKING: # identifiers for subscribers @@ -23,8 +24,8 @@ def add_rpc_subscriber(self, subscriber: 'RpcSubscriber', identifier: 'ID_TYPE | def add_broadcast_subscriber( self, subscriber: 'BroadcastSubscriber', - subject_filters: list[Hashable | Pattern[str]] | None = None, - sender_filters: list[Hashable | Pattern[str]] | None = None, + subject_filters: list[Hashable | re.Pattern[str]] | None = None, + sender_filters: list[Hashable | re.Pattern[str]] | None = None, identifier: 'ID_TYPE | None' = None, ) -> Any: ... diff --git a/src/plumpy/event_helper.py b/src/plumpy/event_helper.py index 47188031..1a55939c 100644 --- a/src/plumpy/event_helper.py +++ b/src/plumpy/event_helper.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING, Any, Callable, Optional, final +from typing import TYPE_CHECKING, Any, Callable, final from typing_extensions import Self @@ -38,7 +38,7 @@ def remove_all_listeners(self) -> None: self._listeners.clear() @classmethod - def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> Self: + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: LoadSaveContext | None = None) -> Self: """ Recreate a :class:`Savable` from a saved state using an optional load context. diff --git a/src/plumpy/events.py b/src/plumpy/events.py index a6e62529..7379a6b6 100644 --- a/src/plumpy/events.py +++ b/src/plumpy/events.py @@ -3,7 +3,7 @@ import asyncio import sys -from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Sequence +from typing import TYPE_CHECKING, Any, Callable, Dict, Sequence if TYPE_CHECKING: from .processes import Process @@ -22,7 +22,7 @@ def new_event_loop(*args: Any, **kwargs: Any) -> asyncio.AbstractEventLoop: class PlumpyEventLoopPolicy(asyncio.DefaultEventLoopPolicy): """Custom event policy that always returns the same event loop that is made reentrant by ``nest_asyncio``.""" - _loop: Optional[asyncio.AbstractEventLoop] = None + _loop: asyncio.AbstractEventLoop | None = None def get_event_loop(self) -> asyncio.AbstractEventLoop: """Return the patched event loop.""" @@ -55,7 +55,7 @@ def reset_event_loop_policy() -> None: asyncio.set_event_loop_policy(None) -def run_until_complete(future: asyncio.Future, loop: Optional[asyncio.AbstractEventLoop] = None) -> Any: +def run_until_complete(future: asyncio.Future, loop: asyncio.AbstractEventLoop | None = None) -> Any: loop = loop or get_event_loop() return loop.run_until_complete(future) diff --git a/src/plumpy/exceptions.py b/src/plumpy/exceptions.py index b4358770..7a559b07 100644 --- a/src/plumpy/exceptions.py +++ b/src/plumpy/exceptions.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- -from typing import Optional + + +from typing import final class KilledError(Exception): @@ -12,10 +14,11 @@ class InvalidStateError(Exception): """ +@final class UnsuccessfulResult: """The result of the process was unsuccessful""" - def __init__(self, result: Optional[int] = None): + def __init__(self, result: int | None = None): """Initialise. :param result: the exit code of the process diff --git a/src/plumpy/futures.py b/src/plumpy/futures.py index 3a59351d..e9d7e928 100644 --- a/src/plumpy/futures.py +++ b/src/plumpy/futures.py @@ -7,7 +7,8 @@ import asyncio import contextlib -from typing import Any, Awaitable, Callable, Generator, Optional +from collections.abc import Awaitable, Generator +from typing import Any, Callable, final class InvalidFutureError(Exception): @@ -33,6 +34,7 @@ def capture_exceptions(future, ignore: tuple[type[BaseException], ...] = ()) -> future.set_exception(exception) +@final class CancellableAction(Future): """ An action that can be launched and potentially cancelled @@ -64,7 +66,7 @@ def run(self, *args: Any, **kwargs: Any) -> None: self._action = None # type: ignore -def create_task(coro: Callable[[], Awaitable[Any]], loop: Optional[asyncio.AbstractEventLoop] = None) -> Future: +def create_task(coro: Callable[[], Awaitable[Any]], loop: asyncio.AbstractEventLoop | None = None) -> Future: """ Schedule a call to a coro in the event loop and wrap the outcome in a future. diff --git a/src/plumpy/loaders.py b/src/plumpy/loaders.py index bb248d6a..87b3b169 100644 --- a/src/plumpy/loaders.py +++ b/src/plumpy/loaders.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import abc import importlib -from typing import Any, Optional +from typing import Any class ObjectLoader(metaclass=abc.ABCMeta): @@ -62,7 +62,7 @@ def identify_object(self, obj: Any) -> str: return identifier -OBJECT_LOADER: Optional[ObjectLoader] = None +OBJECT_LOADER: ObjectLoader | None = None def get_object_loader() -> ObjectLoader: @@ -78,7 +78,7 @@ def get_object_loader() -> ObjectLoader: return OBJECT_LOADER -def set_object_loader(loader: Optional[ObjectLoader]) -> None: +def set_object_loader(loader: ObjectLoader | None) -> None: """ Set the plumpy global object loader diff --git a/src/plumpy/message.py b/src/plumpy/message.py index 04f03bd9..1ed43845 100644 --- a/src/plumpy/message.py +++ b/src/plumpy/message.py @@ -5,7 +5,8 @@ import asyncio import logging -from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence, Union, cast +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any, cast from plumpy.coordinator import Coordinator from plumpy.exceptions import PersistenceError, TaskRejectedError @@ -48,7 +49,7 @@ class Intent: LOGGER = logging.getLogger(__name__) -MessageType = Dict[str, Any] +MessageType = dict[str, Any] class MessageBuilder: @@ -90,12 +91,12 @@ def status(cls, text: str | None = None) -> MessageType: def create_launch_body( process_class: str, - init_args: Optional[Sequence[Any]] = None, - init_kwargs: Optional[Dict[str, Any]] = None, + init_args: Sequence[Any] | None = None, + init_kwargs: dict[str, Any] | None = None, persist: bool = False, - loader: Optional[loaders.ObjectLoader] = None, + loader: loaders.ObjectLoader | None = None, nowait: bool = True, -) -> Dict[str, Any]: +) -> dict[str, Any]: """ Create a message body for the launch action @@ -124,7 +125,7 @@ def create_launch_body( return msg_body -def create_continue_body(pid: 'PID_TYPE', tag: Optional[str] = None, nowait: bool = False) -> Dict[str, Any]: +def create_continue_body(pid: 'PID_TYPE', tag: str | None = None, nowait: bool = False) -> dict[str, Any]: """ Create a message body to continue an existing process :param pid: the pid of the existing process @@ -139,11 +140,11 @@ def create_continue_body(pid: 'PID_TYPE', tag: Optional[str] = None, nowait: boo def create_create_body( process_class: str, - init_args: Optional[Sequence[Any]] = None, - init_kwargs: Optional[Dict[str, Any]] = None, + init_args: Sequence[Any] | None = None, + init_kwargs: dict[str, Any] | None = None, persist: bool = False, - loader: Optional[loaders.ObjectLoader] = None, -) -> Dict[str, Any]: + loader: loaders.ObjectLoader | None = None, +) -> dict[str, Any]: """ Create a message body to create a new process :param process_class: the class of the process to launch @@ -196,10 +197,10 @@ class ProcessLauncher: def __init__( self, - loop: Optional[asyncio.AbstractEventLoop] = None, - persister: Optional[persistence.Persister] = None, - load_context: Optional[persistence.LoadSaveContext] = None, - loader: Optional[loaders.ObjectLoader] = None, + loop: asyncio.AbstractEventLoop | None = None, + persister: persistence.Persister | None = None, + load_context: persistence.LoadSaveContext | None = None, + loader: loaders.ObjectLoader | None = None, ) -> None: self._loop = loop self._persister = persister @@ -211,7 +212,7 @@ def __init__( else: self._loader = loaders.get_object_loader() - async def __call__(self, coordinator: Coordinator, task: Dict[str, Any]) -> Union[PID_TYPE, Any]: + async def __call__(self, coordinator: Coordinator, task: dict[str, Any]) -> PID_TYPE | Any: """ Receive a task. :param task: The task message @@ -231,9 +232,9 @@ async def _launch( process_class: str, persist: bool, nowait: bool, - init_args: Optional[Sequence[Any]] = None, - init_kwargs: Optional[Dict[str, Any]] = None, - ) -> Union[PID_TYPE, Any]: + init_args: Sequence[Any] | None = None, + init_kwargs: dict[str, Any] | None = None, + ) -> PID_TYPE | Any: """ Launch the process @@ -266,7 +267,7 @@ async def _launch( return proc.future().result() - async def _continue(self, pid: 'PID_TYPE', nowait: bool, tag: Optional[str] = None) -> Union[PID_TYPE, Any]: + async def _continue(self, pid: 'PID_TYPE', nowait: bool, tag: str | None = None) -> PID_TYPE | Any: """ Continue the process @@ -295,8 +296,8 @@ async def _create( self, process_class: str, persist: bool, - init_args: Optional[Sequence[Any]] = None, - init_kwargs: Optional[Dict[str, Any]] = None, + init_args: Sequence[Any] | None = None, + init_kwargs: dict[str, Any] | None = None, ) -> 'PID_TYPE': """ Create the process diff --git a/src/plumpy/persistence.py b/src/plumpy/persistence.py index 02b5ff76..fb93ca9f 100644 --- a/src/plumpy/persistence.py +++ b/src/plumpy/persistence.py @@ -10,16 +10,12 @@ import inspect import os import pickle +from collections.abc import Generator, Iterable from typing import ( TYPE_CHECKING, Any, Callable, ClassVar, - Dict, - Generator, - Iterable, - List, - Optional, Protocol, TypeVar, runtime_checkable, @@ -38,7 +34,7 @@ class LoadSaveContext: - def __init__(self, loader: Optional[loaders.ObjectLoader] = None, **kwargs: Any) -> None: + def __init__(self, loader: loaders.ObjectLoader | None = None, **kwargs: Any) -> None: self._values = dict(**kwargs) self.loader = loader @@ -134,7 +130,7 @@ def _bundle_constructor(loader: yaml.Loader, data: Any) -> Generator[Bundle, Non class Persister(metaclass=abc.ABCMeta): @abc.abstractmethod - def save_checkpoint(self, process: 'Process', tag: Optional[str] = None) -> None: + def save_checkpoint(self, process: 'Process', tag: str | None = None) -> None: """ Persist a Process instance @@ -145,7 +141,7 @@ def save_checkpoint(self, process: 'Process', tag: Optional[str] = None) -> None """ @abc.abstractmethod - def load_checkpoint(self, pid: PID_TYPE, tag: Optional[str] = None) -> Bundle: + def load_checkpoint(self, pid: PID_TYPE, tag: str | None = None) -> Bundle: """ Load a process from a persisted checkpoint by its process id @@ -158,7 +154,7 @@ def load_checkpoint(self, pid: PID_TYPE, tag: Optional[str] = None) -> Bundle: """ @abc.abstractmethod - def get_checkpoints(self) -> List[PersistedCheckpoint]: + def get_checkpoints(self) -> list[PersistedCheckpoint]: """ Return a list of all the current persisted process checkpoints with each element containing the process id and optional checkpoint tag @@ -167,7 +163,7 @@ def get_checkpoints(self) -> List[PersistedCheckpoint]: """ @abc.abstractmethod - def get_process_checkpoints(self, pid: PID_TYPE) -> List[PersistedCheckpoint]: + def get_process_checkpoints(self, pid: PID_TYPE) -> list[PersistedCheckpoint]: """ Return a list of all the current persisted process checkpoints for the specified process with each element containing the process id and @@ -178,7 +174,7 @@ def get_process_checkpoints(self, pid: PID_TYPE) -> List[PersistedCheckpoint]: """ @abc.abstractmethod - def delete_checkpoint(self, pid: PID_TYPE, tag: Optional[str] = None) -> None: + def delete_checkpoint(self, pid: PID_TYPE, tag: str | None = None) -> None: """ Delete a persisted process checkpoint. No error will be raised if the checkpoint does not exist @@ -251,7 +247,7 @@ def load_pickle(filepath: str) -> 'PersistedPickle': return persisted_pickle @staticmethod - def pickle_filename(pid: PID_TYPE, tag: Optional[str] = None) -> str: + def pickle_filename(pid: PID_TYPE, tag: str | None = None) -> str: """ Returns the relative filepath of the pickle for the given process id and optional checkpoint tag @@ -263,14 +259,14 @@ def pickle_filename(pid: PID_TYPE, tag: Optional[str] = None) -> str: return filename - def _pickle_filepath(self, pid: PID_TYPE, tag: Optional[str] = None) -> str: + def _pickle_filepath(self, pid: PID_TYPE, tag: str | None = None) -> str: """ Returns the full filepath of the pickle for the given process id and optional checkpoint tag """ return os.path.join(self._pickle_directory, PicklePersister.pickle_filename(pid, tag)) - def save_checkpoint(self, process: 'Process', tag: Optional[str] = None) -> None: + def save_checkpoint(self, process: 'Process', tag: str | None = None) -> None: """ Persist a process to a pickle on disk @@ -285,7 +281,7 @@ def save_checkpoint(self, process: 'Process', tag: Optional[str] = None) -> None with open(self._pickle_filepath(process.pid, tag), 'w+b') as handle: pickle.dump(persisted_pickle, handle) - def load_checkpoint(self, pid: PID_TYPE, tag: Optional[str] = None) -> Bundle: + def load_checkpoint(self, pid: PID_TYPE, tag: str | None = None) -> Bundle: """ Load a process from a persisted checkpoint by its process id @@ -300,7 +296,7 @@ def load_checkpoint(self, pid: PID_TYPE, tag: Optional[str] = None) -> Bundle: return checkpoint.bundle - def get_checkpoints(self) -> List[PersistedCheckpoint]: + def get_checkpoints(self) -> list[PersistedCheckpoint]: """ Return a list of all the current persisted process checkpoints with each element containing the process id and optional checkpoint tag @@ -318,7 +314,7 @@ def get_checkpoints(self) -> List[PersistedCheckpoint]: return checkpoints - def get_process_checkpoints(self, pid: PID_TYPE) -> List[PersistedCheckpoint]: + def get_process_checkpoints(self, pid: PID_TYPE) -> list[PersistedCheckpoint]: """ Return a list of all the current persisted process checkpoints for the specified process with each element containing the process id and @@ -329,7 +325,7 @@ def get_process_checkpoints(self, pid: PID_TYPE) -> List[PersistedCheckpoint]: """ return [c for c in self.get_checkpoints() if c.pid == pid] - def delete_checkpoint(self, pid: PID_TYPE, tag: Optional[str] = None) -> None: + def delete_checkpoint(self, pid: PID_TYPE, tag: str | None = None) -> None: """ Delete a persisted process checkpoint. No error will be raised if the checkpoint does not exist @@ -358,26 +354,26 @@ def delete_process_checkpoints(self, pid: PID_TYPE) -> None: class InMemoryPersister(Persister): """Mainly to be used in testing/debugging""" - def __init__(self, loader: Optional[loaders.ObjectLoader] = None) -> None: + def __init__(self, loader: loaders.ObjectLoader | None = None) -> None: super().__init__() - self._checkpoints: Dict[PID_TYPE, Dict[Optional[str], Bundle]] = {} + self._checkpoints: dict[PID_TYPE, dict[str | None, Bundle]] = {} self._save_context = LoadSaveContext(loader=loader) - def save_checkpoint(self, process: 'Process', tag: Optional[str] = None) -> None: + def save_checkpoint(self, process: 'Process', tag: str | None = None) -> None: self._checkpoints.setdefault(process.pid, {})[tag] = Bundle( process, self._save_context.loader, dereference=True ) - def load_checkpoint(self, pid: PID_TYPE, tag: Optional[str] = None) -> Bundle: + def load_checkpoint(self, pid: PID_TYPE, tag: str | None = None) -> Bundle: return self._checkpoints[pid][tag] - def get_checkpoints(self) -> List[PersistedCheckpoint]: + def get_checkpoints(self) -> list[PersistedCheckpoint]: cps = [] for pid in self._checkpoints: cps.extend(self.get_process_checkpoints(pid)) return cps - def get_process_checkpoints(self, pid: PID_TYPE) -> List[PersistedCheckpoint]: + def get_process_checkpoints(self, pid: PID_TYPE) -> list[PersistedCheckpoint]: cps = [] try: for tag, _ in self._checkpoints[pid].items(): @@ -386,7 +382,7 @@ def get_process_checkpoints(self, pid: PID_TYPE) -> List[PersistedCheckpoint]: pass return cps - def delete_checkpoint(self, pid: PID_TYPE, tag: Optional[str] = None) -> None: + def delete_checkpoint(self, pid: PID_TYPE, tag: str | None = None) -> None: try: del self._checkpoints[pid][tag] except KeyError: @@ -397,7 +393,7 @@ def delete_process_checkpoints(self, pid: PID_TYPE) -> None: del self._checkpoints[pid] -def ensure_object_loader(context: Optional['LoadSaveContext'], saved_state: SAVED_STATE_TYPE) -> 'LoadSaveContext': +def ensure_object_loader(context: 'LoadSaveContext' | None, saved_state: SAVED_STATE_TYPE) -> 'LoadSaveContext': """ Given a LoadSaveContext this method will ensure that it has a valid class loader using the following priorities: @@ -453,7 +449,7 @@ def get_custom_meta(saved_state: SAVED_STATE_TYPE, name: str) -> Any: raise ValueError(f"Unknown meta key '{name}'") @staticmethod - def get_create_meta(out_state: SAVED_STATE_TYPE) -> Dict[str, Any]: + def get_create_meta(out_state: SAVED_STATE_TYPE) -> dict[str, Any]: return out_state.setdefault(META, {}) @staticmethod @@ -605,7 +601,7 @@ def save(self, loader: loaders.ObjectLoader | None = None) -> SAVED_STATE_TYPE: return out_state @classmethod - def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> Self: + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: LoadSaveContext | None = None) -> Self: """ Recreate a :class:`Savable` from a saved state using an optional load context. diff --git a/src/plumpy/ports.py b/src/plumpy/ports.py index 8522f061..d13ce5ad 100644 --- a/src/plumpy/ports.py +++ b/src/plumpy/ports.py @@ -1,13 +1,13 @@ # -*- coding: utf-8 -*- """Module for process ports""" -import collections import copy import inspect import json import logging import warnings -from typing import Any, Callable, Dict, Iterator, List, Mapping, MutableMapping, Optional, Sequence, Type, Union, cast +from collections.abc import Iterator, Mapping, MutableMapping, Sequence +from typing import Any, Callable, cast from plumpy.utils import AttributesFrozendict, is_mutable_property, type_check @@ -18,7 +18,7 @@ This has been deprecated and the new signature is `validator(value, port)` where the `port` argument will be the port instance to which the validator has been assigned.""" -VALIDATOR_TYPE = Callable[[Any, 'Port'], Optional[str]] +VALIDATOR_TYPE = Callable[[Any, 'Port'], str | None] class PortValidationError(Exception): @@ -64,10 +64,10 @@ class Port: def __init__( self, name: str, - valid_type: Optional[Type[Any]] = None, - help: Optional[str] = None, + valid_type: type[Any] | None = None, + help: str | None = None, required: bool = True, - validator: Optional[VALIDATOR_TYPE] = None, + validator: VALIDATOR_TYPE | None = None, ) -> None: self._name = name self._valid_type = valid_type @@ -83,7 +83,7 @@ def __str__(self) -> str: """ return json.dumps(self.get_description()) - def get_description(self) -> Dict[str, Any]: + def get_description(self) -> dict[str, Any]: """Return a description of the Port, which will be a dictionary of its attributes :returns: a dictionary of the stringified Port attributes @@ -106,7 +106,7 @@ def name(self) -> str: return self._name @property - def valid_type(self) -> Optional[Type[Any]]: + def valid_type(self) -> type[Any] | None: """Get the valid value type for this port if one is specified :return: the value value type @@ -115,7 +115,7 @@ def valid_type(self) -> Optional[Type[Any]]: return self._valid_type @valid_type.setter - def valid_type(self, valid_type: Optional[Type[Any]]) -> None: + def valid_type(self, valid_type: type[Any] | None) -> None: """Set the valid value type for this port :param valid_type: the value valid type @@ -124,7 +124,7 @@ def valid_type(self, valid_type: Optional[Type[Any]]) -> None: self._valid_type = valid_type @property - def help(self) -> Optional[str]: + def help(self) -> str | None: """Get the help string for this port :return: the help string @@ -133,7 +133,7 @@ def help(self) -> Optional[str]: return self._help @help.setter - def help(self, help: Optional[str]) -> None: + def help(self, help: str | None) -> None: """Set the help string for this port :param help: the help string @@ -160,16 +160,15 @@ def required(self, required: bool) -> None: self._required = required @property - def validator(self) -> Optional[VALIDATOR_TYPE]: + def validator(self) -> VALIDATOR_TYPE | None: """Get the validator for this port :return: the validator - :rtype: typing.Callable[[typing.Any], typing.Tuple[bool, typing.Optional[str]]] """ return self._validator @validator.setter - def validator(self, validator: Optional[VALIDATOR_TYPE]) -> None: + def validator(self, validator: VALIDATOR_TYPE | None) -> None: """Set the validator for this port :param validator: a validator function @@ -177,7 +176,7 @@ def validator(self, validator: Optional[VALIDATOR_TYPE]) -> None: """ self._validator = validator - def validate(self, value: Any, breadcrumbs: Sequence[str] = ()) -> Optional[PortValidationError]: + def validate(self, value: Any, breadcrumbs: Sequence[str] = ()) -> PortValidationError | None: """Validate a value to see if it is valid for this port :param value: the value to check @@ -231,11 +230,11 @@ def required_override(required: bool, default: Any) -> bool: def __init__( self, name: str, - valid_type: Optional[Type[Any]] = None, - help: Optional[str] = None, + valid_type: type[Any] | None = None, + help: str | None = None, default: Any = UNSPECIFIED, required: bool = True, - validator: Optional[VALIDATOR_TYPE] = None, + validator: VALIDATOR_TYPE | None = None, ) -> None: super().__init__( name, @@ -273,7 +272,7 @@ def default(self) -> Any: def default(self, default: Any) -> None: self._default = default - def get_description(self) -> Dict[str, str]: + def get_description(self) -> dict[str, str]: """ Return a description of the InputPort, which will be a dictionary of its attributes @@ -291,7 +290,7 @@ class OutputPort(Port): pass -class PortNamespace(collections.abc.MutableMapping, Port): +class PortNamespace(MutableMapping, Port): """ A container for Ports. Effectively it maintains a dictionary whose members are either a Port or yet another PortNamespace. This allows for the nesting of ports @@ -302,10 +301,10 @@ class PortNamespace(collections.abc.MutableMapping, Port): def __init__( self, name: str = '', # Note this was set to None, but that would fail if you tried to compute breadcrumbs - help: Optional[str] = None, + help: str | None = None, required: bool = True, - validator: Optional[VALIDATOR_TYPE] = None, - valid_type: Optional[Type[Any]] = None, + validator: VALIDATOR_TYPE | None = None, + valid_type: type[Any] | None = None, default: Any = UNSPECIFIED, dynamic: bool = False, populate_defaults: bool = True, @@ -326,7 +325,7 @@ def __init__( property is ignored and the population of defaults is always performed. """ super().__init__(name=name, help=help, required=required, validator=validator, valid_type=valid_type) - self._ports: Dict[str, Union[Port, 'PortNamespace']] = {} + self._ports: dict[str, 'Port | PortNamespace'] = {} self.default = default self.populate_defaults = populate_defaults self.valid_type = valid_type @@ -347,16 +346,16 @@ def __len__(self) -> int: def __delitem__(self, key: str) -> None: del self._ports[key] - def __getitem__(self, key: str) -> Union[Port, 'PortNamespace']: + def __getitem__(self, key: str) -> 'Port | PortNamespace': return self._ports[key] - def __setitem__(self, key: str, port: Union[Port, 'PortNamespace']) -> None: + def __setitem__(self, key: str, port: 'Port | PortNamespace') -> None: if not isinstance(port, Port): raise TypeError('port needs to be an instance of Port') self._ports[key] = port @property - def ports(self) -> Dict[str, Union[Port, 'PortNamespace']]: + def ports(self) -> dict[str, 'Port | PortNamespace']: return self._ports def has_default(self) -> bool: @@ -379,11 +378,11 @@ def dynamic(self, dynamic: bool) -> None: self._dynamic = dynamic @property - def valid_type(self) -> Optional[Type[Any]]: + def valid_type(self) -> type[Any] | None: return super().valid_type @valid_type.setter - def valid_type(self, valid_type: Optional[Type[Any]]) -> None: + def valid_type(self, valid_type: type[Any] | None) -> None: """Set the `valid_type` for the `PortNamespace`. If the `valid_type` is None, the `dynamic` property will be set to `False`, in all other cases `dynamic` will be @@ -404,7 +403,7 @@ def populate_defaults(self) -> bool: def populate_defaults(self, populate_defaults: bool) -> None: self._populate_defaults = populate_defaults - def get_description(self) -> Dict[str, Dict[str, Any]]: + def get_description(self) -> dict[str, dict[str, Any]]: """ Return a dictionary with a description of the ports this namespace contains Nested PortNamespaces will be properly recursed and Ports will print their properties in a list @@ -426,7 +425,7 @@ def get_description(self) -> Dict[str, Dict[str, Any]]: return description - def get_port(self, name: str, create_dynamically: bool = False) -> Union[Port, 'PortNamespace']: + def get_port(self, name: str, create_dynamically: bool = False) -> 'Port | PortNamespace': """ Retrieve a (namespaced) port from this PortNamespace. If any of the sub namespaces of the terminal port itself cannot be found, a ValueError will be raised @@ -510,10 +509,10 @@ def create_port_namespace(self, name: str, **kwargs: Any) -> 'PortNamespace': def absorb( self, port_namespace: 'PortNamespace', - exclude: Optional[Sequence[str]] = None, - include: Optional[Sequence[str]] = None, - namespace_options: Optional[Dict[str, Any]] = None, - ) -> List[str]: + exclude: Sequence[str] | None = None, + include: Sequence[str] | None = None, + namespace_options: dict[str, Any] | None = None, + ) -> list[str]: """Absorb another PortNamespace instance into oneself, including all its mutable properties and ports. Mutable properties of self will be overwritten with those of the port namespace that is to be absorbed. @@ -611,8 +610,8 @@ def project(self, port_values: MutableMapping[str, Any]) -> MutableMapping[str, return result def validate( - self, port_values: Optional[Mapping[str, Any]] = None, breadcrumbs: Sequence[str] = () - ) -> Optional[PortValidationError]: + self, port_values: Mapping[str, Any] | None = None, breadcrumbs: Sequence[str] = () + ) -> PortValidationError | None: """ Validate the namespace port itself and subsequently all the port_values it contains @@ -622,12 +621,12 @@ def validate( """ breadcrumbs_local = (*breadcrumbs, self.name) - message: Optional[str] + message: str | None if not port_values: port_values = {} - if not isinstance(port_values, collections.abc.Mapping): + if not isinstance(port_values, Mapping): message = f'specified value is of type {type(port_values)} which is not sub class of `Mapping`' return PortValidationError(message, breadcrumbs_to_port(breadcrumbs_local)) @@ -706,7 +705,7 @@ def pre_process(self, port_values: MutableMapping[str, Any]) -> AttributesFrozen def validate_ports( self, port_values: MutableMapping[str, Any], breadcrumbs: Sequence[str] - ) -> Optional[PortValidationError]: + ) -> PortValidationError | None: """ Validate port values with respect to the explicitly defined ports of the port namespace. Ports values that are matched to an actual Port will be popped from the dictionary @@ -725,7 +724,7 @@ def validate_ports( def validate_dynamic_ports( self, port_values: MutableMapping[str, Any], breadcrumbs: Sequence[str] = () - ) -> Optional[PortValidationError]: + ) -> PortValidationError | None: """ Validate port values with respect to the dynamic properties of the port namespace. It will check if the namespace is actually dynamic and if all values adhere to the valid types of @@ -736,7 +735,7 @@ def validate_dynamic_ports( :param breadcrumbs: a tuple of the path to having reached this point in validation :type breadcrumbs: typing.Tuple[str] :return: if invalid returns a string with the reason for the validation failure, otherwise None - :rtype: typing.Optional[str] + :rtype: str | None """ if port_values and not self.dynamic: msg = f'Unexpected ports {port_values}, for a non dynamic namespace' @@ -757,7 +756,7 @@ def validate_dynamic_ports( return None @staticmethod - def strip_namespace(namespace: str, separator: str, rules: Optional[Sequence[str]] = None) -> Optional[List[str]]: + def strip_namespace(namespace: str, separator: str, rules: Sequence[str] | None = None) -> list[str] | None: """Filter given exclude/include rules staring with namespace and strip the first level. For example if the namespace is `base` and the rules are:: diff --git a/src/plumpy/process_listener.py b/src/plumpy/process_listener.py index 8bc7c828..5a9098da 100644 --- a/src/plumpy/process_listener.py +++ b/src/plumpy/process_listener.py @@ -2,7 +2,7 @@ from __future__ import annotations import abc -from typing import TYPE_CHECKING, Any, Dict, Optional +from typing import TYPE_CHECKING, Any from typing_extensions import Self @@ -22,13 +22,13 @@ class ProcessListener(metaclass=abc.ABCMeta): def __init__(self) -> None: super().__init__() - self._params: Dict[str, Any] = {} + self._params: dict[str, Any] = {} def init(self, **kwargs: Any) -> None: self._params = kwargs @classmethod - def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> Self: + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: LoadSaveContext | None = None) -> Self: """ Recreate a :class:`Savable` from a saved state using an optional load context. diff --git a/src/plumpy/process_spec.py b/src/plumpy/process_spec.py index 00f2f3cc..f1566fea 100644 --- a/src/plumpy/process_spec.py +++ b/src/plumpy/process_spec.py @@ -2,14 +2,15 @@ import collections import json import logging -from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence, Type, Union, cast +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any, cast from .ports import InputPort, OutputPort, Port, PortNamespace if TYPE_CHECKING: from .processes import Process -EXPOSED_TYPE = Dict[Optional[str], Dict[Type['Process'], Sequence[str]]] +EXPOSED_TYPE = dict[str | None, dict[type['Process'], Sequence[str]]] class ProcessSpec: @@ -68,7 +69,7 @@ def sealed(self) -> bool: """ return self._sealed - def get_description(self) -> Dict[str, Any]: + def get_description(self) -> dict[str, Any]: """ Get a description of this process specification @@ -101,7 +102,7 @@ def outputs(self) -> PortNamespace: return cast(PortNamespace, self._ports[self.NAME_OUTPUTS_PORT_NAMESPACE]) def _create_port( - self, port_namespace: PortNamespace, port_class: Type[Union[Port, PortNamespace]], name: str, **kwargs: Any + self, port_namespace: PortNamespace, port_class: type[Port | PortNamespace], name: str, **kwargs: Any ) -> None: """ Create a new Port of a given class and name in a given PortNamespace @@ -181,11 +182,11 @@ def has_output(self, name: str) -> bool: def expose_inputs( self, - process_class: Type['Process'], - namespace: Optional[str] = None, - exclude: Optional[Sequence[str]] = None, - include: Optional[Sequence[str]] = None, - namespace_options: Optional[dict] = None, + process_class: type['Process'], + namespace: str | None = None, + exclude: Sequence[str] | None = None, + include: Sequence[str] | None = None, + namespace_options: dict | None = None, ) -> None: """ This method allows one to automatically add the inputs from another Process to this ProcessSpec. @@ -212,11 +213,11 @@ def expose_inputs( def expose_outputs( self, - process_class: Type['Process'], - namespace: Optional[str] = None, - exclude: Optional[Sequence[str]] = None, - include: Optional[Sequence[str]] = None, - namespace_options: Optional[dict] = None, + process_class: type['Process'], + namespace: str | None = None, + exclude: Sequence[str] | None = None, + include: Sequence[str] | None = None, + namespace_options: dict | None = None, ) -> None: """ This method allows one to automatically add the ouputs from another Process to this ProcessSpec. @@ -243,14 +244,14 @@ def expose_outputs( @staticmethod def _expose_ports( - process_class: Type['Process'], + process_class: type['Process'], source: PortNamespace, destination: PortNamespace, expose_memory: EXPOSED_TYPE, - namespace: Optional[str], - exclude: Optional[Sequence[str]], - include: Optional[Sequence[str]], - namespace_options: Optional[dict] = None, + namespace: str | None, + exclude: Sequence[str] | None, + include: Sequence[str] | None, + namespace_options: dict | None = None, ) -> None: """ Expose ports from a source PortNamespace of the ProcessSpec of a Process class into the destination diff --git a/src/plumpy/process_states.py b/src/plumpy/process_states.py index 9874e616..369567d0 100644 --- a/src/plumpy/process_states.py +++ b/src/plumpy/process_states.py @@ -3,17 +3,13 @@ import sys import traceback +from collections.abc import Awaitable from enum import Enum from types import TracebackType from typing import ( Any, - Awaitable, Callable, ClassVar, - Optional, - Tuple, - Type, - Union, cast, final, ) @@ -88,7 +84,7 @@ def __init__(self, msg_text: str | None): class Command: @classmethod - def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> Self: + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: LoadSaveContext | None = None) -> Self: """ Recreate a :class:`Savable` from a saved state using an optional load context. @@ -110,7 +106,7 @@ def save(self, loader: ObjectLoader | None = None) -> SAVED_STATE_TYPE: @auto_persist('msg') class Kill(Command): - def __init__(self, msg: Optional[MessageType] = None): + def __init__(self, msg: MessageType | None = None): super().__init__() self.msg = msg @@ -123,9 +119,9 @@ class Pause(Command): class Wait(Command): def __init__( self, - continue_fn: Optional[Callable[..., Any]] = None, - msg: Optional[Any] = None, - data: Optional[Any] = None, + continue_fn: Callable[..., Any] | None = None, + msg: Any | None = None, + data: Any | None = None, ): super().__init__() self.continue_fn = continue_fn @@ -160,7 +156,7 @@ def save(self, loader: ObjectLoader | None = None) -> SAVED_STATE_TYPE: @override @classmethod - def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> Self: + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: LoadSaveContext | None = None) -> Self: """ Recreate a :class:`Savable` from a saved state using an optional load context. @@ -274,7 +270,7 @@ class Running: is_terminal: ClassVar[bool] = False def __init__( - self, process: 'st.StateMachine', run_fn: Callable[..., Union[Awaitable[Any], Any]], *args: Any, **kwargs: Any + self, process: 'st.StateMachine', run_fn: Callable[..., Awaitable[Any] | Any], *args: Any, **kwargs: Any ) -> None: assert run_fn is not None self.process = process @@ -293,7 +289,7 @@ def save(self, loader: ObjectLoader | None = None) -> SAVED_STATE_TYPE: return out_state @classmethod - def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> Self: + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: LoadSaveContext | None = None) -> Self: """ Recreate a :class:`Savable` from a saved state using an optional load context. @@ -416,9 +412,9 @@ def __str__(self) -> str: def __init__( self, process: 'st.StateMachine', - done_callback: Optional[Callable[..., Any]], - msg: Optional[str] = None, - data: Optional[Any] = None, + done_callback: Callable[..., Any] | None, + msg: str | None = None, + data: Any | None = None, ) -> None: self.process = process self.done_callback = done_callback @@ -435,7 +431,7 @@ def save(self, loader: ObjectLoader | None = None) -> SAVED_STATE_TYPE: return out_state @classmethod - def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> Self: + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: LoadSaveContext | None = None) -> Self: """ Recreate a :class:`Savable` from a saved state using an optional load context. @@ -515,8 +511,8 @@ class Excepted: def __init__( self, - exception: Optional[BaseException], - traceback: Optional[TracebackType] = None, + exception: BaseException | None, + traceback: TracebackType | None = None, ): """ :param exception: The exception instance @@ -539,7 +535,7 @@ def save(self, loader: ObjectLoader | None = None) -> SAVED_STATE_TYPE: return out_state @classmethod - def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> Self: + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: LoadSaveContext | None = None) -> Self: """ Recreate a :class:`Savable` from a saved state using an optional load context. @@ -564,7 +560,7 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[Loa def get_exc_info( self, - ) -> Tuple[Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]]: + ) -> tuple[type[BaseException] | None, BaseException | None, TracebackType | None]: """ Recreate the exc_info tuple and return it """ @@ -598,7 +594,7 @@ def __init__(self, result: Any, successful: bool) -> None: self.successful = successful @classmethod - def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> Self: + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: LoadSaveContext | None = None) -> Self: """ Recreate a :class:`Savable` from a saved state using an optional load context. @@ -639,14 +635,14 @@ class Killed: is_terminal: ClassVar[bool] = True - def __init__(self, msg: Optional[MessageType]): + def __init__(self, msg: MessageType | None): """ :param msg: Optional kill message """ self.msg = msg @classmethod - def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> Self: + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: LoadSaveContext | None = None) -> Self: """ Recreate a :class:`Savable` from a saved state using an optional load context. diff --git a/src/plumpy/processes.py b/src/plumpy/processes.py index 9d985344..6e3610a2 100644 --- a/src/plumpy/processes.py +++ b/src/plumpy/processes.py @@ -16,22 +16,14 @@ import time import uuid import warnings +from collections.abc import Awaitable, Generator, Sequence from types import TracebackType from typing import ( Any, - Awaitable, Callable, ClassVar, - Dict, - Generator, Hashable, - List, - Optional, - Sequence, - Tuple, - Type, TypeVar, - Union, cast, ) @@ -161,18 +153,18 @@ class Process(StateMachine, metaclass=ProcessStateMachineMeta): _spec_class = ProcessSpec # Default placeholders, will be populated in init() _stepping = False - _pausing: Optional[CancellableAction] = None - _paused: Optional[persistence.SavableFuture] = None - _killing: Optional[CancellableAction] = None - _interrupt_action: Optional[CancellableAction] = None + _pausing: CancellableAction | None = None + _paused: persistence.SavableFuture | None = None + _killing: CancellableAction | None = None + _interrupt_action: CancellableAction | None = None _closed = False - _cleanups: Optional[List[Callable[[], None]]] = None + _cleanups: list[Callable[[], None]] | None = None __called: bool = False _auto_persist: ClassVar[set[str]] @classmethod - def current(cls) -> Optional['Process']: + def current(cls) -> 'Process | None': """ Get the currently running process i.e. the one at the top of the stack @@ -185,7 +177,7 @@ def current(cls) -> Optional['Process']: return None @classmethod - def get_states(cls) -> Sequence[Type[state_machine.State]]: + def get_states(cls) -> Sequence[type[state_machine.State]]: """Return all allowed states of the process.""" state_classes = cls.get_state_classes() return ( @@ -194,7 +186,7 @@ def get_states(cls) -> Sequence[Type[state_machine.State]]: ) @classmethod - def get_state_classes(cls) -> dict[process_states.ProcessState, Type[state_machine.State]]: + def get_state_classes(cls) -> dict[process_states.ProcessState, type[state_machine.State]]: # A mapping of the State constants to the corresponding state class return { process_states.ProcessState.CREATED: process_states.Created, @@ -238,14 +230,14 @@ def define(cls, _spec: ProcessSpec) -> None: cls.__called = True @classmethod - def get_description(cls) -> Dict[str, Any]: + def get_description(cls) -> dict[str, Any]: """ Get a human readable description of what this :class:`Process` does. :return: The description. """ - description: Dict[str, Any] = {} + description: dict[str, Any] = {} if cls.__doc__: description['description'] = cls.__doc__.strip() @@ -260,7 +252,7 @@ def get_description(cls) -> Dict[str, Any]: def recreate_from( cls, saved_state: SAVED_STATE_TYPE, - load_context: Optional[persistence.LoadSaveContext] = None, + load_context: persistence.LoadSaveContext | None = None, ) -> Self: """Recreate a process from a saved state, passing any positional @@ -329,11 +321,11 @@ def recreate_from( def __init__( self, - inputs: Optional[dict] = None, - pid: Optional[PID_TYPE] = None, - logger: Optional[logging.Logger] = None, - loop: Optional[asyncio.AbstractEventLoop] = None, - coordinator: Optional[Coordinator] = None, + inputs: dict | None = None, + pid: PID_TYPE | None = None, + logger: logging.Logger | None = None, + loop: asyncio.AbstractEventLoop | None = None, + coordinator: Coordinator | None = None, ) -> None: """ The signature of the constructor should not be changed by subclassing processes. @@ -354,8 +346,8 @@ def __init__( self._setup_event_hooks() - self._status: Optional[str] = None # May hold a current status message - self._pre_paused_status: Optional[str] = ( + self._status: str | None = None # May hold a current status message + self._pre_paused_status: str | None = ( None # Save status when a pause message replaces it, such that it can be restored ) self._paused = None @@ -363,10 +355,10 @@ def __init__( # Input/output self._raw_inputs = None if inputs is None else utils.AttributesFrozendict(inputs) self._pid = pid - self._parsed_inputs: Optional[utils.AttributesFrozendict] = None - self._outputs: Dict[str, Any] = {} - self._uuid: Optional[uuid.UUID] = None - self._creation_time: Optional[float] = None + self._parsed_inputs: utils.AttributesFrozendict | None = None + self._outputs: dict[str, Any] = {} + self._uuid: uuid.UUID | None = None + self._creation_time: float | None = None # Runtime variables self._future = persistence.SavableFuture(loop=self._loop) @@ -421,7 +413,7 @@ def _setup_event_hooks(self) -> None: cast(state_machine.State, state) ), state_machine.StateEventHook.ENTERED_STATE: lambda _s, _h, from_state: self.on_entered( - cast(Optional[state_machine.State], from_state) + cast(state_machine.State | None, from_state) ), state_machine.StateEventHook.EXITING_STATE: lambda _s, _h, _state: self.on_exiting(), } @@ -429,7 +421,7 @@ def _setup_event_hooks(self) -> None: self.add_state_event_callback(hook, callback) @property - def creation_time(self) -> Optional[float]: + def creation_time(self) -> float | None: """ The creation time of this Process as returned by time.time() when instantiated :return: The creation time @@ -437,27 +429,27 @@ def creation_time(self) -> Optional[float]: return self._creation_time @property - def pid(self) -> Optional[PID_TYPE]: + def pid(self) -> PID_TYPE | None: """Return the pid of the process.""" return self._pid @property - def uuid(self) -> Optional[uuid.UUID]: + def uuid(self) -> uuid.UUID | None: """Return the UUID of the process""" return self._uuid @property - def raw_inputs(self) -> Optional[utils.AttributesFrozendict]: + def raw_inputs(self) -> utils.AttributesFrozendict | None: """The `AttributesFrozendict` of inputs (if not None).""" return self._raw_inputs @property - def inputs(self) -> Optional[utils.AttributesFrozendict]: + def inputs(self) -> utils.AttributesFrozendict | None: """Return the parsed inputs.""" return self._parsed_inputs @property - def outputs(self) -> Dict[str, Any]: + def outputs(self) -> dict[str, Any]: """ Get the current outputs emitted by the Process. These may grow over time as the process runs. @@ -482,11 +474,11 @@ def logger(self) -> logging.Logger: return _LOGGER @property - def status(self) -> Optional[str]: + def status(self) -> str | None: """Return the status massage of the process.""" return self._status - def set_status(self, status: Optional[str]) -> None: + def set_status(self, status: str | None) -> None: """Set the status message of the process.""" self._status = status @@ -505,10 +497,10 @@ def future(self) -> persistence.SavableFuture: @ensure_not_closed def launch( self, - process_class: Type['Process'], - inputs: Optional[dict] = None, - pid: Optional[PID_TYPE] = None, - logger: Optional[logging.Logger] = None, + process_class: type['Process'], + inputs: dict | None = None, + pid: PID_TYPE | None = None, + logger: logging.Logger | None = None, ) -> 'Process': """Start running the nested process. @@ -574,14 +566,14 @@ def killed(self) -> bool: """Return whether the process is killed.""" return self.state_label == process_states.ProcessState.KILLED - def killed_msg(self) -> Optional[MessageType]: + def killed_msg(self) -> MessageType | None: """Return the killed message.""" if isinstance(self.state, process_states.Killed): return self.state.msg raise exceptions.InvalidStateError('Has not been killed') - def exception(self) -> Optional[BaseException]: + def exception(self) -> BaseException | None: """Return exception, if the process is terminated in excepted state.""" if isinstance(self.state, process_states.Excepted): return self.state.exception @@ -628,8 +620,8 @@ def call_soon(self, callback: Callable[..., Any], *args: Any, **kwargs: Any) -> def callback_excepted( self, _callback: Callable[..., Any], - exception: Optional[BaseException], - trace: Optional[TracebackType], + exception: BaseException | None, + trace: TracebackType | None, ) -> None: if self.state_label != process_states.ProcessState.EXCEPTED: self.fail(exception, trace) @@ -741,7 +733,7 @@ def on_entering(self, state: state_machine.State) -> None: elif state_label == process_states.ProcessState.EXCEPTED: call_with_super_check(self.on_except, state.get_exc_info()) # type: ignore - def on_entered(self, from_state: Optional[state_machine.State]) -> None: + def on_entered(self, from_state: state_machine.State | None) -> None: # Map these onto direct functions that the subclass can implement state_label = self.state_label if state_label == process_states.ProcessState.RUNNING: @@ -841,11 +833,11 @@ def on_waiting(self) -> None: self._fire_event(ProcessListener.on_process_waiting) @super_check - def on_pausing(self, msg: Optional[str] = None) -> None: + def on_pausing(self, msg: str | None = None) -> None: """The process is being paused.""" @super_check - def on_paused(self, msg: Optional[str] = None) -> None: + def on_paused(self, msg: str | None = None) -> None: """The process was paused.""" self._pausing = None @@ -890,7 +882,7 @@ def on_finished(self) -> None: self._fire_event(ProcessListener.on_process_finished, self.future().result()) @super_check - def on_except(self, exc_info: Tuple[Any, Exception, TracebackType]) -> None: + def on_except(self, exc_info: tuple[Any, Exception, TracebackType]) -> None: """Entering the EXCEPTED state.""" exception = exc_info[1] exception.__traceback__ = exc_info[2] @@ -909,7 +901,7 @@ def on_excepted(self) -> None: self._fire_event(ProcessListener.on_process_excepted, str(self.future().exception())) @super_check - def on_kill(self, msg: Optional[MessageType]) -> None: + def on_kill(self, msg: MessageType | None) -> None: """Entering the KILLED state.""" if msg is None: msg_txt = '' @@ -979,7 +971,7 @@ def message_receive(self, _comm: Coordinator, msg: MessageType) -> Any: if intent == message.Intent.KILL: return self._schedule_rpc(self.kill, msg_text=msg.get(MESSAGE_TEXT_KEY, None)) if intent == message.Intent.STATUS: - status_info: Dict[str, Any] = {} + status_info: dict[str, Any] = {} self.get_status_info(status_info) return status_info @@ -988,7 +980,7 @@ def message_receive(self, _comm: Coordinator, msg: MessageType) -> Any: def broadcast_receive( self, _comm: Coordinator, msg: MessageType, sender: Any, subject: Any, correlation_id: Any - ) -> Optional[concurrent.futures.Future]: + ) -> concurrent.futures.Future | None: """ Coroutine called when the process receives a message from the communicator @@ -1115,7 +1107,7 @@ def transition_failed( new_state = create_state(self, process_states.ProcessState.EXCEPTED, exception=exception, traceback=trace) self.transition_to(new_state) - def pause(self, msg_text: str | None = None) -> Union[bool, CancellableAction]: + def pause(self, msg_text: str | None = None) -> bool | CancellableAction: """Pause the process. :param msg: an optional message to set as the status. The current status will be saved in the private @@ -1158,7 +1150,7 @@ def pause(self, msg_text: str | None = None) -> Union[bool, CancellableAction]: def _interrupt(state: Interruptable, reason: Exception) -> None: state.interrupt(reason) - def _do_pause(self, state_msg: Optional[MessageType], next_state: Optional[state_machine.State] = None) -> bool: + def _do_pause(self, state_msg: MessageType | None, next_state: state_machine.State | None = None) -> bool: """Carry out the pause procedure, optionally transitioning to the next state first""" try: if next_state is not None: @@ -1204,7 +1196,7 @@ def do_kill(_next_state: state_machine.State) -> Any: raise ValueError(f"Got unknown interruption type '{type(exception)}'") - def _set_interrupt_action(self, new_action: Optional[CancellableAction]) -> None: + def _set_interrupt_action(self, new_action: CancellableAction | None) -> None: """ Set the interrupt action cancelling the current one if it exists :param new_action: The new interrupt action to set @@ -1241,7 +1233,7 @@ def resume(self, *args: Any) -> None: return self.state.resume(*args) # type: ignore @event(to_states=process_states.Excepted) - def fail(self, exception: Optional[BaseException], traceback: Optional[TracebackType]) -> None: + def fail(self, exception: BaseException | None, traceback: TracebackType | None) -> None: """ Fail the process in response to an exception :param exception: The exception that caused the failure @@ -1251,7 +1243,7 @@ def fail(self, exception: Optional[BaseException], traceback: Optional[Traceback new_state = create_state(self, process_states.ProcessState.EXCEPTED, exception=exception, traceback=traceback) self.transition_to(new_state) - def kill(self, msg_text: Optional[str] = None) -> Union[bool, asyncio.Future]: + def kill(self, msg_text: str | None = None) -> bool | asyncio.Future: """ Kill the process :param msg: An optional kill message @@ -1318,7 +1310,7 @@ async def run(self) -> Any: """ @ensure_not_closed - def execute(self) -> Optional[Dict[str, Any]]: + def execute(self) -> dict[str, Any] | None: """ Execute the process. This will return if the process terminates or is paused. diff --git a/src/plumpy/utils.py b/src/plumpy/utils.py index cb75f7bd..dea929d1 100644 --- a/src/plumpy/utils.py +++ b/src/plumpy/utils.py @@ -6,18 +6,11 @@ import logging import types from collections import deque -from collections.abc import Mapping +from collections.abc import Awaitable, Iterator, Mapping, MutableMapping from typing import ( Any, - Awaitable, Callable, Hashable, - Iterator, - List, - MutableMapping, - Optional, - Tuple, - Type, ) from . import lang @@ -43,7 +36,7 @@ class Frozendict(Mapping): def __init__(self, *args: Any, **kwargs: Any) -> None: self._dict = dict(*args, **kwargs) - self._hash: Optional[int] = None + self._hash: int | None = None def __getitem__(self, key: str) -> Any: return self._dict[key] @@ -93,7 +86,7 @@ def __getattr__(self, attr: str) -> Any: errmsg = f"'{self.__class__.__name__}' object has no attribute '{attr}'" raise AttributeError(errmsg) - def __dir__(self) -> List[str]: + def __dir__(self) -> list[str]: """ So we get tab completion. :return: The keys of the dict @@ -131,7 +124,7 @@ def get(self, *args: Any, **kwargs: Any) -> Any: return self.__dict__.get(*args, **kwargs) -def load_function(name: str, instance: Optional[Any] = None) -> Callable[..., Any]: +def load_function(name: str, instance: Any | None = None) -> Callable[..., Any]: obj = load_object(name) if inspect.ismethod(obj): if instance is not None: @@ -161,7 +154,7 @@ def load_object(fullname: str) -> Any: return obj -def load_module(fullname: str) -> Tuple[types.ModuleType, deque]: +def load_module(fullname: str) -> tuple[types.ModuleType, deque]: parts = fullname.split('.') # Try to find the module, working our way from the back @@ -180,7 +173,7 @@ def load_module(fullname: str) -> Tuple[types.ModuleType, deque]: return mod, remainder -def type_check(obj: Any, expected_type: Type) -> None: +def type_check(obj: Any, expected_type: type[Any]) -> None: if not isinstance(obj, expected_type): raise TypeError(f"Got object of type '{type(obj)}' when expecting '{expected_type}'") diff --git a/src/plumpy/workchains.py b/src/plumpy/workchains.py index e6e527f3..88b10d0d 100644 --- a/src/plumpy/workchains.py +++ b/src/plumpy/workchains.py @@ -7,19 +7,11 @@ import inspect import logging import re +from collections.abc import Mapping, MutableSequence, Sequence from typing import ( Any, Callable, - Dict, - List, - Mapping, - MutableSequence, - Optional, Protocol, - Sequence, - Tuple, - Type, - Union, cast, ) @@ -48,9 +40,9 @@ class WorkChainSpec(process_spec.ProcessSpec): def __init__(self) -> None: super().__init__() - self._outline: Optional[Union['_Instruction', '_FunctionCall']] = None + self._outline: _Instruction | _FunctionCall | None = None - def get_description(self) -> Dict[str, str]: + def get_description(self) -> dict[str, str]: description = super().get_description() if self._outline: @@ -58,7 +50,7 @@ def get_description(self) -> Dict[str, str]: return description - def outline(self, *commands: Union['_Instruction', WC_COMMAND_TYPE]) -> None: + def outline(self, *commands: '_Instruction | WC_COMMAND_TYPE') -> None: """ Define the outline that describes this work chain. @@ -71,7 +63,7 @@ def outline(self, *commands: Union['_Instruction', WC_COMMAND_TYPE]) -> None: # There are multiple instructions self._outline = _Block(commands) - def get_outline(self) -> Union['_Instruction', '_FunctionCall']: + def get_outline(self) -> '_Instruction | _FunctionCall': assert self._outline is not None, 'outline not yet loaded' return self._outline @@ -84,12 +76,12 @@ class Waiting(process_states.Waiting): def __init__( self, process: 'WorkChain', - done_callback: Optional[Callable[..., Any]], - msg: Optional[str] = None, - data: Optional[Dict[Union[asyncio.Future, processes.Process], str]] = None, + done_callback: Callable[..., Any] | None, + msg: str | None = None, + data: dict[asyncio.Future | processes.Process, str] | None = None, ) -> None: super().__init__(process, done_callback, msg, data) - self._awaiting: Dict[asyncio.Future, str] = {} + self._awaiting: dict[asyncio.Future, str] = {} for awaitable, key in (data or {}).items(): resolved_awaitable = awaitable.future() if isinstance(awaitable, processes.Process) else awaitable self._awaiting[resolved_awaitable] = key @@ -127,26 +119,26 @@ class WorkChain(processes.Process): CONTEXT = 'CONTEXT' @classmethod - def get_state_classes(cls) -> Dict[process_states.ProcessState, Type[state_machine.State]]: + def get_state_classes(cls) -> dict[process_states.ProcessState, type[state_machine.State]]: states_map = super().get_state_classes() states_map[process_states.ProcessState.WAITING] = Waiting return states_map def __init__( self, - inputs: Optional[dict] = None, - pid: Optional[PID_TYPE] = None, - logger: Optional[logging.Logger] = None, - loop: Optional[asyncio.AbstractEventLoop] = None, - coordinator: Optional[Coordinator] = None, + inputs: dict | None = None, + pid: PID_TYPE | None = None, + logger: logging.Logger | None = None, + loop: asyncio.AbstractEventLoop | None = None, + coordinator: Coordinator | None = None, ) -> None: super().__init__(inputs=inputs, pid=pid, logger=logger, loop=loop, coordinator=coordinator) - self._context: Optional[AttributesDict] = AttributesDict() - self._stepper: Optional[Stepper] = None - self._awaitables: Dict[Union[asyncio.Future, processes.Process], str] = {} + self._context: AttributesDict | None = AttributesDict() + self._stepper: Stepper | None = None + self._awaitables: dict[asyncio.Future | processes.Process, str] = {} @property - def ctx(self) -> Optional[AttributesDict]: + def ctx(self) -> AttributesDict | None: return self._context @classmethod @@ -192,7 +184,7 @@ def save(self, loader: ObjectLoader | None = None) -> SAVED_STATE_TYPE: def recreate_from( cls, saved_state: SAVED_STATE_TYPE, - load_context: Optional[persistence.LoadSaveContext] = None, + load_context: persistence.LoadSaveContext | None = None, ) -> Self: """Recreate a workchain from a saved state, passing any positional @@ -270,7 +262,7 @@ def recreate_from( call_with_super_check(proc.init) return proc - def to_context(self, **kwargs: Union[asyncio.Future, processes.Process]) -> None: + def to_context(self, **kwargs: asyncio.Future | processes.Process) -> None: """ This is a convenience method that provides syntactic sugar, for a user to add multiple intersteps that will assign a certain value @@ -307,7 +299,7 @@ def _do_step(self) -> Any: # XXX: Stepper is also a Saver with `save` method. class Stepper(Protocol): - def step(self) -> Tuple[bool, Any]: + def step(self) -> tuple[bool, Any]: """ Execute on step of the instructions. :return: A 2-tuple with entries: @@ -359,7 +351,7 @@ def save(self, loader: ObjectLoader | None = None) -> SAVED_STATE_TYPE: @classmethod def recreate_from( - cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[persistence.LoadSaveContext] = None + cls, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext | None = None ) -> Self: """ Recreate a :class:`Savable` from a saved state using an optional load context. @@ -377,7 +369,7 @@ def recreate_from( return obj - def step(self) -> Tuple[bool, Any]: + def step(self) -> tuple[bool, Any]: return True, self._fn(self._workchain) def __str__(self) -> str: @@ -420,9 +412,9 @@ def __init__(self, block: Sequence[_Instruction], workchain: 'WorkChain') -> Non self._workchain = workchain self._block = block self._pos: int = 0 - self._child_stepper: Optional[Stepper] = self._block[0].create_stepper(self._workchain) + self._child_stepper: Stepper | None = self._block[0].create_stepper(self._workchain) - def step(self) -> Tuple[bool, Any]: + def step(self) -> tuple[bool, Any]: assert not self.finished() and self._child_stepper is not None, "Can't call step after the block is finished" finished, result = self._child_stepper.step() @@ -450,7 +442,7 @@ def save(self, loader: ObjectLoader | None = None) -> SAVED_STATE_TYPE: return out_state @classmethod - def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> Self: + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: LoadSaveContext | None = None) -> Self: """ Recreate a :class:`Savable` from a saved state using an optional load context. @@ -481,7 +473,7 @@ class _Block(_Instruction, collections.abc.Sequence): """ # XXX: swap workchain and instructions - def __init__(self, instructions: Sequence[Union[_Instruction, WC_COMMAND_TYPE]]) -> None: + def __init__(self, instructions: Sequence[_Instruction | WC_COMMAND_TYPE]) -> None: # Build up the list of commands comms: MutableSequence[_Instruction | _FunctionCall] = [] for instruction in instructions: @@ -493,7 +485,7 @@ def __init__(self, instructions: Sequence[Union[_Instruction, WC_COMMAND_TYPE]]) self._instruction: MutableSequence[_Instruction | _FunctionCall] = comms - def __getitem__(self, index: int) -> Union[_Instruction, _FunctionCall]: # type: ignore + def __getitem__(self, index: int) -> _Instruction | _FunctionCall: # type: ignore return self._instruction[index] def __len__(self) -> int: @@ -506,7 +498,7 @@ def recreate_stepper(self, saved_state: SAVED_STATE_TYPE, workchain: 'WorkChain' load_context = persistence.LoadSaveContext(workchain=workchain, block_instruction=self) return _BlockStepper.recreate_from(saved_state, load_context) - def get_description(self) -> List[str]: + def get_description(self) -> list[str]: return [instruction.get_description() for instruction in self._instruction] @@ -526,7 +518,7 @@ class _Conditional: def __init__(self, parent: _Instruction, predicate: PREDICATE_TYPE, label: str) -> None: self._parent = parent self._predicate = predicate - self._body: Optional[_Block] = None + self._body: _Block | None = None self._label = label @property @@ -569,9 +561,9 @@ def __init__(self, if_instruction: '_If', workchain: 'WorkChain') -> None: self._workchain = workchain self._if_instruction = if_instruction self._pos = 0 - self._child_stepper: Optional[Stepper] = None + self._child_stepper: Stepper | None = None - def step(self) -> Tuple[bool, Any]: + def step(self) -> tuple[bool, Any]: if self.finished(): return True, None @@ -606,7 +598,7 @@ def save(self, loader: ObjectLoader | None = None) -> SAVED_STATE_TYPE: return out_state @classmethod - def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[LoadSaveContext] = None) -> Self: + def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: LoadSaveContext | None = None) -> Self: """ Recreate a :class:`Savable` from a saved state using an optional load context. @@ -637,7 +629,7 @@ def __str__(self) -> str: class _If(_Instruction, collections.abc.Sequence): def __init__(self, condition: PREDICATE_TYPE) -> None: super().__init__() - self._ifs: List[_Conditional] = [_Conditional(self, condition, label=if_.__name__)] + self._ifs: list[_Conditional] = [_Conditional(self, condition, label=if_.__name__)] self._sealed = False def __getitem__(self, idx: int) -> _Conditional: # type: ignore @@ -646,7 +638,7 @@ def __getitem__(self, idx: int) -> _Conditional: # type: ignore def __len__(self) -> int: return len(self._ifs) - def __call__(self, *commands: Union[_Instruction, WC_COMMAND_TYPE]) -> '_If': + def __call__(self, *commands: _Instruction | WC_COMMAND_TYPE) -> '_If': """ This is how the commands for the if(...) body are set :param commands: The commands to run on the original if. @@ -659,7 +651,7 @@ def elif_(self, condition: PREDICATE_TYPE) -> _Conditional: self._ifs.append(_Conditional(self, condition, label=self.elif_.__name__)) return self._ifs[-1] - def else_(self, *commands: Union[_Instruction, WC_COMMAND_TYPE]) -> '_If': + def else_(self, *commands: _Instruction | WC_COMMAND_TYPE) -> '_If': assert not self._sealed # Create a dummy conditional that always returns True cond = _Conditional(self, lambda wf: True, label=self.else_.__name__) @@ -690,9 +682,9 @@ class _WhileStepper: def __init__(self, while_instruction: '_While', workchain: 'WorkChain') -> None: self._workchain = workchain self._while_instruction = while_instruction - self._child_stepper: Optional[_BlockStepper] = None + self._child_stepper: _BlockStepper | None = None - def step(self) -> Tuple[bool, Any]: + def step(self) -> tuple[bool, Any]: # Do we need to check the condition? if self._child_stepper is None: # Should we go into the loop body? @@ -717,7 +709,7 @@ def save(self, loader: ObjectLoader | None = None) -> SAVED_STATE_TYPE: @classmethod def recreate_from( - cls, saved_state: SAVED_STATE_TYPE, load_context: Optional[persistence.LoadSaveContext] = None + cls, saved_state: SAVED_STATE_TYPE, load_context: persistence.LoadSaveContext | None = None ) -> Self: """ Recreate a :class:`Savable` from a saved state using an optional load context. @@ -764,12 +756,12 @@ def recreate_stepper(self, saved_state: SAVED_STATE_TYPE, workchain: 'WorkChain' load_context = persistence.LoadSaveContext(workchain=workchain, while_instruction=self) return _WhileStepper.recreate_from(saved_state, load_context) - def get_description(self) -> Dict[str, Any]: + def get_description(self) -> dict[str, Any]: return {f'while({self.predicate.__name__})': self.body.get_description()} class _PropagateReturn(BaseException): - def __init__(self, exit_code: Optional[EXIT_CODE_TYPE]) -> None: + def __init__(self, exit_code: EXIT_CODE_TYPE | None) -> None: super().__init__() self.exit_code = exit_code @@ -779,7 +771,7 @@ def __init__(self, return_instruction: '_Return', workchain: 'WorkChain') -> Non self._workchain = workchain self._return_instruction = return_instruction - def step(self) -> Tuple[bool, Any]: + def step(self) -> tuple[bool, Any]: """ Raise a _PropagateReturn exception where the value is the exit code set in the _Return instruction upon instantiation @@ -793,7 +785,7 @@ class _Return(_Instruction): outline and cease execution immediately. """ - def __init__(self, exit_code: Optional[EXIT_CODE_TYPE] = None) -> None: + def __init__(self, exit_code: EXIT_CODE_TYPE | None = None) -> None: super().__init__() self._exit_code = exit_code @@ -873,7 +865,7 @@ def while_(condition: PREDICATE_TYPE) -> _While: """ -def _ensure_instruction(command: Any) -> Union[_Instruction, _FunctionCall]: +def _ensure_instruction(command: Any) -> _Instruction | _FunctionCall: # There is only a single instruction if isinstance(command, _Instruction): return command From 493e0179c82992e7f8e7877381705922e2d566c1 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Thu, 23 Jan 2025 18:47:43 +0100 Subject: [PATCH 56/64] remove all __future__ annotations --- src/plumpy/controller.py | 1 - src/plumpy/coordinator.py | 1 - src/plumpy/event_helper.py | 1 - src/plumpy/futures.py | 2 -- src/plumpy/message.py | 2 -- src/plumpy/persistence.py | 3 +-- src/plumpy/process_listener.py | 1 - src/plumpy/process_states.py | 1 - src/plumpy/processes.py | 2 -- src/plumpy/rmq/communications.py | 2 -- src/plumpy/rmq/futures.py | 2 -- src/plumpy/rmq/process_control.py | 2 -- src/plumpy/workchains.py | 1 - 13 files changed, 1 insertion(+), 20 deletions(-) diff --git a/src/plumpy/controller.py b/src/plumpy/controller.py index dcf203dc..8e19a811 100644 --- a/src/plumpy/controller.py +++ b/src/plumpy/controller.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import annotations from collections.abc import Sequence from typing import Any, Protocol diff --git a/src/plumpy/coordinator.py b/src/plumpy/coordinator.py index cffd08b5..6aaea475 100644 --- a/src/plumpy/coordinator.py +++ b/src/plumpy/coordinator.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import annotations import re from typing import TYPE_CHECKING, Any, Callable, Hashable, Protocol diff --git a/src/plumpy/event_helper.py b/src/plumpy/event_helper.py index 1a55939c..6fb64982 100644 --- a/src/plumpy/event_helper.py +++ b/src/plumpy/event_helper.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import annotations import logging from typing import TYPE_CHECKING, Any, Callable, final diff --git a/src/plumpy/futures.py b/src/plumpy/futures.py index e9d7e928..7420f147 100644 --- a/src/plumpy/futures.py +++ b/src/plumpy/futures.py @@ -3,8 +3,6 @@ Module containing future related methods and classes """ -from __future__ import annotations - import asyncio import contextlib from collections.abc import Awaitable, Generator diff --git a/src/plumpy/message.py b/src/plumpy/message.py index 1ed43845..26cad8c5 100644 --- a/src/plumpy/message.py +++ b/src/plumpy/message.py @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- """Module for process level coordination functions and classes""" -from __future__ import annotations - import asyncio import logging from collections.abc import Sequence diff --git a/src/plumpy/persistence.py b/src/plumpy/persistence.py index fb93ca9f..30f7d67a 100644 --- a/src/plumpy/persistence.py +++ b/src/plumpy/persistence.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import annotations import abc import asyncio @@ -393,7 +392,7 @@ def delete_process_checkpoints(self, pid: PID_TYPE) -> None: del self._checkpoints[pid] -def ensure_object_loader(context: 'LoadSaveContext' | None, saved_state: SAVED_STATE_TYPE) -> 'LoadSaveContext': +def ensure_object_loader(context: LoadSaveContext | None, saved_state: SAVED_STATE_TYPE) -> 'LoadSaveContext': """ Given a LoadSaveContext this method will ensure that it has a valid class loader using the following priorities: diff --git a/src/plumpy/process_listener.py b/src/plumpy/process_listener.py index 5a9098da..dc86d658 100644 --- a/src/plumpy/process_listener.py +++ b/src/plumpy/process_listener.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import annotations import abc from typing import TYPE_CHECKING, Any diff --git a/src/plumpy/process_states.py b/src/plumpy/process_states.py index 369567d0..dceb44c4 100644 --- a/src/plumpy/process_states.py +++ b/src/plumpy/process_states.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import annotations import sys import traceback diff --git a/src/plumpy/processes.py b/src/plumpy/processes.py index 6e3610a2..de96131a 100644 --- a/src/plumpy/processes.py +++ b/src/plumpy/processes.py @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- """The main Process module""" -from __future__ import annotations - import abc import asyncio import concurrent.futures diff --git a/src/plumpy/rmq/communications.py b/src/plumpy/rmq/communications.py index 50927557..4cc48aba 100644 --- a/src/plumpy/rmq/communications.py +++ b/src/plumpy/rmq/communications.py @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- """Module for general kiwipy communication methods""" -from __future__ import annotations - import asyncio import functools from typing import TYPE_CHECKING, Any, Callable, Generic, Hashable, Optional, TypeVar, final diff --git a/src/plumpy/rmq/futures.py b/src/plumpy/rmq/futures.py index b0da02db..2edb19b2 100644 --- a/src/plumpy/rmq/futures.py +++ b/src/plumpy/rmq/futures.py @@ -2,8 +2,6 @@ # mypy: disable-error-code="no-untyped-def, no-untyped-call" """Module containing future related methods and classes""" -from __future__ import annotations - import asyncio import concurrent.futures from typing import Any diff --git a/src/plumpy/rmq/process_control.py b/src/plumpy/rmq/process_control.py index 0caf1d7a..15a6c816 100644 --- a/src/plumpy/rmq/process_control.py +++ b/src/plumpy/rmq/process_control.py @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- """Module for process level communication functions and classes""" -from __future__ import annotations - import asyncio from typing import Any, Dict, Hashable, Optional, Sequence, Union diff --git a/src/plumpy/workchains.py b/src/plumpy/workchains.py index 88b10d0d..693b0684 100644 --- a/src/plumpy/workchains.py +++ b/src/plumpy/workchains.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import annotations import abc import asyncio From eca3ae399d8d41cbde4a683827626724d7456ae9 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Tue, 28 Jan 2025 23:28:01 +0100 Subject: [PATCH 57/64] Drop unittest entirely (#24) Thanks! https://github.com/pytest-dev/unittest2pytest --- tests/base/test_statemachine.py | 18 +- tests/base/test_utils.py | 10 +- tests/persistence/test_inmemory.py | 19 +- tests/persistence/test_pickle.py | 19 +- tests/rmq/test_process_control.py | 2 +- tests/test_expose.py | 122 ++++++------ tests/test_lang.py | 34 ++-- tests/test_persistence.py | 17 +- tests/test_port.py | 237 +++++++++++----------- tests/test_process_spec.py | 103 +++++----- tests/test_processes.py | 304 ++++++++++++++--------------- tests/test_waiting_process.py | 12 +- tests/utils.py | 5 - tests/workchain/test_workchains.py | 93 +++++---- 14 files changed, 492 insertions(+), 503 deletions(-) diff --git a/tests/base/test_statemachine.py b/tests/base/test_statemachine.py index 44a084d4..e6e8cb92 100644 --- a/tests/base/test_statemachine.py +++ b/tests/base/test_statemachine.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- import time from typing import final -import unittest from plumpy.base import state_machine from plumpy.exceptions import InvalidStateError +import pytest # Events PLAY = 'Play' @@ -147,27 +147,27 @@ def stop(self): self.transition_to(Stopped(self)) -class TestStateMachine(unittest.TestCase): +class TestStateMachine: def test_basic(self): cd_player = CdPlayer() - self.assertEqual(cd_player.state_label, STOPPED) + assert cd_player.state_label == STOPPED cd_player.play('Eminem - The Real Slim Shady') - self.assertEqual(cd_player.state_label, PLAYING) + assert cd_player.state_label == PLAYING time.sleep(1.0) cd_player.pause() - self.assertEqual(cd_player.state_label, PAUSED) + assert cd_player.state_label == PAUSED cd_player.play() - self.assertEqual(cd_player.state_label, PLAYING) + assert cd_player.state_label == PLAYING - self.assertEqual(cd_player.play(), False) + assert cd_player.play() == False cd_player.stop() - self.assertEqual(cd_player.state_label, STOPPED) + assert cd_player.state_label == STOPPED def test_invalid_event(self): cd_player = CdPlayer() - with self.assertRaises(AssertionError): + with pytest.raises(AssertionError): cd_player.play() diff --git a/tests/base/test_utils.py b/tests/base/test_utils.py index d62b1422..09158ba0 100644 --- a/tests/base/test_utils.py +++ b/tests/base/test_utils.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -import unittest from plumpy.base import utils +import pytest class Root: @@ -23,12 +23,12 @@ def method(self): pass -class TestSuperCheckMixin(unittest.TestCase): +class TestSuperCheckMixin: def test_do_call(self): DoCall().do() def test_dont_call(self): - with self.assertRaises(AssertionError): + with pytest.raises(AssertionError): DontCall().do() def dont_call_middle(self): @@ -36,9 +36,9 @@ class ThirdChild(DontCall): def method(self): super().method() - with self.assertRaises(AssertionError): + with pytest.raises(AssertionError): ThirdChild.do() def test_skip_check_call(self): - with self.assertRaises(AssertionError): + with pytest.raises(AssertionError): DoCall().method() diff --git a/tests/persistence/test_inmemory.py b/tests/persistence/test_inmemory.py index 9e3141de..93b48972 100644 --- a/tests/persistence/test_inmemory.py +++ b/tests/persistence/test_inmemory.py @@ -1,12 +1,11 @@ # -*- coding: utf-8 -*- -import unittest import plumpy from ..utils import ProcessWithCheckpoint -class TestInMemoryPersister(unittest.TestCase): +class TestInMemoryPersister: def test_save_load_roundtrip(self): """ Test the plumpy.PicklePersister by taking a dummpy process, saving a checkpoint @@ -33,7 +32,7 @@ def test_get_checkpoints_without_tags(self): retrieved_checkpoints = persister.get_checkpoints() - self.assertSetEqual(set(retrieved_checkpoints), set(checkpoints)) + assert set(retrieved_checkpoints) == set(checkpoints) def test_get_checkpoints_with_tags(self): """ """ @@ -53,7 +52,7 @@ def test_get_checkpoints_with_tags(self): retrieved_checkpoints = persister.get_checkpoints() - self.assertSetEqual(set(retrieved_checkpoints), set(checkpoints)) + assert set(retrieved_checkpoints) == set(checkpoints) def test_get_process_checkpoints(self): """ """ @@ -73,7 +72,7 @@ def test_get_process_checkpoints(self): retrieved_checkpoints = persister.get_process_checkpoints(process_a.pid) - self.assertSetEqual(set(retrieved_checkpoints), set(checkpoints)) + assert set(retrieved_checkpoints) == set(checkpoints) def test_delete_process_checkpoints(self): """ """ @@ -92,14 +91,14 @@ def test_delete_process_checkpoints(self): checkpoints = [checkpoint_a1, checkpoint_a2] retrieved_checkpoints = persister.get_process_checkpoints(process_a.pid) - self.assertSetEqual(set(retrieved_checkpoints), set(checkpoints)) + assert set(retrieved_checkpoints) == set(checkpoints) persister.delete_process_checkpoints(process_a.pid) checkpoints = [] retrieved_checkpoints = persister.get_process_checkpoints(process_a.pid) - self.assertSetEqual(set(retrieved_checkpoints), set(checkpoints)) + assert set(retrieved_checkpoints) == set(checkpoints) def test_delete_checkpoint(self): """ """ @@ -122,18 +121,18 @@ def test_delete_checkpoint(self): checkpoints = [checkpoint_a1, checkpoint_a2, checkpoint_b1, checkpoint_b2] retrieved_checkpoints = persister.get_checkpoints() - self.assertSetEqual(set(retrieved_checkpoints), set(checkpoints)) + assert set(retrieved_checkpoints) == set(checkpoints) persister.delete_checkpoint(process_a.pid, tag='2') checkpoints = [checkpoint_a1, checkpoint_b1, checkpoint_b2] retrieved_checkpoints = persister.get_checkpoints() - self.assertSetEqual(set(retrieved_checkpoints), set(checkpoints)) + assert set(retrieved_checkpoints) == set(checkpoints) persister.delete_checkpoint(process_b.pid, tag='1') checkpoints = [checkpoint_a1, checkpoint_b2] retrieved_checkpoints = persister.get_checkpoints() - self.assertSetEqual(set(retrieved_checkpoints), set(checkpoints)) + assert set(retrieved_checkpoints) == set(checkpoints) diff --git a/tests/persistence/test_pickle.py b/tests/persistence/test_pickle.py index da4ede51..28ea906b 100644 --- a/tests/persistence/test_pickle.py +++ b/tests/persistence/test_pickle.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- import tempfile -import unittest if getattr(tempfile, 'TemporaryDirectory', None) is None: from backports import tempfile @@ -10,7 +9,7 @@ from ..utils import ProcessWithCheckpoint -class TestPicklePersister(unittest.TestCase): +class TestPicklePersister: def test_save_load_roundtrip(self): """ Test the plumpy.PicklePersister by taking a dummpy process, saving a checkpoint @@ -39,7 +38,7 @@ def test_get_checkpoints_without_tags(self): retrieved_checkpoints = persister.get_checkpoints() - self.assertSetEqual(set(retrieved_checkpoints), set(checkpoints)) + assert set(retrieved_checkpoints) == set(checkpoints) def test_get_checkpoints_with_tags(self): """ """ @@ -60,7 +59,7 @@ def test_get_checkpoints_with_tags(self): retrieved_checkpoints = persister.get_checkpoints() - self.assertSetEqual(set(retrieved_checkpoints), set(checkpoints)) + assert set(retrieved_checkpoints) == set(checkpoints) def test_get_process_checkpoints(self): """ """ @@ -81,7 +80,7 @@ def test_get_process_checkpoints(self): retrieved_checkpoints = persister.get_process_checkpoints(process_a.pid) - self.assertSetEqual(set(retrieved_checkpoints), set(checkpoints)) + assert set(retrieved_checkpoints) == set(checkpoints) def test_delete_process_checkpoints(self): """ """ @@ -101,14 +100,14 @@ def test_delete_process_checkpoints(self): checkpoints = [checkpoint_a1, checkpoint_a2] retrieved_checkpoints = persister.get_process_checkpoints(process_a.pid) - self.assertSetEqual(set(retrieved_checkpoints), set(checkpoints)) + assert set(retrieved_checkpoints) == set(checkpoints) persister.delete_process_checkpoints(process_a.pid) checkpoints = [] retrieved_checkpoints = persister.get_process_checkpoints(process_a.pid) - self.assertSetEqual(set(retrieved_checkpoints), set(checkpoints)) + assert set(retrieved_checkpoints) == set(checkpoints) def test_delete_checkpoint(self): """ """ @@ -132,18 +131,18 @@ def test_delete_checkpoint(self): checkpoints = [checkpoint_a1, checkpoint_a2, checkpoint_b1, checkpoint_b2] retrieved_checkpoints = persister.get_checkpoints() - self.assertSetEqual(set(retrieved_checkpoints), set(checkpoints)) + assert set(retrieved_checkpoints) == set(checkpoints) persister.delete_checkpoint(process_a.pid, tag='2') checkpoints = [checkpoint_a1, checkpoint_b1, checkpoint_b2] retrieved_checkpoints = persister.get_checkpoints() - self.assertSetEqual(set(retrieved_checkpoints), set(checkpoints)) + assert set(retrieved_checkpoints) == set(checkpoints) persister.delete_checkpoint(process_b.pid, tag='1') checkpoints = [checkpoint_a1, checkpoint_b2] retrieved_checkpoints = persister.get_checkpoints() - self.assertSetEqual(set(retrieved_checkpoints), set(checkpoints)) + assert set(retrieved_checkpoints) == set(checkpoints) diff --git a/tests/rmq/test_process_control.py b/tests/rmq/test_process_control.py index 7c3b431c..2627035e 100644 --- a/tests/rmq/test_process_control.py +++ b/tests/rmq/test_process_control.py @@ -118,7 +118,7 @@ def on_broadcast_receive(**msg): expected_subjects.append(f'state_changed.{from_state}.{state.value}') for i, message in enumerate(messages): - self.assertEqual(message['subject'], expected_subjects[i]) + assert message['subject'] == expected_subjects[i] class TestRemoteProcessThreadController: diff --git a/tests/test_expose.py b/tests/test_expose.py index c5e6014c..43b26d34 100644 --- a/tests/test_expose.py +++ b/tests/test_expose.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- -import unittest from plumpy.ports import PortNamespace from plumpy.process_spec import ProcessSpec from plumpy.processes import Process from .utils import NewLoopProcess +import pytest def validator_function(input, port): @@ -44,7 +44,7 @@ def define(cls, spec): spec.inputs.valid_type = int -class TestExposeProcess(unittest.TestCase): +class TestExposeProcess: def check_ports(self, process, namespace, expected_port_names): """Check the port namespace of a given process inputs spec for existence of set of expected port names.""" port_namespace = process.spec().inputs @@ -52,7 +52,7 @@ def check_ports(self, process, namespace, expected_port_names): if namespace is not None: port_namespace = process.spec().inputs.get_port(namespace) - self.assertEqual(set(port_namespace.keys()), set(expected_port_names)) + assert set(port_namespace.keys()) == set(expected_port_names) def check_namespace_properties(self, process_left, namespace_left, process_right, namespace_right): """Check that all properties, with exception of ports, of two port namespaces are equal.""" @@ -65,7 +65,7 @@ def check_namespace_properties(self, process_left, namespace_left, process_right left_dict = {k: v for k, v in port_namespace_left.__dict__.items() if k != '_ports'} right_dict = {k: v for k, v in port_namespace_right.__dict__.items() if k != '_ports'} - self.assertEqual(left_dict, right_dict) + assert left_dict == right_dict def test_expose_dynamic(self): """Test that exposing a dynamic namespace remains dynamic.""" @@ -82,59 +82,59 @@ def define(cls, spec): super(Upper, cls).define(spec) spec.expose_inputs(Lower) - self.assertTrue(Lower.spec().inputs['foo'].dynamic) - self.assertTrue(Upper.spec().inputs['foo'].dynamic) + assert Lower.spec().inputs['foo'].dynamic + assert Upper.spec().inputs['foo'].dynamic def test_expose_nested_namespace(self): """Test that expose_inputs can create nested namespaces while maintaining own ports.""" inputs = ExposeProcess.spec().inputs # Verify that the nested namespaces are present - self.assertTrue('base' in inputs) - self.assertTrue('name' in inputs['base']) - self.assertTrue('space' in inputs['base']['name']) + assert 'base' in inputs + assert 'name' in inputs['base'] + assert 'space' in inputs['base']['name'] exposed_inputs = inputs.get_port('base.name.space') - self.assertTrue(isinstance(exposed_inputs, PortNamespace)) + assert isinstance(exposed_inputs, PortNamespace) # Verify that own ports are left untouched (should be three ports, 'c', 'd' and 'base') - self.assertEqual(len(inputs), 3) - self.assertTrue('c' in inputs) - self.assertTrue('d' in inputs) - self.assertEqual(inputs['c'].default, 1) - self.assertEqual(inputs['d'].default, 2) + assert len(inputs) == 3 + assert 'c' in inputs + assert 'd' in inputs + assert inputs['c'].default == 1 + assert inputs['d'].default == 2 def test_expose_ports(self): """Test that the exposed ports are present and properly deepcopied.""" exposed_inputs = ExposeProcess.spec().inputs.get_port('base.name.space') - self.assertEqual(len(exposed_inputs), 2) - self.assertTrue('a' in exposed_inputs) - self.assertTrue('b' in exposed_inputs) - self.assertEqual(exposed_inputs['a'].default, 'a') - self.assertEqual(exposed_inputs['b'].default, 'b') + assert len(exposed_inputs) == 2 + assert 'a' in exposed_inputs + assert 'b' in exposed_inputs + assert exposed_inputs['a'].default == 'a' + assert exposed_inputs['b'].default == 'b' # Change the default of base process port and verify they don't change the exposed port BaseProcess.spec().inputs['a'].default = 'c' - self.assertEqual(BaseProcess.spec().inputs['a'].default, 'c') - self.assertEqual(exposed_inputs['a'].default, 'a') + assert BaseProcess.spec().inputs['a'].default == 'c' + assert exposed_inputs['a'].default == 'a' def test_expose_attributes(self): """Test that the attributes of the exposed PortNamespace are maintained and properly deepcopied.""" inputs = ExposeProcess.spec().inputs exposed_inputs = ExposeProcess.spec().inputs.get_port('base.name.space') - self.assertEqual(str, BaseProcess.spec().inputs.valid_type) - self.assertEqual(str, exposed_inputs.valid_type) - self.assertEqual(int, inputs.valid_type) + assert str == BaseProcess.spec().inputs.valid_type + assert str == exposed_inputs.valid_type + assert int == inputs.valid_type # Now change the valid type of the BaseProcess inputs and verify it does not affect ExposeProcess BaseProcess.spec().inputs.valid_type = float - self.assertEqual(BaseProcess.spec().inputs.valid_type, float) - self.assertEqual(exposed_inputs.valid_type, str) - self.assertEqual(inputs.valid_type, int) + assert BaseProcess.spec().inputs.valid_type == float + assert exposed_inputs.valid_type == str + assert inputs.valid_type == int def test_expose_exclude(self): """Test that the exclude argument of exposed_inputs works correctly and excludes ports from being absorbed.""" @@ -149,8 +149,8 @@ def define(cls, spec): inputs = ExcludeProcess.spec().inputs - self.assertEqual(len(inputs), 3) - self.assertTrue('a' not in inputs) + assert len(inputs) == 3 + assert 'a' not in inputs def test_expose_include(self): """Test that the include argument of exposed_inputs works correctly and includes only specified ports.""" @@ -165,8 +165,8 @@ def define(cls, spec): inputs = ExcludeProcess.spec().inputs - self.assertEqual(len(inputs), 3) - self.assertTrue('a' not in inputs) + assert len(inputs) == 3 + assert 'a' not in inputs def test_expose_exclude_include_mutually_exclusive(self): """Test that passing both exclude and include raises.""" @@ -179,7 +179,7 @@ def define(cls, spec): spec.input('c', valid_type=int, default=1) spec.input('d', valid_type=int, default=2) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): ExcludeProcess.spec() def test_expose_ports_top_level(self): @@ -217,17 +217,17 @@ def validator_function(input, port): ) # Verify that all the ports are there - self.assertIn('a', ParentProcessSpec.inputs) - self.assertIn('b', ParentProcessSpec.inputs) - self.assertIn('c', ParentProcessSpec.inputs) + assert 'a' in ParentProcessSpec.inputs + assert 'b' in ParentProcessSpec.inputs + assert 'c' in ParentProcessSpec.inputs # Verify that all the port namespace attributes are copied over - self.assertEqual(ParentProcessSpec.inputs.validator, validator_function) - self.assertEqual(ParentProcessSpec.inputs.valid_type, bool) - self.assertEqual(ParentProcessSpec.inputs.required, False) - self.assertEqual(ParentProcessSpec.inputs.dynamic, True) - self.assertEqual(ParentProcessSpec.inputs.default, True) - self.assertEqual(ParentProcessSpec.inputs.help, 'testing') + assert ParentProcessSpec.inputs.validator == validator_function + assert ParentProcessSpec.inputs.valid_type == bool + assert ParentProcessSpec.inputs.required == False + assert ParentProcessSpec.inputs.dynamic == True + assert ParentProcessSpec.inputs.default == True + assert ParentProcessSpec.inputs.help == 'testing' def test_expose_ports_top_level_override(self): """ @@ -272,17 +272,17 @@ def validator_function(input, port): ) # Verify that all the ports are there - self.assertIn('a', ParentProcessSpec.inputs) - self.assertIn('b', ParentProcessSpec.inputs) - self.assertIn('c', ParentProcessSpec.inputs) + assert 'a' in ParentProcessSpec.inputs + assert 'b' in ParentProcessSpec.inputs + assert 'c' in ParentProcessSpec.inputs # Verify that all the port namespace attributes correspond to the values passed in the namespace_options - self.assertEqual(ParentProcessSpec.inputs.validator, None) - self.assertEqual(ParentProcessSpec.inputs.valid_type, None) - self.assertEqual(ParentProcessSpec.inputs.required, True) - self.assertEqual(ParentProcessSpec.inputs.dynamic, False) - self.assertEqual(ParentProcessSpec.inputs.default, None) - self.assertEqual(ParentProcessSpec.inputs.help, None) + assert ParentProcessSpec.inputs.validator == None + assert ParentProcessSpec.inputs.valid_type == None + assert ParentProcessSpec.inputs.required == True + assert ParentProcessSpec.inputs.dynamic == False + assert ParentProcessSpec.inputs.default == None + assert ParentProcessSpec.inputs.help == None def test_expose_ports_namespace(self): """ @@ -319,17 +319,17 @@ def validator_function(input, port): ) # Verify that all the ports are there - self.assertIn('a', ParentProcessSpec.inputs['namespace']) - self.assertIn('b', ParentProcessSpec.inputs['namespace']) - self.assertIn('c', ParentProcessSpec.inputs) + assert 'a' in ParentProcessSpec.inputs['namespace'] + assert 'b' in ParentProcessSpec.inputs['namespace'] + assert 'c' in ParentProcessSpec.inputs # Verify that all the port namespace attributes are copied over - self.assertEqual(ParentProcessSpec.inputs['namespace'].validator, validator_function) - self.assertEqual(ParentProcessSpec.inputs['namespace'].valid_type, bool) - self.assertEqual(ParentProcessSpec.inputs['namespace'].required, False) - self.assertEqual(ParentProcessSpec.inputs['namespace'].dynamic, True) - self.assertEqual(ParentProcessSpec.inputs['namespace'].default, True) - self.assertEqual(ParentProcessSpec.inputs['namespace'].help, 'testing') + assert ParentProcessSpec.inputs['namespace'].validator == validator_function + assert ParentProcessSpec.inputs['namespace'].valid_type == bool + assert ParentProcessSpec.inputs['namespace'].required == False + assert ParentProcessSpec.inputs['namespace'].dynamic == True + assert ParentProcessSpec.inputs['namespace'].default == True + assert ParentProcessSpec.inputs['namespace'].help == 'testing' def test_expose_ports_namespace_options_non_existent(self): """ @@ -339,7 +339,7 @@ def test_expose_ports_namespace_options_non_existent(self): ChildProcessSpec = ProcessSpec() # noqa: N806 ParentProcessSpec = ProcessSpec() # noqa: N806 - with self.assertRaises(ValueError): + with pytest.raises(ValueError): ParentProcessSpec._expose_ports( process_class=None, source=ChildProcessSpec.inputs, diff --git a/tests/test_lang.py b/tests/test_lang.py index a55af31a..2d515cc4 100644 --- a/tests/test_lang.py +++ b/tests/test_lang.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from plumpy.lang import override, protected -from .utils import TestCase +import pytest class A: @@ -38,9 +38,9 @@ def testC(self): # noqa: N802 self.protected_property -class TestProtected(TestCase): +class TestProtected: def test_free_function(self): - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): @protected(check=False) def some_func(): @@ -55,24 +55,24 @@ def test_correct_usage(self): def test_incorrect_usage(self): # I shouldn't be able to call the protected function from any of them a = A() - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): a.protected_fn() - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): a.protected_property b = B() - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): b.protected_fn() - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): b.protected_property c = C() - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): c.protected_fn() - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): c.protected_property - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): class TestWrongDecoratorOrder: @protected(check=True) @@ -86,9 +86,9 @@ def test(self): pass -class TestOverride(TestCase): +class TestOverride: def test_free_function(self): - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): @override(check=False) def some_func(): @@ -100,7 +100,7 @@ class Derived(Superclass): def test(self): return True - self.assertTrue(Derived().test()) + assert Derived().test() class Middle(Superclass): pass @@ -110,7 +110,7 @@ class Next(Middle): def test(self): return True - self.assertTrue(Next().test()) + assert Next().test() def test_incorrect_usage(self): class Derived: @@ -118,10 +118,10 @@ class Derived: def test(self): pass - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): Derived().test() - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): class TestWrongDecoratorOrder(Superclass): @override(check=True) @@ -176,7 +176,7 @@ def test(self): # def test(self): # self._c_prime_called = True -# class TestCallSuper(TestCase): +# class TestCallSuper: # def test_one_up(self): # b = B() # b.test() diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 5998817f..4a5e033b 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- import asyncio from typing import Any -import unittest import yaml @@ -102,7 +101,7 @@ def identify_object(self, obj: Any) -> str: return super().identify_object(obj) -class TestSavable(unittest.TestCase): +class TestSavable: def test_empty_savable(self): self._save_round_trip(SaveEmpty()) @@ -127,7 +126,7 @@ def _save_round_trip(self, savable): saved_state1 = savable.save() loaded = savable.recreate_from(saved_state1) saved_state2 = loaded.save() - self.assertDictEqual(saved_state1, saved_state2) + assert saved_state1 == saved_state2 def _save_round_trip_with_loader(self, savable): """ @@ -144,11 +143,11 @@ def _save_round_trip_with_loader(self, savable): loaded = savable.recreate_from(saved_state1) saved_state2 = loaded.save(object_loader) saved_state3 = loaded.save() - self.assertDictEqual(saved_state1, saved_state2) - self.assertNotEqual(saved_state1, saved_state3) + assert saved_state1 == saved_state2 + assert saved_state1 != saved_state3 -class TestBundle(unittest.TestCase): +class TestBundle: def test_bundle_load_context(self): """Check that the loop from the load context is used""" loop1 = asyncio.get_event_loop() @@ -157,12 +156,12 @@ def test_bundle_load_context(self): loop2 = asyncio.new_event_loop() proc2 = bundle.unbundle(plumpy.LoadSaveContext(loop=loop2)) - self.assertIs(loop2, proc2.loop) + assert loop2 is proc2.loop def test_bundle_yaml(self): bundle = plumpy.Bundle(Save1()) represent = yaml.dump({'bundle': bundle}) bundle_loaded = yaml.load(represent, Loader=yaml.Loader)['bundle'] - self.assertIsInstance(bundle_loaded, plumpy.Bundle) - self.assertDictEqual(bundle_loaded, Save1().save()) + assert isinstance(bundle_loaded, plumpy.Bundle) + assert bundle_loaded == Save1().save() diff --git a/tests/test_port.py b/tests/test_port.py index da483e81..d2f88c44 100644 --- a/tests/test_port.py +++ b/tests/test_port.py @@ -2,22 +2,21 @@ import types from plumpy.ports import UNSPECIFIED, InputPort, OutputPort, Port, PortNamespace +import pytest -from .utils import TestCase - -class TestPort(TestCase): +class TestPort: def test_required(self): spec = Port('required_value', required=True) - self.assertIsNotNone(spec.validate(UNSPECIFIED)) - self.assertIsNone(spec.validate(5)) + assert spec.validate(UNSPECIFIED) is not None + assert spec.validate(5) is None def test_validate(self): spec = Port('required_value', valid_type=int) - self.assertIsNone(spec.validate(5)) - self.assertIsNotNone(spec.validate('a')) + assert spec.validate(5) is None + assert spec.validate('a') is not None def test_validator(self): def validate(value, port): @@ -28,8 +27,8 @@ def validate(value, port): spec = Port('valid_with_validator', validator=validate) - self.assertIsNone(spec.validate(5)) - self.assertIsNotNone(spec.validate('s')) + assert spec.validate(5) is None + assert spec.validate('s') is not None def test_validator_not_required(self): """Verify that a validator is not called if no value is specified for a port that is not required.""" @@ -39,16 +38,16 @@ def validate(value, port): spec = Port('valid_with_validator', validator=validate, required=False) - self.assertIsNone(spec.validate(UNSPECIFIED)) + assert spec.validate(UNSPECIFIED) is None -class TestInputPort(TestCase): +class TestInputPort: def test_default(self): """Test the default value property for the InputPort.""" port = InputPort('test', default=5) - self.assertEqual(port.default, 5) + assert port.default == 5 - with self.assertRaises(ValueError): + with pytest.raises(ValueError): InputPort('test', default=4, valid_type=str) def test_validator(self): @@ -60,8 +59,8 @@ def integer_validator(value, port): return 'Only positive integers allowed' port = InputPort('test', validator=integer_validator) - self.assertIsNone(port.validate(5)) - self.assertIsNotNone(port.validate(-5)) + assert port.validate(5) is None + assert port.validate(-5) is not None def test_lambda_default(self): """Test a default with a lambda.""" @@ -73,8 +72,8 @@ def test_lambda_default(self): port = InputPort('test', default=lambda: 5) - self.assertIsNone(port.validate(UNSPECIFIED)) - self.assertIsNone(port.validate(3)) + assert port.validate(UNSPECIFIED) is None + assert port.validate(3) is None # Testing that passing an actual lambda as a value is alos possible port = InputPort('test', valid_type=(types.FunctionType, int), default=lambda: 5) @@ -82,10 +81,10 @@ def test_lambda_default(self): def some_lambda(): return 'string' - self.assertIsNone(port.validate(some_lambda)) + assert port.validate(some_lambda) is None -class TestOutputPort(TestCase): +class TestOutputPort: def test_default(self): """ Test the default value property for the InputPort @@ -99,53 +98,50 @@ def validator(value, port): assert isinstance(port, Port) port = OutputPort(name, valid_type=valid_type, help=help_string, required=required, validator=validator) - self.assertEqual(port.name, name) - self.assertEqual(port.valid_type, valid_type) - self.assertEqual(port.help, help_string) - self.assertEqual(port.required, required) - self.assertEqual(port.validator, validator) + assert port.name == name + assert port.valid_type == valid_type + assert port.help == help_string + assert port.required == required + assert port.validator == validator -class TestPortNamespace(TestCase): +class TestPortNamespace: BASE_PORT_NAME = 'port' BASE_PORT_NAMESPACE_NAME = 'port' - def setUp(self): - self.port = InputPort(self.BASE_PORT_NAME) - self.port_namespace = PortNamespace(self.BASE_PORT_NAMESPACE_NAME) - def test_port_namespace(self): """ Test basic properties and methods of an empty PortNamespace """ - self.assertEqual(self.port_namespace.name, self.BASE_PORT_NAMESPACE_NAME) - self.assertEqual(len(self.port_namespace), 0) + port_namespace = PortNamespace(self.BASE_PORT_NAMESPACE_NAME) + assert port_namespace.name == self.BASE_PORT_NAMESPACE_NAME + assert len(port_namespace) == 0 - with self.assertRaises(TypeError): - self.port_namespace['key'] = 5 + with pytest.raises(TypeError): + port_namespace['key'] = 5 - with self.assertRaises(KeyError): - self.port_namespace['non_existent'] + with pytest.raises(KeyError): + port_namespace['non_existent'] def test_port_namespace_valid_type_and_dynamic(self): """Test that `dynamic` and `valid_type` attributes defined through constructor are properly set.""" # Setting `dynamic=True` should leave `valid_type` untouched port_namespace = PortNamespace(dynamic=True) - self.assertEqual(port_namespace.valid_type, None) - self.assertEqual(port_namespace.dynamic, True) + assert port_namespace.valid_type == None + assert port_namespace.dynamic == True # Setting `valid_type` to not `None` should automatically set `dynamic=True` port_namespace = PortNamespace(valid_type=int) - self.assertEqual(port_namespace.valid_type, int) - self.assertEqual(port_namespace.dynamic, True) + assert port_namespace.valid_type == int + assert port_namespace.dynamic == True # The following does not make sense, but the constructor cannot raise a warning because it cannot detect whether # the `dynamic=False` is explicitly set by the user or is merely the default. In any case, the `dynamic=False` # is simply ignored in this case port_namespace = PortNamespace(dynamic=False, valid_type=int) - self.assertEqual(port_namespace.valid_type, int) - self.assertEqual(port_namespace.dynamic, True) + assert port_namespace.valid_type == int + assert port_namespace.dynamic == True def test_port_namespace_validation(self): """Test validate method of a `PortNamespace`.""" @@ -155,56 +151,59 @@ def validator(port_values, port): if port_values['explicit'] < 0 or port_values['dynamic'] < 0: return 'Only positive integers allowed' - self.port_namespace['explicit'] = InputPort('explicit', valid_type=int) - self.port_namespace.validator = validator - self.port_namespace.valid_type = int + port_namespace = PortNamespace(self.BASE_PORT_NAMESPACE_NAME) + port_namespace['explicit'] = InputPort('explicit', valid_type=int) + port_namespace.validator = validator + port_namespace.valid_type = int # The explicit ports will be validated first before the namespace validator is called. - self.assertIsNone(self.port_namespace.validate({'explicit': 1, 'dynamic': 5})) - self.assertIsNotNone(self.port_namespace.validate({'dynamic': -5})) + assert port_namespace.validate({'explicit': 1, 'dynamic': 5}) is None + assert port_namespace.validate({'dynamic': -5}) is not None # Validator should not be called if the namespace is not required and no value is specified for the namespace - self.port_namespace.required = False - self.assertIsNone(self.port_namespace.validate()) + port_namespace.required = False + assert port_namespace.validate() is None def test_port_namespace_dynamic(self): """ Setting a valid type for a PortNamespace should automatically make it dynamic """ - self.assertFalse(self.port_namespace.dynamic) + port_namespace = PortNamespace(self.BASE_PORT_NAMESPACE_NAME) + assert not port_namespace.dynamic - self.port_namespace.valid_type = (str, int) + port_namespace.valid_type = (str, int) - self.assertTrue(self.port_namespace.dynamic) - self.assertEqual(self.port_namespace.valid_type, (str, int)) + assert port_namespace.dynamic + assert port_namespace.valid_type == (str, int) def test_port_namespace_get_port(self): """ Test get_port of PortNamespace will retrieve nested PortNamespaces and Ports as long as they and all intermediate nested PortNamespaces exist """ - with self.assertRaises(TypeError): - self.port_namespace.get_port() + port = InputPort(self.BASE_PORT_NAME) + port_namespace = PortNamespace(self.BASE_PORT_NAMESPACE_NAME) + with pytest.raises(TypeError): + port_namespace.get_port() - with self.assertRaises(ValueError): - self.port_namespace.get_port(5) + with pytest.raises(ValueError): + port_namespace.get_port(5) - with self.assertRaises(ValueError): - self.port_namespace.get_port('sub') + with pytest.raises(ValueError): + port_namespace.get_port('sub') - port_namespace_sub = self.port_namespace.create_port_namespace('sub') - self.assertEqual(self.port_namespace.get_port('sub'), port_namespace_sub) + port_namespace_sub = port_namespace.create_port_namespace('sub') + assert port_namespace.get_port('sub') == port_namespace_sub - with self.assertRaises(ValueError): - self.port_namespace.get_port('sub.name.space') + with pytest.raises(ValueError): + port_namespace.get_port('sub.name.space') - port_namespace_sub = self.port_namespace.create_port_namespace('sub.name.space') - self.assertEqual(self.port_namespace.get_port('sub.name.space'), port_namespace_sub) + port_namespace_sub = port_namespace.create_port_namespace('sub.name.space') + assert port_namespace.get_port('sub.name.space') == port_namespace_sub # Add Port into subnamespace and try to get it in one go from top level port namespace - port_namespace_sub[self.BASE_PORT_NAME] = self.port - port = self.port_namespace.get_port('sub.name.space.' + self.BASE_PORT_NAME) - self.assertEqual(port, self.port) + port_namespace_sub[self.BASE_PORT_NAME] = port + assert port_namespace.get_port('sub.name.space.' + self.BASE_PORT_NAME) == port def test_port_namespace_get_port_dynamic(self): """Test ``get_port`` with the ``create_dynamically=True`` keyword. @@ -231,41 +230,44 @@ def test_port_namespace_create_port_namespace(self): """ Test the create_port_namespace function of the PortNamespace class """ - with self.assertRaises(TypeError): - self.port_namespace.create_port_namespace() + port = InputPort(self.BASE_PORT_NAME) + port_namespace = PortNamespace(self.BASE_PORT_NAMESPACE_NAME) + with pytest.raises(TypeError): + port_namespace.create_port_namespace() - with self.assertRaises(ValueError): - self.port_namespace.create_port_namespace(5) + with pytest.raises(ValueError): + port_namespace.create_port_namespace(5) - port_namespace_sub = self.port_namespace.create_port_namespace('sub') - port_namespace_sub = self.port_namespace.create_port_namespace('some.nested.sub.space') + port_namespace_sub = port_namespace.create_port_namespace('sub') + port_namespace_sub = port_namespace.create_port_namespace('some.nested.sub.space') # Existing intermediate nested spaces should be no problem - port_namespace_sub = self.port_namespace.create_port_namespace('sub.nested.space') + port_namespace_sub = port_namespace.create_port_namespace('sub.nested.space') # Overriding Port is not possible though - port_namespace_sub[self.BASE_PORT_NAME] = self.port + port_namespace_sub[self.BASE_PORT_NAME] = port - with self.assertRaises(ValueError): - self.port_namespace.create_port_namespace('sub.nested.space.' + self.BASE_PORT_NAME + '.further') + with pytest.raises(ValueError): + port_namespace.create_port_namespace('sub.nested.space.' + self.BASE_PORT_NAME + '.further') def test_port_namespace_set_valid_type(self): """Setting a valid type for a PortNamespace should automatically mark it as dynamic.""" - self.assertFalse(self.port_namespace.dynamic) - self.assertIsNone(self.port_namespace.valid_type) + port_namespace = PortNamespace(self.BASE_PORT_NAMESPACE_NAME) + assert not port_namespace.dynamic + assert port_namespace.valid_type is None # Setting the `valid_type` should automatically set `dynamic=True` because it does not make sense to define a # a specific type but then not allow any values whatsoever. - self.port_namespace.valid_type = int + port_namespace.valid_type = int - self.assertTrue(self.port_namespace.dynamic) - self.assertEqual(self.port_namespace.valid_type, int) + assert port_namespace.dynamic + assert port_namespace.valid_type == int - self.port_namespace.valid_type = None + port_namespace.valid_type = None # Setting `valid_type` to `None` however does not automatically revert the `dynamic` attribute - self.assertTrue(self.port_namespace.dynamic) - self.assertIsNone(self.port_namespace.valid_type) + assert port_namespace.dynamic + assert port_namespace.valid_type is None def test_port_namespace_validate(self): """Check that validating of sub namespaces works correctly. @@ -273,60 +275,61 @@ def test_port_namespace_validate(self): By setting a valid type on a port namespace, it automatically becomes dynamic. Port namespaces that are dynamic should accept arbitrarily nested input and should validate, as long as all leaf values satisfy the `valid_type`. """ - port_namespace_sub = self.port_namespace.create_port_namespace('sub.space') + port_namespace = PortNamespace(self.BASE_PORT_NAMESPACE_NAME) + port_namespace_sub = port_namespace.create_port_namespace('sub.space') port_namespace_sub.valid_type = int assert port_namespace_sub.dynamic # Check that passing a non mapping type raises - validation_error = self.port_namespace.validate(5) - self.assertIsNotNone(validation_error) + validation_error = port_namespace.validate(5) + assert validation_error is not None # Valid input - validation_error = self.port_namespace.validate({'sub': {'space': {'output': 5}}}) - self.assertIsNone(validation_error) + validation_error = port_namespace.validate({'sub': {'space': {'output': 5}}}) + assert validation_error is None # Valid input: `sub.space` is dynamic, so should allow arbitrarily nested namespaces as long as the leaf values # match the valid type, which is `int` in this example. - validation_error = self.port_namespace.validate({'sub': {'space': {'output': {'invalid': 5}}}}) - self.assertIsNone(validation_error) + validation_error = port_namespace.validate({'sub': {'space': {'output': {'invalid': 5}}}}) + assert validation_error is None # Invalid input - the value in ``space`` is not ``int`` but a ``str`` - validation_error = self.port_namespace.validate({'sub': {'space': {'output': '5'}}}) - self.assertIsNotNone(validation_error) + validation_error = port_namespace.validate({'sub': {'space': {'output': '5'}}}) + assert validation_error is not None # Check the breadcrumbs are correct - self.assertEqual( - validation_error.port, - self.port_namespace.NAMESPACE_SEPARATOR.join((self.BASE_PORT_NAMESPACE_NAME, 'sub', 'space', 'output')), + assert validation_error.port == port_namespace.NAMESPACE_SEPARATOR.join( + (self.BASE_PORT_NAMESPACE_NAME, 'sub', 'space', 'output') ) def test_port_namespace_required(self): """Verify that validation will fail if required port is not specified.""" - port_namespace_sub = self.port_namespace.create_port_namespace('sub.space') + port_namespace = PortNamespace(self.BASE_PORT_NAMESPACE_NAME) + port_namespace_sub = port_namespace.create_port_namespace('sub.space') port_namespace_sub.valid_type = int # Create a required port - self.port_namespace['required_port'] = OutputPort('required_port', valid_type=int, required=True) + port_namespace['required_port'] = OutputPort('required_port', valid_type=int, required=True) # No port values at all should fail port_values = {} - validation_error = self.port_namespace.validate(port_values) - self.assertIsNotNone(validation_error) + validation_error = port_namespace.validate(port_values) + assert validation_error is not None # Some port value, but still the required output is not defined, so should fail port_values = {'sub': {'space': {'output': 5}}} - validation_error = self.port_namespace.validate(port_values) - self.assertIsNotNone(validation_error) + validation_error = port_namespace.validate(port_values) + assert validation_error is not None # Specifying the required port and some additional ones should be valid port_values = {'sub': {'space': {'output': 5}}, 'required_port': 1} - validation_error = self.port_namespace.validate(port_values) - self.assertIsNone(validation_error) + validation_error = port_namespace.validate(port_values) + assert validation_error is None # Also just the required port should be valid port_values = {'required_port': 1} - validation_error = self.port_namespace.validate(port_values) - self.assertIsNone(validation_error) + validation_error = port_namespace.validate(port_values) + assert validation_error is None def test_port_namespace_no_populate_defaults(self): """Verify that defaults are not populated for a `populate_defaults=False` namespace in `pre_process`.""" @@ -337,9 +340,9 @@ def test_port_namespace_no_populate_defaults(self): inputs = {} pre_processed = port_namespace.pre_process(inputs) - self.assertIn('normal', pre_processed) - self.assertIn('with_default', pre_processed.normal) - self.assertNotIn('without_default', pre_processed.normal) + assert 'normal' in pre_processed + assert 'with_default' in pre_processed.normal + assert 'without_default' not in pre_processed.normal # Now repeat the test but with a "lazy" namespace where defaults are not populated if not explicitly specified port_namespace = PortNamespace('base') @@ -351,7 +354,7 @@ def test_port_namespace_no_populate_defaults(self): pre_processed = port_namespace.pre_process(inputs) # Because the namespace is lazy and no inputs were passed, the defaults should not have been populated. - self.assertEqual(pre_processed, {}) + assert pre_processed == {} def test_port_namespace_lambda_defaults(self): """Verify that lambda defaults are accepted and properly evaluated.""" @@ -361,17 +364,17 @@ def test_port_namespace_lambda_defaults(self): ) inputs = port_namespace.pre_process({}) - self.assertEqual(inputs['lambda_default'], 1) - self.assertIsNone(port_namespace.validate(inputs)) + assert inputs['lambda_default'] == 1 + assert port_namespace.validate(inputs) is None inputs = port_namespace.pre_process({'lambda_default': 5}) - self.assertEqual(inputs['lambda_default'], 5) - self.assertIsNone(port_namespace.validate(inputs)) + assert inputs['lambda_default'] == 5 + assert port_namespace.validate(inputs) is None # When passing a lambda directly as the value, it should NOT be evaluated during pre_processing def some_lambda(): return 5 inputs = port_namespace.pre_process({'lambda_default': some_lambda}) - self.assertEqual(inputs['lambda_default'], some_lambda) - self.assertIsNone(port_namespace.validate(inputs)) + assert inputs['lambda_default'] == some_lambda + assert port_namespace.validate(inputs) is None diff --git a/tests/test_process_spec.py b/tests/test_process_spec.py index 3be8c1d2..7ae0acc0 100644 --- a/tests/test_process_spec.py +++ b/tests/test_process_spec.py @@ -2,44 +2,42 @@ from plumpy import ProcessSpec from plumpy.ports import InputPort, PortNamespace -from .utils import TestCase - class StrSubtype(str): pass -class TestProcessSpec(TestCase): - def setUp(self): - self.spec = ProcessSpec() +class TestProcessSpec: def test_get_port_namespace_base(self): """ Get the root, inputs and outputs port namespaces of the ProcessSpec """ - input_ports = self.spec.inputs - output_ports = self.spec.outputs + spec = ProcessSpec() + input_ports = spec.inputs + output_ports = spec.outputs - self.assertTrue(input_ports.name, self.spec.NAME_INPUTS_PORT_NAMESPACE) - self.assertTrue(output_ports.name, self.spec.NAME_OUTPUTS_PORT_NAMESPACE) + assert input_ports.name, spec.NAME_INPUTS_PORT_NAMESPACE + assert output_ports.name, spec.NAME_OUTPUTS_PORT_NAMESPACE def test_dynamic_output(self): - self.spec.outputs.dynamic = True - self.spec.outputs.valid_type = str - self.assertIsNone(self.spec.outputs.validate({'dummy': 'foo'})) - self.assertIsNone(self.spec.outputs.validate({'dummy': StrSubtype('bar')})) - self.assertIsNotNone(self.spec.outputs.validate({'dummy': 5})) + spec = ProcessSpec() + spec.outputs.dynamic = True + spec.outputs.valid_type = str + assert spec.outputs.validate({'dummy': 'foo'}) is None + assert spec.outputs.validate({'dummy': StrSubtype('bar')}) is None + assert spec.outputs.validate({'dummy': 5}) is not None # Remove dynamic output - self.spec.outputs.dynamic = False - self.spec.outputs.valid_type = None + spec.outputs.dynamic = False + spec.outputs.valid_type = None # Now add and check behaviour - self.spec.outputs.dynamic = True - self.spec.outputs.valid_type = str - self.assertIsNone(self.spec.outputs.validate({'dummy': 'foo'})) - self.assertIsNone(self.spec.outputs.validate({'dummy': StrSubtype('bar')})) - self.assertIsNotNone(self.spec.outputs.validate({'dummy': 5})) + spec.outputs.dynamic = True + spec.outputs.valid_type = str + assert spec.outputs.validate({'dummy': 'foo'}) is None + assert spec.outputs.validate({'dummy': StrSubtype('bar')}) is None + assert spec.outputs.validate({'dummy': 5}) is not None def test_get_description(self): spec = ProcessSpec() @@ -47,29 +45,30 @@ def test_get_description(self): # Adding an input should create some description spec.input('test') description = spec.get_description() - self.assertNotEqual(description, {}) + assert description != {} # Similar with adding output spec = ProcessSpec() spec.output('test') description = spec.get_description() - self.assertNotEqual(description, {}) + assert description != {} def test_input_namespaced(self): """ Test the creation of a namespaced input port """ - self.spec.input('some.name.space.a', valid_type=int) + spec = ProcessSpec() + spec.input('some.name.space.a', valid_type=int) - self.assertTrue('some' in self.spec.inputs) - self.assertTrue('name' in self.spec.inputs['some']) - self.assertTrue('space' in self.spec.inputs['some']['name']) - self.assertTrue('a' in self.spec.inputs['some']['name']['space']) + assert 'some' in spec.inputs + assert 'name' in spec.inputs['some'] + assert 'space' in spec.inputs['some']['name'] + assert 'a' in spec.inputs['some']['name']['space'] - self.assertTrue(isinstance(self.spec.inputs.get_port('some'), PortNamespace)) - self.assertTrue(isinstance(self.spec.inputs.get_port('some.name'), PortNamespace)) - self.assertTrue(isinstance(self.spec.inputs.get_port('some.name.space'), PortNamespace)) - self.assertTrue(isinstance(self.spec.inputs.get_port('some.name.space.a'), InputPort)) + assert isinstance(spec.inputs.get_port('some'), PortNamespace) + assert isinstance(spec.inputs.get_port('some.name'), PortNamespace) + assert isinstance(spec.inputs.get_port('some.name.space'), PortNamespace) + assert isinstance(spec.inputs.get_port('some.name.space.a'), InputPort) def test_validator(self): """Test the port validator with default.""" @@ -78,17 +77,18 @@ def dict_validator(dictionary, port): if 'key' not in dictionary or dictionary['key'] != 'value': return 'Invalid dictionary' - self.spec.input('dict', default={'key': 'value'}, validator=dict_validator) + spec = ProcessSpec() + spec.input('dict', default={'key': 'value'}, validator=dict_validator) - processed = self.spec.inputs.pre_process({}) - self.assertEqual(processed, {'dict': {'key': 'value'}}) - self.spec.inputs.validate() + processed = spec.inputs.pre_process({}) + assert processed == {'dict': {'key': 'value'}} + spec.inputs.validate() - processed = self.spec.inputs.pre_process({'dict': {'key': 'value'}}) - self.assertEqual(processed, {'dict': {'key': 'value'}}) - self.spec.inputs.validate() + processed = spec.inputs.pre_process({'dict': {'key': 'value'}}) + assert processed == {'dict': {'key': 'value'}} + spec.inputs.validate() - self.assertIsNotNone(self.spec.inputs.validate({'dict': {'wrong_key': 'value'}})) + assert spec.inputs.validate({'dict': {'wrong_key': 'value'}}) is not None def test_validate(self): """Test the global spec validator functionality.""" @@ -98,17 +98,18 @@ def is_valid(inputs, port): return 'Must have a OR b in inputs' return - self.spec.input('a', required=False) - self.spec.input('b', required=False) - self.spec.inputs.validator = is_valid + spec = ProcessSpec() + spec.input('a', required=False) + spec.input('b', required=False) + spec.inputs.validator = is_valid - processed = self.spec.inputs.pre_process({'a': 'a'}) - self.assertEqual(processed, {'a': 'a'}) - self.spec.inputs.validate() + processed = spec.inputs.pre_process({'a': 'a'}) + assert processed == {'a': 'a'} + spec.inputs.validate() - processed = self.spec.inputs.pre_process({'b': 'b'}) - self.assertEqual(processed, {'b': 'b'}) - self.spec.inputs.validate() + processed = spec.inputs.pre_process({'b': 'b'}) + assert processed == {'b': 'b'} + spec.inputs.validate() - self.assertIsNotNone(self.spec.inputs.validate({})) - self.assertIsNotNone(self.spec.inputs.validate({'a': 'a', 'b': 'b'})) + assert spec.inputs.validate({}) is not None + assert spec.inputs.validate({'a': 'a', 'b': 'b'}) is not None diff --git a/tests/test_processes.py b/tests/test_processes.py index 7e6b82c8..19544ae3 100644 --- a/tests/test_processes.py +++ b/tests/test_processes.py @@ -3,7 +3,6 @@ import asyncio import enum -import unittest import pytest from plumpy.futures import CancellableAction @@ -70,22 +69,22 @@ async def task(self, steps: list): await p1task, p2task -class TestProcess(unittest.TestCase): +class TestProcess: def test_spec(self): """ Check that the references to specs are doing the right thing... """ proc = utils.DummyProcess() - self.assertIsNot(utils.DummyProcess.spec(), Process.spec()) - self.assertIs(proc.spec(), utils.DummyProcess.spec()) + assert utils.DummyProcess.spec() is not Process.spec() + assert proc.spec() is utils.DummyProcess.spec() class Proc(utils.DummyProcess): pass - self.assertIsNot(Proc.spec(), Process.spec()) - self.assertIsNot(Proc.spec(), utils.DummyProcess.spec()) + assert Proc.spec() is not Process.spec() + assert Proc.spec() is not utils.DummyProcess.spec() p = Proc() - self.assertIs(p.spec(), Proc.spec()) + assert p.spec() is Proc.spec() def test_dynamic_inputs(self): class NoDynamic(Process): @@ -97,7 +96,7 @@ def define(cls, spec): super().define(spec) spec.inputs.dynamic = True - with self.assertRaises(ValueError): + with pytest.raises(ValueError): NoDynamic(inputs={'a': 5}).execute() proc = WithDynamic(inputs={'a': 5}) @@ -113,8 +112,8 @@ def define(cls, spec): p = Proc({'a': 5}) # Check that we can access the inputs after creating - self.assertEqual(p.raw_inputs.a, 5) - with self.assertRaises(AttributeError): + assert p.raw_inputs.a == 5 + with pytest.raises(AttributeError): p.raw_inputs.b def test_raw_inputs(self): @@ -136,7 +135,7 @@ def define(cls, spec): # Compare against a clone of the original inputs dictionary as the original is modified. It should not contain # the default value of the ``nested.b`` port. - self.assertDictEqual(dict(process.raw_inputs), {'a': 5, 'nested': {'a': 'value'}}) + assert dict(process.raw_inputs) == {'a': 5, 'nested': {'a': 'value'}} def test_inputs_default(self): class Proc(utils.DummyProcess): @@ -147,11 +146,11 @@ def define(cls, spec): # Supply a value p = Proc(inputs={'input': 2}) - self.assertEqual(p.inputs['input'], 2) + assert p.inputs['input'] == 2 # Don't supply, use default p = Proc() - self.assertEqual(p.inputs['input'], 5) + assert p.inputs['input'] == 5 def test_optional_namespace(self): """Process with an optional namespace should not have that in `self.inputs` if not explicitly passed.""" @@ -166,17 +165,17 @@ def define(cls, spec): # If a value is specified for `namespace` it should be present in parsed inputs process = SomeProcess(inputs={'namespace': {'a': 1}}) - self.assertIn('namespace', process.inputs) - self.assertIn('a', process.inputs['namespace']) - self.assertEqual(process.inputs['namespace']['a'], 1) + assert 'namespace' in process.inputs + assert 'a' in process.inputs['namespace'] + assert process.inputs['namespace']['a'] == 1 # If nothing is passed, it should not be present process = SomeProcess() - self.assertNotIn('namespace', process.inputs) + assert 'namespace' not in process.inputs # However, if something is passed it should be there even if it is just an empty mapping process = SomeProcess(inputs={'namespace': {}}) - self.assertIn('namespace', process.inputs) + assert 'namespace' in process.inputs class SomeDefaultProcess(Process): """Process with single dynamic optional namespace, but with one concrete port with a default.""" @@ -190,9 +189,9 @@ def define(cls, spec): # Even though `namespace` is optional and it is not explicitly passed as input, because the port `b` nested # within it has a default, the `namespace` mapping should be present in the parsed inputs. process = SomeDefaultProcess() - self.assertIn('namespace', process.inputs) - self.assertIn('b', process.inputs['namespace']) - self.assertEqual(process.inputs['namespace']['b'], 5) + assert 'namespace' in process.inputs + assert 'b' in process.inputs['namespace'] + assert process.inputs['namespace']['b'] == 5 def test_inputs_default_that_evaluate_to_false(self): for def_val in (True, False, 0, 1): @@ -205,8 +204,8 @@ def define(cls, spec): # Don't supply, use default p = Proc() - self.assertIn('input', p.inputs) - self.assertEqual(p.inputs['input'], def_val) + assert 'input' in p.inputs + assert p.inputs['input'] == def_val def test_nested_namespace_defaults(self): """Process with a default in a nested namespace should be created, even if top level namespace not supplied.""" @@ -219,8 +218,8 @@ def define(cls, spec): spec.input('namespace.sub', default=True) process = SomeProcess() - self.assertIn('sub', process.inputs.namespace) - self.assertEqual(process.inputs.namespace.sub, True) + assert 'sub' in process.inputs.namespace + assert process.inputs.namespace.sub == True def test_raise_in_define(self): """Process which raises in its 'define' method. Check that the spec is not set.""" @@ -231,31 +230,31 @@ def define(cls, spec): super().define(spec) raise ValueError - with self.assertRaises(ValueError): + with pytest.raises(ValueError): BrokenProcess.spec() # Check that the error is still raised when calling .spec() # a second time. - with self.assertRaises(ValueError): + with pytest.raises(ValueError): BrokenProcess.spec() def test_execute(self): proc = utils.DummyProcessWithOutput() proc.execute() - self.assertTrue(proc.has_terminated()) - self.assertEqual(proc.state_label, ProcessState.FINISHED) - self.assertEqual(proc.outputs, {'default': 5}) + assert proc.has_terminated() + assert proc.state_label == ProcessState.FINISHED + assert proc.outputs == {'default': 5} def test_run_from_class(self): # Test running through class method proc = utils.DummyProcessWithOutput() proc.execute() results = proc.outputs - self.assertEqual(results['default'], 5) + assert results['default'] == 5 def test_forget_to_call_parent(self): for event in ('create', 'run', 'finish'): - with self.assertRaises(AssertionError): + with pytest.raises(AssertionError): proc = ForgetToCallParent(event) proc.execute() @@ -267,21 +266,21 @@ def test_forget_to_call_parent_kill(self): def test_pid(self): # Test auto generation of pid process = utils.DummyProcessWithOutput() - self.assertIsNotNone(process.pid) + assert process.pid is not None # Test using integer as pid process = utils.DummyProcessWithOutput(pid=5) - self.assertEqual(process.pid, 5) + assert process.pid == 5 # Test using string as pid process = utils.DummyProcessWithOutput(pid='a') - self.assertEqual(process.pid, 'a') + assert process.pid == 'a' def test_exception(self): proc = utils.ExceptionProcess() - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): proc.execute() - self.assertEqual(proc.state_label, ProcessState.EXCEPTED) + assert proc.state_label == ProcessState.EXCEPTED def test_run_kill(self): proc = utils.KillProcess() @@ -302,21 +301,21 @@ def define(cls, spec): for proc_class in utils.TEST_PROCESSES: desc = proc_class.get_description() - self.assertIsInstance(desc, dict) + assert isinstance(desc, dict) desc_with_spec = ProcWithSpec.get_description() desc_without_spec = ProcWithoutSpec.get_description() - self.assertIsInstance(desc_without_spec, dict) - self.assertTrue('spec' in desc_without_spec) - self.assertTrue('description' not in desc_without_spec) - self.assertIsInstance(desc_with_spec['spec'], dict) + assert isinstance(desc_without_spec, dict) + assert 'spec' in desc_without_spec + assert 'description' not in desc_without_spec + assert isinstance(desc_with_spec['spec'], dict) - self.assertIsInstance(desc_with_spec, dict) - self.assertTrue('spec' in desc_with_spec) - self.assertTrue('description' in desc_with_spec) - self.assertIsInstance(desc_with_spec['spec'], dict) - self.assertIsInstance(desc_with_spec['description'], str) + assert isinstance(desc_with_spec, dict) + assert 'spec' in desc_with_spec + assert 'description' in desc_with_spec + assert isinstance(desc_with_spec['spec'], dict) + assert isinstance(desc_with_spec['description'], str) def test_logging(self): class LoggerTester(Process): @@ -332,9 +331,9 @@ def test_kill(self): msg_text = 'Farewell!' proc.kill(msg_text=msg_text) - self.assertTrue(proc.killed()) - self.assertEqual(proc.killed_msg()[MESSAGE_TEXT_KEY], msg_text) - self.assertEqual(proc.state_label, ProcessState.KILLED) + assert proc.killed() + assert proc.killed_msg()[MESSAGE_TEXT_KEY] == msg_text + assert proc.state_label == ProcessState.KILLED def test_wait_continue(self): proc = utils.WaitForSignalProcess() @@ -347,20 +346,20 @@ def test_wait_continue(self): proc.execute() # Check it's done - self.assertTrue(proc.has_terminated()) - self.assertEqual(proc.state_label, ProcessState.FINISHED) + assert proc.has_terminated() + assert proc.state_label == ProcessState.FINISHED def test_exc_info(self): proc = utils.ExceptionProcess() try: proc.execute() except RuntimeError as e: - self.assertEqual(proc.exception(), e) + assert proc.exception() == e def test_run_done(self): proc = utils.DummyProcess() proc.execute() - self.assertTrue(proc.has_terminated()) + assert proc.has_terminated() def test_wait_pause_play_resume(self): """ @@ -372,23 +371,23 @@ def test_wait_pause_play_resume(self): async def async_test(): await utils.run_until_waiting(proc) - self.assertEqual(proc.state_label, ProcessState.WAITING) + assert proc.state_label == ProcessState.WAITING result = await proc.pause() - self.assertTrue(result) - self.assertTrue(proc.paused) + assert result + assert proc.paused result = proc.play() - self.assertTrue(result) - self.assertFalse(proc.paused) + assert result + assert not proc.paused proc.resume() # Wait until the process is terminated await proc.future() # Check it's done - self.assertTrue(proc.has_terminated()) - self.assertEqual(proc.state_label, ProcessState.FINISHED) + assert proc.has_terminated() + assert proc.state_label == ProcessState.FINISHED loop.create_task(proc.step_until_terminated()) loop.run_until_complete(async_test()) @@ -409,16 +408,16 @@ def test_pause_play_status_messaging(self): async def async_test(): await utils.run_until_waiting(proc) - self.assertEqual(proc.state_label, ProcessState.WAITING) + assert proc.state_label == ProcessState.WAITING result = await proc.pause(PAUSE_STATUS) - self.assertTrue(result) - self.assertTrue(proc.paused) - self.assertEqual(proc.status, PAUSE_STATUS) + assert result + assert proc.paused + assert proc.status == PAUSE_STATUS result = proc.play() - self.assertEqual(proc.status, PLAY_STATUS) - self.assertIsNone(proc._pre_paused_status) + assert proc.status == PLAY_STATUS + assert proc._pre_paused_status is None proc.resume() # Wait until the process is terminated @@ -428,8 +427,8 @@ async def async_test(): loop.create_task(proc.step_until_terminated()) loop.run_until_complete(async_test()) - self.assertTrue(proc.has_terminated()) - self.assertEqual(proc.state_label, ProcessState.FINISHED) + assert proc.has_terminated() + assert proc.state_label == ProcessState.FINISHED def test_kill_in_run(self): class KillProcess(Process): @@ -446,8 +445,8 @@ def run(self, **kwargs): with pytest.raises(plumpy.KilledError, match='killed'): proc.execute() - self.assertTrue(proc.after_kill) - self.assertEqual(proc.state_label, ProcessState.KILLED) + assert proc.after_kill + assert proc.state_label == ProcessState.KILLED def test_kill_when_paused_in_run(self): class PauseProcess(Process): @@ -456,10 +455,10 @@ def run(self, **kwargs): self.kill() proc = PauseProcess() - with self.assertRaises(plumpy.KilledError): + with pytest.raises(plumpy.KilledError): proc.execute() - self.assertEqual(proc.state_label, ProcessState.KILLED) + assert proc.state_label == ProcessState.KILLED def test_kill_when_paused(self): loop = asyncio.get_event_loop() @@ -471,19 +470,19 @@ async def async_test(): saved_state = plumpy.Bundle(proc) result = await proc.pause() - self.assertTrue(result) - self.assertTrue(proc.paused) + assert result + assert proc.paused # Kill the process proc.kill() - with self.assertRaises(plumpy.KilledError): + with pytest.raises(plumpy.KilledError): result = await proc.future() loop.create_task(proc.step_until_terminated()) loop.run_until_complete(async_test()) - self.assertEqual(proc.state_label, ProcessState.KILLED) + assert proc.state_label == ProcessState.KILLED def test_run_multiple(self): # Create and play some processes @@ -499,7 +498,7 @@ def test_run_multiple(self): results = loop.run_until_complete(tasks) for result, proc_class in zip(results, utils.TEST_PROCESSES): - self.assertDictEqual(proc_class.EXPECTED_OUTPUTS, result) + assert proc_class.EXPECTED_OUTPUTS == result def test_invalid_output(self): class InvalidOutput(plumpy.Process): @@ -507,7 +506,7 @@ def run(self): self.out('invalid', 5) proc = InvalidOutput() - with self.assertRaises(ValueError): + with pytest.raises(ValueError): proc.execute() assert proc.is_excepted @@ -515,12 +514,12 @@ def run(self): def test_missing_output(self): proc = utils.MissingOutputProcess() - with self.assertRaises(plumpy.InvalidStateError): + with pytest.raises(plumpy.InvalidStateError): proc.successful() proc.execute() - self.assertFalse(proc.is_successful) + assert not proc.is_successful def test_unsuccessful_result(self): ERROR_CODE = 256 @@ -536,7 +535,7 @@ def run(self): proc = Proc() proc.execute() - self.assertEqual(proc.result(), ERROR_CODE) + assert proc.result() == ERROR_CODE def test_pause_in_process(self): """Test that we can pause and cancel that by playing within the process""" @@ -558,33 +557,30 @@ def run(self): loop.create_task(proc.step_until_terminated()) loop.run_forever() - self.assertTrue(proc.paused) - self.assertEqual(proc.state_label, plumpy.ProcessState.FINISHED) + assert proc.paused + assert proc.state_label == plumpy.ProcessState.FINISHED def test_pause_play_in_process(self): """Test that we can pause and play that by playing within the process""" - test_case = self - class TestPausePlay(plumpy.Process): def run(self): fut = self.pause() - test_case.assertIsInstance(fut, CancellableAction) + assert isinstance(fut, CancellableAction) result = self.play() - test_case.assertTrue(result) + assert result proc = TestPausePlay() proc.execute() - self.assertFalse(proc.paused) - self.assertEqual(proc.state_label, plumpy.ProcessState.FINISHED) + assert not proc.paused + assert proc.state_label == plumpy.ProcessState.FINISHED def test_process_stack(self): - test_case = self class StackTest(plumpy.Process): def run(self): - test_case.assertIs(self, Process.current()) + assert self is Process.current() proc = StackTest() proc.execute() @@ -622,7 +618,7 @@ def run(self): assert all(expect_true) - self.assertEqual(len(expect_true), n_run * 3) + assert len(expect_true) == n_run * 3 def test_process_nested(self): """ @@ -653,7 +649,7 @@ def test_execute_twice(self): """Test a process that is executed once finished raises a ClosedError""" proc = utils.DummyProcess() proc.execute() - with self.assertRaises(plumpy.ClosedError): + with pytest.raises(plumpy.ClosedError): proc.execute() def test_exception_during_on_entered(self): @@ -667,7 +663,7 @@ def on_entered(self, from_state): process = RaisingProcess() - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): process.execute() assert not process.is_successful @@ -681,7 +677,7 @@ def run(self): process = RaisingProcess() - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): process.execute() assert process.is_excepted @@ -705,7 +701,7 @@ def step2(self): self.steps_ran.append(self.step2.__name__) -class TestProcessSaving(unittest.TestCase): +class TestProcessSaving: maxDiff = None def test_running_save(self): @@ -717,21 +713,21 @@ async def async_test(): # Create a checkpoint bundle = plumpy.Bundle(nsync_comeback) - self.assertListEqual([SavePauseProc.run.__name__], nsync_comeback.steps_ran) + assert [SavePauseProc.run.__name__] == nsync_comeback.steps_ran nsync_comeback.play() await nsync_comeback.future() - self.assertListEqual([SavePauseProc.run.__name__, SavePauseProc.step2.__name__], nsync_comeback.steps_ran) + assert [SavePauseProc.run.__name__, SavePauseProc.step2.__name__] == nsync_comeback.steps_ran proc_unbundled = bundle.unbundle() # At bundle time the Process was paused, the future of which will be persisted to the bundle. # As a result the process, recreated from that bundle, will also be paused and will have to be played proc_unbundled.play() - self.assertEqual(0, len(proc_unbundled.steps_ran)) + assert 0 == len(proc_unbundled.steps_ran) await proc_unbundled.step_until_terminated() - self.assertEqual([SavePauseProc.step2.__name__], proc_unbundled.steps_ran) + assert [SavePauseProc.step2.__name__] == proc_unbundled.steps_ran loop.create_task(nsync_comeback.step_until_terminated()) loop.run_until_complete(async_test()) @@ -754,7 +750,7 @@ async def async_test(): proc_unbundled.play() await proc_unbundled.future() - self.assertListEqual([SavePauseProc.run.__name__, SavePauseProc.step2.__name__], proc_unbundled.steps_ran) + assert [SavePauseProc.run.__name__, SavePauseProc.step2.__name__] == proc_unbundled.steps_ran loop.create_task(proc_unbundled.step_until_terminated()) loop.run_until_complete(async_test()) @@ -775,12 +771,12 @@ def test_instance_state_with_outputs(self): for bundle, outputs in zip(saver.snapshots, saver.outputs): # Check that it is a copy - self.assertIsNot(outputs, bundle.get(BundleKeys.OUTPUTS, {})) + assert outputs is not bundle.get(BundleKeys.OUTPUTS, {}) # Check the contents are the same # Remove the ``ProcessSaver`` instance that is only used for testing utils.compare_dictionaries(None, None, outputs, bundle.get(BundleKeys.OUTPUTS, {}), exclude={'_listeners'}) - self.assertIsNot(proc.outputs, saver.snapshots[-1].get(BundleKeys.OUTPUTS, {})) + assert proc.outputs is not saver.snapshots[-1].get(BundleKeys.OUTPUTS, {}) def test_saving_each_step(self): loop = asyncio.get_event_loop() @@ -788,8 +784,8 @@ def test_saving_each_step(self): proc = proc_class() saver = utils.ProcessSaver(proc) saver.capture() - self.assertEqual(proc.state_label, ProcessState.FINISHED) - self.assertTrue(utils.check_process_against_snapshots(loop, proc_class, saver.snapshots)) + assert proc.state_label == ProcessState.FINISHED + assert utils.check_process_against_snapshots(loop, proc_class, saver.snapshots) def test_restart(self): loop = asyncio.get_event_loop() @@ -803,12 +799,12 @@ async def async_test(): # Load a process from the saved state loaded_proc = saved_state.unbundle() - self.assertEqual(loaded_proc.state_label, ProcessState.WAITING) + assert loaded_proc.state_label == ProcessState.WAITING # Now resume it loaded_proc.resume() await loaded_proc.step_until_terminated() - self.assertEqual(loaded_proc.outputs, {'finished': True}) + assert loaded_proc.outputs == {'finished': True} loop.create_task(proc.step_until_terminated()) loop.run_until_complete(async_test()) @@ -826,14 +822,14 @@ async def async_test(): # Load a process from the saved state loaded_proc = saved_state.unbundle() - self.assertEqual(loaded_proc.state_label, ProcessState.WAITING) + assert loaded_proc.state_label == ProcessState.WAITING # Now resume it twice in succession loaded_proc.resume() loaded_proc.resume() await loaded_proc.step_until_terminated() - self.assertEqual(loaded_proc.outputs, {'finished': True}) + assert loaded_proc.outputs == {'finished': True} loop.create_task(proc.step_until_terminated()) loop.run_until_complete(async_test()) @@ -860,7 +856,7 @@ async def async_test(): result2 = await proc2.future() # Check results match - self.assertEqual(result1, result2) + assert result1 == result2 loop.create_task(proc.step_until_terminated()) loop.run_until_complete(async_test()) @@ -868,7 +864,7 @@ async def async_test(): def test_killed(self): proc = utils.DummyProcess() proc.kill() - self.assertEqual(proc.state_label, plumpy.ProcessState.KILLED) + assert proc.state_label == plumpy.ProcessState.KILLED self._check_round_trip(proc) def _check_round_trip(self, proc1): @@ -877,11 +873,11 @@ def _check_round_trip(self, proc1): proc2 = bundle1.unbundle() bundle2 = plumpy.Bundle(proc2) - self.assertEqual(proc1.pid, proc2.pid) + assert proc1.pid == proc2.pid utils.compare_dictionaries(None, None, bundle1, bundle2, exclude={'_listeners'}) -class TestProcessNamespace(unittest.TestCase): +class TestProcessNamespace: def test_namespaced_process(self): """ Test that inputs in nested namespaces are properly validated and the returned @@ -897,15 +893,15 @@ def define(cls, spec): proc = NameSpacedProcess(inputs={'some': {'name': {'space': {'a': 5}}}}) # Test that the namespaced inputs are AttributesFrozendict - self.assertIsInstance(proc.inputs, AttributesFrozendict) - self.assertIsInstance(proc.inputs.some, AttributesFrozendict) - self.assertIsInstance(proc.inputs.some.name, AttributesFrozendict) - self.assertIsInstance(proc.inputs.some.name.space, AttributesFrozendict) + assert isinstance(proc.inputs, AttributesFrozendict) + assert isinstance(proc.inputs.some, AttributesFrozendict) + assert isinstance(proc.inputs.some.name, AttributesFrozendict) + assert isinstance(proc.inputs.some.name.space, AttributesFrozendict) # Test that the input node is in the inputs of the process input_value = proc.inputs.some.name.space.a - self.assertTrue(isinstance(input_value, int)) - self.assertEqual(input_value, 5) + assert isinstance(input_value, int) + assert input_value == 5 def test_namespaced_process_inputs(self): """ @@ -924,12 +920,12 @@ def define(cls, spec): proc = NameSpacedProcess(inputs={'some': {'name': {'space': {'a': 5}}}}) - self.assertEqual(proc.inputs.test, 6) - self.assertEqual(proc.inputs.store_provenance, True) - self.assertEqual(proc.inputs.some.name.space.a, 5) + assert proc.inputs.test == 6 + assert proc.inputs.store_provenance == True + assert proc.inputs.some.name.space.a == 5 - self.assertTrue('label' not in proc.inputs) - self.assertTrue('description' not in proc.inputs) + assert 'label' not in proc.inputs + assert 'description' not in proc.inputs def test_namespaced_process_dynamic(self): """ @@ -951,12 +947,12 @@ def define(cls, spec): proc = DummyDynamicProcess(inputs=inputs) for label, value in proc.inputs['name']['space'].items(): - self.assertTrue(label in inputs['name']['space']) - self.assertEqual(int(label), value) + assert label in inputs['name']['space'] + assert int(label) == value original_inputs.remove(value) # Make sure there are no other inputs - self.assertFalse(original_inputs) + assert not original_inputs def test_namespaced_process_outputs(self): """Test the output namespacing and validation.""" @@ -994,56 +990,56 @@ def run(self): proc = DummyDynamicProcess() proc.execute() - self.assertEqual(proc.state_label, ProcessState.FINISHED) - self.assertFalse(proc.is_successful) - self.assertDictEqual(proc.outputs, {}) + assert proc.state_label == ProcessState.FINISHED + assert not proc.is_successful + assert proc.outputs == {} # Attaching only namespaced ports should fail, because the required port is not added proc = DummyDynamicProcess(inputs={'output_mode': OutputMode.DYNAMIC_PORT_NAMESPACE}) proc.execute() - self.assertEqual(proc.state_label, ProcessState.FINISHED) - self.assertFalse(proc.is_successful) - self.assertEqual(proc.outputs[namespace]['nested']['one'], 1) - self.assertEqual(proc.outputs[namespace]['nested']['two'], 2) + assert proc.state_label == ProcessState.FINISHED + assert not proc.is_successful + assert proc.outputs[namespace]['nested']['one'] == 1 + assert proc.outputs[namespace]['nested']['two'] == 2 # Attaching only the single required top-level port should be fine proc = DummyDynamicProcess(inputs={'output_mode': OutputMode.SINGLE_REQUIRED_PORT}) proc.execute() - self.assertEqual(proc.state_label, ProcessState.FINISHED) - self.assertTrue(proc.is_successful) - self.assertEqual(proc.outputs['required_bool'], False) + assert proc.state_label == ProcessState.FINISHED + assert proc.is_successful + assert proc.outputs['required_bool'] == False # Attaching both the required and namespaced ports should result in a successful termination proc = DummyDynamicProcess(inputs={'output_mode': OutputMode.BOTH_SINGLE_AND_NAMESPACE}) proc.execute() - self.assertIsNotNone(proc.outputs) - self.assertEqual(proc.state_label, ProcessState.FINISHED) - self.assertTrue(proc.is_successful) - self.assertEqual(proc.outputs['required_bool'], False) - self.assertEqual(proc.outputs[namespace]['nested']['one'], 1) - self.assertEqual(proc.outputs[namespace]['nested']['two'], 2) + assert proc.outputs is not None + assert proc.state_label == ProcessState.FINISHED + assert proc.is_successful + assert proc.outputs['required_bool'] == False + assert proc.outputs[namespace]['nested']['one'] == 1 + assert proc.outputs[namespace]['nested']['two'] == 2 -class TestProcessEvents(unittest.TestCase): +class TestProcessEvents: def test_basic_events(self): proc = utils.DummyProcessWithOutput() events_tester = utils.ProcessListenerTester( process=proc, expected_events=('running', 'output_emitted', 'finished') ) proc.execute() - self.assertSetEqual(events_tester.called, events_tester.expected_events) + assert events_tester.called == events_tester.expected_events def test_killed(self): proc = utils.DummyProcessWithOutput() events_tester = utils.ProcessListenerTester(proc, ('killed',)) - self.assertTrue(proc.kill()) + assert proc.kill() # Do the checks - self.assertTrue(proc.killed()) - self.assertSetEqual(events_tester.called, events_tester.expected_events) + assert proc.killed() + assert events_tester.called == events_tester.expected_events def test_excepted(self): proc = utils.ExceptionProcess() @@ -1055,21 +1051,21 @@ def test_excepted(self): 'output_emitted', ), ) - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): proc.execute() proc.result() # Do the checks - self.assertIsNotNone(proc.exception()) - self.assertSetEqual(events_tester.called, events_tester.expected_events) + assert proc.exception() is not None + assert events_tester.called == events_tester.expected_events def test_paused(self): proc = utils.DummyProcessWithOutput() events_tester = utils.ProcessListenerTester(proc, ('paused',)) - self.assertTrue(proc.pause()) + assert proc.pause() # Do the checks - self.assertSetEqual(events_tester.called, events_tester.expected_events) + assert events_tester.called == events_tester.expected_events def test_broadcast(self): coordinator = utils.MockCoordinator() diff --git a/tests/test_waiting_process.py b/tests/test_waiting_process.py index 90427554..3491cd15 100644 --- a/tests/test_waiting_process.py +++ b/tests/test_waiting_process.py @@ -1,21 +1,21 @@ # -*- coding: utf-8 -*- import asyncio -import unittest import plumpy from plumpy import BundleKeys from . import utils +import pytest -class TestWaitingProcess(unittest.TestCase): +class TestWaitingProcess: def test_instance_state(self): proc = utils.ThreeSteps() wl = utils.ProcessSaver(proc) proc.execute() for bundle, outputs in zip(wl.snapshots, wl.outputs): - self.assertEqual(outputs, bundle.get(BundleKeys.OUTPUTS, {})) + assert outputs == bundle.get(BundleKeys.OUTPUTS, {}) def test_saving_each_step(self): loop = asyncio.get_event_loop() @@ -24,7 +24,7 @@ def test_saving_each_step(self): saver = utils.ProcessSaver(proc) saver.capture() - self.assertTrue(utils.check_process_against_snapshots(loop, proc_class, saver.snapshots)) + assert utils.check_process_against_snapshots(loop, proc_class, saver.snapshots) def test_kill(self): process = utils.WaitForSignalProcess() @@ -34,6 +34,6 @@ def test_kill(self): listener.on_process_waiting = lambda _proc: process.kill() process.add_process_listener(listener) - with self.assertRaises(plumpy.KilledError): + with pytest.raises(plumpy.KilledError): process.execute() - self.assertTrue(process.killed()) + assert process.killed() diff --git a/tests/utils.py b/tests/utils.py index 9a1690e3..a19563e1 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -5,7 +5,6 @@ import collections import sys from typing import Any -import unittest from collections.abc import Mapping import concurrent.futures @@ -141,10 +140,6 @@ def _ensure_open(self): raise CoordinatorConnectionError -class TestCase(unittest.TestCase): - pass - - class DummyProcess(processes.Process): """ Process with no inputs or outputs and does nothing when ran. diff --git a/tests/workchain/test_workchains.py b/tests/workchain/test_workchains.py index cb76020c..c717ca73 100644 --- a/tests/workchain/test_workchains.py +++ b/tests/workchain/test_workchains.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- import asyncio import inspect -import unittest import pytest @@ -124,27 +123,27 @@ def do_nothing(self): pass -class TestContext(unittest.TestCase): +class TestContext: def test_attributes(self): wc = DummyWc() wc.ctx.new_attr = 5 - self.assertEqual(wc.ctx.new_attr, 5) + assert wc.ctx.new_attr == 5 del wc.ctx.new_attr - with self.assertRaises(AttributeError): + with pytest.raises(AttributeError): wc.ctx.new_attr def test_dict(self): wc = DummyWc() wc.ctx['new_attr'] = 5 - self.assertEqual(wc.ctx['new_attr'], 5) + assert wc.ctx['new_attr'] == 5 del wc.ctx['new_attr'] - with self.assertRaises(KeyError): + with pytest.raises(KeyError): wc.ctx['new_attr'] -class TestWorkchain(unittest.TestCase): +class TestWorkchain: maxDiff = None def test_run(self): @@ -158,21 +157,21 @@ def test_run(self): # Check the steps that should have been run for step, finished in Wf.finished_steps.items(): if step not in ['s3', 's4', 'isB']: - self.assertTrue(finished, f'Step {step} was not called by workflow') + assert finished, f'Step {step} was not called by workflow' # Try the elif(..) part finished_steps = Wf(inputs=dict(value=B, n=three)).execute() # Check the steps that should have been run for step, finished in finished_steps.items(): if step not in ['isA', 's2', 's4']: - self.assertTrue(finished, f'Step {step} was not called by workflow') + assert finished, f'Step {step} was not called by workflow' # Try the else... part finished_steps = Wf(inputs=dict(value=C, n=three)).execute() # Check the steps that should have been run for step, finished in finished_steps.items(): if step not in ['isA', 's2', 'isB', 's3']: - self.assertTrue(finished, f'Step {step} was not called by workflow') + assert finished, f'Step {step} was not called by workflow' def test_incorrect_outline(self): class Wf(WorkChain): @@ -182,7 +181,7 @@ def define(cls, spec): # Try defining an invalid outline spec.outline(5) - with self.assertRaises(TypeError): + with pytest.raises(TypeError): Wf.spec() def test_same_input_node(self): @@ -247,7 +246,7 @@ def s3(self): Wf().execute() def test_str(self): - self.assertIsInstance(str(Wf.spec()), str) + assert isinstance(str(Wf.spec()), str) def test_malformed_outline(self): """ @@ -255,10 +254,10 @@ def test_malformed_outline(self): """ spec = WorkChainSpec() - with self.assertRaises(TypeError): + with pytest.raises(TypeError): spec.outline(5) - with self.assertRaises(TypeError): + with pytest.raises(TypeError): spec.outline(lambda x, y: 5) def test_checkpointing(self): @@ -272,21 +271,21 @@ def test_checkpointing(self): # Check the steps that should have been run for step, finished in finished_steps.items(): if step not in ['s3', 's4', 'isB']: - self.assertTrue(finished, f'Step {step} was not called by workflow') + assert finished, f'Step {step} was not called by workflow' # Try the elif(..) part finished_steps = self._run_with_checkpoints(Wf, inputs={'value': B, 'n': three}) # Check the steps that should have been run for step, finished in finished_steps.items(): if step not in ['isA', 's2', 's4']: - self.assertTrue(finished, f'Step {step} was not called by workflow') + assert finished, f'Step {step} was not called by workflow' # Try the else... part finished_steps = self._run_with_checkpoints(Wf, inputs={'value': C, 'n': three}) # Check the steps that should have been run for step, finished in finished_steps.items(): if step not in ['isA', 's2', 'isB', 's3']: - self.assertTrue(finished, f'Step {step} was not called by workflow') + assert finished, f'Step {step} was not called by workflow' def test_listener_persistence(self): persister = plumpy.InMemoryPersister() @@ -321,11 +320,11 @@ def step2(self): workchain.execute() - self.assertEqual(process_finished_count, 1) + assert process_finished_count == 1 workchain_checkpoint = persister.load_checkpoint(workchain.pid, 'step1').unbundle() workchain_checkpoint.execute() - self.assertEqual(process_finished_count, 2) + assert process_finished_count == 2 def test_return_in_outline(self): class WcWithReturn(WorkChain): @@ -358,7 +357,7 @@ def default(self): workchain = WcWithReturn(inputs=dict(success=False)) workchain.execute() - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): workchain = WcWithReturn() workchain.execute() @@ -389,7 +388,7 @@ def after(self): workchain = WcWithReturn(inputs=dict(success=False)) workchain.execute() - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): workchain = WcWithReturn() workchain.execute() @@ -424,24 +423,24 @@ def test_if_block_persistence(self): async def async_test(): await utils.run_until_paused(workchain) - self.assertTrue(workchain.ctx.s1) - self.assertFalse(workchain.ctx.s2) + assert workchain.ctx.s1 + assert not workchain.ctx.s2 # Now bundle the thing bundle = plumpy.Bundle(workchain) # Load from saved state workchain2 = bundle.unbundle() - self.assertTrue(workchain2.ctx.s1) - self.assertFalse(workchain2.ctx.s2) + assert workchain2.ctx.s1 + assert not workchain2.ctx.s2 bundle2 = plumpy.Bundle(workchain2) - self.assertDictEqual(bundle, bundle2) + assert bundle == bundle2 workchain.play() await workchain.future() - self.assertTrue(workchain.ctx.s1) - self.assertTrue(workchain.ctx.s2) + assert workchain.ctx.s1 + assert workchain.ctx.s2 loop = asyncio.get_event_loop() loop.create_task(workchain.step_until_terminated()) # noqa: RUF006 @@ -508,9 +507,9 @@ def check(self): raise my_exception workchain = Workchain() - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): workchain.execute() - self.assertNotEqual(workchain.exception(), my_exception) + assert workchain.exception() != my_exception def _run_with_checkpoints(self, wf_class, inputs=None): # TODO: Actually save at each point! @@ -582,10 +581,10 @@ def on_process_running(self, process): '1:while_(do_step)', '2:if_(do_step)', ] - self.assertListEqual(collector.stepper_strings, stepper_strings) + assert collector.stepper_strings == stepper_strings -class TestImmutableInputWorkchain(unittest.TestCase): +class TestImmutableInputWorkchain: """ Test that inputs cannot be modified """ @@ -594,7 +593,6 @@ def test_immutable_input(self): """ Check that from within the WorkChain self.inputs returns an AttributesFrozendict which should be immutable """ - test_class = self class Wf(WorkChain): @classmethod @@ -609,19 +607,19 @@ def define(cls, spec): def step_one(self): # Attempt to manipulate the inputs dictionary which since it is a AttributesFrozendict should raise - with test_class.assertRaises(TypeError): + with pytest.raises(TypeError): self.inputs['a'] = 3 - with test_class.assertRaises(AttributeError): + with pytest.raises(AttributeError): self.inputs.pop('b') - with test_class.assertRaises(TypeError): + with pytest.raises(TypeError): self.inputs['c'] = 4 def step_two(self): # Verify that original inputs are still there with same value and no inputs were added - test_class.assertIn('a', self.inputs) - test_class.assertIn('b', self.inputs) - test_class.assertNotIn('c', self.inputs) - test_class.assertEqual(self.inputs['a'], 1) + assert 'a' in self.inputs + assert 'b' in self.inputs + assert 'c' not in self.inputs + assert self.inputs['a'] == 1 workchain = Wf(inputs=dict(a=1, b=2)) workchain.execute() @@ -630,7 +628,6 @@ def test_immutable_input_namespace(self): """ Check that namespaced inputs also return AttributeFrozendicts and are hence immutable """ - test_class = self class Wf(WorkChain): @classmethod @@ -644,19 +641,19 @@ def define(cls, spec): def step_one(self): # Attempt to manipulate the namespaced inputs dictionary which should raise - with test_class.assertRaises(TypeError): + with pytest.raises(TypeError): self.inputs.subspace['one'] = 3 - with test_class.assertRaises(AttributeError): + with pytest.raises(AttributeError): self.inputs.subspace.pop('two') - with test_class.assertRaises(TypeError): + with pytest.raises(TypeError): self.inputs.subspace['four'] = 4 def step_two(self): # Verify that original inputs are still there with same value and no inputs were added - test_class.assertIn('one', self.inputs.subspace) - test_class.assertIn('two', self.inputs.subspace) - test_class.assertNotIn('four', self.inputs.subspace) - test_class.assertEqual(self.inputs.subspace['one'], 1) + assert 'one' in self.inputs.subspace + assert 'two' in self.inputs.subspace + assert 'four' not in self.inputs.subspace + assert self.inputs.subspace['one'] == 1 workchain = Wf(inputs=dict(subspace={'one': 1, 'two': 2})) workchain.execute() From 4edd4df27bfd3966bedbba6d63fabed522527c21 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Fri, 31 Jan 2025 00:36:10 +0100 Subject: [PATCH 58/64] Use msgpack for control message passing (#27) * Use yaml FullLoader that handle UUID type as well * Use UnsafeLoader explicitly for bundles * Using msgpack for passing control messages --- pyproject.toml | 2 ++ src/plumpy/process_states.py | 3 +- src/plumpy/rmq/process_control.py | 44 +++++++++++++++++++++++++ tests/rmq/test_communicator.py | 6 ++-- tests/test_persistence.py | 2 +- uv.lock | 54 +++++++++++++++++++++++++++++++ 6 files changed, 106 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4989e624..c61add07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ keywords = ['workflow', 'multithreaded', 'rabbitmq'] requires-python = '>=3.10' dependencies = [ 'kiwipy[rmq]~=0.8.5', + 'msgpack~=1.1', 'nest_asyncio~=1.5,>=1.5.1', 'pyyaml~=6.0', 'typing-extensions~=4.12' @@ -131,6 +132,7 @@ module = [ 'aiocontextvars.*', 'frozendict.*', 'kiwipy.*', + 'msgpack.*', 'nest_asyncio.*', 'tblib.*', ] diff --git a/src/plumpy/process_states.py b/src/plumpy/process_states.py index dceb44c4..4f625272 100644 --- a/src/plumpy/process_states.py +++ b/src/plumpy/process_states.py @@ -15,7 +15,6 @@ import yaml from typing_extensions import Self, override -from yaml.loader import Loader from plumpy.loaders import ObjectLoader from plumpy.message import MessageBuilder, MessageType @@ -547,7 +546,7 @@ def recreate_from(cls, saved_state: SAVED_STATE_TYPE, load_context: LoadSaveCont load_context = ensure_object_loader(load_context, saved_state) obj = auto_load(cls, saved_state, load_context) - obj.exception = yaml.load(saved_state[obj.EXC_VALUE], Loader=Loader) + obj.exception = yaml.load(saved_state[obj.EXC_VALUE], Loader=yaml.UnsafeLoader) if _HAS_TBLIB: try: obj.traceback = tblib.Traceback.from_string(saved_state[obj.TRACEBACK], strict=False) diff --git a/src/plumpy/rmq/process_control.py b/src/plumpy/rmq/process_control.py index 15a6c816..c90daec4 100644 --- a/src/plumpy/rmq/process_control.py +++ b/src/plumpy/rmq/process_control.py @@ -2,9 +2,13 @@ """Module for process level communication functions and classes""" import asyncio +import functools +import uuid from typing import Any, Dict, Hashable, Optional, Sequence, Union import kiwipy +import msgpack +import yaml from plumpy import loaders from plumpy.coordinator import Coordinator @@ -27,6 +31,46 @@ ProcessStatus = Any +# Define yaml type represender and constructor for UUID type handling in message passing +# NOTE: it is recommend to use msgpack for sending message, the yaml is only here for reference. +def uuid_representer(dumper, data): # type: ignore + return dumper.represent_scalar('!uuid', str(data)) + + +def uuid_constructor(loader, node): # type: ignore + value = loader.construct_scalar(node) + return uuid.UUID(value) + + +yaml.add_representer(uuid.UUID, uuid_representer) +yaml.add_constructor('!uuid', uuid_constructor) + +YAML_ENCODER = functools.partial(yaml.dump, encoding='utf-8') +YAML_DECODER = functools.partial(yaml.load, Loader=yaml.FullLoader) + +# Define ext hook for msgpack to handle UUID type in message passing + +UUID_EXT_CODE = 42 # Just pick any integer < 128 + + +def default_uuid_ext(obj: Any) -> msgpack.ExtType: + """Convert UUID objects into a custom msgpack.ExtType.""" + if isinstance(obj, uuid.UUID): + return msgpack.ExtType(UUID_EXT_CODE, obj.bytes) + raise TypeError(f'Cannot serialize type {type(obj)}') + + +def ext_hook(code: Any, data: bytes | None) -> Any: + """Recreate the object from the custom msgpack.ExtType.""" + if code == UUID_EXT_CODE: + return uuid.UUID(bytes=data) + return msgpack.ExtType(code, data) + + +MSGPACK_ENCODER = functools.partial(msgpack.packb, default=default_uuid_ext) +MSGPACK_DECODER = functools.partial(msgpack.unpackb, ext_hook=ext_hook) + + # FIXME: the class not fit typing of ProcessController protocol class RemoteProcessController: """ diff --git a/tests/rmq/test_communicator.py b/tests/rmq/test_communicator.py index c5dc156c..240d781a 100644 --- a/tests/rmq/test_communicator.py +++ b/tests/rmq/test_communicator.py @@ -8,7 +8,8 @@ import uuid import pytest import shortuuid -import yaml +# import yaml +import msgpack from kiwipy.rmq import RmqThreadCommunicator @@ -41,7 +42,8 @@ def _coordinator(): message_exchange=message_exchange, task_exchange=task_exchange, task_queue=task_queue, - decoder=functools.partial(yaml.load, Loader=yaml.Loader), + encoder=process_control.MSGPACK_ENCODER, + decoder=process_control.MSGPACK_DECODER, ) loop = asyncio.get_event_loop() diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 4a5e033b..f8185623 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -162,6 +162,6 @@ def test_bundle_yaml(self): bundle = plumpy.Bundle(Save1()) represent = yaml.dump({'bundle': bundle}) - bundle_loaded = yaml.load(represent, Loader=yaml.Loader)['bundle'] + bundle_loaded = yaml.load(represent, Loader=yaml.UnsafeLoader)['bundle'] assert isinstance(bundle_loaded, plumpy.Bundle) assert bundle_loaded == Save1().save() diff --git a/uv.lock b/uv.lock index 934fa850..f888846a 100644 --- a/uv.lock +++ b/uv.lock @@ -1021,6 +1021,58 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/09/ec/4b43dae793655b7d8a25f76119624350b4d65eb663459eb9603d7f1f0345/mistune-0.8.4-py2.py3-none-any.whl", hash = "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4", size = 16220 }, ] +[[package]] +name = "msgpack" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/d0/7555686ae7ff5731205df1012ede15dd9d927f6227ea151e901c7406af4f/msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e", size = 167260 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/f9/a892a6038c861fa849b11a2bb0502c07bc698ab6ea53359e5771397d883b/msgpack-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd", size = 150428 }, + { url = "https://files.pythonhosted.org/packages/df/7a/d174cc6a3b6bb85556e6a046d3193294a92f9a8e583cdbd46dc8a1d7e7f4/msgpack-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d", size = 84131 }, + { url = "https://files.pythonhosted.org/packages/08/52/bf4fbf72f897a23a56b822997a72c16de07d8d56d7bf273242f884055682/msgpack-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:914571a2a5b4e7606997e169f64ce53a8b1e06f2cf2c3a7273aa106236d43dd5", size = 81215 }, + { url = "https://files.pythonhosted.org/packages/02/95/dc0044b439b518236aaf012da4677c1b8183ce388411ad1b1e63c32d8979/msgpack-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c921af52214dcbb75e6bdf6a661b23c3e6417f00c603dd2070bccb5c3ef499f5", size = 371229 }, + { url = "https://files.pythonhosted.org/packages/ff/75/09081792db60470bef19d9c2be89f024d366b1e1973c197bb59e6aabc647/msgpack-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8ce0b22b890be5d252de90d0e0d119f363012027cf256185fc3d474c44b1b9e", size = 378034 }, + { url = "https://files.pythonhosted.org/packages/32/d3/c152e0c55fead87dd948d4b29879b0f14feeeec92ef1fd2ec21b107c3f49/msgpack-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73322a6cc57fcee3c0c57c4463d828e9428275fb85a27aa2aa1a92fdc42afd7b", size = 363070 }, + { url = "https://files.pythonhosted.org/packages/d9/2c/82e73506dd55f9e43ac8aa007c9dd088c6f0de2aa19e8f7330e6a65879fc/msgpack-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e1f3c3d21f7cf67bcf2da8e494d30a75e4cf60041d98b3f79875afb5b96f3a3f", size = 359863 }, + { url = "https://files.pythonhosted.org/packages/cb/a0/3d093b248837094220e1edc9ec4337de3443b1cfeeb6e0896af8ccc4cc7a/msgpack-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64fc9068d701233effd61b19efb1485587560b66fe57b3e50d29c5d78e7fef68", size = 368166 }, + { url = "https://files.pythonhosted.org/packages/e4/13/7646f14f06838b406cf5a6ddbb7e8dc78b4996d891ab3b93c33d1ccc8678/msgpack-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:42f754515e0f683f9c79210a5d1cad631ec3d06cea5172214d2176a42e67e19b", size = 370105 }, + { url = "https://files.pythonhosted.org/packages/67/fa/dbbd2443e4578e165192dabbc6a22c0812cda2649261b1264ff515f19f15/msgpack-1.1.0-cp310-cp310-win32.whl", hash = "sha256:3df7e6b05571b3814361e8464f9304c42d2196808e0119f55d0d3e62cd5ea044", size = 68513 }, + { url = "https://files.pythonhosted.org/packages/24/ce/c2c8fbf0ded750cb63cbcbb61bc1f2dfd69e16dca30a8af8ba80ec182dcd/msgpack-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:685ec345eefc757a7c8af44a3032734a739f8c45d1b0ac45efc5d8977aa4720f", size = 74687 }, + { url = "https://files.pythonhosted.org/packages/b7/5e/a4c7154ba65d93be91f2f1e55f90e76c5f91ccadc7efc4341e6f04c8647f/msgpack-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7", size = 150803 }, + { url = "https://files.pythonhosted.org/packages/60/c2/687684164698f1d51c41778c838d854965dd284a4b9d3a44beba9265c931/msgpack-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa", size = 84343 }, + { url = "https://files.pythonhosted.org/packages/42/ae/d3adea9bb4a1342763556078b5765e666f8fdf242e00f3f6657380920972/msgpack-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701", size = 81408 }, + { url = "https://files.pythonhosted.org/packages/dc/17/6313325a6ff40ce9c3207293aee3ba50104aed6c2c1559d20d09e5c1ff54/msgpack-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e59bca908d9ca0de3dc8684f21ebf9a690fe47b6be93236eb40b99af28b6ea6", size = 396096 }, + { url = "https://files.pythonhosted.org/packages/a8/a1/ad7b84b91ab5a324e707f4c9761633e357820b011a01e34ce658c1dda7cc/msgpack-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1da8f11a3dd397f0a32c76165cf0c4eb95b31013a94f6ecc0b280c05c91b59", size = 403671 }, + { url = "https://files.pythonhosted.org/packages/bb/0b/fd5b7c0b308bbf1831df0ca04ec76fe2f5bf6319833646b0a4bd5e9dc76d/msgpack-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:452aff037287acb1d70a804ffd022b21fa2bb7c46bee884dbc864cc9024128a0", size = 387414 }, + { url = "https://files.pythonhosted.org/packages/f0/03/ff8233b7c6e9929a1f5da3c7860eccd847e2523ca2de0d8ef4878d354cfa/msgpack-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8da4bf6d54ceed70e8861f833f83ce0814a2b72102e890cbdfe4b34764cdd66e", size = 383759 }, + { url = "https://files.pythonhosted.org/packages/1f/1b/eb82e1fed5a16dddd9bc75f0854b6e2fe86c0259c4353666d7fab37d39f4/msgpack-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:41c991beebf175faf352fb940bf2af9ad1fb77fd25f38d9142053914947cdbf6", size = 394405 }, + { url = "https://files.pythonhosted.org/packages/90/2e/962c6004e373d54ecf33d695fb1402f99b51832631e37c49273cc564ffc5/msgpack-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a52a1f3a5af7ba1c9ace055b659189f6c669cf3657095b50f9602af3a3ba0fe5", size = 396041 }, + { url = "https://files.pythonhosted.org/packages/f8/20/6e03342f629474414860c48aeffcc2f7f50ddaf351d95f20c3f1c67399a8/msgpack-1.1.0-cp311-cp311-win32.whl", hash = "sha256:58638690ebd0a06427c5fe1a227bb6b8b9fdc2bd07701bec13c2335c82131a88", size = 68538 }, + { url = "https://files.pythonhosted.org/packages/aa/c4/5a582fc9a87991a3e6f6800e9bb2f3c82972912235eb9539954f3e9997c7/msgpack-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd2906780f25c8ed5d7b323379f6138524ba793428db5d0e9d226d3fa6aa1788", size = 74871 }, + { url = "https://files.pythonhosted.org/packages/e1/d6/716b7ca1dbde63290d2973d22bbef1b5032ca634c3ff4384a958ec3f093a/msgpack-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d", size = 152421 }, + { url = "https://files.pythonhosted.org/packages/70/da/5312b067f6773429cec2f8f08b021c06af416bba340c912c2ec778539ed6/msgpack-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2", size = 85277 }, + { url = "https://files.pythonhosted.org/packages/28/51/da7f3ae4462e8bb98af0d5bdf2707f1b8c65a0d4f496e46b6afb06cbc286/msgpack-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420", size = 82222 }, + { url = "https://files.pythonhosted.org/packages/33/af/dc95c4b2a49cff17ce47611ca9ba218198806cad7796c0b01d1e332c86bb/msgpack-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2", size = 392971 }, + { url = "https://files.pythonhosted.org/packages/f1/54/65af8de681fa8255402c80eda2a501ba467921d5a7a028c9c22a2c2eedb5/msgpack-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39", size = 401403 }, + { url = "https://files.pythonhosted.org/packages/97/8c/e333690777bd33919ab7024269dc3c41c76ef5137b211d776fbb404bfead/msgpack-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f", size = 385356 }, + { url = "https://files.pythonhosted.org/packages/57/52/406795ba478dc1c890559dd4e89280fa86506608a28ccf3a72fbf45df9f5/msgpack-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247", size = 383028 }, + { url = "https://files.pythonhosted.org/packages/e7/69/053b6549bf90a3acadcd8232eae03e2fefc87f066a5b9fbb37e2e608859f/msgpack-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c", size = 391100 }, + { url = "https://files.pythonhosted.org/packages/23/f0/d4101d4da054f04274995ddc4086c2715d9b93111eb9ed49686c0f7ccc8a/msgpack-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b", size = 394254 }, + { url = "https://files.pythonhosted.org/packages/1c/12/cf07458f35d0d775ff3a2dc5559fa2e1fcd06c46f1ef510e594ebefdca01/msgpack-1.1.0-cp312-cp312-win32.whl", hash = "sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b", size = 69085 }, + { url = "https://files.pythonhosted.org/packages/73/80/2708a4641f7d553a63bc934a3eb7214806b5b39d200133ca7f7afb0a53e8/msgpack-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f", size = 75347 }, + { url = "https://files.pythonhosted.org/packages/c8/b0/380f5f639543a4ac413e969109978feb1f3c66e931068f91ab6ab0f8be00/msgpack-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf", size = 151142 }, + { url = "https://files.pythonhosted.org/packages/c8/ee/be57e9702400a6cb2606883d55b05784fada898dfc7fd12608ab1fdb054e/msgpack-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330", size = 84523 }, + { url = "https://files.pythonhosted.org/packages/7e/3a/2919f63acca3c119565449681ad08a2f84b2171ddfcff1dba6959db2cceb/msgpack-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734", size = 81556 }, + { url = "https://files.pythonhosted.org/packages/7c/43/a11113d9e5c1498c145a8925768ea2d5fce7cbab15c99cda655aa09947ed/msgpack-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e", size = 392105 }, + { url = "https://files.pythonhosted.org/packages/2d/7b/2c1d74ca6c94f70a1add74a8393a0138172207dc5de6fc6269483519d048/msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca", size = 399979 }, + { url = "https://files.pythonhosted.org/packages/82/8c/cf64ae518c7b8efc763ca1f1348a96f0e37150061e777a8ea5430b413a74/msgpack-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915", size = 383816 }, + { url = "https://files.pythonhosted.org/packages/69/86/a847ef7a0f5ef3fa94ae20f52a4cacf596a4e4a010197fbcc27744eb9a83/msgpack-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d", size = 380973 }, + { url = "https://files.pythonhosted.org/packages/aa/90/c74cf6e1126faa93185d3b830ee97246ecc4fe12cf9d2d31318ee4246994/msgpack-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434", size = 387435 }, + { url = "https://files.pythonhosted.org/packages/7a/40/631c238f1f338eb09f4acb0f34ab5862c4e9d7eda11c1b685471a4c5ea37/msgpack-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c", size = 399082 }, + { url = "https://files.pythonhosted.org/packages/e9/1b/fa8a952be252a1555ed39f97c06778e3aeb9123aa4cccc0fd2acd0b4e315/msgpack-1.1.0-cp313-cp313-win32.whl", hash = "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc", size = 69037 }, + { url = "https://files.pythonhosted.org/packages/b6/bc/8bd826dd03e022153bfa1766dcdec4976d6c818865ed54223d71f07862b3/msgpack-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f", size = 75140 }, +] + [[package]] name = "multidict" version = "6.1.0" @@ -1452,6 +1504,7 @@ name = "plumpy" source = { editable = "." } dependencies = [ { name = "kiwipy", extra = ["rmq"] }, + { name = "msgpack" }, { name = "nest-asyncio" }, { name = "pyyaml" }, { name = "typing-extensions" }, @@ -1493,6 +1546,7 @@ requires-dist = [ { name = "kiwipy", extras = ["docs"], marker = "extra == 'docs'", specifier = "~=0.8.3" }, { name = "kiwipy", extras = ["rmq"], specifier = "~=0.8.5" }, { name = "markupsafe", marker = "extra == 'docs'", specifier = "==2.0.1" }, + { name = "msgpack", specifier = "~=1.1" }, { name = "mypy", marker = "extra == 'pre-commit'", specifier = "==1.13.0" }, { name = "myst-nb", marker = "extra == 'docs'", specifier = "~=0.11.0" }, { name = "nest-asyncio", specifier = "~=1.5,>=1.5.1" }, From 07303946c7f64ded918d9e2a9b0fda1e72a2c56e Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Fri, 31 Jan 2025 01:20:57 +0100 Subject: [PATCH 59/64] Create/Launch/Continue body into builder (#26) Using the same interface for creating all message type, create/launch/continue, play/pause/kill/status It revert the message API did by using MessageBuilder, it cannot handle `MessageBuilder.continue`, it is not a good API design. --- src/plumpy/__init__.py | 28 ++--- src/plumpy/message.py | 184 +++++++++++++++++------------- src/plumpy/process_states.py | 14 +-- src/plumpy/processes.py | 16 +-- src/plumpy/rmq/process_control.py | 49 ++++---- tests/test_message.py | 8 +- tests/test_process_states.py | 4 +- tests/test_processes.py | 5 +- tests/utils.py | 4 +- 9 files changed, 163 insertions(+), 149 deletions(-) diff --git a/src/plumpy/__init__.py b/src/plumpy/__init__.py index 4cb50820..91d54bf8 100644 --- a/src/plumpy/__init__.py +++ b/src/plumpy/__init__.py @@ -28,7 +28,7 @@ ) from .futures import CancellableAction, Future, capture_exceptions, create_task from .loaders import DefaultObjectLoader, ObjectLoader, get_object_loader, set_object_loader -from .message import MessageBuilder, ProcessLauncher, create_continue_body, create_launch_body +from .message import Message, MsgContinue, MsgCreate, MsgKill, MsgLaunch, MsgPause, MsgPlay, MsgStatus, ProcessLauncher from .persistence import ( Bundle, InMemoryPersister, @@ -64,26 +64,17 @@ from .workchains import ToContext, WorkChain, WorkChainSpec, if_, return_, while_ __all__ = ( - # ports 'UNSPECIFIED', - # utils 'AttributesDict', - # persistence 'Bundle', - # processes 'BundleKeys', - # futures 'CancellableAction', - # exceptions 'ClosedError', - # process_states/States 'Continue', - # coordinator 'Coordinator', 'CoordinatorConnectionError', 'CoordinatorTimeoutError', 'Created', - # loaders 'DefaultObjectLoader', 'Excepted', 'Finished', @@ -92,14 +83,19 @@ 'InputPort', 'Interruption', 'InvalidStateError', - # process_states/Commands 'Kill', 'KillInterruption', 'Killed', 'KilledError', 'LoadSaveContext', - # message - 'MessageBuilder', + 'Message', + 'MsgContinue', + 'MsgCreate', + 'MsgKill', + 'MsgLaunch', + 'MsgPause', + 'MsgPlay', + 'MsgStatus', 'ObjectLoader', 'OutputPort', 'PauseInterruption', @@ -107,16 +103,13 @@ 'PersistenceError', 'Persister', 'PicklePersister', - # event 'PlumpyEventLoopPolicy', 'Port', 'PortNamespace', 'PortValidationError', 'Process', - # controller 'ProcessController', 'ProcessLauncher', - # process_listener 'ProcessListener', 'ProcessSpec', 'ProcessState', @@ -124,7 +117,6 @@ 'Savable', 'SavableFuture', 'Stop', - # workchain 'ToContext', 'TransitionFailed', 'UnsuccessfulResult', @@ -134,8 +126,6 @@ 'WorkChainSpec', 'auto_persist', 'capture_exceptions', - 'create_continue_body', - 'create_launch_body', 'create_task', 'get_event_loop', 'get_object_loader', diff --git a/src/plumpy/message.py b/src/plumpy/message.py index 26cad8c5..95c0d754 100644 --- a/src/plumpy/message.py +++ b/src/plumpy/message.py @@ -48,124 +48,146 @@ class Intent: LOGGER = logging.getLogger(__name__) MessageType = dict[str, Any] +Message = dict[str, Any] -class MessageBuilder: - """MessageBuilder will construct different messages that can passing over coordinator.""" - +class MsgPlay: @classmethod - def play(cls, text: str | None = None) -> MessageType: + def new(cls, text: str | None = None) -> Message: """The play message send over coordinator.""" return { INTENT_KEY: Intent.PLAY, MESSAGE_TEXT_KEY: text, } + +class MsgPause: + """ + The 'pause' message sent over a coordinator. + """ + @classmethod - def pause(cls, text: str | None = None) -> MessageType: - """The pause message send over coordinator.""" + def new(cls, text: str | None = None) -> MessageType: return { INTENT_KEY: Intent.PAUSE, MESSAGE_TEXT_KEY: text, } + +class MsgKill: + """ + The 'kill' message sent over a coordinator. + """ + @classmethod - def kill(cls, text: str | None = None, force_kill: bool = False) -> MessageType: - """The kill message send over coordinator.""" + def new(cls, text: str | None = None, force_kill: bool = False) -> MessageType: return { INTENT_KEY: Intent.KILL, MESSAGE_TEXT_KEY: text, FORCE_KILL_KEY: force_kill, } + +class MsgStatus: + """ + The 'status' message sent over a coordinator. + """ + @classmethod - def status(cls, text: str | None = None) -> MessageType: - """The status message send over coordinator.""" + def new(cls, text: str | None = None) -> MessageType: return { INTENT_KEY: Intent.STATUS, MESSAGE_TEXT_KEY: text, } -def create_launch_body( - process_class: str, - init_args: Sequence[Any] | None = None, - init_kwargs: dict[str, Any] | None = None, - persist: bool = False, - loader: loaders.ObjectLoader | None = None, - nowait: bool = True, -) -> dict[str, Any]: - """ - Create a message body for the launch action - - :param process_class: the class of the process to launch - :param init_args: any initialisation positional arguments - :param init_kwargs: any initialisation keyword arguments - :param persist: persist this process if True, otherwise don't - :param loader: the loader to use to load the persisted process - :param nowait: wait for the process to finish before completing the task, otherwise just return the PID - :return: a dictionary with the body of the message to launch the process - :rtype: dict +class MsgLaunch: """ - if loader is None: - loader = loaders.get_object_loader() - - msg_body = { - TASK_KEY: LAUNCH_TASK, - TASK_ARGS: { - PROCESS_CLASS_KEY: loader.identify_object(process_class), - PERSIST_KEY: persist, - NOWAIT_KEY: nowait, - ARGS_KEY: init_args, - KWARGS_KEY: init_kwargs, - }, - } - return msg_body - - -def create_continue_body(pid: 'PID_TYPE', tag: str | None = None, nowait: bool = False) -> dict[str, Any]: + Create the message payload for the launch action. """ - Create a message body to continue an existing process - :param pid: the pid of the existing process - :param tag: the optional persistence tag - :param nowait: wait for the process to finish before completing the task, otherwise just return the PID - :return: a dictionary with the body of the message to continue the process - """ - msg_body = {TASK_KEY: CONTINUE_TASK, TASK_ARGS: {PID_KEY: pid, NOWAIT_KEY: nowait, TAG_KEY: tag}} - return msg_body + @classmethod + def new( + cls, + process_class: str, + init_args: Sequence[Any] | None = None, + init_kwargs: dict[str, Any] | None = None, + persist: bool = False, + loader: 'loaders.ObjectLoader | None' = None, + nowait: bool = True, + ) -> dict[str, Any]: + """ + Create a message body for the launch action + """ + if loader is None: + loader = loaders.get_object_loader() + + return { + TASK_KEY: LAUNCH_TASK, + TASK_ARGS: { + PROCESS_CLASS_KEY: loader.identify_object(process_class), + PERSIST_KEY: persist, + NOWAIT_KEY: nowait, + ARGS_KEY: init_args, + KWARGS_KEY: init_kwargs, + }, + } -def create_create_body( - process_class: str, - init_args: Sequence[Any] | None = None, - init_kwargs: dict[str, Any] | None = None, - persist: bool = False, - loader: loaders.ObjectLoader | None = None, -) -> dict[str, Any]: +class MsgContinue: + """ + Create the message payload to continue an existing process. """ - Create a message body to create a new process - :param process_class: the class of the process to launch - :param init_args: any initialisation positional arguments - :param init_kwargs: any initialisation keyword arguments - :param persist: persist this process if True, otherwise don't - :param loader: the loader to use to load the persisted process - :return: a dictionary with the body of the message to launch the process + @classmethod + def new( + cls, + pid: 'PID_TYPE', + tag: str | None = None, + nowait: bool = False, + ) -> dict[str, Any]: + """ + Create a message body to continue an existing process. + """ + return { + TASK_KEY: CONTINUE_TASK, + TASK_ARGS: { + PID_KEY: pid, + NOWAIT_KEY: nowait, + TAG_KEY: tag, + }, + } + + +class MsgCreate: + """ + Create the message payload to create a new process. """ - if loader is None: - loader = loaders.get_object_loader() - - msg_body = { - TASK_KEY: CREATE_TASK, - TASK_ARGS: { - PROCESS_CLASS_KEY: loader.identify_object(process_class), - PERSIST_KEY: persist, - ARGS_KEY: init_args, - KWARGS_KEY: init_kwargs, - }, - } - return msg_body + + @classmethod + def new( + cls, + process_class: str, + init_args: Sequence[Any] | None = None, + init_kwargs: dict[str, Any] | None = None, + persist: bool = False, + loader: 'loaders.ObjectLoader | None' = None, + ) -> dict[str, Any]: + """ + Create a message body to create a new process. + """ + if loader is None: + loader = loaders.get_object_loader() + + return { + TASK_KEY: CREATE_TASK, + TASK_ARGS: { + PROCESS_CLASS_KEY: loader.identify_object(process_class), + PERSIST_KEY: persist, + ARGS_KEY: init_args, + KWARGS_KEY: init_kwargs, + }, + } class ProcessLauncher: diff --git a/src/plumpy/process_states.py b/src/plumpy/process_states.py index 4f625272..0603a2ef 100644 --- a/src/plumpy/process_states.py +++ b/src/plumpy/process_states.py @@ -17,7 +17,7 @@ from typing_extensions import Self, override from plumpy.loaders import ObjectLoader -from plumpy.message import MessageBuilder, MessageType +from plumpy.message import Message, MsgKill, MsgPause from plumpy.persistence import ensure_object_loader try: @@ -64,17 +64,17 @@ class Interruption(Exception): # noqa: N818 class KillInterruption(Interruption): def __init__(self, msg_text: str | None): super().__init__() - msg = MessageBuilder.kill(text=msg_text) + msg = MsgKill.new(text=msg_text) - self.msg: MessageType = msg + self.msg: Message = msg class PauseInterruption(Interruption): def __init__(self, msg_text: str | None): super().__init__() - msg = MessageBuilder.pause(text=msg_text) + msg = MsgPause.new(text=msg_text) - self.msg: MessageType = msg + self.msg: Message = msg # region Commands @@ -104,7 +104,7 @@ def save(self, loader: ObjectLoader | None = None) -> SAVED_STATE_TYPE: @auto_persist('msg') class Kill(Command): - def __init__(self, msg: MessageType | None = None): + def __init__(self, msg: Message | None = None): super().__init__() self.msg = msg @@ -633,7 +633,7 @@ class Killed: is_terminal: ClassVar[bool] = True - def __init__(self, msg: MessageType | None): + def __init__(self, msg: Message | None): """ :param msg: Optional kill message """ diff --git a/src/plumpy/processes.py b/src/plumpy/processes.py index de96131a..ec215ecc 100644 --- a/src/plumpy/processes.py +++ b/src/plumpy/processes.py @@ -53,7 +53,7 @@ from .base.utils import call_with_super_check, super_check from .event_helper import EventHelper from .futures import CancellableAction, capture_exceptions -from .message import MESSAGE_TEXT_KEY, MessageBuilder, MessageType +from .message import MESSAGE_TEXT_KEY, Message, MsgKill, MsgPause from .process_listener import ProcessListener from .process_spec import ProcessSpec from .utils import PID_TYPE, SAVED_STATE_TYPE, protected @@ -564,7 +564,7 @@ def killed(self) -> bool: """Return whether the process is killed.""" return self.state_label == process_states.ProcessState.KILLED - def killed_msg(self) -> MessageType | None: + def killed_msg(self) -> Message | None: """Return the killed message.""" if isinstance(self.state, process_states.Killed): return self.state.msg @@ -899,7 +899,7 @@ def on_excepted(self) -> None: self._fire_event(ProcessListener.on_process_excepted, str(self.future().exception())) @super_check - def on_kill(self, msg: MessageType | None) -> None: + def on_kill(self, msg: Message | None) -> None: """Entering the KILLED state.""" if msg is None: msg_txt = '' @@ -945,7 +945,7 @@ def _fire_event(self, evt: Callable[..., Any], *args: Any, **kwargs: Any) -> Non # region Communication - def message_receive(self, _comm: Coordinator, msg: MessageType) -> Any: + def message_receive(self, _comm: Coordinator, msg: Message) -> Any: """ Coroutine called when the process receives a message from the communicator @@ -977,7 +977,7 @@ def message_receive(self, _comm: Coordinator, msg: MessageType) -> Any: raise RuntimeError('Unknown intent') def broadcast_receive( - self, _comm: Coordinator, msg: MessageType, sender: Any, subject: Any, correlation_id: Any + self, _comm: Coordinator, msg: Message, sender: Any, subject: Any, correlation_id: Any ) -> concurrent.futures.Future | None: """ Coroutine called when the process receives a message from the communicator @@ -1141,14 +1141,14 @@ def pause(self, msg_text: str | None = None) -> bool | CancellableAction: self._state.interrupt(interrupt_exception) return cast(CancellableAction, self._interrupt_action) - msg = MessageBuilder.pause(msg_text) + msg = MsgPause.new(msg_text) return self._do_pause(state_msg=msg) @staticmethod def _interrupt(state: Interruptable, reason: Exception) -> None: state.interrupt(reason) - def _do_pause(self, state_msg: MessageType | None, next_state: state_machine.State | None = None) -> bool: + def _do_pause(self, state_msg: Message | None, next_state: state_machine.State | None = None) -> bool: """Carry out the pause procedure, optionally transitioning to the next state first""" try: if next_state is not None: @@ -1267,7 +1267,7 @@ def kill(self, msg_text: str | None = None) -> bool | asyncio.Future: self._state.interrupt(interrupt_exception) return cast(CancellableAction, self._interrupt_action) - msg = MessageBuilder.kill(msg_text) + msg = MsgKill.new(msg_text) new_state = create_state(self, process_states.ProcessState.KILLED, msg=msg) self.transition_to(new_state) return True diff --git a/src/plumpy/rmq/process_control.py b/src/plumpy/rmq/process_control.py index c90daec4..4c644502 100644 --- a/src/plumpy/rmq/process_control.py +++ b/src/plumpy/rmq/process_control.py @@ -14,11 +14,14 @@ from plumpy.coordinator import Coordinator from plumpy.message import ( Intent, - MessageBuilder, - MessageType, - create_continue_body, - create_create_body, - create_launch_body, + Message, + MsgContinue, + MsgCreate, + MsgKill, + MsgLaunch, + MsgPause, + MsgPlay, + MsgStatus, ) from plumpy.utils import PID_TYPE @@ -87,7 +90,7 @@ async def get_status(self, pid: 'PID_TYPE') -> 'ProcessStatus': :param pid: the process id :return: the status response from the process """ - future = self._coordinator.rpc_send(pid, MessageBuilder.status()) + future = self._coordinator.rpc_send(pid, MsgStatus.new()) result = await asyncio.wrap_future(future) return result @@ -99,7 +102,7 @@ async def pause_process(self, pid: 'PID_TYPE', msg_text: Optional[str] = None) - :param msg: optional pause message :return: True if paused, False otherwise """ - msg = MessageBuilder.pause(text=msg_text) + msg = MsgPause.new(text=msg_text) pause_future = self._coordinator.rpc_send(pid, msg) # rpc_send return a thread future from coordinator @@ -115,7 +118,7 @@ async def play_process(self, pid: 'PID_TYPE') -> 'ProcessResult': :param pid: the pid of the process to play :return: True if played, False otherwise """ - play_future = self._coordinator.rpc_send(pid, MessageBuilder.play()) + play_future = self._coordinator.rpc_send(pid, MsgPlay.new()) future = await asyncio.wrap_future(play_future) result = await asyncio.wrap_future(future) return result @@ -128,7 +131,7 @@ async def kill_process(self, pid: 'PID_TYPE', msg_text: Optional[str] = None) -> :param msg: optional kill message :return: True if killed, False otherwise """ - msg = MessageBuilder.kill(text=msg_text) + msg = MsgKill.new(text=msg_text) # Wait for the communication to go through kill_future = self._coordinator.rpc_send(pid, msg) @@ -147,7 +150,7 @@ async def continue_process( :param pid: the pid of the process to continue :param tag: the checkpoint tag to continue from """ - message = create_continue_body(pid=pid, tag=tag, nowait=nowait) + message = MsgContinue.new(pid=pid, tag=tag, nowait=nowait) # Wait for the communication to go through continue_future = self._coordinator.task_send(message, no_reply=no_reply) future = await asyncio.wrap_future(continue_future) @@ -182,7 +185,7 @@ async def launch_process( :return: the result of launching the process """ - message = create_launch_body(process_class, init_args, init_kwargs, persist, loader, nowait) + message = MsgLaunch.new(process_class, init_args, init_kwargs, persist, loader, nowait) launch_future = self._coordinator.task_send(message, no_reply=no_reply) future = await asyncio.wrap_future(launch_future) @@ -215,13 +218,13 @@ async def execute_process( :return: the result of executing the process """ - message = create_create_body(process_class, init_args, init_kwargs, persist=True, loader=loader) + message = MsgCreate.new(process_class, init_args, init_kwargs, persist=True, loader=loader) create_future = self._coordinator.task_send(message) future = await asyncio.wrap_future(create_future) pid: 'PID_TYPE' = await asyncio.wrap_future(future) - message = create_continue_body(pid, nowait=nowait) + message = MsgContinue.new(pid, nowait=nowait) continue_future = self._coordinator.task_send(message, no_reply=no_reply) future = await asyncio.wrap_future(continue_future) @@ -253,7 +256,7 @@ def get_status(self, pid: 'PID_TYPE') -> kiwipy.Future: :param pid: the process id :return: the status response from the process """ - return self._coordinator.rpc_send(pid, MessageBuilder.status()) + return self._coordinator.rpc_send(pid, MsgStatus.new()) def pause_process(self, pid: 'PID_TYPE', msg_text: Optional[str] = None) -> kiwipy.Future: """ @@ -264,7 +267,7 @@ def pause_process(self, pid: 'PID_TYPE', msg_text: Optional[str] = None) -> kiwi :return: a response future from the process to be paused """ - msg = MessageBuilder.pause(text=msg_text) + msg = MsgPause.new(text=msg_text) return self._coordinator.rpc_send(pid, msg) @@ -274,7 +277,7 @@ def pause_all(self, msg_text: Optional[str]) -> None: :param msg: an optional pause message """ - msg = MessageBuilder.pause(text=msg_text) + msg = MsgPause.new(text=msg_text) self._coordinator.broadcast_send(msg, subject=Intent.PAUSE) def play_process(self, pid: 'PID_TYPE') -> kiwipy.Future: @@ -285,7 +288,7 @@ def play_process(self, pid: 'PID_TYPE') -> kiwipy.Future: :return: a response future from the process to be played """ - return self._coordinator.rpc_send(pid, MessageBuilder.play()) + return self._coordinator.rpc_send(pid, MsgPlay.new()) def play_all(self) -> None: """ @@ -301,7 +304,7 @@ def kill_process(self, pid: 'PID_TYPE', msg_text: Optional[str] = None) -> kiwip :param msg: optional kill message :return: a response future from the process to be killed """ - msg = MessageBuilder.kill(text=msg_text) + msg = MsgKill.new(text=msg_text) return self._coordinator.rpc_send(pid, msg) def kill_all(self, msg_text: Optional[str]) -> None: @@ -310,11 +313,11 @@ def kill_all(self, msg_text: Optional[str]) -> None: :param msg: an optional pause message """ - msg = MessageBuilder.kill(msg_text) + msg = MsgKill.new(msg_text) self._coordinator.broadcast_send(msg, subject=Intent.KILL) - def notify_msg(self, msg: MessageType, sender: Hashable | None = None, subject: str | None = None) -> None: + def notify_msg(self, msg: Message, sender: Hashable | None = None, subject: str | None = None) -> None: """ Notify all processes by broadcasting of a msg @@ -325,7 +328,7 @@ def notify_msg(self, msg: MessageType, sender: Hashable | None = None, subject: def continue_process( self, pid: 'PID_TYPE', tag: Optional[str] = None, nowait: bool = False, no_reply: bool = False ) -> Union[None, PID_TYPE, ProcessResult]: - message = create_continue_body(pid=pid, tag=tag, nowait=nowait) + message = MsgContinue.new(pid=pid, tag=tag, nowait=nowait) return self._coordinator.task_send(message, no_reply=no_reply) def launch_process( @@ -350,7 +353,7 @@ def launch_process( :param no_reply: don't send a reply to the sender :return: the pid of the created process or the outputs (if nowait=False) """ - message = create_launch_body(process_class, init_args, init_kwargs, persist, loader, nowait) + message = MsgLaunch.new(process_class, init_args, init_kwargs, persist, loader, nowait) return self._coordinator.task_send(message, no_reply=no_reply) def execute_process( @@ -375,7 +378,7 @@ def execute_process( :param no_reply: if True, this call will be fire-and-forget, i.e. no return value :return: the result of executing the process """ - message = create_create_body(process_class, init_args, init_kwargs, persist=True, loader=loader) + message = MsgCreate.new(process_class, init_args, init_kwargs, persist=True, loader=loader) execute_future = kiwipy.Future() create_future = self._coordinator.task_send(message) diff --git a/tests/test_message.py b/tests/test_message.py index 0a6ee96c..dd42c137 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -2,7 +2,7 @@ import pytest import plumpy -from plumpy import message +from plumpy.message import MsgContinue, TASK_ARGS from tests import utils @@ -37,7 +37,7 @@ async def test_continue(): del process process = None - result = await launcher._continue(**plumpy.create_continue_body(pid)[message.TASK_ARGS]) + result = await launcher._continue(**MsgContinue.new(pid)[TASK_ARGS]) assert result == utils.DummyProcess.EXPECTED_OUTPUTS @@ -50,6 +50,6 @@ async def test_loader_is_used(): persister.save_checkpoint(proc) launcher = plumpy.ProcessLauncher(persister=persister, loader=loader) - continue_task = plumpy.create_continue_body(proc.pid) - result = await launcher._continue(**continue_task[message.TASK_ARGS]) + continue_task = MsgContinue.new(proc.pid) + result = await launcher._continue(**continue_task[TASK_ARGS]) assert result == utils.DummyProcess.EXPECTED_OUTPUTS diff --git a/tests/test_process_states.py b/tests/test_process_states.py index 07f41634..be142b59 100644 --- a/tests/test_process_states.py +++ b/tests/test_process_states.py @@ -4,7 +4,7 @@ from plumpy import process_states from plumpy.base.state_machine import StateMachine -from plumpy.message import MessageBuilder +from plumpy.message import MsgKill from plumpy.persistence import LoadSaveContext, Savable, load from plumpy.process_states import Command, Created, Excepted, Finished, Killed, Running, Waiting from plumpy.processes import Process @@ -84,7 +84,7 @@ def test_finished_savable(): def test_killed_savable(): - state = Killed(msg=MessageBuilder.kill('kill it')) + state = Killed(msg=MsgKill.new('kill it')) assert isinstance(state, Savable) saved_state = state.save() diff --git a/tests/test_processes.py b/tests/test_processes.py index 19544ae3..767442e4 100644 --- a/tests/test_processes.py +++ b/tests/test_processes.py @@ -9,7 +9,7 @@ import plumpy from plumpy import BundleKeys, Process, ProcessState -from plumpy.message import MESSAGE_TEXT_KEY, MessageBuilder +from plumpy.message import MESSAGE_TEXT_KEY, MsgKill from plumpy.persistence import Savable from plumpy.utils import AttributesFrozendict from . import utils @@ -435,7 +435,7 @@ class KillProcess(Process): after_kill = False def run(self, **kwargs): - msg = MessageBuilder.kill(text='killed') + msg = MsgKill.new(text='killed') self.kill(msg) # The following line should be executed because kill will not # interrupt execution of a method call in the RUNNING state @@ -577,7 +577,6 @@ def run(self): assert proc.state_label == plumpy.ProcessState.FINISHED def test_process_stack(self): - class StackTest(plumpy.Process): def run(self): assert self is Process.current() diff --git a/tests/utils.py b/tests/utils.py index a19563e1..2d19e333 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -11,7 +11,7 @@ import plumpy from plumpy import persistence, process_states, processes, utils from plumpy.exceptions import CoordinatorConnectionError -from plumpy.message import MessageBuilder +from plumpy.message import MsgKill from plumpy.rmq import TaskRejected import shortuuid @@ -208,7 +208,7 @@ def last_step(self): class KillProcess(processes.Process): @utils.override def run(self): - msg = MessageBuilder.kill(text='killed') + msg = MsgKill.new(text='killed') return process_states.Kill(msg=msg) From c6d5b8fd9749308e4b4750106bca68df98a73546 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Fri, 31 Jan 2025 01:21:52 +0100 Subject: [PATCH 60/64] Correct typing in workchain.py (#21) --- src/plumpy/workchains.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/plumpy/workchains.py b/src/plumpy/workchains.py index 693b0684..b2ab7cb1 100644 --- a/src/plumpy/workchains.py +++ b/src/plumpy/workchains.py @@ -2,7 +2,6 @@ import abc import asyncio -import collections import inspect import logging import re @@ -157,6 +156,7 @@ def save(self, loader: ObjectLoader | None = None) -> SAVED_STATE_TYPE: """ out_state: SAVED_STATE_TYPE = auto_save(self, loader) + # TODO: revert condition and raise if not savable if isinstance(self._state, persistence.Savable): out_state['_state'] = self._state.save() @@ -466,15 +466,14 @@ def __str__(self) -> str: return str(self._pos) + ':' + str(self._child_stepper) -class _Block(_Instruction, collections.abc.Sequence): +class _Block(_Instruction, Sequence[_Instruction]): """ Represents a block of instructions i.e. a sequential list of instructions. """ - # XXX: swap workchain and instructions def __init__(self, instructions: Sequence[_Instruction | WC_COMMAND_TYPE]) -> None: # Build up the list of commands - comms: MutableSequence[_Instruction | _FunctionCall] = [] + comms: MutableSequence[_Instruction] = [] for instruction in instructions: if not isinstance(instruction, _Instruction): # Assume it's a function call @@ -482,9 +481,9 @@ def __init__(self, instructions: Sequence[_Instruction | WC_COMMAND_TYPE]) -> No else: comms.append(instruction) - self._instruction: MutableSequence[_Instruction | _FunctionCall] = comms + self._instruction: MutableSequence[_Instruction] = comms - def __getitem__(self, index: int) -> _Instruction | _FunctionCall: # type: ignore + def __getitem__(self, index: int) -> _Instruction: # type: ignore return self._instruction[index] def __len__(self) -> int: @@ -625,7 +624,7 @@ def __str__(self) -> str: return string -class _If(_Instruction, collections.abc.Sequence): +class _If(_Instruction, Sequence[_Conditional]): def __init__(self, condition: PREDICATE_TYPE) -> None: super().__init__() self._ifs: list[_Conditional] = [_Conditional(self, condition, label=if_.__name__)] @@ -668,6 +667,8 @@ def recreate_stepper(self, saved_state: SAVED_STATE_TYPE, workchain: 'WorkChain' return _IfStepper.recreate_from(saved_state, load_context) def get_description(self) -> Mapping[str, Any]: + import collections + description = collections.OrderedDict() description[f'if({self._ifs[0].predicate.__name__})'] = self._ifs[0].body.get_description() @@ -737,7 +738,7 @@ def __str__(self) -> str: return string -class _While(_Conditional, _Instruction, collections.abc.Sequence): +class _While(_Conditional, _Instruction, Sequence[_Conditional]): def __init__(self, predicate: PREDICATE_TYPE) -> None: super().__init__(self, predicate, label=while_.__name__) From 49ea4e1de2c7a90350ac508015a2e58242a443f8 Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Fri, 31 Jan 2025 14:03:39 +0100 Subject: [PATCH 61/64] Bump to use pytest-asyncio ~=0.25 which deprecate get_event_loop (#29) The event loop set up of coordinator and controller is from using the same event loop of a single async pytest. This maximum the decouple of tests in `tests/rmq/test_communication.py`. The fixture and the event loop scope of async test are both set to "function". --- pyproject.toml | 5 +- src/plumpy/rmq/process_control.py | 8 ++ tests/rmq/test_communicator.py | 137 ++++++++++++++++++++---------- uv.lock | 16 ++-- 4 files changed, 109 insertions(+), 57 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c61add07..77f56c0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,8 +57,8 @@ pre-commit = [ ] tests = [ 'ipykernel==6.12.1', - 'pytest~=7.0', - 'pytest-asyncio~=0.12,<0.17', + 'pytest~=8.0', + 'pytest-asyncio~=0.25', 'pytest-cov~=4.1', 'pytest-notebook>=0.8.0', 'shortuuid==1.0.8', @@ -163,6 +163,7 @@ testpaths = [ 'test', ] filterwarnings = [] +asyncio_default_fixture_loop_scope = "function" [tool.yapf] align_closing_bracket_with_visual_indent = true diff --git a/src/plumpy/rmq/process_control.py b/src/plumpy/rmq/process_control.py index 4c644502..078a926e 100644 --- a/src/plumpy/rmq/process_control.py +++ b/src/plumpy/rmq/process_control.py @@ -84,6 +84,10 @@ class RemoteProcessController: def __init__(self, coordinator: Coordinator) -> None: self._coordinator = coordinator + @property + def coordinator(self) -> Coordinator: + return self._coordinator + async def get_status(self, pid: 'PID_TYPE') -> 'ProcessStatus': """ Get the status of a process with the given PID @@ -250,6 +254,10 @@ def __init__(self, coordinator: Coordinator): """ self._coordinator = coordinator + @property + def coordinator(self) -> Coordinator: + return self._coordinator + def get_status(self, pid: 'PID_TYPE') -> kiwipy.Future: """Get the status of a process with the given PID. diff --git a/tests/rmq/test_communicator.py b/tests/rmq/test_communicator.py index 240d781a..8f4a0de1 100644 --- a/tests/rmq/test_communicator.py +++ b/tests/rmq/test_communicator.py @@ -2,19 +2,15 @@ """Tests for the :mod:`plumpy.rmq.communicator` module.""" import asyncio -import functools import shutil import tempfile import uuid import pytest import shortuuid -# import yaml -import msgpack from kiwipy.rmq import RmqThreadCommunicator import plumpy -from plumpy.coordinator import Coordinator from plumpy.rmq import communications, process_control from . import RmqCoordinator @@ -56,36 +52,71 @@ def _coordinator(): coordinator.close() -@pytest.fixture -def async_controller(_coordinator): - yield process_control.RemoteProcessController(_coordinator) +@pytest.fixture(scope='function') +def make_coordinator(): + def _coordinator(loop=None): + message_exchange = f'{__file__}.{shortuuid.uuid()}' + task_exchange = f'{__file__}.{shortuuid.uuid()}' + task_queue = f'{__file__}.{shortuuid.uuid()}' + + thread_comm = RmqThreadCommunicator.connect( + connection_params={'url': 'amqp://guest:guest@localhost:5672/'}, + message_exchange=message_exchange, + task_exchange=task_exchange, + task_queue=task_queue, + encoder=process_control.MSGPACK_ENCODER, + decoder=process_control.MSGPACK_DECODER, + ) + + loop = loop or asyncio.get_event_loop() + loop.set_debug(True) + comm = communications.LoopCommunicator(thread_comm, loop=loop) + coordinator = RmqCoordinator(comm) + + return coordinator + + return _coordinator + + +@pytest.fixture(scope='function') +def make_controller(make_coordinator): + def _controller(loop=None): + coordinator = make_coordinator(loop) + controller = process_control.RemoteProcessController(coordinator) + + return controller + + return _controller class TestLoopCommunicator: """Make sure the loop communicator is working as expected""" @pytest.mark.asyncio - async def test_broadcast(self, _coordinator): + async def test_broadcast(self, make_coordinator): + loop = asyncio.get_running_loop() + coordinator = make_coordinator(loop) BROADCAST = {'body': 'present', 'sender': 'Martin', 'subject': 'sup', 'correlation_id': 420} # noqa: N806 broadcast_future = asyncio.Future() - loop = asyncio.get_event_loop() - def get_broadcast(_comm, body, sender, subject, correlation_id): - assert loop is asyncio.get_event_loop() + assert loop is asyncio.get_running_loop() broadcast_future.set_result( {'body': body, 'sender': sender, 'subject': subject, 'correlation_id': correlation_id} ) - _coordinator.add_broadcast_subscriber(get_broadcast) - _coordinator.broadcast_send(**BROADCAST) + coordinator.add_broadcast_subscriber(get_broadcast) + coordinator.broadcast_send(**BROADCAST) result = await broadcast_future assert result == BROADCAST @pytest.mark.asyncio - async def test_broadcast_filter(self, _coordinator: Coordinator): + async def test_broadcast_filter(self, make_coordinator): + loop = asyncio.get_running_loop() + coordinator = make_coordinator(loop) + broadcast_future = asyncio.Future() def ignore_broadcast(_comm, body, sender, subject, correlation_id): @@ -94,15 +125,18 @@ def ignore_broadcast(_comm, body, sender, subject, correlation_id): def get_broadcast(_comm, body, sender, subject, correlation_id): broadcast_future.set_result(True) - _coordinator.add_broadcast_subscriber(ignore_broadcast, subject_filters=['other']) - _coordinator.add_broadcast_subscriber(get_broadcast) - _coordinator.broadcast_send(**{'body': 'present', 'sender': 'Martin', 'subject': 'sup', 'correlation_id': 420}) + coordinator.add_broadcast_subscriber(ignore_broadcast, subject_filters=['other']) + coordinator.add_broadcast_subscriber(get_broadcast) + coordinator.broadcast_send(**{'body': 'present', 'sender': 'Martin', 'subject': 'sup', 'correlation_id': 420}) result = await broadcast_future assert result is True @pytest.mark.asyncio - async def test_rpc(self, _coordinator): + async def test_rpc(self, make_coordinator): + loop = asyncio.get_running_loop() + coordinator = make_coordinator(loop) + MSG = 'rpc this' # noqa: N806 rpc_future = asyncio.Future() @@ -112,14 +146,17 @@ def get_rpc(_comm, msg): assert loop is asyncio.get_event_loop() rpc_future.set_result(msg) - _coordinator.add_rpc_subscriber(get_rpc, 'rpc') - _coordinator.rpc_send('rpc', MSG) + coordinator.add_rpc_subscriber(get_rpc, 'rpc') + coordinator.rpc_send('rpc', MSG) result = await rpc_future assert result == MSG @pytest.mark.asyncio - async def test_task(self, _coordinator): + async def test_task(self, make_coordinator): + loop = asyncio.get_running_loop() + coordinator = make_coordinator(loop) + TASK = 'task this' # noqa: N806 task_future = asyncio.Future() @@ -129,8 +166,8 @@ def get_task(_comm, msg): assert loop is asyncio.get_event_loop() task_future.set_result(msg) - _coordinator.add_task_subscriber(get_task) - _coordinator.task_send(TASK) + coordinator.add_task_subscriber(get_task) + coordinator.task_send(TASK) result = await task_future assert result == TASK @@ -138,48 +175,53 @@ def get_task(_comm, msg): class TestTaskActions: @pytest.mark.asyncio - async def test_launch(self, _coordinator, async_controller, persister): + async def test_launch(self, make_controller, persister): # Let the process run to the end - loop = asyncio.get_event_loop() - _coordinator.add_task_subscriber(plumpy.ProcessLauncher(loop, persister=persister)) - result = await async_controller.launch_process(utils.DummyProcess) + loop = asyncio.get_running_loop() + controller = make_controller(loop) + controller.coordinator.add_task_subscriber(plumpy.ProcessLauncher(loop, persister=persister)) + result = await controller.launch_process(utils.DummyProcess) # Check that we got a result assert result == utils.DummyProcess.EXPECTED_OUTPUTS @pytest.mark.asyncio - async def test_launch_nowait(self, _coordinator, async_controller, persister): + async def test_launch_nowait(self, make_controller, persister): """Testing launching but don't wait, just get the pid""" - loop = asyncio.get_event_loop() - _coordinator.add_task_subscriber(plumpy.ProcessLauncher(loop, persister=persister)) - pid = await async_controller.launch_process(utils.DummyProcess, nowait=True) + loop = asyncio.get_running_loop() + controller = make_controller(loop) + controller.coordinator.add_task_subscriber(plumpy.ProcessLauncher(loop, persister=persister)) + pid = await controller.launch_process(utils.DummyProcess, nowait=True) assert isinstance(pid, uuid.UUID) @pytest.mark.asyncio - async def test_execute_action(self, _coordinator, async_controller, persister): + async def test_execute_action(self, make_controller, persister): """Test the process execute action""" - loop = asyncio.get_event_loop() - _coordinator.add_task_subscriber(plumpy.ProcessLauncher(loop, persister=persister)) - result = await async_controller.execute_process(utils.DummyProcessWithOutput) + loop = asyncio.get_running_loop() + controller = make_controller(loop) + controller.coordinator.add_task_subscriber(plumpy.ProcessLauncher(loop, persister=persister)) + result = await controller.execute_process(utils.DummyProcessWithOutput) assert utils.DummyProcessWithOutput.EXPECTED_OUTPUTS == result @pytest.mark.asyncio - async def test_execute_action_nowait(self, _coordinator, async_controller, persister): + async def test_execute_action_nowait(self, make_controller, persister): """Test the process execute action""" - loop = asyncio.get_event_loop() - _coordinator.add_task_subscriber(plumpy.ProcessLauncher(loop, persister=persister)) - pid = await async_controller.execute_process(utils.DummyProcessWithOutput, nowait=True) + loop = asyncio.get_running_loop() + controller = make_controller(loop) + controller.coordinator.add_task_subscriber(plumpy.ProcessLauncher(loop, persister=persister)) + pid = await controller.execute_process(utils.DummyProcessWithOutput, nowait=True) assert isinstance(pid, uuid.UUID) @pytest.mark.asyncio - async def test_launch_many(self, _coordinator, async_controller, persister): + async def test_launch_many(self, make_controller, persister): """Test launching multiple processes""" - loop = asyncio.get_event_loop() - _coordinator.add_task_subscriber(plumpy.ProcessLauncher(loop, persister=persister)) + loop = asyncio.get_running_loop() + controller = make_controller(loop) + controller.coordinator.add_task_subscriber(plumpy.ProcessLauncher(loop, persister=persister)) num_to_launch = 10 launch_futures = [] for _ in range(num_to_launch): - launch = async_controller.launch_process(utils.DummyProcess, nowait=True) + launch = controller.launch_process(utils.DummyProcess, nowait=True) launch_futures.append(launch) results = await asyncio.gather(*launch_futures) @@ -187,15 +229,16 @@ async def test_launch_many(self, _coordinator, async_controller, persister): assert isinstance(result, uuid.UUID) @pytest.mark.asyncio - async def test_continue(self, _coordinator, async_controller, persister): + async def test_continue(self, make_controller, persister): """Test continuing a saved process""" - loop = asyncio.get_event_loop() - _coordinator.add_task_subscriber(plumpy.ProcessLauncher(loop, persister=persister)) + loop = asyncio.get_running_loop() + controller = make_controller(loop) + controller.coordinator.add_task_subscriber(plumpy.ProcessLauncher(loop, persister=persister)) process = utils.DummyProcessWithOutput() persister.save_checkpoint(process) pid = process.pid del process # Let the process run to the end - result = await async_controller.continue_process(pid) + result = await controller.continue_process(pid) assert result, utils.DummyProcessWithOutput.EXPECTED_OUTPUTS diff --git a/uv.lock b/uv.lock index f888846a..97fd97d1 100644 --- a/uv.lock +++ b/uv.lock @@ -1551,8 +1551,8 @@ requires-dist = [ { name = "myst-nb", marker = "extra == 'docs'", specifier = "~=0.11.0" }, { name = "nest-asyncio", specifier = "~=1.5,>=1.5.1" }, { name = "pre-commit", marker = "extra == 'pre-commit'", specifier = "~=2.2" }, - { name = "pytest", marker = "extra == 'tests'", specifier = "~=7.0" }, - { name = "pytest-asyncio", marker = "extra == 'tests'", specifier = "~=0.12,<0.17" }, + { name = "pytest", marker = "extra == 'tests'", specifier = "~=8.0" }, + { name = "pytest-asyncio", marker = "extra == 'tests'", specifier = "~=0.25" }, { name = "pytest-cov", marker = "extra == 'tests'", specifier = "~=4.1" }, { name = "pytest-notebook", marker = "extra == 'tests'", specifier = ">=0.8.0" }, { name = "pyyaml", specifier = "~=6.0" }, @@ -1765,7 +1765,7 @@ wheels = [ [[package]] name = "pytest" -version = "7.4.4" +version = "8.3.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -1775,21 +1775,21 @@ dependencies = [ { name = "pluggy" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/80/1f/9d8e98e4133ffb16c90f3b405c43e38d3abb715bb5d7a63a5a684f7e46a3/pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", size = 1357116 } +sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8", size = 325287 }, + { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, ] [[package]] name = "pytest-asyncio" -version = "0.16.0" +version = "0.25.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/89/53/8844d99d5343eecbb6d740d708581fbf63cefd560c07c7164b12691e54eb/pytest-asyncio-0.16.0.tar.gz", hash = "sha256:7496c5977ce88c34379df64a66459fe395cd05543f0a2f837016e7144391fcfb", size = 12095 } +sdist = { url = "https://files.pythonhosted.org/packages/f2/a8/ecbc8ede70921dd2f544ab1cadd3ff3bf842af27f87bbdea774c7baa1d38/pytest_asyncio-0.25.3.tar.gz", hash = "sha256:fc1da2cf9f125ada7e710b4ddad05518d4cee187ae9412e9ac9271003497f07a", size = 54239 } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/d0/d9bd672577857bb59004d7a0902abb5f27770c1d234860a08898eb058bd2/pytest_asyncio-0.16.0-py3-none-any.whl", hash = "sha256:5f2a21273c47b331ae6aa5b36087047b4899e40f03f18397c0e65fa5cca54e9b", size = 12119 }, + { url = "https://files.pythonhosted.org/packages/67/17/3493c5624e48fd97156ebaec380dcaafee9506d7e2c46218ceebbb57d7de/pytest_asyncio-0.25.3-py3-none-any.whl", hash = "sha256:9e89518e0f9bd08928f97a3482fdc4e244df17529460bc038291ccaf8f85c7c3", size = 19467 }, ] [[package]] From 2dc2a31a2ca364abb42d1479d056b30005e7b1aa Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Fri, 31 Jan 2025 16:19:39 +0100 Subject: [PATCH 62/64] Using customized get_event_loop from PlumpyEventLoopPolicy (#30) This can remove the get_event_loop deprecation warning. --- src/plumpy/events.py | 18 +++++++++++------- src/plumpy/processes.py | 2 +- tests/conftest.py | 12 +++++++++--- tests/persistence/test_inmemory.py | 8 +++++++- tests/persistence/test_pickle.py | 8 ++++++++ tests/rmq/test_communications.py | 6 ++++++ tests/rmq/test_communicator.py | 25 ------------------------- tests/rmq/test_process_control.py | 6 ++++++ 8 files changed, 48 insertions(+), 37 deletions(-) diff --git a/src/plumpy/events.py b/src/plumpy/events.py index 7379a6b6..27a594d4 100644 --- a/src/plumpy/events.py +++ b/src/plumpy/events.py @@ -24,16 +24,19 @@ class PlumpyEventLoopPolicy(asyncio.DefaultEventLoopPolicy): _loop: asyncio.AbstractEventLoop | None = None - def get_event_loop(self) -> asyncio.AbstractEventLoop: - """Return the patched event loop.""" + def new_event_loop(self) -> asyncio.AbstractEventLoop: + """Create new event loop and patch as re-entrant loop.""" import nest_asyncio - if self._loop is None: - self._loop = super().get_event_loop() - nest_asyncio.apply(self._loop) + self._loop = super().new_event_loop() + nest_asyncio.apply(self._loop) return self._loop + def get_event_loop(self) -> asyncio.AbstractEventLoop: + """Return the patched event loop.""" + return self._loop or self.new_event_loop() + def set_event_loop_policy() -> None: """Enable plumpy's event loop policy that will make event loop's reentrant.""" @@ -44,8 +47,9 @@ def set_event_loop_policy() -> None: def reset_event_loop_policy() -> None: - """Reset the event loop policy to the default.""" - loop = get_event_loop() + """Reset the event loop policy to the asyncio default.""" + # purge weakref to prevent memory leak + loop = asyncio.get_event_loop() cls = loop.__class__ diff --git a/src/plumpy/processes.py b/src/plumpy/processes.py index ec215ecc..36bef047 100644 --- a/src/plumpy/processes.py +++ b/src/plumpy/processes.py @@ -340,7 +340,7 @@ def __init__( # Don't allow the spec to be changed anymore self.spec().seal() - self._loop = loop if loop is not None else asyncio.get_event_loop() + self._loop = loop or asyncio.get_event_loop() self._setup_event_hooks() diff --git a/tests/conftest.py b/tests/conftest.py index c70088fa..3980484f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,15 @@ # -*- coding: utf-8 -*- import pytest +from plumpy.events import set_event_loop_policy, reset_event_loop_policy -@pytest.fixture(scope='session') -def set_event_loop_policy(): - from plumpy import set_event_loop_policy +@pytest.fixture(scope='function') +def custom_event_loop_policy(): + """This is the fixture for changing the event loop of synchronous tests. + If using `@pytest.mark.asyncio`, the event loop can be set by `event_loop_policy` + fixture of pytest-asyncio. + """ set_event_loop_policy() + yield + reset_event_loop_policy() diff --git a/tests/persistence/test_inmemory.py b/tests/persistence/test_inmemory.py index 93b48972..b27cb129 100644 --- a/tests/persistence/test_inmemory.py +++ b/tests/persistence/test_inmemory.py @@ -1,11 +1,12 @@ # -*- coding: utf-8 -*- - +import pytest import plumpy from ..utils import ProcessWithCheckpoint class TestInMemoryPersister: + @pytest.mark.usefixtures('custom_event_loop_policy') def test_save_load_roundtrip(self): """ Test the plumpy.PicklePersister by taking a dummpy process, saving a checkpoint @@ -16,6 +17,7 @@ def test_save_load_roundtrip(self): persister = plumpy.InMemoryPersister() persister.save_checkpoint(process) + @pytest.mark.usefixtures('custom_event_loop_policy') def test_get_checkpoints_without_tags(self): """ """ process_a = ProcessWithCheckpoint() @@ -34,6 +36,7 @@ def test_get_checkpoints_without_tags(self): assert set(retrieved_checkpoints) == set(checkpoints) + @pytest.mark.usefixtures('custom_event_loop_policy') def test_get_checkpoints_with_tags(self): """ """ process_a = ProcessWithCheckpoint() @@ -54,6 +57,7 @@ def test_get_checkpoints_with_tags(self): assert set(retrieved_checkpoints) == set(checkpoints) + @pytest.mark.usefixtures('custom_event_loop_policy') def test_get_process_checkpoints(self): """ """ process_a = ProcessWithCheckpoint() @@ -74,6 +78,7 @@ def test_get_process_checkpoints(self): assert set(retrieved_checkpoints) == set(checkpoints) + @pytest.mark.usefixtures('custom_event_loop_policy') def test_delete_process_checkpoints(self): """ """ process_a = ProcessWithCheckpoint() @@ -100,6 +105,7 @@ def test_delete_process_checkpoints(self): assert set(retrieved_checkpoints) == set(checkpoints) + @pytest.mark.usefixtures('custom_event_loop_policy') def test_delete_checkpoint(self): """ """ process_a = ProcessWithCheckpoint() diff --git a/tests/persistence/test_pickle.py b/tests/persistence/test_pickle.py index 28ea906b..ffa8559b 100644 --- a/tests/persistence/test_pickle.py +++ b/tests/persistence/test_pickle.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- import tempfile +import pytest + if getattr(tempfile, 'TemporaryDirectory', None) is None: from backports import tempfile @@ -10,6 +12,7 @@ class TestPicklePersister: + @pytest.mark.usefixtures('custom_event_loop_policy') def test_save_load_roundtrip(self): """ Test the plumpy.PicklePersister by taking a dummpy process, saving a checkpoint @@ -21,6 +24,7 @@ def test_save_load_roundtrip(self): persister = plumpy.PicklePersister(directory) persister.save_checkpoint(process) + @pytest.mark.usefixtures('custom_event_loop_policy') def test_get_checkpoints_without_tags(self): """ """ process_a = ProcessWithCheckpoint() @@ -40,6 +44,7 @@ def test_get_checkpoints_without_tags(self): assert set(retrieved_checkpoints) == set(checkpoints) + @pytest.mark.usefixtures('custom_event_loop_policy') def test_get_checkpoints_with_tags(self): """ """ process_a = ProcessWithCheckpoint() @@ -61,6 +66,7 @@ def test_get_checkpoints_with_tags(self): assert set(retrieved_checkpoints) == set(checkpoints) + @pytest.mark.usefixtures('custom_event_loop_policy') def test_get_process_checkpoints(self): """ """ process_a = ProcessWithCheckpoint() @@ -82,6 +88,7 @@ def test_get_process_checkpoints(self): assert set(retrieved_checkpoints) == set(checkpoints) + @pytest.mark.usefixtures('custom_event_loop_policy') def test_delete_process_checkpoints(self): """ """ process_a = ProcessWithCheckpoint() @@ -109,6 +116,7 @@ def test_delete_process_checkpoints(self): assert set(retrieved_checkpoints) == set(checkpoints) + @pytest.mark.usefixtures('custom_event_loop_policy') def test_delete_checkpoint(self): """ """ process_a = ProcessWithCheckpoint() diff --git a/tests/rmq/test_communications.py b/tests/rmq/test_communications.py index e45994b2..d1386a11 100644 --- a/tests/rmq/test_communications.py +++ b/tests/rmq/test_communications.py @@ -43,6 +43,7 @@ def __call__(self): return Subscriber() +@pytest.mark.usefixtures('custom_event_loop_policy') def test_add_rpc_subscriber(_coordinator, subscriber): """Test the `LoopCommunicator.add_rpc_subscriber` method.""" assert _coordinator.add_rpc_subscriber(subscriber) is not None @@ -51,12 +52,14 @@ def test_add_rpc_subscriber(_coordinator, subscriber): assert _coordinator.add_rpc_subscriber(subscriber, identifier) == identifier +@pytest.mark.usefixtures('custom_event_loop_policy') def test_remove_rpc_subscriber(_coordinator, subscriber): """Test the `LoopCommunicator.remove_rpc_subscriber` method.""" identifier = _coordinator.add_rpc_subscriber(subscriber) _coordinator.remove_rpc_subscriber(identifier) +@pytest.mark.usefixtures('custom_event_loop_policy') def test_add_broadcast_subscriber(_coordinator, subscriber): """Test the `LoopCommunicator.add_broadcast_subscriber` method.""" assert _coordinator.add_broadcast_subscriber(subscriber) is not None @@ -65,17 +68,20 @@ def test_add_broadcast_subscriber(_coordinator, subscriber): assert _coordinator.add_broadcast_subscriber(subscriber, identifier=identifier) == identifier +@pytest.mark.usefixtures('custom_event_loop_policy') def test_remove_broadcast_subscriber(_coordinator, subscriber): """Test the `LoopCommunicator.remove_broadcast_subscriber` method.""" identifier = _coordinator.add_broadcast_subscriber(subscriber) _coordinator.remove_broadcast_subscriber(identifier) +@pytest.mark.usefixtures('custom_event_loop_policy') def test_add_task_subscriber(_coordinator, subscriber): """Test the `LoopCommunicator.add_task_subscriber` method.""" assert _coordinator.add_task_subscriber(subscriber) is not None +@pytest.mark.usefixtures('custom_event_loop_policy') def test_remove_task_subscriber(_coordinator, subscriber): """Test the `LoopCommunicator.remove_task_subscriber` method.""" identifier = _coordinator.add_task_subscriber(subscriber) diff --git a/tests/rmq/test_communicator.py b/tests/rmq/test_communicator.py index 8f4a0de1..47ae2816 100644 --- a/tests/rmq/test_communicator.py +++ b/tests/rmq/test_communicator.py @@ -27,31 +27,6 @@ def persister(): shutil.rmtree(_tmppath) -@pytest.fixture -def _coordinator(): - message_exchange = f'{__file__}.{shortuuid.uuid()}' - task_exchange = f'{__file__}.{shortuuid.uuid()}' - task_queue = f'{__file__}.{shortuuid.uuid()}' - - thread_comm = RmqThreadCommunicator.connect( - connection_params={'url': 'amqp://guest:guest@localhost:5672/'}, - message_exchange=message_exchange, - task_exchange=task_exchange, - task_queue=task_queue, - encoder=process_control.MSGPACK_ENCODER, - decoder=process_control.MSGPACK_DECODER, - ) - - loop = asyncio.get_event_loop() - loop.set_debug(True) - comm = communications.LoopCommunicator(thread_comm, loop=loop) - coordinator = RmqCoordinator(comm) - - yield coordinator - - coordinator.close() - - @pytest.fixture(scope='function') def make_coordinator(): def _coordinator(loop=None): diff --git a/tests/rmq/test_process_control.py b/tests/rmq/test_process_control.py index 2627035e..b431e5db 100644 --- a/tests/rmq/test_process_control.py +++ b/tests/rmq/test_process_control.py @@ -7,12 +7,18 @@ from kiwipy import rmq import plumpy +from plumpy.events import PlumpyEventLoopPolicy from plumpy.rmq import process_control from . import RmqCoordinator from .. import utils +@pytest.fixture(scope='module') +def event_loop_policy(request): + return PlumpyEventLoopPolicy() + + @pytest.fixture def _coordinator(): message_exchange = f'{__file__}.{shortuuid.uuid()}' From b91478100aa97673b0bc21b564725859a26b8b9b Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Fri, 31 Jan 2025 16:43:19 +0100 Subject: [PATCH 63/64] Using custom event loop for nested process tests (#31) fixes #20 As expected, should using the plumpy event loop. But it shows a problem that the custom event loop from tests in other places not set back to the regular event loop. --- src/plumpy/events.py | 18 +++++++++++++----- tests/test_processes.py | 3 +++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/plumpy/events.py b/src/plumpy/events.py index 27a594d4..5312895f 100644 --- a/src/plumpy/events.py +++ b/src/plumpy/events.py @@ -48,16 +48,24 @@ def set_event_loop_policy() -> None: def reset_event_loop_policy() -> None: """Reset the event loop policy to the asyncio default.""" - # purge weakref to prevent memory leak - loop = asyncio.get_event_loop() + # 1. Close the existing event loop (if it isn't already closed): + old_loop = asyncio.get_event_loop() + if not old_loop.is_closed(): + # purge weakref to prevent memory leak + cls = old_loop.__class__ - cls = loop.__class__ + del cls._check_running # type: ignore + del cls._nest_patched # type: ignore - del cls._check_running # type: ignore - del cls._nest_patched # type: ignore + old_loop.close() + # 2. Reset the policy to the default (i.e. None): asyncio.set_event_loop_policy(None) + # 3. Create a brand-new event loop under this default policy: + new_loop = asyncio.new_event_loop() + asyncio.set_event_loop(new_loop) + def run_until_complete(future: asyncio.Future, loop: asyncio.AbstractEventLoop | None = None) -> Any: loop = loop or get_event_loop() diff --git a/tests/test_processes.py b/tests/test_processes.py index 767442e4..9dc75a08 100644 --- a/tests/test_processes.py +++ b/tests/test_processes.py @@ -41,6 +41,7 @@ def on_kill(self, msg): super().on_kill(msg) +@pytest.mark.usefixtures('custom_event_loop_policy') def test_process_is_savable(): proc = utils.DummyProcess() assert isinstance(proc, Savable) @@ -584,6 +585,7 @@ def run(self): proc = StackTest() proc.execute() + @pytest.mark.usefixtures('custom_event_loop_policy') def test_process_stack_multiple(self): """ Run multiple and nested processes to make sure the process stack is always correct @@ -619,6 +621,7 @@ def run(self): assert len(expect_true) == n_run * 3 + @pytest.mark.usefixtures('custom_event_loop_policy') def test_process_nested(self): """ Run multiple and nested processes to make sure the process stack is always correct From 09012602c22f491b4d95d4b7c69c652e94502eba Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Fri, 31 Jan 2025 21:34:57 +0100 Subject: [PATCH 64/64] Use asyncio.run --- src/plumpy/processes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plumpy/processes.py b/src/plumpy/processes.py index 36bef047..94129464 100644 --- a/src/plumpy/processes.py +++ b/src/plumpy/processes.py @@ -1315,7 +1315,7 @@ def execute(self) -> dict[str, Any] | None: :return: None if not terminated, otherwise `self.outputs` """ if not self.has_terminated(): - self.loop.run_until_complete(self.step_until_terminated()) + asyncio.run(self.step_until_terminated()) return self.future().result()