blockbuster-1.5.26/.pre-commit-config.yaml0000644000000000000000000000033213615410400015323 0ustar00repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.8.3 hooks: - id: ruff name: ruff check args: [--fix] - id: ruff-format args: [--config, pyproject.toml] blockbuster-1.5.26/uv.lock0000644000000000000000000015560413615410400012363 0ustar00version = 1 revision = 1 requires-python = ">=3.8" [[package]] name = "aiofile" version = "3.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "caio" }, ] sdist = { url = "https://files.pythonhosted.org/packages/67/e2/d7cb819de8df6b5c1968a2756c3cb4122d4fa2b8fc768b53b7c9e5edb646/aiofile-3.9.0.tar.gz", hash = "sha256:e5ad718bb148b265b6df1b3752c4d1d83024b93da9bd599df74b9d9ffcf7919b", size = 17943 } wheels = [ { url = "https://files.pythonhosted.org/packages/50/25/da1f0b4dd970e52bf5a36c204c107e11a0c6d3ed195eba0bfbc664c312b2/aiofile-3.9.0-py3-none-any.whl", hash = "sha256:ce2f6c1571538cbdfa0143b04e16b208ecb0e9cb4148e528af8a640ed51cc8aa", size = 19539 }, ] [[package]] name = "blockbuster" version = "1.5.25" source = { editable = "." } dependencies = [ { name = "forbiddenfruit", marker = "implementation_name == 'cpython'" }, ] [package.dev-dependencies] dev = [ { name = "aiofile" }, { name = "mypy" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "requests" }, { name = "ruff" }, { name = "types-requests" }, ] [package.metadata] requires-dist = [{ name = "forbiddenfruit", marker = "implementation_name == 'cpython'", specifier = ">=0.1.4" }] [package.metadata.requires-dev] dev = [ { name = "aiofile", specifier = ">=3.9.0" }, { name = "mypy", specifier = ">=1.13.0" }, { name = "pytest", specifier = ">=8.3.3" }, { name = "pytest-asyncio", specifier = ">=0.24.0" }, { name = "requests", specifier = ">=2.32.3" }, { name = "ruff", specifier = ">=0.8.0,<0.9" }, { name = "types-requests", specifier = ">=2.32.0.20241016" }, ] [[package]] name = "caio" version = "0.9.17" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/88/cf/59b868909a85ad9eca985ad5bbeb3c0a8cd435e50ae12def770d16911753/caio-0.9.17.tar.gz", hash = "sha256:8f30511526814d961aeef389ea6885273abe6c655f1e08abbadb95d12fdd9b4f", size = 25001 } wheels = [ { url = "https://files.pythonhosted.org/packages/db/c3/17bc41b7c795d91d58ee7a70ad98e23f1ba0d50bdeadd82173bb02cddc8e/caio-0.9.17-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3f69395fdd45c115b2ef59732e3c8664722a2b51de2d6eedb3d354b2f5f3be3c", size = 37849 }, { url = "https://files.pythonhosted.org/packages/bf/3f/0ae9f69deb3dc96b20bf084cc262b438a154d5de08a064628272713ff239/caio-0.9.17-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3028b746e9ec7f6d6ebb386a7fd8caf0eebed5d6e6b4f18c8ef25861934b1673", size = 79417 }, { url = "https://files.pythonhosted.org/packages/52/44/a79c7004a9562a176d78437816c736c34ab9fb6233a4b8164eb25628a09c/caio-0.9.17-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:079730a353bbde03796fab681e969472eace09ffbe5000e584868a7fe389ba6f", size = 37842 }, { url = "https://files.pythonhosted.org/packages/57/e2/1d04e506a5fd735856f0bb95b4d03b800947bd43c98193ee57d37070a51e/caio-0.9.17-cp311-cp311-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:549caa51b475877fe32856a26fe937366ae7a1c23a9727005b441db9abb12bcc", size = 79985 }, { url = "https://files.pythonhosted.org/packages/c3/b8/37dcee4bc4fae1701a86373a297bbca797f6b7bfe5f85993a11049649c63/caio-0.9.17-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0ddb253b145a53ecca76381677ce465bc5efeaecb6aaf493fac43ae79659f0fb", size = 37909 }, { url = "https://files.pythonhosted.org/packages/80/f5/5e993120daeb4ec084f5f84c118bbd48b65379f63ed56919bf224e0eab42/caio-0.9.17-cp312-cp312-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3e320b0ea371c810359934f8e8fe81777c493cc5fb4d41de44277cbe7336e74", size = 81709 }, { url = "https://files.pythonhosted.org/packages/75/80/c54fb589cc6e00085656d9ff93d6a0e36f403648dbb38a300f237ba875ae/caio-0.9.17-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:a39a49e279f82aa022f0786339d45d9550b5aa3e46eec7d08e0f351c503df0a5", size = 37995 }, { url = "https://files.pythonhosted.org/packages/44/eb/313a36e1faa015a6ab76393b48f1f9bd56d9eaf79f650b5ddfad884d81f7/caio-0.9.17-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3e96925b9f15f43e6ef1d42a83edfd937eb11a984cb6ef7c10527e963595497", size = 79786 }, { url = "https://files.pythonhosted.org/packages/00/a2/3bfb94f7edaa4361ce62692e1bbd70355693cd45d96342eecf152862f6ae/caio-0.9.17-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:fca916240597005d2b734f1442fa3c3cfb612bf46e0978b5232e5492a371de38", size = 37864 }, { url = "https://files.pythonhosted.org/packages/77/15/d9410b3538e9454380469b43a4750f176ff543c17bba524fd25e55bf2054/caio-0.9.17-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40bd0afbd3491d1e407bcf74e3a9e9cc67a7f290ed29518325194184d63cc2b6", size = 78683 }, { url = "https://files.pythonhosted.org/packages/58/72/3f4895adb1d23b0ac1d8afc748405a2ad3c77d8d0f23b05a64ff583c11e5/caio-0.9.17-py3-none-any.whl", hash = "sha256:c55d4dc6b3a36f93237ecd6360e1c131c3808bc47d4191a130148a99b80bb311", size = 19062 }, ] [[package]] name = "certifi" version = "2024.8.30" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } wheels = [ { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, ] [[package]] name = "charset-normalizer" version = "3.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 } wheels = [ { url = "https://files.pythonhosted.org/packages/69/8b/825cc84cf13a28bfbcba7c416ec22bf85a9584971be15b21dd8300c65b7f/charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", size = 196363 }, { url = "https://files.pythonhosted.org/packages/23/81/d7eef6a99e42c77f444fdd7bc894b0ceca6c3a95c51239e74a722039521c/charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", size = 125639 }, { url = "https://files.pythonhosted.org/packages/21/67/b4564d81f48042f520c948abac7079356e94b30cb8ffb22e747532cf469d/charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", size = 120451 }, { url = "https://files.pythonhosted.org/packages/c2/72/12a7f0943dd71fb5b4e7b55c41327ac0a1663046a868ee4d0d8e9c369b85/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", size = 140041 }, { url = "https://files.pythonhosted.org/packages/67/56/fa28c2c3e31217c4c52158537a2cf5d98a6c1e89d31faf476c89391cd16b/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", size = 150333 }, { url = "https://files.pythonhosted.org/packages/f9/d2/466a9be1f32d89eb1554cf84073a5ed9262047acee1ab39cbaefc19635d2/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", size = 142921 }, { url = "https://files.pythonhosted.org/packages/f8/01/344ec40cf5d85c1da3c1f57566c59e0c9b56bcc5566c08804a95a6cc8257/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", size = 144785 }, { url = "https://files.pythonhosted.org/packages/73/8b/2102692cb6d7e9f03b9a33a710e0164cadfce312872e3efc7cfe22ed26b4/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", size = 146631 }, { url = "https://files.pythonhosted.org/packages/d8/96/cc2c1b5d994119ce9f088a9a0c3ebd489d360a2eb058e2c8049f27092847/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", size = 140867 }, { url = "https://files.pythonhosted.org/packages/c9/27/cde291783715b8ec30a61c810d0120411844bc4c23b50189b81188b273db/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", size = 149273 }, { url = "https://files.pythonhosted.org/packages/3a/a4/8633b0fc1a2d1834d5393dafecce4a1cc56727bfd82b4dc18fc92f0d3cc3/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", size = 152437 }, { url = "https://files.pythonhosted.org/packages/64/ea/69af161062166b5975ccbb0961fd2384853190c70786f288684490913bf5/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", size = 150087 }, { url = "https://files.pythonhosted.org/packages/3b/fd/e60a9d9fd967f4ad5a92810138192f825d77b4fa2a557990fd575a47695b/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", size = 145142 }, { url = "https://files.pythonhosted.org/packages/6d/02/8cb0988a1e49ac9ce2eed1e07b77ff118f2923e9ebd0ede41ba85f2dcb04/charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", size = 94701 }, { url = "https://files.pythonhosted.org/packages/d6/20/f1d4670a8a723c46be695dff449d86d6092916f9e99c53051954ee33a1bc/charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", size = 102191 }, { url = "https://files.pythonhosted.org/packages/9c/61/73589dcc7a719582bf56aae309b6103d2762b526bffe189d635a7fcfd998/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", size = 193339 }, { url = "https://files.pythonhosted.org/packages/77/d5/8c982d58144de49f59571f940e329ad6e8615e1e82ef84584c5eeb5e1d72/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", size = 124366 }, { url = "https://files.pythonhosted.org/packages/bf/19/411a64f01ee971bed3231111b69eb56f9331a769072de479eae7de52296d/charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", size = 118874 }, { url = "https://files.pythonhosted.org/packages/4c/92/97509850f0d00e9f14a46bc751daabd0ad7765cff29cdfb66c68b6dad57f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", size = 138243 }, { url = "https://files.pythonhosted.org/packages/e2/29/d227805bff72ed6d6cb1ce08eec707f7cfbd9868044893617eb331f16295/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", size = 148676 }, { url = "https://files.pythonhosted.org/packages/13/bc/87c2c9f2c144bedfa62f894c3007cd4530ba4b5351acb10dc786428a50f0/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", size = 141289 }, { url = "https://files.pythonhosted.org/packages/eb/5b/6f10bad0f6461fa272bfbbdf5d0023b5fb9bc6217c92bf068fa5a99820f5/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", size = 142585 }, { url = "https://files.pythonhosted.org/packages/3b/a0/a68980ab8a1f45a36d9745d35049c1af57d27255eff8c907e3add84cf68f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", size = 144408 }, { url = "https://files.pythonhosted.org/packages/d7/a1/493919799446464ed0299c8eef3c3fad0daf1c3cd48bff9263c731b0d9e2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", size = 139076 }, { url = "https://files.pythonhosted.org/packages/fb/9d/9c13753a5a6e0db4a0a6edb1cef7aee39859177b64e1a1e748a6e3ba62c2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", size = 146874 }, { url = "https://files.pythonhosted.org/packages/75/d2/0ab54463d3410709c09266dfb416d032a08f97fd7d60e94b8c6ef54ae14b/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", size = 150871 }, { url = "https://files.pythonhosted.org/packages/8d/c9/27e41d481557be53d51e60750b85aa40eaf52b841946b3cdeff363105737/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", size = 148546 }, { url = "https://files.pythonhosted.org/packages/ee/44/4f62042ca8cdc0cabf87c0fc00ae27cd8b53ab68be3605ba6d071f742ad3/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", size = 143048 }, { url = "https://files.pythonhosted.org/packages/01/f8/38842422988b795220eb8038745d27a675ce066e2ada79516c118f291f07/charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", size = 94389 }, { url = "https://files.pythonhosted.org/packages/0b/6e/b13bd47fa9023b3699e94abf565b5a2f0b0be6e9ddac9812182596ee62e4/charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", size = 101752 }, { url = "https://files.pythonhosted.org/packages/d3/0b/4b7a70987abf9b8196845806198975b6aab4ce016632f817ad758a5aa056/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", size = 194445 }, { url = "https://files.pythonhosted.org/packages/50/89/354cc56cf4dd2449715bc9a0f54f3aef3dc700d2d62d1fa5bbea53b13426/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", size = 125275 }, { url = "https://files.pythonhosted.org/packages/fa/44/b730e2a2580110ced837ac083d8ad222343c96bb6b66e9e4e706e4d0b6df/charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", size = 119020 }, { url = "https://files.pythonhosted.org/packages/9d/e4/9263b8240ed9472a2ae7ddc3e516e71ef46617fe40eaa51221ccd4ad9a27/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", size = 139128 }, { url = "https://files.pythonhosted.org/packages/6b/e3/9f73e779315a54334240353eaea75854a9a690f3f580e4bd85d977cb2204/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", size = 149277 }, { url = "https://files.pythonhosted.org/packages/1a/cf/f1f50c2f295312edb8a548d3fa56a5c923b146cd3f24114d5adb7e7be558/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", size = 142174 }, { url = "https://files.pythonhosted.org/packages/16/92/92a76dc2ff3a12e69ba94e7e05168d37d0345fa08c87e1fe24d0c2a42223/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", size = 143838 }, { url = "https://files.pythonhosted.org/packages/a4/01/2117ff2b1dfc61695daf2babe4a874bca328489afa85952440b59819e9d7/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", size = 146149 }, { url = "https://files.pythonhosted.org/packages/f6/9b/93a332b8d25b347f6839ca0a61b7f0287b0930216994e8bf67a75d050255/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", size = 140043 }, { url = "https://files.pythonhosted.org/packages/ab/f6/7ac4a01adcdecbc7a7587767c776d53d369b8b971382b91211489535acf0/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", size = 148229 }, { url = "https://files.pythonhosted.org/packages/9d/be/5708ad18161dee7dc6a0f7e6cf3a88ea6279c3e8484844c0590e50e803ef/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", size = 151556 }, { url = "https://files.pythonhosted.org/packages/5a/bb/3d8bc22bacb9eb89785e83e6723f9888265f3a0de3b9ce724d66bd49884e/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", size = 149772 }, { url = "https://files.pythonhosted.org/packages/f7/fa/d3fc622de05a86f30beea5fc4e9ac46aead4731e73fd9055496732bcc0a4/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", size = 144800 }, { url = "https://files.pythonhosted.org/packages/9a/65/bdb9bc496d7d190d725e96816e20e2ae3a6fa42a5cac99c3c3d6ff884118/charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", size = 94836 }, { url = "https://files.pythonhosted.org/packages/3e/67/7b72b69d25b89c0b3cea583ee372c43aa24df15f0e0f8d3982c57804984b/charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", size = 102187 }, { url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617 }, { url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310 }, { url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126 }, { url = "https://files.pythonhosted.org/packages/ff/6e/e445afe4f7fda27a533f3234b627b3e515a1b9429bc981c9a5e2aa5d97b6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", size = 139342 }, { url = "https://files.pythonhosted.org/packages/a1/b2/4af9993b532d93270538ad4926c8e37dc29f2111c36f9c629840c57cd9b3/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", size = 149383 }, { url = "https://files.pythonhosted.org/packages/fb/6f/4e78c3b97686b871db9be6f31d64e9264e889f8c9d7ab33c771f847f79b7/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", size = 142214 }, { url = "https://files.pythonhosted.org/packages/2b/c9/1c8fe3ce05d30c87eff498592c89015b19fade13df42850aafae09e94f35/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", size = 144104 }, { url = "https://files.pythonhosted.org/packages/ee/68/efad5dcb306bf37db7db338338e7bb8ebd8cf38ee5bbd5ceaaaa46f257e6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", size = 146255 }, { url = "https://files.pythonhosted.org/packages/0c/75/1ed813c3ffd200b1f3e71121c95da3f79e6d2a96120163443b3ad1057505/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", size = 140251 }, { url = "https://files.pythonhosted.org/packages/7d/0d/6f32255c1979653b448d3c709583557a4d24ff97ac4f3a5be156b2e6a210/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", size = 148474 }, { url = "https://files.pythonhosted.org/packages/ac/a0/c1b5298de4670d997101fef95b97ac440e8c8d8b4efa5a4d1ef44af82f0d/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", size = 151849 }, { url = "https://files.pythonhosted.org/packages/04/4f/b3961ba0c664989ba63e30595a3ed0875d6790ff26671e2aae2fdc28a399/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", size = 149781 }, { url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970 }, { url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973 }, { url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308 }, { url = "https://files.pythonhosted.org/packages/86/f4/ccab93e631e7293cca82f9f7ba39783c967f823a0000df2d8dd743cad74f/charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578", size = 193961 }, { url = "https://files.pythonhosted.org/packages/94/d4/2b21cb277bac9605026d2d91a4a8872bc82199ed11072d035dc674c27223/charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6", size = 124507 }, { url = "https://files.pythonhosted.org/packages/9a/e0/a7c1fcdff20d9c667342e0391cfeb33ab01468d7d276b2c7914b371667cc/charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417", size = 119298 }, { url = "https://files.pythonhosted.org/packages/70/de/1538bb2f84ac9940f7fa39945a5dd1d22b295a89c98240b262fc4b9fcfe0/charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51", size = 139328 }, { url = "https://files.pythonhosted.org/packages/e9/ca/288bb1a6bc2b74fb3990bdc515012b47c4bc5925c8304fc915d03f94b027/charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41", size = 149368 }, { url = "https://files.pythonhosted.org/packages/aa/75/58374fdaaf8406f373e508dab3486a31091f760f99f832d3951ee93313e8/charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f", size = 141944 }, { url = "https://files.pythonhosted.org/packages/32/c8/0bc558f7260db6ffca991ed7166494a7da4fda5983ee0b0bfc8ed2ac6ff9/charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8", size = 143326 }, { url = "https://files.pythonhosted.org/packages/0e/dd/7f6fec09a1686446cee713f38cf7d5e0669e0bcc8288c8e2924e998cf87d/charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab", size = 146171 }, { url = "https://files.pythonhosted.org/packages/4c/a8/440f1926d6d8740c34d3ca388fbd718191ec97d3d457a0677eb3aa718fce/charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12", size = 139711 }, { url = "https://files.pythonhosted.org/packages/e9/7f/4b71e350a3377ddd70b980bea1e2cc0983faf45ba43032b24b2578c14314/charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19", size = 148348 }, { url = "https://files.pythonhosted.org/packages/1e/70/17b1b9202531a33ed7ef41885f0d2575ae42a1e330c67fddda5d99ad1208/charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea", size = 151290 }, { url = "https://files.pythonhosted.org/packages/44/30/574b5b5933d77ecb015550aafe1c7d14a8cd41e7e6c4dcea5ae9e8d496c3/charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858", size = 149114 }, { url = "https://files.pythonhosted.org/packages/0b/11/ca7786f7e13708687443082af20d8341c02e01024275a28bc75032c5ce5d/charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654", size = 143856 }, { url = "https://files.pythonhosted.org/packages/f9/c2/1727c1438256c71ed32753b23ec2e6fe7b6dff66a598f6566cfe8139305e/charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613", size = 94333 }, { url = "https://files.pythonhosted.org/packages/09/c8/0e17270496a05839f8b500c1166e3261d1226e39b698a735805ec206967b/charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade", size = 101454 }, { url = "https://files.pythonhosted.org/packages/54/2f/28659eee7f5d003e0f5a3b572765bf76d6e0fe6601ab1f1b1dd4cba7e4f1/charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", size = 196326 }, { url = "https://files.pythonhosted.org/packages/d1/18/92869d5c0057baa973a3ee2af71573be7b084b3c3d428fe6463ce71167f8/charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a", size = 125614 }, { url = "https://files.pythonhosted.org/packages/d6/27/327904c5a54a7796bb9f36810ec4173d2df5d88b401d2b95ef53111d214e/charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", size = 120450 }, { url = "https://files.pythonhosted.org/packages/a4/23/65af317914a0308495133b2d654cf67b11bbd6ca16637c4e8a38f80a5a69/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", size = 140135 }, { url = "https://files.pythonhosted.org/packages/f2/41/6190102ad521a8aa888519bb014a74251ac4586cde9b38e790901684f9ab/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", size = 150413 }, { url = "https://files.pythonhosted.org/packages/7b/ab/f47b0159a69eab9bd915591106859f49670c75f9a19082505ff16f50efc0/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b", size = 142992 }, { url = "https://files.pythonhosted.org/packages/28/89/60f51ad71f63aaaa7e51a2a2ad37919985a341a1d267070f212cdf6c2d22/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", size = 144871 }, { url = "https://files.pythonhosted.org/packages/0c/48/0050550275fea585a6e24460b42465020b53375017d8596c96be57bfabca/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", size = 146756 }, { url = "https://files.pythonhosted.org/packages/dc/b5/47f8ee91455946f745e6c9ddbb0f8f50314d2416dd922b213e7d5551ad09/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", size = 141034 }, { url = "https://files.pythonhosted.org/packages/84/79/5c731059ebab43e80bf61fa51666b9b18167974b82004f18c76378ed31a3/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", size = 149434 }, { url = "https://files.pythonhosted.org/packages/ca/f3/0719cd09fc4dc42066f239cb3c48ced17fc3316afca3e2a30a4756fe49ab/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", size = 152443 }, { url = "https://files.pythonhosted.org/packages/f7/0e/c6357297f1157c8e8227ff337e93fd0a90e498e3d6ab96b2782204ecae48/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", size = 150294 }, { url = "https://files.pythonhosted.org/packages/54/9a/acfa96dc4ea8c928040b15822b59d0863d6e1757fba8bd7de3dc4f761c13/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", size = 145314 }, { url = "https://files.pythonhosted.org/packages/73/1c/b10a63032eaebb8d7bcb8544f12f063f41f5f463778ac61da15d9985e8b6/charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", size = 94724 }, { url = "https://files.pythonhosted.org/packages/c5/77/3a78bf28bfaa0863f9cfef278dbeadf55efe064eafff8c7c424ae3c4c1bf/charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", size = 102159 }, { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] [[package]] name = "exceptiongroup" version = "1.2.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } wheels = [ { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, ] [[package]] name = "forbiddenfruit" version = "0.1.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e6/79/d4f20e91327c98096d605646bdc6a5ffedae820f38d378d3515c42ec5e60/forbiddenfruit-0.1.4.tar.gz", hash = "sha256:e3f7e66561a29ae129aac139a85d610dbf3dd896128187ed5454b6421f624253", size = 43756 } [[package]] name = "idna" version = "3.10" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, ] [[package]] name = "iniconfig" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } wheels = [ { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, ] [[package]] name = "mypy" version = "1.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e8/21/7e9e523537991d145ab8a0a2fd98548d67646dc2aaaf6091c31ad883e7c1/mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e", size = 3152532 } wheels = [ { url = "https://files.pythonhosted.org/packages/5e/8c/206de95a27722b5b5a8c85ba3100467bd86299d92a4f71c6b9aa448bfa2f/mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a", size = 11020731 }, { url = "https://files.pythonhosted.org/packages/ab/bb/b31695a29eea76b1569fd28b4ab141a1adc9842edde080d1e8e1776862c7/mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80", size = 10184276 }, { url = "https://files.pythonhosted.org/packages/a5/2d/4a23849729bb27934a0e079c9c1aad912167d875c7b070382a408d459651/mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7", size = 12587706 }, { url = "https://files.pythonhosted.org/packages/5c/c3/d318e38ada50255e22e23353a469c791379825240e71b0ad03e76ca07ae6/mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f", size = 13105586 }, { url = "https://files.pythonhosted.org/packages/4a/25/3918bc64952370c3dbdbd8c82c363804678127815febd2925b7273d9482c/mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372", size = 9632318 }, { url = "https://files.pythonhosted.org/packages/d0/19/de0822609e5b93d02579075248c7aa6ceaddcea92f00bf4ea8e4c22e3598/mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d", size = 10939027 }, { url = "https://files.pythonhosted.org/packages/c8/71/6950fcc6ca84179137e4cbf7cf41e6b68b4a339a1f5d3e954f8c34e02d66/mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d", size = 10108699 }, { url = "https://files.pythonhosted.org/packages/26/50/29d3e7dd166e74dc13d46050b23f7d6d7533acf48f5217663a3719db024e/mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b", size = 12506263 }, { url = "https://files.pythonhosted.org/packages/3f/1d/676e76f07f7d5ddcd4227af3938a9c9640f293b7d8a44dd4ff41d4db25c1/mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73", size = 12984688 }, { url = "https://files.pythonhosted.org/packages/9c/03/5a85a30ae5407b1d28fab51bd3e2103e52ad0918d1e68f02a7778669a307/mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca", size = 9626811 }, { url = "https://files.pythonhosted.org/packages/fb/31/c526a7bd2e5c710ae47717c7a5f53f616db6d9097caf48ad650581e81748/mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5", size = 11077900 }, { url = "https://files.pythonhosted.org/packages/83/67/b7419c6b503679d10bd26fc67529bc6a1f7a5f220bbb9f292dc10d33352f/mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e", size = 10074818 }, { url = "https://files.pythonhosted.org/packages/ba/07/37d67048786ae84e6612575e173d713c9a05d0ae495dde1e68d972207d98/mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2", size = 12589275 }, { url = "https://files.pythonhosted.org/packages/1f/17/b1018c6bb3e9f1ce3956722b3bf91bff86c1cefccca71cec05eae49d6d41/mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0", size = 13037783 }, { url = "https://files.pythonhosted.org/packages/cb/32/cd540755579e54a88099aee0287086d996f5a24281a673f78a0e14dba150/mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2", size = 9726197 }, { url = "https://files.pythonhosted.org/packages/11/bb/ab4cfdc562cad80418f077d8be9b4491ee4fb257440da951b85cbb0a639e/mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7", size = 11069721 }, { url = "https://files.pythonhosted.org/packages/59/3b/a393b1607cb749ea2c621def5ba8c58308ff05e30d9dbdc7c15028bca111/mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62", size = 10063996 }, { url = "https://files.pythonhosted.org/packages/d1/1f/6b76be289a5a521bb1caedc1f08e76ff17ab59061007f201a8a18cc514d1/mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8", size = 12584043 }, { url = "https://files.pythonhosted.org/packages/a6/83/5a85c9a5976c6f96e3a5a7591aa28b4a6ca3a07e9e5ba0cec090c8b596d6/mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7", size = 13036996 }, { url = "https://files.pythonhosted.org/packages/b4/59/c39a6f752f1f893fccbcf1bdd2aca67c79c842402b5283563d006a67cf76/mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc", size = 9737709 }, { url = "https://files.pythonhosted.org/packages/5e/2a/13e9ad339131c0fba5c70584f639005a47088f5eed77081a3d00479df0ca/mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a", size = 10955147 }, { url = "https://files.pythonhosted.org/packages/94/39/02929067dc16b72d78109195cfed349ac4ec85f3d52517ac62b9a5263685/mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb", size = 10138373 }, { url = "https://files.pythonhosted.org/packages/4a/cc/066709bb01734e3dbbd1375749f8789bf9693f8b842344fc0cf52109694f/mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b", size = 12543621 }, { url = "https://files.pythonhosted.org/packages/f5/a2/124df839025348c7b9877d0ce134832a9249968e3ab36bb826bab0e9a1cf/mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74", size = 13050348 }, { url = "https://files.pythonhosted.org/packages/45/86/cc94b1e7f7e756a63043cf425c24fb7470013ee1c032180282db75b1b335/mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6", size = 9615311 }, { url = "https://files.pythonhosted.org/packages/5f/d4/b33ddd40dad230efb317898a2d1c267c04edba73bc5086bf77edeb410fb2/mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc", size = 11013906 }, { url = "https://files.pythonhosted.org/packages/f4/e6/f414bca465b44d01cd5f4a82761e15044bedd1bf8025c5af3cc64518fac5/mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732", size = 10180657 }, { url = "https://files.pythonhosted.org/packages/38/e9/fc3865e417722f98d58409770be01afb961e2c1f99930659ff4ae7ca8b7e/mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc", size = 12586394 }, { url = "https://files.pythonhosted.org/packages/2e/35/f4d8b6d2cb0b3dad63e96caf159419dda023f45a358c6c9ac582ccaee354/mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d", size = 13103591 }, { url = "https://files.pythonhosted.org/packages/22/1d/80594aef135f921dd52e142fa0acd19df197690bd0cde42cea7b88cf5aa2/mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24", size = 9634690 }, { url = "https://files.pythonhosted.org/packages/3b/86/72ce7f57431d87a7ff17d442f521146a6585019eb8f4f31b7c02801f78ad/mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a", size = 2647043 }, ] [[package]] name = "mypy-extensions" version = "1.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } wheels = [ { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, ] [[package]] name = "packaging" version = "24.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } wheels = [ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, ] [[package]] name = "pluggy" version = "1.5.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } wheels = [ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, ] [[package]] name = "pytest" version = "8.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487 } wheels = [ { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 }, ] [[package]] name = "pytest-asyncio" version = "0.24.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/52/6d/c6cf50ce320cf8611df7a1254d86233b3df7cc07f9b5f5cbcb82e08aa534/pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276", size = 49855 } wheels = [ { url = "https://files.pythonhosted.org/packages/96/31/6607dab48616902f76885dfcf62c08d929796fc3b2d2318faf9fd54dbed9/pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b", size = 18024 }, ] [[package]] name = "requests" version = "2.32.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "charset-normalizer" }, { name = "idna" }, { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } wheels = [ { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, ] [[package]] name = "ruff" version = "0.8.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b2/d6/a2373f3ba7180ddb44420d2a9d1f1510e1a4d162b3d27282bedcb09c8da9/ruff-0.8.0.tar.gz", hash = "sha256:a7ccfe6331bf8c8dad715753e157457faf7351c2b69f62f32c165c2dbcbacd44", size = 3276537 } wheels = [ { url = "https://files.pythonhosted.org/packages/ec/77/e889ee3ce7fd8baa3ed1b77a03b9fb8ec1be68be1418261522fd6a5405e0/ruff-0.8.0-py3-none-linux_armv6l.whl", hash = "sha256:fcb1bf2cc6706adae9d79c8d86478677e3bbd4ced796ccad106fd4776d395fea", size = 10518283 }, { url = "https://files.pythonhosted.org/packages/da/c8/0a47de01edf19fb22f5f9b7964f46a68d0bdff20144d134556ffd1ba9154/ruff-0.8.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:295bb4c02d58ff2ef4378a1870c20af30723013f441c9d1637a008baaf928c8b", size = 10317691 }, { url = "https://files.pythonhosted.org/packages/41/17/9885e4a0eeae07abd2a4ebabc3246f556719f24efa477ba2739146c4635a/ruff-0.8.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7b1f1c76b47c18fa92ee78b60d2d20d7e866c55ee603e7d19c1e991fad933a9a", size = 9940999 }, { url = "https://files.pythonhosted.org/packages/3e/cd/46b6f7043597eb318b5f5482c8ae8f5491cccce771e85f59d23106f2d179/ruff-0.8.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb0d4f250a7711b67ad513fde67e8870109e5ce590a801c3722580fe98c33a99", size = 10772437 }, { url = "https://files.pythonhosted.org/packages/5d/87/afc95aeb8bc78b1d8a3461717a4419c05aa8aa943d4c9cbd441630f85584/ruff-0.8.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e55cce9aa93c5d0d4e3937e47b169035c7e91c8655b0974e61bb79cf398d49c", size = 10299156 }, { url = "https://files.pythonhosted.org/packages/65/fa/04c647bb809c4d65e8eae1ed1c654d9481b21dd942e743cd33511687b9f9/ruff-0.8.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f4cd64916d8e732ce6b87f3f5296a8942d285bbbc161acee7fe561134af64f9", size = 11325819 }, { url = "https://files.pythonhosted.org/packages/90/26/7dad6e7d833d391a8a1afe4ee70ca6f36c4a297d3cca83ef10e83e9aacf3/ruff-0.8.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c5c1466be2a2ebdf7c5450dd5d980cc87c8ba6976fb82582fea18823da6fa362", size = 12023927 }, { url = "https://files.pythonhosted.org/packages/24/a0/be5296dda6428ba8a13bda8d09fbc0e14c810b485478733886e61597ae2b/ruff-0.8.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2dabfd05b96b7b8f2da00d53c514eea842bff83e41e1cceb08ae1966254a51df", size = 11589702 }, { url = "https://files.pythonhosted.org/packages/26/3f/7602eb11d2886db545834182a9dbe500b8211fcbc9b4064bf9d358bbbbb4/ruff-0.8.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:facebdfe5a5af6b1588a1d26d170635ead6892d0e314477e80256ef4a8470cf3", size = 12782936 }, { url = "https://files.pythonhosted.org/packages/4c/5d/083181bdec4ec92a431c1291d3fff65eef3ded630a4b55eb735000ef5f3b/ruff-0.8.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87a8e86bae0dbd749c815211ca11e3a7bd559b9710746c559ed63106d382bd9c", size = 11138488 }, { url = "https://files.pythonhosted.org/packages/b7/23/c12cdef58413cee2436d6a177aa06f7a366ebbca916cf10820706f632459/ruff-0.8.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:85e654f0ded7befe2d61eeaf3d3b1e4ef3894469cd664ffa85006c7720f1e4a2", size = 10744474 }, { url = "https://files.pythonhosted.org/packages/29/61/a12f3b81520083cd7c5caa24ba61bb99fd1060256482eff0ef04cc5ccd1b/ruff-0.8.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:83a55679c4cb449fa527b8497cadf54f076603cc36779b2170b24f704171ce70", size = 10369029 }, { url = "https://files.pythonhosted.org/packages/08/2a/c013f4f3e4a54596c369cee74c24870ed1d534f31a35504908b1fc97017a/ruff-0.8.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:812e2052121634cf13cd6fddf0c1871d0ead1aad40a1a258753c04c18bb71bbd", size = 10867481 }, { url = "https://files.pythonhosted.org/packages/d5/f7/685b1e1d42a3e94ceb25eab23c70bdd8c0ab66a43121ef83fe6db5a58756/ruff-0.8.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:780d5d8523c04202184405e60c98d7595bdb498c3c6abba3b6d4cdf2ca2af426", size = 11237117 }, { url = "https://files.pythonhosted.org/packages/03/20/401132c0908e8837625e3b7e32df9962e7cd681a4df1e16a10e2a5b4ecda/ruff-0.8.0-py3-none-win32.whl", hash = "sha256:5fdb6efecc3eb60bba5819679466471fd7d13c53487df7248d6e27146e985468", size = 8783511 }, { url = "https://files.pythonhosted.org/packages/1d/5c/4d800fca7854f62ad77f2c0d99b4b585f03e2d87a6ec1ecea85543a14a3c/ruff-0.8.0-py3-none-win_amd64.whl", hash = "sha256:582891c57b96228d146725975fbb942e1f30a0c4ba19722e692ca3eb25cc9b4f", size = 9559876 }, { url = "https://files.pythonhosted.org/packages/5b/bc/cc8a6a5ca4960b226dc15dd8fb511dd11f2014ff89d325c0b9b9faa9871f/ruff-0.8.0-py3-none-win_arm64.whl", hash = "sha256:ba93e6294e9a737cd726b74b09a6972e36bb511f9a102f1d9a7e1ce94dd206a6", size = 8939733 }, ] [[package]] name = "tomli" version = "2.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1e/e4/1b6cbcc82d8832dd0ce34767d5c560df8a3547ad8cbc427f34601415930a/tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8", size = 16622 } wheels = [ { url = "https://files.pythonhosted.org/packages/de/f7/4da0ffe1892122c9ea096c57f64c2753ae5dd3ce85488802d11b0992cc6d/tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391", size = 13750 }, ] [[package]] name = "types-requests" version = "2.32.0.20241016" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fa/3c/4f2a430c01a22abd49a583b6b944173e39e7d01b688190a5618bd59a2e22/types-requests-2.32.0.20241016.tar.gz", hash = "sha256:0d9cad2f27515d0e3e3da7134a1b6f28fb97129d86b867f24d9c726452634d95", size = 18065 } wheels = [ { url = "https://files.pythonhosted.org/packages/d7/01/485b3026ff90e5190b5e24f1711522e06c79f4a56c8f4b95848ac072e20f/types_requests-2.32.0.20241016-py3-none-any.whl", hash = "sha256:4195d62d6d3e043a4eaaf08ff8a62184584d2e8684e9d2aa178c7915a7da3747", size = 15836 }, ] [[package]] name = "typing-extensions" version = "4.12.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } wheels = [ { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, ] [[package]] name = "urllib3" version = "2.2.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } wheels = [ { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, ] blockbuster-1.5.26/.github/actions/setup-uv/action.yml0000644000000000000000000000122713615410400017636 0ustar00name: "Setup uv" description: "Checks out code, installs uv, and sets up Python environment" runs: using: "composite" steps: - name: Install uv uses: astral-sh/setup-uv@v3 with: enable-cache: true cache-dependency-glob: "uv.lock" - name: "Set up Python" uses: actions/setup-python@v5 with: python-version-file: "pyproject.toml" - name: Restore uv cache uses: actions/cache@v4 with: path: /tmp/.uv-cache key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }} restore-keys: | uv-${{ runner.os }}-${{ hashFiles('uv.lock') }} uv-${{ runner.os }} blockbuster-1.5.26/.github/workflows/lint.yml0000644000000000000000000000232213615410400016131 0ustar00name: Python lint on: push: branches: - main pull_request: branches: - main jobs: build: name: Lint - Python ${{ matrix.python-version }} runs-on: ubuntu-latest strategy: matrix: python-version: ['3.8', '3.13'] steps: - uses: actions/checkout@v4 with: ref: ${{ github.ref }} - name: Install uv uses: astral-sh/setup-uv@v3 with: enable-cache: true cache-dependency-glob: "uv.lock" - name: "Set up Python" uses: actions/setup-python@v5 with: python-version-file: "pyproject.toml" - name: Restore uv cache uses: actions/cache@v4 with: path: /tmp/.uv-cache key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }} restore-keys: | uv-${{ runner.os }}-${{ hashFiles('uv.lock') }} uv-${{ runner.os }} - name: Install the project run: uv sync --dev - name: Run ruff format check run: uv run ruff format --check - name: Run ruff check run: uv run ruff check - name: Run mypy run: uv run mypy . - name: Minimize uv cache run: uv cache prune --ci blockbuster-1.5.26/.github/workflows/python_test.yml0000644000000000000000000000240013615410400017540 0ustar00name: Python tests on: push: branches: - main pull_request: branches: - main jobs: build: name: Unit Tests - Python ${{ matrix.python-version }} - ${{ matrix.os }} runs-on: ${{ matrix.os }}-latest strategy: fail-fast: false matrix: python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] os: - macos - ubuntu - windows steps: - uses: actions/checkout@v4 with: ref: ${{ github.ref }} - name: Install uv uses: astral-sh/setup-uv@v3 with: enable-cache: true cache-dependency-glob: "uv.lock" - name: "Set up Python" uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Restore uv cache uses: actions/cache@v4 with: path: /tmp/.uv-cache key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }} restore-keys: | uv-${{ runner.os }}-${{ hashFiles('uv.lock') }} uv-${{ runner.os }} - name: Install the project run: uv sync --dev - name: Run unit tests run: uv run pytest tests -vv - name: Minimize uv cache run: uv cache prune --ci blockbuster-1.5.26/blockbuster/__init__.py0000644000000000000000000000044213615410400015474 0ustar00"""Blockbuster is a utility to detect blocking calls in the async event loop.""" from blockbuster.blockbuster import ( BlockBuster, BlockBusterFunction, BlockingError, blockbuster_ctx, ) __all__ = ["BlockBuster", "BlockBusterFunction", "BlockingError", "blockbuster_ctx"] blockbuster-1.5.26/blockbuster/blockbuster.py0000644000000000000000000005604413615410400016265 0ustar00"""BlockBuster module.""" from __future__ import annotations import _thread import asyncio import importlib import inspect import io import logging import os import platform import sys from contextlib import contextmanager from contextvars import ContextVar from pathlib import Path from types import ModuleType from typing import TYPE_CHECKING, Any, TypeVar, Union if TYPE_CHECKING: import socket import threading from collections.abc import Callable, Iterable, Iterator, Sequence _ModuleList = Union[Sequence[Union[str, ModuleType]], None] _ModuleOrModuleList = Union[str, ModuleType, _ModuleList] if platform.python_implementation() == "CPython": import forbiddenfruit HAS_FORBIDDENFRUIT = True else: HAS_FORBIDDENFRUIT = False class BlockingError(Exception): """BlockingError class.""" def __init__(self, func: str) -> None: """Initialize BlockingError. Args: func: The blocking function. """ super().__init__(f"Blocking call to {func}") def _blocking_error(func: Callable[..., Any]) -> BlockingError: if inspect.isbuiltin(func): msg = f"Blocking call to {func.__qualname__} ({func.__self__})" elif inspect.ismethoddescriptor(func): msg = f"Blocking call to {func}" else: msg = f"Blocking call to {func.__module__}.{func.__qualname__}" return BlockingError(msg) _T = TypeVar("_T") blockbuster_skip: ContextVar[bool] = ContextVar("blockbuster_skip") def _wrap_blocking( modules: list[str], excluded_modules: list[str], func: Callable[..., _T], func_name: str, can_block_functions: list[tuple[str, Iterable[str]]], can_block_predicate: Callable[..., bool], ) -> Callable[..., _T]: """Wrap blocking function.""" def wrapper(*args: Any, **kwargs: Any) -> _T: if blockbuster_skip.get(False): return func(*args, **kwargs) try: asyncio.get_running_loop() except RuntimeError: return func(*args, **kwargs) skip_token = blockbuster_skip.set(True) try: if can_block_predicate(*args, **kwargs): return func(*args, **kwargs) frame = inspect.currentframe() in_test_module = False while frame: frame_info = inspect.getframeinfo(frame) if not in_test_module: in_excluded_module = False for excluded_module in excluded_modules: if frame_info.filename.startswith(excluded_module): in_excluded_module = True break if not in_excluded_module: for module in modules: if frame_info.filename.startswith(module): in_test_module = True break frame_file_name = Path(frame_info.filename).as_posix() for filename, functions in can_block_functions: if ( frame_file_name.endswith(filename) and frame_info.function in functions ): return func(*args, **kwargs) frame = frame.f_back if not modules or in_test_module: raise BlockingError(func_name) return func(*args, **kwargs) finally: blockbuster_skip.reset(skip_token) return wrapper def _resolve_module_paths(modules: Sequence[str | ModuleType]) -> list[str]: resolved: list[str] = [] for module in modules: module_ = importlib.import_module(module) if isinstance(module, str) else module if hasattr(module_, "__path__"): resolved.append(module_.__path__[0]) elif file := module_.__file__: resolved.append(file) else: logging.warning("Cannot get path for %s", module_) return resolved class BlockBusterFunction: """BlockBusterFunction class.""" def __init__( self, module: ModuleType | type | None, func_name: str, *, scanned_modules: _ModuleOrModuleList = None, excluded_modules: _ModuleList = None, can_block_functions: list[tuple[str, Iterable[str]]] | None = None, can_block_predicate: Callable[..., bool] = lambda *_, **__: False, ) -> None: """Create a BlockBusterFunction. Args: module: The module that contains the blocking function. func_name: The name of the blocking function. scanned_modules: The modules from which blocking calls are detected. If None, the blocking calls are detected from all the modules. Can be a module name, a module object, a list of module names or a list of module objects. excluded_modules: Sub-modules that are excluded from scanned modules. It doesn't mean that blocking is allowed in these modules. It means that they are not considered as scanned modules. It is helpful for instance if the tests use a pytest plugin that is part of the scanned modules. Can be a list of module names or module objects. can_block_functions: Optional functions in the stack where blocking is allowed. can_block_predicate: An optional predicate that determines if blocking is allowed. """ if module: self.module = module self.func_name = func_name self.original_func = getattr(module, func_name, None) else: tokens = func_name.split(".") if len(tokens) < 2: # noqa: PLR2004 msg = "module is required if func_name does not contain '.'" raise ValueError(msg) self.module = importlib.import_module(tokens.pop(0)) while len(tokens) > 1: self.module = getattr(self.module, tokens.pop(0)) self.func_name = tokens[0] self.original_func = getattr(self.module, self.func_name, None) if module is None: self.full_name = func_name else: self.full_name = f"{module.__name__}.{func_name}" self.can_block_functions: list[tuple[str, Iterable[str]]] = ( can_block_functions or [] ) self.can_block_predicate: Callable[..., bool] = can_block_predicate self.activated = False if isinstance(scanned_modules, (str, ModuleType)): _scanned_modules: Sequence[str | ModuleType] = [scanned_modules] else: _scanned_modules = scanned_modules or [] self._scanned_modules = _resolve_module_paths(_scanned_modules) self._excluded_modules = _resolve_module_paths(excluded_modules or []) def activate(self) -> BlockBusterFunction: """Activate the blocking detection.""" if self.original_func is None or self.activated: return self self.activated = True checker = _wrap_blocking( self._scanned_modules, self._excluded_modules, self.original_func, self.full_name, self.can_block_functions, self.can_block_predicate, ) try: setattr(self.module, self.func_name, checker) except TypeError: if HAS_FORBIDDENFRUIT: forbiddenfruit.curse(self.module, self.func_name, checker) return self def deactivate(self) -> BlockBusterFunction: """Deactivate the blocking detection.""" if self.original_func is None or not self.activated: return self self.activated = False try: setattr(self.module, self.func_name, self.original_func) except TypeError: if HAS_FORBIDDENFRUIT: forbiddenfruit.curse(self.module, self.func_name, self.original_func) return self def can_block_in( self, filename: str, functions: str | Iterable[str] ) -> BlockBusterFunction: """Add functions where it is allowed to block. Args: filename (str): The filename that contains the functions. functions (str | Iterable[str]): The functions where blocking is allowed. """ if isinstance(functions, str): functions = {functions} self.can_block_functions.append((filename, functions)) return self def _get_time_wrapped_functions( modules: _ModuleOrModuleList = None, excluded_modules: _ModuleList = None ) -> dict[str, BlockBusterFunction]: return { "time.sleep": BlockBusterFunction( None, "time.sleep", can_block_functions=[ ("/pydevd.py", {"_do_wait_suspend"}), ], scanned_modules=modules, excluded_modules=excluded_modules, ) } def _get_os_wrapped_functions( modules: _ModuleOrModuleList = None, excluded_modules: _ModuleList = None ) -> dict[str, BlockBusterFunction]: functions = { f"os.{method}": BlockBusterFunction( None, f"os.{method}", scanned_modules=modules, excluded_modules=excluded_modules, ) for method in ( "statvfs", "rename", "remove", "rmdir", "link", "symlink", "listdir", "access", ) } functions["os.getcwd"] = BlockBusterFunction( None, "os.getcwd", can_block_functions=[ ("coverage/control.py", {"_should_trace"}), ], scanned_modules=modules, excluded_modules=excluded_modules, ) functions["os.stat"] = BlockBusterFunction( None, "os.stat", can_block_functions=[ ("", {"_find_and_load"}), ("linecache.py", {"checkcache", "updatecache"}), ("coverage/control.py", {"_should_trace"}), ("asyncio/unix_events.py", {"create_unix_server", "_stop_serving"}), ], scanned_modules=modules, excluded_modules=excluded_modules, ) functions["os.mkdir"] = BlockBusterFunction( None, "os.mkdir", can_block_functions=[("_pytest/assertion/rewrite.py", {"try_makedirs"})], scanned_modules=modules, excluded_modules=excluded_modules, ) functions["os.replace"] = BlockBusterFunction( None, "os.replace", can_block_functions=[("_pytest/assertion/rewrite.py", {"_write_pyc"})], scanned_modules=modules, excluded_modules=excluded_modules, ) functions["os.readlink"] = BlockBusterFunction( None, "os.readlink", can_block_functions=[ ("coverage/control.py", {"_should_trace"}), ], scanned_modules=modules, excluded_modules=excluded_modules, ) functions["os.sendfile"] = BlockBusterFunction( None, "os.sendfile", can_block_functions=[ ("asyncio/base_events.py", {"sendfile"}), ], scanned_modules=modules, excluded_modules=excluded_modules, ) functions["os.unlink"] = BlockBusterFunction( None, "os.unlink", can_block_functions=[ ("asyncio/unix_events.py", {"_stop_serving"}), ], scanned_modules=modules, excluded_modules=excluded_modules, ) if platform.python_implementation() != "CPython" or sys.version_info >= (3, 9): with os.scandir() as scandir_it: functions["os.scandir"] = BlockBusterFunction( type(scandir_it), "__next__", scanned_modules=modules, excluded_modules=excluded_modules, ) for method in ( "ismount", "samestat", "sameopenfile", ): functions[f"os.path.{method}"] = BlockBusterFunction( None, f"os.path.{method}", scanned_modules=modules, excluded_modules=excluded_modules, ) functions["os.path.islink"] = BlockBusterFunction( None, "os.path.islink", can_block_functions=[ ("coverage/control.py", {"_should_trace"}), ("/pydevd_file_utils.py", {"get_abs_path_real_path_and_base_from_file"}), ], scanned_modules=modules, excluded_modules=excluded_modules, ) functions["os.path.abspath"] = BlockBusterFunction( None, "os.path.abspath", can_block_functions=[ ("_pytest/assertion/rewrite.py", {"_should_rewrite"}), ("coverage/control.py", {"_should_trace"}), ("/pydevd_file_utils.py", {"get_abs_path_real_path_and_base_from_file"}), ], scanned_modules=modules, excluded_modules=excluded_modules, ) def os_rw_exclude(fd: int, *_: Any, **__: Any) -> bool: return hasattr(os, "get_blocking") and not os.get_blocking(fd) os_rw_kwargs = ( {} if platform.system() == "Windows" else {"can_block_predicate": os_rw_exclude} ) functions["os.read"] = BlockBusterFunction( None, "os.read", can_block_functions=[ ("asyncio/base_events.py", {"subprocess_exec"}), ("asyncio/base_events.py", {"subprocess_shell"}), ], scanned_modules=modules, excluded_modules=excluded_modules, **os_rw_kwargs, ) functions["os.write"] = BlockBusterFunction( None, "os.write", can_block_functions=None, scanned_modules=modules, excluded_modules=excluded_modules, **os_rw_kwargs, ) return functions def _get_io_wrapped_functions( modules: _ModuleOrModuleList = None, excluded_modules: _ModuleList = None ) -> dict[str, BlockBusterFunction]: stdout = sys.stdout stderr = sys.stderr def file_read_exclude(file: io.IOBase, *_: Any, **__: Any) -> bool: try: file.fileno() except io.UnsupportedOperation: return not file.isatty() return False def file_write_exclude(file: io.IOBase, *_: Any, **__: Any) -> bool: if file in {stdout, stderr, sys.stdout, sys.stderr} or file.isatty(): return True try: file.fileno() except io.UnsupportedOperation: return True return False return { "io.BufferedReader.read": BlockBusterFunction( None, "io.BufferedReader.read", can_block_functions=[ ("", {"get_data"}), ("_pytest/assertion/rewrite.py", {"_rewrite_test", "_read_pyc"}), ], can_block_predicate=file_read_exclude, scanned_modules=modules, excluded_modules=excluded_modules, ), "io.BufferedWriter.write": BlockBusterFunction( None, "io.BufferedWriter.write", can_block_functions=[ ("", {"_find_and_load"}), ("_pytest/assertion/rewrite.py", {"_write_pyc"}), ], can_block_predicate=file_write_exclude, scanned_modules=modules, excluded_modules=excluded_modules, ), "io.BufferedRandom.read": BlockBusterFunction( None, "io.BufferedRandom.read", can_block_predicate=file_read_exclude, scanned_modules=modules, excluded_modules=excluded_modules, ), "io.BufferedRandom.write": BlockBusterFunction( None, "io.BufferedRandom.write", can_block_predicate=file_write_exclude, scanned_modules=modules, excluded_modules=excluded_modules, ), "io.TextIOWrapper.read": BlockBusterFunction( None, "io.TextIOWrapper.read", can_block_functions=[("aiofile/version.py", {""})], can_block_predicate=file_read_exclude, scanned_modules=modules, excluded_modules=excluded_modules, ), "io.TextIOWrapper.write": BlockBusterFunction( None, "io.TextIOWrapper.write", can_block_predicate=file_write_exclude, scanned_modules=modules, excluded_modules=excluded_modules, ), } def _socket_exclude(sock: socket.socket, *_: Any, **__: Any) -> bool: return not sock.getblocking() def _get_socket_wrapped_functions( modules: _ModuleOrModuleList = None, excluded_modules: _ModuleList = None ) -> dict[str, BlockBusterFunction]: return { f"socket.socket.{method}": BlockBusterFunction( None, f"socket.socket.{method}", can_block_predicate=_socket_exclude, scanned_modules=modules, excluded_modules=excluded_modules, ) for method in ( "connect", "accept", "send", "sendall", "sendto", "recv", "recv_into", "recvfrom", "recvfrom_into", "recvmsg", ) } def _get_ssl_wrapped_functions( modules: _ModuleOrModuleList = None, excluded_modules: _ModuleList = None ) -> dict[str, BlockBusterFunction]: return { f"ssl.SSLSocket.{method}": BlockBusterFunction( None, f"ssl.SSLSocket.{method}", can_block_predicate=_socket_exclude, scanned_modules=modules, excluded_modules=excluded_modules, ) for method in ("write", "send", "read", "recv") } def _get_sqlite_wrapped_functions( modules: _ModuleOrModuleList = None, excluded_modules: _ModuleList = None ) -> dict[str, BlockBusterFunction]: functions = { f"sqlite3.Cursor.{method}": BlockBusterFunction( None, f"sqlite3.Cursor.{method}", scanned_modules=modules, excluded_modules=excluded_modules, ) for method in ( "execute", "executemany", "executescript", "fetchone", "fetchmany", "fetchall", ) } for method in ("execute", "executemany", "executescript", "commit", "rollback"): functions[f"sqlite3.Connection.{method}"] = BlockBusterFunction( None, f"sqlite3.Connection.{method}", scanned_modules=modules, excluded_modules=excluded_modules, ) return functions def _get_lock_wrapped_functions( modules: _ModuleOrModuleList = None, excluded_modules: _ModuleList = None ) -> dict[str, BlockBusterFunction]: def lock_acquire_exclude( lock: threading.Lock, blocking: bool = True, # noqa: FBT001, FBT002 timeout: int = -1, ) -> bool: return not blocking or timeout == 0 or not lock.locked() return { "threading.Lock.acquire": BlockBusterFunction( _thread.LockType, "acquire", can_block_predicate=lock_acquire_exclude, can_block_functions=[ ("threading.py", {"start"}), ("/pydevd.py", {"_do_wait_suspend"}), ("asyncio/base_events.py", {"shutdown_default_executor"}), ("concurrent/futures/thread.py", {"submit"}), ], scanned_modules=modules, excluded_modules=excluded_modules, ), "threading.Lock.acquire_lock": BlockBusterFunction( _thread.LockType, "acquire_lock", can_block_predicate=lock_acquire_exclude, can_block_functions=[ ("threading.py", {"start"}), ("concurrent/futures/thread.py", {"submit"}), ], scanned_modules=modules, excluded_modules=excluded_modules, ), } def _get_builtins_wrapped_functions( modules: _ModuleOrModuleList = None, excluded_modules: _ModuleList = None ) -> dict[str, BlockBusterFunction]: return { "builtins.input": BlockBusterFunction( None, "builtins.input", scanned_modules=modules, excluded_modules=excluded_modules, ) } class BlockBuster: """BlockBuster class.""" def __init__( self, scanned_modules: _ModuleOrModuleList = None, *, excluded_modules: _ModuleList = None, ) -> None: """Initialize BlockBuster. Args: scanned_modules: The modules from which blocking calls are detected. If None, the blocking calls are detected from all the modules. Can be a module name, a module object, a list of module names or a list of module objects. excluded_modules: Sub-modules that are excluded from scanned modules. It doesn't mean that blocking is allowed in these modules. It means that they are not considered as scanned modules. It is helpful for instance if the tests use a pytest plugin that is part of the scanned modules. Can be a list of module names or module objects. """ self.functions = { **_get_time_wrapped_functions(scanned_modules, excluded_modules), **_get_os_wrapped_functions(scanned_modules, excluded_modules), **_get_io_wrapped_functions(scanned_modules, excluded_modules), **_get_socket_wrapped_functions(scanned_modules, excluded_modules), **_get_ssl_wrapped_functions(scanned_modules, excluded_modules), **_get_sqlite_wrapped_functions(scanned_modules, excluded_modules), **_get_lock_wrapped_functions(scanned_modules, excluded_modules), **_get_builtins_wrapped_functions(scanned_modules, excluded_modules), } def activate(self) -> None: """Activate all the functions.""" for wrapped_function in self.functions.values(): wrapped_function.activate() def deactivate(self) -> None: """Deactivate all the functions.""" for wrapped_function in self.functions.values(): wrapped_function.deactivate() @contextmanager def blockbuster_ctx( scanned_modules: _ModuleOrModuleList = None, *, excluded_modules: _ModuleList = None ) -> Iterator[BlockBuster]: """Context manager for using BlockBuster. Args: scanned_modules: The modules from which blocking calls are detected. If None, the blocking calls are detected from all the modules. Can be a list of module names or module objects. excluded_modules: Sub-modules that are excluded from scanned modules. It doesn't mean that blocking is allowed in these modules. It means that they are not considered as scanned modules. It is helpful for instance if the tests use a pytest plugin that is part of the scanned modules. Can be a list of module names or module objects. """ blockbuster = BlockBuster(scanned_modules, excluded_modules=excluded_modules) blockbuster.activate() yield blockbuster blockbuster.deactivate() blockbuster-1.5.26/blockbuster/py.typed0000644000000000000000000000000013615410400015050 0ustar00blockbuster-1.5.26/tests/__init__.py0000644000000000000000000000000013615410400014305 0ustar00blockbuster-1.5.26/tests/test_blockbuster.py0000644000000000000000000003361113615410400016142 0ustar00import asyncio import contextlib import contextvars import functools import importlib import io import os import platform import re import socket import sqlite3 import sys import tempfile import threading import time from asyncio import events from pathlib import Path from typing import Any, Callable, Iterator, TypeVar import pytest import requests import tests from blockbuster import BlockBuster, BlockingError, blockbuster_ctx from tests import subpackage _T = TypeVar("_T") async def to_thread(func: Callable[..., _T], /, *args: Any, **kwargs: Any) -> _T: loop = events.get_running_loop() ctx = contextvars.copy_context() func_call = functools.partial(ctx.run, func, *args, **kwargs) return await loop.run_in_executor(None, func_call) @pytest.fixture(autouse=True) def blockbuster() -> Iterator[BlockBuster]: with blockbuster_ctx() as bb: yield bb @pytest.fixture def test_file() -> Iterator[Path]: with tempfile.NamedTemporaryFile(delete=False) as f: path = Path(f.name) yield path os.unlink(path) async def test_time_sleep() -> None: with pytest.raises(BlockingError, match="Blocking call to time.sleep"): time.sleep(1) # noqa: ASYNC251 PORT = 65432 def tcp_server() -> None: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.bind(("127.0.0.1", PORT)) s.listen() conn, _addr = s.accept() with conn: conn.sendall(b"Hello, world") with contextlib.suppress(ConnectionResetError): conn.recv(1024) async def test_socket_connect() -> None: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s, pytest.raises( BlockingError, match="Blocking call to socket.socket.connect" ): s.connect(("127.0.0.1", PORT)) async def test_socket_send() -> None: tcp_server_task = asyncio.create_task(to_thread(tcp_server)) with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: while True: with contextlib.suppress(ConnectionRefusedError): await asyncio.sleep(0.1) await to_thread(s.connect, ("127.0.0.1", PORT)) break with pytest.raises(BlockingError, match="Blocking call to socket.socket.send"): s.send(b"Hello, world") await tcp_server_task async def test_socket_send_non_blocking() -> None: tcp_server_task = asyncio.create_task(to_thread(tcp_server)) with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: while True: with contextlib.suppress(ConnectionRefusedError): await asyncio.sleep(0.1) await to_thread(s.connect, ("127.0.0.1", PORT)) break blocking = False s.setblocking(blocking) s.send(b"Hello, world") await tcp_server_task async def test_ssl_socket(blockbuster: BlockBuster) -> None: blockbuster.functions["socket.socket.connect"].deactivate() blockbuster.functions["os.stat"].deactivate() with pytest.raises(BlockingError, match="Blocking call to ssl.SSLSocket.send"): requests.get("https://google.com", timeout=10) # noqa: ASYNC210 async def test_file_text(test_file: Path) -> None: with test_file.open(mode="r+", encoding="utf-8") as f: assert isinstance(f, io.TextIOWrapper) with pytest.raises( BlockingError, match="Blocking call to io.TextIOWrapper.write" ): f.write("foo") with pytest.raises( BlockingError, match="Blocking call to io.TextIOWrapper.read" ): f.read(1) async def test_file_random(test_file: Path) -> None: with test_file.open(mode="r+b") as f: assert isinstance(f, io.BufferedRandom) with pytest.raises( BlockingError, match="Blocking call to io.BufferedRandom.write" ): f.write(b"foo") with pytest.raises( BlockingError, match="Blocking call to io.BufferedRandom.read" ): f.read(1) async def test_file_read_bytes(test_file: Path) -> None: with test_file.open(mode="rb") as f: assert isinstance(f, io.BufferedReader) with pytest.raises(BlockingError, match="io.BufferedReader.read"): f.read(1) async def test_file_write_bytes(test_file: Path) -> None: with test_file.open(mode="wb") as f: assert isinstance(f, io.BufferedWriter) with pytest.raises( BlockingError, match="Blocking call to io.BufferedWriter.write" ): f.write(b"foo") async def test_write_std() -> None: sys.stdout.write("test") sys.stderr.write("test") async def test_sqlite_connnection_execute() -> None: with contextlib.closing(sqlite3.connect(":memory:")) as connection, pytest.raises( BlockingError, match="Blocking call to sqlite3.Connection.execute" ): connection.execute("SELECT 1") async def test_sqlite_cursor_execute() -> None: with contextlib.closing( sqlite3.connect(":memory:") ) as connection, contextlib.closing(connection.cursor()) as cursor, pytest.raises( BlockingError, match="Blocking call to sqlite3.Cursor.execute" ): cursor.execute("SELECT 1") async def test_lock() -> None: lock = threading.Lock() assert lock.acquire() is True with pytest.raises(BlockingError, match="Blocking call to lock.acquire"): lock.acquire() async def test_lock_timeout_zero() -> None: lock = threading.Lock() assert lock.acquire() is True assert lock.acquire(timeout=0) is False async def test_lock_non_blocking() -> None: lock = threading.Lock() assert lock.acquire() is True assert lock.acquire(blocking=False) is False async def test_thread_start() -> None: t = threading.Thread(target=lambda: None) t.start() t.join() async def test_import_module() -> None: importlib.reload(requests) def allowed_read(test_file: Path) -> None: with test_file.open(mode="rb") as f: f.read(1) async def test_custom_stack_exclude(blockbuster: BlockBuster, test_file: Path) -> None: blockbuster.functions["io.BufferedReader.read"].can_block_functions.append( ("tests/test_blockbuster.py", {"allowed_read"}) ) allowed_read(test_file) async def test_cleanup(blockbuster: BlockBuster, test_file: Path) -> None: blockbuster.deactivate() with test_file.open(mode="wb") as f: f.write(b"foo") async def test_scanned_modules(blockbuster: BlockBuster, test_file: Path) -> None: blockbuster.deactivate() # Multiple scanned packages with blockbuster_ctx(["tests.subpackage"]): # Call not from subpackage doesn't trigger BlockingError with test_file.open(mode="wb") as f: f.write(b"foo") # Call from subpackage triggers BlockingError with pytest.raises(BlockingError): subpackage.bar(test_file) # Single scanned package with blockbuster_ctx("tests.subpackage"), pytest.raises(BlockingError): subpackage.bar(test_file) # Scanned module file with blockbuster_ctx(["tests.subpackage.foo"]), pytest.raises(BlockingError): subpackage.bar(test_file) # Scanned module object with blockbuster_ctx(tests.subpackage.foo), pytest.raises(BlockingError): subpackage.bar(test_file) subpackage.bar(test_file) # Excluded module name with blockbuster_ctx("tests.subpackage", excluded_modules=["tests.subpackage.foo"]): subpackage.bar(test_file) # Excluded module object with blockbuster_ctx("tests.subpackage", excluded_modules=[tests.subpackage.foo]): subpackage.bar(test_file) async def test_os_read() -> None: fd = os.open(os.devnull, os.O_RDONLY) with pytest.raises(BlockingError, match="Blocking call to os.read"): os.read(fd, 1) @pytest.mark.skipif( platform.system() == "Windows", reason="O_NONBLOCK not supported on Windows" ) async def test_os_read_non_blocking() -> None: fd = os.open(os.devnull, os.O_NONBLOCK | os.O_RDONLY) os.read(fd, 1) async def test_os_write() -> None: fd = os.open(os.devnull, os.O_RDWR) with pytest.raises(BlockingError, match="Blocking call to os.write"): os.write(fd, b"foo") @pytest.mark.skipif( platform.system() == "Windows", reason="O_NONBLOCK not supported on Windows" ) async def test_os_write_non_blocking() -> None: fd = os.open(os.devnull, os.O_NONBLOCK | os.O_RDWR) os.write(fd, b"foo") async def test_os_stat() -> None: with pytest.raises(BlockingError, match="Blocking call to os.stat"): os.stat("/1") async def test_os_getcwd() -> None: with pytest.raises(BlockingError, match="Blocking call to os.getcwd"): os.getcwd() @pytest.mark.skipif(not hasattr(os, "statvfs"), reason="statvfs is not available") async def test_os_statvfs() -> None: with pytest.raises(BlockingError, match="Blocking call to os.statvfs"): os.statvfs("/") @pytest.mark.skipif(not hasattr(os, "sendfile"), reason="sendfile is not available") async def test_os_sendfile() -> None: with pytest.raises(BlockingError, match="Blocking call to os.sendfile"): os.sendfile(0, 1, 0, 1) async def test_os_rename() -> None: with pytest.raises(BlockingError, match="Blocking call to os.rename"): os.rename("/1", "/2") async def test_os_renames() -> None: with pytest.raises(BlockingError, match="Blocking call to os.(stat|rename)"): os.renames("/1", "/2") async def test_os_replace() -> None: with pytest.raises(BlockingError, match="Blocking call to os.replace"): os.replace("/1", "/2") async def test_os_unlink() -> None: with pytest.raises(BlockingError, match="Blocking call to os.unlink"): os.unlink("/1") async def test_os_mkdir() -> None: with pytest.raises(BlockingError, match="Blocking call to os.mkdir"): os.mkdir("/1") async def test_os_makedirs() -> None: with pytest.raises(BlockingError, match="Blocking call to os.(stat|mkdir)"): os.makedirs("/1") async def test_os_rmdir() -> None: with pytest.raises(BlockingError, match="Blocking call to os.rmdir"): os.rmdir("/1") async def test_os_removedirs() -> None: with pytest.raises(BlockingError, match="Blocking call to os.rmdir"): os.removedirs("/1") async def test_os_link() -> None: with pytest.raises(BlockingError, match="Blocking call to os.link"): os.link("/1", "/2") async def test_os_symlink() -> None: with pytest.raises(BlockingError, match="Blocking call to os.symlink"): os.symlink("/1", "/2") async def test_os_readlink() -> None: with pytest.raises(BlockingError, match="Blocking call to os.readlink"): os.readlink("/1") async def test_os_listdir() -> None: with pytest.raises(BlockingError, match="Blocking call to os.listdir"): os.listdir("/1") @pytest.mark.skipif(sys.version_info < (3, 9), reason="requires Python 3.9+") async def test_os_scandir() -> None: with os.scandir(tempfile.tempdir) as files, pytest.raises( BlockingError, match="Blocking call to ScandirIterator.__next__" ): next(files) async def test_os_access() -> None: with pytest.raises(BlockingError, match="Blocking call to os.access"): os.access("/1", os.F_OK) @pytest.mark.skipif( platform.system() == "Windows", reason="os.path.exists not detected on Windows at the moment", ) async def test_os_path_exists() -> None: with pytest.raises(BlockingError, match="Blocking call to os.stat"): os.path.exists("/1") @pytest.mark.skipif( platform.system() == "Windows", reason="os.path.isfile not detected on Windows at the moment", ) async def test_os_path_isfile() -> None: with pytest.raises(BlockingError, match="Blocking call to os.stat"): os.path.isfile("/1") @pytest.mark.skipif( platform.system() == "Windows", reason="os.path.isdir not detected on Windows at the moment", ) async def test_os_path_isdir() -> None: with pytest.raises(BlockingError, match="Blocking call to os.stat"): os.path.isdir("/1") async def test_os_path_islink() -> None: with pytest.raises(BlockingError, match="path.islink"): os.path.islink("/1") async def test_os_path_ismount() -> None: with pytest.raises(BlockingError, match="path.ismount"): os.path.ismount("/1") async def test_os_path_getsize() -> None: with pytest.raises(BlockingError, match="Blocking call to os.stat"): os.path.getsize("/1") async def test_os_path_getmtime() -> None: with pytest.raises(BlockingError, match="Blocking call to os.stat"): os.path.getmtime("/1") async def test_os_path_getatime() -> None: with pytest.raises(BlockingError, match="Blocking call to os.stat"): os.path.getatime("/1") async def test_os_path_getctime() -> None: with pytest.raises(BlockingError, match="Blocking call to os.stat"): os.path.getctime("/1") async def test_os_path_samefile() -> None: with pytest.raises(BlockingError, match="Blocking call to os.stat"): os.path.samefile("/1", "/2") async def test_os_path_sameopenfile() -> None: with pytest.raises(BlockingError, match="path.sameopenfile"): os.path.sameopenfile(0, 0) async def test_os_path_samestat(blockbuster: BlockBuster) -> None: blockbuster.functions["os.stat"].deactivate() with pytest.raises(BlockingError, match="path.samestat"): os.path.samestat(os.stat(0), os.stat(0)) async def test_os_path_abspath() -> None: with pytest.raises(BlockingError, match="path.abspath"): os.path.abspath("/1") async def test_builtins_input() -> None: with pytest.raises( BlockingError, match=re.escape("Blocking call to builtins.input") ): input() def test_can_block_in_builder(blockbuster: BlockBuster) -> None: blockbuster.functions["os.stat"].can_block_in("foo.py", {"bar"}).can_block_in( "baz.py", "qux" ) assert ("foo.py", {"bar"}) in blockbuster.functions["os.stat"].can_block_functions assert ("baz.py", {"qux"}) in blockbuster.functions["os.stat"].can_block_functions blockbuster-1.5.26/tests/subpackage/__init__.py0000644000000000000000000000005013615410400016417 0ustar00from .foo import bar __all__ = ["bar"] blockbuster-1.5.26/tests/subpackage/foo.py0000644000000000000000000000016413615410400015451 0ustar00from pathlib import Path def bar(path: Path) -> None: with path.open(mode="wb") as f: f.write(b"foo") blockbuster-1.5.26/.gitignore0000644000000000000000000000611613615410400013040 0ustar00# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. #pdm.lock # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # in version control. # https://pdm.fming.dev/latest/usage/project/#working-with-version-control .pdm.toml .pdm-python .pdm-build/ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. .idea/ .DS_Store blockbuster-1.5.26/LICENSE0000644000000000000000000002613513615410400012060 0ustar00 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. blockbuster-1.5.26/README.md0000644000000000000000000002200313615410400012320 0ustar00# Blockbuster Blockbuster is a Python package designed to detect and prevent blocking calls within an asynchronous event loop. It is particularly useful when executing tests to ensure that your asynchronous code does not inadvertently call blocking operations, which can lead to performance bottlenecks and unpredictable behavior. In Python, the asynchronous event loop allows for concurrent execution of tasks without the need for multiple threads or processes. This is achieved by running tasks cooperatively, where tasks yield control back to the event loop when they are waiting for I/O operations or other long-running tasks to complete. However, blocking calls, such as file I/O operations or certain networking operations, can halt the entire event loop, preventing other tasks from running. This can lead to increased latency and reduced performance, defeating the purpose of using asynchronous programming. The difficulty with blocking calls is that they are not always obvious, especially when working with third-party libraries or legacy code. This is where Blockbuster comes in: it helps you identify and eliminate blocking calls in your codebase during testing, ensuring that your asynchronous code runs smoothly and efficiently. It does this by wrapping common blocking functions and raising an exception when they are called within an asynchronous context. Notes: - Blockbuster currently only detects `asyncio` event loops. - Blockbuster is tested only with CPython. It may work with other Python implementations if it's possible to monkey-patch the functions with `setattr`. ## Installation The package is named `blockbuster`. For instance with `pip`: ```bash pip install blockbuster ``` It is recommended to constrain the version of Blockbuster. Blockbuster doesn't strictly follow semver. Breaking changes such as new rules added may be introduced between minor versions, but not between patch versions. So it is recommended to constrain the Blockbuster version on the minor version. For instance, with `uv`: ```bash uv add "blockbuster>=1.5.5,<1.6" ``` ## Using BlockBuster ### Manually To activate BlockBuster manually, create an instance of the `BlockBuster` class and call the `activate()` method: ```python from blockbuster import BlockBuster blockbuster = BlockBuster() blockbuster.activate() ``` Once activated, BlockBuster will raise a `BlockingError` exception whenever a blocking call is detected within an `asyncio` event loop. To deactivate BlockBuster, call the `deactivate()` method: ```python blockbuster.deactivate() ``` ### Using the context manager BlockBuster can also be activated using a context manager, which automatically activates and deactivates the checks within the `with` block: ```python from blockbuster import blockbuster_ctx with blockbuster_ctx(): # Your test code here ``` ### Usage with Pytest Blockbuster is intended to be used with testing frameworks like `pytest` to catch blocking calls. Here's how you can integrate Blockbuster into your `pytest` test suite: ```python import pytest import time from blockbuster import BlockBuster, BlockingError, blockbuster_ctx from typing import Iterator @pytest.fixture(autouse=True) def blockbuster() -> Iterator[BlockBuster]: with blockbuster_ctx() as bb: yield bb async def test_time_sleep() -> None: with pytest.raises(BlockingError, match="sleep"): time.sleep(1) # This should raise a BlockingError ``` By using the `blockbuster_ctx` context manager, Blockbuster is automatically activated for every test, and blocking calls will raise a `BlockingError`. ## How it works Blockbuster works by wrapping common blocking functions from various modules (e.g., `os`, `socket`, `time`) and replacing them with versions that check if they are being called from within an `asyncio` event loop. If such a call is detected, Blockbuster raises a `BlockingError` to indicate that a blocking operation is being performed inappropriately. Blockbuster supports by default the following functions and modules: - **Time Functions**: - `time.sleep` - **OS Functions**: - `os.getcwd` - `os.statvfs` - `os.sendfile` - `os.rename` - `os.remove` - `os.unlink` - `os.mkdir` - `os.rmdir` - `os.link` - `os.symlink` - `os.readlink` - `os.listdir` - `os.scandir` - `os.access` - `os.stat` - `os.replace` - `os.read` - `os.write` - **OS path Functions**: - `os.path.ismount` - `os.path.samestat` - `os.path.sameopenfile` - `os.path.islink` - `os.path.abspath` - **IO Functions**: - `io.BufferedReader.read` - `io.BufferedWriter.write` - `io.BufferedRandom.read` - `io.BufferedRandom.write` - `io.TextIOWrapper.read` - `io.TextIOWrapper.write` - **Socket Functions**: - `socket.socket.connect` - `socket.socket.accept` - `socket.socket.send` - `socket.socket.sendall` - `socket.socket.sendto` - `socket.socket.recv` - `socket.socket.recv_into` - `socket.socket.recvfrom` - `socket.socket.recvfrom_into` - `socket.socket.recvmsg` - `ssl.SSLSocket.write` - `ssl.SSLSocket.send` - `ssl.SSLSocket.read` - `ssl.SSLSocket.recv` - **SQLite Functions**: - `sqlite3.Cursor.execute` - `sqlite3.Cursor.executemany` - `sqlite3.Cursor.executescript` - `sqlite3.Cursor.fetchone` - `sqlite3.Cursor.fetchmany` - `sqlite3.Cursor.fetchall` - `sqlite3.Connection.execute` - `sqlite3.Connection.executemany` - `sqlite3.Connection.executescript` - `sqlite3.Connection.commit` - `sqlite3.Connection.rollback` - **Thread lock Functions**: - `threading.Lock.acquire` - `threading.Lock.acquire_lock` - **Built-in Functions**: - `input` Some exceptions to the rules are already in place: - Importing modules does blocking calls as it interacts with the file system. Since this operation is cached and very hard to avoid, it is excluded from the detection. - Blocking calls done by the `pydevd` debugger. - Blocking calls done by the `pytest` framework. ## Customizing Blockbuster ### Adding custom rules Blockbuster is not a silver bullet and may not catch all blocking calls. In particular, it will not catch blocking calls that are done by third-party libraries that do blocking calls in C extensions. For these third-party libraries, you can declare your own custom rules to Blockbuster to catch these blocking calls. Eg.: ```python from blockbuster import BlockBuster, BlockBusterFunction import mymodule blockbuster = BlockBuster() blockbuster.functions["my_module.my_function"] = BlockBusterFunction(my_module, "my_function") blockbuster.activate() ``` Note: if blockbuster has already been activated, you will need to activate the custom rule yourself. ```python from blockbuster import blockbuster_ctx, BlockBusterFunction import mymodule with blockbuster_ctx() as blockbuster: blockbuster.functions["my_module.my_function"] = BlockBusterFunction(my_module, "my_function") blockbuster.functions["my_module.my_function"].activate() ``` ### Allowing blocking calls in specific contexts You can customize Blockbuster to allow blocking calls in specific functions by using the `can_block_in` method of the `BlockBusterFunction` class. This method allows you to specify exceptions for particular files and functions where blocking calls are allowed. ```python from blockbuster import BlockBuster blockbuster = BlockBuster() blockbuster.activate() blockbuster.functions["os.stat"].can_block_in("specific_file.py", {"allowed_function"}) ``` ### Deactivating specific checks If you need to deactivate specific checks, you can directly call the `deactivate` method on the corresponding `BlockBusterFunction` instance: ```python from blockbuster import BlockBuster blockbuster = BlockBuster() blockbuster.activate() blockbuster.functions["socket.socket.connect"].deactivate() ``` ## Contributing Contributions are welcome! If you encounter any issues or have suggestions for improvements, please open an issue on the GitHub repository. ### Development Setup Blockbuster uses `uv` to manage its development environment. See the [uv documentation](https://docs.astral.sh/uv/getting-started/installation/) for more informationand how to install it. To install the required dependencies, run the following command: ```bash uv sync ``` ### Running Tests Tests are written using `pytest`. To run the tests, use the following command: ```bash uv run pytest ``` ### Code Formatting Code formatting is done using `ruff`. To format the code, run the following command: ```bash uv run ruff format ``` ### Code Linting Code linting is done using `ruff`. To lint the code, fixing any issues that can be automatically fixed, run the following command: ```bash uv run ruff check --fix ``` ## License This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details. ## Acknowledgments Blockbuster uses the [forbiddenfruit](https://clarete.li/forbiddenfruit/) library to monkey-patch CPython immutable builtin functions and methods. Blockbuster was greatly inspired by the [BlockHound](https://github.com/reactor/BlockHound) library for Java, which serves a similar purpose of detecting blocking calls in JVM reactive applications. blockbuster-1.5.26/pyproject.toml0000644000000000000000000000404513615410400013763 0ustar00[project] name = "blockbuster" version = "1.5.26" description = "Utility to detect blocking calls in the async event loop" readme = "README.md" keywords = ["async", "block", "detect", "event loop", "asyncio"] authors = [ { name = "Christophe Bornet", email = "bornet.chris@gmail.com" }, ] requires-python = ">=3.8" dependencies = [ "forbiddenfruit>=0.1.4; implementation_name== 'cpython'", ] classifiers = [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "Topic :: Software Development :: Build Tools", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", ] [project.urls] Repository = "https://github.com/cbornet/blockbuster.git" Issues = "https://github.com/cbornet/blockbuster/issues" [dependency-groups] dev = [ "mypy>=1.13.0", "pytest-asyncio>=0.24.0", "pytest>=8.3.3", "ruff>=0.8.0,<0.9", "requests>=2.32.3", "types-requests>=2.32.0.20241016", "aiofile>=3.9.0", ] [tool.pytest.ini_options] asyncio_mode = "auto" [tool.ruff.lint] select = ["ALL"] ignore = [ "C90", # Complexity "CPY", # Missing copyright "COM812", # Messes with the formatter "ISC001", # Messes with the formatter "PERF203", # Rarely useful "PLR09", # Too many something (arg, statements, etc) ] pydocstyle.convention = "google" flake8-annotations.allow-star-arg-any = true flake8-annotations.mypy-init-return = true [tool.ruff.lint.per-file-ignores] "tests/*" = [ "D1", "S101", "PTH", ] [tool.mypy] strict = true warn_unreachable = true pretty = true show_error_codes = true show_error_context = true [[tool.mypy.overrides]] module = "forbiddenfruit.*" ignore_missing_imports = true [tool.hatch.build.targets.wheel] packages = ["blockbuster"] [build-system] requires = ["hatchling"] build-backend = "hatchling.build" blockbuster-1.5.26/PKG-INFO0000644000000000000000000002401213615410400012140 0ustar00Metadata-Version: 2.4 Name: blockbuster Version: 1.5.26 Summary: Utility to detect blocking calls in the async event loop Project-URL: Repository, https://github.com/cbornet/blockbuster.git Project-URL: Issues, https://github.com/cbornet/blockbuster/issues Author-email: Christophe Bornet License-File: LICENSE Keywords: async,asyncio,block,detect,event loop Classifier: Development Status :: 3 - Alpha Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Topic :: Software Development :: Build Tools Requires-Python: >=3.8 Requires-Dist: forbiddenfruit>=0.1.4; implementation_name == 'cpython' Description-Content-Type: text/markdown # Blockbuster Blockbuster is a Python package designed to detect and prevent blocking calls within an asynchronous event loop. It is particularly useful when executing tests to ensure that your asynchronous code does not inadvertently call blocking operations, which can lead to performance bottlenecks and unpredictable behavior. In Python, the asynchronous event loop allows for concurrent execution of tasks without the need for multiple threads or processes. This is achieved by running tasks cooperatively, where tasks yield control back to the event loop when they are waiting for I/O operations or other long-running tasks to complete. However, blocking calls, such as file I/O operations or certain networking operations, can halt the entire event loop, preventing other tasks from running. This can lead to increased latency and reduced performance, defeating the purpose of using asynchronous programming. The difficulty with blocking calls is that they are not always obvious, especially when working with third-party libraries or legacy code. This is where Blockbuster comes in: it helps you identify and eliminate blocking calls in your codebase during testing, ensuring that your asynchronous code runs smoothly and efficiently. It does this by wrapping common blocking functions and raising an exception when they are called within an asynchronous context. Notes: - Blockbuster currently only detects `asyncio` event loops. - Blockbuster is tested only with CPython. It may work with other Python implementations if it's possible to monkey-patch the functions with `setattr`. ## Installation The package is named `blockbuster`. For instance with `pip`: ```bash pip install blockbuster ``` It is recommended to constrain the version of Blockbuster. Blockbuster doesn't strictly follow semver. Breaking changes such as new rules added may be introduced between minor versions, but not between patch versions. So it is recommended to constrain the Blockbuster version on the minor version. For instance, with `uv`: ```bash uv add "blockbuster>=1.5.5,<1.6" ``` ## Using BlockBuster ### Manually To activate BlockBuster manually, create an instance of the `BlockBuster` class and call the `activate()` method: ```python from blockbuster import BlockBuster blockbuster = BlockBuster() blockbuster.activate() ``` Once activated, BlockBuster will raise a `BlockingError` exception whenever a blocking call is detected within an `asyncio` event loop. To deactivate BlockBuster, call the `deactivate()` method: ```python blockbuster.deactivate() ``` ### Using the context manager BlockBuster can also be activated using a context manager, which automatically activates and deactivates the checks within the `with` block: ```python from blockbuster import blockbuster_ctx with blockbuster_ctx(): # Your test code here ``` ### Usage with Pytest Blockbuster is intended to be used with testing frameworks like `pytest` to catch blocking calls. Here's how you can integrate Blockbuster into your `pytest` test suite: ```python import pytest import time from blockbuster import BlockBuster, BlockingError, blockbuster_ctx from typing import Iterator @pytest.fixture(autouse=True) def blockbuster() -> Iterator[BlockBuster]: with blockbuster_ctx() as bb: yield bb async def test_time_sleep() -> None: with pytest.raises(BlockingError, match="sleep"): time.sleep(1) # This should raise a BlockingError ``` By using the `blockbuster_ctx` context manager, Blockbuster is automatically activated for every test, and blocking calls will raise a `BlockingError`. ## How it works Blockbuster works by wrapping common blocking functions from various modules (e.g., `os`, `socket`, `time`) and replacing them with versions that check if they are being called from within an `asyncio` event loop. If such a call is detected, Blockbuster raises a `BlockingError` to indicate that a blocking operation is being performed inappropriately. Blockbuster supports by default the following functions and modules: - **Time Functions**: - `time.sleep` - **OS Functions**: - `os.getcwd` - `os.statvfs` - `os.sendfile` - `os.rename` - `os.remove` - `os.unlink` - `os.mkdir` - `os.rmdir` - `os.link` - `os.symlink` - `os.readlink` - `os.listdir` - `os.scandir` - `os.access` - `os.stat` - `os.replace` - `os.read` - `os.write` - **OS path Functions**: - `os.path.ismount` - `os.path.samestat` - `os.path.sameopenfile` - `os.path.islink` - `os.path.abspath` - **IO Functions**: - `io.BufferedReader.read` - `io.BufferedWriter.write` - `io.BufferedRandom.read` - `io.BufferedRandom.write` - `io.TextIOWrapper.read` - `io.TextIOWrapper.write` - **Socket Functions**: - `socket.socket.connect` - `socket.socket.accept` - `socket.socket.send` - `socket.socket.sendall` - `socket.socket.sendto` - `socket.socket.recv` - `socket.socket.recv_into` - `socket.socket.recvfrom` - `socket.socket.recvfrom_into` - `socket.socket.recvmsg` - `ssl.SSLSocket.write` - `ssl.SSLSocket.send` - `ssl.SSLSocket.read` - `ssl.SSLSocket.recv` - **SQLite Functions**: - `sqlite3.Cursor.execute` - `sqlite3.Cursor.executemany` - `sqlite3.Cursor.executescript` - `sqlite3.Cursor.fetchone` - `sqlite3.Cursor.fetchmany` - `sqlite3.Cursor.fetchall` - `sqlite3.Connection.execute` - `sqlite3.Connection.executemany` - `sqlite3.Connection.executescript` - `sqlite3.Connection.commit` - `sqlite3.Connection.rollback` - **Thread lock Functions**: - `threading.Lock.acquire` - `threading.Lock.acquire_lock` - **Built-in Functions**: - `input` Some exceptions to the rules are already in place: - Importing modules does blocking calls as it interacts with the file system. Since this operation is cached and very hard to avoid, it is excluded from the detection. - Blocking calls done by the `pydevd` debugger. - Blocking calls done by the `pytest` framework. ## Customizing Blockbuster ### Adding custom rules Blockbuster is not a silver bullet and may not catch all blocking calls. In particular, it will not catch blocking calls that are done by third-party libraries that do blocking calls in C extensions. For these third-party libraries, you can declare your own custom rules to Blockbuster to catch these blocking calls. Eg.: ```python from blockbuster import BlockBuster, BlockBusterFunction import mymodule blockbuster = BlockBuster() blockbuster.functions["my_module.my_function"] = BlockBusterFunction(my_module, "my_function") blockbuster.activate() ``` Note: if blockbuster has already been activated, you will need to activate the custom rule yourself. ```python from blockbuster import blockbuster_ctx, BlockBusterFunction import mymodule with blockbuster_ctx() as blockbuster: blockbuster.functions["my_module.my_function"] = BlockBusterFunction(my_module, "my_function") blockbuster.functions["my_module.my_function"].activate() ``` ### Allowing blocking calls in specific contexts You can customize Blockbuster to allow blocking calls in specific functions by using the `can_block_in` method of the `BlockBusterFunction` class. This method allows you to specify exceptions for particular files and functions where blocking calls are allowed. ```python from blockbuster import BlockBuster blockbuster = BlockBuster() blockbuster.activate() blockbuster.functions["os.stat"].can_block_in("specific_file.py", {"allowed_function"}) ``` ### Deactivating specific checks If you need to deactivate specific checks, you can directly call the `deactivate` method on the corresponding `BlockBusterFunction` instance: ```python from blockbuster import BlockBuster blockbuster = BlockBuster() blockbuster.activate() blockbuster.functions["socket.socket.connect"].deactivate() ``` ## Contributing Contributions are welcome! If you encounter any issues or have suggestions for improvements, please open an issue on the GitHub repository. ### Development Setup Blockbuster uses `uv` to manage its development environment. See the [uv documentation](https://docs.astral.sh/uv/getting-started/installation/) for more informationand how to install it. To install the required dependencies, run the following command: ```bash uv sync ``` ### Running Tests Tests are written using `pytest`. To run the tests, use the following command: ```bash uv run pytest ``` ### Code Formatting Code formatting is done using `ruff`. To format the code, run the following command: ```bash uv run ruff format ``` ### Code Linting Code linting is done using `ruff`. To lint the code, fixing any issues that can be automatically fixed, run the following command: ```bash uv run ruff check --fix ``` ## License This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details. ## Acknowledgments Blockbuster uses the [forbiddenfruit](https://clarete.li/forbiddenfruit/) library to monkey-patch CPython immutable builtin functions and methods. Blockbuster was greatly inspired by the [BlockHound](https://github.com/reactor/BlockHound) library for Java, which serves a similar purpose of detecting blocking calls in JVM reactive applications.