From e1a91a267ba5ddca9ef8f0182b63693de0d609d3 Mon Sep 17 00:00:00 2001 From: "Craig.Walton" Date: Tue, 17 Dec 2024 11:49:46 +0000 Subject: [PATCH 1/7] Migrate pyproject.toml, removing unnecessary or internal-only information, adapting for new repo structure. --- poetry.lock | 2680 ++++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 51 + 2 files changed, 2731 insertions(+) create mode 100644 poetry.lock create mode 100644 pyproject.toml diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..f9e813a --- /dev/null +++ b/poetry.lock @@ -0,0 +1,2680 @@ +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. + +[[package]] +name = "aiobotocore" +version = "2.15.2" +description = "Async client for aws services using botocore and aiohttp" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiobotocore-2.15.2-py3-none-any.whl", hash = "sha256:d4d3128b4b558e2b4c369bfa963b022d7e87303adb82eec623cec8aa77ae578a"}, + {file = "aiobotocore-2.15.2.tar.gz", hash = "sha256:9ac1cfcaccccc80602968174aa032bf978abe36bd4e55e6781d6500909af1375"}, +] + +[package.dependencies] +aiohttp = ">=3.9.2,<4.0.0" +aioitertools = ">=0.5.1,<1.0.0" +botocore = ">=1.35.16,<1.35.37" +wrapt = ">=1.10.10,<2.0.0" + +[package.extras] +awscli = ["awscli (>=1.34.16,<1.35.3)"] +boto3 = ["boto3 (>=1.35.16,<1.35.37)"] + +[[package]] +name = "aiofiles" +version = "24.1.0" +description = "File support for asyncio." +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"}, + {file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"}, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.4.4" +description = "Happy Eyeballs for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiohappyeyeballs-2.4.4-py3-none-any.whl", hash = "sha256:a980909d50efcd44795c4afeca523296716d50cd756ddca6af8c65b996e27de8"}, + {file = "aiohappyeyeballs-2.4.4.tar.gz", hash = "sha256:5fdd7d87889c63183afc18ce9271f9b0a7d32c2303e394468dd45d514a757745"}, +] + +[[package]] +name = "aiohttp" +version = "3.11.10" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.9" +files = [ + {file = "aiohttp-3.11.10-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cbad88a61fa743c5d283ad501b01c153820734118b65aee2bd7dbb735475ce0d"}, + {file = "aiohttp-3.11.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80886dac673ceaef499de2f393fc80bb4481a129e6cb29e624a12e3296cc088f"}, + {file = "aiohttp-3.11.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:61b9bae80ed1f338c42f57c16918853dc51775fb5cb61da70d590de14d8b5fb4"}, + {file = "aiohttp-3.11.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e2e576caec5c6a6b93f41626c9c02fc87cd91538b81a3670b2e04452a63def6"}, + {file = "aiohttp-3.11.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02c13415b5732fb6ee7ff64583a5e6ed1c57aa68f17d2bda79c04888dfdc2769"}, + {file = "aiohttp-3.11.10-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4cfce37f31f20800a6a6620ce2cdd6737b82e42e06e6e9bd1b36f546feb3c44f"}, + {file = "aiohttp-3.11.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3bbbfff4c679c64e6e23cb213f57cc2c9165c9a65d63717108a644eb5a7398df"}, + {file = "aiohttp-3.11.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49c7dbbc1a559ae14fc48387a115b7d4bbc84b4a2c3b9299c31696953c2a5219"}, + {file = "aiohttp-3.11.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:68386d78743e6570f054fe7949d6cb37ef2b672b4d3405ce91fafa996f7d9b4d"}, + {file = "aiohttp-3.11.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9ef405356ba989fb57f84cac66f7b0260772836191ccefbb987f414bcd2979d9"}, + {file = "aiohttp-3.11.10-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5d6958671b296febe7f5f859bea581a21c1d05430d1bbdcf2b393599b1cdce77"}, + {file = "aiohttp-3.11.10-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:99b7920e7165be5a9e9a3a7f1b680f06f68ff0d0328ff4079e5163990d046767"}, + {file = "aiohttp-3.11.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0dc49f42422163efb7e6f1df2636fe3db72713f6cd94688e339dbe33fe06d61d"}, + {file = "aiohttp-3.11.10-cp310-cp310-win32.whl", hash = "sha256:40d1c7a7f750b5648642586ba7206999650208dbe5afbcc5284bcec6579c9b91"}, + {file = "aiohttp-3.11.10-cp310-cp310-win_amd64.whl", hash = "sha256:68ff6f48b51bd78ea92b31079817aff539f6c8fc80b6b8d6ca347d7c02384e33"}, + {file = "aiohttp-3.11.10-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:77c4aa15a89847b9891abf97f3d4048f3c2d667e00f8a623c89ad2dccee6771b"}, + {file = "aiohttp-3.11.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:909af95a72cedbefe5596f0bdf3055740f96c1a4baa0dd11fd74ca4de0b4e3f1"}, + {file = "aiohttp-3.11.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:386fbe79863eb564e9f3615b959e28b222259da0c48fd1be5929ac838bc65683"}, + {file = "aiohttp-3.11.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3de34936eb1a647aa919655ff8d38b618e9f6b7f250cc19a57a4bf7fd2062b6d"}, + {file = "aiohttp-3.11.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c9527819b29cd2b9f52033e7fb9ff08073df49b4799c89cb5754624ecd98299"}, + {file = "aiohttp-3.11.10-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a96e3e03300b41f261bbfd40dfdbf1c301e87eab7cd61c054b1f2e7c89b9e8"}, + {file = "aiohttp-3.11.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98f5635f7b74bcd4f6f72fcd85bea2154b323a9f05226a80bc7398d0c90763b0"}, + {file = "aiohttp-3.11.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:03b6002e20938fc6ee0918c81d9e776bebccc84690e2b03ed132331cca065ee5"}, + {file = "aiohttp-3.11.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6362cc6c23c08d18ddbf0e8c4d5159b5df74fea1a5278ff4f2c79aed3f4e9f46"}, + {file = "aiohttp-3.11.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3691ed7726fef54e928fe26344d930c0c8575bc968c3e239c2e1a04bd8cf7838"}, + {file = "aiohttp-3.11.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31d5093d3acd02b31c649d3a69bb072d539d4c7659b87caa4f6d2bcf57c2fa2b"}, + {file = "aiohttp-3.11.10-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:8b3cf2dc0f0690a33f2d2b2cb15db87a65f1c609f53c37e226f84edb08d10f52"}, + {file = "aiohttp-3.11.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fbbaea811a2bba171197b08eea288b9402faa2bab2ba0858eecdd0a4105753a3"}, + {file = "aiohttp-3.11.10-cp311-cp311-win32.whl", hash = "sha256:4b2c7ac59c5698a7a8207ba72d9e9c15b0fc484a560be0788b31312c2c5504e4"}, + {file = "aiohttp-3.11.10-cp311-cp311-win_amd64.whl", hash = "sha256:974d3a2cce5fcfa32f06b13ccc8f20c6ad9c51802bb7f829eae8a1845c4019ec"}, + {file = "aiohttp-3.11.10-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b78f053a7ecfc35f0451d961dacdc671f4bcbc2f58241a7c820e9d82559844cf"}, + {file = "aiohttp-3.11.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab7485222db0959a87fbe8125e233b5a6f01f4400785b36e8a7878170d8c3138"}, + {file = "aiohttp-3.11.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cf14627232dfa8730453752e9cdc210966490992234d77ff90bc8dc0dce361d5"}, + {file = "aiohttp-3.11.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:076bc454a7e6fd646bc82ea7f98296be0b1219b5e3ef8a488afbdd8e81fbac50"}, + {file = "aiohttp-3.11.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:482cafb7dc886bebeb6c9ba7925e03591a62ab34298ee70d3dd47ba966370d2c"}, + {file = "aiohttp-3.11.10-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf3d1a519a324af764a46da4115bdbd566b3c73fb793ffb97f9111dbc684fc4d"}, + {file = "aiohttp-3.11.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24213ba85a419103e641e55c27dc7ff03536c4873470c2478cce3311ba1eee7b"}, + {file = "aiohttp-3.11.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b99acd4730ad1b196bfb03ee0803e4adac371ae8efa7e1cbc820200fc5ded109"}, + {file = "aiohttp-3.11.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:14cdb5a9570be5a04eec2ace174a48ae85833c2aadc86de68f55541f66ce42ab"}, + {file = "aiohttp-3.11.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7e97d622cb083e86f18317282084bc9fbf261801b0192c34fe4b1febd9f7ae69"}, + {file = "aiohttp-3.11.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:012f176945af138abc10c4a48743327a92b4ca9adc7a0e078077cdb5dbab7be0"}, + {file = "aiohttp-3.11.10-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44224d815853962f48fe124748227773acd9686eba6dc102578defd6fc99e8d9"}, + {file = "aiohttp-3.11.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c87bf31b7fdab94ae3adbe4a48e711bfc5f89d21cf4c197e75561def39e223bc"}, + {file = "aiohttp-3.11.10-cp312-cp312-win32.whl", hash = "sha256:06a8e2ee1cbac16fe61e51e0b0c269400e781b13bcfc33f5425912391a542985"}, + {file = "aiohttp-3.11.10-cp312-cp312-win_amd64.whl", hash = "sha256:be2b516f56ea883a3e14dda17059716593526e10fb6303189aaf5503937db408"}, + {file = "aiohttp-3.11.10-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8cc5203b817b748adccb07f36390feb730b1bc5f56683445bfe924fc270b8816"}, + {file = "aiohttp-3.11.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ef359ebc6949e3a34c65ce20230fae70920714367c63afd80ea0c2702902ccf"}, + {file = "aiohttp-3.11.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9bca390cb247dbfaec3c664326e034ef23882c3f3bfa5fbf0b56cad0320aaca5"}, + {file = "aiohttp-3.11.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:811f23b3351ca532af598405db1093f018edf81368e689d1b508c57dcc6b6a32"}, + {file = "aiohttp-3.11.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddf5f7d877615f6a1e75971bfa5ac88609af3b74796ff3e06879e8422729fd01"}, + {file = "aiohttp-3.11.10-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6ab29b8a0beb6f8eaf1e5049252cfe74adbaafd39ba91e10f18caeb0e99ffb34"}, + {file = "aiohttp-3.11.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c49a76c1038c2dd116fa443eba26bbb8e6c37e924e2513574856de3b6516be99"}, + {file = "aiohttp-3.11.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f3dc0e330575f5b134918976a645e79adf333c0a1439dcf6899a80776c9ab39"}, + {file = "aiohttp-3.11.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:efb15a17a12497685304b2d976cb4939e55137df7b09fa53f1b6a023f01fcb4e"}, + {file = "aiohttp-3.11.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:db1d0b28fcb7f1d35600150c3e4b490775251dea70f894bf15c678fdd84eda6a"}, + {file = "aiohttp-3.11.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:15fccaf62a4889527539ecb86834084ecf6e9ea70588efde86e8bc775e0e7542"}, + {file = "aiohttp-3.11.10-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:593c114a2221444f30749cc5e5f4012488f56bd14de2af44fe23e1e9894a9c60"}, + {file = "aiohttp-3.11.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7852bbcb4d0d2f0c4d583f40c3bc750ee033265d80598d0f9cb6f372baa6b836"}, + {file = "aiohttp-3.11.10-cp313-cp313-win32.whl", hash = "sha256:65e55ca7debae8faaffee0ebb4b47a51b4075f01e9b641c31e554fd376595c6c"}, + {file = "aiohttp-3.11.10-cp313-cp313-win_amd64.whl", hash = "sha256:beb39a6d60a709ae3fb3516a1581777e7e8b76933bb88c8f4420d875bb0267c6"}, + {file = "aiohttp-3.11.10-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0580f2e12de2138f34debcd5d88894786453a76e98febaf3e8fe5db62d01c9bf"}, + {file = "aiohttp-3.11.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a55d2ad345684e7c3dd2c20d2f9572e9e1d5446d57200ff630e6ede7612e307f"}, + {file = "aiohttp-3.11.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:04814571cb72d65a6899db6099e377ed00710bf2e3eafd2985166f2918beaf59"}, + {file = "aiohttp-3.11.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e44a9a3c053b90c6f09b1bb4edd880959f5328cf63052503f892c41ea786d99f"}, + {file = "aiohttp-3.11.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:502a1464ccbc800b4b1995b302efaf426e8763fadf185e933c2931df7db9a199"}, + {file = "aiohttp-3.11.10-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:613e5169f8ae77b1933e42e418a95931fb4867b2991fc311430b15901ed67079"}, + {file = "aiohttp-3.11.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cca22a61b7fe45da8fc73c3443150c3608750bbe27641fc7558ec5117b27fdf"}, + {file = "aiohttp-3.11.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:86a5dfcc39309470bd7b68c591d84056d195428d5d2e0b5ccadfbaf25b026ebc"}, + {file = "aiohttp-3.11.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:77ae58586930ee6b2b6f696c82cf8e78c8016ec4795c53e36718365f6959dc82"}, + {file = "aiohttp-3.11.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:78153314f26d5abef3239b4a9af20c229c6f3ecb97d4c1c01b22c4f87669820c"}, + {file = "aiohttp-3.11.10-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:98283b94cc0e11c73acaf1c9698dea80c830ca476492c0fe2622bd931f34b487"}, + {file = "aiohttp-3.11.10-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:53bf2097e05c2accc166c142a2090e4c6fd86581bde3fd9b2d3f9e93dda66ac1"}, + {file = "aiohttp-3.11.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c5532f0441fc09c119e1dca18fbc0687e64fbeb45aa4d6a87211ceaee50a74c4"}, + {file = "aiohttp-3.11.10-cp39-cp39-win32.whl", hash = "sha256:47ad15a65fb41c570cd0ad9a9ff8012489e68176e7207ec7b82a0940dddfd8be"}, + {file = "aiohttp-3.11.10-cp39-cp39-win_amd64.whl", hash = "sha256:c6b9e6d7e41656d78e37ce754813fa44b455c3d0d0dced2a047def7dc5570b74"}, + {file = "aiohttp-3.11.10.tar.gz", hash = "sha256:b1fc6b45010a8d0ff9e88f9f2418c6fd408c99c211257334aff41597ebece42e"}, +] + +[package.dependencies] +aiohappyeyeballs = ">=2.3.0" +aiosignal = ">=1.1.2" +async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""} +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +propcache = ">=0.2.0" +yarl = ">=1.17.0,<2.0" + +[package.extras] +speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"] + +[[package]] +name = "aioitertools" +version = "0.12.0" +description = "itertools and builtins for AsyncIO and mixed iterables" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aioitertools-0.12.0-py3-none-any.whl", hash = "sha256:fc1f5fac3d737354de8831cbba3eb04f79dd649d8f3afb4c5b114925e662a796"}, + {file = "aioitertools-0.12.0.tar.gz", hash = "sha256:c2a9055b4fbb7705f561b9d86053e8af5d10cc845d22c32008c43490b2d8dd6b"}, +] + +[package.extras] +dev = ["attribution (==1.8.0)", "black (==24.8.0)", "build (>=1.2)", "coverage (==7.6.1)", "flake8 (==7.1.1)", "flit (==3.9.0)", "mypy (==1.11.2)", "ufmt (==2.7.1)", "usort (==1.0.8.post1)"] +docs = ["sphinx (==8.0.2)", "sphinx-mdinclude (==0.6.2)"] + +[[package]] +name = "aiosignal" +version = "1.3.2" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.9" +files = [ + {file = "aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5"}, + {file = "aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "anyio" +version = "4.7.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.9" +files = [ + {file = "anyio-4.7.0-py3-none-any.whl", hash = "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352"}, + {file = "anyio-4.7.0.tar.gz", hash = "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} + +[package.extras] +doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] +trio = ["trio (>=0.26.1)"] + +[[package]] +name = "async-timeout" +version = "5.0.1" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, + {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, +] + +[[package]] +name = "attrs" +version = "24.3.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.8" +files = [ + {file = "attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308"}, + {file = "attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff"}, +] + +[package.extras] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] + +[[package]] +name = "beautifulsoup4" +version = "4.12.3" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, + {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, +] + +[package.dependencies] +soupsieve = ">1.2" + +[package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] + +[[package]] +name = "botocore" +version = "1.35.36" +description = "Low-level, data-driven core of boto 3." +optional = false +python-versions = ">=3.8" +files = [ + {file = "botocore-1.35.36-py3-none-any.whl", hash = "sha256:64241c778bf2dc863d93abab159e14024d97a926a5715056ef6411418cb9ead3"}, + {file = "botocore-1.35.36.tar.gz", hash = "sha256:354ec1b766f0029b5d6ff0c45d1a0f9e5007b7d2f3ec89bcdd755b208c5bc797"}, +] + +[package.dependencies] +jmespath = ">=0.7.1,<2.0.0" +python-dateutil = ">=2.1,<3.0.0" +urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""} + +[package.extras] +crt = ["awscrt (==0.22.0)"] + +[[package]] +name = "cachetools" +version = "5.5.0" +description = "Extensible memoizing collections and decorators" +optional = false +python-versions = ">=3.7" +files = [ + {file = "cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292"}, + {file = "cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a"}, +] + +[[package]] +name = "certifi" +version = "2024.12.14" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, + {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.0" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, + {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, + {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "debugpy" +version = "1.8.11" +description = "An implementation of the Debug Adapter Protocol for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "debugpy-1.8.11-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:2b26fefc4e31ff85593d68b9022e35e8925714a10ab4858fb1b577a8a48cb8cd"}, + {file = "debugpy-1.8.11-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61bc8b3b265e6949855300e84dc93d02d7a3a637f2aec6d382afd4ceb9120c9f"}, + {file = "debugpy-1.8.11-cp310-cp310-win32.whl", hash = "sha256:c928bbf47f65288574b78518449edaa46c82572d340e2750889bbf8cd92f3737"}, + {file = "debugpy-1.8.11-cp310-cp310-win_amd64.whl", hash = "sha256:8da1db4ca4f22583e834dcabdc7832e56fe16275253ee53ba66627b86e304da1"}, + {file = "debugpy-1.8.11-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:85de8474ad53ad546ff1c7c7c89230db215b9b8a02754d41cb5a76f70d0be296"}, + {file = "debugpy-1.8.11-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ffc382e4afa4aee367bf413f55ed17bd91b191dcaf979890af239dda435f2a1"}, + {file = "debugpy-1.8.11-cp311-cp311-win32.whl", hash = "sha256:40499a9979c55f72f4eb2fc38695419546b62594f8af194b879d2a18439c97a9"}, + {file = "debugpy-1.8.11-cp311-cp311-win_amd64.whl", hash = "sha256:987bce16e86efa86f747d5151c54e91b3c1e36acc03ce1ddb50f9d09d16ded0e"}, + {file = "debugpy-1.8.11-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:84e511a7545d11683d32cdb8f809ef63fc17ea2a00455cc62d0a4dbb4ed1c308"}, + {file = "debugpy-1.8.11-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce291a5aca4985d82875d6779f61375e959208cdf09fcec40001e65fb0a54768"}, + {file = "debugpy-1.8.11-cp312-cp312-win32.whl", hash = "sha256:28e45b3f827d3bf2592f3cf7ae63282e859f3259db44ed2b129093ca0ac7940b"}, + {file = "debugpy-1.8.11-cp312-cp312-win_amd64.whl", hash = "sha256:44b1b8e6253bceada11f714acf4309ffb98bfa9ac55e4fce14f9e5d4484287a1"}, + {file = "debugpy-1.8.11-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:8988f7163e4381b0da7696f37eec7aca19deb02e500245df68a7159739bbd0d3"}, + {file = "debugpy-1.8.11-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c1f6a173d1140e557347419767d2b14ac1c9cd847e0b4c5444c7f3144697e4e"}, + {file = "debugpy-1.8.11-cp313-cp313-win32.whl", hash = "sha256:bb3b15e25891f38da3ca0740271e63ab9db61f41d4d8541745cfc1824252cb28"}, + {file = "debugpy-1.8.11-cp313-cp313-win_amd64.whl", hash = "sha256:d8768edcbeb34da9e11bcb8b5c2e0958d25218df7a6e56adf415ef262cd7b6d1"}, + {file = "debugpy-1.8.11-cp38-cp38-macosx_14_0_x86_64.whl", hash = "sha256:ad7efe588c8f5cf940f40c3de0cd683cc5b76819446abaa50dc0829a30c094db"}, + {file = "debugpy-1.8.11-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:189058d03a40103a57144752652b3ab08ff02b7595d0ce1f651b9acc3a3a35a0"}, + {file = "debugpy-1.8.11-cp38-cp38-win32.whl", hash = "sha256:32db46ba45849daed7ccf3f2e26f7a386867b077f39b2a974bb5c4c2c3b0a280"}, + {file = "debugpy-1.8.11-cp38-cp38-win_amd64.whl", hash = "sha256:116bf8342062246ca749013df4f6ea106f23bc159305843491f64672a55af2e5"}, + {file = "debugpy-1.8.11-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:654130ca6ad5de73d978057eaf9e582244ff72d4574b3e106fb8d3d2a0d32458"}, + {file = "debugpy-1.8.11-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23dc34c5e03b0212fa3c49a874df2b8b1b8fda95160bd79c01eb3ab51ea8d851"}, + {file = "debugpy-1.8.11-cp39-cp39-win32.whl", hash = "sha256:52d8a3166c9f2815bfae05f386114b0b2d274456980d41f320299a8d9a5615a7"}, + {file = "debugpy-1.8.11-cp39-cp39-win_amd64.whl", hash = "sha256:52c3cf9ecda273a19cc092961ee34eb9ba8687d67ba34cc7b79a521c1c64c4c0"}, + {file = "debugpy-1.8.11-py2.py3-none-any.whl", hash = "sha256:0e22f846f4211383e6a416d04b4c13ed174d24cc5d43f5fd52e7821d0ebc8920"}, + {file = "debugpy-1.8.11.tar.gz", hash = "sha256:6ad2688b69235c43b020e04fecccdf6a96c8943ca9c2fb340b8adc103c655e57"}, +] + +[[package]] +name = "distlib" +version = "0.3.9" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, + {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, +] + +[[package]] +name = "docstring-parser" +version = "0.16" +description = "Parse Python docstrings in reST, Google and Numpydoc format" +optional = false +python-versions = ">=3.6,<4.0" +files = [ + {file = "docstring_parser-0.16-py3-none-any.whl", hash = "sha256:bf0a1387354d3691d102edef7ec124f219ef639982d096e26e3b60aeffa90637"}, + {file = "docstring_parser-0.16.tar.gz", hash = "sha256:538beabd0af1e2db0146b6bd3caa526c35a34d61af9fd2887f3a8a27a739aa6e"}, +] + +[[package]] +name = "durationpy" +version = "0.9" +description = "Module for converting between datetime.timedelta and Go's Duration strings." +optional = false +python-versions = "*" +files = [ + {file = "durationpy-0.9-py3-none-any.whl", hash = "sha256:e65359a7af5cedad07fb77a2dd3f390f8eb0b74cb845589fa6c057086834dd38"}, + {file = "durationpy-0.9.tar.gz", hash = "sha256:fd3feb0a69a0057d582ef643c355c40d2fa1c942191f914d12203b1a01ac722a"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "filelock" +version = "3.16.1" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, + {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] +typing = ["typing-extensions (>=4.12.2)"] + +[[package]] +name = "frozenlist" +version = "1.5.0" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.8" +files = [ + {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a"}, + {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb"}, + {file = "frozenlist-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5"}, + {file = "frozenlist-1.5.0-cp310-cp310-win32.whl", hash = "sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb"}, + {file = "frozenlist-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4"}, + {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30"}, + {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5"}, + {file = "frozenlist-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf"}, + {file = "frozenlist-1.5.0-cp311-cp311-win32.whl", hash = "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942"}, + {file = "frozenlist-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f"}, + {file = "frozenlist-1.5.0-cp312-cp312-win32.whl", hash = "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8"}, + {file = "frozenlist-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f"}, + {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953"}, + {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0"}, + {file = "frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03"}, + {file = "frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c"}, + {file = "frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28"}, + {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:dd94994fc91a6177bfaafd7d9fd951bc8689b0a98168aa26b5f543868548d3ca"}, + {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0da8bbec082bf6bf18345b180958775363588678f64998c2b7609e34719b10"}, + {file = "frozenlist-1.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:73f2e31ea8dd7df61a359b731716018c2be196e5bb3b74ddba107f694fbd7604"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:828afae9f17e6de596825cf4228ff28fbdf6065974e5ac1410cecc22f699d2b3"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1577515d35ed5649d52ab4319db757bb881ce3b2b796d7283e6634d99ace307"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2150cc6305a2c2ab33299453e2968611dacb970d2283a14955923062c8d00b10"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a72b7a6e3cd2725eff67cd64c8f13335ee18fc3c7befc05aed043d24c7b9ccb9"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c16d2fa63e0800723139137d667e1056bee1a1cf7965153d2d104b62855e9b99"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:17dcc32fc7bda7ce5875435003220a457bcfa34ab7924a49a1c19f55b6ee185c"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:97160e245ea33d8609cd2b8fd997c850b56db147a304a262abc2b3be021a9171"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f1e6540b7fa044eee0bb5111ada694cf3dc15f2b0347ca125ee9ca984d5e9e6e"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:91d6c171862df0a6c61479d9724f22efb6109111017c87567cfeb7b5d1449fdf"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c1fac3e2ace2eb1052e9f7c7db480818371134410e1f5c55d65e8f3ac6d1407e"}, + {file = "frozenlist-1.5.0-cp38-cp38-win32.whl", hash = "sha256:b97f7b575ab4a8af9b7bc1d2ef7f29d3afee2226bd03ca3875c16451ad5a7723"}, + {file = "frozenlist-1.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:374ca2dabdccad8e2a76d40b1d037f5bd16824933bf7bcea3e59c891fd4a0923"}, + {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9bbcdfaf4af7ce002694a4e10a0159d5a8d20056a12b05b45cea944a4953f972"}, + {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1893f948bf6681733aaccf36c5232c231e3b5166d607c5fa77773611df6dc336"}, + {file = "frozenlist-1.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2b5e23253bb709ef57a8e95e6ae48daa9ac5f265637529e4ce6b003a37b2621f"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f253985bb515ecd89629db13cb58d702035ecd8cfbca7d7a7e29a0e6d39af5f"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04a5c6babd5e8fb7d3c871dc8b321166b80e41b637c31a995ed844a6139942b6"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fe0f1c29ba24ba6ff6abf688cb0b7cf1efab6b6aa6adc55441773c252f7411"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:226d72559fa19babe2ccd920273e767c96a49b9d3d38badd7c91a0fdeda8ea08"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b731db116ab3aedec558573c1a5eec78822b32292fe4f2f0345b7f697745c2"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:366d8f93e3edfe5a918c874702f78faac300209a4d5bf38352b2c1bdc07a766d"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1b96af8c582b94d381a1c1f51ffaedeb77c821c690ea5f01da3d70a487dd0a9b"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c03eff4a41bd4e38415cbed054bbaff4a075b093e2394b6915dca34a40d1e38b"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:50cf5e7ee9b98f22bdecbabf3800ae78ddcc26e4a435515fc72d97903e8488e0"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1e76bfbc72353269c44e0bc2cfe171900fbf7f722ad74c9a7b638052afe6a00c"}, + {file = "frozenlist-1.5.0-cp39-cp39-win32.whl", hash = "sha256:666534d15ba8f0fda3f53969117383d5dc021266b3c1a42c9ec4855e4b58b9d3"}, + {file = "frozenlist-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:5c28f4b5dbef8a0d8aad0d4de24d1e9e981728628afaf4ea0792f5d0939372f0"}, + {file = "frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3"}, + {file = "frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817"}, +] + +[[package]] +name = "fsspec" +version = "2024.10.0" +description = "File-system specification" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fsspec-2024.10.0-py3-none-any.whl", hash = "sha256:03b9a6785766a4de40368b88906366755e2819e758b83705c88cd7cb5fe81871"}, + {file = "fsspec-2024.10.0.tar.gz", hash = "sha256:eda2d8a4116d4f2429db8550f2457da57279247dd930bb12f821b58391359493"}, +] + +[package.extras] +abfs = ["adlfs"] +adl = ["adlfs"] +arrow = ["pyarrow (>=1)"] +dask = ["dask", "distributed"] +dev = ["pre-commit", "ruff"] +doc = ["numpydoc", "sphinx", "sphinx-design", "sphinx-rtd-theme", "yarl"] +dropbox = ["dropbox", "dropboxdrivefs", "requests"] +full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "dask", "distributed", "dropbox", "dropboxdrivefs", "fusepy", "gcsfs", "libarchive-c", "ocifs", "panel", "paramiko", "pyarrow (>=1)", "pygit2", "requests", "s3fs", "smbprotocol", "tqdm"] +fuse = ["fusepy"] +gcs = ["gcsfs"] +git = ["pygit2"] +github = ["requests"] +gs = ["gcsfs"] +gui = ["panel"] +hdfs = ["pyarrow (>=1)"] +http = ["aiohttp (!=4.0.0a0,!=4.0.0a1)"] +libarchive = ["libarchive-c"] +oci = ["ocifs"] +s3 = ["s3fs"] +sftp = ["paramiko"] +smb = ["smbprotocol"] +ssh = ["paramiko"] +test = ["aiohttp (!=4.0.0a0,!=4.0.0a1)", "numpy", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "requests"] +test-downstream = ["aiobotocore (>=2.5.4,<3.0.0)", "dask-expr", "dask[dataframe,test]", "moto[server] (>4,<5)", "pytest-timeout", "xarray"] +test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas", "panel", "paramiko", "pyarrow", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "smbprotocol", "tqdm", "urllib3", "zarr", "zstandard"] +tqdm = ["tqdm"] + +[[package]] +name = "google-auth" +version = "2.37.0" +description = "Google Authentication Library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "google_auth-2.37.0-py2.py3-none-any.whl", hash = "sha256:42664f18290a6be591be5329a96fe30184be1a1badb7292a7f686a9659de9ca0"}, + {file = "google_auth-2.37.0.tar.gz", hash = "sha256:0054623abf1f9c83492c63d3f47e77f0a544caa3d40b2d98e099a611c2dd5d00"}, +] + +[package.dependencies] +cachetools = ">=2.0.0,<6.0" +pyasn1-modules = ">=0.2.1" +rsa = ">=3.1.4,<5" + +[package.extras] +aiohttp = ["aiohttp (>=3.6.2,<4.0.0.dev0)", "requests (>=2.20.0,<3.0.0.dev0)"] +enterprise-cert = ["cryptography", "pyopenssl"] +pyjwt = ["cryptography (>=38.0.3)", "pyjwt (>=2.0)"] +pyopenssl = ["cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] +reauth = ["pyu2f (>=0.1.5)"] +requests = ["requests (>=2.20.0,<3.0.0.dev0)"] + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "httpcore" +version = "1.0.7" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, + {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.13,<0.15" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] + +[[package]] +name = "httpx" +version = "0.28.1" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "identify" +version = "2.6.3" +description = "File identification library for Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "identify-2.6.3-py2.py3-none-any.whl", hash = "sha256:9edba65473324c2ea9684b1f944fe3191db3345e50b6d04571d10ed164f8d7bd"}, + {file = "identify-2.6.3.tar.gz", hash = "sha256:62f5dae9b5fef52c84cc188514e9ea4f3f636b1d8799ab5ebc475471f9e47a02"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "ijson" +version = "3.3.0" +description = "Iterative JSON parser with standard Python iterator interfaces" +optional = false +python-versions = "*" +files = [ + {file = "ijson-3.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7f7a5250599c366369fbf3bc4e176f5daa28eb6bc7d6130d02462ed335361675"}, + {file = "ijson-3.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f87a7e52f79059f9c58f6886c262061065eb6f7554a587be7ed3aa63e6b71b34"}, + {file = "ijson-3.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b73b493af9e947caed75d329676b1b801d673b17481962823a3e55fe529c8b8b"}, + {file = "ijson-3.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5576415f3d76290b160aa093ff968f8bf6de7d681e16e463a0134106b506f49"}, + {file = "ijson-3.3.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e9ffe358d5fdd6b878a8a364e96e15ca7ca57b92a48f588378cef315a8b019e"}, + {file = "ijson-3.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8643c255a25824ddd0895c59f2319c019e13e949dc37162f876c41a283361527"}, + {file = "ijson-3.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:df3ab5e078cab19f7eaeef1d5f063103e1ebf8c26d059767b26a6a0ad8b250a3"}, + {file = "ijson-3.3.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3dc1fb02c6ed0bae1b4bf96971258bf88aea72051b6e4cebae97cff7090c0607"}, + {file = "ijson-3.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e9afd97339fc5a20f0542c971f90f3ca97e73d3050cdc488d540b63fae45329a"}, + {file = "ijson-3.3.0-cp310-cp310-win32.whl", hash = "sha256:844c0d1c04c40fd1b60f148dc829d3f69b2de789d0ba239c35136efe9a386529"}, + {file = "ijson-3.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:d654d045adafdcc6c100e8e911508a2eedbd2a1b5f93f930ba13ea67d7704ee9"}, + {file = "ijson-3.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:501dce8eaa537e728aa35810656aa00460a2547dcb60937c8139f36ec344d7fc"}, + {file = "ijson-3.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:658ba9cad0374d37b38c9893f4864f284cdcc7d32041f9808fba8c7bcaadf134"}, + {file = "ijson-3.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2636cb8c0f1023ef16173f4b9a233bcdb1df11c400c603d5f299fac143ca8d70"}, + {file = "ijson-3.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd174b90db68c3bcca273e9391934a25d76929d727dc75224bf244446b28b03b"}, + {file = "ijson-3.3.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:97a9aea46e2a8371c4cf5386d881de833ed782901ac9f67ebcb63bb3b7d115af"}, + {file = "ijson-3.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c594c0abe69d9d6099f4ece17763d53072f65ba60b372d8ba6de8695ce6ee39e"}, + {file = "ijson-3.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8e0ff16c224d9bfe4e9e6bd0395826096cda4a3ef51e6c301e1b61007ee2bd24"}, + {file = "ijson-3.3.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0015354011303175eae7e2ef5136414e91de2298e5a2e9580ed100b728c07e51"}, + {file = "ijson-3.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:034642558afa57351a0ffe6de89e63907c4cf6849070cc10a3b2542dccda1afe"}, + {file = "ijson-3.3.0-cp311-cp311-win32.whl", hash = "sha256:192e4b65495978b0bce0c78e859d14772e841724d3269fc1667dc6d2f53cc0ea"}, + {file = "ijson-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:72e3488453754bdb45c878e31ce557ea87e1eb0f8b4fc610373da35e8074ce42"}, + {file = "ijson-3.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:988e959f2f3d59ebd9c2962ae71b97c0df58323910d0b368cc190ad07429d1bb"}, + {file = "ijson-3.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b2f73f0d0fce5300f23a1383d19b44d103bb113b57a69c36fd95b7c03099b181"}, + {file = "ijson-3.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0ee57a28c6bf523d7cb0513096e4eb4dac16cd935695049de7608ec110c2b751"}, + {file = "ijson-3.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0155a8f079c688c2ccaea05de1ad69877995c547ba3d3612c1c336edc12a3a5"}, + {file = "ijson-3.3.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ab00721304af1ae1afa4313ecfa1bf16b07f55ef91e4a5b93aeaa3e2bd7917c"}, + {file = "ijson-3.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40ee3821ee90be0f0e95dcf9862d786a7439bd1113e370736bfdf197e9765bfb"}, + {file = "ijson-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3b6987a0bc3e6d0f721b42c7a0198ef897ae50579547b0345f7f02486898f5"}, + {file = "ijson-3.3.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:63afea5f2d50d931feb20dcc50954e23cef4127606cc0ecf7a27128ed9f9a9e6"}, + {file = "ijson-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b5c3e285e0735fd8c5a26d177eca8b52512cdd8687ca86ec77a0c66e9c510182"}, + {file = "ijson-3.3.0-cp312-cp312-win32.whl", hash = "sha256:907f3a8674e489abdcb0206723e5560a5cb1fa42470dcc637942d7b10f28b695"}, + {file = "ijson-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:8f890d04ad33262d0c77ead53c85f13abfb82f2c8f078dfbf24b78f59534dfdd"}, + {file = "ijson-3.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b9d85a02e77ee8ea6d9e3fd5d515bcc3d798d9c1ea54817e5feb97a9bc5d52fe"}, + {file = "ijson-3.3.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6576cdc36d5a09b0c1a3d81e13a45d41a6763188f9eaae2da2839e8a4240bce"}, + {file = "ijson-3.3.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5589225c2da4bb732c9c370c5961c39a6db72cf69fb2a28868a5413ed7f39e6"}, + {file = "ijson-3.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad04cf38164d983e85f9cba2804566c0160b47086dcca4cf059f7e26c5ace8ca"}, + {file = "ijson-3.3.0-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:a3b730ef664b2ef0e99dec01b6573b9b085c766400af363833e08ebc1e38eb2f"}, + {file = "ijson-3.3.0-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:4690e3af7b134298055993fcbea161598d23b6d3ede11b12dca6815d82d101d5"}, + {file = "ijson-3.3.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:aaa6bfc2180c31a45fac35d40e3312a3d09954638ce0b2e9424a88e24d262a13"}, + {file = "ijson-3.3.0-cp36-cp36m-win32.whl", hash = "sha256:44367090a5a876809eb24943f31e470ba372aaa0d7396b92b953dda953a95d14"}, + {file = "ijson-3.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7e2b3e9ca957153557d06c50a26abaf0d0d6c0ddf462271854c968277a6b5372"}, + {file = "ijson-3.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:47c144117e5c0e2babb559bc8f3f76153863b8dd90b2d550c51dab5f4b84a87f"}, + {file = "ijson-3.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ce02af5fbf9ba6abb70765e66930aedf73311c7d840478f1ccecac53fefbf3"}, + {file = "ijson-3.3.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ac6c3eeed25e3e2cb9b379b48196413e40ac4e2239d910bb33e4e7f6c137745"}, + {file = "ijson-3.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d92e339c69b585e7b1d857308ad3ca1636b899e4557897ccd91bb9e4a56c965b"}, + {file = "ijson-3.3.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:8c85447569041939111b8c7dbf6f8fa7a0eb5b2c4aebb3c3bec0fb50d7025121"}, + {file = "ijson-3.3.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:542c1e8fddf082159a5d759ee1412c73e944a9a2412077ed00b303ff796907dc"}, + {file = "ijson-3.3.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:30cfea40936afb33b57d24ceaf60d0a2e3d5c1f2335ba2623f21d560737cc730"}, + {file = "ijson-3.3.0-cp37-cp37m-win32.whl", hash = "sha256:6b661a959226ad0d255e49b77dba1d13782f028589a42dc3172398dd3814c797"}, + {file = "ijson-3.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:0b003501ee0301dbf07d1597482009295e16d647bb177ce52076c2d5e64113e0"}, + {file = "ijson-3.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3e8d8de44effe2dbd0d8f3eb9840344b2d5b4cc284a14eb8678aec31d1b6bea8"}, + {file = "ijson-3.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9cd5c03c63ae06d4f876b9844c5898d0044c7940ff7460db9f4cd984ac7862b5"}, + {file = "ijson-3.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:04366e7e4a4078d410845e58a2987fd9c45e63df70773d7b6e87ceef771b51ee"}, + {file = "ijson-3.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de7c1ddb80fa7a3ab045266dca169004b93f284756ad198306533b792774f10a"}, + {file = "ijson-3.3.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8851584fb931cffc0caa395f6980525fd5116eab8f73ece9d95e6f9c2c326c4c"}, + {file = "ijson-3.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdcfc88347fd981e53c33d832ce4d3e981a0d696b712fbcb45dcc1a43fe65c65"}, + {file = "ijson-3.3.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3917b2b3d0dbbe3296505da52b3cb0befbaf76119b2edaff30bd448af20b5400"}, + {file = "ijson-3.3.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:e10c14535abc7ddf3fd024aa36563cd8ab5d2bb6234a5d22c77c30e30fa4fb2b"}, + {file = "ijson-3.3.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3aba5c4f97f4e2ce854b5591a8b0711ca3b0c64d1b253b04ea7b004b0a197ef6"}, + {file = "ijson-3.3.0-cp38-cp38-win32.whl", hash = "sha256:b325f42e26659df1a0de66fdb5cde8dd48613da9c99c07d04e9fb9e254b7ee1c"}, + {file = "ijson-3.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:ff835906f84451e143f31c4ce8ad73d83ef4476b944c2a2da91aec8b649570e1"}, + {file = "ijson-3.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3c556f5553368dff690c11d0a1fb435d4ff1f84382d904ccc2dc53beb27ba62e"}, + {file = "ijson-3.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e4396b55a364a03ff7e71a34828c3ed0c506814dd1f50e16ebed3fc447d5188e"}, + {file = "ijson-3.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e6850ae33529d1e43791b30575070670070d5fe007c37f5d06aebc1dd152ab3f"}, + {file = "ijson-3.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36aa56d68ea8def26778eb21576ae13f27b4a47263a7a2581ab2ef58b8de4451"}, + {file = "ijson-3.3.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7ec759c4a0fc820ad5dc6a58e9c391e7b16edcb618056baedbedbb9ea3b1524"}, + {file = "ijson-3.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b51bab2c4e545dde93cb6d6bb34bf63300b7cd06716f195dd92d9255df728331"}, + {file = "ijson-3.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:92355f95a0e4da96d4c404aa3cff2ff033f9180a9515f813255e1526551298c1"}, + {file = "ijson-3.3.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8795e88adff5aa3c248c1edce932db003d37a623b5787669ccf205c422b91e4a"}, + {file = "ijson-3.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8f83f553f4cde6d3d4eaf58ec11c939c94a0ec545c5b287461cafb184f4b3a14"}, + {file = "ijson-3.3.0-cp39-cp39-win32.whl", hash = "sha256:ead50635fb56577c07eff3e557dac39533e0fe603000684eea2af3ed1ad8f941"}, + {file = "ijson-3.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:c8a9befb0c0369f0cf5c1b94178d0d78f66d9cebb9265b36be6e4f66236076b8"}, + {file = "ijson-3.3.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2af323a8aec8a50fa9effa6d640691a30a9f8c4925bd5364a1ca97f1ac6b9b5c"}, + {file = "ijson-3.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f64f01795119880023ba3ce43072283a393f0b90f52b66cc0ea1a89aa64a9ccb"}, + {file = "ijson-3.3.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a716e05547a39b788deaf22725490855337fc36613288aa8ae1601dc8c525553"}, + {file = "ijson-3.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:473f5d921fadc135d1ad698e2697025045cd8ed7e5e842258295012d8a3bc702"}, + {file = "ijson-3.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd26b396bc3a1e85f4acebeadbf627fa6117b97f4c10b177d5779577c6607744"}, + {file = "ijson-3.3.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:25fd49031cdf5fd5f1fd21cb45259a64dad30b67e64f745cc8926af1c8c243d3"}, + {file = "ijson-3.3.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b72178b1e565d06ab19319965022b36ef41bcea7ea153b32ec31194bec032a2"}, + {file = "ijson-3.3.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d0b6b637d05dbdb29d0bfac2ed8425bb369e7af5271b0cc7cf8b801cb7360c2"}, + {file = "ijson-3.3.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5378d0baa59ae422905c5f182ea0fd74fe7e52a23e3821067a7d58c8306b2191"}, + {file = "ijson-3.3.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:99f5c8ab048ee4233cc4f2b461b205cbe01194f6201018174ac269bf09995749"}, + {file = "ijson-3.3.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:45ff05de889f3dc3d37a59d02096948ce470699f2368b32113954818b21aa74a"}, + {file = "ijson-3.3.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1efb521090dd6cefa7aafd120581947b29af1713c902ff54336b7c7130f04c47"}, + {file = "ijson-3.3.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87c727691858fd3a1c085d9980d12395517fcbbf02c69fbb22dede8ee03422da"}, + {file = "ijson-3.3.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0420c24e50389bc251b43c8ed379ab3e3ba065ac8262d98beb6735ab14844460"}, + {file = "ijson-3.3.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:8fdf3721a2aa7d96577970f5604bd81f426969c1822d467f07b3d844fa2fecc7"}, + {file = "ijson-3.3.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:891f95c036df1bc95309951940f8eea8537f102fa65715cdc5aae20b8523813b"}, + {file = "ijson-3.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed1336a2a6e5c427f419da0154e775834abcbc8ddd703004108121c6dd9eba9d"}, + {file = "ijson-3.3.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0c819f83e4f7b7f7463b2dc10d626a8be0c85fbc7b3db0edc098c2b16ac968e"}, + {file = "ijson-3.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33afc25057377a6a43c892de34d229a86f89ea6c4ca3dd3db0dcd17becae0dbb"}, + {file = "ijson-3.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7914d0cf083471856e9bc2001102a20f08e82311dfc8cf1a91aa422f9414a0d6"}, + {file = "ijson-3.3.0.tar.gz", hash = "sha256:7f172e6ba1bee0d4c8f8ebd639577bfe429dee0f3f96775a067b8bae4492d8a0"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "inspect-ai" +version = "0.3.52" +description = "Framework for large language model evaluations" +optional = false +python-versions = ">=3.10" +files = [ + {file = "inspect_ai-0.3.52-py3-none-any.whl", hash = "sha256:44d8c93490ca0b111a6140283eb28545de133f3d6b6f4e39ff424c472de59378"}, + {file = "inspect_ai-0.3.52.tar.gz", hash = "sha256:e4811da1d36f5f34dcd2159e9416052d5eb301cf02c9cc5909e2c90320d8bca1"}, +] + +[package.dependencies] +aiofiles = "*" +aiohttp = ">=3.9.0" +anyio = ">=4.4.0" +beautifulsoup4 = "*" +click = ">=8.1.3" +debugpy = "*" +docstring-parser = ">=0.16" +fsspec = ">=2021.09.0" +httpx = "*" +ijson = ">=3.2.0" +jsonlines = ">=3.0.0" +jsonpatch = ">=1.32" +jsonschema = ">3.1.1" +mmh3 = ">3.1.0" +nest_asyncio = "*" +numpy = "*" +platformdirs = ">=2.3.0" +psutil = "*" +pydantic = ">=2" +python-dotenv = ">=0.16.0" +pyyaml = "*" +rich = ">=13.3.3" +s3fs = ">=2023" +semver = ">=3.0.0" +shortuuid = "*" +tenacity = "*" +textual = ">=0.86.2" +typing_extensions = ">=4.9.0" +zipp = ">=3.19.1" + +[package.extras] +dev = ["aioboto3", "anthropic", "azure-ai-inference", "google-cloud-aiplatform", "google-generativeai", "groq", "ipython", "mistralai", "moto[server]", "mypy", "nbformat", "openai", "pre-commit", "pytest", "pytest-asyncio", "pytest-cov", "pytest-dotenv", "pytest-xdist", "ruff (==0.8.3)", "textual-dev (>=0.86.2)", "types-PyYAML", "types-aioboto3", "types-aiofiles", "types-beautifulsoup4", "types-boto3", "types-botocore", "types-jsonpatch", "types-jsonschema", "types-protobuf", "types-psutil", "types-python-dateutil"] +dist = ["build", "twine"] +doc = ["jupyter", "quarto-cli"] + +[[package]] +name = "jmespath" +version = "1.0.1" +description = "JSON Matching Expressions" +optional = false +python-versions = ">=3.7" +files = [ + {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, + {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, +] + +[[package]] +name = "jsonlines" +version = "4.0.0" +description = "Library with helpers for the jsonlines file format" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jsonlines-4.0.0-py3-none-any.whl", hash = "sha256:185b334ff2ca5a91362993f42e83588a360cf95ce4b71a73548502bda52a7c55"}, + {file = "jsonlines-4.0.0.tar.gz", hash = "sha256:0c6d2c09117550c089995247f605ae4cf77dd1533041d366351f6f298822ea74"}, +] + +[package.dependencies] +attrs = ">=19.2.0" + +[[package]] +name = "jsonpatch" +version = "1.33" +description = "Apply JSON-Patches (RFC 6902)" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" +files = [ + {file = "jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade"}, + {file = "jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c"}, +] + +[package.dependencies] +jsonpointer = ">=1.9" + +[[package]] +name = "jsonpointer" +version = "3.0.0" +description = "Identify specific nodes in a JSON document (RFC 6901)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"}, + {file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"}, +] + +[[package]] +name = "jsonschema" +version = "4.23.0" +description = "An implementation of JSON Schema validation for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566"}, + {file = "jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +jsonschema-specifications = ">=2023.03.6" +referencing = ">=0.28.4" +rpds-py = ">=0.7.1" + +[package.extras] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=24.6.0)"] + +[[package]] +name = "jsonschema-specifications" +version = "2024.10.1" +description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +optional = false +python-versions = ">=3.9" +files = [ + {file = "jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf"}, + {file = "jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272"}, +] + +[package.dependencies] +referencing = ">=0.31.0" + +[[package]] +name = "kubernetes" +version = "31.0.0" +description = "Kubernetes python client" +optional = false +python-versions = ">=3.6" +files = [ + {file = "kubernetes-31.0.0-py2.py3-none-any.whl", hash = "sha256:bf141e2d380c8520eada8b351f4e319ffee9636328c137aa432bc486ca1200e1"}, + {file = "kubernetes-31.0.0.tar.gz", hash = "sha256:28945de906c8c259c1ebe62703b56a03b714049372196f854105afe4e6d014c0"}, +] + +[package.dependencies] +certifi = ">=14.05.14" +durationpy = ">=0.7" +google-auth = ">=1.0.1" +oauthlib = ">=3.2.2" +python-dateutil = ">=2.5.3" +pyyaml = ">=5.4.1" +requests = "*" +requests-oauthlib = "*" +six = ">=1.9.0" +urllib3 = ">=1.24.2" +websocket-client = ">=0.32.0,<0.40.0 || >0.40.0,<0.41.dev0 || >=0.43.dev0" + +[package.extras] +adal = ["adal (>=1.0.2)"] + +[[package]] +name = "linkify-it-py" +version = "2.0.3" +description = "Links recognition library with FULL unicode support." +optional = false +python-versions = ">=3.7" +files = [ + {file = "linkify-it-py-2.0.3.tar.gz", hash = "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048"}, + {file = "linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79"}, +] + +[package.dependencies] +uc-micro-py = "*" + +[package.extras] +benchmark = ["pytest", "pytest-benchmark"] +dev = ["black", "flake8", "isort", "pre-commit", "pyproject-flake8"] +doc = ["myst-parser", "sphinx", "sphinx-book-theme"] +test = ["coverage", "pytest", "pytest-cov"] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +linkify-it-py = {version = ">=1,<3", optional = true, markers = "extra == \"linkify\""} +mdit-py-plugins = {version = "*", optional = true, markers = "extra == \"plugins\""} +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "mdit-py-plugins" +version = "0.4.2" +description = "Collection of plugins for markdown-it-py" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636"}, + {file = "mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5"}, +] + +[package.dependencies] +markdown-it-py = ">=1.0.0,<4.0.0" + +[package.extras] +code-style = ["pre-commit"] +rtd = ["myst-parser", "sphinx-book-theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "mmh3" +version = "5.0.1" +description = "Python extension for MurmurHash (MurmurHash3), a set of fast and robust hash functions." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mmh3-5.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f0a4b4bf05778ed77d820d6e7d0e9bd6beb0c01af10e1ce9233f5d2f814fcafa"}, + {file = "mmh3-5.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac7a391039aeab95810c2d020b69a94eb6b4b37d4e2374831e92db3a0cdf71c6"}, + {file = "mmh3-5.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3a2583b5521ca49756d8d8bceba80627a9cc295f255dcab4e3df7ccc2f09679a"}, + {file = "mmh3-5.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:081a8423fe53c1ac94f87165f3e4c500125d343410c1a0c5f1703e898a3ef038"}, + {file = "mmh3-5.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8b4d72713799755dc8954a7d36d5c20a6c8de7b233c82404d122c7c7c1707cc"}, + {file = "mmh3-5.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:389a6fd51efc76d3182d36ec306448559c1244f11227d2bb771bdd0e6cc91321"}, + {file = "mmh3-5.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39f4128edaa074bff721b1d31a72508cba4d2887ee7867f22082e1fe9d4edea0"}, + {file = "mmh3-5.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d5d23a94d91aabba3386b3769048d5f4210fdfef80393fece2f34ba5a7b466c"}, + {file = "mmh3-5.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:16347d038361f8b8f24fd2b7ef378c9b68ddee9f7706e46269b6e0d322814713"}, + {file = "mmh3-5.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6e299408565af7d61f2d20a5ffdd77cf2ed902460fe4e6726839d59ba4b72316"}, + {file = "mmh3-5.0.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42050af21ddfc5445ee5a66e73a8fc758c71790305e3ee9e4a85a8e69e810f94"}, + {file = "mmh3-5.0.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2ae9b1f5ef27ec54659920f0404b7ceb39966e28867c461bfe83a05e8d18ddb0"}, + {file = "mmh3-5.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:50c2495a02045f3047d71d4ae9cdd7a15efc0bcbb7ff17a18346834a8e2d1d19"}, + {file = "mmh3-5.0.1-cp310-cp310-win32.whl", hash = "sha256:c028fa77cddf351ca13b4a56d43c1775652cde0764cadb39120b68f02a23ecf6"}, + {file = "mmh3-5.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:c5e741e421ec14400c4aae30890515c201f518403bdef29ae1e00d375bb4bbb5"}, + {file = "mmh3-5.0.1-cp310-cp310-win_arm64.whl", hash = "sha256:b17156d56fabc73dbf41bca677ceb6faed435cc8544f6566d72ea77d8a17e9d0"}, + {file = "mmh3-5.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a6d5a9b1b923f1643559ba1fc0bf7a5076c90cbb558878d3bf3641ce458f25d"}, + {file = "mmh3-5.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3349b968be555f7334bbcce839da98f50e1e80b1c615d8e2aa847ea4a964a012"}, + {file = "mmh3-5.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1bd3c94b110e55db02ab9b605029f48a2f7f677c6e58c09d44e42402d438b7e1"}, + {file = "mmh3-5.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47ba84d48608f79adbb10bb09986b6dc33eeda5c2d1bd75d00820081b73bde9"}, + {file = "mmh3-5.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c0217987a8b8525c8d9170f66d036dec4ab45cfbd53d47e8d76125791ceb155e"}, + {file = "mmh3-5.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2797063a34e78d1b61639a98b0edec1c856fa86ab80c7ec859f1796d10ba429"}, + {file = "mmh3-5.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8bba16340adcbd47853a2fbe5afdb397549e8f2e79324ff1dced69a3f8afe7c3"}, + {file = "mmh3-5.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:282797957c9f60b51b9d768a602c25f579420cc9af46feb77d457a27823d270a"}, + {file = "mmh3-5.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e4fb670c29e63f954f9e7a2cdcd57b36a854c2538f579ef62681ccbaa1de2b69"}, + {file = "mmh3-5.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ee7d85438dc6aff328e19ab052086a3c29e8a9b632998a49e5c4b0034e9e8d6"}, + {file = "mmh3-5.0.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b7fb5db231f3092444bc13901e6a8d299667126b00636ffbad4a7b45e1051e2f"}, + {file = "mmh3-5.0.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c100dd441703da5ec136b1d9003ed4a041d8a1136234c9acd887499796df6ad8"}, + {file = "mmh3-5.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:71f3b765138260fd7a7a2dba0ea5727dabcd18c1f80323c9cfef97a7e86e01d0"}, + {file = "mmh3-5.0.1-cp311-cp311-win32.whl", hash = "sha256:9a76518336247fd17689ce3ae5b16883fd86a490947d46a0193d47fb913e26e3"}, + {file = "mmh3-5.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:336bc4df2e44271f1c302d289cc3d78bd52d3eed8d306c7e4bff8361a12bf148"}, + {file = "mmh3-5.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:af6522722fbbc5999aa66f7244d0986767a46f1fb05accc5200f75b72428a508"}, + {file = "mmh3-5.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f2730bb263ed9c388e8860438b057a53e3cc701134a6ea140f90443c4c11aa40"}, + {file = "mmh3-5.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6246927bc293f6d56724536400b85fb85f5be26101fa77d5f97dd5e2a4c69bf2"}, + {file = "mmh3-5.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fbca322519a6e6e25b6abf43e940e1667cf8ea12510e07fb4919b48a0cd1c411"}, + {file = "mmh3-5.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eae8c19903ed8a1724ad9e67e86f15d198a7a1271a4f9be83d47e38f312ed672"}, + {file = "mmh3-5.0.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a09fd6cc72c07c0c07c3357714234b646d78052487c4a3bd5f7f6e08408cff60"}, + {file = "mmh3-5.0.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ff8551fee7ae3b11c5d986b6347ade0dccaadd4670ffdb2b944dee120ffcc84"}, + {file = "mmh3-5.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e39694c73a5a20c8bf36dfd8676ed351e5234d55751ba4f7562d85449b21ef3f"}, + {file = "mmh3-5.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eba6001989a92f72a89c7cf382fda831678bd780707a66b4f8ca90239fdf2123"}, + {file = "mmh3-5.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0771f90c9911811cc606a5c7b7b58f33501c9ee896ed68a6ac22c7d55878ecc0"}, + {file = "mmh3-5.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:09b31ed0c0c0920363e96641fac4efde65b1ab62b8df86293142f35a254e72b4"}, + {file = "mmh3-5.0.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5cf4a8deda0235312db12075331cb417c4ba163770edfe789bde71d08a24b692"}, + {file = "mmh3-5.0.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:41f7090a95185ef20ac018581a99337f0cbc84a2135171ee3290a9c0d9519585"}, + {file = "mmh3-5.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b97b5b368fb7ff22194ec5854f5b12d8de9ab67a0f304728c7f16e5d12135b76"}, + {file = "mmh3-5.0.1-cp312-cp312-win32.whl", hash = "sha256:842516acf04da546f94fad52db125ee619ccbdcada179da51c326a22c4578cb9"}, + {file = "mmh3-5.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:d963be0dbfd9fca209c17172f6110787ebf78934af25e3694fe2ba40e55c1e2b"}, + {file = "mmh3-5.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:a5da292ceeed8ce8e32b68847261a462d30fd7b478c3f55daae841404f433c15"}, + {file = "mmh3-5.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:673e3f1c8d4231d6fb0271484ee34cb7146a6499fc0df80788adb56fd76842da"}, + {file = "mmh3-5.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f795a306bd16a52ad578b663462cc8e95500b3925d64118ae63453485d67282b"}, + {file = "mmh3-5.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5ed57a5e28e502a1d60436cc25c76c3a5ba57545f250f2969af231dc1221e0a5"}, + {file = "mmh3-5.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:632c28e7612e909dbb6cbe2fe496201ada4695b7715584005689c5dc038e59ad"}, + {file = "mmh3-5.0.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53fd6bd525a5985e391c43384672d9d6b317fcb36726447347c7fc75bfed34ec"}, + {file = "mmh3-5.0.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dceacf6b0b961a0e499836af3aa62d60633265607aef551b2a3e3c48cdaa5edd"}, + {file = "mmh3-5.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8f0738d478fdfb5d920f6aff5452c78f2c35b0eff72caa2a97dfe38e82f93da2"}, + {file = "mmh3-5.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e70285e7391ab88b872e5bef632bad16b9d99a6d3ca0590656a4753d55988af"}, + {file = "mmh3-5.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:27e5fc6360aa6b828546a4318da1a7da6bf6e5474ccb053c3a6aa8ef19ff97bd"}, + {file = "mmh3-5.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7989530c3c1e2c17bf5a0ec2bba09fd19819078ba90beedabb1c3885f5040b0d"}, + {file = "mmh3-5.0.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:cdad7bee649950da7ecd3cbbbd12fb81f1161072ecbdb5acfa0018338c5cb9cf"}, + {file = "mmh3-5.0.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e143b8f184c1bb58cecd85ab4a4fd6dc65a2d71aee74157392c3fddac2a4a331"}, + {file = "mmh3-5.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5eb12e886f3646dd636f16b76eb23fc0c27e8ff3c1ae73d4391e50ef60b40f6"}, + {file = "mmh3-5.0.1-cp313-cp313-win32.whl", hash = "sha256:16e6dddfa98e1c2d021268e72c78951234186deb4df6630e984ac82df63d0a5d"}, + {file = "mmh3-5.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:d3ffb792d70b8c4a2382af3598dad6ae0c5bd9cee5b7ffcc99aa2f5fd2c1bf70"}, + {file = "mmh3-5.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:122fa9ec148383f9124292962bda745f192b47bfd470b2af5fe7bb3982b17896"}, + {file = "mmh3-5.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b12bad8c75e6ff5d67319794fb6a5e8c713826c818d47f850ad08b4aa06960c6"}, + {file = "mmh3-5.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e5bbb066538c1048d542246fc347bb7994bdda29a3aea61c22f9f8b57111ce69"}, + {file = "mmh3-5.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:eee6134273f64e2a106827cc8fd77e70cc7239a285006fc6ab4977d59b015af2"}, + {file = "mmh3-5.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d04d9aa19d48e4c7bbec9cabc2c4dccc6ff3b2402f856d5bf0de03e10f167b5b"}, + {file = "mmh3-5.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79f37da1eed034d06567a69a7988456345c7f29e49192831c3975b464493b16e"}, + {file = "mmh3-5.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:242f77666743337aa828a2bf2da71b6ba79623ee7f93edb11e009f69237c8561"}, + {file = "mmh3-5.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffd943fff690463945f6441a2465555b3146deaadf6a5e88f2590d14c655d71b"}, + {file = "mmh3-5.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:565b15f8d7df43acb791ff5a360795c20bfa68bca8b352509e0fbabd06cc48cd"}, + {file = "mmh3-5.0.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fc6aafb867c2030df98ac7760ff76b500359252867985f357bd387739f3d5287"}, + {file = "mmh3-5.0.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:32898170644d45aa27c974ab0d067809c066205110f5c6d09f47d9ece6978bfe"}, + {file = "mmh3-5.0.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:42865567838d2193eb64e0ef571f678bf361a254fcdef0c5c8e73243217829bd"}, + {file = "mmh3-5.0.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:5ff5c1f301c4a8b6916498969c0fcc7e3dbc56b4bfce5cfe3fe31f3f4609e5ae"}, + {file = "mmh3-5.0.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:be74c2dda8a6f44a504450aa2c3507f8067a159201586fc01dd41ab80efc350f"}, + {file = "mmh3-5.0.1-cp38-cp38-win32.whl", hash = "sha256:5610a842621ff76c04b20b29cf5f809b131f241a19d4937971ba77dc99a7f330"}, + {file = "mmh3-5.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:de15739ac50776fe8aa1ef13f1be46a6ee1fbd45f6d0651084097eb2be0a5aa4"}, + {file = "mmh3-5.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:48e84cf3cc7e8c41bc07de72299a73b92d9e3cde51d97851420055b1484995f7"}, + {file = "mmh3-5.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6dd9dc28c2d168c49928195c2e29b96f9582a5d07bd690a28aede4cc07b0e696"}, + {file = "mmh3-5.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2771a1c56a3d4bdad990309cff5d0a8051f29c8ec752d001f97d6392194ae880"}, + {file = "mmh3-5.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5ff2a8322ba40951a84411550352fba1073ce1c1d1213bb7530f09aed7f8caf"}, + {file = "mmh3-5.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a16bd3ec90682c9e0a343e6bd4c778c09947c8c5395cdb9e5d9b82b2559efbca"}, + {file = "mmh3-5.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d45733a78d68b5b05ff4a823aea51fa664df1d3bf4929b152ff4fd6dea2dd69b"}, + {file = "mmh3-5.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:904285e83cedebc8873b0838ed54c20f7344120be26e2ca5a907ab007a18a7a0"}, + {file = "mmh3-5.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac4aeb1784e43df728034d0ed72e4b2648db1a69fef48fa58e810e13230ae5ff"}, + {file = "mmh3-5.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:cb3d4f751a0b8b4c8d06ef1c085216c8fddcc8b8c8d72445976b5167a40c6d1e"}, + {file = "mmh3-5.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8021851935600e60c42122ed1176399d7692df338d606195cd599d228a04c1c6"}, + {file = "mmh3-5.0.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:6182d5924a5efc451900f864cbb021d7e8ad5d524816ca17304a0f663bc09bb5"}, + {file = "mmh3-5.0.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:5f30b834552a4f79c92e3d266336fb87fd92ce1d36dc6813d3e151035890abbd"}, + {file = "mmh3-5.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cd4383f35e915e06d077df27e04ffd3be7513ec6a9de2d31f430393f67e192a7"}, + {file = "mmh3-5.0.1-cp39-cp39-win32.whl", hash = "sha256:1455fb6b42665a97db8fc66e89a861e52b567bce27ed054c47877183f86ea6e3"}, + {file = "mmh3-5.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:9e26a0f4eb9855a143f5938a53592fa14c2d3b25801c2106886ab6c173982780"}, + {file = "mmh3-5.0.1-cp39-cp39-win_arm64.whl", hash = "sha256:0d0a35a69abdad7549c4030a714bb4ad07902edb3bbe61e1bbc403ded5d678be"}, + {file = "mmh3-5.0.1.tar.gz", hash = "sha256:7dab080061aeb31a6069a181f27c473a1f67933854e36a3464931f2716508896"}, +] + +[package.extras] +benchmark = ["pymmh3 (==0.0.5)", "pyperf (==2.7.0)", "xxhash (==3.5.0)"] +docs = ["myst-parser (==4.0.0)", "shibuya (==2024.8.30)", "sphinx (==8.0.2)", "sphinx-copybutton (==0.5.2)"] +lint = ["black (==24.8.0)", "clang-format (==18.1.8)", "isort (==5.13.2)", "pylint (==3.2.7)"] +plot = ["matplotlib (==3.9.2)", "pandas (==2.2.2)"] +test = ["pytest (==8.3.3)", "pytest-sugar (==1.0.0)"] +type = ["mypy (==1.11.2)"] + +[[package]] +name = "multidict" +version = "6.1.0" +description = "multidict implementation" +optional = false +python-versions = ">=3.8" +files = [ + {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60"}, + {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1"}, + {file = "multidict-6.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7"}, + {file = "multidict-6.1.0-cp310-cp310-win32.whl", hash = "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0"}, + {file = "multidict-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753"}, + {file = "multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80"}, + {file = "multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3"}, + {file = "multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133"}, + {file = "multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6"}, + {file = "multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81"}, + {file = "multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:db7457bac39421addd0c8449933ac32d8042aae84a14911a757ae6ca3eef1392"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d094ddec350a2fb899fec68d8353c78233debde9b7d8b4beeafa70825f1c281a"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5845c1fd4866bb5dd3125d89b90e57ed3138241540897de748cdf19de8a2fca2"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9079dfc6a70abe341f521f78405b8949f96db48da98aeb43f9907f342f627cdc"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3914f5aaa0f36d5d60e8ece6a308ee1c9784cd75ec8151062614657a114c4478"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c08be4f460903e5a9d0f76818db3250f12e9c344e79314d1d570fc69d7f4eae4"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d093be959277cb7dee84b801eb1af388b6ad3ca6a6b6bf1ed7585895789d027d"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3702ea6872c5a2a4eeefa6ffd36b042e9773f05b1f37ae3ef7264b1163c2dcf6"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2090f6a85cafc5b2db085124d752757c9d251548cedabe9bd31afe6363e0aff2"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:f67f217af4b1ff66c68a87318012de788dd95fcfeb24cc889011f4e1c7454dfd"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:189f652a87e876098bbc67b4da1049afb5f5dfbaa310dd67c594b01c10388db6"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:6bb5992037f7a9eff7991ebe4273ea7f51f1c1c511e6a2ce511d0e7bdb754492"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f4c2b9e770c4e393876e35a7046879d195cd123b4f116d299d442b335bcd"}, + {file = "multidict-6.1.0-cp38-cp38-win32.whl", hash = "sha256:e27bbb6d14416713a8bd7aaa1313c0fc8d44ee48d74497a0ff4c3a1b6ccb5167"}, + {file = "multidict-6.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:22f3105d4fb15c8f57ff3959a58fcab6ce36814486500cd7485651230ad4d4ef"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43"}, + {file = "multidict-6.1.0-cp39-cp39-win32.whl", hash = "sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada"}, + {file = "multidict-6.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a"}, + {file = "multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506"}, + {file = "multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} + +[[package]] +name = "mypy" +version = "1.13.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, + {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, + {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"}, + {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"}, + {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"}, + {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"}, + {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"}, + {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"}, + {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"}, + {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"}, + {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"}, + {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"}, + {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"}, + {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"}, + {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"}, + {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"}, + {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"}, + {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"}, + {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"}, + {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"}, + {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"}, + {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +description = "Patch asyncio to allow nested event loops" +optional = false +python-versions = ">=3.5" +files = [ + {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"}, + {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + +[[package]] +name = "numpy" +version = "2.2.0" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.10" +files = [ + {file = "numpy-2.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1e25507d85da11ff5066269d0bd25d06e0a0f2e908415534f3e603d2a78e4ffa"}, + {file = "numpy-2.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a62eb442011776e4036af5c8b1a00b706c5bc02dc15eb5344b0c750428c94219"}, + {file = "numpy-2.2.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:b606b1aaf802e6468c2608c65ff7ece53eae1a6874b3765f69b8ceb20c5fa78e"}, + {file = "numpy-2.2.0-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:36b2b43146f646642b425dd2027730f99bac962618ec2052932157e213a040e9"}, + {file = "numpy-2.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fe8f3583e0607ad4e43a954e35c1748b553bfe9fdac8635c02058023277d1b3"}, + {file = "numpy-2.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:122fd2fcfafdefc889c64ad99c228d5a1f9692c3a83f56c292618a59aa60ae83"}, + {file = "numpy-2.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3f2f5cddeaa4424a0a118924b988746db6ffa8565e5829b1841a8a3bd73eb59a"}, + {file = "numpy-2.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7fe4bb0695fe986a9e4deec3b6857003b4cfe5c5e4aac0b95f6a658c14635e31"}, + {file = "numpy-2.2.0-cp310-cp310-win32.whl", hash = "sha256:b30042fe92dbd79f1ba7f6898fada10bdaad1847c44f2dff9a16147e00a93661"}, + {file = "numpy-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:54dc1d6d66f8d37843ed281773c7174f03bf7ad826523f73435deb88ba60d2d4"}, + {file = "numpy-2.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9874bc2ff574c40ab7a5cbb7464bf9b045d617e36754a7bc93f933d52bd9ffc6"}, + {file = "numpy-2.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0da8495970f6b101ddd0c38ace92edea30e7e12b9a926b57f5fabb1ecc25bb90"}, + {file = "numpy-2.2.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0557eebc699c1c34cccdd8c3778c9294e8196df27d713706895edc6f57d29608"}, + {file = "numpy-2.2.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:3579eaeb5e07f3ded59298ce22b65f877a86ba8e9fe701f5576c99bb17c283da"}, + {file = "numpy-2.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40deb10198bbaa531509aad0cd2f9fadb26c8b94070831e2208e7df543562b74"}, + {file = "numpy-2.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2aed8fcf8abc3020d6a9ccb31dbc9e7d7819c56a348cc88fd44be269b37427e"}, + {file = "numpy-2.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a222d764352c773aa5ebde02dd84dba3279c81c6db2e482d62a3fa54e5ece69b"}, + {file = "numpy-2.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4e58666988605e251d42c2818c7d3d8991555381be26399303053b58a5bbf30d"}, + {file = "numpy-2.2.0-cp311-cp311-win32.whl", hash = "sha256:4723a50e1523e1de4fccd1b9a6dcea750c2102461e9a02b2ac55ffeae09a4410"}, + {file = "numpy-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:16757cf28621e43e252c560d25b15f18a2f11da94fea344bf26c599b9cf54b73"}, + {file = "numpy-2.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cff210198bb4cae3f3c100444c5eaa573a823f05c253e7188e1362a5555235b3"}, + {file = "numpy-2.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58b92a5828bd4d9aa0952492b7de803135038de47343b2aa3cc23f3b71a3dc4e"}, + {file = "numpy-2.2.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:ebe5e59545401fbb1b24da76f006ab19734ae71e703cdb4a8b347e84a0cece67"}, + {file = "numpy-2.2.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e2b8cd48a9942ed3f85b95ca4105c45758438c7ed28fff1e4ce3e57c3b589d8e"}, + {file = "numpy-2.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57fcc997ffc0bef234b8875a54d4058afa92b0b0c4223fc1f62f24b3b5e86038"}, + {file = "numpy-2.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85ad7d11b309bd132d74397fcf2920933c9d1dc865487128f5c03d580f2c3d03"}, + {file = "numpy-2.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cb24cca1968b21355cc6f3da1a20cd1cebd8a023e3c5b09b432444617949085a"}, + {file = "numpy-2.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0798b138c291d792f8ea40fe3768610f3c7dd2574389e37c3f26573757c8f7ef"}, + {file = "numpy-2.2.0-cp312-cp312-win32.whl", hash = "sha256:afe8fb968743d40435c3827632fd36c5fbde633b0423da7692e426529b1759b1"}, + {file = "numpy-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:3a4199f519e57d517ebd48cb76b36c82da0360781c6a0353e64c0cac30ecaad3"}, + {file = "numpy-2.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f8c8b141ef9699ae777c6278b52c706b653bf15d135d302754f6b2e90eb30367"}, + {file = "numpy-2.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f0986e917aca18f7a567b812ef7ca9391288e2acb7a4308aa9d265bd724bdae"}, + {file = "numpy-2.2.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:1c92113619f7b272838b8d6702a7f8ebe5edea0df48166c47929611d0b4dea69"}, + {file = "numpy-2.2.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5a145e956b374e72ad1dff82779177d4a3c62bc8248f41b80cb5122e68f22d13"}, + {file = "numpy-2.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18142b497d70a34b01642b9feabb70156311b326fdddd875a9981f34a369b671"}, + {file = "numpy-2.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7d41d1612c1a82b64697e894b75db6758d4f21c3ec069d841e60ebe54b5b571"}, + {file = "numpy-2.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a98f6f20465e7618c83252c02041517bd2f7ea29be5378f09667a8f654a5918d"}, + {file = "numpy-2.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e09d40edfdb4e260cb1567d8ae770ccf3b8b7e9f0d9b5c2a9992696b30ce2742"}, + {file = "numpy-2.2.0-cp313-cp313-win32.whl", hash = "sha256:3905a5fffcc23e597ee4d9fb3fcd209bd658c352657548db7316e810ca80458e"}, + {file = "numpy-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a184288538e6ad699cbe6b24859206e38ce5fba28f3bcfa51c90d0502c1582b2"}, + {file = "numpy-2.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7832f9e8eb00be32f15fdfb9a981d6955ea9adc8574c521d48710171b6c55e95"}, + {file = "numpy-2.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f0dd071b95bbca244f4cb7f70b77d2ff3aaaba7fa16dc41f58d14854a6204e6c"}, + {file = "numpy-2.2.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:b0b227dcff8cdc3efbce66d4e50891f04d0a387cce282fe1e66199146a6a8fca"}, + {file = "numpy-2.2.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:6ab153263a7c5ccaf6dfe7e53447b74f77789f28ecb278c3b5d49db7ece10d6d"}, + {file = "numpy-2.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e500aba968a48e9019e42c0c199b7ec0696a97fa69037bea163b55398e390529"}, + {file = "numpy-2.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:440cfb3db4c5029775803794f8638fbdbf71ec702caf32735f53b008e1eaece3"}, + {file = "numpy-2.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a55dc7a7f0b6198b07ec0cd445fbb98b05234e8b00c5ac4874a63372ba98d4ab"}, + {file = "numpy-2.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4bddbaa30d78c86329b26bd6aaaea06b1e47444da99eddac7bf1e2fab717bd72"}, + {file = "numpy-2.2.0-cp313-cp313t-win32.whl", hash = "sha256:30bf971c12e4365153afb31fc73f441d4da157153f3400b82db32d04de1e4066"}, + {file = "numpy-2.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d35717333b39d1b6bb8433fa758a55f1081543de527171543a2b710551d40881"}, + {file = "numpy-2.2.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e12c6c1ce84628c52d6367863773f7c8c8241be554e8b79686e91a43f1733773"}, + {file = "numpy-2.2.0-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:b6207dc8fb3c8cb5668e885cef9ec7f70189bec4e276f0ff70d5aa078d32c88e"}, + {file = "numpy-2.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a50aeff71d0f97b6450d33940c7181b08be1441c6c193e678211bff11aa725e7"}, + {file = "numpy-2.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:df12a1f99b99f569a7c2ae59aa2d31724e8d835fc7f33e14f4792e3071d11221"}, + {file = "numpy-2.2.0.tar.gz", hash = "sha256:140dd80ff8981a583a60980be1a655068f8adebf7a45a06a6858c873fcdcd4a0"}, +] + +[[package]] +name = "oauthlib" +version = "3.2.2" +description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +optional = false +python-versions = ">=3.6" +files = [ + {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, + {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, +] + +[package.extras] +rsa = ["cryptography (>=3.0.0)"] +signals = ["blinker (>=1.4.0)"] +signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] + +[[package]] +name = "packaging" +version = "24.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "3.8.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, + {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "propcache" +version = "0.2.1" +description = "Accelerated property cache" +optional = false +python-versions = ">=3.9" +files = [ + {file = "propcache-0.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6b3f39a85d671436ee3d12c017f8fdea38509e4f25b28eb25877293c98c243f6"}, + {file = "propcache-0.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d51fbe4285d5db5d92a929e3e21536ea3dd43732c5b177c7ef03f918dff9f2"}, + {file = "propcache-0.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6445804cf4ec763dc70de65a3b0d9954e868609e83850a47ca4f0cb64bd79fea"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9479aa06a793c5aeba49ce5c5692ffb51fcd9a7016e017d555d5e2b0045d212"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9631c5e8b5b3a0fda99cb0d29c18133bca1e18aea9effe55adb3da1adef80d3"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3156628250f46a0895f1f36e1d4fbe062a1af8718ec3ebeb746f1d23f0c5dc4d"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b6fb63ae352e13748289f04f37868099e69dba4c2b3e271c46061e82c745634"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:887d9b0a65404929641a9fabb6452b07fe4572b269d901d622d8a34a4e9043b2"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a96dc1fa45bd8c407a0af03b2d5218392729e1822b0c32e62c5bf7eeb5fb3958"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a7e65eb5c003a303b94aa2c3852ef130230ec79e349632d030e9571b87c4698c"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:999779addc413181912e984b942fbcc951be1f5b3663cd80b2687758f434c583"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:19a0f89a7bb9d8048d9c4370c9c543c396e894c76be5525f5e1ad287f1750ddf"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1ac2f5fe02fa75f56e1ad473f1175e11f475606ec9bd0be2e78e4734ad575034"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:574faa3b79e8ebac7cb1d7930f51184ba1ccf69adfdec53a12f319a06030a68b"}, + {file = "propcache-0.2.1-cp310-cp310-win32.whl", hash = "sha256:03ff9d3f665769b2a85e6157ac8b439644f2d7fd17615a82fa55739bc97863f4"}, + {file = "propcache-0.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2d3af2e79991102678f53e0dbf4c35de99b6b8b58f29a27ca0325816364caaba"}, + {file = "propcache-0.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ffc3cca89bb438fb9c95c13fc874012f7b9466b89328c3c8b1aa93cdcfadd16"}, + {file = "propcache-0.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f174bbd484294ed9fdf09437f889f95807e5f229d5d93588d34e92106fbf6717"}, + {file = "propcache-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:70693319e0b8fd35dd863e3e29513875eb15c51945bf32519ef52927ca883bc3"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b480c6a4e1138e1aa137c0079b9b6305ec6dcc1098a8ca5196283e8a49df95a9"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d27b84d5880f6d8aa9ae3edb253c59d9f6642ffbb2c889b78b60361eed449787"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:857112b22acd417c40fa4595db2fe28ab900c8c5fe4670c7989b1c0230955465"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf6c4150f8c0e32d241436526f3c3f9cbd34429492abddbada2ffcff506c51af"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66d4cfda1d8ed687daa4bc0274fcfd5267873db9a5bc0418c2da19273040eeb7"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2f992c07c0fca81655066705beae35fc95a2fa7366467366db627d9f2ee097f"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:4a571d97dbe66ef38e472703067021b1467025ec85707d57e78711c085984e54"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bb6178c241278d5fe853b3de743087be7f5f4c6f7d6d22a3b524d323eecec505"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ad1af54a62ffe39cf34db1aa6ed1a1873bd548f6401db39d8e7cd060b9211f82"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e7048abd75fe40712005bcfc06bb44b9dfcd8e101dda2ecf2f5aa46115ad07ca"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:160291c60081f23ee43d44b08a7e5fb76681221a8e10b3139618c5a9a291b84e"}, + {file = "propcache-0.2.1-cp311-cp311-win32.whl", hash = "sha256:819ce3b883b7576ca28da3861c7e1a88afd08cc8c96908e08a3f4dd64a228034"}, + {file = "propcache-0.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:edc9fc7051e3350643ad929df55c451899bb9ae6d24998a949d2e4c87fb596d3"}, + {file = "propcache-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:081a430aa8d5e8876c6909b67bd2d937bfd531b0382d3fdedb82612c618bc41a"}, + {file = "propcache-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2ccec9ac47cf4e04897619c0e0c1a48c54a71bdf045117d3a26f80d38ab1fb0"}, + {file = "propcache-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14d86fe14b7e04fa306e0c43cdbeebe6b2c2156a0c9ce56b815faacc193e320d"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:049324ee97bb67285b49632132db351b41e77833678432be52bdd0289c0e05e4"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cd9a1d071158de1cc1c71a26014dcdfa7dd3d5f4f88c298c7f90ad6f27bb46d"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98110aa363f1bb4c073e8dcfaefd3a5cea0f0834c2aab23dda657e4dab2f53b5"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:647894f5ae99c4cf6bb82a1bb3a796f6e06af3caa3d32e26d2350d0e3e3faf24"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfd3223c15bebe26518d58ccf9a39b93948d3dcb3e57a20480dfdd315356baff"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d71264a80f3fcf512eb4f18f59423fe82d6e346ee97b90625f283df56aee103f"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e73091191e4280403bde6c9a52a6999d69cdfde498f1fdf629105247599b57ec"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3935bfa5fede35fb202c4b569bb9c042f337ca4ff7bd540a0aa5e37131659348"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f508b0491767bb1f2b87fdfacaba5f7eddc2f867740ec69ece6d1946d29029a6"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1672137af7c46662a1c2be1e8dc78cb6d224319aaa40271c9257d886be4363a6"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b74c261802d3d2b85c9df2dfb2fa81b6f90deeef63c2db9f0e029a3cac50b518"}, + {file = "propcache-0.2.1-cp312-cp312-win32.whl", hash = "sha256:d09c333d36c1409d56a9d29b3a1b800a42c76a57a5a8907eacdbce3f18768246"}, + {file = "propcache-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:c214999039d4f2a5b2073ac506bba279945233da8c786e490d411dfc30f855c1"}, + {file = "propcache-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aca405706e0b0a44cc6bfd41fbe89919a6a56999157f6de7e182a990c36e37bc"}, + {file = "propcache-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:12d1083f001ace206fe34b6bdc2cb94be66d57a850866f0b908972f90996b3e9"}, + {file = "propcache-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d93f3307ad32a27bda2e88ec81134b823c240aa3abb55821a8da553eed8d9439"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba278acf14471d36316159c94a802933d10b6a1e117b8554fe0d0d9b75c9d536"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e6281aedfca15301c41f74d7005e6e3f4ca143584ba696ac69df4f02f40d629"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b750a8e5a1262434fb1517ddf64b5de58327f1adc3524a5e44c2ca43305eb0b"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf72af5e0fb40e9babf594308911436c8efde3cb5e75b6f206c34ad18be5c052"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2d0a12018b04f4cb820781ec0dffb5f7c7c1d2a5cd22bff7fb055a2cb19ebce"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e800776a79a5aabdb17dcc2346a7d66d0777e942e4cd251defeb084762ecd17d"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4160d9283bd382fa6c0c2b5e017acc95bc183570cd70968b9202ad6d8fc48dce"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b43e74f1359353341a7adb783c8f1b1c676367b011709f466f42fda2045e95"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:58791550b27d5488b1bb52bc96328456095d96206a250d28d874fafe11b3dfaf"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0f022d381747f0dfe27e99d928e31bc51a18b65bb9e481ae0af1380a6725dd1f"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:297878dc9d0a334358f9b608b56d02e72899f3b8499fc6044133f0d319e2ec30"}, + {file = "propcache-0.2.1-cp313-cp313-win32.whl", hash = "sha256:ddfab44e4489bd79bda09d84c430677fc7f0a4939a73d2bba3073036f487a0a6"}, + {file = "propcache-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:556fc6c10989f19a179e4321e5d678db8eb2924131e64652a51fe83e4c3db0e1"}, + {file = "propcache-0.2.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6a9a8c34fb7bb609419a211e59da8887eeca40d300b5ea8e56af98f6fbbb1541"}, + {file = "propcache-0.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ae1aa1cd222c6d205853b3013c69cd04515f9d6ab6de4b0603e2e1c33221303e"}, + {file = "propcache-0.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:accb6150ce61c9c4b7738d45550806aa2b71c7668c6942f17b0ac182b6142fd4"}, + {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5eee736daafa7af6d0a2dc15cc75e05c64f37fc37bafef2e00d77c14171c2097"}, + {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7a31fc1e1bd362874863fdeed71aed92d348f5336fd84f2197ba40c59f061bd"}, + {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba4cfa1052819d16699e1d55d18c92b6e094d4517c41dd231a8b9f87b6fa681"}, + {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f089118d584e859c62b3da0892b88a83d611c2033ac410e929cb6754eec0ed16"}, + {file = "propcache-0.2.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:781e65134efaf88feb447e8c97a51772aa75e48b794352f94cb7ea717dedda0d"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31f5af773530fd3c658b32b6bdc2d0838543de70eb9a2156c03e410f7b0d3aae"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:a7a078f5d37bee6690959c813977da5291b24286e7b962e62a94cec31aa5188b"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cea7daf9fc7ae6687cf1e2c049752f19f146fdc37c2cc376e7d0032cf4f25347"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:8b3489ff1ed1e8315674d0775dc7d2195fb13ca17b3808721b54dbe9fd020faf"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9403db39be1393618dd80c746cb22ccda168efce239c73af13c3763ef56ffc04"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5d97151bc92d2b2578ff7ce779cdb9174337390a535953cbb9452fb65164c587"}, + {file = "propcache-0.2.1-cp39-cp39-win32.whl", hash = "sha256:9caac6b54914bdf41bcc91e7eb9147d331d29235a7c967c150ef5df6464fd1bb"}, + {file = "propcache-0.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:92fc4500fcb33899b05ba73276dfb684a20d31caa567b7cb5252d48f896a91b1"}, + {file = "propcache-0.2.1-py3-none-any.whl", hash = "sha256:52277518d6aae65536e9cea52d4e7fd2f7a66f4aa2d30ed3f2fcea620ace3c54"}, + {file = "propcache-0.2.1.tar.gz", hash = "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64"}, +] + +[[package]] +name = "psutil" +version = "6.1.0" +description = "Cross-platform lib for process and system monitoring in Python." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "psutil-6.1.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ff34df86226c0227c52f38b919213157588a678d049688eded74c76c8ba4a5d0"}, + {file = "psutil-6.1.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:c0e0c00aa18ca2d3b2b991643b799a15fc8f0563d2ebb6040f64ce8dc027b942"}, + {file = "psutil-6.1.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:000d1d1ebd634b4efb383f4034437384e44a6d455260aaee2eca1e9c1b55f047"}, + {file = "psutil-6.1.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:5cd2bcdc75b452ba2e10f0e8ecc0b57b827dd5d7aaffbc6821b2a9a242823a76"}, + {file = "psutil-6.1.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:045f00a43c737f960d273a83973b2511430d61f283a44c96bf13a6e829ba8fdc"}, + {file = "psutil-6.1.0-cp27-none-win32.whl", hash = "sha256:9118f27452b70bb1d9ab3198c1f626c2499384935aaf55388211ad982611407e"}, + {file = "psutil-6.1.0-cp27-none-win_amd64.whl", hash = "sha256:a8506f6119cff7015678e2bce904a4da21025cc70ad283a53b099e7620061d85"}, + {file = "psutil-6.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6e2dcd475ce8b80522e51d923d10c7871e45f20918e027ab682f94f1c6351688"}, + {file = "psutil-6.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0895b8414afafc526712c498bd9de2b063deaac4021a3b3c34566283464aff8e"}, + {file = "psutil-6.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dcbfce5d89f1d1f2546a2090f4fcf87c7f669d1d90aacb7d7582addece9fb38"}, + {file = "psutil-6.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:498c6979f9c6637ebc3a73b3f87f9eb1ec24e1ce53a7c5173b8508981614a90b"}, + {file = "psutil-6.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d905186d647b16755a800e7263d43df08b790d709d575105d419f8b6ef65423a"}, + {file = "psutil-6.1.0-cp36-cp36m-win32.whl", hash = "sha256:6d3fbbc8d23fcdcb500d2c9f94e07b1342df8ed71b948a2649b5cb060a7c94ca"}, + {file = "psutil-6.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1209036fbd0421afde505a4879dee3b2fd7b1e14fee81c0069807adcbbcca747"}, + {file = "psutil-6.1.0-cp37-abi3-win32.whl", hash = "sha256:1ad45a1f5d0b608253b11508f80940985d1d0c8f6111b5cb637533a0e6ddc13e"}, + {file = "psutil-6.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:a8fb3752b491d246034fa4d279ff076501588ce8cbcdbb62c32fd7a377d996be"}, + {file = "psutil-6.1.0.tar.gz", hash = "sha256:353815f59a7f64cdaca1c0307ee13558a0512f6db064e92fe833784f08539c7a"}, +] + +[package.extras] +dev = ["black", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest-cov", "requests", "rstcheck", "ruff", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "wheel"] +test = ["pytest", "pytest-xdist", "setuptools"] + +[[package]] +name = "pyasn1" +version = "0.6.1" +description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"}, + {file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"}, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.1" +description = "A collection of ASN.1-based protocols modules" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd"}, + {file = "pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c"}, +] + +[package.dependencies] +pyasn1 = ">=0.4.6,<0.7.0" + +[[package]] +name = "pydantic" +version = "2.10.3" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.10.3-py3-none-any.whl", hash = "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d"}, + {file = "pydantic-2.10.3.tar.gz", hash = "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.27.1" +typing-extensions = ">=4.12.2" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] + +[[package]] +name = "pydantic-core" +version = "2.27.1" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a"}, + {file = "pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6"}, + {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807"}, + {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c"}, + {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206"}, + {file = "pydantic_core-2.27.1-cp310-none-win32.whl", hash = "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c"}, + {file = "pydantic_core-2.27.1-cp310-none-win_amd64.whl", hash = "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17"}, + {file = "pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8"}, + {file = "pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e"}, + {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919"}, + {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c"}, + {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc"}, + {file = "pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9"}, + {file = "pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5"}, + {file = "pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89"}, + {file = "pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f"}, + {file = "pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089"}, + {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381"}, + {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb"}, + {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae"}, + {file = "pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c"}, + {file = "pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16"}, + {file = "pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e"}, + {file = "pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073"}, + {file = "pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a"}, + {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc"}, + {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960"}, + {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23"}, + {file = "pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05"}, + {file = "pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337"}, + {file = "pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5"}, + {file = "pydantic_core-2.27.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:5897bec80a09b4084aee23f9b73a9477a46c3304ad1d2d07acca19723fb1de62"}, + {file = "pydantic_core-2.27.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d0165ab2914379bd56908c02294ed8405c252250668ebcb438a55494c69f44ab"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b9af86e1d8e4cfc82c2022bfaa6f459381a50b94a29e95dcdda8442d6d83864"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f6c8a66741c5f5447e047ab0ba7a1c61d1e95580d64bce852e3df1f895c4067"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a42d6a8156ff78981f8aa56eb6394114e0dedb217cf8b729f438f643608cbcd"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64c65f40b4cd8b0e049a8edde07e38b476da7e3aaebe63287c899d2cff253fa5"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdcf339322a3fae5cbd504edcefddd5a50d9ee00d968696846f089b4432cf78"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bf99c8404f008750c846cb4ac4667b798a9f7de673ff719d705d9b2d6de49c5f"}, + {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8f1edcea27918d748c7e5e4d917297b2a0ab80cad10f86631e488b7cddf76a36"}, + {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:159cac0a3d096f79ab6a44d77a961917219707e2a130739c64d4dd46281f5c2a"}, + {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:029d9757eb621cc6e1848fa0b0310310de7301057f623985698ed7ebb014391b"}, + {file = "pydantic_core-2.27.1-cp38-none-win32.whl", hash = "sha256:a28af0695a45f7060e6f9b7092558a928a28553366519f64083c63a44f70e618"}, + {file = "pydantic_core-2.27.1-cp38-none-win_amd64.whl", hash = "sha256:2d4567c850905d5eaaed2f7a404e61012a51caf288292e016360aa2b96ff38d4"}, + {file = "pydantic_core-2.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e9386266798d64eeb19dd3677051f5705bf873e98e15897ddb7d76f477131967"}, + {file = "pydantic_core-2.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4228b5b646caa73f119b1ae756216b59cc6e2267201c27d3912b592c5e323b60"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3dfe500de26c52abe0477dde16192ac39c98f05bf2d80e76102d394bd13854"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aee66be87825cdf72ac64cb03ad4c15ffef4143dbf5c113f64a5ff4f81477bf9"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b748c44bb9f53031c8cbc99a8a061bc181c1000c60a30f55393b6e9c45cc5bd"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ca038c7f6a0afd0b2448941b6ef9d5e1949e999f9e5517692eb6da58e9d44be"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e0bd57539da59a3e4671b90a502da9a28c72322a4f17866ba3ac63a82c4498e"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac6c2c45c847bbf8f91930d88716a0fb924b51e0c6dad329b793d670ec5db792"}, + {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b94d4ba43739bbe8b0ce4262bcc3b7b9f31459ad120fb595627eaeb7f9b9ca01"}, + {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:00e6424f4b26fe82d44577b4c842d7df97c20be6439e8e685d0d715feceb9fb9"}, + {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:38de0a70160dd97540335b7ad3a74571b24f1dc3ed33f815f0880682e6880131"}, + {file = "pydantic_core-2.27.1-cp39-none-win32.whl", hash = "sha256:7ccebf51efc61634f6c2344da73e366c75e735960b5654b63d7e6f69a5885fa3"}, + {file = "pydantic_core-2.27.1-cp39-none-win_amd64.whl", hash = "sha256:a57847b090d7892f123726202b7daa20df6694cbd583b67a592e856bff603d6c"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5fde892e6c697ce3e30c61b239330fc5d569a71fefd4eb6512fc6caec9dd9e2f"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:816f5aa087094099fff7edabb5e01cc370eb21aa1a1d44fe2d2aefdfb5599b31"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c10c309e18e443ddb108f0ef64e8729363adbfd92d6d57beec680f6261556f3"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98476c98b02c8e9b2eec76ac4156fd006628b1b2d0ef27e548ffa978393fd154"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3027001c28434e7ca5a6e1e527487051136aa81803ac812be51802150d880dd"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:7699b1df36a48169cdebda7ab5a2bac265204003f153b4bd17276153d997670a"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1c39b07d90be6b48968ddc8c19e7585052088fd7ec8d568bb31ff64c70ae3c97"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:46ccfe3032b3915586e469d4972973f893c0a2bb65669194a5bdea9bacc088c2"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:62ba45e21cf6571d7f716d903b5b7b6d2617e2d5d67c0923dc47b9d41369f840"}, + {file = "pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pygments" +version = "2.18.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pytest" +version = "8.3.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, + {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.23.8" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"}, + {file = "pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"}, +] + +[package.dependencies] +pytest = ">=7.0.0,<9" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + +[[package]] +name = "pytest-repeat" +version = "0.9.3" +description = "pytest plugin for repeating tests" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest_repeat-0.9.3-py3-none-any.whl", hash = "sha256:26ab2df18226af9d5ce441c858f273121e92ff55f5bb311d25755b8d7abdd8ed"}, + {file = "pytest_repeat-0.9.3.tar.gz", hash = "sha256:ffd3836dfcd67bb270bec648b330e20be37d2966448c4148c4092d1e8aba8185"}, +] + +[package.dependencies] +pytest = "*" + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-dotenv" +version = "1.0.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, + {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "referencing" +version = "0.35.1" +description = "JSON Referencing + Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "referencing-0.35.1-py3-none-any.whl", hash = "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de"}, + {file = "referencing-0.35.1.tar.gz", hash = "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +rpds-py = ">=0.7.0" + +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +description = "OAuthlib authentication support for Requests." +optional = false +python-versions = ">=3.4" +files = [ + {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"}, + {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"}, +] + +[package.dependencies] +oauthlib = ">=3.0.0" +requests = ">=2.0.0" + +[package.extras] +rsa = ["oauthlib[signedtoken] (>=3.0.0)"] + +[[package]] +name = "rich" +version = "13.9.4" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, + {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "rpds-py" +version = "0.22.3" +description = "Python bindings to Rust's persistent data structures (rpds)" +optional = false +python-versions = ">=3.9" +files = [ + {file = "rpds_py-0.22.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:6c7b99ca52c2c1752b544e310101b98a659b720b21db00e65edca34483259967"}, + {file = "rpds_py-0.22.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:be2eb3f2495ba669d2a985f9b426c1797b7d48d6963899276d22f23e33d47e37"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70eb60b3ae9245ddea20f8a4190bd79c705a22f8028aaf8bbdebe4716c3fab24"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4041711832360a9b75cfb11b25a6a97c8fb49c07b8bd43d0d02b45d0b499a4ff"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64607d4cbf1b7e3c3c8a14948b99345eda0e161b852e122c6bb71aab6d1d798c"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e69b0a0e2537f26d73b4e43ad7bc8c8efb39621639b4434b76a3de50c6966e"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc27863442d388870c1809a87507727b799c8460573cfbb6dc0eeaef5a11b5ec"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e79dd39f1e8c3504be0607e5fc6e86bb60fe3584bec8b782578c3b0fde8d932c"}, + {file = "rpds_py-0.22.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e0fa2d4ec53dc51cf7d3bb22e0aa0143966119f42a0c3e4998293a3dd2856b09"}, + {file = "rpds_py-0.22.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fda7cb070f442bf80b642cd56483b5548e43d366fe3f39b98e67cce780cded00"}, + {file = "rpds_py-0.22.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cff63a0272fcd259dcc3be1657b07c929c466b067ceb1c20060e8d10af56f5bf"}, + {file = "rpds_py-0.22.3-cp310-cp310-win32.whl", hash = "sha256:9bd7228827ec7bb817089e2eb301d907c0d9827a9e558f22f762bb690b131652"}, + {file = "rpds_py-0.22.3-cp310-cp310-win_amd64.whl", hash = "sha256:9beeb01d8c190d7581a4d59522cd3d4b6887040dcfc744af99aa59fef3e041a8"}, + {file = "rpds_py-0.22.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d20cfb4e099748ea39e6f7b16c91ab057989712d31761d3300d43134e26e165f"}, + {file = "rpds_py-0.22.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:68049202f67380ff9aa52f12e92b1c30115f32e6895cd7198fa2a7961621fc5a"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb4f868f712b2dd4bcc538b0a0c1f63a2b1d584c925e69a224d759e7070a12d5"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bc51abd01f08117283c5ebf64844a35144a0843ff7b2983e0648e4d3d9f10dbb"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f3cec041684de9a4684b1572fe28c7267410e02450f4561700ca5a3bc6695a2"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7ef9d9da710be50ff6809fed8f1963fecdfecc8b86656cadfca3bc24289414b0"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59f4a79c19232a5774aee369a0c296712ad0e77f24e62cad53160312b1c1eaa1"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a60bce91f81ddaac922a40bbb571a12c1070cb20ebd6d49c48e0b101d87300d"}, + {file = "rpds_py-0.22.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e89391e6d60251560f0a8f4bd32137b077a80d9b7dbe6d5cab1cd80d2746f648"}, + {file = "rpds_py-0.22.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e3fb866d9932a3d7d0c82da76d816996d1667c44891bd861a0f97ba27e84fc74"}, + {file = "rpds_py-0.22.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1352ae4f7c717ae8cba93421a63373e582d19d55d2ee2cbb184344c82d2ae55a"}, + {file = "rpds_py-0.22.3-cp311-cp311-win32.whl", hash = "sha256:b0b4136a252cadfa1adb705bb81524eee47d9f6aab4f2ee4fa1e9d3cd4581f64"}, + {file = "rpds_py-0.22.3-cp311-cp311-win_amd64.whl", hash = "sha256:8bd7c8cfc0b8247c8799080fbff54e0b9619e17cdfeb0478ba7295d43f635d7c"}, + {file = "rpds_py-0.22.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:27e98004595899949bd7a7b34e91fa7c44d7a97c40fcaf1d874168bb652ec67e"}, + {file = "rpds_py-0.22.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1978d0021e943aae58b9b0b196fb4895a25cc53d3956b8e35e0b7682eefb6d56"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:655ca44a831ecb238d124e0402d98f6212ac527a0ba6c55ca26f616604e60a45"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:feea821ee2a9273771bae61194004ee2fc33f8ec7db08117ef9147d4bbcbca8e"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22bebe05a9ffc70ebfa127efbc429bc26ec9e9b4ee4d15a740033efda515cf3d"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3af6e48651c4e0d2d166dc1b033b7042ea3f871504b6805ba5f4fe31581d8d38"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67ba3c290821343c192f7eae1d8fd5999ca2dc99994114643e2f2d3e6138b15"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:02fbb9c288ae08bcb34fb41d516d5eeb0455ac35b5512d03181d755d80810059"}, + {file = "rpds_py-0.22.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f56a6b404f74ab372da986d240e2e002769a7d7102cc73eb238a4f72eec5284e"}, + {file = "rpds_py-0.22.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0a0461200769ab3b9ab7e513f6013b7a97fdeee41c29b9db343f3c5a8e2b9e61"}, + {file = "rpds_py-0.22.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8633e471c6207a039eff6aa116e35f69f3156b3989ea3e2d755f7bc41754a4a7"}, + {file = "rpds_py-0.22.3-cp312-cp312-win32.whl", hash = "sha256:593eba61ba0c3baae5bc9be2f5232430453fb4432048de28399ca7376de9c627"}, + {file = "rpds_py-0.22.3-cp312-cp312-win_amd64.whl", hash = "sha256:d115bffdd417c6d806ea9069237a4ae02f513b778e3789a359bc5856e0404cc4"}, + {file = "rpds_py-0.22.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ea7433ce7e4bfc3a85654aeb6747babe3f66eaf9a1d0c1e7a4435bbdf27fea84"}, + {file = "rpds_py-0.22.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6dd9412824c4ce1aca56c47b0991e65bebb7ac3f4edccfd3f156150c96a7bf25"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20070c65396f7373f5df4005862fa162db5d25d56150bddd0b3e8214e8ef45b4"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0b09865a9abc0ddff4e50b5ef65467cd94176bf1e0004184eb915cbc10fc05c5"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3453e8d41fe5f17d1f8e9c383a7473cd46a63661628ec58e07777c2fff7196dc"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5d36399a1b96e1a5fdc91e0522544580dbebeb1f77f27b2b0ab25559e103b8b"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:009de23c9c9ee54bf11303a966edf4d9087cd43a6003672e6aa7def643d06518"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1aef18820ef3e4587ebe8b3bc9ba6e55892a6d7b93bac6d29d9f631a3b4befbd"}, + {file = "rpds_py-0.22.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f60bd8423be1d9d833f230fdbccf8f57af322d96bcad6599e5a771b151398eb2"}, + {file = "rpds_py-0.22.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:62d9cfcf4948683a18a9aff0ab7e1474d407b7bab2ca03116109f8464698ab16"}, + {file = "rpds_py-0.22.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9253fc214112405f0afa7db88739294295f0e08466987f1d70e29930262b4c8f"}, + {file = "rpds_py-0.22.3-cp313-cp313-win32.whl", hash = "sha256:fb0ba113b4983beac1a2eb16faffd76cb41e176bf58c4afe3e14b9c681f702de"}, + {file = "rpds_py-0.22.3-cp313-cp313-win_amd64.whl", hash = "sha256:c58e2339def52ef6b71b8f36d13c3688ea23fa093353f3a4fee2556e62086ec9"}, + {file = "rpds_py-0.22.3-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f82a116a1d03628a8ace4859556fb39fd1424c933341a08ea3ed6de1edb0283b"}, + {file = "rpds_py-0.22.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3dfcbc95bd7992b16f3f7ba05af8a64ca694331bd24f9157b49dadeeb287493b"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59259dc58e57b10e7e18ce02c311804c10c5a793e6568f8af4dead03264584d1"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5725dd9cc02068996d4438d397e255dcb1df776b7ceea3b9cb972bdb11260a83"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99b37292234e61325e7a5bb9689e55e48c3f5f603af88b1642666277a81f1fbd"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:27b1d3b3915a99208fee9ab092b8184c420f2905b7d7feb4aeb5e4a9c509b8a1"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f612463ac081803f243ff13cccc648578e2279295048f2a8d5eb430af2bae6e3"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f73d3fef726b3243a811121de45193c0ca75f6407fe66f3f4e183c983573e130"}, + {file = "rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3f21f0495edea7fdbaaa87e633a8689cd285f8f4af5c869f27bc8074638ad69c"}, + {file = "rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1e9663daaf7a63ceccbbb8e3808fe90415b0757e2abddbfc2e06c857bf8c5e2b"}, + {file = "rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a76e42402542b1fae59798fab64432b2d015ab9d0c8c47ba7addddbaf7952333"}, + {file = "rpds_py-0.22.3-cp313-cp313t-win32.whl", hash = "sha256:69803198097467ee7282750acb507fba35ca22cc3b85f16cf45fb01cb9097730"}, + {file = "rpds_py-0.22.3-cp313-cp313t-win_amd64.whl", hash = "sha256:f5cf2a0c2bdadf3791b5c205d55a37a54025c6e18a71c71f82bb536cf9a454bf"}, + {file = "rpds_py-0.22.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:378753b4a4de2a7b34063d6f95ae81bfa7b15f2c1a04a9518e8644e81807ebea"}, + {file = "rpds_py-0.22.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3445e07bf2e8ecfeef6ef67ac83de670358abf2996916039b16a218e3d95e97e"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b2513ba235829860b13faa931f3b6846548021846ac808455301c23a101689d"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eaf16ae9ae519a0e237a0f528fd9f0197b9bb70f40263ee57ae53c2b8d48aeb3"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:583f6a1993ca3369e0f80ba99d796d8e6b1a3a2a442dd4e1a79e652116413091"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4617e1915a539a0d9a9567795023de41a87106522ff83fbfaf1f6baf8e85437e"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c150c7a61ed4a4f4955a96626574e9baf1adf772c2fb61ef6a5027e52803543"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fa4331c200c2521512595253f5bb70858b90f750d39b8cbfd67465f8d1b596d"}, + {file = "rpds_py-0.22.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:214b7a953d73b5e87f0ebece4a32a5bd83c60a3ecc9d4ec8f1dca968a2d91e99"}, + {file = "rpds_py-0.22.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f47ad3d5f3258bd7058d2d506852217865afefe6153a36eb4b6928758041d831"}, + {file = "rpds_py-0.22.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f276b245347e6e36526cbd4a266a417796fc531ddf391e43574cf6466c492520"}, + {file = "rpds_py-0.22.3-cp39-cp39-win32.whl", hash = "sha256:bbb232860e3d03d544bc03ac57855cd82ddf19c7a07651a7c0fdb95e9efea8b9"}, + {file = "rpds_py-0.22.3-cp39-cp39-win_amd64.whl", hash = "sha256:cfbc454a2880389dbb9b5b398e50d439e2e58669160f27b60e5eca11f68ae17c"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d48424e39c2611ee1b84ad0f44fb3b2b53d473e65de061e3f460fc0be5f1939d"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:24e8abb5878e250f2eb0d7859a8e561846f98910326d06c0d51381fed59357bd"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b232061ca880db21fa14defe219840ad9b74b6158adb52ddf0e87bead9e8493"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac0a03221cdb5058ce0167ecc92a8c89e8d0decdc9e99a2ec23380793c4dcb96"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb0c341fa71df5a4595f9501df4ac5abfb5a09580081dffbd1ddd4654e6e9123"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf9db5488121b596dbfc6718c76092fda77b703c1f7533a226a5a9f65248f8ad"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b8db6b5b2d4491ad5b6bdc2bc7c017eec108acbf4e6785f42a9eb0ba234f4c9"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b3d504047aba448d70cf6fa22e06cb09f7cbd761939fdd47604f5e007675c24e"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:e61b02c3f7a1e0b75e20c3978f7135fd13cb6cf551bf4a6d29b999a88830a338"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:e35ba67d65d49080e8e5a1dd40101fccdd9798adb9b050ff670b7d74fa41c566"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:26fd7cac7dd51011a245f29a2cc6489c4608b5a8ce8d75661bb4a1066c52dfbe"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:177c7c0fce2855833819c98e43c262007f42ce86651ffbb84f37883308cb0e7d"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bb47271f60660803ad11f4c61b42242b8c1312a31c98c578f79ef9387bbde21c"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:70fb28128acbfd264eda9bf47015537ba3fe86e40d046eb2963d75024be4d055"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44d61b4b7d0c2c9ac019c314e52d7cbda0ae31078aabd0f22e583af3e0d79723"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f0e260eaf54380380ac3808aa4ebe2d8ca28b9087cf411649f96bad6900c728"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b25bc607423935079e05619d7de556c91fb6adeae9d5f80868dde3468657994b"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fb6116dfb8d1925cbdb52595560584db42a7f664617a1f7d7f6e32f138cdf37d"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a63cbdd98acef6570c62b92a1e43266f9e8b21e699c363c0fef13bd530799c11"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2b8f60e1b739a74bab7e01fcbe3dddd4657ec685caa04681df9d562ef15b625f"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2e8b55d8517a2fda8d95cb45d62a5a8bbf9dd0ad39c5b25c8833efea07b880ca"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:2de29005e11637e7a2361fa151f780ff8eb2543a0da1413bb951e9f14b699ef3"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:666ecce376999bf619756a24ce15bb14c5bfaf04bf00abc7e663ce17c3f34fe7"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:5246b14ca64a8675e0a7161f7af68fe3e910e6b90542b4bfb5439ba752191df6"}, + {file = "rpds_py-0.22.3.tar.gz", hash = "sha256:e32fee8ab45d3c2db6da19a5323bc3362237c8b653c70194414b892fd06a080d"}, +] + +[[package]] +name = "rsa" +version = "4.9" +description = "Pure-Python RSA implementation" +optional = false +python-versions = ">=3.6,<4" +files = [ + {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, + {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, +] + +[package.dependencies] +pyasn1 = ">=0.1.3" + +[[package]] +name = "ruff" +version = "0.6.9" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.6.9-py3-none-linux_armv6l.whl", hash = "sha256:064df58d84ccc0ac0fcd63bc3090b251d90e2a372558c0f057c3f75ed73e1ccd"}, + {file = "ruff-0.6.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:140d4b5c9f5fc7a7b074908a78ab8d384dd7f6510402267bc76c37195c02a7ec"}, + {file = "ruff-0.6.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53fd8ca5e82bdee8da7f506d7b03a261f24cd43d090ea9db9a1dc59d9313914c"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645d7d8761f915e48a00d4ecc3686969761df69fb561dd914a773c1a8266e14e"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eae02b700763e3847595b9d2891488989cac00214da7f845f4bcf2989007d577"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d5ccc9e58112441de8ad4b29dcb7a86dc25c5f770e3c06a9d57e0e5eba48829"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:417b81aa1c9b60b2f8edc463c58363075412866ae4e2b9ab0f690dc1e87ac1b5"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c866b631f5fbce896a74a6e4383407ba7507b815ccc52bcedabb6810fdb3ef7"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b118afbb3202f5911486ad52da86d1d52305b59e7ef2031cea3425142b97d6f"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a67267654edc23c97335586774790cde402fb6bbdb3c2314f1fc087dee320bfa"}, + {file = "ruff-0.6.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3ef0cc774b00fec123f635ce5c547dac263f6ee9fb9cc83437c5904183b55ceb"}, + {file = "ruff-0.6.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:12edd2af0c60fa61ff31cefb90aef4288ac4d372b4962c2864aeea3a1a2460c0"}, + {file = "ruff-0.6.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:55bb01caeaf3a60b2b2bba07308a02fca6ab56233302406ed5245180a05c5625"}, + {file = "ruff-0.6.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:925d26471fa24b0ce5a6cdfab1bb526fb4159952385f386bdcc643813d472039"}, + {file = "ruff-0.6.9-py3-none-win32.whl", hash = "sha256:eb61ec9bdb2506cffd492e05ac40e5bc6284873aceb605503d8494180d6fc84d"}, + {file = "ruff-0.6.9-py3-none-win_amd64.whl", hash = "sha256:785d31851c1ae91f45b3d8fe23b8ae4b5170089021fbb42402d811135f0b7117"}, + {file = "ruff-0.6.9-py3-none-win_arm64.whl", hash = "sha256:a9641e31476d601f83cd602608739a0840e348bda93fec9f1ee816f8b6798b93"}, + {file = "ruff-0.6.9.tar.gz", hash = "sha256:b076ef717a8e5bc819514ee1d602bbdca5b4420ae13a9cf61a0c0a4f53a2baa2"}, +] + +[[package]] +name = "s3fs" +version = "2024.10.0" +description = "Convenient Filesystem interface over S3" +optional = false +python-versions = ">=3.8" +files = [ + {file = "s3fs-2024.10.0-py3-none-any.whl", hash = "sha256:7a2025d60d5b1a6025726b3a5e292a8e5aa713abc3b16fd1f81735181f7bb282"}, + {file = "s3fs-2024.10.0.tar.gz", hash = "sha256:58b8c3650f8b99dbedf361543da3533aac8707035a104db5d80b094617ad4a3f"}, +] + +[package.dependencies] +aiobotocore = ">=2.5.4,<3.0.0" +aiohttp = "<4.0.0a0 || >4.0.0a0,<4.0.0a1 || >4.0.0a1" +fsspec = "==2024.10.0.*" + +[package.extras] +awscli = ["aiobotocore[awscli] (>=2.5.4,<3.0.0)"] +boto3 = ["aiobotocore[boto3] (>=2.5.4,<3.0.0)"] + +[[package]] +name = "semver" +version = "3.0.2" +description = "Python helper for Semantic Versioning (https://semver.org)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "semver-3.0.2-py3-none-any.whl", hash = "sha256:b1ea4686fe70b981f85359eda33199d60c53964284e0cfb4977d243e37cf4bf4"}, + {file = "semver-3.0.2.tar.gz", hash = "sha256:6253adb39c70f6e51afed2fa7152bcd414c411286088fb4b9effb133885ab4cc"}, +] + +[[package]] +name = "shortuuid" +version = "1.0.13" +description = "A generator library for concise, unambiguous and URL-safe UUIDs." +optional = false +python-versions = ">=3.6" +files = [ + {file = "shortuuid-1.0.13-py3-none-any.whl", hash = "sha256:a482a497300b49b4953e15108a7913244e1bb0d41f9d332f5e9925dba33a3c5a"}, + {file = "shortuuid-1.0.13.tar.gz", hash = "sha256:3bb9cf07f606260584b1df46399c0b87dd84773e7b25912b7e391e30797c5e72"}, +] + +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "soupsieve" +version = "2.6" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.8" +files = [ + {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, + {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, +] + +[[package]] +name = "tenacity" +version = "9.0.0" +description = "Retry code until it succeeds" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tenacity-9.0.0-py3-none-any.whl", hash = "sha256:93de0c98785b27fcf659856aa9f54bfbd399e29969b0621bc7f762bd441b4539"}, + {file = "tenacity-9.0.0.tar.gz", hash = "sha256:807f37ca97d62aa361264d497b0e31e92b8027044942bfa756160d908320d73b"}, +] + +[package.extras] +doc = ["reno", "sphinx"] +test = ["pytest", "tornado (>=4.5)", "typeguard"] + +[[package]] +name = "textual" +version = "1.0.0" +description = "Modern Text User Interface framework" +optional = false +python-versions = "<4.0.0,>=3.8.1" +files = [ + {file = "textual-1.0.0-py3-none-any.whl", hash = "sha256:2d4a701781c05104925e463ae370c630567c70c2880e92ab838052e3e23c986f"}, + {file = "textual-1.0.0.tar.gz", hash = "sha256:bec9fe63547c1c552569d1b75d309038b7d456c03f86dfa3706ddb099b151399"}, +] + +[package.dependencies] +markdown-it-py = {version = ">=2.1.0", extras = ["linkify", "plugins"]} +platformdirs = ">=3.6.0,<5" +rich = ">=13.3.3" +typing-extensions = ">=4.4.0,<5.0.0" + +[package.extras] +syntax = ["tree-sitter (>=0.23.0)", "tree-sitter-bash (>=0.23.0)", "tree-sitter-css (>=0.23.0)", "tree-sitter-go (>=0.23.0)", "tree-sitter-html (>=0.23.0)", "tree-sitter-java (>=0.23.0)", "tree-sitter-javascript (>=0.23.0)", "tree-sitter-json (>=0.24.0)", "tree-sitter-markdown (>=0.3.0)", "tree-sitter-python (>=0.23.0)", "tree-sitter-regex (>=0.24.0)", "tree-sitter-rust (>=0.23.0)", "tree-sitter-sql (>=0.3.0)", "tree-sitter-toml (>=0.6.0)", "tree-sitter-xml (>=0.7.0)", "tree-sitter-yaml (>=0.6.0)"] + +[[package]] +name = "tomli" +version = "2.2.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, +] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.20240917" +description = "Typing stubs for PyYAML" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-PyYAML-6.0.12.20240917.tar.gz", hash = "sha256:d1405a86f9576682234ef83bcb4e6fff7c9305c8b1fbad5e0bcd4f7dbdc9c587"}, + {file = "types_PyYAML-6.0.12.20240917-py3-none-any.whl", hash = "sha256:392b267f1c0fe6022952462bf5d6523f31e37f6cea49b14cee7ad634b6301570"}, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "uc-micro-py" +version = "1.0.3" +description = "Micro subset of unicode data files for linkify-it-py projects." +optional = false +python-versions = ">=3.7" +files = [ + {file = "uc-micro-py-1.0.3.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a"}, + {file = "uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5"}, +] + +[package.extras] +test = ["coverage", "pytest", "pytest-cov"] + +[[package]] +name = "urllib3" +version = "2.2.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, + {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "virtualenv" +version = "20.28.0" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.8" +files = [ + {file = "virtualenv-20.28.0-py3-none-any.whl", hash = "sha256:23eae1b4516ecd610481eda647f3a7c09aea295055337331bb4e6892ecce47b0"}, + {file = "virtualenv-20.28.0.tar.gz", hash = "sha256:2c9c3262bb8e7b87ea801d715fae4495e6032450c71d2309be9550e7364049aa"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + +[[package]] +name = "websocket-client" +version = "1.8.0" +description = "WebSocket client for Python with low level API options" +optional = false +python-versions = ">=3.8" +files = [ + {file = "websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526"}, + {file = "websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da"}, +] + +[package.extras] +docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx-rtd-theme (>=1.1.0)"] +optional = ["python-socks", "wsaccel"] +test = ["websockets"] + +[[package]] +name = "wrapt" +version = "1.17.0" +description = "Module for decorators, wrappers and monkey patching." +optional = false +python-versions = ">=3.8" +files = [ + {file = "wrapt-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a0c23b8319848426f305f9cb0c98a6e32ee68a36264f45948ccf8e7d2b941f8"}, + {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ca5f060e205f72bec57faae5bd817a1560fcfc4af03f414b08fa29106b7e2d"}, + {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e185ec6060e301a7e5f8461c86fb3640a7beb1a0f0208ffde7a65ec4074931df"}, + {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb90765dd91aed05b53cd7a87bd7f5c188fcd95960914bae0d32c5e7f899719d"}, + {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:879591c2b5ab0a7184258274c42a126b74a2c3d5a329df16d69f9cee07bba6ea"}, + {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fce6fee67c318fdfb7f285c29a82d84782ae2579c0e1b385b7f36c6e8074fffb"}, + {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0698d3a86f68abc894d537887b9bbf84d29bcfbc759e23f4644be27acf6da301"}, + {file = "wrapt-1.17.0-cp310-cp310-win32.whl", hash = "sha256:69d093792dc34a9c4c8a70e4973a3361c7a7578e9cd86961b2bbf38ca71e4e22"}, + {file = "wrapt-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:f28b29dc158ca5d6ac396c8e0a2ef45c4e97bb7e65522bfc04c989e6fe814575"}, + {file = "wrapt-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74bf625b1b4caaa7bad51d9003f8b07a468a704e0644a700e936c357c17dd45a"}, + {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f2a28eb35cf99d5f5bd12f5dd44a0f41d206db226535b37b0c60e9da162c3ed"}, + {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:81b1289e99cf4bad07c23393ab447e5e96db0ab50974a280f7954b071d41b489"}, + {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2939cd4a2a52ca32bc0b359015718472d7f6de870760342e7ba295be9ebaf9"}, + {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a9653131bda68a1f029c52157fd81e11f07d485df55410401f745007bd6d339"}, + {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4e4b4385363de9052dac1a67bfb535c376f3d19c238b5f36bddc95efae15e12d"}, + {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bdf62d25234290db1837875d4dceb2151e4ea7f9fff2ed41c0fde23ed542eb5b"}, + {file = "wrapt-1.17.0-cp311-cp311-win32.whl", hash = "sha256:5d8fd17635b262448ab8f99230fe4dac991af1dabdbb92f7a70a6afac8a7e346"}, + {file = "wrapt-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:92a3d214d5e53cb1db8b015f30d544bc9d3f7179a05feb8f16df713cecc2620a"}, + {file = "wrapt-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:89fc28495896097622c3fc238915c79365dd0ede02f9a82ce436b13bd0ab7569"}, + {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875d240fdbdbe9e11f9831901fb8719da0bd4e6131f83aa9f69b96d18fae7504"}, + {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ed16d95fd142e9c72b6c10b06514ad30e846a0d0917ab406186541fe68b451"}, + {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b956061b8db634120b58f668592a772e87e2e78bc1f6a906cfcaa0cc7991c1"}, + {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:daba396199399ccabafbfc509037ac635a6bc18510ad1add8fd16d4739cdd106"}, + {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4d63f4d446e10ad19ed01188d6c1e1bb134cde8c18b0aa2acfd973d41fcc5ada"}, + {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8a5e7cc39a45fc430af1aefc4d77ee6bad72c5bcdb1322cfde852c15192b8bd4"}, + {file = "wrapt-1.17.0-cp312-cp312-win32.whl", hash = "sha256:0a0a1a1ec28b641f2a3a2c35cbe86c00051c04fffcfcc577ffcdd707df3f8635"}, + {file = "wrapt-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:3c34f6896a01b84bab196f7119770fd8466c8ae3dfa73c59c0bb281e7b588ce7"}, + {file = "wrapt-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:714c12485aa52efbc0fc0ade1e9ab3a70343db82627f90f2ecbc898fdf0bb181"}, + {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da427d311782324a376cacb47c1a4adc43f99fd9d996ffc1b3e8529c4074d393"}, + {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba1739fb38441a27a676f4de4123d3e858e494fac05868b7a281c0a383c098f4"}, + {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e711fc1acc7468463bc084d1b68561e40d1eaa135d8c509a65dd534403d83d7b"}, + {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:140ea00c87fafc42739bd74a94a5a9003f8e72c27c47cd4f61d8e05e6dec8721"}, + {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:73a96fd11d2b2e77d623a7f26e004cc31f131a365add1ce1ce9a19e55a1eef90"}, + {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0b48554952f0f387984da81ccfa73b62e52817a4386d070c75e4db7d43a28c4a"}, + {file = "wrapt-1.17.0-cp313-cp313-win32.whl", hash = "sha256:498fec8da10e3e62edd1e7368f4b24aa362ac0ad931e678332d1b209aec93045"}, + {file = "wrapt-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd136bb85f4568fffca995bd3c8d52080b1e5b225dbf1c2b17b66b4c5fa02838"}, + {file = "wrapt-1.17.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:17fcf043d0b4724858f25b8826c36e08f9fb2e475410bece0ec44a22d533da9b"}, + {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4a557d97f12813dc5e18dad9fa765ae44ddd56a672bb5de4825527c847d6379"}, + {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0229b247b0fc7dee0d36176cbb79dbaf2a9eb7ecc50ec3121f40ef443155fb1d"}, + {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8425cfce27b8b20c9b89d77fb50e368d8306a90bf2b6eef2cdf5cd5083adf83f"}, + {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9c900108df470060174108012de06d45f514aa4ec21a191e7ab42988ff42a86c"}, + {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4e547b447073fc0dbfcbff15154c1be8823d10dab4ad401bdb1575e3fdedff1b"}, + {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:914f66f3b6fc7b915d46c1cc424bc2441841083de01b90f9e81109c9759e43ab"}, + {file = "wrapt-1.17.0-cp313-cp313t-win32.whl", hash = "sha256:a4192b45dff127c7d69b3bdfb4d3e47b64179a0b9900b6351859f3001397dabf"}, + {file = "wrapt-1.17.0-cp313-cp313t-win_amd64.whl", hash = "sha256:4f643df3d4419ea3f856c5c3f40fec1d65ea2e89ec812c83f7767c8730f9827a"}, + {file = "wrapt-1.17.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:69c40d4655e078ede067a7095544bcec5a963566e17503e75a3a3e0fe2803b13"}, + {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f495b6754358979379f84534f8dd7a43ff8cff2558dcdea4a148a6e713a758f"}, + {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:baa7ef4e0886a6f482e00d1d5bcd37c201b383f1d314643dfb0367169f94f04c"}, + {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fc931382e56627ec4acb01e09ce66e5c03c384ca52606111cee50d931a342d"}, + {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8f8909cdb9f1b237786c09a810e24ee5e15ef17019f7cecb207ce205b9b5fcce"}, + {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ad47b095f0bdc5585bced35bd088cbfe4177236c7df9984b3cc46b391cc60627"}, + {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:948a9bd0fb2c5120457b07e59c8d7210cbc8703243225dbd78f4dfc13c8d2d1f"}, + {file = "wrapt-1.17.0-cp38-cp38-win32.whl", hash = "sha256:5ae271862b2142f4bc687bdbfcc942e2473a89999a54231aa1c2c676e28f29ea"}, + {file = "wrapt-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:f335579a1b485c834849e9075191c9898e0731af45705c2ebf70e0cd5d58beed"}, + {file = "wrapt-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d751300b94e35b6016d4b1e7d0e7bbc3b5e1751e2405ef908316c2a9024008a1"}, + {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7264cbb4a18dc4acfd73b63e4bcfec9c9802614572025bdd44d0721983fc1d9c"}, + {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33539c6f5b96cf0b1105a0ff4cf5db9332e773bb521cc804a90e58dc49b10578"}, + {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c30970bdee1cad6a8da2044febd824ef6dc4cc0b19e39af3085c763fdec7de33"}, + {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bc7f729a72b16ee21795a943f85c6244971724819819a41ddbaeb691b2dd85ad"}, + {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:6ff02a91c4fc9b6a94e1c9c20f62ea06a7e375f42fe57587f004d1078ac86ca9"}, + {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2dfb7cff84e72e7bf975b06b4989477873dcf160b2fd89959c629535df53d4e0"}, + {file = "wrapt-1.17.0-cp39-cp39-win32.whl", hash = "sha256:2399408ac33ffd5b200480ee858baa58d77dd30e0dd0cab6a8a9547135f30a88"}, + {file = "wrapt-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:4f763a29ee6a20c529496a20a7bcb16a73de27f5da6a843249c7047daf135977"}, + {file = "wrapt-1.17.0-py3-none-any.whl", hash = "sha256:d2c63b93548eda58abf5188e505ffed0229bf675f7c3090f8e36ad55b8cbc371"}, + {file = "wrapt-1.17.0.tar.gz", hash = "sha256:16187aa2317c731170a88ef35e8937ae0f533c402872c1ee5e6d079fcf320801"}, +] + +[[package]] +name = "yarl" +version = "1.18.3" +description = "Yet another URL library" +optional = false +python-versions = ">=3.9" +files = [ + {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34"}, + {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7"}, + {file = "yarl-1.18.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:602d98f2c2d929f8e697ed274fbadc09902c4025c5a9963bf4e9edfc3ab6f7ed"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c654d5207c78e0bd6d749f6dae1dcbbfde3403ad3a4b11f3c5544d9906969dde"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5094d9206c64181d0f6e76ebd8fb2f8fe274950a63890ee9e0ebfd58bf9d787b"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35098b24e0327fc4ebdc8ffe336cee0a87a700c24ffed13161af80124b7dc8e5"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3236da9272872443f81fedc389bace88408f64f89f75d1bdb2256069a8730ccc"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2c08cc9b16f4f4bc522771d96734c7901e7ebef70c6c5c35dd0f10845270bcd"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80316a8bd5109320d38eef8833ccf5f89608c9107d02d2a7f985f98ed6876990"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1e1cc06da1491e6734f0ea1e6294ce00792193c463350626571c287c9a704db"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fea09ca13323376a2fdfb353a5fa2e59f90cd18d7ca4eaa1fd31f0a8b4f91e62"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e3b9fd71836999aad54084906f8663dffcd2a7fb5cdafd6c37713b2e72be1760"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:757e81cae69244257d125ff31663249b3013b5dc0a8520d73694aed497fb195b"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b1771de9944d875f1b98a745bc547e684b863abf8f8287da8466cf470ef52690"}, + {file = "yarl-1.18.3-cp310-cp310-win32.whl", hash = "sha256:8874027a53e3aea659a6d62751800cf6e63314c160fd607489ba5c2edd753cf6"}, + {file = "yarl-1.18.3-cp310-cp310-win_amd64.whl", hash = "sha256:93b2e109287f93db79210f86deb6b9bbb81ac32fc97236b16f7433db7fc437d8"}, + {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8503ad47387b8ebd39cbbbdf0bf113e17330ffd339ba1144074da24c545f0069"}, + {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02ddb6756f8f4517a2d5e99d8b2f272488e18dd0bfbc802f31c16c6c20f22193"}, + {file = "yarl-1.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:67a283dd2882ac98cc6318384f565bffc751ab564605959df4752d42483ad889"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d980e0325b6eddc81331d3f4551e2a333999fb176fd153e075c6d1c2530aa8a8"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b643562c12680b01e17239be267bc306bbc6aac1f34f6444d1bded0c5ce438ca"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c017a3b6df3a1bd45b9fa49a0f54005e53fbcad16633870104b66fa1a30a29d8"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccaa3a4b521b780a7e771cc336a2dba389a0861592bbce09a476190bb0c8b4b3"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d06d3005e668744e11ed80812e61efd77d70bb7f03e33c1598c301eea20efbb"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9d41beda9dc97ca9ab0b9888cb71f7539124bc05df02c0cff6e5acc5a19dcc6e"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ba23302c0c61a9999784e73809427c9dbedd79f66a13d84ad1b1943802eaaf59"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6748dbf9bfa5ba1afcc7556b71cda0d7ce5f24768043a02a58846e4a443d808d"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0b0cad37311123211dc91eadcb322ef4d4a66008d3e1bdc404808992260e1a0e"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fb2171a4486bb075316ee754c6d8382ea6eb8b399d4ec62fde2b591f879778a"}, + {file = "yarl-1.18.3-cp311-cp311-win32.whl", hash = "sha256:61b1a825a13bef4a5f10b1885245377d3cd0bf87cba068e1d9a88c2ae36880e1"}, + {file = "yarl-1.18.3-cp311-cp311-win_amd64.whl", hash = "sha256:b9d60031cf568c627d028239693fd718025719c02c9f55df0a53e587aab951b5"}, + {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50"}, + {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576"}, + {file = "yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285"}, + {file = "yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2"}, + {file = "yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477"}, + {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb"}, + {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa"}, + {file = "yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8"}, + {file = "yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d"}, + {file = "yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c"}, + {file = "yarl-1.18.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:61e5e68cb65ac8f547f6b5ef933f510134a6bf31bb178be428994b0cb46c2a04"}, + {file = "yarl-1.18.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe57328fbc1bfd0bd0514470ac692630f3901c0ee39052ae47acd1d90a436719"}, + {file = "yarl-1.18.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a440a2a624683108a1b454705ecd7afc1c3438a08e890a1513d468671d90a04e"}, + {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09c7907c8548bcd6ab860e5f513e727c53b4a714f459b084f6580b49fa1b9cee"}, + {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4f6450109834af88cb4cc5ecddfc5380ebb9c228695afc11915a0bf82116789"}, + {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9ca04806f3be0ac6d558fffc2fdf8fcef767e0489d2684a21912cc4ed0cd1b8"}, + {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77a6e85b90a7641d2e07184df5557132a337f136250caafc9ccaa4a2a998ca2c"}, + {file = "yarl-1.18.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6333c5a377c8e2f5fae35e7b8f145c617b02c939d04110c76f29ee3676b5f9a5"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0b3c92fa08759dbf12b3a59579a4096ba9af8dd344d9a813fc7f5070d86bbab1"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:4ac515b860c36becb81bb84b667466885096b5fc85596948548b667da3bf9f24"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:045b8482ce9483ada4f3f23b3774f4e1bf4f23a2d5c912ed5170f68efb053318"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:a4bb030cf46a434ec0225bddbebd4b89e6471814ca851abb8696170adb163985"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:54d6921f07555713b9300bee9c50fb46e57e2e639027089b1d795ecd9f7fa910"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1d407181cfa6e70077df3377938c08012d18893f9f20e92f7d2f314a437c30b1"}, + {file = "yarl-1.18.3-cp39-cp39-win32.whl", hash = "sha256:ac36703a585e0929b032fbaab0707b75dc12703766d0b53486eabd5139ebadd5"}, + {file = "yarl-1.18.3-cp39-cp39-win_amd64.whl", hash = "sha256:ba87babd629f8af77f557b61e49e7c7cac36f22f871156b91e10a6e9d4f829e9"}, + {file = "yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b"}, + {file = "yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" +propcache = ">=0.2.0" + +[[package]] +name = "zipp" +version = "3.21.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.9" +files = [ + {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, + {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.10" +content-hash = "61c2ca857f09089fbbecbf958cc1d921532fae4d7999ac70da3a76c950263d87" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8fdf13a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,51 @@ +[tool.poetry] +name = "inspect-k8s-sandbox" +version = "0.1.0" +description = "A Kubernetes Sandbox Environment for Inspect" +authors = ["Craig "] +readme = "README.md" +packages = [ + {include = "k8s_sandbox", from = "src"}, +] + +[tool.poetry.dependencies] +python = "^3.10" +inspect-ai = ">=0.3.50" +kubernetes = "^31.0.0" + +[tool.poetry.group.dev.dependencies] +mypy = "^1.9.0" +pre-commit = "^3.6.2" +pytest = "^8.1.1" +pytest-asyncio = "^0.23.7" +pytest-repeat = "^0.9.3" +ruff = "^0.6.0" +types-pyyaml = "^6.0.12" + +[tool.poetry.plugins.inspect_ai] +k8s-sandbox = "k8s_sandbox._sandbox_environment" + + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + + +[tool.ruff.lint] +select = ["E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # flake8 + "D", # pydocstyle + "I", # isort + ] +ignore = ["E203", "D10", "D203", "D212"] + +[tool.ruff.lint.pydocstyle] +convention = "google" + + +[tool.pytest.ini_options] +asyncio_mode = "auto" +markers = [ + "req_k8s: marks tests as requiring a test Kubernetes cluster (deselect with '-m \"not req_k8s\"')" +] From 368e4902b17bae54286950e12b31f6dca99ec9bd Mon Sep 17 00:00:00 2001 From: "Craig.Walton" Date: Tue, 17 Dec 2024 11:54:26 +0000 Subject: [PATCH 2/7] Initial migration of src/k8s_sandbox from internal repo. --- src/k8s_sandbox/__init__.py | 14 + src/k8s_sandbox/_helm.py | 238 ++++++++++++++++ src/k8s_sandbox/_kubernetes_api.py | 44 +++ src/k8s_sandbox/_logger.py | 59 ++++ src/k8s_sandbox/_manager.py | 120 ++++++++ src/k8s_sandbox/_pod/__init__.py | 9 + src/k8s_sandbox/_pod/buffer.py | 32 +++ src/k8s_sandbox/_pod/error.py | 20 ++ src/k8s_sandbox/_pod/execute.py | 164 +++++++++++ src/k8s_sandbox/_pod/executor.py | 66 +++++ src/k8s_sandbox/_pod/get_returncode.py | 36 +++ src/k8s_sandbox/_pod/op.py | 105 +++++++ src/k8s_sandbox/_pod/pod.py | 113 ++++++++ src/k8s_sandbox/_pod/read.py | 67 +++++ src/k8s_sandbox/_pod/write.py | 75 +++++ src/k8s_sandbox/_prereqs.py | 47 ++++ src/k8s_sandbox/_sandbox_environment.py | 259 ++++++++++++++++++ .../resources/helm/agent-env/Chart.yaml | 3 + .../resources/helm/agent-env/README.md | 38 +++ .../resources/helm/agent-env/generate-docs.md | 6 + .../templates/additional-resources.yaml | 6 + .../helm/agent-env/templates/coredns.yaml | 42 +++ .../agent-env/templates/helpers/_helpers.tpl | 63 +++++ .../agent-env/templates/network-policy.yaml | 103 +++++++ .../helm/agent-env/templates/pvc.yaml | 21 ++ .../helm/agent-env/templates/services.yaml | 183 +++++++++++++ .../helm/agent-env/values.schema.json | 178 ++++++++++++ .../resources/helm/agent-env/values.yaml | 127 +++++++++ 28 files changed, 2238 insertions(+) create mode 100644 src/k8s_sandbox/__init__.py create mode 100644 src/k8s_sandbox/_helm.py create mode 100644 src/k8s_sandbox/_kubernetes_api.py create mode 100644 src/k8s_sandbox/_logger.py create mode 100644 src/k8s_sandbox/_manager.py create mode 100644 src/k8s_sandbox/_pod/__init__.py create mode 100644 src/k8s_sandbox/_pod/buffer.py create mode 100644 src/k8s_sandbox/_pod/error.py create mode 100644 src/k8s_sandbox/_pod/execute.py create mode 100644 src/k8s_sandbox/_pod/executor.py create mode 100644 src/k8s_sandbox/_pod/get_returncode.py create mode 100644 src/k8s_sandbox/_pod/op.py create mode 100644 src/k8s_sandbox/_pod/pod.py create mode 100644 src/k8s_sandbox/_pod/read.py create mode 100644 src/k8s_sandbox/_pod/write.py create mode 100644 src/k8s_sandbox/_prereqs.py create mode 100644 src/k8s_sandbox/_sandbox_environment.py create mode 100644 src/k8s_sandbox/resources/helm/agent-env/Chart.yaml create mode 100644 src/k8s_sandbox/resources/helm/agent-env/README.md create mode 100644 src/k8s_sandbox/resources/helm/agent-env/generate-docs.md create mode 100644 src/k8s_sandbox/resources/helm/agent-env/templates/additional-resources.yaml create mode 100644 src/k8s_sandbox/resources/helm/agent-env/templates/coredns.yaml create mode 100644 src/k8s_sandbox/resources/helm/agent-env/templates/helpers/_helpers.tpl create mode 100644 src/k8s_sandbox/resources/helm/agent-env/templates/network-policy.yaml create mode 100644 src/k8s_sandbox/resources/helm/agent-env/templates/pvc.yaml create mode 100644 src/k8s_sandbox/resources/helm/agent-env/templates/services.yaml create mode 100644 src/k8s_sandbox/resources/helm/agent-env/values.schema.json create mode 100644 src/k8s_sandbox/resources/helm/agent-env/values.yaml diff --git a/src/k8s_sandbox/__init__.py b/src/k8s_sandbox/__init__.py new file mode 100644 index 0000000..a48098c --- /dev/null +++ b/src/k8s_sandbox/__init__.py @@ -0,0 +1,14 @@ +from aisitools.k8s_sandbox._pod import GetReturncodeError, PodError +from aisitools.k8s_sandbox._sandbox_environment import ( + K8sError, + K8sSandboxEnvironment, + K8sSandboxEnvironmentConfig, +) + +__all__ = [ + "GetReturncodeError", + "PodError", + "K8sError", + "K8sSandboxEnvironment", + "K8sSandboxEnvironmentConfig", +] diff --git a/src/k8s_sandbox/_helm.py b/src/k8s_sandbox/_helm.py new file mode 100644 index 0000000..bbb4fba --- /dev/null +++ b/src/k8s_sandbox/_helm.py @@ -0,0 +1,238 @@ +import asyncio +import logging +import os +import re +from pathlib import Path +from typing import Any, NoReturn + +from inspect_ai.util import ExecResult, concurrency +from kubernetes.client.rest import ApiException # type: ignore +from shortuuid import uuid + +from aisitools.k8s_sandbox._kubernetes_api import ( + get_current_context_namespace, + k8s_client, +) +from aisitools.k8s_sandbox._logger import format_log_message, sandbox_log +from aisitools.k8s_sandbox._pod import Pod + +DEFAULT_CHART = Path(__file__).parent / "resources" / "helm" / "agent-env" +DEFAULT_TIMEOUT = 300 +MAX_INSTALL_ATTEMPTS = 3 +INSTALL_RETRY_DELAY_SECONDS = 5 + + +logger = logging.getLogger(__name__) + + +class _ResourceQuotaModifiedError(Exception): + pass + + +class Release: + """A release of a Helm chart.""" + + def __init__( + self, + task_name: str, + chart_path: Path | None = None, + values_path: Path | None = None, + ) -> None: + self.task_name = task_name + self._chart_path = chart_path or DEFAULT_CHART + self._values_path = values_path + self._namespace = get_current_context_namespace() + # The release name is used in pod names too, so limit it to 8 chars. + self.release_name = self._generate_release_name() + + def _generate_release_name(self) -> str: + return uuid().lower()[:8] + + async def install(self) -> None: + async with _install_semaphore(): + sandbox_log( + "Installing helm chart.", + chart=self._chart_path, + release=self.release_name, + values=self._values_path, + namespace=self._namespace, + task=self.task_name, + ) + attempt = 1 + while True: + try: + await self._install(upgrade=attempt > 1) + break + except _ResourceQuotaModifiedError: + if attempt >= MAX_INSTALL_ATTEMPTS: + raise + attempt += 1 + await asyncio.sleep(INSTALL_RETRY_DELAY_SECONDS) + + async def uninstall(self, quiet: bool) -> None: + await uninstall(self.release_name, quiet) + + async def get_sandbox_pods(self) -> dict[str, Pod]: + client = k8s_client() + loop = asyncio.get_running_loop() + try: + pods = await loop.run_in_executor( + None, + lambda: client.list_namespaced_pod( + self._namespace, + label_selector=f"app.kubernetes.io/instance={self.release_name}", + ), + ) + except ApiException as e: + _raise_runtime_error( + "Failed to list pods.", release=self.release_name, from_exception=e + ) + if not pods.items: + _raise_runtime_error("No pods found.", release=self.release_name) + sandboxes = dict() + for pod in pods.items: + service_name = pod.metadata.labels.get("inspect/service") + # Depending on the Helm chart, some Pods may not have a service label. + # These should not be considered to be a sandbox pod (as per our docs). + if service_name is not None: + default_container_name = pod.spec.containers[0].name + sandboxes[service_name] = Pod( + pod.metadata.name, self._namespace, default_container_name + ) + return sandboxes + + async def _install(self, upgrade: bool) -> None: + # Whilst `upgrade --install` could always be used, prefer explicitly using + # `install` for the first attempt. + subcommand = ["upgrade", "--install"] if upgrade else ["install"] + values = ["--values", str(self._values_path)] if self._values_path else [] + result = await _run_subprocess( + "helm", + subcommand + + [ + self.release_name, + str(self._chart_path), + "--namespace", + self._namespace, + "--wait", + "--timeout", + f"{_get_timeout()}s", + "--set", + # Annotation do not have strict length reqs. Quoting/escaping + # handled by asyncio.create_subprocess_exec. + f"annotations.inspectTaskName={self.task_name}", + ] + + values, + capture_output=True, + ) + if not result.success: + self._raise_install_error(result) + + def _raise_install_error(self, result: ExecResult[str]) -> NoReturn: + # When concurrent helm operations are modifying the same resource quota, the + # following error occasionally occurs. Retry. + if re.search( + r"Operation cannot be fulfilled on resourcequotas \".*\": the object has " + r"been modified; please apply your changes to the latest version and try " + r"again", + result.stderr, + ): + sandbox_log( + "resourcequota modified error whilst installing helm chart.", + release=self.release_name, + error=result.stderr, + ) + raise _ResourceQuotaModifiedError(result.stderr) + _raise_runtime_error( + "Helm install failed.", release=self.release_name, result=result + ) + + +async def uninstall(release_name: str, quiet: bool) -> None: + namespace = get_current_context_namespace() + async with _uninstall_semaphore(): + sandbox_log( + "Uninstalling helm release.", release=release_name, namespace=namespace + ) + result = await _run_subprocess( + "helm", + [ + "uninstall", + release_name, + "--namespace", + namespace, + "--wait", + "--timeout", + f"{_get_timeout()}s", + ], + capture_output=quiet, + ) + if not result.success: + captured_output = result.stdout if not quiet else "not captured" + _raise_runtime_error( + "Helm uninstall failed.", release=release_name, result=captured_output + ) + + +def _raise_runtime_error( + message: str, from_exception: Exception | None = None, **kwargs: Any +) -> NoReturn: + formatted = format_log_message(message, **kwargs) + logger.error(formatted) + if from_exception: + raise RuntimeError(formatted) from from_exception + else: + raise RuntimeError(formatted) + + +async def _run_subprocess( + cmd: str, args: list[str], capture_output: bool +) -> ExecResult[str]: + proc = await asyncio.create_subprocess_exec( + cmd, + *args, + stdout=asyncio.subprocess.PIPE if capture_output else None, + stderr=asyncio.subprocess.PIPE if capture_output else None, + ) + stdout, stderr = await proc.communicate() + return ExecResult( + success=proc.returncode == 0, + returncode=proc.returncode or 1, + stdout=stdout.decode() if stdout else "", + stderr=stderr.decode() if stderr else "", + ) + + +def _get_timeout() -> int: + if user_configured_timeout := os.environ.get("INSPECT_HELM_TIMEOUT"): + timeout_int = int(user_configured_timeout) + if timeout_int <= 0: + raise ValueError( + "INSPECT_HELM_TIMEOUT must be a positive int: " + f"{user_configured_timeout}" + ) + return timeout_int + return DEFAULT_TIMEOUT + + +def _install_semaphore() -> asyncio.Semaphore: + # Limit concurrent subprocess calls to `helm install` and `helm uninstall`. + # Use distinct semaphores for each operation to avoid deadlocks where all permits + # are acquired by the "install" operations which are waiting for cluster resources + # to be released by the "uninstall" operations. + # Use Inspect's concurrency function as this ensures each asyncio.Semaphore is + # unique per event loop. + return concurrency("helm-install", _get_environ_int("INSPECT_MAX_HELM_INSTALL", 8)) + + +def _uninstall_semaphore() -> asyncio.Semaphore: + return concurrency( + "helm-uninstall", _get_environ_int("INSPECT_MAX_HELM_UNINSTALL", 8) + ) + + +def _get_environ_int(name: str, default: int) -> int: + try: + return int(os.environ[name]) + except (KeyError, ValueError): + return default diff --git a/src/k8s_sandbox/_kubernetes_api.py b/src/k8s_sandbox/_kubernetes_api.py new file mode 100644 index 0000000..04fa3f0 --- /dev/null +++ b/src/k8s_sandbox/_kubernetes_api.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +import logging +import threading + +from kubernetes import client, config # type: ignore + +logger = logging.getLogger(__name__) + +_thread_local = threading.local() +_load_config_lock = threading.Lock() +_config_loaded = False + + +def k8s_client() -> client.CoreV1Api: + """ + Gets a thread-local Kubernetes client. + + This function is thread-safe and ensures that the Kubernetes configuration is + loaded. + A Kubernetes client cannot be used simultaneously from multiple threads (which are + used because the kubernetes client is not async). + """ + _ensure_config_loaded() + if not hasattr(_thread_local, "client"): + _thread_local.client = client.CoreV1Api() + return _thread_local.client + + +def get_current_context_namespace() -> str: + """Get the current context's namespace from the Kubernetes configuration.""" + _ensure_config_loaded() + _, current_ctx = config.list_kube_config_contexts() + namespace = current_ctx["context"]["namespace"] + assert isinstance(namespace, str) + return namespace + + +def _ensure_config_loaded() -> None: + with _load_config_lock: + global _config_loaded + if not _config_loaded: + config.load_kube_config() + _config_loaded = True diff --git a/src/k8s_sandbox/_logger.py b/src/k8s_sandbox/_logger.py new file mode 100644 index 0000000..d45bbba --- /dev/null +++ b/src/k8s_sandbox/_logger.py @@ -0,0 +1,59 @@ +import json +import logging +import os +from typing import Any + +# TODO: We're accessing an internal constant. Can this be made public by inspect? +from inspect_ai._util.constants import SANDBOX + +logger = logging.getLogger(__name__) + +TRUNCATED_SUFFIX = "..." +# The threshold at which to truncate individual arguments in logging messages. +# Some ExecResults can contain very large outputs. +DEFAULT_ARG_TRUNCATION_THRESHOLD = 1000 + + +def sandbox_log(message: str, level: int = SANDBOX, **kwargs: Any) -> None: + """Format and log a message with "K8S: " prefix. + + Args: + message: The log message. + level: The log level. Defaults to SANDBOX. + **kwargs: Key-value pairs to include in the log message. Values are truncated if + they exceed DEFAULT_ARG_TRUNCATION_THRESHOLD (which can be overridden with env + var INSPECT_K8S_LOG_TRUNCATION_THRESHOLD). + """ + formatted = format_log_message(message, **kwargs) + logger.log(level, f"K8S: {formatted}") + + +def format_log_message(message: str, **kwargs: Any) -> str: + """Format message in a structured fashion. + + Args: + message: The log message. + **kwargs: Key-value pairs to include in the log message. Values are truncated if + they exceed DEFAULT_ARG_TRUNCATION_THRESHOLD (which can be overridden with env + var INSPECT_K8S_LOG_TRUNCATION_THRESHOLD). + """ + if not kwargs: + return message + truncated_kwargs = {k: _truncate_arg(v) for k, v in kwargs.items()} + json_args = json.dumps(truncated_kwargs, ensure_ascii=False) + return f"{message} {json_args}" + + +def _truncate_arg(arg: Any) -> str: + arg_str = str(arg) + truncation_threshold = _get_arg_truncation_threshold() + if len(arg_str) > truncation_threshold: + return arg_str[:truncation_threshold] + TRUNCATED_SUFFIX + return arg_str + + +def _get_arg_truncation_threshold() -> int: + try: + return int(os.environ["INSPECT_K8S_LOG_TRUNCATION_THRESHOLD"]) + except (KeyError, ValueError): + return DEFAULT_ARG_TRUNCATION_THRESHOLD diff --git a/src/k8s_sandbox/_manager.py b/src/k8s_sandbox/_manager.py new file mode 100644 index 0000000..53adf5d --- /dev/null +++ b/src/k8s_sandbox/_manager.py @@ -0,0 +1,120 @@ +from __future__ import annotations + +import asyncio +from contextvars import ContextVar + +from rich import box, print +from rich.panel import Panel +from rich.table import Table + +from aisitools.k8s_sandbox._helm import Release +from aisitools.k8s_sandbox._helm import uninstall as helm_uninstall + + +class HelmReleaseManager: + """ + Manages the lifecycle of Helm releases. + + Each instance of this class is scoped to a single async context. + """ + + _context_var: ContextVar[HelmReleaseManager] = ContextVar("k8s_manager_instance") + + def __init__(self) -> None: + self._installed_releases: list[Release] = [] + + @classmethod + def get_instance(cls) -> HelmReleaseManager: + """Gets the Manager instance for the current async context.""" + try: + return cls._context_var.get() + except LookupError: + manager = cls() + cls._context_var.set(manager) + return manager + + async def install(self, release: Release) -> None: + """ + Installs a release and tracks it for eventual cleanup. + + Args: + release (Release): The release to install and track. + """ + # Track the release regardless of the install result. + self._installed_releases.append(release) + await release.install() + + async def uninstall(self, release: Release, quiet: bool) -> None: + """ + Uninstalls a release managed by this instance. + + Args: + release (Release): The release to uninstall. + quiet (bool): If True, suppress output to the console. + """ + await release.uninstall(quiet) + self._installed_releases.remove(release) + + async def uninstall_all(self, print_only: bool) -> None: + """Uninstalls all releases managed by this instance. + + This method is not quiet i.e. it will print output to the console. + + Args: + print_only (bool): If True, print cleanup instructions without actually + uninstalling anything. + """ + if len(self._installed_releases) == 0: + return + if print_only: + self._print_cleanup_instructions() + return + _print_do_not_interrupt() + tasks = [release.uninstall(quiet=False) for release in self._installed_releases] + # Clear the list before awaiting the tasks to prevent other calls to this method + # from interfering. + self._installed_releases.clear() + await asyncio.gather(*tasks, return_exceptions=True) + + def _print_cleanup_instructions(self) -> None: + table = Table( + title="K8s Sandbox Environments (not yet cleaned up):", + box=box.SQUARE_DOUBLE_HEAD, + show_lines=True, + title_style="bold", + title_justify="left", + ) + table.add_column("Container(s)", no_wrap=True) + table.add_column("Cleanup") + for release in self._installed_releases: + table.add_row( + release.release_name, + f"[blue]inspect sandbox cleanup k8s {release.release_name}[/blue]", + ) + print("") + print(table) + # TODO: Once supported, tell user how to cleanup all environments. + # print( + # "\nCleanup all environments with: " + # "[blue]inspect sandbox cleanup k8s[/blue]\n" + # ) + + +async def uninstall_unmanaged_release(release_name: str) -> None: + """ + Uninstall a Helm release which is not managed by a HelmReleaseManager. + + Args: + release_name (str): The name of the release to uninstall (e.g. "lsphdyup"). + """ + _print_do_not_interrupt() + await helm_uninstall(release_name, quiet=False) + + +def _print_do_not_interrupt() -> None: + print( + Panel( + "[bold][blue]Cleaning up K8s resources (please do not interrupt this " + "operation!):[/blue][/bold]", + ) + ) diff --git a/src/k8s_sandbox/_pod/__init__.py b/src/k8s_sandbox/_pod/__init__.py new file mode 100644 index 0000000..9f22213 --- /dev/null +++ b/src/k8s_sandbox/_pod/__init__.py @@ -0,0 +1,9 @@ +from aisitools.k8s_sandbox._pod.error import PodError +from aisitools.k8s_sandbox._pod.get_returncode import GetReturncodeError +from aisitools.k8s_sandbox._pod.pod import Pod + +__all__ = [ + "GetReturncodeError", + "PodError", + "Pod", +] diff --git a/src/k8s_sandbox/_pod/buffer.py b/src/k8s_sandbox/_pod/buffer.py new file mode 100644 index 0000000..96e6443 --- /dev/null +++ b/src/k8s_sandbox/_pod/buffer.py @@ -0,0 +1,32 @@ +class LimitedBuffer: + """ + A buffer with a limited capacity. + + Once the buffer is full, `truncated` is set and further appends are ignored. + + The buffer can be converted to a string (utf-8). Will raise a UnicodeDecodeError if + the buffer contains invalid utf-8 data (except it it was as a result of truncation). + """ + + def __init__(self, limit: int) -> None: + self._buffer = bytearray() + self._limit = limit + self.truncated = False + + def append(self, data: bytes) -> None: + if self.truncated: + return + remaining_space = self._limit - len(self._buffer) + if len(data) > remaining_space: + self.truncated = True + self._buffer.extend(data[:remaining_space]) + + def __str__(self) -> str: + # If we're truncated the data, there may be an incomplete character right at the + # end of the buffer. + try: + return self._buffer.decode("utf-8", errors="strict") + except UnicodeDecodeError as e: + if self.truncated and e.end == len(self._buffer): + return self._buffer[: e.start].decode("utf-8", errors="strict") + raise diff --git a/src/k8s_sandbox/_pod/error.py b/src/k8s_sandbox/_pod/error.py new file mode 100644 index 0000000..dbe4f67 --- /dev/null +++ b/src/k8s_sandbox/_pod/error.py @@ -0,0 +1,20 @@ +from typing import Any + +from aisitools.k8s_sandbox._logger import format_log_message + + +class PodError(Exception): + """ + A generic error raised when interacting with a pod. + + This will typically cause the eval to fail. + """ + + def __init__(self, message: str, **kwargs: Any) -> None: + super().__init__(format_log_message(message, **kwargs)) + + +class GetReturncodeError(Exception): + """The return code of a pod operation could not be retrieved.""" + + pass diff --git a/src/k8s_sandbox/_pod/execute.py b/src/k8s_sandbox/_pod/execute.py new file mode 100644 index 0000000..615f0b7 --- /dev/null +++ b/src/k8s_sandbox/_pod/execute.py @@ -0,0 +1,164 @@ +import base64 +import re +import shlex +from contextlib import contextmanager +from typing import Generator + +from inspect_ai.util import ExecResult, OutputLimitExceededError +from inspect_ai.util import SandboxEnvironmentLimits as limits +from kubernetes.stream.ws_client import WSClient # type: ignore + +from aisitools.k8s_sandbox._pod.buffer import LimitedBuffer +from aisitools.k8s_sandbox._pod.get_returncode import get_returncode +from aisitools.k8s_sandbox._pod.op import PodOperation + +COMPLETED_SENTINEL = "completed-sentinel-value" +COMPLETED_SENTINEL_PATTERN = re.compile(rf"<{COMPLETED_SENTINEL}-(\d+)>") + + +class ExecuteOperation(PodOperation): + def exec( + self, + cmd: list[str], + stdin: str | bytes | None, + cwd: str | None, + env: dict[str, str], + timeout: int | None, + ) -> ExecResult[str]: + shell_script = self._build_shell_script(cmd, stdin, cwd, env, timeout) + with self._interactive_shell() as ws_client: + # Write the script to the shell's stdin rather than passing it as a command + # argument (-c) to better support potentially long commands. + ws_client.write_stdin(shell_script) + result = self._handle_shell_output(ws_client, timeout) + return result + + @contextmanager + def _interactive_shell(self) -> Generator[WSClient, None, None]: + yield from self.create_websocket_client_for_exec( + command=["/bin/sh"], + stderr=True, + stdin=True, + stdout=True, + # Leave stdout and stderr as binary. Has no effect on stdin. + binary=True, + ) + + def _build_shell_script( + self, + command: list[str], + stdin: str | bytes | None, + cwd: str | None, + env: dict[str, str], + timeout: int | None, + ) -> str: + def generate() -> Generator[str, None, None]: + if cwd is not None: + yield f"cd {shlex.quote(cwd)} || exit $?\n" + for key, value in env.items(): + yield f"export {shlex.quote(key)}={shlex.quote(value)}\n" + if stdin is not None: + yield self._pipe_user_input(stdin) + yield f"{self._prefix_timeout(timeout)}{shlex.join(command)}\n" + # Store the returncode so that the `echo` below doesn't overwrite it. + yield "returncode=$?\n" + # Ensure stdout and stderr are flushed before writing the sentinel value. + yield "sync\n" + # Write a sentinel value to stdout to determine when the user command + # has completed. Also write the returncode as we won't have access to it if + # we manually close the websocket connection. + yield f'echo -n "<{COMPLETED_SENTINEL}-$returncode>"\n' + # Exit the shell. This won't actually close the websocket connection until + # stdout and stderr (which have been inherited by the user command) are + # closed. But it will force the echo above to be flushed. + yield "exit $returncode\n" + + return "".join(generate()) + + def _pipe_user_input(self, stdin: str | bytes) -> str: + # Encode the user-provided input as base64 for 2 reasons: + # 1. To avoid issues with special characters (e.g. new lines) in the input. + # 2. To support binary input (e.g. null byte). + stdin_b64 = base64.b64encode( + stdin if isinstance(stdin, bytes) else stdin.encode("utf-8") + ).decode("ascii") + # Pipe user input. Simply writing it to the shell's stdin after a command e.g. + # `cat` results in `cat` blocking indefinitely as there is no way to close the + # stdin stream in v4.channel.k8s.io. + return f"echo '{stdin_b64}' | base64 -d | " + + def _prefix_timeout(self, timeout: int | None) -> str: + if timeout is None: + return "" + # Enforce timeout using `timeout` on the Pod. Simpler than alternative of + # enforcing this on the client side (requires terminating the remote process). + # `-k 5s` sends SIGKILL after grace period in case user command doesn't respect + # SIGTERM. + return f"timeout -k 5s {timeout}s " + + def _handle_shell_output( + self, ws_client: WSClient, timeout: int | None + ) -> ExecResult[str]: + def stream_output() -> ExecResult[str]: + stdout = LimitedBuffer(limits.MAX_EXEC_OUTPUT_SIZE) + stderr = LimitedBuffer(limits.MAX_EXEC_OUTPUT_SIZE) + returncode: int | None = None + while ws_client.is_open(): + # `timeout=None` means `update` will block indefinitely until there is + # data to read from the socket. + ws_client.update(timeout=None) + # Note: `peek_*()` and `read_*()` may call `update(timeout=0)`. + if ws_client.peek_stderr(): + stderr.append(ws_client.read_stderr()) + # Handle stdout _after_ stderr to guarantee that, if buffered, the + # sentinel is actioned before the blocking `ws_client.update(None)`. + if ws_client.peek_stdout(): + frame = ws_client.read_stdout() + # Assumption: The sentinel value is written to stdout in a single + # frame and not split by other writes to stdout. + filtered, returncode = self._filter_sentinel_and_returncode(frame) + stdout.append(filtered) + if returncode is not None: + ws_client.close() + self._verify_output_limit(stdout, stderr) + # returncode won't be set if setup commands e.g. `cd` failed. + if returncode is None: + returncode = get_returncode(ws_client) + return ExecResult( + success=returncode == 0, + returncode=returncode, + stdout=str(stdout), + stderr=str(stderr), + ) + + result = stream_output() + # 124 is the exit code for the `timeout` command. + if timeout is not None and result.returncode == 124: + raise TimeoutError(f"Command timed out after {timeout}s. {result}") + # The Inspect SandboxEnvironment interface expects us to raise a + # PermissionError for exit code 126 and stderr containing "permission denied". + if result.returncode == 126 and "permission denied" in result.stderr.casefold(): + raise PermissionError(f"Permission denied executing command. {result}") + return result + + def _filter_sentinel_and_returncode(self, frame: bytes) -> tuple[bytes, int | None]: + # We don't support returning binary data from an exec() command and are expected + # to raise a UnicodeDecodeError if we encounter one, so errors="strict". + # Assumption: individual frames are valid utf-8 (i.e. characters are not split + # across frames). + decoded = frame.decode("utf-8", errors="strict") + split_frame = re.split(COMPLETED_SENTINEL_PATTERN, decoded) + if len(split_frame) == 1: + return frame, None + # Remove the sentinel value from the stdout frame. + filtered = split_frame[0] + split_frame[2] + return filtered.encode("utf-8"), int(split_frame[1]) + + def _verify_output_limit( + self, stdout: LimitedBuffer, stderr: LimitedBuffer + ) -> None: + if stdout.truncated or stderr.truncated: + raise OutputLimitExceededError( + limit_str=limits.MAX_EXEC_OUTPUT_SIZE_STR, + truncated_output=str(stdout) + str(stderr), + ) diff --git a/src/k8s_sandbox/_pod/executor.py b/src/k8s_sandbox/_pod/executor.py new file mode 100644 index 0000000..58e04a1 --- /dev/null +++ b/src/k8s_sandbox/_pod/executor.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +import asyncio +import os +from concurrent.futures import ThreadPoolExecutor +from typing import Callable, TypeVar + +from inspect_ai.util import concurrency + +from aisitools.k8s_sandbox._logger import sandbox_log + +T = TypeVar("T") + + +class PodOpExecutor: + """ + A singleton class that manages a thread pool executor for running pod operations. + + This class's API is asynchronous, but the operations it runs are synchronous. It + runs operations in a thread pool executor. + + Interacts with Inspect's concurrency context manager for the purpose of displaying + the number of ongoing operations. + """ + + _instance: PodOpExecutor | None = None + + def __init__(self) -> None: + try: + self._max_workers = int(os.environ["INSPECT_MAX_POD_OPS"]) + except (KeyError, ValueError): + cpu_count = os.cpu_count() or 1 + # Pod operations are typically I/O-bound (from the client's perspective). + self._max_workers = cpu_count * 4 + sandbox_log("Creating PodOpExecutor.", max_workers=self._max_workers) + self._executor = ThreadPoolExecutor( + max_workers=self._max_workers, thread_name_prefix="pod-op-executor" + ) + + @classmethod + def get_instance(cls) -> PodOpExecutor: + """Gets the singleton instance of the PodOpExecutor. + + This method is async-safe (because it doesn't await anything) but not + thread-safe. + """ + if cls._instance is None: + cls._instance = cls() + return cls._instance + + async def queue_operation(self, callable: Callable[[], T]) -> T: + """ + Queue a synchronous pod operation to run asynchronously and return the result. + + A thread pool executor is used to run the operation in another thread. + + Inspect's concurrency context manager is used so that the user gets visibility + of the number of ongoing operations. Other than the user display, the + use of the semaphore is redundant. + + This method is async-safe but not thread-safe. + """ + async with concurrency("pod-op", self._max_workers): + return await asyncio.get_event_loop().run_in_executor( + self._executor, callable + ) diff --git a/src/k8s_sandbox/_pod/get_returncode.py b/src/k8s_sandbox/_pod/get_returncode.py new file mode 100644 index 0000000..39719d7 --- /dev/null +++ b/src/k8s_sandbox/_pod/get_returncode.py @@ -0,0 +1,36 @@ +import yaml +from kubernetes.stream.ws_client import ERROR_CHANNEL, WSClient # type: ignore + +from aisitools.k8s_sandbox._pod.error import GetReturncodeError + + +def get_returncode(ws_client: WSClient) -> int: + """ + Extracts the returncode from a websocket client. + + Similar to the `WSClient.returncode` property, but with additional and more + informative error handling. + """ + assert not ws_client.is_open(), "ws_client must be closed to get return code." + # Note: ERROR_CHANNEL is not the same as stderr. Aka status channel. + channel_value = ws_client.read_channel(ERROR_CHANNEL) + if not channel_value: + raise GetReturncodeError( + "Failed to get returncode from k8s error channel because it was empty." + ) + loaded = yaml.safe_load(channel_value) + if "status" not in loaded: + raise GetReturncodeError( + "Failed to get returncode from k8s error channel because it did not " + f"contain a `status` key. Error channel: {channel_value}", + ) + if loaded["status"] == "Success": + return 0 + for cause in loaded["details"]["causes"]: + if cause.get("reason") == "ExitCode": + return int(cause["message"]) + raise GetReturncodeError( + "Failed to get returncode from k8s error channel because `status`!='Success' " + "and there was no entry in `details.causes` with `reason`=='ExitCode'. " + f"Error channel: {channel_value}", + ) diff --git a/src/k8s_sandbox/_pod/op.py b/src/k8s_sandbox/_pod/op.py new file mode 100644 index 0000000..ab1c1ae --- /dev/null +++ b/src/k8s_sandbox/_pod/op.py @@ -0,0 +1,105 @@ +import logging +from abc import ABC +from dataclasses import dataclass +from typing import Generator + +from kubernetes.stream import stream # type: ignore +from kubernetes.stream.ws_client import WSClient # type: ignore + +from aisitools.k8s_sandbox._kubernetes_api import k8s_client + +# The duration to wait for an initial response from the k8s API server. +# The initial response is received before the command is necessarily complete, so +# long-running commands will not be affected by this timeout. +# https://github.com/kubernetes-client/python/blob/master/examples/watch/timeout-settings.md +API_TIMEOUT = 60 + +logger = logging.getLogger(__name__) + + +@dataclass(frozen=True) +class PodInfo: + """ + Information required to interact with a Kubernetes pod. + + This class is immutable and thread-safe. + """ + + name: str + namespace: str + default_container_name: str + + +class PodOperation(ABC): + """ + A base class for a synchronous operation on a pod. + + The purpose of splitting these operations into separate classes is to encapsulate + and isolate their respective behaviour. + """ + + _failed_to_discard_duplicate_channel = False + + def __init__(self, pod: PodInfo): + self._pod = pod + + def create_websocket_client_for_exec( + self, **kwargs + ) -> Generator[WSClient, None, None]: + client = k8s_client() + # Note: ApiException is intentionally not caught; it should fail the eval. + ws_client = stream( + client.connect_get_namespaced_pod_exec, + name=self._pod.name, + namespace=self._pod.namespace, + container=self._pod.default_container_name, + _preload_content=False, + # This is the timeout for the API request, not the command itself. + _request_timeout=API_TIMEOUT, + **kwargs, + ) + try: + self._discard_duplicate_channel(ws_client) + yield ws_client + finally: + ws_client.close() + + def _discard_duplicate_channel(self, ws_client: WSClient) -> None: + # Avoid issuing a warning multiple times. + if PodOperation._failed_to_discard_duplicate_channel: + return + # WSClient stores all stdout and stderr in WSClient._all in addition to the + # relevant channels. Set the _all channel to IgnoredIO to reduce memory usage. + # https://github.com/kubernetes-client/python/issues/2302 + # Handle ImportError as we're importing a private class. + try: + from kubernetes.stream.ws_client import _IgnoredIO # type: ignore + except ImportError as e: + logger.warning( + f"Failed to set Kubernetes' WSClient._all channel to _IgnoredIO: {e}" + ) + PodOperation._failed_to_discard_duplicate_channel = True + return + # Whilst we can set the _all attribute whether it exists or not, we should + # log a warning if it doesn't exist as this may indicate a change in the + # Kubernetes library. + if not hasattr(ws_client, "_all"): + logger.warning( + "Failed to set Kubernetes' WSClient._all channel to _IgnoredIO: there " + "was no _all attribute on the WSClient object." + ) + PodOperation._failed_to_discard_duplicate_channel = True + return + ws_client._all = _IgnoredIO() + + +def raise_for_known_read_write_errors(stderr: str) -> None: + # The Inspect Sandbox interface asks us to raise specific exceptions for recognised + # error messages. + casefolded = stderr.casefold() + if "no such file or directory" in casefolded: + raise FileNotFoundError(stderr) + if "permission denied" in casefolded: + raise PermissionError(stderr) + if "is a directory" in casefolded: + raise IsADirectoryError(stderr) diff --git a/src/k8s_sandbox/_pod/pod.py b/src/k8s_sandbox/_pod/pod.py new file mode 100644 index 0000000..4011a16 --- /dev/null +++ b/src/k8s_sandbox/_pod/pod.py @@ -0,0 +1,113 @@ +from __future__ import annotations + +from pathlib import Path +from typing import IO, Callable, TypeVar + +from inspect_ai.util import ExecResult + +from aisitools.k8s_sandbox._pod.execute import ExecuteOperation +from aisitools.k8s_sandbox._pod.executor import PodOpExecutor +from aisitools.k8s_sandbox._pod.op import PodInfo +from aisitools.k8s_sandbox._pod.read import ReadFileOperation +from aisitools.k8s_sandbox._pod.write import WriteFileOperation + +T = TypeVar("T") + + +class Pod: + def __init__(self, name: str, namespace: str, default_container_name: str) -> None: + self.info = PodInfo(name, namespace, default_container_name) + + async def exec( + self, + cmd: list[str], + stdin: str | bytes | None, + cwd: str | None, + env: dict[str, str], + timeout: int | None, + ) -> ExecResult[str]: + """ + Execute a command in a pod. + + This method will return when and only when the supplied command exits, even if + the command has launched background processes (e.g. with `bash -c "foo &"`). + Any background processes will continue to run and will not be subject to the + optional timeout. + + When executing a command over connect_get_namespaced_pod_exec, the websocket + connection is not "naturally" closed until both: + - The command has exited. + - The stdout and stderr streams have been closed (including by any commands + which have inherited them). + This is behaviour of the CRI-O implementation which is running on the Kubernetes + nodes. + + To support the required functionality, the supplied command is executed in a + shell (/bin/sh). + + To allow this method to return when the supplied command has completed, even if + backgrounded processes which inherit stdout or stderr are still running, a + sentinel value is written to stdout after the supplied command has completed. + When this sentinel value is detected, we close the websocket connection. This + sentinel value also includes the exit code of the supplied command, as we won't + have access to /bin/sh's return code if we manually close the websocket. + + Args: + cmd (list[str]): The command and arguments to execute. + stdin (str | bytes | None): The optional standard input to pipe into cmd. + The stdin file descriptor will be closed after the input has been written. + cwd (str | None): The working directory to change to before executing cmd. + Relative directories will be resolved relative to the pod's default working + directory. If None, the default working directory is used. If the provided + directory does not exist, an unsuccessful ExecResult will be returned and + cmd will not be run. + env (dict[str, str]): The environment variables to set before running cmd. + timeout (int | None): The optional timeout for cmd to complete in. Defaults to + no timeout. If provided, SIGTERM will be sent to cmd once the timeout has + elapsed. This is enforced by the `timeout` command on the pod. This will not + terminate background processes started by cmd. + """ + executor = ExecuteOperation(self.info) + result = await self._run_async( + lambda: executor.exec(cmd, stdin, cwd, env, timeout) + ) + return result + + async def write_file(self, src: IO[bytes], dst: Path) -> None: + """ + Copy a file-like object (src) from the client to a path on the pod (dst). + + Existing files on the pod will be overwritten. + + The source will be read from its current position to the end of the file. The + file position will be restored after the copy. The file-like object must be + opened in binary mode. + + Args: + src (IO[bytes]): The file-like object which contains the contents to be + written to the pod. + dst (Path): The path to write the file to on the pod. Relative paths will be + resolved relative to the pod's default working directory. + """ + writer = WriteFileOperation(self.info) + await self._run_async(lambda: writer.write_file(src, dst)) + + async def read_file(self, src: Path, dst: IO[bytes]) -> None: + """ + Copy a file from the pod (src) to the a file-like object (dst) on the client. + + The file-like object will not be seeked before or after the read. The file-like + object must be opened for writing in binary mode. + + Args: + src (Path): The path to the file on the pod. Relative paths will be resolved + relative to the pod's default working directory. + dst (IO[bytes]): A file-like object to write the file to on the client system. + """ + reader = ReadFileOperation(self.info) + await self._run_async(lambda: reader.read_file(src, dst)) + + async def _run_async(self, callable: Callable[[], T]) -> T: + """Run a synchronous function asynchronously.""" + executor = PodOpExecutor.get_instance() + return await executor.queue_operation(callable) diff --git a/src/k8s_sandbox/_pod/read.py b/src/k8s_sandbox/_pod/read.py new file mode 100644 index 0000000..2469d2e --- /dev/null +++ b/src/k8s_sandbox/_pod/read.py @@ -0,0 +1,67 @@ +from contextlib import contextmanager +from pathlib import Path +from typing import IO, Generator + +from inspect_ai.util import OutputLimitExceededError +from inspect_ai.util import SandboxEnvironmentLimits as limits +from kubernetes.stream.ws_client import WSClient # type: ignore + +from aisitools.k8s_sandbox._pod.buffer import LimitedBuffer +from aisitools.k8s_sandbox._pod.error import PodError +from aisitools.k8s_sandbox._pod.get_returncode import get_returncode +from aisitools.k8s_sandbox._pod.op import ( + PodOperation, + raise_for_known_read_write_errors, +) + + +class ReadFileOperation(PodOperation): + def read_file(self, src: Path, dst: IO[bytes]) -> None: + with self._start_read_command(src) as ws_client: + self._handle_stream_output(ws_client, dst) + + @contextmanager + def _start_read_command(self, src: Path) -> Generator[WSClient, None, None]: + # Limit number of bytes read (-c) to 1 byte over the limit (to detect if the + # file is too large). + command = ["head", "-c", limits.MAX_READ_FILE_SIZE + 1, src.as_posix()] + yield from self.create_websocket_client_for_exec( + command=command, + stderr=True, + stdin=False, + stdout=True, + # Leave stdout (and stderr) as binary. + binary=True, + ) + + def _handle_stream_output(self, ws_client: WSClient, dst: IO[bytes]) -> None: + # `head` should not produce large amounts of stderr, but limit it nonetheless. + stderr = LimitedBuffer(limits.MAX_EXEC_OUTPUT_SIZE) + start_position = dst.tell() + # Stream the response, writing it to dst as we go to avoid holding the whole + # response in memory. + while ws_client.is_open(): + # `timeout=None` means `update` will block indefinitely until there is + # data to read. + ws_client.update(timeout=None) + if ws_client.peek_stdout(): + dst.write(ws_client.read_stdout()) + self._verify_output_limit(dst.tell() - start_position) + if ws_client.peek_stderr(): + stderr.append(ws_client.read_stderr()) + returncode = get_returncode(ws_client) + if returncode != 0: + stderr_str = str(stderr) + raise_for_known_read_write_errors(stderr_str) + raise PodError( + "Unrecognised error reading file from pod.", + returncode=returncode, + stderr=stderr_str, + ) + dst.flush() + + def _verify_output_limit(self, bytes_written: int) -> None: + if bytes_written > limits.MAX_READ_FILE_SIZE: + raise OutputLimitExceededError( + limit_str=limits.MAX_READ_FILE_SIZE_STR, truncated_output=None + ) diff --git a/src/k8s_sandbox/_pod/write.py b/src/k8s_sandbox/_pod/write.py new file mode 100644 index 0000000..cfa2197 --- /dev/null +++ b/src/k8s_sandbox/_pod/write.py @@ -0,0 +1,75 @@ +import io +import shlex +from contextlib import contextmanager +from pathlib import Path +from typing import IO, Generator + +from kubernetes.stream.ws_client import WSClient # type: ignore + +from aisitools.k8s_sandbox._pod.error import PodError +from aisitools.k8s_sandbox._pod.get_returncode import get_returncode +from aisitools.k8s_sandbox._pod.op import ( + PodOperation, + raise_for_known_read_write_errors, +) + + +class WriteFileOperation(PodOperation): + def write_file(self, src: IO[bytes], dst: Path) -> None: + file_size = self._get_file_size(src) + with self._start_write_command(dst, file_size) as ws_client: + self._write_data_to_stdin(ws_client, src) + self._handle_stream_output(ws_client) + + def _get_file_size(self, file: IO[bytes]) -> int: + original_position = file.tell() + file.seek(0, io.SEEK_END) + file_size = file.tell() + file.seek(original_position) + return file_size + + @contextmanager + def _start_write_command( + self, dst: Path, file_size: int + ) -> Generator[WSClient, None, None]: + mkdir_command = f"mkdir -p {shlex.quote(dst.parent.as_posix())}" + # Use `head` with `-c ` because we have no way of closing the stdin + # stream in v4.channel.k8s.io (which means the websocket connection would never + # close). + head_command = f"head -c {file_size}" + command = [ + "/bin/sh", + "-c", + f"{mkdir_command} && {head_command} > {shlex.quote(dst.as_posix())}", + ] + yield from self.create_websocket_client_for_exec( + command=command, + stderr=True, + stdin=True, + stdout=True, + # Read stdout and stderr as text. Has no effect on stdin. + binary=False, + ) + + def _write_data_to_stdin(self, ws_client: WSClient, src: IO[bytes]) -> None: + original_position = src.tell() + # Write the src in chunks of 1MiB as large writes (~100MiB) result in + # ssl.SSLEOFError. + chunk_size = 1024**2 # 1 MiB + while data := src.read(chunk_size): + ws_client.write_stdin(data) + src.seek(original_position) + + def _handle_stream_output(self, ws_client: WSClient) -> None: + # Wait until the websocket connection is closed. All stderr will be stored by us + # in memory anyway so there is no value in streaming it. + ws_client.run_forever() + returncode = get_returncode(ws_client) + if returncode != 0: + stderr = ws_client.read_stderr() + raise_for_known_read_write_errors(stderr) + raise PodError( + "Unrecognised error writing file to pod.", + returncode=returncode, + stderr=stderr, + ) diff --git a/src/k8s_sandbox/_prereqs.py b/src/k8s_sandbox/_prereqs.py new file mode 100644 index 0000000..83975b2 --- /dev/null +++ b/src/k8s_sandbox/_prereqs.py @@ -0,0 +1,47 @@ +import logging + +from inspect_ai._util.error import PrerequisiteError # TODO: Using private package. +from inspect_ai.util import subprocess +from semver import Version + +logger = logging.getLogger(__name__) + +# This was picked as it is over 2 years old. It may be possible that older versions +# work but would require testing. +MINIMUM_HELM_VERSION = "3.10.0" + + +async def validate_prereqs() -> None: + await _validate_helm() + + +async def _validate_helm() -> None: + """Validate that helm is installed and the version is >= REQUIRED_HELM_VERSION.""" + try: + result = await subprocess(["helm", "version", "--short"]) + # Inspect's `subprocess` raises FileNotFoundError if the command is not found. + except FileNotFoundError: + _raise("Helm is not installed.") + except Exception: + logger.warning( + "Unexpected exception when executing `helm version`.", exc_info=True + ) + _raise("Failed to determine which version of helm is installed.") + installed_version = _parse_version(result.stdout) + if installed_version.compare(MINIMUM_HELM_VERSION) < 0: + _raise(f"Found version {installed_version}.") + + +def _raise(message: str) -> None: + raise PrerequisiteError( + "K8s sandbox environments require helm (CLI) version >= " + f"{MINIMUM_HELM_VERSION}. {message} " + "See https://helm.sh/docs/intro/install/" + ) + + +def _parse_version(version: str) -> Version: + # Typical output: "v3.15.3+g3bb50bb" + if version.startswith("v"): + return Version.parse(version[1:]) + return Version.parse(version) diff --git a/src/k8s_sandbox/_sandbox_environment.py b/src/k8s_sandbox/_sandbox_environment.py new file mode 100644 index 0000000..94f41f4 --- /dev/null +++ b/src/k8s_sandbox/_sandbox_environment.py @@ -0,0 +1,259 @@ +import logging +import tempfile +from contextlib import contextmanager +from pathlib import Path +from typing import Any, Callable, Generator, Literal, cast, overload + +from inspect_ai.util import ( + ExecResult, + OutputLimitExceededError, + SandboxEnvironment, + SandboxEnvironmentConfigType, + sandboxenv, +) +from pydantic import BaseModel + +from aisitools.k8s_sandbox._helm import Release +from aisitools.k8s_sandbox._logger import format_log_message, sandbox_log +from aisitools.k8s_sandbox._manager import ( + HelmReleaseManager, + uninstall_unmanaged_release, +) +from aisitools.k8s_sandbox._pod import Pod +from aisitools.k8s_sandbox._prereqs import validate_prereqs + + +@sandboxenv(name="k8s") +class K8sSandboxEnvironment(SandboxEnvironment): + """An Inspect sandbox environment for a Kubernetes (k8s) cluster.""" + + def __init__(self, release: Release, pod: Pod): + self.release = release + self._pod = pod + + @classmethod + def config_files(cls) -> list[str]: + return ["values.yaml", "helm-values.yaml"] + + @classmethod + async def task_init( + cls, task_name: str, config: SandboxEnvironmentConfigType | None + ) -> None: + await validate_prereqs() + # Sample contexts will be copied from the task context, so initialise the + # manager in the task context so that task_cleanup() accesses a manager which + # is tracking the releases for all of the task's samples. + HelmReleaseManager.get_instance() + + @classmethod + async def task_cleanup( + cls, task_name: str, config: SandboxEnvironmentConfigType | None, cleanup: bool + ) -> None: + # Uninstall any releases which were not uninstalled by sample_cleanup(). + await HelmReleaseManager.get_instance().uninstall_all(print_only=not cleanup) + + @classmethod + async def cli_cleanup(cls, id: str | None) -> None: + if id is None: + # TODO: How can we avoid uninstalling others' releases? Uninstall all + # releases in namespace regardless? + raise NotImplementedError( + "Cleanup all for k8s is not supported. Please specify a release name." + ) + await uninstall_unmanaged_release(id) + + @classmethod + async def sample_init( + cls, + task_name: str, + config: SandboxEnvironmentConfigType | None, + metadata: dict[str, str], + ) -> dict[str, SandboxEnvironment]: + async def get_sandboxes(release: Release) -> dict[str, SandboxEnvironment]: + pods = await release.get_sandbox_pods() + sandbox_envs: dict[str, SandboxEnvironment] = {} + for key, pod in pods.items(): + sandbox_envs[key] = cls(release, pod) + sandbox_log(f"Available sandboxes: {list(sandbox_envs.keys())}") + return sandbox_envs + + def reorder_default_first( + sandboxes: dict[str, SandboxEnvironment], + ) -> dict[str, SandboxEnvironment]: + # Inspect expects the default sandbox to be the first sandbox in the dict. + if "default" in sandboxes: + default = sandboxes.pop("default") + return {"default": default, **sandboxes} + return sandboxes + + release = _create_release(task_name, config) + await HelmReleaseManager.get_instance().install(release) + return reorder_default_first(await get_sandboxes(release)) + + @classmethod + async def sample_cleanup( + cls, + task_name: str, + config: SandboxEnvironmentConfigType | None, + environments: dict[str, SandboxEnvironment], + interrupted: bool, + ) -> None: + # If we were interrupted, wait unil the end of the task to cleanup (this enables + # us to show output for the cleanup operation). + if interrupted: + return + sandbox: K8sSandboxEnvironment = cast( + K8sSandboxEnvironment, next(iter(environments.values())) + ) + await HelmReleaseManager.get_instance().uninstall(sandbox.release, quiet=True) + + async def exec( + self, + cmd: list[str], + input: str | bytes | None = None, + cwd: str | None = None, + env: dict[str, str] = {}, + user: str | None = None, + timeout: int | None = None, + ) -> ExecResult[str]: + if user is not None: + raise NotImplementedError( + "The user parameter for exec() is not yet supported." + ) + log_kwargs = dict(cmd=cmd, stdin=input, cwd=cwd, env=env, timeout=timeout) + # Do not log these at error level or re-raise as enriched K8sError. + expected_exceptions = ( + TimeoutError, + UnicodeDecodeError, + PermissionError, + OutputLimitExceededError, + ) + with self._log_op( + "Execute command in pod.", expected_exceptions, **log_kwargs + ) as set_result: + result = await self._pod.exec(cmd, input, cwd, env, timeout) + set_result(result) + return result + + async def write_file(self, file: str, contents: str | bytes) -> None: + # Write contents to a temporary file on the client system and pass the file + # handle. + with tempfile.NamedTemporaryFile("w+b") as temp_file: + if isinstance(contents, str): + temp_file.write(contents.encode("utf-8")) + else: + temp_file.write(contents) + temp_file.seek(0) + # Do not log these at error level or re-raise as enriched K8sError. + expected_exceptions = (PermissionError, IsADirectoryError) + with self._log_op("Write file to pod.", expected_exceptions, file=file): + await self._pod.write_file(temp_file.file, Path(file)) + + @overload + async def read_file(self, file: str, text: Literal[True] = True) -> str: ... + + @overload + async def read_file(self, file: str, text: Literal[False]) -> bytes: ... + + async def read_file(self, file: str, text: bool = True) -> str | bytes: + # Create and open a temporary file on the client system which the file will be + # written to. + with tempfile.NamedTemporaryFile("w+b") as temp_file: + # Do not log these at error level or re-raise as enriched K8sError. + expected_exceptions = ( + FileNotFoundError, + UnicodeDecodeError, + PermissionError, + IsADirectoryError, + OutputLimitExceededError, + ) + with self._log_op("Read file from pod.", expected_exceptions, file=file): + await self._pod.read_file(Path(file), temp_file) + temp_file.seek(0) + return ( + temp_file.read() if not text else temp_file.read().decode("utf-8") + ) + + @contextmanager + def _log_op( + self, op: str, expected_exceptions: tuple, **log_kwargs + ) -> Generator[Callable[[ExecResult], None], None, None]: + """Logs the lifecycle of an operation and enriches unexpected exceptions. + + The pod name and task name are included all log messages in addition to + log_kwargs. + + For "expected" exceptions (e.g. TimeoutError), the exception is logged at + "SANDBOX" level and re-raised. + For "unexpected" exceptions (e.g. ApiException), the exception is logged at + "ERROR" level and re-raised as a K8sError which includes additional context for + debugging. + + Optionally, a result can be set by the caller for inclusion in the "completed" + log message. + """ + log_kwargs = dict( + pod=self._pod.info.name, task_name=self.release.task_name, **log_kwargs + ) + sandbox_log(f"Starting: {op}", **log_kwargs) + result_dict = dict() + try: + # Allow the caller to set the result of the operation for logging. + yield lambda x: result_dict.update({"result": x}) + except expected_exceptions as e: + sandbox_log(f"Error during: {op}", cause=e, **log_kwargs) + raise + except Exception as e: + sandbox_log( + f"Error during: {op}", level=logging.ERROR, cause=e, **log_kwargs + ) + # Enrich the unexpected exception with additional context. + raise K8sError(f"Error during: {op}", **log_kwargs) from e + sandbox_log(f"Completed: {op}", **{**result_dict, **log_kwargs}) + + +class K8sSandboxEnvironmentConfig(BaseModel, frozen=True): + """A config Pydantic model for the K8s sandbox environment.""" + + # In future, charts from Helm repositories may be supported, hence str over Path. + chart: str | None = None + values: Path | None = None + + +class K8sError(Exception): + """An error that occurred during a Kubernetes operation. + + This will typically cause the eval to fail. + """ + + def __init__(self, message: str, **kwargs: Any): + super().__init__(format_log_message(message, **kwargs)) + + +def _create_release( + task_name: str, config: SandboxEnvironmentConfigType | None +) -> Release: + def validate_values_file(values: Path | None) -> None: + if values is not None and not values.is_file(): + raise FileNotFoundError(f"Helm values file not found: '{values}'.") + + def validate_chart_dir(chart: Path | None) -> None: + if chart is not None and not chart.is_dir(): + raise NotADirectoryError( + f"Helm chart directory not found: '{chart}'. At present, only " + "charts from local directories are supported." + ) + + if config is None: + return Release(task_name) + if isinstance(config, K8sSandboxEnvironmentConfig): + chart = Path(config.chart).resolve() if config.chart else None + validate_chart_dir(chart) + values = config.values.resolve() if config.values else None + validate_values_file(values) + return Release(task_name, chart_path=chart, values_path=values) + if isinstance(config, str): + values = Path(config).resolve() + validate_values_file(values) + return Release(task_name, values_path=values) + raise TypeError(f"Invalid config type: {type(config)}.") diff --git a/src/k8s_sandbox/resources/helm/agent-env/Chart.yaml b/src/k8s_sandbox/resources/helm/agent-env/Chart.yaml new file mode 100644 index 0000000..e241814 --- /dev/null +++ b/src/k8s_sandbox/resources/helm/agent-env/Chart.yaml @@ -0,0 +1,3 @@ +apiVersion: v2 +name: agent-env +version: 0.10.0 diff --git a/src/k8s_sandbox/resources/helm/agent-env/README.md b/src/k8s_sandbox/resources/helm/agent-env/README.md new file mode 100644 index 0000000..d10dbbb --- /dev/null +++ b/src/k8s_sandbox/resources/helm/agent-env/README.md @@ -0,0 +1,38 @@ +# agent-env + +![Version: 0.10.0](https://img.shields.io/badge/Version-0.10.0-informational?style=flat-square) + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| additionalResources | list | `[]` | A list of additional resources to deploy within the agent environment. | +| allowCIDR | list | Empty list (no additional CIDR ranges compared to default policies) | A list of CIDR ranges (e.g. 1.1.1.1/32) that pods within the agent environment are allowed to access. | +| allowDomains | list | Empty list (no internet access) | A list of fully qualified domain names that pods within the agent environment are allowed to access. | +| allowEntities | list | Empty list (no additional entities compared to default policies) | A list of Cilium entities (e.g. "world") that pods within the agent environment are allowed to access. | +| annotations | object | `{}` | A dict of annotations to apply to resources within the agent environment. | +| global | object | set by inspect | The name of the agent environment, only overwrite in cases where e.g. name lengths are causing failures. | +| imagePullSecrets | list | `[]` | References to pre-existing secrets that contain registry credentials. | +| networks | object | `{}` | Defines network names that can be attached to services in order to specify subsets of services that can communicate with one another. | +| services | object | see [values.yaml](./values.yaml) | A collection of services to deploy within the agent environment. A service can connect to another service using DNS, e.g. `http://nginx:80`. | +| services.default | object | see [values.yaml](./values.yaml) | The default service, this is required for the agent environment to function. | +| services.default.additionalDnsRecords | list | `[]` | A list of additional domains which will resolve to this service from within the agent environment (e.g. example.com). If one or more records are provided, `dnsRecord` is automatically set to true. | +| services.default.args | list | `[]` | The container's entrypoint arguments. | +| services.default.command | list | `["tail","-f","/dev/null"]` | The container's entrypoint command. | +| services.default.dnsRecord | bool | false | Whether to create a DNS record which will resolve to this service from within the agent environment, using the service name as the domain (e.g. default). | +| services.default.env | list | `[]` | Environment variables that will be set in the container. | +| services.default.image | string | `"python:3.12-bookworm"` | The container's image name. | +| services.default.imagePullPolicy | string | `nil` | The container's image pull policy. | +| services.default.livenessProbe | object | `{}` | A probe which is used to determine when to restart a container. | +| services.default.nodeSelector | object | `{}` | Node selector settings for the Pod. | +| services.default.ports | list | `[]` | Deprecated. All ports of services with a DNS record are accessible (though not necessarily open) to other services within the agent environment. If one or more ports are provided, `dnsRecord` is automatically set to true. | +| services.default.readinessProbe | object | `{}` | A probe which is used to determine when the container is ready to accept. traffic. | +| services.default.resources | object | see [templates/services.yaml](./templates/services.yaml) | Resource requests and limits for the container. | +| services.default.runtimeClassName | string | `"gvisor"` | The container runtime e.g. gvisor or runc. The default is gvisor if not specified or set to `null`. | +| services.default.securityContext | object | `{}` | Privilege and access control settings for the container. | +| services.default.tolerations | list | `[]` | Toleration settings for the Pod. | +| services.default.volumeMounts | list | `[]` | Volume mounts that will be mounted in the container. Volumes defined in `volumes:` as colon-separated strings will automatically be mounted at their specified mount paths. | +| services.default.volumes | list | `[]` | Volumes accessible to the container. Supports arbitrary yaml or colon-separated strings of the form `volume-name:/mount-path`. | +| services.default.workingDir | string | `nil` | The container's working directory. | +| volumes | object | `{}` | A dict of volumes to deploy within the agent environment as NFS-CSI PersistentVolumeClaims. These volumes can be mounted in services using the `volumes:` field. The actual volume name will include the release name. | + diff --git a/src/k8s_sandbox/resources/helm/agent-env/generate-docs.md b/src/k8s_sandbox/resources/helm/agent-env/generate-docs.md new file mode 100644 index 0000000..82829f4 --- /dev/null +++ b/src/k8s_sandbox/resources/helm/agent-env/generate-docs.md @@ -0,0 +1,6 @@ +# Generate docs + +You can regenerate docs using [helm-docs](https://github.com/norwoodj/helm-docs) after +changing values.yaml. + +This is performed automatically by the a pre-commit hook. diff --git a/src/k8s_sandbox/resources/helm/agent-env/templates/additional-resources.yaml b/src/k8s_sandbox/resources/helm/agent-env/templates/additional-resources.yaml new file mode 100644 index 0000000..0f450f4 --- /dev/null +++ b/src/k8s_sandbox/resources/helm/agent-env/templates/additional-resources.yaml @@ -0,0 +1,6 @@ +{{- if .Values.additionalResources }} +{{- range .Values.additionalResources }} +{{ . | toYaml }} +--- +{{- end }} +{{- end }} diff --git a/src/k8s_sandbox/resources/helm/agent-env/templates/coredns.yaml b/src/k8s_sandbox/resources/helm/agent-env/templates/coredns.yaml new file mode 100644 index 0000000..f08bbff --- /dev/null +++ b/src/k8s_sandbox/resources/helm/agent-env/templates/coredns.yaml @@ -0,0 +1,42 @@ + +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ template "agentEnv.fullname" $ -}}-coredns-configmap +data: + Corefile: | + .:53 { + log + errors + ready + + # Add rewrite rules for each service which exposes ports. + + {{- range $name, $service := .Values.services }} + {{- if include "shouldCreateDnsRecord" $service }} + + # {{ $name }} + rewrite name exact {{ $name }} {{ template "agentEnv.fullname" $ -}}-{{ $name }}.{{ $.Release.Namespace }}.svc.cluster.local + # For backwards compatibility with $AGENT_ENV usage. + rewrite name exact {{ template "agentEnv.fullname" $ -}}-{{ $name }} {{ template "agentEnv.fullname" $ -}}-{{ $name }}.{{ $.Release.Namespace }}.svc.cluster.local + {{- range $host := $service.additionalDnsRecords }} + rewrite name exact {{ $host }} {{ template "agentEnv.fullname" $ -}}-{{ $name }}.{{ $.Release.Namespace }}.svc.cluster.local + {{- end }} + {{- end }} + {{- end }} + + # Forward all queries to the Kubernetes cluster DNS server. + forward . /etc/resolv.conf + + # Only process DNS requests from localhost as we're deployed as a sidecar. + # This prevents our port 53 from appearing open to other Pods. + bind 127.0.0.1 + } +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ template "agentEnv.fullname" $ -}}-resolv-conf +data: + resolv.conf: | + nameserver 127.0.0.1 diff --git a/src/k8s_sandbox/resources/helm/agent-env/templates/helpers/_helpers.tpl b/src/k8s_sandbox/resources/helm/agent-env/templates/helpers/_helpers.tpl new file mode 100644 index 0000000..6cf7539 --- /dev/null +++ b/src/k8s_sandbox/resources/helm/agent-env/templates/helpers/_helpers.tpl @@ -0,0 +1,63 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "agentEnv.name" -}} +{{- default .Chart.Name .Values.global.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "agentEnv.fullname" -}} +{{- if .Values.global.fullnameOverride -}} +{{- .Values.global.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.global.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" $name .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "agentEnv.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Common labels +*/}} +{{- define "agentEnv.labels" -}} +helm.sh/chart: {{ include "agentEnv.chart" . }} +{{ include "agentEnv.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end -}} + +{{/* +Selector labels +*/}} +{{- define "agentEnv.selectorLabels" -}} +app.kubernetes.io/name: {{ include "agentEnv.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end -}} + +{{/* +Whether to create a ClusterIP type Service (which implies a DNS record) for a service +*/}} +{{- define "shouldCreateDnsRecord" -}} +{{- $service := . -}} +{{- if or $service.dnsRecord $service.additionalDnsRecords $service.ports -}} +true +{{- else -}} +{{- /* An empty value represents false */ -}} +{{- end -}} +{{- end -}} diff --git a/src/k8s_sandbox/resources/helm/agent-env/templates/network-policy.yaml b/src/k8s_sandbox/resources/helm/agent-env/templates/network-policy.yaml new file mode 100644 index 0000000..0fd9b0b --- /dev/null +++ b/src/k8s_sandbox/resources/helm/agent-env/templates/network-policy.yaml @@ -0,0 +1,103 @@ +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: {{ template "agentEnv.fullname" $ -}}-sandbox-egress + annotations: + {{- toYaml $.Values.annotations | nindent 4 }} +spec: + description: | + Allow egress only to cluster-wide DNS, everything in the sandbox, and any allowDomains. + endpointSelector: + matchLabels: + io.kubernetes.pod.namespace: {{ .Release.Namespace }} + {{- include "agentEnv.selectorLabels" $ | nindent 6 }} + egress: + - toEndpoints: + - matchLabels: + io.kubernetes.pod.namespace: kube-system + k8s-app: kube-dns + toPorts: + - ports: + - port: "53" + protocol: ANY + rules: + dns: + - matchPattern: "*" + - toEndpoints: + - matchLabels: + io.kubernetes.pod.namespace: {{ .Release.Namespace }} + {{- include "agentEnv.selectorLabels" $ | nindent 10 }} + {{- if .Values.allowDomains }} + - toFQDNs: + {{- range .Values.allowDomains }} + - matchPattern: "{{ . }}" + {{- end }} + {{- end }} + {{- with .Values.allowEntities }} + - toEntities: + {{- toYaml . | nindent 6 }} + {{- end }} + {{- with .Values.allowCIDR }} + - toCIDR: + {{- toYaml . | nindent 6 }} + {{- end }} +--- +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: {{ template "agentEnv.fullname" $ -}}-sandbox-default-deny-ingress + annotations: + {{- toYaml $.Values.annotations | nindent 4 }} +spec: + description: Default deny ingress. Allow other policies to be more specific. + endpointSelector: + matchLabels: + io.kubernetes.pod.namespace: {{ .Release.Namespace }} + {{- include "agentEnv.selectorLabels" $ | nindent 6 }} + ingress: + - {} +{{- if .Values.networks }} +{{- range $networkName, $networkConfig := .Values.networks }} +--- +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: {{ template "agentEnv.fullname" $ }}-sandbox-{{ $networkName }}-ingress + annotations: + {{- toYaml $.Values.annotations | nindent 4 }} +spec: + description: | + Allow ingress from other pods in the "{{ $networkName }}" agent sandbox "network". + endpointSelector: + matchLabels: + io.kubernetes.pod.namespace: {{ $.Release.Namespace }} + {{- include "agentEnv.selectorLabels" $ | nindent 6 }} + aisi.gov.uk/network-{{ $networkName }}: "true" + ingress: + - fromEndpoints: + - matchLabels: + io.kubernetes.pod.namespace: {{ $.Release.Namespace }} + aisi.gov.uk/network-{{ $networkName }}: "true" + {{- include "agentEnv.selectorLabels" $ | nindent 10 }} +{{- end }} +{{- else }} +--- +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: {{ template "agentEnv.fullname" $ }}-sandbox-ingress + annotations: + {{- toYaml $.Values.annotations | nindent 4 }} +spec: + description: | + Allow ingress from other pods in the same agent sandbox. + endpointSelector: + matchLabels: + io.kubernetes.pod.namespace: {{ .Release.Namespace }} + {{- include "agentEnv.selectorLabels" $ | nindent 6 }} + ingress: + - fromEndpoints: + - matchLabels: + io.kubernetes.pod.namespace: {{ .Release.Namespace }} + {{- include "agentEnv.selectorLabels" $ | nindent 10 }} +{{- end }} diff --git a/src/k8s_sandbox/resources/helm/agent-env/templates/pvc.yaml b/src/k8s_sandbox/resources/helm/agent-env/templates/pvc.yaml new file mode 100644 index 0000000..70ee828 --- /dev/null +++ b/src/k8s_sandbox/resources/helm/agent-env/templates/pvc.yaml @@ -0,0 +1,21 @@ +{{- range $name, $volume := .Values.volumes }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: "{{ template "agentEnv.fullname" $ -}}-{{ $name }}" + annotations: + {{- toYaml $.Values.annotations | nindent 4 }} +spec: + {{- if and $volume $volume.spec }} + {{- toYaml $volume.spec | nindent 2 }} + {{- else }} + storageClassName: nfs-csi + accessModes: + - ReadWriteMany + resources: + requests: + # The storage request is required, but is not used. + storage: 1Ki + {{- end }} +--- +{{- end }} diff --git a/src/k8s_sandbox/resources/helm/agent-env/templates/services.yaml b/src/k8s_sandbox/resources/helm/agent-env/templates/services.yaml new file mode 100644 index 0000000..c65f6ca --- /dev/null +++ b/src/k8s_sandbox/resources/helm/agent-env/templates/services.yaml @@ -0,0 +1,183 @@ +{{- range $name, $service := .Values.services }} +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ template "agentEnv.fullname" $ -}}-{{ $name }} + labels: + {{- include "agentEnv.labels" $ | nindent 4 }} + inspect/service: {{ $name }} + annotations: + {{- toYaml $.Values.annotations | nindent 4 }} +spec: + serviceName: {{ $name }}-service + replicas: 1 + selector: + matchLabels: + {{- include "agentEnv.selectorLabels" $ | nindent 6 }} + inspect/service: {{ $name }} + template: + metadata: + labels: + {{- include "agentEnv.selectorLabels" $ | nindent 8 }} + {{- if $service.networks }} + {{- range $service.networks }} + aisi.gov.uk/network-{{ . }}: "true" + {{- end }} + {{- end }} + inspect/service: {{ $name }} + annotations: + {{- toYaml $.Values.annotations | nindent 8 }} + spec: + runtimeClassName: {{ $service.runtimeClassName | default "gvisor" }} + {{- /* Do not leak info on services via env vars */}} + enableServiceLinks: false + terminationGracePeriodSeconds: 0 + containers: + - name: {{ $name }} + image: {{ $service.image }} + {{- if $service.imagePullPolicy }} + imagePullPolicy: {{ $service.imagePullPolicy }} + {{- end }} + {{- if $service.command }} + command: + {{- toYaml $service.command | nindent 10 }} + {{- end }} + {{- if $service.args }} + args: {{ $service.args }} + {{- end }} + {{- if $service.workingDir }} + workingDir: {{ $service.workingDir }} + {{- end }} + env: + # Retained for service resolution backwards compatibility. + - name: AGENT_ENV + value: "{{ template "agentEnv.fullname" $ -}}" + {{- if $service.env }} + {{ range $service.env }} + - name: {{ .name }} + value: {{ .value | quote }} + {{- end }} + {{- end }} + {{- with $service.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 10 }} + {{- end }} + {{- with $service.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 10 }} + {{- end }} + resources: + {{- if $service.resources }} + {{- toYaml $service.resources | nindent 10 }} + {{- else }} + # Equal limits and requests for Guaranteed QoS class. + limits: + memory: "2Gi" + cpu: "500m" + requests: + memory: "2Gi" + cpu: "500m" + {{- end }} + volumeMounts: + {{- with $service.volumeMounts }} + {{- toYaml . | nindent 10 }} + {{- end }} + {{- range $key, $value := $service.volumes }} + {{- if kindIs "string" $value }} + {{- $parts := split ":" $value }} + {{- $volumeName := $parts._0 }} + {{- $mountPath := $parts._1 }} + - mountPath: {{ $mountPath | quote }} + name: {{ template "agentEnv.fullname" $ -}}-{{ $volumeName }} + {{- end }} + {{- end }} + - name: resolv-conf + mountPath: /etc/resolv.conf + subPath: resolv.conf + {{- with $service.securityContext }} + securityContext: + {{- toYaml . | nindent 10 }} + {{- end }} + - name: coredns + image: coredns/coredns:1.8.3 + command: + - /coredns + - -conf + - /etc/coredns/Corefile + resources: + requests: + memory: "64Mi" + cpu: "50m" + limits: + memory: "128Mi" + cpu: "100m" + ports: + - containerPort: 53 + protocol: UDP + name: dns + - containerPort: 53 + protocol: TCP + name: dns-tcp + volumeMounts: + - name: coredns-config + mountPath: /etc/coredns/Corefile + subPath: Corefile + {{- with $service.tolerations }} + tolerations: + {{- toYaml . | nindent 10 }} + {{- end }} + {{- with $service.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 10 }} + {{- end }} + volumes: + - name: coredns-config + configMap: + name: {{ template "agentEnv.fullname" $ -}}-coredns-configmap + - name: resolv-conf + configMap: + name: {{ template "agentEnv.fullname" $ -}}-resolv-conf + {{- range $key, $value := $service.volumes }} + {{- if kindIs "string" $value }} + {{- $parts := split ":" $value }} + {{- $volumeName := $parts._0 }} + - name: {{ template "agentEnv.fullname" $ -}}-{{ $volumeName }} + persistentVolumeClaim: + claimName: {{ template "agentEnv.fullname" $ -}}-{{ $volumeName }} + {{- else }} + - {{- toYaml . | nindent 10 }} + {{- end }} + {{- end }} + {{- with $.Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} +--- +{{- if include "shouldCreateDnsRecord" $service }} +apiVersion: v1 +kind: Service +metadata: + name: {{ template "agentEnv.fullname" $ -}}-{{ $name }} + annotations: + {{- toYaml $.Values.annotations | nindent 4 }} +spec: + # Make this a headless service: other pods in this agent-env may connect directly to + # the pod on all ports. This enables the use of tools like `nc` on any port or `ping` + # without Cilium blocking the traffic. + clusterIP: None + selector: + {{- include "agentEnv.selectorLabels" $ | nindent 4 }} + inspect/service: {{ $name }} + {{- if $service.ports }} + # ports are deprecated within this Helm chart. Maintained for backwards compatibility. + ports: + {{- range $service.ports }} + - protocol: {{ .protocol }} + port: {{ .port }} + {{- /* A name is required when multiple ports are exposed. */}} + name: port-{{ .port }} + {{- end }} + {{- end }} +--- +{{- end }} +{{- end }} diff --git a/src/k8s_sandbox/resources/helm/agent-env/values.schema.json b/src/k8s_sandbox/resources/helm/agent-env/values.schema.json new file mode 100644 index 0000000..c658b7e --- /dev/null +++ b/src/k8s_sandbox/resources/helm/agent-env/values.schema.json @@ -0,0 +1,178 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "global": { + "type": "object" + }, + "allowDomains": { + "type": "array", + "items": { + "type": "string" + } + }, + "allowEntities": { + "type": "array", + "items": { + "type": "string" + } + }, + "allowCIDR": { + "type": "array", + "items": { + "type": "string" + } + }, + "networks": { + "type": "object" + }, + "imagePullSecrets": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + }, + "services": { + "type": "object", + "patternProperties": { + ".*": { + "type": "object", + "properties": { + "networks": { + "type": "array", + "items": { + "type": "string" + } + }, + "runtimeClassName": { + "type": "string" + }, + "image": { + "type": "string" + }, + "imagePullPolicy": { + "type": [ + "string", + "null" + ] + }, + "additionalDnsRecords": { + "type": "array", + "items": { + "type": "string" + } + }, + "ports": { + "type": "array", + "items": { + "type": "object", + "properties": { + "protocol": { + "type": "string" + }, + "port": { + "type": "integer" + } + }, + "required": [ + "protocol", + "port" + ] + } + }, + "dnsRecord": { + "type": "boolean" + }, + "command": { + "type": "array" + }, + "args": { + "type": "array" + }, + "workingDir": { + "type": [ + "string", + "null" + ] + }, + "readinessProbe": { + "type": "object" + }, + "livenessProbe": { + "type": "object" + }, + "env": { + "type": "array" + }, + "resources": { + "type": "object" + }, + "securityContext": { + "type": "object" + }, + "volumeMounts": { + "type": "array" + }, + "volumes": { + "type": "array" + }, + "tolerations": { + "type": "array" + }, + "nodeSelector": { + "type": "object" + } + }, + "required": [ + "image" + ], + "additionalProperties": false + }, + "additionalProperties": false + }, + "required": [ + "default" + ] + }, + "volumes": { + "type": "object", + "patternProperties": { + ".*": { + "type": [ + "object", + "null" + ], + "properties": { + "spec": { + "type": "object" + } + }, + "additionalProperties": false + }, + "additionalProperties": false + } + }, + "additionalResources": { + "type": "array", + "items": { + "type": "object" + } + }, + "annotations": { + "type": "object" + } + }, + "required": [ + "global", + "services" + ], + "additionalProperties": false +} diff --git a/src/k8s_sandbox/resources/helm/agent-env/values.yaml b/src/k8s_sandbox/resources/helm/agent-env/values.yaml new file mode 100644 index 0000000..e028085 --- /dev/null +++ b/src/k8s_sandbox/resources/helm/agent-env/values.yaml @@ -0,0 +1,127 @@ +# -- The name of the agent environment, only overwrite in cases where e.g. name lengths +# are causing failures. +# @default -- set by inspect +global: + nameOverride: "" + fullnameOverride: "" +# -- A list of fully qualified domain names that pods within the agent environment are +# allowed to access. +# @default -- Empty list (no internet access) +allowDomains: [] +# - "pypi.org" +# - "files.pythonhosted.org" +# -- A list of CIDR ranges (e.g. 1.1.1.1/32) that pods within the agent environment are +# allowed to access. +# @default -- Empty list (no additional CIDR ranges compared to default policies) +allowCIDR: [] +# -- A list of Cilium entities (e.g. "world") that pods within the agent environment are +# allowed to access. +# @default -- Empty list (no additional entities compared to default policies) +allowEntities: [] +# -- References to pre-existing secrets that contain registry credentials. +imagePullSecrets: [] +# - name: "gcr-json-key" +# -- Defines network names that can be attached to services in order to specify subsets +# of services that can communicate with one another. +networks: {} +# -- A collection of services to deploy within the agent environment. A service can +# connect to another service using DNS, e.g. `http://nginx:80`. +# @default -- see [values.yaml](./values.yaml) +services: + # -- The default service, this is required for the agent environment to function. + # @default -- see [values.yaml](./values.yaml) + default: + # -- The container runtime e.g. gvisor or runc. The default is gvisor if not + # specified or set to `null`. + runtimeClassName: gvisor + # -- The container's image name. + image: "python:3.12-bookworm" + # -- The container's entrypoint command. + command: ["tail", "-f", "/dev/null"] + # -- The container's entrypoint arguments. + args: [] + # -- The container's working directory. + workingDir: null + # -- Whether to create a DNS record which will resolve to this service from within + # the agent environment, using the service name as the domain (e.g. default). + # @default -- false + dnsRecord: false + # -- A list of additional domains which will resolve to this service from within the + # agent environment (e.g. example.com). If one or more records are provided, + # `dnsRecord` is automatically set to true. + additionalDnsRecords: [] + # -- Deprecated. All ports of services with a DNS record are accessible (though not + # necessarily open) to other services within the agent environment. If one or more + # ports are provided, `dnsRecord` is automatically set to true. + ports: [] + # -- Environment variables that will be set in the container. + env: [] + # -- Volumes accessible to the container. Supports arbitrary yaml or colon-separated + # strings of the form `volume-name:/mount-path`. + volumes: [] + # -- Volume mounts that will be mounted in the container. Volumes defined in + # `volumes:` as colon-separated strings will automatically be mounted at their + # specified mount paths. + volumeMounts: [] + # -- Resource requests and limits for the container. + # @default -- see [templates/services.yaml](./templates/services.yaml) + resources: {} + # -- A probe which is used to determine when the container is ready to accept. + # traffic. + readinessProbe: {} + # -- A probe which is used to determine when to restart a container. + livenessProbe: {} + # -- The container's image pull policy. + imagePullPolicy: null + # -- Privilege and access control settings for the container. + securityContext: {} + # -- Toleration settings for the Pod. + tolerations: [] + # -- Node selector settings for the Pod. + nodeSelector: {} + # nginx: + # runtimeClassName: gvisor + # image: "nginx" + # dnsRecord: true + # additionalDnsRecords: + # - "nginx.com" + # - "my-fake-domain.org" + # env: + # - name: "SOME_ENV_VAR" + # value: "some-value" + # resources: + # requests: + # memory: "64Mi" + # cpu: "250m" + # limits: + # memory: "128Mi" + # cpu: "500m" + # readinessProbe: + # tcpSocket: + # port: 80 + # initialDelaySeconds: 5 + # periodSeconds: 5 + # livenessProbe: + # tcpSocket: + # port: 80 + # initialDelaySeconds: 5 + # periodSeconds: 5 + # volumeMounts: + # - mountPath: /mypath + # name: custom-volume + # volumes: + # - name: custom-volume + # emptyDir: {} + # - "shared-volume:/mount-path" + # securityContext: + # allowPrivilegeEscalation: false +# -- A dict of volumes to deploy within the agent environment as NFS-CSI +# PersistentVolumeClaims. These volumes can be mounted in services using the `volumes:` +# field. The actual volume name will include the release name. +volumes: {} +# shared-volume: +# -- A list of additional resources to deploy within the agent environment. +additionalResources: [] +# -- A dict of annotations to apply to resources within the agent environment. +annotations: {} + # inspectTaskName: "task-name" From 8960531941f11ef042f293caa0717d652d17d4d6 Mon Sep 17 00:00:00 2001 From: "Craig.Walton" Date: Tue, 17 Dec 2024 11:56:20 +0000 Subject: [PATCH 3/7] Initial migration of test/k8s_sandbox from internal repo. --- test/k8s_sandbox/__init__.py | 0 test/k8s_sandbox/conftest.py | 9 + .../additional-resources-values.yaml | 15 + .../resources/dns-record-values.yaml | 16 + .../resources/env-types-values.yaml | 10 + .../resources/multiple-ports-values.yaml | 8 + .../resources/multiple-services-values.yaml | 3 + .../helm_chart/resources/volumes-values.yaml | 30 + .../k8s_sandbox/helm_chart/test_helm_chart.py | 194 +++++ .../inspect_integration/__init__.py | 0 .../custom_chart/__init__.py | 0 .../custom_chart/my-custom-chart/Chart.yaml | 3 + .../my-custom-chart/templates/template.yaml | 19 + .../custom_chart/test_integration.py | 58 ++ .../custom_chart/values.yaml | 1 + .../inferred_values/__init__.py | 0 .../inferred_values/test_integration.py | 24 + .../inferred_values/values.yaml | 3 + .../multiple_sandbox_envs/__init__.py | 0 .../multiple_sandbox_envs/test_integration.py | 46 ++ .../multiple_sandbox_envs/values.yaml | 7 + .../sandbox_env_ordering/__init__.py | 0 .../sandbox_env_ordering/test_integration.py | 24 + .../sandbox_env_ordering/values.yaml | 16 + .../inspect_integration/test_cleanup.py | 43 ++ .../test_default_values.py | 23 + .../testing_utils/__init__.py | 0 .../testing_utils/mock_model.py | 51 ++ .../testing_utils/utils.py | 103 +++ .../inspect_integration/values/__init__.py | 0 .../inspect_integration/values/my-values.yaml | 5 + .../values/test_integration.py | 41 + .../inspect_integration/values/values.yaml | 5 + test/k8s_sandbox/pod/test_executor.py | 73 ++ test/k8s_sandbox/pod/test_get_returncode.py | 122 +++ test/k8s_sandbox/pod/test_pod.py | 59 ++ test/k8s_sandbox/resources/dns-values.yaml | 46 ++ test/k8s_sandbox/resources/netpol-values.yaml | 16 + .../resources/netpol-world-values.yaml | 11 + .../resources/networks-values.yaml | 54 ++ .../resources/runtime-class-values.yaml | 41 + test/k8s_sandbox/resources/values.yaml | 35 + test/k8s_sandbox/resources/volume-values.yaml | 27 + test/k8s_sandbox/test_config_validation.py | 35 + test/k8s_sandbox/test_dns.py | 73 ++ test/k8s_sandbox/test_helm.py | 54 ++ test/k8s_sandbox/test_inspect_self_check.py | 63 ++ .../k8s_sandbox/test_limited_stream_buffer.py | 97 +++ test/k8s_sandbox/test_logger.py | 78 ++ test/k8s_sandbox/test_network_policy.py | 68 ++ test/k8s_sandbox/test_networks.py | 39 + test/k8s_sandbox/test_prereqs.py | 18 + test/k8s_sandbox/test_runtime_class.py | 49 ++ test/k8s_sandbox/test_sandbox.py | 721 ++++++++++++++++++ test/k8s_sandbox/test_volume.py | 22 + test/k8s_sandbox/utils.py | 28 + 56 files changed, 2586 insertions(+) create mode 100644 test/k8s_sandbox/__init__.py create mode 100644 test/k8s_sandbox/conftest.py create mode 100644 test/k8s_sandbox/helm_chart/resources/additional-resources-values.yaml create mode 100644 test/k8s_sandbox/helm_chart/resources/dns-record-values.yaml create mode 100644 test/k8s_sandbox/helm_chart/resources/env-types-values.yaml create mode 100644 test/k8s_sandbox/helm_chart/resources/multiple-ports-values.yaml create mode 100644 test/k8s_sandbox/helm_chart/resources/multiple-services-values.yaml create mode 100644 test/k8s_sandbox/helm_chart/resources/volumes-values.yaml create mode 100644 test/k8s_sandbox/helm_chart/test_helm_chart.py create mode 100644 test/k8s_sandbox/inspect_integration/__init__.py create mode 100644 test/k8s_sandbox/inspect_integration/custom_chart/__init__.py create mode 100644 test/k8s_sandbox/inspect_integration/custom_chart/my-custom-chart/Chart.yaml create mode 100644 test/k8s_sandbox/inspect_integration/custom_chart/my-custom-chart/templates/template.yaml create mode 100644 test/k8s_sandbox/inspect_integration/custom_chart/test_integration.py create mode 100644 test/k8s_sandbox/inspect_integration/custom_chart/values.yaml create mode 100644 test/k8s_sandbox/inspect_integration/inferred_values/__init__.py create mode 100644 test/k8s_sandbox/inspect_integration/inferred_values/test_integration.py create mode 100644 test/k8s_sandbox/inspect_integration/inferred_values/values.yaml create mode 100644 test/k8s_sandbox/inspect_integration/multiple_sandbox_envs/__init__.py create mode 100644 test/k8s_sandbox/inspect_integration/multiple_sandbox_envs/test_integration.py create mode 100644 test/k8s_sandbox/inspect_integration/multiple_sandbox_envs/values.yaml create mode 100644 test/k8s_sandbox/inspect_integration/sandbox_env_ordering/__init__.py create mode 100644 test/k8s_sandbox/inspect_integration/sandbox_env_ordering/test_integration.py create mode 100644 test/k8s_sandbox/inspect_integration/sandbox_env_ordering/values.yaml create mode 100644 test/k8s_sandbox/inspect_integration/test_cleanup.py create mode 100644 test/k8s_sandbox/inspect_integration/test_default_values.py create mode 100644 test/k8s_sandbox/inspect_integration/testing_utils/__init__.py create mode 100644 test/k8s_sandbox/inspect_integration/testing_utils/mock_model.py create mode 100644 test/k8s_sandbox/inspect_integration/testing_utils/utils.py create mode 100644 test/k8s_sandbox/inspect_integration/values/__init__.py create mode 100644 test/k8s_sandbox/inspect_integration/values/my-values.yaml create mode 100644 test/k8s_sandbox/inspect_integration/values/test_integration.py create mode 100644 test/k8s_sandbox/inspect_integration/values/values.yaml create mode 100644 test/k8s_sandbox/pod/test_executor.py create mode 100644 test/k8s_sandbox/pod/test_get_returncode.py create mode 100644 test/k8s_sandbox/pod/test_pod.py create mode 100644 test/k8s_sandbox/resources/dns-values.yaml create mode 100644 test/k8s_sandbox/resources/netpol-values.yaml create mode 100644 test/k8s_sandbox/resources/netpol-world-values.yaml create mode 100644 test/k8s_sandbox/resources/networks-values.yaml create mode 100644 test/k8s_sandbox/resources/runtime-class-values.yaml create mode 100644 test/k8s_sandbox/resources/values.yaml create mode 100644 test/k8s_sandbox/resources/volume-values.yaml create mode 100644 test/k8s_sandbox/test_config_validation.py create mode 100644 test/k8s_sandbox/test_dns.py create mode 100644 test/k8s_sandbox/test_helm.py create mode 100644 test/k8s_sandbox/test_inspect_self_check.py create mode 100644 test/k8s_sandbox/test_limited_stream_buffer.py create mode 100644 test/k8s_sandbox/test_logger.py create mode 100644 test/k8s_sandbox/test_network_policy.py create mode 100644 test/k8s_sandbox/test_networks.py create mode 100644 test/k8s_sandbox/test_prereqs.py create mode 100644 test/k8s_sandbox/test_runtime_class.py create mode 100644 test/k8s_sandbox/test_sandbox.py create mode 100644 test/k8s_sandbox/test_volume.py create mode 100644 test/k8s_sandbox/utils.py diff --git a/test/k8s_sandbox/__init__.py b/test/k8s_sandbox/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/k8s_sandbox/conftest.py b/test/k8s_sandbox/conftest.py new file mode 100644 index 0000000..5ba7a2a --- /dev/null +++ b/test/k8s_sandbox/conftest.py @@ -0,0 +1,9 @@ +import logging + +from inspect_ai._util.constants import SANDBOX + + +def pytest_configure(config): + # Set the log level to SANDBOX for the tests in this directory. + # This lets us see log messages when used with pytest -s. + logging.basicConfig(level=SANDBOX) diff --git a/test/k8s_sandbox/helm_chart/resources/additional-resources-values.yaml b/test/k8s_sandbox/helm_chart/resources/additional-resources-values.yaml new file mode 100644 index 0000000..ee3a617 --- /dev/null +++ b/test/k8s_sandbox/helm_chart/resources/additional-resources-values.yaml @@ -0,0 +1,15 @@ +additionalResources: +- apiVersion: v1 + kind: Secret + metadata: + name: my-first-secret + type: Opaque + data: + password: mypassword +- apiVersion: v1 + kind: Secret + metadata: + name: my-second-secret + type: Opaque + data: + password: mypassword diff --git a/test/k8s_sandbox/helm_chart/resources/dns-record-values.yaml b/test/k8s_sandbox/helm_chart/resources/dns-record-values.yaml new file mode 100644 index 0000000..c5477d6 --- /dev/null +++ b/test/k8s_sandbox/helm_chart/resources/dns-record-values.yaml @@ -0,0 +1,16 @@ +services: + a: + image: "nginx" + b: + image: "nginx" + dnsRecord: true + c: + image: "nginx" + additionalDnsRecords: + - "nginx.com" + - "my-fake-domain.org" + d: + image: "nginx" + ports: + - protocol: TCP + port: 80 diff --git a/test/k8s_sandbox/helm_chart/resources/env-types-values.yaml b/test/k8s_sandbox/helm_chart/resources/env-types-values.yaml new file mode 100644 index 0000000..8d90d25 --- /dev/null +++ b/test/k8s_sandbox/helm_chart/resources/env-types-values.yaml @@ -0,0 +1,10 @@ +services: + default: + image: "nginx" + env: + - name: "A" + value: 1 + - name: "B" + value: "2" + - name: "C" + value: "three" diff --git a/test/k8s_sandbox/helm_chart/resources/multiple-ports-values.yaml b/test/k8s_sandbox/helm_chart/resources/multiple-ports-values.yaml new file mode 100644 index 0000000..f1e7ddc --- /dev/null +++ b/test/k8s_sandbox/helm_chart/resources/multiple-ports-values.yaml @@ -0,0 +1,8 @@ +services: + default: + image: "python:3.12-bookworm" + ports: + - protocol: TCP + port: 80 + - protocol: TCP + port: 81 diff --git a/test/k8s_sandbox/helm_chart/resources/multiple-services-values.yaml b/test/k8s_sandbox/helm_chart/resources/multiple-services-values.yaml new file mode 100644 index 0000000..ef00450 --- /dev/null +++ b/test/k8s_sandbox/helm_chart/resources/multiple-services-values.yaml @@ -0,0 +1,3 @@ +services: + server: + image: "nginx" diff --git a/test/k8s_sandbox/helm_chart/resources/volumes-values.yaml b/test/k8s_sandbox/helm_chart/resources/volumes-values.yaml new file mode 100644 index 0000000..3e06d7e --- /dev/null +++ b/test/k8s_sandbox/helm_chart/resources/volumes-values.yaml @@ -0,0 +1,30 @@ +services: + default: + image: "python:3.12-bookworm" + volumeMounts: + - mountPath: /manual-volume-mount-path + name: manual-volume + volumes: + - name: manual-volume + emptyDir: {} + - "simple-volume-1:/simple-volume-mount-path" + other: + image: "python:3.12-bookworm" + volumeMounts: + - mountPath: /manual-volume-mount-path + name: manual-volume + volumes: + - name: manual-volume + emptyDir: {} + - "simple-volume-1:/simple-volume-mount-path" +volumes: + simple-volume-1: + simple-volume-2: + custom-volume: + spec: + storageClassName: nfs-client + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 42Mi diff --git a/test/k8s_sandbox/helm_chart/test_helm_chart.py b/test/k8s_sandbox/helm_chart/test_helm_chart.py new file mode 100644 index 0000000..02803c2 --- /dev/null +++ b/test/k8s_sandbox/helm_chart/test_helm_chart.py @@ -0,0 +1,194 @@ +import subprocess +from pathlib import Path +from typing import Any + +import pytest +import yaml + +import aisitools.k8s_sandbox + + +@pytest.fixture +def chart_dir() -> Path: + k8s_src = Path(aisitools.k8s_sandbox.__file__).parent.resolve() + return k8s_src / "resources" / "helm" / "agent-env" + + +@pytest.fixture +def test_resources_dir() -> Path: + return Path(__file__).parent.resolve() / "resources" + + +def test_default_chart(chart_dir: Path) -> None: + documents = _run_helm_template(chart_dir) + + services = _get_documents(documents, "StatefulSet") + assert services[0]["metadata"]["name"] == "agent-env-my-release-default" + assert ( + services[0]["spec"]["template"]["spec"]["containers"][0]["image"] + == "python:3.12-bookworm" + ) + + +def test_additional_resources(chart_dir: Path, test_resources_dir: Path) -> None: + documents = _run_helm_template( + chart_dir, test_resources_dir / "additional-resources-values.yaml" + ) + + secrets = _get_documents(documents, "Secret") + assert secrets[0]["metadata"]["name"] == "my-first-secret" + assert secrets[1]["metadata"]["name"] == "my-second-secret" + + +def test_multiple_ports(chart_dir: Path, test_resources_dir: Path) -> None: + documents = _run_helm_template( + chart_dir, test_resources_dir / "multiple-ports-values.yaml" + ) + + services = _get_documents(documents, "Service") + service = next( + service for service in services if "coredns" not in service["metadata"]["name"] + ) + # When multiple ports are defined, each port must have a name or helm install fails. + assert service["spec"]["ports"] == [ + {"name": "port-80", "port": 80, "protocol": "TCP"}, + {"name": "port-81", "port": 81, "protocol": "TCP"}, + ] + + +def test_volumes(chart_dir: Path, test_resources_dir: Path) -> None: + documents = _run_helm_template( + chart_dir, test_resources_dir / "volumes-values.yaml" + ) + + # Verify PVCs. + pvcs = _get_documents(documents, "PersistentVolumeClaim") + assert len(pvcs) == 3 + assert pvcs[0]["metadata"]["name"] == "agent-env-my-release-custom-volume" + assert pvcs[0]["spec"]["resources"]["requests"]["storage"] == "42Mi" + assert pvcs[1]["metadata"]["name"] == "agent-env-my-release-simple-volume-1" + assert pvcs[2]["metadata"]["name"] == "agent-env-my-release-simple-volume-2" + # Verify StatefulSet volume and volumeMounts. + expected_volume_mounts = yaml.safe_load(""" +- mountPath: /manual-volume-mount-path + name: manual-volume +- mountPath: /simple-volume-mount-path + name: agent-env-my-release-simple-volume-1 +- mountPath: /etc/resolv.conf + name: resolv-conf + subPath: resolv.conf +""") + expected_volumes = yaml.safe_load(""" +- name: coredns-config + configMap: + name: agent-env-my-release-coredns-configmap +- name: resolv-conf + configMap: + name: agent-env-my-release-resolv-conf +- emptyDir: {} + name: manual-volume +- name: agent-env-my-release-simple-volume-1 + persistentVolumeClaim: + claimName: agent-env-my-release-simple-volume-1 +""") + services = _get_documents(documents, "StatefulSet") + assert len(services) == 2 + for service in services: + template_spec = service["spec"]["template"]["spec"] + assert template_spec["containers"][0]["volumeMounts"] == expected_volume_mounts + assert template_spec["volumes"] == expected_volumes + + +def test_annotations(chart_dir: Path, test_resources_dir: Path) -> None: + attr_value = "my=!:. '\"value" + + documents = _run_helm_template( + chart_dir, + test_resources_dir / "volumes-values.yaml", + f"annotations.myValue={attr_value}", + ) + + for stateful_set in _get_documents(documents, "StatefulSet"): + assert stateful_set["metadata"]["annotations"]["myValue"] == attr_value + template = stateful_set["spec"]["template"] + assert template["metadata"]["annotations"]["myValue"] == attr_value + for network_policy in _get_documents(documents, "NetworkPolicy"): + assert network_policy["metadata"]["annotations"]["myValue"] == attr_value + for pvc in _get_documents(documents, "PersistentVolumeClaim"): + assert pvc["metadata"]["annotations"]["myValue"] == attr_value + for service in _get_documents(documents, "Service"): + assert service["metadata"]["annotations"]["myValue"] == attr_value + for deployment in _get_documents(documents, "Deployment"): + assert deployment["metadata"]["annotations"]["myValue"] == attr_value + + +def test_resource_requests_and_limits( + chart_dir: Path, test_resources_dir: Path +) -> None: + documents = _run_helm_template( + chart_dir, test_resources_dir / "multiple-services-values.yaml" + ) + + stateful_sets = _get_documents(documents, "StatefulSet") + assert len(stateful_sets) == 2 + for item in stateful_sets: + container = item["spec"]["template"]["spec"]["containers"][0] + assert "resources" in container + assert "limits" in container["resources"] + assert "requests" in container["resources"] + + +def test_dns_records_and_ports(chart_dir: Path, test_resources_dir: Path) -> None: + documents = _run_helm_template( + chart_dir, test_resources_dir / "dns-record-values.yaml" + ) + + services = _get_documents(documents, "Service") + headless_services = [s for s in services if "coredns" not in s["metadata"]["name"]] + assert len(headless_services) == 3 + assert all(service["spec"]["clusterIP"] == "None" for service in headless_services) + # a does not get a service. + b = headless_services[0] + assert b["metadata"]["name"] == "agent-env-my-release-b" + assert "ports" not in b["spec"] + c = headless_services[1] + assert c["metadata"]["name"] == "agent-env-my-release-c" + assert "ports" not in c["spec"] + d = headless_services[2] + assert d["metadata"]["name"] == "agent-env-my-release-d" + assert d["spec"]["ports"] == [{"name": "port-80", "port": 80, "protocol": "TCP"}] + + +def test_quotes_env_var_values(chart_dir: Path, test_resources_dir: Path) -> None: + documents = _run_helm_template( + chart_dir, test_resources_dir / "env-types-values.yaml" + ) + + stateful_sets = _get_documents(documents, "StatefulSet") + env = stateful_sets[0]["spec"]["template"]["spec"]["containers"][0]["env"] + # Verify that the env var values are quoted (i.e. strings). Helm install fails + # if env var values are not strings (even if the values.yaml file used strings). + assert env[1] == {"name": "A", "value": "1"} + assert env[2] == {"name": "B", "value": "2"} + assert env[3] == {"name": "C", "value": "three"} + + +def _run_helm_template( + chart_dir: Path, values_file: Path | None = None, set_str: str | None = None +) -> list[dict[str, Any]]: + cmd = [ + "helm", + "template", + "my-release", + str(chart_dir), + ] + if values_file: + cmd += ["-f", str(values_file)] + if set_str: + cmd += ["--set", set_str] + result = subprocess.run(cmd, stdout=subprocess.PIPE, check=True) + return list(yaml.safe_load_all(result.stdout)) + + +def _get_documents(documents: list[Any], doc_type_filter: str) -> list[dict[str, Any]]: + return [doc for doc in documents if doc["kind"] == doc_type_filter] diff --git a/test/k8s_sandbox/inspect_integration/__init__.py b/test/k8s_sandbox/inspect_integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/k8s_sandbox/inspect_integration/custom_chart/__init__.py b/test/k8s_sandbox/inspect_integration/custom_chart/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/k8s_sandbox/inspect_integration/custom_chart/my-custom-chart/Chart.yaml b/test/k8s_sandbox/inspect_integration/custom_chart/my-custom-chart/Chart.yaml new file mode 100644 index 0000000..b4601ab --- /dev/null +++ b/test/k8s_sandbox/inspect_integration/custom_chart/my-custom-chart/Chart.yaml @@ -0,0 +1,3 @@ +apiVersion: v2 +name: my-custom-chart +version: 1.0.0 diff --git a/test/k8s_sandbox/inspect_integration/custom_chart/my-custom-chart/templates/template.yaml b/test/k8s_sandbox/inspect_integration/custom_chart/my-custom-chart/templates/template.yaml new file mode 100644 index 0000000..42b5b10 --- /dev/null +++ b/test/k8s_sandbox/inspect_integration/custom_chart/my-custom-chart/templates/template.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Pod +metadata: + name: my-custom-chart-pod + labels: + app.kubernetes.io/name: {{ .Chart.Name }} + app.kubernetes.io/instance: {{ .Release.Name }} + inspect/service: default + annotations: + {{- toYaml $.Values.annotations | nindent 4 }} +spec: + containers: + - name: default-container + image: python:3.12-bookworm + command: ["sleep", "infinity"] + env: + - name: POD_ENV_VAR + value: {{ default "chart-default" .Values.podEnvVar }} + terminationGracePeriodSeconds: 0 diff --git a/test/k8s_sandbox/inspect_integration/custom_chart/test_integration.py b/test/k8s_sandbox/inspect_integration/custom_chart/test_integration.py new file mode 100644 index 0000000..d09291c --- /dev/null +++ b/test/k8s_sandbox/inspect_integration/custom_chart/test_integration.py @@ -0,0 +1,58 @@ +from pathlib import Path + +import pytest +from inspect_ai.model import Model +from inspect_ai.util import SandboxEnvironmentSpec + +from aisitools.k8s_sandbox import K8sSandboxEnvironmentConfig +from test.aisitools.k8s_sandbox.inspect_integration.testing_utils.mock_model import ( + MockToolCallModel, +) +from test.aisitools.k8s_sandbox.inspect_integration.testing_utils.utils import ( + create_task, + run_and_verify_inspect_eval, + tool_call, +) + +# Mark all tests in this module as requiring a Kubernetes cluster. +pytestmark = pytest.mark.req_k8s + + +@pytest.fixture +def model() -> Model: + return MockToolCallModel( + [tool_call("bash", {"cmd": "echo $POD_ENV_VAR"})], + ) + + +def test_custom_chart_default_values(model: Model) -> None: + task = create_task( + __file__, + target="chart-default", + sandbox=SandboxEnvironmentSpec( + "k8s", K8sSandboxEnvironmentConfig(chart="my-custom-chart") + ), + ) + + result = run_and_verify_inspect_eval(task=task, model=model)[0] + + assert result.scores is not None + assert result.scores["match"].value == "C" + + +def test_custom_chart_custom_values(model: Model) -> None: + task = create_task( + __file__, + target="overridden-by-values", + sandbox=SandboxEnvironmentSpec( + "k8s", + K8sSandboxEnvironmentConfig( + chart="my-custom-chart", values=Path("values.yaml") + ), + ), + ) + + result = run_and_verify_inspect_eval(task=task, model=model)[0] + + assert result.scores is not None + assert result.scores["match"].value == "C" diff --git a/test/k8s_sandbox/inspect_integration/custom_chart/values.yaml b/test/k8s_sandbox/inspect_integration/custom_chart/values.yaml new file mode 100644 index 0000000..f319aab --- /dev/null +++ b/test/k8s_sandbox/inspect_integration/custom_chart/values.yaml @@ -0,0 +1 @@ +podEnvVar: "overridden-by-values" diff --git a/test/k8s_sandbox/inspect_integration/inferred_values/__init__.py b/test/k8s_sandbox/inspect_integration/inferred_values/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/k8s_sandbox/inspect_integration/inferred_values/test_integration.py b/test/k8s_sandbox/inspect_integration/inferred_values/test_integration.py new file mode 100644 index 0000000..0d788be --- /dev/null +++ b/test/k8s_sandbox/inspect_integration/inferred_values/test_integration.py @@ -0,0 +1,24 @@ +import pytest + +from test.aisitools.k8s_sandbox.inspect_integration.testing_utils.mock_model import ( + MockToolCallModel, +) +from test.aisitools.k8s_sandbox.inspect_integration.testing_utils.utils import ( + create_task, + run_and_verify_inspect_eval, + tool_call, +) + + +@pytest.mark.req_k8s +def test_default_chart_with_inferred_values_yaml() -> None: + # Verify that the `values.yaml` file in the task dir is automatically used. + model = MockToolCallModel( + [tool_call("bash", {"cmd": "cat /etc/os-release | grep VERSION_CODENAME"})], + ) + task = create_task(__file__, target="bullseye") + + result = run_and_verify_inspect_eval(task=task, model=model)[0] + + assert result.scores is not None + assert result.scores["match"].value == "C" diff --git a/test/k8s_sandbox/inspect_integration/inferred_values/values.yaml b/test/k8s_sandbox/inspect_integration/inferred_values/values.yaml new file mode 100644 index 0000000..7cc08ba --- /dev/null +++ b/test/k8s_sandbox/inspect_integration/inferred_values/values.yaml @@ -0,0 +1,3 @@ +services: + default: + image: "python:3.12-bullseye" diff --git a/test/k8s_sandbox/inspect_integration/multiple_sandbox_envs/__init__.py b/test/k8s_sandbox/inspect_integration/multiple_sandbox_envs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/k8s_sandbox/inspect_integration/multiple_sandbox_envs/test_integration.py b/test/k8s_sandbox/inspect_integration/multiple_sandbox_envs/test_integration.py new file mode 100644 index 0000000..c013ab5 --- /dev/null +++ b/test/k8s_sandbox/inspect_integration/multiple_sandbox_envs/test_integration.py @@ -0,0 +1,46 @@ +import pytest +from inspect_ai.tool import Tool, ToolError, tool +from inspect_ai.util import sandbox + +from test.aisitools.k8s_sandbox.inspect_integration.testing_utils.mock_model import ( + MockToolCallModel, +) +from test.aisitools.k8s_sandbox.inspect_integration.testing_utils.utils import ( + create_task, + run_and_verify_inspect_eval, + tool_call, +) + + +@pytest.mark.req_k8s +def test_other_sandbox_env() -> None: + model = MockToolCallModel( + [tool_call("bash_other", {"cmd": "echo $TEST_ENV_VAR"})], + ) + task = create_task(__file__, target="other", tools=[bash_other()]) + + result = run_and_verify_inspect_eval(task=task, model=model)[0] + + assert result.scores is not None + assert result.scores["match"].value == "C" + + +@tool +def bash_other() -> Tool: + async def execute(cmd: str) -> str: + """ + Execute a bash command. + + Args: + cmd (str): The bash command to execute. + + Returns: + The output of the command. + """ + result = await sandbox("other").exec(cmd=["bash", "-c", cmd]) + if result.success: + return result.stdout + else: + raise ToolError(result.stderr) + + return execute diff --git a/test/k8s_sandbox/inspect_integration/multiple_sandbox_envs/values.yaml b/test/k8s_sandbox/inspect_integration/multiple_sandbox_envs/values.yaml new file mode 100644 index 0000000..f01a799 --- /dev/null +++ b/test/k8s_sandbox/inspect_integration/multiple_sandbox_envs/values.yaml @@ -0,0 +1,7 @@ +services: + other: + image: "python:3.12-bookworm" + command: ["tail", "-f", "/dev/null"] + env: + - name: "TEST_ENV_VAR" + value: "other" diff --git a/test/k8s_sandbox/inspect_integration/sandbox_env_ordering/__init__.py b/test/k8s_sandbox/inspect_integration/sandbox_env_ordering/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/k8s_sandbox/inspect_integration/sandbox_env_ordering/test_integration.py b/test/k8s_sandbox/inspect_integration/sandbox_env_ordering/test_integration.py new file mode 100644 index 0000000..5b12973 --- /dev/null +++ b/test/k8s_sandbox/inspect_integration/sandbox_env_ordering/test_integration.py @@ -0,0 +1,24 @@ +import pytest +from inspect_ai.tool import bash + +from test.aisitools.k8s_sandbox.inspect_integration.testing_utils.mock_model import ( + MockToolCallModel, +) +from test.aisitools.k8s_sandbox.inspect_integration.testing_utils.utils import ( + create_task, + run_and_verify_inspect_eval, + tool_call, +) + + +@pytest.mark.req_k8s +def test_sandbox_env_ordering() -> None: + model = MockToolCallModel( + [tool_call("bash", {"cmd": "echo $SERVICE_NAME"})], + ) + task = create_task(__file__, target="default", tools=[bash()]) + + result = run_and_verify_inspect_eval(task=task, model=model)[0] + + assert result.scores is not None + assert result.scores["match"].value == "C" diff --git a/test/k8s_sandbox/inspect_integration/sandbox_env_ordering/values.yaml b/test/k8s_sandbox/inspect_integration/sandbox_env_ordering/values.yaml new file mode 100644 index 0000000..8fa1d36 --- /dev/null +++ b/test/k8s_sandbox/inspect_integration/sandbox_env_ordering/values.yaml @@ -0,0 +1,16 @@ +services: + # Pods are typically returned in alphabetical order from k8s API; name this service + # such that it will likely be returned first from the k8s API to verify that we do + # re-order the key/value pairs in the dict before passing to inspect. + another: + image: "python:3.12-bookworm" + command: ["tail", "-f", "/dev/null"] + env: + - name: "SERVICE_NAME" + value: "another" + default: + image: "python:3.12-bookworm" + command: ["tail", "-f", "/dev/null"] + env: + - name: "SERVICE_NAME" + value: "default" diff --git a/test/k8s_sandbox/inspect_integration/test_cleanup.py b/test/k8s_sandbox/inspect_integration/test_cleanup.py new file mode 100644 index 0000000..405aa6b --- /dev/null +++ b/test/k8s_sandbox/inspect_integration/test_cleanup.py @@ -0,0 +1,43 @@ +import asyncio +from unittest.mock import patch + +import pytest + +from aisitools.k8s_sandbox._helm import uninstall +from test.aisitools.k8s_sandbox.inspect_integration.testing_utils.mock_model import ( + MockToolCallModel, +) +from test.aisitools.k8s_sandbox.inspect_integration.testing_utils.utils import ( + create_task, + run_and_verify_inspect_eval, + tool_call, +) + +# Mark all tests in this module as requiring a Kubernetes cluster. +pytestmark = pytest.mark.req_k8s + + +def test_with_cleanup() -> None: + model = MockToolCallModel([tool_call("bash", {"cmd": "echo 'success'"})]) + task = create_task(__file__, target="success") + + with patch("aisitools.k8s_sandbox._helm.uninstall", wraps=uninstall) as spy: + run_and_verify_inspect_eval(task=task, model=model) + + assert spy.call_count == 1 + + +def test_without_cleanup() -> None: + model = MockToolCallModel([tool_call("bash", {"cmd": "echo 'success'"})]) + task = create_task(__file__, target="success") + release = "no-clean" + + with patch( + "aisitools.k8s_sandbox._helm.Release._generate_release_name", + return_value=release, + ): + with patch("aisitools.k8s_sandbox._helm.uninstall", wraps=uninstall) as spy: + run_and_verify_inspect_eval(task=task, model=model, sandbox_cleanup=False) + + assert spy.call_count == 0 + asyncio.run(uninstall(release, quiet=False)) diff --git a/test/k8s_sandbox/inspect_integration/test_default_values.py b/test/k8s_sandbox/inspect_integration/test_default_values.py new file mode 100644 index 0000000..cb45582 --- /dev/null +++ b/test/k8s_sandbox/inspect_integration/test_default_values.py @@ -0,0 +1,23 @@ +import pytest + +from test.aisitools.k8s_sandbox.inspect_integration.testing_utils.mock_model import ( + MockToolCallModel, +) +from test.aisitools.k8s_sandbox.inspect_integration.testing_utils.utils import ( + create_task, + run_and_verify_inspect_eval, + tool_call, +) + + +@pytest.mark.req_k8s +def test_default_chart_with_no_values() -> None: + model = MockToolCallModel( + [tool_call("bash", {"cmd": "cat /etc/os-release | grep VERSION_CODENAME"})], + ) + task = create_task(__file__, target="bookworm") + + result = run_and_verify_inspect_eval(task=task, model=model)[0] + + assert result.scores is not None + assert result.scores["match"].value == "C" diff --git a/test/k8s_sandbox/inspect_integration/testing_utils/__init__.py b/test/k8s_sandbox/inspect_integration/testing_utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/k8s_sandbox/inspect_integration/testing_utils/mock_model.py b/test/k8s_sandbox/inspect_integration/testing_utils/mock_model.py new file mode 100644 index 0000000..42c4cde --- /dev/null +++ b/test/k8s_sandbox/inspect_integration/testing_utils/mock_model.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +from inspect_ai.model import ( + ChatMessage, + GenerateConfig, + Model, + ModelAPI, + ModelOutput, + modelapi, +) +from inspect_ai.tool import ToolCall, ToolChoice, ToolInfo + +from test.aisitools.k8s_sandbox.inspect_integration.testing_utils.utils import ( + output_from_tool_call, +) + + +class MockToolCallModel(Model): + """Requests a sequence of tool calls and outputs the result of the final call.""" + + def __init__(self, tool_calls: list[ToolCall]) -> None: + super().__init__(MockToolCallModelApi(tool_calls), GenerateConfig()) + + @classmethod + def from_tool_call(cls, tool_call: ToolCall) -> MockToolCallModel: + return cls([tool_call]) + + @classmethod + def from_tool_calls(cls, tool_calls: list[ToolCall]) -> MockToolCallModel: + return cls(tool_calls) + + +@modelapi(name="tool_call_model") +class MockToolCallModelApi(ModelAPI): + def __init__(self, tool_calls: list[ToolCall]) -> None: + super().__init__("tool_call_model", None, config=GenerateConfig()) + self._tool_calls = tool_calls + + async def generate( + self, + input: list[ChatMessage], + tools: list[ToolInfo], + tool_choice: ToolChoice, + config: GenerateConfig, + ) -> ModelOutput: + if self._tool_calls: + return output_from_tool_call(self._tool_calls.pop(0)) + # If we've used all the tools in the queue, response with the output of the last + # tool call. + last_tool_call_output = input[-1].text.strip() + return ModelOutput.from_content("tool_call_model", last_tool_call_output) diff --git a/test/k8s_sandbox/inspect_integration/testing_utils/utils.py b/test/k8s_sandbox/inspect_integration/testing_utils/utils.py new file mode 100644 index 0000000..80ba1f4 --- /dev/null +++ b/test/k8s_sandbox/inspect_integration/testing_utils/utils.py @@ -0,0 +1,103 @@ +import os +from uuid import uuid4 + +from inspect_ai import Task, eval +from inspect_ai.dataset import MemoryDataset, Sample +from inspect_ai.log import EvalSample +from inspect_ai.model import ( + ChatCompletionChoice, + ChatMessageAssistant, + Model, + ModelOutput, +) +from inspect_ai.scorer import match +from inspect_ai.solver import generate, use_tools +from inspect_ai.tool import Tool, ToolCall, bash +from inspect_ai.util import SandboxEnvironmentType + + +def create_task( + test_dir: str, + target: str = "", + sandbox: SandboxEnvironmentType = "k8s", + tools: list[Tool] | None = None, +) -> Task: + return Task( + MemoryDataset( + samples=[ + Sample( + input="This is a test.", + target=target, + ) + ], + location=os.path.dirname(test_dir), + ), + solver=[ + use_tools(tools or [bash(timeout=10)]), + generate(), + ], + name=test_dir, + sandbox=sandbox, + scorer=match(), + max_messages=10, + ) + + +def run_and_verify_inspect_eval( + task: Task, model: Model, sandbox_cleanup: bool = True +) -> list[EvalSample]: + # Run the task with the cwd as the task directory. This allows Inspect to discover + # any compose.yaml file and resolve relative file paths from challenge.yaml. + assert task.dataset.location + log_dir = os.path.join(os.getcwd(), "logs") + with _ChangeDir(task.dataset.location): + # log_level="SANDBOX" to facilitate test debugging. + logs = eval( + task, + model=model, + log_dir=log_dir, + log_level="SANDBOX", + sandbox_cleanup=sandbox_cleanup, + ) + log = logs[0] + assert log.status == "success" + assert log.samples + return log.samples + + +def tool_call(function: str, arguments: dict[str, str]) -> ToolCall: + return ToolCall( + id=uuid4().hex, + function=function, + arguments=arguments, + type="function", + ) + + +def output_from_tool_call(tool_call: ToolCall) -> ModelOutput: + return ModelOutput( + choices=[ + ChatCompletionChoice( + message=ChatMessageAssistant( + content="I'd like to use a tool.", + tool_calls=[tool_call], + source="generate", + ), + stop_reason="tool_calls", + ) + ] + ) + + +class _ChangeDir: + def __init__(self, new_path: str) -> None: + self.new_path = new_path + self.saved_path: str | None = None + + def __enter__(self) -> None: + self.saved_path = os.getcwd() + os.chdir(self.new_path) + + def __exit__(self, etype, value, traceback) -> None: # type: ignore + assert self.saved_path is not None + os.chdir(self.saved_path) diff --git a/test/k8s_sandbox/inspect_integration/values/__init__.py b/test/k8s_sandbox/inspect_integration/values/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/k8s_sandbox/inspect_integration/values/my-values.yaml b/test/k8s_sandbox/inspect_integration/values/my-values.yaml new file mode 100644 index 0000000..7d6ac4c --- /dev/null +++ b/test/k8s_sandbox/inspect_integration/values/my-values.yaml @@ -0,0 +1,5 @@ +services: + default: + env: + - name: "VALUES_SOURCE" + value: "my-values.yaml" diff --git a/test/k8s_sandbox/inspect_integration/values/test_integration.py b/test/k8s_sandbox/inspect_integration/values/test_integration.py new file mode 100644 index 0000000..29abe28 --- /dev/null +++ b/test/k8s_sandbox/inspect_integration/values/test_integration.py @@ -0,0 +1,41 @@ +import pytest +from inspect_ai.model import Model + +from test.aisitools.k8s_sandbox.inspect_integration.testing_utils.mock_model import ( + MockToolCallModel, +) +from test.aisitools.k8s_sandbox.inspect_integration.testing_utils.utils import ( + create_task, + run_and_verify_inspect_eval, + tool_call, +) + +# Mark all tests in this module as requiring a Kubernetes cluster. +pytestmark = pytest.mark.req_k8s + + +@pytest.fixture +def model() -> MockToolCallModel: + return MockToolCallModel( + [tool_call("bash", {"cmd": "echo $VALUES_SOURCE"})], + ) + + +def test_default_chart_with_inferred_values_yaml(model: Model) -> None: + task = create_task(__file__, target="values.yaml") + + result = run_and_verify_inspect_eval(task=task, model=model)[0] + + assert result.scores is not None + assert result.scores["match"].value == "C" + + +def test_default_chart_with_explicit_values_yaml(model: Model) -> None: + task = create_task( + __file__, target="my-values.yaml", sandbox=("k8s", "my-values.yaml") + ) + + result = run_and_verify_inspect_eval(task=task, model=model)[0] + + assert result.scores is not None + assert result.scores["match"].value == "C" diff --git a/test/k8s_sandbox/inspect_integration/values/values.yaml b/test/k8s_sandbox/inspect_integration/values/values.yaml new file mode 100644 index 0000000..cb72a80 --- /dev/null +++ b/test/k8s_sandbox/inspect_integration/values/values.yaml @@ -0,0 +1,5 @@ +services: + default: + env: + - name: "VALUES_SOURCE" + value: "values.yaml" diff --git a/test/k8s_sandbox/pod/test_executor.py b/test/k8s_sandbox/pod/test_executor.py new file mode 100644 index 0000000..41d242c --- /dev/null +++ b/test/k8s_sandbox/pod/test_executor.py @@ -0,0 +1,73 @@ +import asyncio +import threading +from time import sleep +from typing import Generator +from unittest.mock import patch + +import pytest +from pytest import MonkeyPatch + +from aisitools.k8s_sandbox._pod.executor import PodOpExecutor + + +@pytest.fixture(autouse=True) +def reset_singleton() -> Generator: + # Ensure that each test starts with a fresh singleton instance. + PodOpExecutor._instance = None + yield + + +def test_get_instance() -> None: + result1 = PodOpExecutor.get_instance() + result2 = PodOpExecutor.get_instance() + + assert result1 == result2 + + +def test_default_max_workers(monkeypatch: MonkeyPatch) -> None: + monkeypatch.delenv("INSPECT_MAX_POD_OPS", raising=False) + + with patch("os.cpu_count", return_value=4): + actual = PodOpExecutor.get_instance() + + assert actual._max_workers == 16 + + +def test_max_workers_via_env_var(monkeypatch: MonkeyPatch) -> None: + monkeypatch.setenv("INSPECT_MAX_POD_OPS", "42") + + actual = PodOpExecutor.get_instance() + + assert actual._max_workers == 42 + + +async def test_queue_operation(monkeypatch: MonkeyPatch) -> None: + monkeypatch.setenv("INSPECT_MAX_POD_OPS", "10") + executor = PodOpExecutor.get_instance() + + op1 = executor.queue_operation(lambda: _synchronous_operation(1)) + op2 = executor.queue_operation(lambda: _synchronous_operation(2)) + + result1, result2 = await asyncio.gather(op1, op2) + assert result1 == (1, "pod-op-executor_0") + assert result2 == (2, "pod-op-executor_1") + + +async def test_queue_more_operations_than_max_workers(monkeypatch: MonkeyPatch) -> None: + monkeypatch.setenv("INSPECT_MAX_POD_OPS", "2") + executor = PodOpExecutor.get_instance() + + op1 = executor.queue_operation(lambda: _synchronous_operation(1)) + op2 = executor.queue_operation(lambda: _synchronous_operation(2)) + op3 = executor.queue_operation(lambda: _synchronous_operation(3)) + + result1, result2, result3 = await asyncio.gather(op1, op2, op3) + assert result1 == (1, "pod-op-executor_0") + assert result2 == (2, "pod-op-executor_1") + # The third operation should be executed by one of the two existing workers. + assert result3 == (3, "pod-op-executor_0") or result3 == (3, "pod-op-executor_1") + + +def _synchronous_operation(value: int) -> tuple[int, str]: + sleep(1) + return value, threading.current_thread().name diff --git a/test/k8s_sandbox/pod/test_get_returncode.py b/test/k8s_sandbox/pod/test_get_returncode.py new file mode 100644 index 0000000..9b8d4c2 --- /dev/null +++ b/test/k8s_sandbox/pod/test_get_returncode.py @@ -0,0 +1,122 @@ +from unittest.mock import MagicMock + +import pytest +import yaml +from kubernetes.stream.ws_client import WSClient # type: ignore + +from aisitools.k8s_sandbox._pod.get_returncode import ( + GetReturncodeError, + get_returncode, +) + + +@pytest.fixture +def mock_response() -> WSClient: + mock_client: WSClient = MagicMock() + mock_client.is_open.return_value = False + return mock_client + + +def test_get_returncode_with_none(mock_response: WSClient) -> None: + mock_response.read_channel.return_value = None + + with pytest.raises(GetReturncodeError) as e: + get_returncode(mock_response) + + assert "because it was empty" in str(e.value) + + +def test_get_returncode_with_empty_response(mock_response: WSClient) -> None: + mock_response.read_channel.return_value = "" + + with pytest.raises(GetReturncodeError) as e: + get_returncode(mock_response) + + assert "because it was empty" in str(e.value) + + +def test_get_returncode_with_empty_yaml_response(mock_response: WSClient) -> None: + mock_response.read_channel.return_value = yaml.dump({}) + + with pytest.raises(GetReturncodeError) as e: + get_returncode(mock_response) + + assert "because it did not contain a `status` key" in str(e.value) + + +def test_get_returncode_with_no_status(mock_response: WSClient) -> None: + mock_response.read_channel.return_value = yaml.dump({"metadata": {}, "foo": "bar"}) + + with pytest.raises(GetReturncodeError) as e: + get_returncode(mock_response) + + assert "because it did not contain a `status` key" in str(e.value) + + +def test_get_returncode_with_real_success_response(mock_response: WSClient) -> None: + mock_response.read_channel.return_value = yaml.dump( + {"metadata": {}, "status": "Success"} + ) + + assert get_returncode(mock_response) == 0 + + +def test_get_returncode_failure(mock_response: WSClient) -> None: + mock_response.read_channel.return_value = yaml.dump( + { + "status": "Failure", + "details": { + "causes": [ + {"reason": "Foo", "message": "bar"}, + {"reason": "ExitCode", "message": "42"}, + ] + }, + } + ) + + assert get_returncode(mock_response) == 42 + + +def test_get_returncode_real_non_zero_response(mock_response: WSClient) -> None: + mock_response.read_channel.return_value = yaml.dump( + { + "metadata": {}, + "status": "Failure", + "message": "command terminated with non-zero exit code: error executing command [bash -c timeout 1 sleep 10], exit code 124", # noqa: E501 + "reason": "NonZeroExitCode", + "details": {"causes": [{"reason": "ExitCode", "message": "124"}]}, + } + ) + + assert get_returncode(mock_response) == 124 + + +def test_get_returncode_real_no_exit_code_response(mock_response: WSClient) -> None: + mock_response.read_channel.return_value = yaml.dump( + { + "metadata": {}, + "status": "Failure", + "message": 'Internal error occurred: error executing command in container: failed to exec in container: failed to create exec "d5b4b8c74fd16c6f74b048f8d4349110071dd60c16867a057c77212115c319a7": cannot exec in a stopped state: unknown', # noqa: E501 + "reason": "InternalError", + "details": { + "causes": [ + { + "message": 'error executing command in container: failed to exec in container: failed to create exec "d5b4b8c74fd16c6f74b048f8d4349110071dd60c16867a057c77212115c319a7": cannot exec in a stopped state: unknown' # noqa: E501 + } + ] + }, + "code": 500, + } + ) + + with pytest.raises(GetReturncodeError) as e: + get_returncode(mock_response) + + assert "no entry in `details.causes` with `reason`=='ExitCode'" in str(e.value) + + +def test_get_returncode_when_response_is_open(mock_response: WSClient) -> None: + mock_response.is_open.return_value = True + + with pytest.raises(AssertionError): + get_returncode(mock_response) diff --git a/test/k8s_sandbox/pod/test_pod.py b/test/k8s_sandbox/pod/test_pod.py new file mode 100644 index 0000000..2568b81 --- /dev/null +++ b/test/k8s_sandbox/pod/test_pod.py @@ -0,0 +1,59 @@ +from unittest.mock import MagicMock + +from aisitools.k8s_sandbox._pod.execute import ExecuteOperation + + +def test_filter_sentinel_and_returncode(): + executor = ExecuteOperation(MagicMock()) + frame = b"beforeafter" + + assert executor._filter_sentinel_and_returncode(frame) == (b"beforeafter", 42) + + +def test_filter_sentinel_and_returncode_new_lines(): + executor = ExecuteOperation(MagicMock()) + frame = b"a\nb\nc\nd" + + assert executor._filter_sentinel_and_returncode(frame) == (b"a\nb\nc\nd", 42) + + +def test_filter_sentinel_and_returncode_not_present(): + executor = ExecuteOperation(MagicMock()) + frame = b"stdout" + + assert executor._filter_sentinel_and_returncode(frame) == (b"stdout", None) + + +def test_filter_sentinel_and_returncode_empty(): + executor = ExecuteOperation(MagicMock()) + frame = b"" + + assert executor._filter_sentinel_and_returncode(frame) == (b"", None) + + +def test_filter_sentinel_and_returncode_nothing_preceeding(): + executor = ExecuteOperation(MagicMock()) + frame = b"after" + + assert executor._filter_sentinel_and_returncode(frame) == (b"after", 42) + + +def test_filter_sentinel_and_returncode_nothing_following(): + executor = ExecuteOperation(MagicMock()) + frame = b"before" + + assert executor._filter_sentinel_and_returncode(frame) == (b"before", 42) + + +def test_filter_sentinel_and_returncode_0(): + executor = ExecuteOperation(MagicMock()) + frame = b"" + + assert executor._filter_sentinel_and_returncode(frame) == (b"", 0) + + +def test_filter_sentinel_and_returncode_255(): + executor = ExecuteOperation(MagicMock()) + frame = b"" + + assert executor._filter_sentinel_and_returncode(frame) == (b"", 255) diff --git a/test/k8s_sandbox/resources/dns-values.yaml b/test/k8s_sandbox/resources/dns-values.yaml new file mode 100644 index 0000000..6fafdfb --- /dev/null +++ b/test/k8s_sandbox/resources/dns-values.yaml @@ -0,0 +1,46 @@ +services: + default: + # Contains networking tools. + image: "nicolaka/netshoot:v0.13" + command: ["tail", "-f", "/dev/null"] + resources: + limits: + memory: "128Mi" + cpu: "100m" + requests: + memory: "128Mi" + cpu: "100m" + victim: + image: "nginx:1.27.0" + dnsRecord: true + resources: + limits: + memory: "128Mi" + cpu: "100m" + requests: + memory: "128Mi" + cpu: "100m" + victim-google: + image: "nginx:1.27.0" + additionalDnsRecords: + - "google.com" + resources: + limits: + memory: "128Mi" + cpu: "100m" + requests: + memory: "128Mi" + cpu: "100m" + victim-ports: + image: "nginx:1.27.0" + ports: + - port: 80 + targetPort: 80 + protocol: TCP + resources: + limits: + memory: "128Mi" + cpu: "100m" + requests: + memory: "128Mi" + cpu: "100m" diff --git a/test/k8s_sandbox/resources/netpol-values.yaml b/test/k8s_sandbox/resources/netpol-values.yaml new file mode 100644 index 0000000..12ff740 --- /dev/null +++ b/test/k8s_sandbox/resources/netpol-values.yaml @@ -0,0 +1,16 @@ +services: + default: + resources: + limits: + memory: "128Mi" + cpu: "100m" + requests: + memory: "128Mi" + cpu: "100m" +allowDomains: + - "pypi.org" + - "files.pythonhosted.org" + - "google.com" +allowCIDR: + # Allow access to 1.1.1.1 (32 indicates all bits must match). + - "1.1.1.1/32" diff --git a/test/k8s_sandbox/resources/netpol-world-values.yaml b/test/k8s_sandbox/resources/netpol-world-values.yaml new file mode 100644 index 0000000..4162cab --- /dev/null +++ b/test/k8s_sandbox/resources/netpol-world-values.yaml @@ -0,0 +1,11 @@ +services: + default: + resources: + limits: + memory: "128Mi" + cpu: "100m" + requests: + memory: "128Mi" + cpu: "100m" +allowEntities: + - world diff --git a/test/k8s_sandbox/resources/networks-values.yaml b/test/k8s_sandbox/resources/networks-values.yaml new file mode 100644 index 0000000..123e0ec --- /dev/null +++ b/test/k8s_sandbox/resources/networks-values.yaml @@ -0,0 +1,54 @@ +networks: + default-victim: + driver: k8s + victim-1-3: + driver: k8s +services: + default: + # Contains networking tools. + image: "nicolaka/netshoot:v0.13" + command: ["tail", "-f", "/dev/null"] + networks: + - default-victim + resources: + limits: + memory: "128Mi" + cpu: "100m" + requests: + memory: "128Mi" + cpu: "100m" + victim1: + image: "nginx:1.27.0" + dnsRecord: true + networks: + - default-victim + - victim-1-3 + resources: + limits: + memory: "128Mi" + cpu: "100m" + requests: + memory: "128Mi" + cpu: "100m" + victim2: + image: "nginx:1.27.0" + dnsRecord: true + resources: + limits: + memory: "128Mi" + cpu: "100m" + requests: + memory: "128Mi" + cpu: "100m" + victim3: + image: "nginx:1.27.0" + dnsRecord: true + networks: + - victim-1-3 + resources: + limits: + memory: "128Mi" + cpu: "100m" + requests: + memory: "128Mi" + cpu: "100m" diff --git a/test/k8s_sandbox/resources/runtime-class-values.yaml b/test/k8s_sandbox/resources/runtime-class-values.yaml new file mode 100644 index 0000000..0a2efea --- /dev/null +++ b/test/k8s_sandbox/resources/runtime-class-values.yaml @@ -0,0 +1,41 @@ +services: + default: + resources: + limits: + memory: "128Mi" + cpu: "100m" + requests: + memory: "128Mi" + cpu: "100m" + gvisor-specified: + image: "ubuntu:24.04" + command: ["tail", "-f", "/dev/null"] + runtimeClassName: gvisor + resources: + limits: + memory: "128Mi" + cpu: "100m" + requests: + memory: "128Mi" + cpu: "100m" + runc-specified: + image: "ubuntu:24.04" + command: ["tail", "-f", "/dev/null"] + runtimeClassName: runc + resources: + limits: + memory: "128Mi" + cpu: "100m" + requests: + memory: "128Mi" + cpu: "100m" + unspecified: + image: "ubuntu:24.04" + command: ["tail", "-f", "/dev/null"] + resources: + limits: + memory: "128Mi" + cpu: "100m" + requests: + memory: "128Mi" + cpu: "100m" diff --git a/test/k8s_sandbox/resources/values.yaml b/test/k8s_sandbox/resources/values.yaml new file mode 100644 index 0000000..722352b --- /dev/null +++ b/test/k8s_sandbox/resources/values.yaml @@ -0,0 +1,35 @@ +services: + default: + workingDir: "/root" + resources: + limits: + memory: "128Mi" + cpu: "100m" + requests: + memory: "128Mi" + cpu: "100m" + # ubuntu:24.04 has a `ubuntu` user with UID 1000. + nonroot: + image: "ubuntu:24.04" + command: ["tail", "-f", "/dev/null"] + workingDir: "/home/ubuntu" + securityContext: + runAsUser: 1000 + runAsNonRoot: true + resources: + limits: + memory: "128Mi" + cpu: "100m" + requests: + memory: "128Mi" + cpu: "100m" + busybox: + image: "busybox:1.37.0" + command: ["tail", "-f", "/dev/null"] + resources: + limits: + memory: "128Mi" + cpu: "100m" + requests: + memory: "128Mi" + cpu: "100m" diff --git a/test/k8s_sandbox/resources/volume-values.yaml b/test/k8s_sandbox/resources/volume-values.yaml new file mode 100644 index 0000000..d42e1a0 --- /dev/null +++ b/test/k8s_sandbox/resources/volume-values.yaml @@ -0,0 +1,27 @@ +services: + default: + image: ubuntu:24.04 + command: ["tail", "-f", "/dev/null"] + volumes: + - "shared:/mount" + resources: + limits: + memory: "128Mi" + cpu: "100m" + requests: + memory: "128Mi" + cpu: "100m" + writer: + image: ubuntu:24.04 + command: ["bash", "-c", "echo test > /mount/test.txt && tail -f /dev/null"] + volumes: + - "shared:/mount" + resources: + limits: + memory: "128Mi" + cpu: "100m" + requests: + memory: "128Mi" + cpu: "100m" +volumes: + shared: diff --git a/test/k8s_sandbox/test_config_validation.py b/test/k8s_sandbox/test_config_validation.py new file mode 100644 index 0000000..06225ae --- /dev/null +++ b/test/k8s_sandbox/test_config_validation.py @@ -0,0 +1,35 @@ +from pathlib import Path + +import pytest +from pydantic import BaseModel + +from aisitools.k8s_sandbox import K8sSandboxEnvironment, K8sSandboxEnvironmentConfig + +VALID_VALUES = str(Path(__file__).parent / "resources" / "values.yaml") + + +async def test_invalid_values_path_as_str() -> None: + with pytest.raises(FileNotFoundError): + await K8sSandboxEnvironment.sample_init(__file__, "fake.yaml", {}) + + +async def test_invalid_values_path() -> None: + with pytest.raises(FileNotFoundError): + await K8sSandboxEnvironment.sample_init( + __file__, K8sSandboxEnvironmentConfig(values=Path("fake.yaml")), {} + ) + + +async def test_invalid_chart() -> None: + with pytest.raises(NotADirectoryError): + await K8sSandboxEnvironment.sample_init( + __file__, K8sSandboxEnvironmentConfig(chart="chart-does-not-exist"), {} + ) + + +async def test_invalid_config_type() -> None: + class MyModel(BaseModel, frozen=True): + pass + + with pytest.raises(TypeError): + await K8sSandboxEnvironment.sample_init(__file__, MyModel(), {}) diff --git a/test/k8s_sandbox/test_dns.py b/test/k8s_sandbox/test_dns.py new file mode 100644 index 0000000..125cb9d --- /dev/null +++ b/test/k8s_sandbox/test_dns.py @@ -0,0 +1,73 @@ +from typing import AsyncGenerator + +import pytest +import pytest_asyncio + +from aisitools.k8s_sandbox._sandbox_environment import K8sSandboxEnvironment +from test.aisitools.k8s_sandbox.utils import install_sandbox_environments + +# Mark all tests in this module as requiring a Kubernetes cluster. +pytestmark = pytest.mark.req_k8s + + +@pytest_asyncio.fixture(scope="module") +async def sandbox() -> AsyncGenerator[K8sSandboxEnvironment, None]: + async with install_sandbox_environments(__file__, "dns-values.yaml") as envs: + yield envs["default"] + + +# The inclusion of additionalDnsRecords or ports should result in a DNS entry being +# created for the service using its service name. +@pytest.mark.parametrize("hostname", ["victim", "victim-google", "victim-ports"]) +async def test_victim_is_resolved_by_service_name( + sandbox: K8sSandboxEnvironment, hostname: str +): + result = await sandbox.exec(["curl", "-sI", hostname], timeout=10) + + assert "Server: nginx/1.27.0" in result.stdout + + +async def test_victim_is_resolved_by_additional_dns_record( + sandbox: K8sSandboxEnvironment, +): + # google.com is an additional DNS record in the Helm chart values which resolves to + # victim-google. + result = await sandbox.exec(["curl", "-sI", "http://google.com"], timeout=10) + + assert "Server: nginx/1.27.0" in result.stdout + + +async def test_victim_is_resolved_by_env_var_and_service_name( + sandbox: K8sSandboxEnvironment, +): + # Verify that $AGENT_ENV-victim is resolved for backward compatibility. + result = await sandbox.exec( + ["bash", "-c", "curl -sI http://${AGENT_ENV}-victim"], timeout=10 + ) + + assert "Server: nginx/1.27.0" in result.stdout + + +async def test_netcat_on_victim(sandbox: K8sSandboxEnvironment): + # If victim's ClusterIP Service is not headless, netcat will hang on ports which + # aren't explicitly listed on the Service. + result = await sandbox.exec(["nc", "-zv", "victim", "1-80"], timeout=10) + + assert "port 79 (tcp) failed: Connection refused" in result.stderr + assert "80 port [tcp/http] succeeded" in result.stderr + + +async def test_ping_on_victim(sandbox: K8sSandboxEnvironment): + # If victim's ClusterIP Service is not headless, ping will hang. + result = await sandbox.exec(["ping", "-c", "1", "victim"], timeout=10) + + assert "1 packets transmitted, 1 received" in result.stdout + + +async def test_non_existent_service(sandbox: K8sSandboxEnvironment): + result = await sandbox.exec( + ["curl", "-sI", "http://non-existent-service"], + timeout=10, + ) + + assert not result.success diff --git a/test/k8s_sandbox/test_helm.py b/test/k8s_sandbox/test_helm.py new file mode 100644 index 0000000..1b8d44b --- /dev/null +++ b/test/k8s_sandbox/test_helm.py @@ -0,0 +1,54 @@ +from pathlib import Path +from unittest.mock import patch + +import pytest +from inspect_ai.util import ExecResult + +from aisitools.k8s_sandbox._helm import Release, _run_subprocess + + +@pytest.fixture +def uninstallable_release() -> Release: + return Release(__file__, chart_path=Path("/non_existent_chart")) + + +async def test_helm_install_error(uninstallable_release: Release) -> None: + with patch( + "aisitools.k8s_sandbox._helm._run_subprocess", wraps=_run_subprocess + ) as spy: + with pytest.raises(RuntimeError) as excinfo: + await uninstallable_release.install() + + assert spy.call_count == 1 + assert "not found" in str(excinfo.value) + + +async def test_helm_resourcequota_retries(uninstallable_release: Release) -> None: + fail_result = ExecResult( + False, + 1, + "", + "Error: INSTALLATION FAILED: create: failed to create: Operation cannot be " + 'fulfilled on resourcequotas "resource-quota": the object has been ' + "modified; please apply your changes to the latest version and try again\n", + ) + + with patch("aisitools.k8s_sandbox._helm.INSTALL_RETRY_DELAY_SECONDS", 0): + with patch( + "aisitools.k8s_sandbox._helm._run_subprocess", return_value=fail_result + ) as mock: + with pytest.raises(Exception) as excinfo: + await uninstallable_release.install() + + assert mock.call_count == 3 + assert "resourcequotas" in str(excinfo.value) + + +@pytest.mark.parametrize("value", ["0", "-1", "abcd"]) +async def test_invalid_helm_timeout( + uninstallable_release: Release, value: str, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setenv("INSPECT_HELM_TIMEOUT", value) + + with pytest.raises(ValueError): + await uninstallable_release.install() diff --git a/test/k8s_sandbox/test_inspect_self_check.py b/test/k8s_sandbox/test_inspect_self_check.py new file mode 100644 index 0000000..58a6e7c --- /dev/null +++ b/test/k8s_sandbox/test_inspect_self_check.py @@ -0,0 +1,63 @@ +from typing import AsyncGenerator + +import pytest_asyncio +from inspect_ai.util import SandboxEnvironment +from inspect_ai.util._sandbox.self_check import self_check + +from aisitools.k8s_sandbox._sandbox_environment import K8sSandboxEnvironment +from test.aisitools.k8s_sandbox.utils import install_sandbox_environments + + +@pytest_asyncio.fixture(scope="module") +async def sandboxes() -> AsyncGenerator[dict[str, K8sSandboxEnvironment], None]: + async with install_sandbox_environments(__file__, "values.yaml") as envs: + yield envs + + +@pytest_asyncio.fixture(scope="module") +async def root(sandboxes: dict[str, K8sSandboxEnvironment]) -> K8sSandboxEnvironment: + return sandboxes["default"] + + +@pytest_asyncio.fixture(scope="module") +async def non_root( + sandboxes: dict[str, K8sSandboxEnvironment], +) -> K8sSandboxEnvironment: + return sandboxes["nonroot"] + + +async def test_self_check_k8s_default_root(root: SandboxEnvironment) -> None: + known_failures = [ + # The user parameter is not supported in K8sSandboxEnvironment. + "test_exec_as_user", + "test_exec_as_nonexistent_user", + # Root can read from files after `chmod -r`. + "test_read_file_not_allowed", + # Root can write to files after `chmod -w`. + "test_write_file_without_permissions", + ] + + return await _run_self_check(root, known_failures) + + +async def test_self_check_k8s_non_root(non_root: SandboxEnvironment) -> None: + known_failures = [ + # The user parameter is not supported in K8sSandboxEnvironment. + "test_exec_as_user", + "test_exec_as_nonexistent_user", + ] + + return await _run_self_check(non_root, known_failures) + + +async def _run_self_check( + sandbox_env: SandboxEnvironment, known_failures: list[str] = [] +) -> None: + """Self-check is the name of Inspect's test suite which runs against sandboxes.""" + self_check_results = await self_check(sandbox_env) + failures = [] + for test_name, result in self_check_results.items(): + if result is not True and test_name not in known_failures: + failures.append(f"Test {test_name} failed: {result}") + if failures: + assert False, "\n".join(failures) diff --git a/test/k8s_sandbox/test_limited_stream_buffer.py b/test/k8s_sandbox/test_limited_stream_buffer.py new file mode 100644 index 0000000..23c68ca --- /dev/null +++ b/test/k8s_sandbox/test_limited_stream_buffer.py @@ -0,0 +1,97 @@ +import pytest + +from aisitools.k8s_sandbox._pod.buffer import LimitedBuffer + + +@pytest.fixture +def invalid_utf8() -> bytes: + return b"\x80" + + +def test_does_not_truncate(): + sut = LimitedBuffer(1024) + + sut.append(b"abcde") + sut.append(b"fghij") + actual = str(sut) + + assert actual == "abcdefghij" + assert not sut.truncated + + +def test_does_not_truncate_on_limit(): + sut = LimitedBuffer(5) + + sut.append(b"abcde") + actual = str(sut) + + assert actual == "abcde" + assert not sut.truncated + + +def test_truncates_ascii_one_append(): + sut = LimitedBuffer(5) + + sut.append(b"abcdefghij") + actual = str(sut) + + assert actual == "abcde" + assert sut.truncated + + +def test_truncates_ascii_multiple_appends(): + sut = LimitedBuffer(5) + + sut.append(b"abcde") + sut.append(b"fghij") + actual = str(sut) + + assert actual == "abcde" + assert sut.truncated + + +def test_truncates_unicode(): + sut = LimitedBuffer(7) + + # a: 1 byte, Ǟ: 2 bytes, 😀: 4 bytes + sut.append("aǞ😀xxx".encode("utf-8")) + actual = str(sut) + + assert actual == "aǞ😀" + assert _count_bytes(actual) == 7 + assert sut.truncated + + +def test_truncates_unicode_without_raising_decode_error(): + sut = LimitedBuffer(5) + + # 😀: 4 bytes + sut.append("abcd😀".encode("utf-8")) + actual = str(sut) + + # The 4-byte character is simply discarded. + assert actual == "abcd" + assert _count_bytes(actual) == 4 + assert sut.truncated + + +def test_raises_unicode_decode_error(invalid_utf8: bytes): + sut = LimitedBuffer(1024) + + sut.append(b"abcde" + invalid_utf8 + b"fghij") + + with pytest.raises(UnicodeDecodeError): + str(sut) + + +def test_raises_unicode_decode_error_at_end_if_not_truncated(invalid_utf8: bytes): + sut = LimitedBuffer(1024) + + sut.append(b"abcde" + invalid_utf8) + + with pytest.raises(UnicodeDecodeError): + str(sut) + + +def _count_bytes(string: str) -> int: + return len(string.encode("utf-8")) diff --git a/test/k8s_sandbox/test_logger.py b/test/k8s_sandbox/test_logger.py new file mode 100644 index 0000000..748df6f --- /dev/null +++ b/test/k8s_sandbox/test_logger.py @@ -0,0 +1,78 @@ +import pytest +from pytest import MonkeyPatch + +from aisitools.k8s_sandbox._logger import format_log_message + + +@pytest.fixture +def str_2000_chars() -> str: + return "0123456789" * 200 + + +def test_format_log_message() -> None: + result = format_log_message("My message.") + + assert result == "My message." + + +def test_format_log_message_with_kwargs() -> None: + result = format_log_message("My message.", a="1", b="2", c="3") + + assert result == 'My message. {"a": "1", "b": "2", "c": "3"}' + + +def test_format_log_message_with_non_str_kwargs() -> None: + result = format_log_message("My message.", a=1, b=2.0, c=Exception("3")) + + assert result == 'My message. {"a": "1", "b": "2.0", "c": "3"}' + + +def test_format_log_message_truncates_values(str_2000_chars: str) -> None: + result = format_log_message("My message.", my_value=str_2000_chars) + + assert len(result) < 1100 + assert result.endswith('..."}') + + +def test_format_log_message_escapes_values() -> None: + value = "'\"\\" + result = format_log_message("My message.", my_value=value) + + assert result == 'My message. {"my_value": "\'\\"\\\\"}' + + +def test_format_log_message_non_ascii() -> None: + value = "日本語😀" + result = format_log_message("My message.", my_value=value) + + assert result == 'My message. {"my_value": "日本語😀"}' + + +def test_truncation_threshold_is_loaded_from_env_var( + str_2000_chars: str, monkeypatch: MonkeyPatch +) -> None: + monkeypatch.setenv("INSPECT_K8S_LOG_TRUNCATION_THRESHOLD", "100") + + result = format_log_message("My message.", myvalue=str_2000_chars) + + assert len(result) < 200 + + +def test_truncation_threshold_with_invalid_env_var( + str_2000_chars: str, monkeypatch: MonkeyPatch +) -> None: + monkeypatch.setenv("INSPECT_K8S_LOG_TRUNCATION_THRESHOLD", "invalid") + + result = format_log_message("My message.", myvalue=str_2000_chars) + + assert 1000 < len(result) < 1100 + + +def test_truncation_threshold_with_unset_env_var( + str_2000_chars: str, monkeypatch: MonkeyPatch +) -> None: + monkeypatch.delenv("INSPECT_K8S_LOG_TRUNCATION_THRESHOLD", raising=False) + + result = format_log_message("My message.", myvalue=str_2000_chars) + + assert 1000 < len(result) < 1100 diff --git a/test/k8s_sandbox/test_network_policy.py b/test/k8s_sandbox/test_network_policy.py new file mode 100644 index 0000000..0e71a82 --- /dev/null +++ b/test/k8s_sandbox/test_network_policy.py @@ -0,0 +1,68 @@ +from typing import AsyncGenerator + +import pytest +import pytest_asyncio + +from aisitools.k8s_sandbox._sandbox_environment import K8sSandboxEnvironment +from test.aisitools.k8s_sandbox.utils import install_sandbox_environments + +# Mark all tests in this module as requiring a Kubernetes cluster. +pytestmark = pytest.mark.req_k8s + + +@pytest_asyncio.fixture(scope="module") +async def sandbox() -> AsyncGenerator[K8sSandboxEnvironment, None]: + async with install_sandbox_environments(__file__, "netpol-values.yaml") as envs: + yield envs["default"] + + +@pytest_asyncio.fixture(scope="module") +async def sandbox_entities_world() -> AsyncGenerator[K8sSandboxEnvironment, None]: + async with install_sandbox_environments( + __file__, "netpol-world-values.yaml" + ) as envs: + yield envs["default"] + + +async def test_allowed_fqdn(sandbox: K8sSandboxEnvironment) -> None: + result = await sandbox.exec(["curl", "-I", "https://google.com"], timeout=10) + + assert result.returncode == 0 + + +async def test_blocked_fqdn(sandbox: K8sSandboxEnvironment) -> None: + with pytest.raises(TimeoutError): + await sandbox.exec(["wget", "https://yahoo.com"], timeout=10) + + +async def test_allowed_cidr(sandbox: K8sSandboxEnvironment) -> None: + result = await sandbox.exec(["curl", "-I", "1.1.1.1"], timeout=10) + + assert result.returncode == 0 + + +async def test_blocked_cidr(sandbox: K8sSandboxEnvironment) -> None: + with pytest.raises(TimeoutError): + await sandbox.exec(["curl", "-I", "8.8.8.8"], timeout=10) + + +async def test_allowed_entity(sandbox_entities_world: K8sSandboxEnvironment) -> None: + # allowEntities: ["world"] + result = await sandbox_entities_world.exec(["curl", "-I", "yahoo.com"], timeout=10) + + assert result.returncode == 0 + + +async def test_pip_install(sandbox: K8sSandboxEnvironment) -> None: + result = await sandbox.exec( + [ + "bash", + "-c", + "pip install --no-cache-dir --no-input requests > /dev/null 2>&1 && " + "echo 'success' || echo 'failed'", + ], + # Test occasionally failed with TimeoutError when timeout is set to 10 + timeout=30, + ) + + assert result.stdout.strip() == "success" diff --git a/test/k8s_sandbox/test_networks.py b/test/k8s_sandbox/test_networks.py new file mode 100644 index 0000000..3691088 --- /dev/null +++ b/test/k8s_sandbox/test_networks.py @@ -0,0 +1,39 @@ +from typing import AsyncGenerator + +import pytest +import pytest_asyncio + +from aisitools.k8s_sandbox._sandbox_environment import K8sSandboxEnvironment +from test.aisitools.k8s_sandbox.utils import install_sandbox_environments + +# Mark all tests in this module as requiring a Kubernetes cluster. +pytestmark = pytest.mark.req_k8s + + +@pytest_asyncio.fixture(scope="module") +async def sandbox() -> AsyncGenerator[K8sSandboxEnvironment, None]: + async with install_sandbox_environments(__file__, "networks-values.yaml") as envs: + yield envs["default"] + + +async def can_ping_service(service: str, sandbox: K8sSandboxEnvironment) -> bool: + result = await sandbox.exec(["ping", "-c", "1", service], timeout=3) + return "1 packets transmitted, 1 received" in result.stdout + + +async def test_can_ping_service_on_same_network( + sandbox: K8sSandboxEnvironment, +): + assert await can_ping_service("victim1", sandbox) + + +async def test_cannot_ping_service_on_no_network( + sandbox: K8sSandboxEnvironment, +): + assert not await can_ping_service("victim2", sandbox) + + +async def test_cannot_ping_service_on_different_network( + sandbox: K8sSandboxEnvironment, +): + assert not await can_ping_service("victim3", sandbox) diff --git a/test/k8s_sandbox/test_prereqs.py b/test/k8s_sandbox/test_prereqs.py new file mode 100644 index 0000000..61017d6 --- /dev/null +++ b/test/k8s_sandbox/test_prereqs.py @@ -0,0 +1,18 @@ +from unittest.mock import patch + +import pytest +from inspect_ai._util.error import PrerequisiteError + +from aisitools.k8s_sandbox._prereqs import validate_prereqs + + +async def test_helm_version_too_low() -> None: + with patch("aisitools.k8s_sandbox._prereqs.MINIMUM_HELM_VERSION", "999.0.0"): + with pytest.raises(PrerequisiteError) as error: + await validate_prereqs() + + assert error.match("Found version") + + +async def test_helm_version_satisfactory() -> None: + await validate_prereqs() diff --git a/test/k8s_sandbox/test_runtime_class.py b/test/k8s_sandbox/test_runtime_class.py new file mode 100644 index 0000000..1a25a6b --- /dev/null +++ b/test/k8s_sandbox/test_runtime_class.py @@ -0,0 +1,49 @@ +from typing import AsyncGenerator + +import pytest +import pytest_asyncio + +from aisitools.k8s_sandbox._sandbox_environment import K8sSandboxEnvironment +from test.aisitools.k8s_sandbox.utils import install_sandbox_environments + +# Mark all tests in this module as requiring a Kubernetes cluster. +pytestmark = pytest.mark.req_k8s + + +@pytest_asyncio.fixture(scope="module") +async def sandboxes() -> AsyncGenerator[dict[str, K8sSandboxEnvironment], None]: + async with install_sandbox_environments( + __file__, "runtime-class-values.yaml" + ) as envs: + yield envs + + +async def test_default(sandboxes: dict[str, K8sSandboxEnvironment]) -> None: + actual = await _infer_runtime_class(sandboxes["default"]) + + assert actual == "gvisor" + + +async def test_gvisor_specified(sandboxes: dict[str, K8sSandboxEnvironment]) -> None: + actual = await _infer_runtime_class(sandboxes["gvisor-specified"]) + + assert actual == "gvisor" + + +async def test_runc_specified(sandboxes: dict[str, K8sSandboxEnvironment]) -> None: + actual = await _infer_runtime_class(sandboxes["runc-specified"]) + + assert actual == "runc" + + +async def test_unspecified(sandboxes: dict[str, K8sSandboxEnvironment]) -> None: + actual = await _infer_runtime_class(sandboxes["unspecified"]) + + assert actual == "gvisor" + + +async def _infer_runtime_class(sandbox: K8sSandboxEnvironment) -> str: + result = await sandbox.exec( + ["sh", "-c", "dmesg | grep -i 'starting gvisor'"], timeout=5 + ) + return "gvisor" if result.returncode == 0 else "runc" diff --git a/test/k8s_sandbox/test_sandbox.py b/test/k8s_sandbox/test_sandbox.py new file mode 100644 index 0000000..44805fd --- /dev/null +++ b/test/k8s_sandbox/test_sandbox.py @@ -0,0 +1,721 @@ +import logging +import os +from textwrap import dedent +from typing import AsyncGenerator +from unittest.mock import patch + +import pytest +import pytest_asyncio +from inspect_ai.util import OutputLimitExceededError, SandboxEnvironmentLimits +from kubernetes.stream.ws_client import ApiException, WSClient # type: ignore +from pytest import LogCaptureFixture + +from aisitools.k8s_sandbox._sandbox_environment import K8sError, K8sSandboxEnvironment +from test.aisitools.k8s_sandbox.utils import install_sandbox_environments + +# Mark all tests in this module as requiring a Kubernetes cluster. +pytestmark = pytest.mark.req_k8s + + +@pytest_asyncio.fixture(scope="module") +async def sandboxes() -> AsyncGenerator[dict[str, K8sSandboxEnvironment], None]: + async with install_sandbox_environments(__file__, "values.yaml") as envs: + yield envs + + +@pytest_asyncio.fixture(scope="module") +async def sandbox(sandboxes: dict[str, K8sSandboxEnvironment]) -> K8sSandboxEnvironment: + default_sandbox = sandboxes["default"] + cwd = (await default_sandbox.exec(["pwd"])).stdout.strip() + assert cwd == "/root", ( + "Some tests assume that the pod's cwd is /root. " + f"From running `pwd`, it appears to be '{cwd}'." + ) + return default_sandbox + + +@pytest_asyncio.fixture(scope="module") +async def sandbox_non_root( + sandboxes: dict[str, K8sSandboxEnvironment], +) -> K8sSandboxEnvironment: + return sandboxes["nonroot"] + + +@pytest_asyncio.fixture(scope="module") +async def sandbox_busybox( + sandboxes: dict[str, K8sSandboxEnvironment], +) -> K8sSandboxEnvironment: + return sandboxes["busybox"] + + +@pytest.fixture +def binary_data() -> bytes: + return bytes(range(256)) + + +@pytest.fixture +def log_err(caplog: LogCaptureFixture) -> LogCaptureFixture: + # Note: this will prevent lower level messages from being shown in pytest output. + caplog.set_level(logging.ERROR) + return caplog + + +### exec() ### + + +@pytest.mark.parametrize( + "cmd", [["echo", "Hello, World!"], ["bash", "-c", "echo Hello, World!"]] +) +async def test_exec_with_success( + sandbox: K8sSandboxEnvironment, cmd: list[str] +) -> None: + result = await sandbox.exec(cmd) + + assert result.success + assert result.returncode == 0 + assert result.stdout.strip() == "Hello, World!" + assert result.stderr == "" + + +async def test_exec_with_error(sandbox: K8sSandboxEnvironment) -> None: + # sudo is not installed in the container. + result = await sandbox.exec(["sudo"]) + + assert not result.success + assert result.returncode == 127 + assert result.stdout == "" + assert "sudo: not found" in result.stderr.casefold() + + +async def test_exec_with_error_via_bash(sandbox: K8sSandboxEnvironment) -> None: + # sudo is not installed in the container. + result = await sandbox.exec(["bash", "-c", "sudo"]) + + assert not result.success + assert result.returncode == 127 + assert result.stdout == "" + assert "command not found" in result.stderr.casefold() + + +async def test_exec_flushes_stderr(sandbox: K8sSandboxEnvironment) -> None: + head_limit = 1024 # 1 KiB + + result = await sandbox.exec(["sh", "-c", f"yes | head -c {head_limit} 1>&2"]) + + assert result.success + assert len(result.stderr) == head_limit + + +async def test_exec_stdin_as_text(sandbox: K8sSandboxEnvironment) -> None: + cmd = """read value && echo "Echoed from stdin: $value" """ + + result = await sandbox.exec(["bash", "-c", cmd], input="success\n", timeout=5) + + assert result.success + assert result.returncode == 0 + assert "Echoed from stdin: success" in result.stdout + assert result.stderr == "" + + +async def test_exec_stdin_with_newlines(sandbox: K8sSandboxEnvironment) -> None: + result = await sandbox.exec( + ["bash", "-c", "cat"], input="some\nnew\nlines", timeout=5 + ) + + assert result.success + assert result.returncode == 0 + assert "some\nnew\nlines" in result.stdout + assert result.stderr == "" + + +async def test_exec_stdin_empty(sandbox: K8sSandboxEnvironment) -> None: + result = await sandbox.exec(["bash", "-c", "cat"], input="", timeout=5) + + assert result.success + assert result.returncode == 0 + assert result.stdout == "" + assert result.stderr == "" + + +async def test_exec_stdin_requiring_quotes(sandbox: K8sSandboxEnvironment) -> None: + stdin = "cmd_injection'\";\n\n 😀 exit 1" + + result = await sandbox.exec(["bash", "-c", "cat"], input=stdin, timeout=5) + + assert result.success + assert result.returncode == 0 + assert result.stdout == stdin + assert result.stderr == "" + + +async def test_exec_stdin_as_bytes( + sandbox: K8sSandboxEnvironment, binary_data: bytes +) -> None: + await sandbox.exec(["bash", "-c", "cat > file.bin"], input=binary_data, timeout=5) + + actual = await sandbox.read_file("file.bin", text=False) + assert actual == binary_data + + +async def test_exec_cwd_absolute(sandbox: K8sSandboxEnvironment) -> None: + result = await sandbox.exec(["pwd"], cwd="/tmp", timeout=10) + + assert result.success + assert result.stdout.strip() == "/tmp" + + +async def test_exec_cwd_relative(sandbox: K8sSandboxEnvironment) -> None: + await sandbox.exec(["mkdir", "-p", "/root/exec-cwd"]) + + result = await sandbox.exec(["pwd"], cwd="exec-cwd") + + assert result.success + assert result.stdout.strip() == "/root/exec-cwd" + + +async def test_exec_cwd_with_spaces(sandbox: K8sSandboxEnvironment) -> None: + await sandbox.exec(["mkdir", "-p", "/root/dir with spaces"]) + + result = await sandbox.exec(["pwd"], cwd="/root/dir with spaces") + + assert result.success + assert result.stdout.strip() == "/root/dir with spaces" + + +async def test_exec_cwd_does_not_exist(sandbox: K8sSandboxEnvironment) -> None: + result = await sandbox.exec(["pwd"], cwd="/exec-cwd/does/not/exist") + + assert result.returncode == 2 + assert "can't cd to /exec-cwd/does/not/exist" in result.stderr + + +async def test_exec_env(sandbox: K8sSandboxEnvironment) -> None: + result = await sandbox.exec(["bash", "-c", "echo $FOO"], env={"FOO": "bar"}) + + assert result.success + assert result.stdout.strip() == "bar" + + +async def test_exec_env_not_persisted(sandbox: K8sSandboxEnvironment) -> None: + await sandbox.exec(["bash", "-c", "echo $FOO"], env={"FOO": "bar"}) + result = await sandbox.exec(["bash", "-c", "echo $FOO"]) + + assert result.success + assert result.stdout.strip() == "" + + +async def test_exec_env_invalid_keys(sandbox: K8sSandboxEnvironment) -> None: + result1 = await sandbox.exec(["bash", "-c", "echo $FOO"], env={"": "bar"}) + result2 = await sandbox.exec(["bash", "-c", "echo $FOO"], env={"F'O": "bar"}) + + assert not result1.success + assert not result2.success + + +async def test_exec_env_empty_values(sandbox: K8sSandboxEnvironment) -> None: + result = await sandbox.exec(["bash", "-c", "echo $FOO"], env={"FOO": ""}) + + assert result.success + assert result.stdout.strip() == "" + + +async def test_exec_env_requiring_quotes(sandbox: K8sSandboxEnvironment) -> None: + result = await sandbox.exec(["bash", "-c", "echo $FOO"], env={"FOO": "b'\"a r"}) + + assert result.success + assert result.stdout.strip() == "b'\"a r" + + +@pytest.mark.parametrize("cmd", [["bash", "-c", "sleep 10"], ["sleep", "10"]]) +async def test_exec_timeout( + sandbox: K8sSandboxEnvironment, cmd: list[str], log_err: LogCaptureFixture +) -> None: + with pytest.raises(TimeoutError) as excinfo: + await sandbox.exec(cmd, timeout=1) + + assert "Command timed out after 1s" in str(excinfo) + assert not log_err.records + + +async def test_exec_raises_permission_error( + sandbox: K8sSandboxEnvironment, log_err: LogCaptureFixture +) -> None: + with pytest.raises(PermissionError): + # /etc/hosts is not an executable file. + await sandbox.exec(["bash", "-c", "/etc/hosts"]) + + assert not log_err.records + + +async def test_exec_does_not_raise_permission_error( + sandbox: K8sSandboxEnvironment, +) -> None: + result = await sandbox.exec(["mount", "-o", "remount,ro", "/"]) + + # Despite "permission denied" being in stderr, the error was not 126 + # "Command invoked cannot execute". + # https://tldp.org/LDP/abs/html/exitcodes.html#EXITCODESREF + # So no exception is raised. + assert "permission denied" in result.stderr.casefold() + + +async def test_exec_timeout_terminates_foreground_commands( + sandbox: K8sSandboxEnvironment, +) -> None: + sentinel = "/tmp/timeout-terminate.txt" + with pytest.raises(TimeoutError): + await sandbox.exec(["bash", "-c", f"sleep 2; touch {sentinel}"], timeout=1) + + # Wait for the sleep process to complete and check that no file was created. + await sandbox.exec(["sleep", "2"]) + file_exists_result = await sandbox.exec(["test", "!", "-e", sentinel]) + + assert file_exists_result.success + + +async def test_exec_unicode_decode_error(sandbox: K8sSandboxEnvironment) -> None: + with pytest.raises(UnicodeDecodeError): + await sandbox.exec(["head", "-c", "1024", "/bin/ls"]) + + +async def test_exec_background_returns(sandbox: K8sSandboxEnvironment) -> None: + # Check that a backgrounded process does not prevent the exec call from returning. + result = await sandbox.exec(["bash", "-c", "sleep infinity &"]) + + assert result.success + + +async def test_exec_background_completes(sandbox: K8sSandboxEnvironment) -> None: + sentinel = "/tmp/bg-complete.txt" + + result = await sandbox.exec(["bash", "-c", f"(sleep 1 && touch {sentinel}) &"]) + + assert result.success + + # Wait for the background process to complete and check that it was successful. + await sandbox.exec(["sleep", "2"]) + file_exists_result = await sandbox.exec(["test", "-e", sentinel]) + assert file_exists_result.success + + +async def test_exec_background_completes_with_timeout( + sandbox: K8sSandboxEnvironment, +) -> None: + sentinel = "/tmp/bg-timeout-complete.txt" + + # The shell will be terminated after 1s, but we don't raise a TimeoutError because + # the user-supplied command was backgrounded. + await sandbox.exec(["bash", "-c", f"(sleep 2 && touch {sentinel}) &"], timeout=1) + + # Wait for the background process to complete and check that it was successful. + await sandbox.exec(["sleep", "3"]) + file_exists_result = await sandbox.exec(["test", "-e", sentinel]) + assert file_exists_result.success + + +async def test_exec_file_not_found_error(sandbox: K8sSandboxEnvironment) -> None: + result = await sandbox.exec(["cat", "/does/not/exist"]) + + assert not result.success + assert result.returncode != 0 + assert result.stdout == "" + assert "no such file or directory" in result.stderr.casefold() + + +async def test_exec_stdout_truncation(sandbox: K8sSandboxEnvironment) -> None: + limit_10_MiB = 10 * 1024**2 # 10 MiB + with pytest.raises(OutputLimitExceededError) as excinfo: + head_limit = limit_10_MiB + 1024 # 10 MiB + 1 KiB + await sandbox.exec(["bash", "-c", f"yes | head -c {head_limit}"]) + + truncated_output = excinfo.value.truncated_output + assert truncated_output and len(truncated_output) == limit_10_MiB + + +async def test_exec_stderr_truncation(sandbox: K8sSandboxEnvironment) -> None: + limit_10_MiB = 10 * 1024**2 # 10 MiB + with pytest.raises(OutputLimitExceededError) as excinfo: + head_limit = limit_10_MiB + 1024 # 10 MiB + 1 KiB + await sandbox.exec(["bash", "-c", f"yes | head -n {head_limit} 1>&2"]) + + truncated_output = excinfo.value.truncated_output + assert truncated_output and len(truncated_output) == limit_10_MiB + + +async def test_exec_api_error( + sandbox: K8sSandboxEnvironment, log_err: LogCaptureFixture +) -> None: + with patch.object( + WSClient, "read_channel", side_effect=ApiException(reason="my-reason") + ): + with pytest.raises(K8sError) as excinfo: + await sandbox.exec(["true"]) + + assert "my-reason" in str(excinfo.value.__cause__) + # ApiException should be logged as an error. + assert "my-reason" in log_err.records[0].message + + +async def test_exec_inspect_python_tool(sandbox: K8sSandboxEnvironment) -> None: + python_code = dedent(""" + import os + print(f"Home: {os.getenv('HOME')}") + print("new\\nline") + """) + + # This is how Inspect's standard python tool is implemented. + result = await sandbox.exec(["python3"], input=python_code, timeout=5) + + assert result.success + assert result.returncode == 0 + assert result.stdout.splitlines() == ["Home: /root", "new", "line"] + assert result.stderr == "" + + +async def test_exec_complex_bash_commend(sandbox: K8sSandboxEnvironment) -> None: + # This is similar to how an AISI-internal stateful bash tool is implemented. + cmd_template = dedent(""" + finally() {{ + pwd > /tmp/bash_tool_last_cwd + export -p > /tmp/bash_tool_last_env + }} + trap 'finally' EXIT + + if [ -f /tmp/bash_tool_last_env ]; then + source /tmp/bash_tool_last_env &> /dev/null + fi + + if [ -f /tmp/bash_tool_last_cwd ]; then + cd $(cat /tmp/bash_tool_last_cwd) &> /dev/null + fi + + {command} + """) + + await sandbox.exec( + ["bash", "-c", cmd_template.format(command="cd /tmp && export FOO=bar")], + timeout=5, + ) + result = await sandbox.exec( + ["bash", "-c", cmd_template.format(command="pwd && echo $FOO")], timeout=5 + ) + + assert result.success + assert result.returncode == 0 + assert result.stdout.splitlines() == ["/tmp", "bar"] + assert result.stderr == "" + + +async def test_exec_backgrounded_command_and_non_zero_exit_code( + sandbox: K8sSandboxEnvironment, +) -> None: + command = ["bash", "-c", "sleep infinity & exit 42"] + + result = await sandbox.exec(command, timeout=5) + + assert result.returncode == 42 + + +async def test_exec_only_executes_once(sandbox: K8sSandboxEnvironment) -> None: + # Historical issue: The remote command was being run twice. + result = await sandbox.exec(["mkdir", "/tmp/only-once"]) + + assert result.success + + +@pytest.mark.repeat(100) +async def test_exec_reliability(sandbox: K8sSandboxEnvironment) -> None: + # Historical issue: sentinel value written to stdout occasionally appeared to be + # buffered and only flushed once the process had completed (by `timeout`). Verify + # that a simple command can be executed reliably without resulting in a timeout. + result = await sandbox.exec(["pwd"], timeout=5) + assert result.success + + +async def test_exec_timeout_which_ignores_sigterm( + sandbox: K8sSandboxEnvironment, +) -> None: + # Historical issue: certain commands ignore SIGTERM sent by timeout (e.g. mpg123 + # under certain conditions). Ensure that the command cannot run forever. + result = await sandbox.exec( + ["bash", "-c", "trap '' TERM; sleep infinity"], timeout=1 + ) + + assert result.returncode == 137 + assert result.stderr == "Killed\n" + + +async def test_api_timeout_is_not_triggered_by_long_running_commands( + sandbox: K8sSandboxEnvironment, +) -> None: + with patch("aisitools.k8s_sandbox._pod.op.API_TIMEOUT", 1): + result = await sandbox.exec(["sleep", "3"]) + + assert result.success + assert result.returncode == 0 + + +### #write_file() ### + + +async def test_write_file(sandbox: K8sSandboxEnvironment) -> None: + dst = "test-write-file.txt" + + await sandbox.write_file(dst, "Hello, World!") + + cat_result = await sandbox.exec(["cat", dst]) + assert cat_result.stdout == "Hello, World!" + # See round-trip test for binary data verification. + + +async def test_write_file_requiring_quotes(sandbox: K8sSandboxEnvironment) -> None: + # The spaces in the filename require quotes. Also verify that quotes are escaped. + dst = "test write file requiring '\"quotes\"'.txt" + + await sandbox.write_file(dst, "Hello, World!") + + cat_result = await sandbox.exec(["cat", dst]) + assert cat_result.stdout == "Hello, World!" + + +# The pod's cwd is /root. +@pytest.mark.parametrize("dst_dir", ["/", "/tmp-abs", "tmp-rel", "../tmp-rel"]) +async def test_write_file_absolute_and_relative( + sandbox: K8sSandboxEnvironment, dst_dir: str +) -> None: + dst = os.path.join(dst_dir, "test_write_file_absolute_and_relative.txt") + content = f"Hello, World! {dst_dir}" + + await sandbox.write_file(dst, content) + + cat_result = await sandbox.exec(["cat", dst]) + assert cat_result.stdout == content + + +@pytest.mark.parametrize("dst_dir", ["/absolute/new/dir", "relative/new/dir"]) +async def test_write_file_creates_dirs( + sandbox: K8sSandboxEnvironment, dst_dir: str +) -> None: + dst = os.path.join(dst_dir, "test_write_file_creates_dirs.txt") + + await sandbox.write_file(dst, "Hello, World!") + + cat_result = await sandbox.exec(["cat", dst]) + assert cat_result.stdout == "Hello, World!" + + +async def test_write_file_generic_error( + sandbox: K8sSandboxEnvironment, log_err: LogCaptureFixture +) -> None: + with pytest.raises(K8sError) as excinfo: + await sandbox.write_file("/proc/version", "Hello, World!") + + assert "write error" in str(excinfo.value.__cause__) + # PodError should be logged as an error. + assert "write error" in log_err.records[0].message + + +async def test_write_file_api_error( + sandbox: K8sSandboxEnvironment, log_err: LogCaptureFixture +) -> None: + with patch.object( + WSClient, "read_channel", side_effect=ApiException(reason="my-reason") + ): + with pytest.raises(K8sError) as excinfo: + await sandbox.write_file("/api-error-file.txt", "Hello, World!") + + assert "my-reason" in str(excinfo.value.__cause__) + # ApiException should be logged as an error. + assert "my-reason" in log_err.records[0].message + + +async def test_write_file_overwrites_existing_regular_file( + sandbox: K8sSandboxEnvironment, +) -> None: + dst = "/test_write_file_overwrites_existing_regular_file.txt" + await sandbox.exec(["sh", "-c", f"echo 'original contents' > {dst}"]) + + await sandbox.write_file(dst, "new contents") + + cat_result = await sandbox.exec(["cat", dst]) + assert cat_result.stdout == "new contents" + + +async def test_write_file_overwrites_existing_special_file( + sandbox: K8sSandboxEnvironment, +) -> None: + # /etc/hosts behaves differently to regular files when written to. + await sandbox.write_file("/etc/hosts", "Hello, World!") + + cat_result = await sandbox.exec(["cat", "/etc/hosts"]) + assert cat_result.stdout == "Hello, World!" + + +async def test_write_file_permission_error( + sandbox_non_root: K8sSandboxEnvironment, log_err: LogCaptureFixture +) -> None: + with pytest.raises(PermissionError): + await sandbox_non_root.write_file("/root/file", "Hello, World!") + + assert not log_err.records + + +async def test_write_file_is_a_directory_error( + sandbox: K8sSandboxEnvironment, log_err: LogCaptureFixture +) -> None: + with pytest.raises(IsADirectoryError): + await sandbox.write_file("/root/", "Hello, World!") + + assert not log_err.records + + +### #read_file() ### + + +async def test_read_file(sandbox: K8sSandboxEnvironment) -> None: + result = await sandbox.read_file("/proc/version") + + assert "Linux" in result + # See round-trip test for binary data verification. + + +async def test_read_file_requiring_quotes(sandbox: K8sSandboxEnvironment) -> None: + # The spaces in the filename require quotes. Also verify that quotes are escaped. + file = "test read file requiring '\"quotes\"'.txt" + await sandbox.write_file(file, "Hello, World!") + + result = await sandbox.read_file(file) + + assert result == "Hello, World!" + + +async def test_read_file_from_relative_path(sandbox: K8sSandboxEnvironment) -> None: + await sandbox.write_file( + "/root/relative/dir/test_read_file_from_relative_path.txt", "Hello, World!" + ) + + # The current working directory is /root. + result = await sandbox.read_file( + "relative/dir/test_read_file_from_relative_path.txt" + ) + + assert result == "Hello, World!" + + +async def test_read_file_not_found( + sandbox: K8sSandboxEnvironment, log_err: LogCaptureFixture +) -> None: + with pytest.raises(FileNotFoundError): + await sandbox.read_file("/does/not/exist") + + assert not log_err.records + + +async def test_read_file_decode_error( + sandbox: K8sSandboxEnvironment, log_err: LogCaptureFixture +) -> None: + with pytest.raises(UnicodeDecodeError) as excinfo: + await sandbox.read_file("/bin/ls", text=True) + + assert "can't decode byte" in str(excinfo.value) + assert not log_err.records + + +async def test_read_file_permission_error( + sandbox_non_root: K8sSandboxEnvironment, log_err: LogCaptureFixture +) -> None: + with pytest.raises(PermissionError): + await sandbox_non_root.read_file("/etc/shadow") + + assert not log_err.records + + +async def test_read_file_is_a_directory_error( + sandbox: K8sSandboxEnvironment, log_err: LogCaptureFixture +) -> None: + with pytest.raises(IsADirectoryError): + await sandbox.read_file("/etc") + + assert not log_err.records + + +async def test_read_file_limit_exceeded( + sandbox: K8sSandboxEnvironment, log_err: LogCaptureFixture +) -> None: + await sandbox.write_file("large-file.txt", "a" * 2048) # 2KiB + + # Patch limit down to 1KiB for the test to save us from writing a 100MiB file. + with patch.object(SandboxEnvironmentLimits, "MAX_READ_FILE_SIZE", 1024): + with pytest.raises(OutputLimitExceededError): + await sandbox.read_file("large-file.txt", text=True) + + assert not log_err.records + + +async def test_read_file_api_error( + sandbox: K8sSandboxEnvironment, log_err: LogCaptureFixture +) -> None: + with patch.object( + WSClient, "read_channel", side_effect=ApiException(reason="my-reason") + ): + with pytest.raises(K8sError) as excinfo: + await sandbox.read_file("/etc/hosts") + + assert "my-reason" in str(excinfo.value.__cause__) + # ApiException should be logged as an error. + assert "my-reason" in log_err.records[0].message + + +### Round-trip ### + + +async def test_read_write_file_string_round_trip( + sandbox: K8sSandboxEnvironment, +) -> None: + pod_path = "/my/dir/round-trip.txt" + contents = "Hello, World!\nRound-trip test.\n" + + await sandbox.write_file(pod_path, contents) + result = await sandbox.read_file(pod_path, text=True) + + assert result == contents + + +async def test_read_write_file_bytes_round_trip( + sandbox: K8sSandboxEnvironment, binary_data: bytes +) -> None: + pod_path = "/my/dir/round-trip.bin" + contents = b"Hello, World!\nRound-trip test.\n" + binary_data + + await sandbox.write_file(pod_path, contents) + result = await sandbox.read_file(pod_path, text=False) + + assert result == contents + + +async def test_read_write_large_file_round_trip( + sandbox: K8sSandboxEnvironment, binary_data: bytes +) -> None: + pod_path = "/my/dir/round-trip-large.bin" + contents = binary_data[:100] * 1024**2 # 100 MiB + + await sandbox.write_file(pod_path, contents) + result = await sandbox.read_file(pod_path, text=False) + + assert result == contents + + +### Alternative images ### + + +async def test_sandbox_with_minimal_tools( + sandbox_busybox: K8sSandboxEnvironment, +) -> None: + # busybox has a minimal set of tools available. Verify that we're not relying on + # any tools that are not available. + exec_result = await sandbox_busybox.exec(["pwd"], timeout=5) + await sandbox_busybox.write_file("/tmp/test-busybox", "Hello, World!") + read_result = await sandbox_busybox.read_file("/tmp/test-busybox") + + assert exec_result.stdout.strip() == "/" + assert read_result == "Hello, World!" diff --git a/test/k8s_sandbox/test_volume.py b/test/k8s_sandbox/test_volume.py new file mode 100644 index 0000000..daeb649 --- /dev/null +++ b/test/k8s_sandbox/test_volume.py @@ -0,0 +1,22 @@ +from typing import AsyncGenerator + +import pytest +import pytest_asyncio + +from aisitools.k8s_sandbox._sandbox_environment import K8sSandboxEnvironment +from test.aisitools.k8s_sandbox.utils import install_sandbox_environments + +# Mark all tests in this module as requiring a Kubernetes cluster. +pytestmark = pytest.mark.req_k8s + + +@pytest_asyncio.fixture(scope="module") +async def sandbox() -> AsyncGenerator[K8sSandboxEnvironment, None]: + async with install_sandbox_environments(__file__, "volume-values.yaml") as envs: + yield envs["default"] + + +async def test_volumes(sandbox: K8sSandboxEnvironment): + result = await sandbox.read_file("/mount/test.txt") + + assert result == "test\n" diff --git a/test/k8s_sandbox/utils.py b/test/k8s_sandbox/utils.py new file mode 100644 index 0000000..6b59c7a --- /dev/null +++ b/test/k8s_sandbox/utils.py @@ -0,0 +1,28 @@ +from contextlib import asynccontextmanager +from pathlib import Path +from typing import AsyncGenerator + +from aisitools.k8s_sandbox._helm import Release +from aisitools.k8s_sandbox._sandbox_environment import K8sSandboxEnvironment + + +@asynccontextmanager +async def install_sandbox_environments( + task_name: str, values_filename: str | None +) -> AsyncGenerator[dict[str, K8sSandboxEnvironment], None]: + values_path = ( + Path(__file__).parent / "resources" / values_filename + if values_filename + else None + ) + release = Release(task_name=task_name, values_path=values_path) + try: + await release.install() + pods = await release.get_sandbox_pods() + sandbox_envs = { + pod_name: K8sSandboxEnvironment(release, pod) + for pod_name, pod in pods.items() + } + yield sandbox_envs + finally: + await release.uninstall(quiet=True) From 079668bf6c979cca3526f448a9eadd093925df07 Mon Sep 17 00:00:00 2001 From: "Craig.Walton" Date: Tue, 17 Dec 2024 13:47:20 +0000 Subject: [PATCH 4/7] Make test a package so that mypy doesn't consider k8s_sandbox to be duplicated. --- test/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/__init__.py diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 From 16f6ac161c3c35bb3e503caa25a356e3e4ac89a8 Mon Sep 17 00:00:00 2001 From: "Craig.Walton" Date: Tue, 17 Dec 2024 13:52:55 +0000 Subject: [PATCH 5/7] Initial migration: adjust package structure in imports. --- src/k8s_sandbox/__init__.py | 4 ++-- src/k8s_sandbox/_helm.py | 6 +++--- src/k8s_sandbox/_manager.py | 4 ++-- src/k8s_sandbox/_pod/__init__.py | 6 +++--- src/k8s_sandbox/_pod/error.py | 2 +- src/k8s_sandbox/_pod/execute.py | 6 +++--- src/k8s_sandbox/_pod/executor.py | 2 +- src/k8s_sandbox/_pod/get_returncode.py | 2 +- src/k8s_sandbox/_pod/op.py | 2 +- src/k8s_sandbox/_pod/pod.py | 10 +++++----- src/k8s_sandbox/_pod/read.py | 8 ++++---- src/k8s_sandbox/_pod/write.py | 6 +++--- src/k8s_sandbox/_sandbox_environment.py | 10 +++++----- test/k8s_sandbox/helm_chart/test_helm_chart.py | 4 ++-- .../custom_chart/test_integration.py | 6 +++--- .../inferred_values/test_integration.py | 4 ++-- .../multiple_sandbox_envs/test_integration.py | 4 ++-- .../sandbox_env_ordering/test_integration.py | 4 ++-- test/k8s_sandbox/inspect_integration/test_cleanup.py | 12 ++++++------ .../inspect_integration/test_default_values.py | 4 ++-- .../inspect_integration/testing_utils/mock_model.py | 2 +- .../inspect_integration/values/test_integration.py | 4 ++-- test/k8s_sandbox/pod/test_executor.py | 2 +- test/k8s_sandbox/pod/test_get_returncode.py | 2 +- test/k8s_sandbox/pod/test_pod.py | 2 +- test/k8s_sandbox/test_config_validation.py | 2 +- test/k8s_sandbox/test_dns.py | 4 ++-- test/k8s_sandbox/test_helm.py | 10 ++++------ test/k8s_sandbox/test_inspect_self_check.py | 4 ++-- test/k8s_sandbox/test_limited_stream_buffer.py | 2 +- test/k8s_sandbox/test_logger.py | 2 +- test/k8s_sandbox/test_network_policy.py | 4 ++-- test/k8s_sandbox/test_networks.py | 4 ++-- test/k8s_sandbox/test_prereqs.py | 4 ++-- test/k8s_sandbox/test_runtime_class.py | 4 ++-- test/k8s_sandbox/test_sandbox.py | 6 +++--- test/k8s_sandbox/test_volume.py | 4 ++-- test/k8s_sandbox/utils.py | 4 ++-- 38 files changed, 85 insertions(+), 87 deletions(-) diff --git a/src/k8s_sandbox/__init__.py b/src/k8s_sandbox/__init__.py index a48098c..6587c9b 100644 --- a/src/k8s_sandbox/__init__.py +++ b/src/k8s_sandbox/__init__.py @@ -1,5 +1,5 @@ -from aisitools.k8s_sandbox._pod import GetReturncodeError, PodError -from aisitools.k8s_sandbox._sandbox_environment import ( +from k8s_sandbox._pod import GetReturncodeError, PodError +from k8s_sandbox._sandbox_environment import ( K8sError, K8sSandboxEnvironment, K8sSandboxEnvironmentConfig, diff --git a/src/k8s_sandbox/_helm.py b/src/k8s_sandbox/_helm.py index bbb4fba..f83acd0 100644 --- a/src/k8s_sandbox/_helm.py +++ b/src/k8s_sandbox/_helm.py @@ -9,12 +9,12 @@ from kubernetes.client.rest import ApiException # type: ignore from shortuuid import uuid -from aisitools.k8s_sandbox._kubernetes_api import ( +from k8s_sandbox._kubernetes_api import ( get_current_context_namespace, k8s_client, ) -from aisitools.k8s_sandbox._logger import format_log_message, sandbox_log -from aisitools.k8s_sandbox._pod import Pod +from k8s_sandbox._logger import format_log_message, sandbox_log +from k8s_sandbox._pod import Pod DEFAULT_CHART = Path(__file__).parent / "resources" / "helm" / "agent-env" DEFAULT_TIMEOUT = 300 diff --git a/src/k8s_sandbox/_manager.py b/src/k8s_sandbox/_manager.py index 53adf5d..eab26ef 100644 --- a/src/k8s_sandbox/_manager.py +++ b/src/k8s_sandbox/_manager.py @@ -7,8 +7,8 @@ from rich.panel import Panel from rich.table import Table -from aisitools.k8s_sandbox._helm import Release -from aisitools.k8s_sandbox._helm import uninstall as helm_uninstall +from k8s_sandbox._helm import Release +from k8s_sandbox._helm import uninstall as helm_uninstall class HelmReleaseManager: diff --git a/src/k8s_sandbox/_pod/__init__.py b/src/k8s_sandbox/_pod/__init__.py index 9f22213..2630c43 100644 --- a/src/k8s_sandbox/_pod/__init__.py +++ b/src/k8s_sandbox/_pod/__init__.py @@ -1,6 +1,6 @@ -from aisitools.k8s_sandbox._pod.error import PodError -from aisitools.k8s_sandbox._pod.get_returncode import GetReturncodeError -from aisitools.k8s_sandbox._pod.pod import Pod +from k8s_sandbox._pod.error import PodError +from k8s_sandbox._pod.get_returncode import GetReturncodeError +from k8s_sandbox._pod.pod import Pod __all__ = [ "GetReturncodeError", diff --git a/src/k8s_sandbox/_pod/error.py b/src/k8s_sandbox/_pod/error.py index dbe4f67..ef5b08c 100644 --- a/src/k8s_sandbox/_pod/error.py +++ b/src/k8s_sandbox/_pod/error.py @@ -1,6 +1,6 @@ from typing import Any -from aisitools.k8s_sandbox._logger import format_log_message +from k8s_sandbox._logger import format_log_message class PodError(Exception): diff --git a/src/k8s_sandbox/_pod/execute.py b/src/k8s_sandbox/_pod/execute.py index 615f0b7..da18b98 100644 --- a/src/k8s_sandbox/_pod/execute.py +++ b/src/k8s_sandbox/_pod/execute.py @@ -8,9 +8,9 @@ from inspect_ai.util import SandboxEnvironmentLimits as limits from kubernetes.stream.ws_client import WSClient # type: ignore -from aisitools.k8s_sandbox._pod.buffer import LimitedBuffer -from aisitools.k8s_sandbox._pod.get_returncode import get_returncode -from aisitools.k8s_sandbox._pod.op import PodOperation +from k8s_sandbox._pod.buffer import LimitedBuffer +from k8s_sandbox._pod.get_returncode import get_returncode +from k8s_sandbox._pod.op import PodOperation COMPLETED_SENTINEL = "completed-sentinel-value" COMPLETED_SENTINEL_PATTERN = re.compile(rf"<{COMPLETED_SENTINEL}-(\d+)>") diff --git a/src/k8s_sandbox/_pod/executor.py b/src/k8s_sandbox/_pod/executor.py index 58e04a1..bacd8ed 100644 --- a/src/k8s_sandbox/_pod/executor.py +++ b/src/k8s_sandbox/_pod/executor.py @@ -7,7 +7,7 @@ from inspect_ai.util import concurrency -from aisitools.k8s_sandbox._logger import sandbox_log +from k8s_sandbox._logger import sandbox_log T = TypeVar("T") diff --git a/src/k8s_sandbox/_pod/get_returncode.py b/src/k8s_sandbox/_pod/get_returncode.py index 39719d7..e689e14 100644 --- a/src/k8s_sandbox/_pod/get_returncode.py +++ b/src/k8s_sandbox/_pod/get_returncode.py @@ -1,7 +1,7 @@ import yaml from kubernetes.stream.ws_client import ERROR_CHANNEL, WSClient # type: ignore -from aisitools.k8s_sandbox._pod.error import GetReturncodeError +from k8s_sandbox._pod.error import GetReturncodeError def get_returncode(ws_client: WSClient) -> int: diff --git a/src/k8s_sandbox/_pod/op.py b/src/k8s_sandbox/_pod/op.py index ab1c1ae..05fe586 100644 --- a/src/k8s_sandbox/_pod/op.py +++ b/src/k8s_sandbox/_pod/op.py @@ -6,7 +6,7 @@ from kubernetes.stream import stream # type: ignore from kubernetes.stream.ws_client import WSClient # type: ignore -from aisitools.k8s_sandbox._kubernetes_api import k8s_client +from k8s_sandbox._kubernetes_api import k8s_client # The duration to wait for an initial response from the k8s API server. # The initial response is received before the command is necessarily complete, so diff --git a/src/k8s_sandbox/_pod/pod.py b/src/k8s_sandbox/_pod/pod.py index 4011a16..2f73da0 100644 --- a/src/k8s_sandbox/_pod/pod.py +++ b/src/k8s_sandbox/_pod/pod.py @@ -5,11 +5,11 @@ from inspect_ai.util import ExecResult -from aisitools.k8s_sandbox._pod.execute import ExecuteOperation -from aisitools.k8s_sandbox._pod.executor import PodOpExecutor -from aisitools.k8s_sandbox._pod.op import PodInfo -from aisitools.k8s_sandbox._pod.read import ReadFileOperation -from aisitools.k8s_sandbox._pod.write import WriteFileOperation +from k8s_sandbox._pod.execute import ExecuteOperation +from k8s_sandbox._pod.executor import PodOpExecutor +from k8s_sandbox._pod.op import PodInfo +from k8s_sandbox._pod.read import ReadFileOperation +from k8s_sandbox._pod.write import WriteFileOperation T = TypeVar("T") diff --git a/src/k8s_sandbox/_pod/read.py b/src/k8s_sandbox/_pod/read.py index 2469d2e..d6595a6 100644 --- a/src/k8s_sandbox/_pod/read.py +++ b/src/k8s_sandbox/_pod/read.py @@ -6,10 +6,10 @@ from inspect_ai.util import SandboxEnvironmentLimits as limits from kubernetes.stream.ws_client import WSClient # type: ignore -from aisitools.k8s_sandbox._pod.buffer import LimitedBuffer -from aisitools.k8s_sandbox._pod.error import PodError -from aisitools.k8s_sandbox._pod.get_returncode import get_returncode -from aisitools.k8s_sandbox._pod.op import ( +from k8s_sandbox._pod.buffer import LimitedBuffer +from k8s_sandbox._pod.error import PodError +from k8s_sandbox._pod.get_returncode import get_returncode +from k8s_sandbox._pod.op import ( PodOperation, raise_for_known_read_write_errors, ) diff --git a/src/k8s_sandbox/_pod/write.py b/src/k8s_sandbox/_pod/write.py index cfa2197..b0e90ce 100644 --- a/src/k8s_sandbox/_pod/write.py +++ b/src/k8s_sandbox/_pod/write.py @@ -6,9 +6,9 @@ from kubernetes.stream.ws_client import WSClient # type: ignore -from aisitools.k8s_sandbox._pod.error import PodError -from aisitools.k8s_sandbox._pod.get_returncode import get_returncode -from aisitools.k8s_sandbox._pod.op import ( +from k8s_sandbox._pod.error import PodError +from k8s_sandbox._pod.get_returncode import get_returncode +from k8s_sandbox._pod.op import ( PodOperation, raise_for_known_read_write_errors, ) diff --git a/src/k8s_sandbox/_sandbox_environment.py b/src/k8s_sandbox/_sandbox_environment.py index 94f41f4..ef7c66e 100644 --- a/src/k8s_sandbox/_sandbox_environment.py +++ b/src/k8s_sandbox/_sandbox_environment.py @@ -13,14 +13,14 @@ ) from pydantic import BaseModel -from aisitools.k8s_sandbox._helm import Release -from aisitools.k8s_sandbox._logger import format_log_message, sandbox_log -from aisitools.k8s_sandbox._manager import ( +from k8s_sandbox._helm import Release +from k8s_sandbox._logger import format_log_message, sandbox_log +from k8s_sandbox._manager import ( HelmReleaseManager, uninstall_unmanaged_release, ) -from aisitools.k8s_sandbox._pod import Pod -from aisitools.k8s_sandbox._prereqs import validate_prereqs +from k8s_sandbox._pod import Pod +from k8s_sandbox._prereqs import validate_prereqs @sandboxenv(name="k8s") diff --git a/test/k8s_sandbox/helm_chart/test_helm_chart.py b/test/k8s_sandbox/helm_chart/test_helm_chart.py index 02803c2..8af4555 100644 --- a/test/k8s_sandbox/helm_chart/test_helm_chart.py +++ b/test/k8s_sandbox/helm_chart/test_helm_chart.py @@ -5,12 +5,12 @@ import pytest import yaml -import aisitools.k8s_sandbox +import k8s_sandbox @pytest.fixture def chart_dir() -> Path: - k8s_src = Path(aisitools.k8s_sandbox.__file__).parent.resolve() + k8s_src = Path(k8s_sandbox.__file__).parent.resolve() return k8s_src / "resources" / "helm" / "agent-env" diff --git a/test/k8s_sandbox/inspect_integration/custom_chart/test_integration.py b/test/k8s_sandbox/inspect_integration/custom_chart/test_integration.py index d09291c..04072b3 100644 --- a/test/k8s_sandbox/inspect_integration/custom_chart/test_integration.py +++ b/test/k8s_sandbox/inspect_integration/custom_chart/test_integration.py @@ -4,11 +4,11 @@ from inspect_ai.model import Model from inspect_ai.util import SandboxEnvironmentSpec -from aisitools.k8s_sandbox import K8sSandboxEnvironmentConfig -from test.aisitools.k8s_sandbox.inspect_integration.testing_utils.mock_model import ( +from k8s_sandbox import K8sSandboxEnvironmentConfig +from test.k8s_sandbox.inspect_integration.testing_utils.mock_model import ( MockToolCallModel, ) -from test.aisitools.k8s_sandbox.inspect_integration.testing_utils.utils import ( +from test.k8s_sandbox.inspect_integration.testing_utils.utils import ( create_task, run_and_verify_inspect_eval, tool_call, diff --git a/test/k8s_sandbox/inspect_integration/inferred_values/test_integration.py b/test/k8s_sandbox/inspect_integration/inferred_values/test_integration.py index 0d788be..913f1db 100644 --- a/test/k8s_sandbox/inspect_integration/inferred_values/test_integration.py +++ b/test/k8s_sandbox/inspect_integration/inferred_values/test_integration.py @@ -1,9 +1,9 @@ import pytest -from test.aisitools.k8s_sandbox.inspect_integration.testing_utils.mock_model import ( +from test.k8s_sandbox.inspect_integration.testing_utils.mock_model import ( MockToolCallModel, ) -from test.aisitools.k8s_sandbox.inspect_integration.testing_utils.utils import ( +from test.k8s_sandbox.inspect_integration.testing_utils.utils import ( create_task, run_and_verify_inspect_eval, tool_call, diff --git a/test/k8s_sandbox/inspect_integration/multiple_sandbox_envs/test_integration.py b/test/k8s_sandbox/inspect_integration/multiple_sandbox_envs/test_integration.py index c013ab5..982ef14 100644 --- a/test/k8s_sandbox/inspect_integration/multiple_sandbox_envs/test_integration.py +++ b/test/k8s_sandbox/inspect_integration/multiple_sandbox_envs/test_integration.py @@ -2,10 +2,10 @@ from inspect_ai.tool import Tool, ToolError, tool from inspect_ai.util import sandbox -from test.aisitools.k8s_sandbox.inspect_integration.testing_utils.mock_model import ( +from test.k8s_sandbox.inspect_integration.testing_utils.mock_model import ( MockToolCallModel, ) -from test.aisitools.k8s_sandbox.inspect_integration.testing_utils.utils import ( +from test.k8s_sandbox.inspect_integration.testing_utils.utils import ( create_task, run_and_verify_inspect_eval, tool_call, diff --git a/test/k8s_sandbox/inspect_integration/sandbox_env_ordering/test_integration.py b/test/k8s_sandbox/inspect_integration/sandbox_env_ordering/test_integration.py index 5b12973..88d1c5c 100644 --- a/test/k8s_sandbox/inspect_integration/sandbox_env_ordering/test_integration.py +++ b/test/k8s_sandbox/inspect_integration/sandbox_env_ordering/test_integration.py @@ -1,10 +1,10 @@ import pytest from inspect_ai.tool import bash -from test.aisitools.k8s_sandbox.inspect_integration.testing_utils.mock_model import ( +from test.k8s_sandbox.inspect_integration.testing_utils.mock_model import ( MockToolCallModel, ) -from test.aisitools.k8s_sandbox.inspect_integration.testing_utils.utils import ( +from test.k8s_sandbox.inspect_integration.testing_utils.utils import ( create_task, run_and_verify_inspect_eval, tool_call, diff --git a/test/k8s_sandbox/inspect_integration/test_cleanup.py b/test/k8s_sandbox/inspect_integration/test_cleanup.py index 405aa6b..d67519c 100644 --- a/test/k8s_sandbox/inspect_integration/test_cleanup.py +++ b/test/k8s_sandbox/inspect_integration/test_cleanup.py @@ -3,11 +3,11 @@ import pytest -from aisitools.k8s_sandbox._helm import uninstall -from test.aisitools.k8s_sandbox.inspect_integration.testing_utils.mock_model import ( +from k8s_sandbox._helm import uninstall +from test.k8s_sandbox.inspect_integration.testing_utils.mock_model import ( MockToolCallModel, ) -from test.aisitools.k8s_sandbox.inspect_integration.testing_utils.utils import ( +from test.k8s_sandbox.inspect_integration.testing_utils.utils import ( create_task, run_and_verify_inspect_eval, tool_call, @@ -21,7 +21,7 @@ def test_with_cleanup() -> None: model = MockToolCallModel([tool_call("bash", {"cmd": "echo 'success'"})]) task = create_task(__file__, target="success") - with patch("aisitools.k8s_sandbox._helm.uninstall", wraps=uninstall) as spy: + with patch("k8s_sandbox._helm.uninstall", wraps=uninstall) as spy: run_and_verify_inspect_eval(task=task, model=model) assert spy.call_count == 1 @@ -33,10 +33,10 @@ def test_without_cleanup() -> None: release = "no-clean" with patch( - "aisitools.k8s_sandbox._helm.Release._generate_release_name", + "k8s_sandbox._helm.Release._generate_release_name", return_value=release, ): - with patch("aisitools.k8s_sandbox._helm.uninstall", wraps=uninstall) as spy: + with patch("k8s_sandbox._helm.uninstall", wraps=uninstall) as spy: run_and_verify_inspect_eval(task=task, model=model, sandbox_cleanup=False) assert spy.call_count == 0 diff --git a/test/k8s_sandbox/inspect_integration/test_default_values.py b/test/k8s_sandbox/inspect_integration/test_default_values.py index cb45582..31810f0 100644 --- a/test/k8s_sandbox/inspect_integration/test_default_values.py +++ b/test/k8s_sandbox/inspect_integration/test_default_values.py @@ -1,9 +1,9 @@ import pytest -from test.aisitools.k8s_sandbox.inspect_integration.testing_utils.mock_model import ( +from test.k8s_sandbox.inspect_integration.testing_utils.mock_model import ( MockToolCallModel, ) -from test.aisitools.k8s_sandbox.inspect_integration.testing_utils.utils import ( +from test.k8s_sandbox.inspect_integration.testing_utils.utils import ( create_task, run_and_verify_inspect_eval, tool_call, diff --git a/test/k8s_sandbox/inspect_integration/testing_utils/mock_model.py b/test/k8s_sandbox/inspect_integration/testing_utils/mock_model.py index 42c4cde..98b9b1c 100644 --- a/test/k8s_sandbox/inspect_integration/testing_utils/mock_model.py +++ b/test/k8s_sandbox/inspect_integration/testing_utils/mock_model.py @@ -10,7 +10,7 @@ ) from inspect_ai.tool import ToolCall, ToolChoice, ToolInfo -from test.aisitools.k8s_sandbox.inspect_integration.testing_utils.utils import ( +from test.k8s_sandbox.inspect_integration.testing_utils.utils import ( output_from_tool_call, ) diff --git a/test/k8s_sandbox/inspect_integration/values/test_integration.py b/test/k8s_sandbox/inspect_integration/values/test_integration.py index 29abe28..2937037 100644 --- a/test/k8s_sandbox/inspect_integration/values/test_integration.py +++ b/test/k8s_sandbox/inspect_integration/values/test_integration.py @@ -1,10 +1,10 @@ import pytest from inspect_ai.model import Model -from test.aisitools.k8s_sandbox.inspect_integration.testing_utils.mock_model import ( +from test.k8s_sandbox.inspect_integration.testing_utils.mock_model import ( MockToolCallModel, ) -from test.aisitools.k8s_sandbox.inspect_integration.testing_utils.utils import ( +from test.k8s_sandbox.inspect_integration.testing_utils.utils import ( create_task, run_and_verify_inspect_eval, tool_call, diff --git a/test/k8s_sandbox/pod/test_executor.py b/test/k8s_sandbox/pod/test_executor.py index 41d242c..b0d1ae1 100644 --- a/test/k8s_sandbox/pod/test_executor.py +++ b/test/k8s_sandbox/pod/test_executor.py @@ -7,7 +7,7 @@ import pytest from pytest import MonkeyPatch -from aisitools.k8s_sandbox._pod.executor import PodOpExecutor +from k8s_sandbox._pod.executor import PodOpExecutor @pytest.fixture(autouse=True) diff --git a/test/k8s_sandbox/pod/test_get_returncode.py b/test/k8s_sandbox/pod/test_get_returncode.py index 9b8d4c2..ae199a5 100644 --- a/test/k8s_sandbox/pod/test_get_returncode.py +++ b/test/k8s_sandbox/pod/test_get_returncode.py @@ -4,7 +4,7 @@ import yaml from kubernetes.stream.ws_client import WSClient # type: ignore -from aisitools.k8s_sandbox._pod.get_returncode import ( +from k8s_sandbox._pod.get_returncode import ( GetReturncodeError, get_returncode, ) diff --git a/test/k8s_sandbox/pod/test_pod.py b/test/k8s_sandbox/pod/test_pod.py index 2568b81..8800ab3 100644 --- a/test/k8s_sandbox/pod/test_pod.py +++ b/test/k8s_sandbox/pod/test_pod.py @@ -1,6 +1,6 @@ from unittest.mock import MagicMock -from aisitools.k8s_sandbox._pod.execute import ExecuteOperation +from k8s_sandbox._pod.execute import ExecuteOperation def test_filter_sentinel_and_returncode(): diff --git a/test/k8s_sandbox/test_config_validation.py b/test/k8s_sandbox/test_config_validation.py index 06225ae..79a6b57 100644 --- a/test/k8s_sandbox/test_config_validation.py +++ b/test/k8s_sandbox/test_config_validation.py @@ -3,7 +3,7 @@ import pytest from pydantic import BaseModel -from aisitools.k8s_sandbox import K8sSandboxEnvironment, K8sSandboxEnvironmentConfig +from k8s_sandbox import K8sSandboxEnvironment, K8sSandboxEnvironmentConfig VALID_VALUES = str(Path(__file__).parent / "resources" / "values.yaml") diff --git a/test/k8s_sandbox/test_dns.py b/test/k8s_sandbox/test_dns.py index 125cb9d..82d736f 100644 --- a/test/k8s_sandbox/test_dns.py +++ b/test/k8s_sandbox/test_dns.py @@ -3,8 +3,8 @@ import pytest import pytest_asyncio -from aisitools.k8s_sandbox._sandbox_environment import K8sSandboxEnvironment -from test.aisitools.k8s_sandbox.utils import install_sandbox_environments +from k8s_sandbox._sandbox_environment import K8sSandboxEnvironment +from test.k8s_sandbox.utils import install_sandbox_environments # Mark all tests in this module as requiring a Kubernetes cluster. pytestmark = pytest.mark.req_k8s diff --git a/test/k8s_sandbox/test_helm.py b/test/k8s_sandbox/test_helm.py index 1b8d44b..09f40e1 100644 --- a/test/k8s_sandbox/test_helm.py +++ b/test/k8s_sandbox/test_helm.py @@ -4,7 +4,7 @@ import pytest from inspect_ai.util import ExecResult -from aisitools.k8s_sandbox._helm import Release, _run_subprocess +from k8s_sandbox._helm import Release, _run_subprocess @pytest.fixture @@ -13,9 +13,7 @@ def uninstallable_release() -> Release: async def test_helm_install_error(uninstallable_release: Release) -> None: - with patch( - "aisitools.k8s_sandbox._helm._run_subprocess", wraps=_run_subprocess - ) as spy: + with patch("k8s_sandbox._helm._run_subprocess", wraps=_run_subprocess) as spy: with pytest.raises(RuntimeError) as excinfo: await uninstallable_release.install() @@ -33,9 +31,9 @@ async def test_helm_resourcequota_retries(uninstallable_release: Release) -> Non "modified; please apply your changes to the latest version and try again\n", ) - with patch("aisitools.k8s_sandbox._helm.INSTALL_RETRY_DELAY_SECONDS", 0): + with patch("k8s_sandbox._helm.INSTALL_RETRY_DELAY_SECONDS", 0): with patch( - "aisitools.k8s_sandbox._helm._run_subprocess", return_value=fail_result + "k8s_sandbox._helm._run_subprocess", return_value=fail_result ) as mock: with pytest.raises(Exception) as excinfo: await uninstallable_release.install() diff --git a/test/k8s_sandbox/test_inspect_self_check.py b/test/k8s_sandbox/test_inspect_self_check.py index 58a6e7c..51d9b5b 100644 --- a/test/k8s_sandbox/test_inspect_self_check.py +++ b/test/k8s_sandbox/test_inspect_self_check.py @@ -4,8 +4,8 @@ from inspect_ai.util import SandboxEnvironment from inspect_ai.util._sandbox.self_check import self_check -from aisitools.k8s_sandbox._sandbox_environment import K8sSandboxEnvironment -from test.aisitools.k8s_sandbox.utils import install_sandbox_environments +from k8s_sandbox._sandbox_environment import K8sSandboxEnvironment +from test.k8s_sandbox.utils import install_sandbox_environments @pytest_asyncio.fixture(scope="module") diff --git a/test/k8s_sandbox/test_limited_stream_buffer.py b/test/k8s_sandbox/test_limited_stream_buffer.py index 23c68ca..8efea44 100644 --- a/test/k8s_sandbox/test_limited_stream_buffer.py +++ b/test/k8s_sandbox/test_limited_stream_buffer.py @@ -1,6 +1,6 @@ import pytest -from aisitools.k8s_sandbox._pod.buffer import LimitedBuffer +from k8s_sandbox._pod.buffer import LimitedBuffer @pytest.fixture diff --git a/test/k8s_sandbox/test_logger.py b/test/k8s_sandbox/test_logger.py index 748df6f..f0810a3 100644 --- a/test/k8s_sandbox/test_logger.py +++ b/test/k8s_sandbox/test_logger.py @@ -1,7 +1,7 @@ import pytest from pytest import MonkeyPatch -from aisitools.k8s_sandbox._logger import format_log_message +from k8s_sandbox._logger import format_log_message @pytest.fixture diff --git a/test/k8s_sandbox/test_network_policy.py b/test/k8s_sandbox/test_network_policy.py index 0e71a82..acdd73f 100644 --- a/test/k8s_sandbox/test_network_policy.py +++ b/test/k8s_sandbox/test_network_policy.py @@ -3,8 +3,8 @@ import pytest import pytest_asyncio -from aisitools.k8s_sandbox._sandbox_environment import K8sSandboxEnvironment -from test.aisitools.k8s_sandbox.utils import install_sandbox_environments +from k8s_sandbox._sandbox_environment import K8sSandboxEnvironment +from test.k8s_sandbox.utils import install_sandbox_environments # Mark all tests in this module as requiring a Kubernetes cluster. pytestmark = pytest.mark.req_k8s diff --git a/test/k8s_sandbox/test_networks.py b/test/k8s_sandbox/test_networks.py index 3691088..417ea39 100644 --- a/test/k8s_sandbox/test_networks.py +++ b/test/k8s_sandbox/test_networks.py @@ -3,8 +3,8 @@ import pytest import pytest_asyncio -from aisitools.k8s_sandbox._sandbox_environment import K8sSandboxEnvironment -from test.aisitools.k8s_sandbox.utils import install_sandbox_environments +from k8s_sandbox._sandbox_environment import K8sSandboxEnvironment +from test.k8s_sandbox.utils import install_sandbox_environments # Mark all tests in this module as requiring a Kubernetes cluster. pytestmark = pytest.mark.req_k8s diff --git a/test/k8s_sandbox/test_prereqs.py b/test/k8s_sandbox/test_prereqs.py index 61017d6..ad6b750 100644 --- a/test/k8s_sandbox/test_prereqs.py +++ b/test/k8s_sandbox/test_prereqs.py @@ -3,11 +3,11 @@ import pytest from inspect_ai._util.error import PrerequisiteError -from aisitools.k8s_sandbox._prereqs import validate_prereqs +from k8s_sandbox._prereqs import validate_prereqs async def test_helm_version_too_low() -> None: - with patch("aisitools.k8s_sandbox._prereqs.MINIMUM_HELM_VERSION", "999.0.0"): + with patch("k8s_sandbox._prereqs.MINIMUM_HELM_VERSION", "999.0.0"): with pytest.raises(PrerequisiteError) as error: await validate_prereqs() diff --git a/test/k8s_sandbox/test_runtime_class.py b/test/k8s_sandbox/test_runtime_class.py index 1a25a6b..f645a45 100644 --- a/test/k8s_sandbox/test_runtime_class.py +++ b/test/k8s_sandbox/test_runtime_class.py @@ -3,8 +3,8 @@ import pytest import pytest_asyncio -from aisitools.k8s_sandbox._sandbox_environment import K8sSandboxEnvironment -from test.aisitools.k8s_sandbox.utils import install_sandbox_environments +from k8s_sandbox._sandbox_environment import K8sSandboxEnvironment +from test.k8s_sandbox.utils import install_sandbox_environments # Mark all tests in this module as requiring a Kubernetes cluster. pytestmark = pytest.mark.req_k8s diff --git a/test/k8s_sandbox/test_sandbox.py b/test/k8s_sandbox/test_sandbox.py index 44805fd..b337b46 100644 --- a/test/k8s_sandbox/test_sandbox.py +++ b/test/k8s_sandbox/test_sandbox.py @@ -10,8 +10,8 @@ from kubernetes.stream.ws_client import ApiException, WSClient # type: ignore from pytest import LogCaptureFixture -from aisitools.k8s_sandbox._sandbox_environment import K8sError, K8sSandboxEnvironment -from test.aisitools.k8s_sandbox.utils import install_sandbox_environments +from k8s_sandbox._sandbox_environment import K8sError, K8sSandboxEnvironment +from test.k8s_sandbox.utils import install_sandbox_environments # Mark all tests in this module as requiring a Kubernetes cluster. pytestmark = pytest.mark.req_k8s @@ -448,7 +448,7 @@ async def test_exec_timeout_which_ignores_sigterm( async def test_api_timeout_is_not_triggered_by_long_running_commands( sandbox: K8sSandboxEnvironment, ) -> None: - with patch("aisitools.k8s_sandbox._pod.op.API_TIMEOUT", 1): + with patch("k8s_sandbox._pod.op.API_TIMEOUT", 1): result = await sandbox.exec(["sleep", "3"]) assert result.success diff --git a/test/k8s_sandbox/test_volume.py b/test/k8s_sandbox/test_volume.py index daeb649..cc911e9 100644 --- a/test/k8s_sandbox/test_volume.py +++ b/test/k8s_sandbox/test_volume.py @@ -3,8 +3,8 @@ import pytest import pytest_asyncio -from aisitools.k8s_sandbox._sandbox_environment import K8sSandboxEnvironment -from test.aisitools.k8s_sandbox.utils import install_sandbox_environments +from k8s_sandbox._sandbox_environment import K8sSandboxEnvironment +from test.k8s_sandbox.utils import install_sandbox_environments # Mark all tests in this module as requiring a Kubernetes cluster. pytestmark = pytest.mark.req_k8s diff --git a/test/k8s_sandbox/utils.py b/test/k8s_sandbox/utils.py index 6b59c7a..bd36df1 100644 --- a/test/k8s_sandbox/utils.py +++ b/test/k8s_sandbox/utils.py @@ -2,8 +2,8 @@ from pathlib import Path from typing import AsyncGenerator -from aisitools.k8s_sandbox._helm import Release -from aisitools.k8s_sandbox._sandbox_environment import K8sSandboxEnvironment +from k8s_sandbox._helm import Release +from k8s_sandbox._sandbox_environment import K8sSandboxEnvironment @asynccontextmanager From 4ef16ade20161d1883169205f11ff312fef889a7 Mon Sep 17 00:00:00 2001 From: "Craig.Walton" Date: Tue, 17 Dec 2024 14:00:35 +0000 Subject: [PATCH 6/7] Fix path to Helm chart in pre-commit config. --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bfbabfa..97bc270 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,4 +30,4 @@ repos: - id: helm-docs-built args: # Make the tool search for charts only under the `charts` directory - - --chart-search-root=src/aisitools/k8s_sandbox/resources/helm/agent-env + - --chart-search-root=src/k8s_sandbox/resources/helm/agent-env From a6b63bd723ed78e06a4fecc4705e054f0038b078 Mon Sep 17 00:00:00 2001 From: "Craig.Walton" Date: Tue, 17 Dec 2024 17:30:30 +0000 Subject: [PATCH 7/7] Update authors to be "UK AI Safety Institute" (as per inspect_ai). --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8fdf13a..fe3efb3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "inspect-k8s-sandbox" version = "0.1.0" description = "A Kubernetes Sandbox Environment for Inspect" -authors = ["Craig "] +authors = ["UK AI Safety Institute"] readme = "README.md" packages = [ {include = "k8s_sandbox", from = "src"},