From ad6ecc3bbcdd3c1685c974ca106ea88c3bc8f0e0 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 29 Feb 2024 14:04:44 +0700 Subject: [PATCH 001/125] =?UTF-8?q?=E2=9C=A8=20Add=20LocalBox=20example=20?= =?UTF-8?q?script?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/localbox.py | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 examples/localbox.py diff --git a/examples/localbox.py b/examples/localbox.py new file mode 100644 index 0000000..204887e --- /dev/null +++ b/examples/localbox.py @@ -0,0 +1,6 @@ +from codeboxapi.box import LocalBox + +with LocalBox() as box: + box.run("print('Hello, world!')") + box.install("pandas") + v = box.run("pandas.__version__") From 44e1db99c959a54cbbb97db4ebdb05e650fef0f0 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 29 Feb 2024 14:04:44 +0700 Subject: [PATCH 002/125] =?UTF-8?q?=F0=9F=9B=A0=20Adjusted=20parameter=20i?= =?UTF-8?q?n=20run?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/plot_dataset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/plot_dataset.py b/examples/plot_dataset.py index 51d9da0..d60f3c6 100644 --- a/examples/plot_dataset.py +++ b/examples/plot_dataset.py @@ -17,7 +17,7 @@ file_path = Path("examples/assets/dataset_code.txt") # run the code - output = codebox.run(file_path=file_path) + output = codebox.run(code=file_path) print(output.type) if output.type == "image/png" and os.environ.get("CODEBOX_TEST") == "False": From 6c082ed0c07a83345c8e2b9050087964789a2745 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 29 Feb 2024 14:04:44 +0700 Subject: [PATCH 003/125] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Removed=20poetr?= =?UTF-8?q?y.lock=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 3547 --------------------------------------------------- 1 file changed, 3547 deletions(-) delete mode 100644 poetry.lock diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index 0937b41..0000000 --- a/poetry.lock +++ /dev/null @@ -1,3547 +0,0 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. - -[[package]] -name = "aiohttp" -version = "3.8.6" -description = "Async http client/server framework (asyncio)" -optional = false -python-versions = ">=3.6" -files = [ - {file = "aiohttp-3.8.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:41d55fc043954cddbbd82503d9cc3f4814a40bcef30b3569bc7b5e34130718c1"}, - {file = "aiohttp-3.8.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1d84166673694841d8953f0a8d0c90e1087739d24632fe86b1a08819168b4566"}, - {file = "aiohttp-3.8.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:253bf92b744b3170eb4c4ca2fa58f9c4b87aeb1df42f71d4e78815e6e8b73c9e"}, - {file = "aiohttp-3.8.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fd194939b1f764d6bb05490987bfe104287bbf51b8d862261ccf66f48fb4096"}, - {file = "aiohttp-3.8.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c5f938d199a6fdbdc10bbb9447496561c3a9a565b43be564648d81e1102ac22"}, - {file = "aiohttp-3.8.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2817b2f66ca82ee699acd90e05c95e79bbf1dc986abb62b61ec8aaf851e81c93"}, - {file = "aiohttp-3.8.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fa375b3d34e71ccccf172cab401cd94a72de7a8cc01847a7b3386204093bb47"}, - {file = "aiohttp-3.8.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9de50a199b7710fa2904be5a4a9b51af587ab24c8e540a7243ab737b45844543"}, - {file = "aiohttp-3.8.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e1d8cb0b56b3587c5c01de3bf2f600f186da7e7b5f7353d1bf26a8ddca57f965"}, - {file = "aiohttp-3.8.6-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8e31e9db1bee8b4f407b77fd2507337a0a80665ad7b6c749d08df595d88f1cf5"}, - {file = "aiohttp-3.8.6-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7bc88fc494b1f0311d67f29fee6fd636606f4697e8cc793a2d912ac5b19aa38d"}, - {file = "aiohttp-3.8.6-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ec00c3305788e04bf6d29d42e504560e159ccaf0be30c09203b468a6c1ccd3b2"}, - {file = "aiohttp-3.8.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ad1407db8f2f49329729564f71685557157bfa42b48f4b93e53721a16eb813ed"}, - {file = "aiohttp-3.8.6-cp310-cp310-win32.whl", hash = "sha256:ccc360e87341ad47c777f5723f68adbb52b37ab450c8bc3ca9ca1f3e849e5fe2"}, - {file = "aiohttp-3.8.6-cp310-cp310-win_amd64.whl", hash = "sha256:93c15c8e48e5e7b89d5cb4613479d144fda8344e2d886cf694fd36db4cc86865"}, - {file = "aiohttp-3.8.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e2f9cc8e5328f829f6e1fb74a0a3a939b14e67e80832975e01929e320386b34"}, - {file = "aiohttp-3.8.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e6a00ffcc173e765e200ceefb06399ba09c06db97f401f920513a10c803604ca"}, - {file = "aiohttp-3.8.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:41bdc2ba359032e36c0e9de5a3bd00d6fb7ea558a6ce6b70acedf0da86458321"}, - {file = "aiohttp-3.8.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14cd52ccf40006c7a6cd34a0f8663734e5363fd981807173faf3a017e202fec9"}, - {file = "aiohttp-3.8.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2d5b785c792802e7b275c420d84f3397668e9d49ab1cb52bd916b3b3ffcf09ad"}, - {file = "aiohttp-3.8.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1bed815f3dc3d915c5c1e556c397c8667826fbc1b935d95b0ad680787896a358"}, - {file = "aiohttp-3.8.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96603a562b546632441926cd1293cfcb5b69f0b4159e6077f7c7dbdfb686af4d"}, - {file = "aiohttp-3.8.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d76e8b13161a202d14c9584590c4df4d068c9567c99506497bdd67eaedf36403"}, - {file = "aiohttp-3.8.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e3f1e3f1a1751bb62b4a1b7f4e435afcdade6c17a4fd9b9d43607cebd242924a"}, - {file = "aiohttp-3.8.6-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:76b36b3124f0223903609944a3c8bf28a599b2cc0ce0be60b45211c8e9be97f8"}, - {file = "aiohttp-3.8.6-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:a2ece4af1f3c967a4390c284797ab595a9f1bc1130ef8b01828915a05a6ae684"}, - {file = "aiohttp-3.8.6-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:16d330b3b9db87c3883e565340d292638a878236418b23cc8b9b11a054aaa887"}, - {file = "aiohttp-3.8.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:42c89579f82e49db436b69c938ab3e1559e5a4409eb8639eb4143989bc390f2f"}, - {file = "aiohttp-3.8.6-cp311-cp311-win32.whl", hash = "sha256:efd2fcf7e7b9d7ab16e6b7d54205beded0a9c8566cb30f09c1abe42b4e22bdcb"}, - {file = "aiohttp-3.8.6-cp311-cp311-win_amd64.whl", hash = "sha256:3b2ab182fc28e7a81f6c70bfbd829045d9480063f5ab06f6e601a3eddbbd49a0"}, - {file = "aiohttp-3.8.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:fdee8405931b0615220e5ddf8cd7edd8592c606a8e4ca2a00704883c396e4479"}, - {file = "aiohttp-3.8.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d25036d161c4fe2225d1abff2bd52c34ed0b1099f02c208cd34d8c05729882f0"}, - {file = "aiohttp-3.8.6-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d791245a894be071d5ab04bbb4850534261a7d4fd363b094a7b9963e8cdbd31"}, - {file = "aiohttp-3.8.6-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0cccd1de239afa866e4ce5c789b3032442f19c261c7d8a01183fd956b1935349"}, - {file = "aiohttp-3.8.6-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f13f60d78224f0dace220d8ab4ef1dbc37115eeeab8c06804fec11bec2bbd07"}, - {file = "aiohttp-3.8.6-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a9b5a0606faca4f6cc0d338359d6fa137104c337f489cd135bb7fbdbccb1e39"}, - {file = "aiohttp-3.8.6-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:13da35c9ceb847732bf5c6c5781dcf4780e14392e5d3b3c689f6d22f8e15ae31"}, - {file = "aiohttp-3.8.6-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:4d4cbe4ffa9d05f46a28252efc5941e0462792930caa370a6efaf491f412bc66"}, - {file = "aiohttp-3.8.6-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:229852e147f44da0241954fc6cb910ba074e597f06789c867cb7fb0621e0ba7a"}, - {file = "aiohttp-3.8.6-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:713103a8bdde61d13490adf47171a1039fd880113981e55401a0f7b42c37d071"}, - {file = "aiohttp-3.8.6-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:45ad816b2c8e3b60b510f30dbd37fe74fd4a772248a52bb021f6fd65dff809b6"}, - {file = "aiohttp-3.8.6-cp36-cp36m-win32.whl", hash = "sha256:2b8d4e166e600dcfbff51919c7a3789ff6ca8b3ecce16e1d9c96d95dd569eb4c"}, - {file = "aiohttp-3.8.6-cp36-cp36m-win_amd64.whl", hash = "sha256:0912ed87fee967940aacc5306d3aa8ba3a459fcd12add0b407081fbefc931e53"}, - {file = "aiohttp-3.8.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e2a988a0c673c2e12084f5e6ba3392d76c75ddb8ebc6c7e9ead68248101cd446"}, - {file = "aiohttp-3.8.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebf3fd9f141700b510d4b190094db0ce37ac6361a6806c153c161dc6c041ccda"}, - {file = "aiohttp-3.8.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3161ce82ab85acd267c8f4b14aa226047a6bee1e4e6adb74b798bd42c6ae1f80"}, - {file = "aiohttp-3.8.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95fc1bf33a9a81469aa760617b5971331cdd74370d1214f0b3109272c0e1e3c"}, - {file = "aiohttp-3.8.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c43ecfef7deaf0617cee936836518e7424ee12cb709883f2c9a1adda63cc460"}, - {file = "aiohttp-3.8.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca80e1b90a05a4f476547f904992ae81eda5c2c85c66ee4195bb8f9c5fb47f28"}, - {file = "aiohttp-3.8.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:90c72ebb7cb3a08a7f40061079817133f502a160561d0675b0a6adf231382c92"}, - {file = "aiohttp-3.8.6-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bb54c54510e47a8c7c8e63454a6acc817519337b2b78606c4e840871a3e15349"}, - {file = "aiohttp-3.8.6-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:de6a1c9f6803b90e20869e6b99c2c18cef5cc691363954c93cb9adeb26d9f3ae"}, - {file = "aiohttp-3.8.6-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:a3628b6c7b880b181a3ae0a0683698513874df63783fd89de99b7b7539e3e8a8"}, - {file = "aiohttp-3.8.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:fc37e9aef10a696a5a4474802930079ccfc14d9f9c10b4662169671ff034b7df"}, - {file = "aiohttp-3.8.6-cp37-cp37m-win32.whl", hash = "sha256:f8ef51e459eb2ad8e7a66c1d6440c808485840ad55ecc3cafefadea47d1b1ba2"}, - {file = "aiohttp-3.8.6-cp37-cp37m-win_amd64.whl", hash = "sha256:b2fe42e523be344124c6c8ef32a011444e869dc5f883c591ed87f84339de5976"}, - {file = "aiohttp-3.8.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9e2ee0ac5a1f5c7dd3197de309adfb99ac4617ff02b0603fd1e65b07dc772e4b"}, - {file = "aiohttp-3.8.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:01770d8c04bd8db568abb636c1fdd4f7140b284b8b3e0b4584f070180c1e5c62"}, - {file = "aiohttp-3.8.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3c68330a59506254b556b99a91857428cab98b2f84061260a67865f7f52899f5"}, - {file = "aiohttp-3.8.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89341b2c19fb5eac30c341133ae2cc3544d40d9b1892749cdd25892bbc6ac951"}, - {file = "aiohttp-3.8.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:71783b0b6455ac8f34b5ec99d83e686892c50498d5d00b8e56d47f41b38fbe04"}, - {file = "aiohttp-3.8.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f628dbf3c91e12f4d6c8b3f092069567d8eb17814aebba3d7d60c149391aee3a"}, - {file = "aiohttp-3.8.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b04691bc6601ef47c88f0255043df6f570ada1a9ebef99c34bd0b72866c217ae"}, - {file = "aiohttp-3.8.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ee912f7e78287516df155f69da575a0ba33b02dd7c1d6614dbc9463f43066e3"}, - {file = "aiohttp-3.8.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9c19b26acdd08dd239e0d3669a3dddafd600902e37881f13fbd8a53943079dbc"}, - {file = "aiohttp-3.8.6-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:99c5ac4ad492b4a19fc132306cd57075c28446ec2ed970973bbf036bcda1bcc6"}, - {file = "aiohttp-3.8.6-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:f0f03211fd14a6a0aed2997d4b1c013d49fb7b50eeb9ffdf5e51f23cfe2c77fa"}, - {file = "aiohttp-3.8.6-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:8d399dade330c53b4106160f75f55407e9ae7505263ea86f2ccca6bfcbdb4921"}, - {file = "aiohttp-3.8.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ec4fd86658c6a8964d75426517dc01cbf840bbf32d055ce64a9e63a40fd7b771"}, - {file = "aiohttp-3.8.6-cp38-cp38-win32.whl", hash = "sha256:33164093be11fcef3ce2571a0dccd9041c9a93fa3bde86569d7b03120d276c6f"}, - {file = "aiohttp-3.8.6-cp38-cp38-win_amd64.whl", hash = "sha256:bdf70bfe5a1414ba9afb9d49f0c912dc524cf60141102f3a11143ba3d291870f"}, - {file = "aiohttp-3.8.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d52d5dc7c6682b720280f9d9db41d36ebe4791622c842e258c9206232251ab2b"}, - {file = "aiohttp-3.8.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4ac39027011414dbd3d87f7edb31680e1f430834c8cef029f11c66dad0670aa5"}, - {file = "aiohttp-3.8.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3f5c7ce535a1d2429a634310e308fb7d718905487257060e5d4598e29dc17f0b"}, - {file = "aiohttp-3.8.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b30e963f9e0d52c28f284d554a9469af073030030cef8693106d918b2ca92f54"}, - {file = "aiohttp-3.8.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:918810ef188f84152af6b938254911055a72e0f935b5fbc4c1a4ed0b0584aed1"}, - {file = "aiohttp-3.8.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:002f23e6ea8d3dd8d149e569fd580c999232b5fbc601c48d55398fbc2e582e8c"}, - {file = "aiohttp-3.8.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fcf3eabd3fd1a5e6092d1242295fa37d0354b2eb2077e6eb670accad78e40e1"}, - {file = "aiohttp-3.8.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:255ba9d6d5ff1a382bb9a578cd563605aa69bec845680e21c44afc2670607a95"}, - {file = "aiohttp-3.8.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d67f8baed00870aa390ea2590798766256f31dc5ed3ecc737debb6e97e2ede78"}, - {file = "aiohttp-3.8.6-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:86f20cee0f0a317c76573b627b954c412ea766d6ada1a9fcf1b805763ae7feeb"}, - {file = "aiohttp-3.8.6-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:39a312d0e991690ccc1a61f1e9e42daa519dcc34ad03eb6f826d94c1190190dd"}, - {file = "aiohttp-3.8.6-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e827d48cf802de06d9c935088c2924e3c7e7533377d66b6f31ed175c1620e05e"}, - {file = "aiohttp-3.8.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bd111d7fc5591ddf377a408ed9067045259ff2770f37e2d94e6478d0f3fc0c17"}, - {file = "aiohttp-3.8.6-cp39-cp39-win32.whl", hash = "sha256:caf486ac1e689dda3502567eb89ffe02876546599bbf915ec94b1fa424eeffd4"}, - {file = "aiohttp-3.8.6-cp39-cp39-win_amd64.whl", hash = "sha256:3f0e27e5b733803333bb2371249f41cf42bae8884863e8e8965ec69bebe53132"}, - {file = "aiohttp-3.8.6.tar.gz", hash = "sha256:b0cf2a4501bff9330a8a5248b4ce951851e415bdcce9dc158e76cfd55e15085c"}, -] - -[package.dependencies] -aiosignal = ">=1.1.2" -async-timeout = ">=4.0.0a3,<5.0" -attrs = ">=17.3.0" -charset-normalizer = ">=2.0,<4.0" -frozenlist = ">=1.1.1" -multidict = ">=4.5,<7.0" -yarl = ">=1.0,<2.0" - -[package.extras] -speedups = ["Brotli", "aiodns", "cchardet"] - -[[package]] -name = "aiosignal" -version = "1.3.1" -description = "aiosignal: a list of registered asynchronous callbacks" -optional = false -python-versions = ">=3.7" -files = [ - {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, - {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, -] - -[package.dependencies] -frozenlist = ">=1.1.0" - -[[package]] -name = "annotated-types" -version = "0.6.0" -description = "Reusable constraint types to use with typing.Annotated" -optional = false -python-versions = ">=3.8" -files = [ - {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, - {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, -] - -[[package]] -name = "anyio" -version = "4.0.0" -description = "High level compatibility layer for multiple asynchronous event loop implementations" -optional = false -python-versions = ">=3.8" -files = [ - {file = "anyio-4.0.0-py3-none-any.whl", hash = "sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f"}, - {file = "anyio-4.0.0.tar.gz", hash = "sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a"}, -] - -[package.dependencies] -exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} -idna = ">=2.8" -sniffio = ">=1.1" - -[package.extras] -doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (>=0.22)"] - -[[package]] -name = "appnope" -version = "0.1.3" -description = "Disable App Nap on macOS >= 10.9" -optional = true -python-versions = "*" -files = [ - {file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"}, - {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"}, -] - -[[package]] -name = "argon2-cffi" -version = "23.1.0" -description = "Argon2 for Python" -optional = true -python-versions = ">=3.7" -files = [ - {file = "argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea"}, - {file = "argon2_cffi-23.1.0.tar.gz", hash = "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08"}, -] - -[package.dependencies] -argon2-cffi-bindings = "*" - -[package.extras] -dev = ["argon2-cffi[tests,typing]", "tox (>4)"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-copybutton", "sphinx-notfound-page"] -tests = ["hypothesis", "pytest"] -typing = ["mypy"] - -[[package]] -name = "argon2-cffi-bindings" -version = "21.2.0" -description = "Low-level CFFI bindings for Argon2" -optional = true -python-versions = ">=3.6" -files = [ - {file = "argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f"}, - {file = "argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93"}, - {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3e385d1c39c520c08b53d63300c3ecc28622f076f4c2b0e6d7e796e9f6502194"}, - {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3e3cc67fdb7d82c4718f19b4e7a87123caf8a93fde7e23cf66ac0337d3cb3f"}, - {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a22ad9800121b71099d0fb0a65323810a15f2e292f2ba450810a7316e128ee5"}, - {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9f8b450ed0547e3d473fdc8612083fd08dd2120d6ac8f73828df9b7d45bb351"}, - {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:93f9bf70084f97245ba10ee36575f0c3f1e7d7724d67d8e5b08e61787c320ed7"}, - {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3b9ef65804859d335dc6b31582cad2c5166f0c3e7975f324d9ffaa34ee7e6583"}, - {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4966ef5848d820776f5f562a7d45fdd70c2f330c961d0d745b784034bd9f48d"}, - {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ef543a89dee4db46a1a6e206cd015360e5a75822f76df533845c3cbaf72670"}, - {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2937d286e2ad0cc79a7087d3c272832865f779430e0cc2b4f3718d3159b0cb"}, - {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5e00316dabdaea0b2dd82d141cc66889ced0cdcbfa599e8b471cf22c620c329a"}, -] - -[package.dependencies] -cffi = ">=1.0.1" - -[package.extras] -dev = ["cogapp", "pre-commit", "pytest", "wheel"] -tests = ["pytest"] - -[[package]] -name = "arrow" -version = "1.3.0" -description = "Better dates & times for Python" -optional = true -python-versions = ">=3.8" -files = [ - {file = "arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80"}, - {file = "arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85"}, -] - -[package.dependencies] -python-dateutil = ">=2.7.0" -types-python-dateutil = ">=2.8.10" - -[package.extras] -doc = ["doc8", "sphinx (>=7.0.0)", "sphinx-autobuild", "sphinx-autodoc-typehints", "sphinx_rtd_theme (>=1.3.0)"] -test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2021.1)", "simplejson (==3.*)"] - -[[package]] -name = "asttokens" -version = "2.4.1" -description = "Annotate AST trees with source code positions" -optional = true -python-versions = "*" -files = [ - {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, - {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, -] - -[package.dependencies] -six = ">=1.12.0" - -[package.extras] -astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] -test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] - -[[package]] -name = "async-timeout" -version = "4.0.3" -description = "Timeout context manager for asyncio programs" -optional = false -python-versions = ">=3.7" -files = [ - {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, - {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, -] - -[[package]] -name = "attrs" -version = "23.1.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.7" -files = [ - {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, - {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, -] - -[package.extras] -cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[docs,tests]", "pre-commit"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] -tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] - -[[package]] -name = "babel" -version = "2.13.1" -description = "Internationalization utilities" -optional = false -python-versions = ">=3.7" -files = [ - {file = "Babel-2.13.1-py3-none-any.whl", hash = "sha256:7077a4984b02b6727ac10f1f7294484f737443d7e2e66c5e4380e41a3ae0b4ed"}, - {file = "Babel-2.13.1.tar.gz", hash = "sha256:33e0952d7dd6374af8dbf6768cc4ddf3ccfefc244f9986d4074704f2fbd18900"}, -] - -[package.dependencies] -setuptools = {version = "*", markers = "python_version >= \"3.12\""} - -[package.extras] -dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] - -[[package]] -name = "backcall" -version = "0.2.0" -description = "Specifications for callback functions passed in to an API" -optional = true -python-versions = "*" -files = [ - {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, - {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, -] - -[[package]] -name = "beautifulsoup4" -version = "4.12.2" -description = "Screen-scraping library" -optional = true -python-versions = ">=3.6.0" -files = [ - {file = "beautifulsoup4-4.12.2-py3-none-any.whl", hash = "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a"}, - {file = "beautifulsoup4-4.12.2.tar.gz", hash = "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da"}, -] - -[package.dependencies] -soupsieve = ">1.2" - -[package.extras] -html5lib = ["html5lib"] -lxml = ["lxml"] - -[[package]] -name = "black" -version = "23.10.1" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.8" -files = [ - {file = "black-23.10.1-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:ec3f8e6234c4e46ff9e16d9ae96f4ef69fa328bb4ad08198c8cee45bb1f08c69"}, - {file = "black-23.10.1-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:1b917a2aa020ca600483a7b340c165970b26e9029067f019e3755b56e8dd5916"}, - {file = "black-23.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c74de4c77b849e6359c6f01987e94873c707098322b91490d24296f66d067dc"}, - {file = "black-23.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:7b4d10b0f016616a0d93d24a448100adf1699712fb7a4efd0e2c32bbb219b173"}, - {file = "black-23.10.1-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b15b75fc53a2fbcac8a87d3e20f69874d161beef13954747e053bca7a1ce53a0"}, - {file = "black-23.10.1-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:e293e4c2f4a992b980032bbd62df07c1bcff82d6964d6c9496f2cd726e246ace"}, - {file = "black-23.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d56124b7a61d092cb52cce34182a5280e160e6aff3137172a68c2c2c4b76bcb"}, - {file = "black-23.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:3f157a8945a7b2d424da3335f7ace89c14a3b0625e6593d21139c2d8214d55ce"}, - {file = "black-23.10.1-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:cfcce6f0a384d0da692119f2d72d79ed07c7159879d0bb1bb32d2e443382bf3a"}, - {file = "black-23.10.1-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:33d40f5b06be80c1bbce17b173cda17994fbad096ce60eb22054da021bf933d1"}, - {file = "black-23.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:840015166dbdfbc47992871325799fd2dc0dcf9395e401ada6d88fe11498abad"}, - {file = "black-23.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:037e9b4664cafda5f025a1728c50a9e9aedb99a759c89f760bd83730e76ba884"}, - {file = "black-23.10.1-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:7cb5936e686e782fddb1c73f8aa6f459e1ad38a6a7b0e54b403f1f05a1507ee9"}, - {file = "black-23.10.1-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:7670242e90dc129c539e9ca17665e39a146a761e681805c54fbd86015c7c84f7"}, - {file = "black-23.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ed45ac9a613fb52dad3b61c8dea2ec9510bf3108d4db88422bacc7d1ba1243d"}, - {file = "black-23.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:6d23d7822140e3fef190734216cefb262521789367fbdc0b3f22af6744058982"}, - {file = "black-23.10.1-py3-none-any.whl", hash = "sha256:d431e6739f727bb2e0495df64a6c7a5310758e87505f5f8cde9ff6c0f2d7e4fe"}, - {file = "black-23.10.1.tar.gz", hash = "sha256:1f8ce316753428ff68749c65a5f7844631aa18c8679dfd3ca9dc1a289979c258"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - -[[package]] -name = "bleach" -version = "6.1.0" -description = "An easy safelist-based HTML-sanitizing tool." -optional = true -python-versions = ">=3.8" -files = [ - {file = "bleach-6.1.0-py3-none-any.whl", hash = "sha256:3225f354cfc436b9789c66c4ee030194bee0568fbf9cbdad3bc8b5c26c5f12b6"}, - {file = "bleach-6.1.0.tar.gz", hash = "sha256:0a31f1837963c41d46bbf1331b8778e1308ea0791db03cc4e7357b97cf42a8fe"}, -] - -[package.dependencies] -six = ">=1.9.0" -webencodings = "*" - -[package.extras] -css = ["tinycss2 (>=1.1.0,<1.3)"] - -[[package]] -name = "certifi" -version = "2023.7.22" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.6" -files = [ - {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, - {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, -] - -[[package]] -name = "cffi" -version = "1.16.0" -description = "Foreign Function Interface for Python calling C code." -optional = true -python-versions = ">=3.8" -files = [ - {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, - {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, - {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, - {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, - {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, - {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, - {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, - {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, - {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, - {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, - {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, - {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, - {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, - {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, - {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, -] - -[package.dependencies] -pycparser = "*" - -[[package]] -name = "cfgv" -version = "3.4.0" -description = "Validate configuration and produce human readable error messages." -optional = false -python-versions = ">=3.8" -files = [ - {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, - {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.3.1" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "charset-normalizer-3.3.1.tar.gz", hash = "sha256:d9137a876020661972ca6eec0766d81aef8a5627df628b664b234b73396e727e"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8aee051c89e13565c6bd366813c386939f8e928af93c29fda4af86d25b73d8f8"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:352a88c3df0d1fa886562384b86f9a9e27563d4704ee0e9d56ec6fcd270ea690"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:223b4d54561c01048f657fa6ce41461d5ad8ff128b9678cfe8b2ecd951e3f8a2"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f861d94c2a450b974b86093c6c027888627b8082f1299dfd5a4bae8e2292821"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1171ef1fc5ab4693c5d151ae0fdad7f7349920eabbaca6271f95969fa0756c2d"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28f512b9a33235545fbbdac6a330a510b63be278a50071a336afc1b78781b147"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0e842112fe3f1a4ffcf64b06dc4c61a88441c2f02f373367f7b4c1aa9be2ad5"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f9bc2ce123637a60ebe819f9fccc614da1bcc05798bbbaf2dd4ec91f3e08846"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f194cce575e59ffe442c10a360182a986535fd90b57f7debfaa5c845c409ecc3"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9a74041ba0bfa9bc9b9bb2cd3238a6ab3b7618e759b41bd15b5f6ad958d17605"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b578cbe580e3b41ad17b1c428f382c814b32a6ce90f2d8e39e2e635d49e498d1"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:6db3cfb9b4fcecb4390db154e75b49578c87a3b9979b40cdf90d7e4b945656e1"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:debb633f3f7856f95ad957d9b9c781f8e2c6303ef21724ec94bea2ce2fcbd056"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-win32.whl", hash = "sha256:87071618d3d8ec8b186d53cb6e66955ef2a0e4fa63ccd3709c0c90ac5a43520f"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:e372d7dfd154009142631de2d316adad3cc1c36c32a38b16a4751ba78da2a397"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ae4070f741f8d809075ef697877fd350ecf0b7c5837ed68738607ee0a2c572cf"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:58e875eb7016fd014c0eea46c6fa92b87b62c0cb31b9feae25cbbe62c919f54d"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dbd95e300367aa0827496fe75a1766d198d34385a58f97683fe6e07f89ca3e3c"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de0b4caa1c8a21394e8ce971997614a17648f94e1cd0640fbd6b4d14cab13a72"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:985c7965f62f6f32bf432e2681173db41336a9c2611693247069288bcb0c7f8b"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a15c1fe6d26e83fd2e5972425a772cca158eae58b05d4a25a4e474c221053e2d"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae55d592b02c4349525b6ed8f74c692509e5adffa842e582c0f861751701a673"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be4d9c2770044a59715eb57c1144dedea7c5d5ae80c68fb9959515037cde2008"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:851cf693fb3aaef71031237cd68699dded198657ec1e76a76eb8be58c03a5d1f"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:31bbaba7218904d2eabecf4feec0d07469284e952a27400f23b6628439439fa7"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:871d045d6ccc181fd863a3cd66ee8e395523ebfbc57f85f91f035f50cee8e3d4"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:501adc5eb6cd5f40a6f77fbd90e5ab915c8fd6e8c614af2db5561e16c600d6f3"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f5fb672c396d826ca16a022ac04c9dce74e00a1c344f6ad1a0fdc1ba1f332213"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-win32.whl", hash = "sha256:bb06098d019766ca16fc915ecaa455c1f1cd594204e7f840cd6258237b5079a8"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:8af5a8917b8af42295e86b64903156b4f110a30dca5f3b5aedea123fbd638bff"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7ae8e5142dcc7a49168f4055255dbcced01dc1714a90a21f87448dc8d90617d1"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5b70bab78accbc672f50e878a5b73ca692f45f5b5e25c8066d748c09405e6a55"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ceca5876032362ae73b83347be8b5dbd2d1faf3358deb38c9c88776779b2e2f"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34d95638ff3613849f473afc33f65c401a89f3b9528d0d213c7037c398a51296"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9edbe6a5bf8b56a4a84533ba2b2f489d0046e755c29616ef8830f9e7d9cf5728"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6a02a3c7950cafaadcd46a226ad9e12fc9744652cc69f9e5534f98b47f3bbcf"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10b8dd31e10f32410751b3430996f9807fc4d1587ca69772e2aa940a82ab571a"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edc0202099ea1d82844316604e17d2b175044f9bcb6b398aab781eba957224bd"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b891a2f68e09c5ef989007fac11476ed33c5c9994449a4e2c3386529d703dc8b"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:71ef3b9be10070360f289aea4838c784f8b851be3ba58cf796262b57775c2f14"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:55602981b2dbf8184c098bc10287e8c245e351cd4fdcad050bd7199d5a8bf514"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:46fb9970aa5eeca547d7aa0de5d4b124a288b42eaefac677bde805013c95725c"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:520b7a142d2524f999447b3a0cf95115df81c4f33003c51a6ab637cbda9d0bf4"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-win32.whl", hash = "sha256:8ec8ef42c6cd5856a7613dcd1eaf21e5573b2185263d87d27c8edcae33b62a61"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:baec8148d6b8bd5cee1ae138ba658c71f5b03e0d69d5907703e3e1df96db5e41"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63a6f59e2d01310f754c270e4a257426fe5a591dc487f1983b3bbe793cf6bac6"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d6bfc32a68bc0933819cfdfe45f9abc3cae3877e1d90aac7259d57e6e0f85b1"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f3100d86dcd03c03f7e9c3fdb23d92e32abbca07e7c13ebd7ddfbcb06f5991f"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39b70a6f88eebe239fa775190796d55a33cfb6d36b9ffdd37843f7c4c1b5dc67"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e12f8ee80aa35e746230a2af83e81bd6b52daa92a8afaef4fea4a2ce9b9f4fa"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b6cefa579e1237ce198619b76eaa148b71894fb0d6bcf9024460f9bf30fd228"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:61f1e3fb621f5420523abb71f5771a204b33c21d31e7d9d86881b2cffe92c47c"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4f6e2a839f83a6a76854d12dbebde50e4b1afa63e27761549d006fa53e9aa80e"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:1ec937546cad86d0dce5396748bf392bb7b62a9eeb8c66efac60e947697f0e58"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:82ca51ff0fc5b641a2d4e1cc8c5ff108699b7a56d7f3ad6f6da9dbb6f0145b48"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:633968254f8d421e70f91c6ebe71ed0ab140220469cf87a9857e21c16687c034"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-win32.whl", hash = "sha256:c0c72d34e7de5604df0fde3644cc079feee5e55464967d10b24b1de268deceb9"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:63accd11149c0f9a99e3bc095bbdb5a464862d77a7e309ad5938fbc8721235ae"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5a3580a4fdc4ac05f9e53c57f965e3594b2f99796231380adb2baaab96e22761"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2465aa50c9299d615d757c1c888bc6fef384b7c4aec81c05a0172b4400f98557"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cb7cd68814308aade9d0c93c5bd2ade9f9441666f8ba5aa9c2d4b389cb5e2a45"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91e43805ccafa0a91831f9cd5443aa34528c0c3f2cc48c4cb3d9a7721053874b"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:854cc74367180beb327ab9d00f964f6d91da06450b0855cbbb09187bcdb02de5"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c15070ebf11b8b7fd1bfff7217e9324963c82dbdf6182ff7050519e350e7ad9f"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c4c99f98fc3a1835af8179dcc9013f93594d0670e2fa80c83aa36346ee763d2"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fb765362688821404ad6cf86772fc54993ec11577cd5a92ac44b4c2ba52155b"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dced27917823df984fe0c80a5c4ad75cf58df0fbfae890bc08004cd3888922a2"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a66bcdf19c1a523e41b8e9d53d0cedbfbac2e93c649a2e9502cb26c014d0980c"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ecd26be9f112c4f96718290c10f4caea6cc798459a3a76636b817a0ed7874e42"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:3f70fd716855cd3b855316b226a1ac8bdb3caf4f7ea96edcccc6f484217c9597"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:17a866d61259c7de1bdadef418a37755050ddb4b922df8b356503234fff7932c"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-win32.whl", hash = "sha256:548eefad783ed787b38cb6f9a574bd8664468cc76d1538215d510a3cd41406cb"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:45f053a0ece92c734d874861ffe6e3cc92150e32136dd59ab1fb070575189c97"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bc791ec3fd0c4309a753f95bb6c749ef0d8ea3aea91f07ee1cf06b7b02118f2f"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0c8c61fb505c7dad1d251c284e712d4e0372cef3b067f7ddf82a7fa82e1e9a93"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2c092be3885a1b7899cd85ce24acedc1034199d6fca1483fa2c3a35c86e43041"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2000c54c395d9e5e44c99dc7c20a64dc371f777faf8bae4919ad3e99ce5253e"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4cb50a0335382aac15c31b61d8531bc9bb657cfd848b1d7158009472189f3d62"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c30187840d36d0ba2893bc3271a36a517a717f9fd383a98e2697ee890a37c273"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe81b35c33772e56f4b6cf62cf4aedc1762ef7162a31e6ac7fe5e40d0149eb67"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0bf89afcbcf4d1bb2652f6580e5e55a840fdf87384f6063c4a4f0c95e378656"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:06cf46bdff72f58645434d467bf5228080801298fbba19fe268a01b4534467f5"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:3c66df3f41abee950d6638adc7eac4730a306b022570f71dd0bd6ba53503ab57"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd805513198304026bd379d1d516afbf6c3c13f4382134a2c526b8b854da1c2e"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:9505dc359edb6a330efcd2be825fdb73ee3e628d9010597aa1aee5aa63442e97"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:31445f38053476a0c4e6d12b047b08ced81e2c7c712e5a1ad97bc913256f91b2"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-win32.whl", hash = "sha256:bd28b31730f0e982ace8663d108e01199098432a30a4c410d06fe08fdb9e93f4"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:555fe186da0068d3354cdf4bbcbc609b0ecae4d04c921cc13e209eece7720727"}, - {file = "charset_normalizer-3.3.1-py3-none-any.whl", hash = "sha256:800561453acdecedaac137bf09cd719c7a440b6800ec182f077bb8e7025fb708"}, -] - -[[package]] -name = "click" -version = "8.1.7" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.7" -files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "comm" -version = "0.1.4" -description = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc." -optional = true -python-versions = ">=3.6" -files = [ - {file = "comm-0.1.4-py3-none-any.whl", hash = "sha256:6d52794cba11b36ed9860999cd10fd02d6b2eac177068fdd585e1e2f8a96e67a"}, - {file = "comm-0.1.4.tar.gz", hash = "sha256:354e40a59c9dd6db50c5cc6b4acc887d82e9603787f83b68c01a80a923984d15"}, -] - -[package.dependencies] -traitlets = ">=4" - -[package.extras] -lint = ["black (>=22.6.0)", "mdformat (>0.7)", "mdformat-gfm (>=0.3.5)", "ruff (>=0.0.156)"] -test = ["pytest"] -typing = ["mypy (>=0.990)"] - -[[package]] -name = "debugpy" -version = "1.8.0" -description = "An implementation of the Debug Adapter Protocol for Python" -optional = true -python-versions = ">=3.8" -files = [ - {file = "debugpy-1.8.0-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:7fb95ca78f7ac43393cd0e0f2b6deda438ec7c5e47fa5d38553340897d2fbdfb"}, - {file = "debugpy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef9ab7df0b9a42ed9c878afd3eaaff471fce3fa73df96022e1f5c9f8f8c87ada"}, - {file = "debugpy-1.8.0-cp310-cp310-win32.whl", hash = "sha256:a8b7a2fd27cd9f3553ac112f356ad4ca93338feadd8910277aff71ab24d8775f"}, - {file = "debugpy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:5d9de202f5d42e62f932507ee8b21e30d49aae7e46d5b1dd5c908db1d7068637"}, - {file = "debugpy-1.8.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:ef54404365fae8d45cf450d0544ee40cefbcb9cb85ea7afe89a963c27028261e"}, - {file = "debugpy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60009b132c91951354f54363f8ebdf7457aeb150e84abba5ae251b8e9f29a8a6"}, - {file = "debugpy-1.8.0-cp311-cp311-win32.whl", hash = "sha256:8cd0197141eb9e8a4566794550cfdcdb8b3db0818bdf8c49a8e8f8053e56e38b"}, - {file = "debugpy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:a64093656c4c64dc6a438e11d59369875d200bd5abb8f9b26c1f5f723622e153"}, - {file = "debugpy-1.8.0-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:b05a6b503ed520ad58c8dc682749113d2fd9f41ffd45daec16e558ca884008cd"}, - {file = "debugpy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c6fb41c98ec51dd010d7ed650accfd07a87fe5e93eca9d5f584d0578f28f35f"}, - {file = "debugpy-1.8.0-cp38-cp38-win32.whl", hash = "sha256:46ab6780159eeabb43c1495d9c84cf85d62975e48b6ec21ee10c95767c0590aa"}, - {file = "debugpy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:bdc5ef99d14b9c0fcb35351b4fbfc06ac0ee576aeab6b2511702e5a648a2e595"}, - {file = "debugpy-1.8.0-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:61eab4a4c8b6125d41a34bad4e5fe3d2cc145caecd63c3fe953be4cc53e65bf8"}, - {file = "debugpy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:125b9a637e013f9faac0a3d6a82bd17c8b5d2c875fb6b7e2772c5aba6d082332"}, - {file = "debugpy-1.8.0-cp39-cp39-win32.whl", hash = "sha256:57161629133113c97b387382045649a2b985a348f0c9366e22217c87b68b73c6"}, - {file = "debugpy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:e3412f9faa9ade82aa64a50b602544efcba848c91384e9f93497a458767e6926"}, - {file = "debugpy-1.8.0-py2.py3-none-any.whl", hash = "sha256:9c9b0ac1ce2a42888199df1a1906e45e6f3c9555497643a85e0bf2406e3ffbc4"}, - {file = "debugpy-1.8.0.zip", hash = "sha256:12af2c55b419521e33d5fb21bd022df0b5eb267c3e178f1d374a63a2a6bdccd0"}, -] - -[[package]] -name = "decorator" -version = "5.1.1" -description = "Decorators for Humans" -optional = true -python-versions = ">=3.5" -files = [ - {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, - {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, -] - -[[package]] -name = "defusedxml" -version = "0.7.1" -description = "XML bomb protection for Python stdlib modules" -optional = true -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, - {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, -] - -[[package]] -name = "distlib" -version = "0.3.7" -description = "Distribution utilities" -optional = false -python-versions = "*" -files = [ - {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, - {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, -] - -[[package]] -name = "entrypoints" -version = "0.4" -description = "Discover and load entry points from installed packages." -optional = true -python-versions = ">=3.6" -files = [ - {file = "entrypoints-0.4-py3-none-any.whl", hash = "sha256:f174b5ff827504fd3cd97cc3f8649f3693f51538c7e4bdf3ef002c8429d42f9f"}, - {file = "entrypoints-0.4.tar.gz", hash = "sha256:b706eddaa9218a19ebcd67b56818f05bb27589b1ca9e8d797b74affad4ccacd4"}, -] - -[[package]] -name = "essentials" -version = "1.1.5" -description = "General purpose classes and functions, reusable in any kind of Python application" -optional = false -python-versions = "*" -files = [ - {file = "essentials-1.1.5-py3-none-any.whl", hash = "sha256:905fa4a69fcd2b2cf41ecc6cc65827e30c87ef91f3f5c71540bcc5e984fa8360"}, - {file = "essentials-1.1.5.tar.gz", hash = "sha256:8736f738bb2c51d5069b2de2cf9146f7d402f25f9f95636781e59a422c908c46"}, -] - -[[package]] -name = "essentials-openapi" -version = "1.0.8" -description = "Classes to generate OpenAPI Documentation v3 and v2, in JSON and YAML." -optional = false -python-versions = ">=3.7" -files = [ - {file = "essentials_openapi-1.0.8-py3-none-any.whl", hash = "sha256:1d3afc67cefc952aade7d406c2ffac81634e54ce7e597ddd8ceb1bfe1fe62e46"}, - {file = "essentials_openapi-1.0.8.tar.gz", hash = "sha256:37832b7b422939bdcf9cc6f52d2317e0ee19907ac8aba2c77a8cff49a2c0b26a"}, -] - -[package.dependencies] -essentials = ">=1.1.5" -pyyaml = ">=6" - -[package.extras] -full = ["click (>=8.1.3,<8.2.0)", "httpx (<1)", "jinja2 (>=3.1.2,<3.2.0)", "markupsafe (==2.1.2)", "rich (>=12.6.0,<12.7.0)"] - -[[package]] -name = "exceptiongroup" -version = "1.1.3" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -files = [ - {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, - {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, -] - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "executing" -version = "2.0.0" -description = "Get the currently executing AST node of a frame, and other information" -optional = true -python-versions = "*" -files = [ - {file = "executing-2.0.0-py2.py3-none-any.whl", hash = "sha256:06df6183df67389625f4e763921c6cf978944721abf3e714000200aab95b0657"}, - {file = "executing-2.0.0.tar.gz", hash = "sha256:0ff053696fdeef426cda5bd18eacd94f82c91f49823a2e9090124212ceea9b08"}, -] - -[package.extras] -tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] - -[[package]] -name = "fastjsonschema" -version = "2.18.1" -description = "Fastest Python implementation of JSON schema" -optional = true -python-versions = "*" -files = [ - {file = "fastjsonschema-2.18.1-py3-none-any.whl", hash = "sha256:aec6a19e9f66e9810ab371cc913ad5f4e9e479b63a7072a2cd060a9369e329a8"}, - {file = "fastjsonschema-2.18.1.tar.gz", hash = "sha256:06dc8680d937628e993fa0cd278f196d20449a1adc087640710846b324d422ea"}, -] - -[package.extras] -devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"] - -[[package]] -name = "filelock" -version = "3.12.4" -description = "A platform independent file lock." -optional = false -python-versions = ">=3.8" -files = [ - {file = "filelock-3.12.4-py3-none-any.whl", hash = "sha256:08c21d87ded6e2b9da6728c3dff51baf1dcecf973b768ef35bcbc3447edb9ad4"}, - {file = "filelock-3.12.4.tar.gz", hash = "sha256:2e6f249f1f3654291606e046b09f1fd5eac39b360664c27f5aad072012f8bcbd"}, -] - -[package.extras] -docs = ["furo (>=2023.7.26)", "sphinx (>=7.1.2)", "sphinx-autodoc-typehints (>=1.24)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3)", "diff-cover (>=7.7)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-timeout (>=2.1)"] -typing = ["typing-extensions (>=4.7.1)"] - -[[package]] -name = "flake8" -version = "6.1.0" -description = "the modular source code checker: pep8 pyflakes and co" -optional = false -python-versions = ">=3.8.1" -files = [ - {file = "flake8-6.1.0-py2.py3-none-any.whl", hash = "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5"}, - {file = "flake8-6.1.0.tar.gz", hash = "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23"}, -] - -[package.dependencies] -mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.11.0,<2.12.0" -pyflakes = ">=3.1.0,<3.2.0" - -[[package]] -name = "fqdn" -version = "1.5.1" -description = "Validates fully-qualified domain names against RFC 1123, so that they are acceptable to modern bowsers" -optional = true -python-versions = ">=2.7, !=3.0, !=3.1, !=3.2, !=3.3, !=3.4, <4" -files = [ - {file = "fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014"}, - {file = "fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f"}, -] - -[[package]] -name = "frozenlist" -version = "1.4.0" -description = "A list-like structure which implements collections.abc.MutableSequence" -optional = false -python-versions = ">=3.8" -files = [ - {file = "frozenlist-1.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:764226ceef3125e53ea2cb275000e309c0aa5464d43bd72abd661e27fffc26ab"}, - {file = "frozenlist-1.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d6484756b12f40003c6128bfcc3fa9f0d49a687e171186c2d85ec82e3758c559"}, - {file = "frozenlist-1.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9ac08e601308e41eb533f232dbf6b7e4cea762f9f84f6357136eed926c15d12c"}, - {file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d081f13b095d74b67d550de04df1c756831f3b83dc9881c38985834387487f1b"}, - {file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:71932b597f9895f011f47f17d6428252fc728ba2ae6024e13c3398a087c2cdea"}, - {file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:981b9ab5a0a3178ff413bca62526bb784249421c24ad7381e39d67981be2c326"}, - {file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e41f3de4df3e80de75845d3e743b3f1c4c8613c3997a912dbf0229fc61a8b963"}, - {file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6918d49b1f90821e93069682c06ffde41829c346c66b721e65a5c62b4bab0300"}, - {file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0e5c8764c7829343d919cc2dfc587a8db01c4f70a4ebbc49abde5d4b158b007b"}, - {file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8d0edd6b1c7fb94922bf569c9b092ee187a83f03fb1a63076e7774b60f9481a8"}, - {file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e29cda763f752553fa14c68fb2195150bfab22b352572cb36c43c47bedba70eb"}, - {file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:0c7c1b47859ee2cac3846fde1c1dc0f15da6cec5a0e5c72d101e0f83dcb67ff9"}, - {file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:901289d524fdd571be1c7be054f48b1f88ce8dddcbdf1ec698b27d4b8b9e5d62"}, - {file = "frozenlist-1.4.0-cp310-cp310-win32.whl", hash = "sha256:1a0848b52815006ea6596c395f87449f693dc419061cc21e970f139d466dc0a0"}, - {file = "frozenlist-1.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:b206646d176a007466358aa21d85cd8600a415c67c9bd15403336c331a10d956"}, - {file = "frozenlist-1.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:de343e75f40e972bae1ef6090267f8260c1446a1695e77096db6cfa25e759a95"}, - {file = "frozenlist-1.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad2a9eb6d9839ae241701d0918f54c51365a51407fd80f6b8289e2dfca977cc3"}, - {file = "frozenlist-1.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bd7bd3b3830247580de99c99ea2a01416dfc3c34471ca1298bccabf86d0ff4dc"}, - {file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdf1847068c362f16b353163391210269e4f0569a3c166bc6a9f74ccbfc7e839"}, - {file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38461d02d66de17455072c9ba981d35f1d2a73024bee7790ac2f9e361ef1cd0c"}, - {file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5a32087d720c608f42caed0ef36d2b3ea61a9d09ee59a5142d6070da9041b8f"}, - {file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd65632acaf0d47608190a71bfe46b209719bf2beb59507db08ccdbe712f969b"}, - {file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261b9f5d17cac914531331ff1b1d452125bf5daa05faf73b71d935485b0c510b"}, - {file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b89ac9768b82205936771f8d2eb3ce88503b1556324c9f903e7156669f521472"}, - {file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:008eb8b31b3ea6896da16c38c1b136cb9fec9e249e77f6211d479db79a4eaf01"}, - {file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e74b0506fa5aa5598ac6a975a12aa8928cbb58e1f5ac8360792ef15de1aa848f"}, - {file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:490132667476f6781b4c9458298b0c1cddf237488abd228b0b3650e5ecba7467"}, - {file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:76d4711f6f6d08551a7e9ef28c722f4a50dd0fc204c56b4bcd95c6cc05ce6fbb"}, - {file = "frozenlist-1.4.0-cp311-cp311-win32.whl", hash = "sha256:a02eb8ab2b8f200179b5f62b59757685ae9987996ae549ccf30f983f40602431"}, - {file = "frozenlist-1.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:515e1abc578dd3b275d6a5114030b1330ba044ffba03f94091842852f806f1c1"}, - {file = "frozenlist-1.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:f0ed05f5079c708fe74bf9027e95125334b6978bf07fd5ab923e9e55e5fbb9d3"}, - {file = "frozenlist-1.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ca265542ca427bf97aed183c1676e2a9c66942e822b14dc6e5f42e038f92a503"}, - {file = "frozenlist-1.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:491e014f5c43656da08958808588cc6c016847b4360e327a62cb308c791bd2d9"}, - {file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17ae5cd0f333f94f2e03aaf140bb762c64783935cc764ff9c82dff626089bebf"}, - {file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e78fb68cf9c1a6aa4a9a12e960a5c9dfbdb89b3695197aa7064705662515de2"}, - {file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5655a942f5f5d2c9ed93d72148226d75369b4f6952680211972a33e59b1dfdc"}, - {file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c11b0746f5d946fecf750428a95f3e9ebe792c1ee3b1e96eeba145dc631a9672"}, - {file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e66d2a64d44d50d2543405fb183a21f76b3b5fd16f130f5c99187c3fb4e64919"}, - {file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:88f7bc0fcca81f985f78dd0fa68d2c75abf8272b1f5c323ea4a01a4d7a614efc"}, - {file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5833593c25ac59ede40ed4de6d67eb42928cca97f26feea219f21d0ed0959b79"}, - {file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:fec520865f42e5c7f050c2a79038897b1c7d1595e907a9e08e3353293ffc948e"}, - {file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:b826d97e4276750beca7c8f0f1a4938892697a6bcd8ec8217b3312dad6982781"}, - {file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ceb6ec0a10c65540421e20ebd29083c50e6d1143278746a4ef6bcf6153171eb8"}, - {file = "frozenlist-1.4.0-cp38-cp38-win32.whl", hash = "sha256:2b8bcf994563466db019fab287ff390fffbfdb4f905fc77bc1c1d604b1c689cc"}, - {file = "frozenlist-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:a6c8097e01886188e5be3e6b14e94ab365f384736aa1fca6a0b9e35bd4a30bc7"}, - {file = "frozenlist-1.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6c38721585f285203e4b4132a352eb3daa19121a035f3182e08e437cface44bf"}, - {file = "frozenlist-1.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a0c6da9aee33ff0b1a451e867da0c1f47408112b3391dd43133838339e410963"}, - {file = "frozenlist-1.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:93ea75c050c5bb3d98016b4ba2497851eadf0ac154d88a67d7a6816206f6fa7f"}, - {file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f61e2dc5ad442c52b4887f1fdc112f97caeff4d9e6ebe78879364ac59f1663e1"}, - {file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa384489fefeb62321b238e64c07ef48398fe80f9e1e6afeff22e140e0850eef"}, - {file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:10ff5faaa22786315ef57097a279b833ecab1a0bfb07d604c9cbb1c4cdc2ed87"}, - {file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:007df07a6e3eb3e33e9a1fe6a9db7af152bbd8a185f9aaa6ece10a3529e3e1c6"}, - {file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f4f399d28478d1f604c2ff9119907af9726aed73680e5ed1ca634d377abb087"}, - {file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c5374b80521d3d3f2ec5572e05adc94601985cc526fb276d0c8574a6d749f1b3"}, - {file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ce31ae3e19f3c902de379cf1323d90c649425b86de7bbdf82871b8a2a0615f3d"}, - {file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7211ef110a9194b6042449431e08c4d80c0481e5891e58d429df5899690511c2"}, - {file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:556de4430ce324c836789fa4560ca62d1591d2538b8ceb0b4f68fb7b2384a27a"}, - {file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7645a8e814a3ee34a89c4a372011dcd817964ce8cb273c8ed6119d706e9613e3"}, - {file = "frozenlist-1.4.0-cp39-cp39-win32.whl", hash = "sha256:19488c57c12d4e8095a922f328df3f179c820c212940a498623ed39160bc3c2f"}, - {file = "frozenlist-1.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:6221d84d463fb110bdd7619b69cb43878a11d51cbb9394ae3105d082d5199167"}, - {file = "frozenlist-1.4.0.tar.gz", hash = "sha256:09163bdf0b2907454042edb19f887c6d33806adc71fbd54afc14908bfdc22251"}, -] - -[[package]] -name = "ghp-import" -version = "2.1.0" -description = "Copy your docs directly to the gh-pages branch." -optional = false -python-versions = "*" -files = [ - {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, - {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, -] - -[package.dependencies] -python-dateutil = ">=2.8.1" - -[package.extras] -dev = ["flake8", "markdown", "twine", "wheel"] - -[[package]] -name = "h11" -version = "0.14.0" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -optional = false -python-versions = ">=3.7" -files = [ - {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, -] - -[[package]] -name = "httpcore" -version = "0.18.0" -description = "A minimal low-level HTTP client." -optional = false -python-versions = ">=3.8" -files = [ - {file = "httpcore-0.18.0-py3-none-any.whl", hash = "sha256:adc5398ee0a476567bf87467063ee63584a8bce86078bf748e48754f60202ced"}, - {file = "httpcore-0.18.0.tar.gz", hash = "sha256:13b5e5cd1dca1a6636a6aaea212b19f4f85cd88c366a2b82304181b769aab3c9"}, -] - -[package.dependencies] -anyio = ">=3.0,<5.0" -certifi = "*" -h11 = ">=0.13,<0.15" -sniffio = "==1.*" - -[package.extras] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] - -[[package]] -name = "httpx" -version = "0.25.0" -description = "The next generation HTTP client." -optional = false -python-versions = ">=3.8" -files = [ - {file = "httpx-0.25.0-py3-none-any.whl", hash = "sha256:181ea7f8ba3a82578be86ef4171554dd45fec26a02556a744db029a0a27b7100"}, - {file = "httpx-0.25.0.tar.gz", hash = "sha256:47ecda285389cb32bb2691cc6e069e3ab0205956f681c5b2ad2325719751d875"}, -] - -[package.dependencies] -certifi = "*" -httpcore = ">=0.18.0,<0.19.0" -idna = "*" -sniffio = "*" - -[package.extras] -brotli = ["brotli", "brotlicffi"] -cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] - -[[package]] -name = "identify" -version = "2.5.30" -description = "File identification library for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "identify-2.5.30-py2.py3-none-any.whl", hash = "sha256:afe67f26ae29bab007ec21b03d4114f41316ab9dd15aa8736a167481e108da54"}, - {file = "identify-2.5.30.tar.gz", hash = "sha256:f302a4256a15c849b91cfcdcec052a8ce914634b2f77ae87dad29cd749f2d88d"}, -] - -[package.extras] -license = ["ukkonen"] - -[[package]] -name = "idna" -version = "3.4" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.5" -files = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, -] - -[[package]] -name = "importlib-metadata" -version = "6.8.0" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "importlib_metadata-6.8.0-py3-none-any.whl", hash = "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb"}, - {file = "importlib_metadata-6.8.0.tar.gz", hash = "sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743"}, -] - -[package.dependencies] -zipp = ">=0.5" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] - -[[package]] -name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.7" -files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] - -[[package]] -name = "ipykernel" -version = "6.26.0" -description = "IPython Kernel for Jupyter" -optional = true -python-versions = ">=3.8" -files = [ - {file = "ipykernel-6.26.0-py3-none-any.whl", hash = "sha256:3ba3dc97424b87b31bb46586b5167b3161b32d7820b9201a9e698c71e271602c"}, - {file = "ipykernel-6.26.0.tar.gz", hash = "sha256:553856658eb8430bbe9653ea041a41bff63e9606fc4628873fc92a6cf3abd404"}, -] - -[package.dependencies] -appnope = {version = "*", markers = "platform_system == \"Darwin\""} -comm = ">=0.1.1" -debugpy = ">=1.6.5" -ipython = ">=7.23.1" -jupyter-client = ">=6.1.12" -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" -matplotlib-inline = ">=0.1" -nest-asyncio = "*" -packaging = "*" -psutil = "*" -pyzmq = ">=20" -tornado = ">=6.1" -traitlets = ">=5.4.0" - -[package.extras] -cov = ["coverage[toml]", "curio", "matplotlib", "pytest-cov", "trio"] -docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "trio"] -pyqt5 = ["pyqt5"] -pyside6 = ["pyside6"] -test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio", "pytest-cov", "pytest-timeout"] - -[[package]] -name = "ipython" -version = "8.16.1" -description = "IPython: Productive Interactive Computing" -optional = true -python-versions = ">=3.9" -files = [ - {file = "ipython-8.16.1-py3-none-any.whl", hash = "sha256:0852469d4d579d9cd613c220af7bf0c9cc251813e12be647cb9d463939db9b1e"}, - {file = "ipython-8.16.1.tar.gz", hash = "sha256:ad52f58fca8f9f848e256c629eff888efc0528c12fe0f8ec14f33205f23ef938"}, -] - -[package.dependencies] -appnope = {version = "*", markers = "sys_platform == \"darwin\""} -backcall = "*" -colorama = {version = "*", markers = "sys_platform == \"win32\""} -decorator = "*" -exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} -jedi = ">=0.16" -matplotlib-inline = "*" -pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} -pickleshare = "*" -prompt-toolkit = ">=3.0.30,<3.0.37 || >3.0.37,<3.1.0" -pygments = ">=2.4.0" -stack-data = "*" -traitlets = ">=5" -typing-extensions = {version = "*", markers = "python_version < \"3.10\""} - -[package.extras] -all = ["black", "curio", "docrepr", "exceptiongroup", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.21)", "pandas", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] -black = ["black"] -doc = ["docrepr", "exceptiongroup", "ipykernel", "matplotlib", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] -kernel = ["ipykernel"] -nbconvert = ["nbconvert"] -nbformat = ["nbformat"] -notebook = ["ipywidgets", "notebook"] -parallel = ["ipyparallel"] -qtconsole = ["qtconsole"] -test = ["pytest (<7.1)", "pytest-asyncio", "testpath"] -test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.21)", "pandas", "pytest (<7.1)", "pytest-asyncio", "testpath", "trio"] - -[[package]] -name = "ipython-genutils" -version = "0.2.0" -description = "Vestigial utilities from IPython" -optional = true -python-versions = "*" -files = [ - {file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"}, - {file = "ipython_genutils-0.2.0.tar.gz", hash = "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"}, -] - -[[package]] -name = "isoduration" -version = "20.11.0" -description = "Operations with ISO 8601 durations" -optional = true -python-versions = ">=3.7" -files = [ - {file = "isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042"}, - {file = "isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9"}, -] - -[package.dependencies] -arrow = ">=0.15.0" - -[[package]] -name = "isort" -version = "5.12.0" -description = "A Python utility / library to sort Python imports." -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, - {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, -] - -[package.extras] -colors = ["colorama (>=0.4.3)"] -pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] -plugins = ["setuptools"] -requirements-deprecated-finder = ["pip-api", "pipreqs"] - -[[package]] -name = "jedi" -version = "0.19.1" -description = "An autocompletion tool for Python that can be used for text editors." -optional = true -python-versions = ">=3.6" -files = [ - {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"}, - {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"}, -] - -[package.dependencies] -parso = ">=0.8.3,<0.9.0" - -[package.extras] -docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] -qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] -testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] - -[[package]] -name = "jinja2" -version = "3.1.2" -description = "A very fast and expressive template engine." -optional = false -python-versions = ">=3.7" -files = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, -] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "jsonpointer" -version = "2.4" -description = "Identify specific nodes in a JSON document (RFC 6901)" -optional = true -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" -files = [ - {file = "jsonpointer-2.4-py2.py3-none-any.whl", hash = "sha256:15d51bba20eea3165644553647711d150376234112651b4f1811022aecad7d7a"}, - {file = "jsonpointer-2.4.tar.gz", hash = "sha256:585cee82b70211fa9e6043b7bb89db6e1aa49524340dde8ad6b63206ea689d88"}, -] - -[[package]] -name = "jsonschema" -version = "4.19.1" -description = "An implementation of JSON Schema validation for Python" -optional = true -python-versions = ">=3.8" -files = [ - {file = "jsonschema-4.19.1-py3-none-any.whl", hash = "sha256:cd5f1f9ed9444e554b38ba003af06c0a8c2868131e56bfbef0550fb450c0330e"}, - {file = "jsonschema-4.19.1.tar.gz", hash = "sha256:ec84cc37cfa703ef7cd4928db24f9cb31428a5d0fa77747b8b51a847458e0bbf"}, -] - -[package.dependencies] -attrs = ">=22.2.0" -fqdn = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} -idna = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} -isoduration = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} -jsonpointer = {version = ">1.13", optional = true, markers = "extra == \"format-nongpl\""} -jsonschema-specifications = ">=2023.03.6" -referencing = ">=0.28.4" -rfc3339-validator = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} -rfc3986-validator = {version = ">0.1.0", optional = true, markers = "extra == \"format-nongpl\""} -rpds-py = ">=0.7.1" -uri-template = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} -webcolors = {version = ">=1.11", optional = true, markers = "extra == \"format-nongpl\""} - -[package.extras] -format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] -format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] - -[[package]] -name = "jsonschema-specifications" -version = "2023.7.1" -description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" -optional = true -python-versions = ">=3.8" -files = [ - {file = "jsonschema_specifications-2023.7.1-py3-none-any.whl", hash = "sha256:05adf340b659828a004220a9613be00fa3f223f2b82002e273dee62fd50524b1"}, - {file = "jsonschema_specifications-2023.7.1.tar.gz", hash = "sha256:c91a50404e88a1f6ba40636778e2ee08f6e24c5613fe4c53ac24578a5a7f72bb"}, -] - -[package.dependencies] -referencing = ">=0.28.0" - -[[package]] -name = "jupyter-client" -version = "7.4.9" -description = "Jupyter protocol implementation and client libraries" -optional = true -python-versions = ">=3.7" -files = [ - {file = "jupyter_client-7.4.9-py3-none-any.whl", hash = "sha256:214668aaea208195f4c13d28eb272ba79f945fc0cf3f11c7092c20b2ca1980e7"}, - {file = "jupyter_client-7.4.9.tar.gz", hash = "sha256:52be28e04171f07aed8f20e1616a5a552ab9fee9cbbe6c1896ae170c3880d392"}, -] - -[package.dependencies] -entrypoints = "*" -jupyter-core = ">=4.9.2" -nest-asyncio = ">=1.5.4" -python-dateutil = ">=2.8.2" -pyzmq = ">=23.0" -tornado = ">=6.2" -traitlets = "*" - -[package.extras] -doc = ["ipykernel", "myst-parser", "sphinx (>=1.3.6)", "sphinx-rtd-theme", "sphinxcontrib-github-alt"] -test = ["codecov", "coverage", "ipykernel (>=6.12)", "ipython", "mypy", "pre-commit", "pytest", "pytest-asyncio (>=0.18)", "pytest-cov", "pytest-timeout"] - -[[package]] -name = "jupyter-core" -version = "5.4.0" -description = "Jupyter core package. A base package on which Jupyter projects rely." -optional = true -python-versions = ">=3.8" -files = [ - {file = "jupyter_core-5.4.0-py3-none-any.whl", hash = "sha256:66e252f675ac04dcf2feb6ed4afb3cd7f68cf92f483607522dc251f32d471571"}, - {file = "jupyter_core-5.4.0.tar.gz", hash = "sha256:e4b98344bb94ee2e3e6c4519a97d001656009f9cb2b7f2baf15b3c205770011d"}, -] - -[package.dependencies] -platformdirs = ">=2.5" -pywin32 = {version = ">=300", markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\""} -traitlets = ">=5.3" - -[package.extras] -docs = ["myst-parser", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "traitlets"] -test = ["ipykernel", "pre-commit", "pytest", "pytest-cov", "pytest-timeout"] - -[[package]] -name = "jupyter-events" -version = "0.8.0" -description = "Jupyter Event System library" -optional = true -python-versions = ">=3.8" -files = [ - {file = "jupyter_events-0.8.0-py3-none-any.whl", hash = "sha256:81f07375c7673ff298bfb9302b4a981864ec64edaed75ca0fe6f850b9b045525"}, - {file = "jupyter_events-0.8.0.tar.gz", hash = "sha256:fda08f0defce5e16930542ce60634ba48e010830d50073c3dfd235759cee77bf"}, -] - -[package.dependencies] -jsonschema = {version = ">=4.18.0", extras = ["format-nongpl"]} -python-json-logger = ">=2.0.4" -pyyaml = ">=5.3" -referencing = "*" -rfc3339-validator = "*" -rfc3986-validator = ">=0.1.1" -traitlets = ">=5.3" - -[package.extras] -cli = ["click", "rich"] -docs = ["jupyterlite-sphinx", "myst-parser", "pydata-sphinx-theme", "sphinxcontrib-spelling"] -test = ["click", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (>=0.19.0)", "pytest-console-scripts", "rich"] - -[[package]] -name = "jupyter-kernel-gateway" -version = "2.5.2" -description = "A web server for spawning and communicating with Jupyter kernels" -optional = true -python-versions = ">=3.7" -files = [ - {file = "jupyter_kernel_gateway-2.5.2-py3-none-any.whl", hash = "sha256:0a9d1a9cd70f1c59fd2a1e5e843d11f81b05c0e05cc08c01e28416c67472e3db"}, - {file = "jupyter_kernel_gateway-2.5.2.tar.gz", hash = "sha256:3d93f05a06e7fb2112255ff45596cce48963b8eaf309efa2e79a6ad0d96a7f06"}, -] - -[package.dependencies] -jupyter-client = ">=5.2.0,<8.0" -jupyter-core = ">=4.4.0" -notebook = ">=5.7.6,<7.0" -requests = ">=2.7,<3.0" -tornado = ">=4.2.0" -traitlets = ">=4.2.0" - -[[package]] -name = "jupyter-server" -version = "2.9.1" -description = "The backend—i.e. core services, APIs, and REST endpoints—to Jupyter web applications." -optional = true -python-versions = ">=3.8" -files = [ - {file = "jupyter_server-2.9.1-py3-none-any.whl", hash = "sha256:21ad1a3d455d5a79ce4bef5201925cd17510c17898cf9d54e3ccfb6b12734948"}, - {file = "jupyter_server-2.9.1.tar.gz", hash = "sha256:9ba71be4b9c16e479e4c50c929f8ac4b1015baf90237a08681397a98c76c7e5e"}, -] - -[package.dependencies] -anyio = ">=3.1.0" -argon2-cffi = "*" -jinja2 = "*" -jupyter-client = ">=7.4.4" -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" -jupyter-events = ">=0.6.0" -jupyter-server-terminals = "*" -nbconvert = ">=6.4.4" -nbformat = ">=5.3.0" -overrides = "*" -packaging = "*" -prometheus-client = "*" -pywinpty = {version = "*", markers = "os_name == \"nt\""} -pyzmq = ">=24" -send2trash = ">=1.8.2" -terminado = ">=0.8.3" -tornado = ">=6.2.0" -traitlets = ">=5.6.0" -websocket-client = "*" - -[package.extras] -docs = ["ipykernel", "jinja2", "jupyter-client", "jupyter-server", "myst-parser", "nbformat", "prometheus-client", "pydata-sphinx-theme", "send2trash", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-openapi (>=0.8.0)", "sphinxcontrib-spelling", "sphinxemoji", "tornado", "typing-extensions"] -test = ["flaky", "ipykernel", "pre-commit", "pytest (>=7.0)", "pytest-console-scripts", "pytest-jupyter[server] (>=0.4)", "pytest-timeout", "requests"] - -[[package]] -name = "jupyter-server-terminals" -version = "0.4.4" -description = "A Jupyter Server Extension Providing Terminals." -optional = true -python-versions = ">=3.8" -files = [ - {file = "jupyter_server_terminals-0.4.4-py3-none-any.whl", hash = "sha256:75779164661cec02a8758a5311e18bb8eb70c4e86c6b699403100f1585a12a36"}, - {file = "jupyter_server_terminals-0.4.4.tar.gz", hash = "sha256:57ab779797c25a7ba68e97bcfb5d7740f2b5e8a83b5e8102b10438041a7eac5d"}, -] - -[package.dependencies] -pywinpty = {version = ">=2.0.3", markers = "os_name == \"nt\""} -terminado = ">=0.8.3" - -[package.extras] -docs = ["jinja2", "jupyter-server", "mistune (<3.0)", "myst-parser", "nbformat", "packaging", "pydata-sphinx-theme", "sphinxcontrib-github-alt", "sphinxcontrib-openapi", "sphinxcontrib-spelling", "sphinxemoji", "tornado"] -test = ["coverage", "jupyter-server (>=2.0.0)", "pytest (>=7.0)", "pytest-cov", "pytest-jupyter[server] (>=0.5.3)", "pytest-timeout"] - -[[package]] -name = "jupyterlab-pygments" -version = "0.2.2" -description = "Pygments theme using JupyterLab CSS variables" -optional = true -python-versions = ">=3.7" -files = [ - {file = "jupyterlab_pygments-0.2.2-py2.py3-none-any.whl", hash = "sha256:2405800db07c9f770863bcf8049a529c3dd4d3e28536638bd7c1c01d2748309f"}, - {file = "jupyterlab_pygments-0.2.2.tar.gz", hash = "sha256:7405d7fde60819d905a9fa8ce89e4cd830e318cdad22a0030f7a901da705585d"}, -] - -[[package]] -name = "markdown" -version = "3.5" -description = "Python implementation of John Gruber's Markdown." -optional = false -python-versions = ">=3.8" -files = [ - {file = "Markdown-3.5-py3-none-any.whl", hash = "sha256:4afb124395ce5fc34e6d9886dab977fd9ae987fc6e85689f08278cf0c69d4bf3"}, - {file = "Markdown-3.5.tar.gz", hash = "sha256:a807eb2e4778d9156c8f07876c6e4d50b5494c5665c4834f67b06459dfd877b3"}, -] - -[package.dependencies] -importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} - -[package.extras] -docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] -testing = ["coverage", "pyyaml"] - -[[package]] -name = "markdown-it-py" -version = "3.0.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -optional = false -python-versions = ">=3.8" -files = [ - {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, - {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, -] - -[package.dependencies] -mdurl = ">=0.1,<1.0" - -[package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark"] -code-style = ["pre-commit (>=3.0,<4.0)"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] -linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins"] -profiling = ["gprof2dot"] -rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - -[[package]] -name = "markupsafe" -version = "2.1.3" -description = "Safely add untrusted strings to HTML/XML markup." -optional = false -python-versions = ">=3.7" -files = [ - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, - {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, -] - -[[package]] -name = "matplotlib-inline" -version = "0.1.6" -description = "Inline Matplotlib backend for Jupyter" -optional = true -python-versions = ">=3.5" -files = [ - {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"}, - {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"}, -] - -[package.dependencies] -traitlets = "*" - -[[package]] -name = "mccabe" -version = "0.7.0" -description = "McCabe checker, plugin for flake8" -optional = false -python-versions = ">=3.6" -files = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -description = "Markdown URL utilities" -optional = false -python-versions = ">=3.7" -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - -[[package]] -name = "mergedeep" -version = "1.3.4" -description = "A deep merge function for 🐍." -optional = false -python-versions = ">=3.6" -files = [ - {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, - {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, -] - -[[package]] -name = "mistune" -version = "3.0.2" -description = "A sane and fast Markdown parser with useful plugins and renderers" -optional = true -python-versions = ">=3.7" -files = [ - {file = "mistune-3.0.2-py3-none-any.whl", hash = "sha256:71481854c30fdbc938963d3605b72501f5c10a9320ecd412c121c163a1c7d205"}, - {file = "mistune-3.0.2.tar.gz", hash = "sha256:fc7f93ded930c92394ef2cb6f04a8aabab4117a91449e72dcc8dfa646a508be8"}, -] - -[[package]] -name = "mkdocs" -version = "1.5.3" -description = "Project documentation with Markdown." -optional = false -python-versions = ">=3.7" -files = [ - {file = "mkdocs-1.5.3-py3-none-any.whl", hash = "sha256:3b3a78e736b31158d64dbb2f8ba29bd46a379d0c6e324c2246c3bc3d2189cfc1"}, - {file = "mkdocs-1.5.3.tar.gz", hash = "sha256:eb7c99214dcb945313ba30426c2451b735992c73c2e10838f76d09e39ff4d0e2"}, -] - -[package.dependencies] -click = ">=7.0" -colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} -ghp-import = ">=1.0" -importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""} -jinja2 = ">=2.11.1" -markdown = ">=3.2.1" -markupsafe = ">=2.0.1" -mergedeep = ">=1.3.4" -packaging = ">=20.5" -pathspec = ">=0.11.1" -platformdirs = ">=2.2.0" -pyyaml = ">=5.1" -pyyaml-env-tag = ">=0.1" -watchdog = ">=2.0" - -[package.extras] -i18n = ["babel (>=2.9.0)"] -min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.3)", "jinja2 (==2.11.1)", "markdown (==3.2.1)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "packaging (==20.5)", "pathspec (==0.11.1)", "platformdirs (==2.2.0)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "typing-extensions (==3.10)", "watchdog (==2.0)"] - -[[package]] -name = "mkdocs-material" -version = "9.4.6" -description = "Documentation that simply works" -optional = false -python-versions = ">=3.8" -files = [ - {file = "mkdocs_material-9.4.6-py3-none-any.whl", hash = "sha256:78802035d5768a78139c84ad7dce0c6493e8f7dc4861727d36ed91d1520a54da"}, - {file = "mkdocs_material-9.4.6.tar.gz", hash = "sha256:09665e60df7ee9e5ff3a54af173f6d45be718b1ee7dd962bcff3102b81fb0c14"}, -] - -[package.dependencies] -babel = ">=2.10,<3.0" -colorama = ">=0.4,<1.0" -jinja2 = ">=3.0,<4.0" -markdown = ">=3.2,<4.0" -mkdocs = ">=1.5.3,<2.0" -mkdocs-material-extensions = ">=1.2,<2.0" -paginate = ">=0.5,<1.0" -pygments = ">=2.16,<3.0" -pymdown-extensions = ">=10.2,<11.0" -regex = ">=2022.4" -requests = ">=2.26,<3.0" - -[package.extras] -git = ["mkdocs-git-committers-plugin-2 (>=1.1,<2.0)", "mkdocs-git-revision-date-localized-plugin (>=1.2,<2.0)"] -imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=9.4,<10.0)"] -recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"] - -[[package]] -name = "mkdocs-material-extensions" -version = "1.3" -description = "Extension pack for Python Markdown and MkDocs Material." -optional = false -python-versions = ">=3.8" -files = [ - {file = "mkdocs_material_extensions-1.3-py3-none-any.whl", hash = "sha256:0297cc48ba68a9fdd1ef3780a3b41b534b0d0df1d1181a44676fda5f464eeadc"}, - {file = "mkdocs_material_extensions-1.3.tar.gz", hash = "sha256:f0446091503acb110a7cab9349cbc90eeac51b58d1caa92a704a81ca1e24ddbd"}, -] - -[[package]] -name = "multidict" -version = "6.0.4" -description = "multidict implementation" -optional = false -python-versions = ">=3.7" -files = [ - {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"}, - {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"}, - {file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5"}, - {file = "multidict-6.0.4-cp310-cp310-win32.whl", hash = "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8"}, - {file = "multidict-6.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc"}, - {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03"}, - {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3"}, - {file = "multidict-6.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461"}, - {file = "multidict-6.0.4-cp311-cp311-win32.whl", hash = "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636"}, - {file = "multidict-6.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0"}, - {file = "multidict-6.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d"}, - {file = "multidict-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775"}, - {file = "multidict-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e"}, - {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c"}, - {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161"}, - {file = "multidict-6.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1"}, - {file = "multidict-6.0.4-cp38-cp38-win32.whl", hash = "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779"}, - {file = "multidict-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480"}, - {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664"}, - {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35"}, - {file = "multidict-6.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95"}, - {file = "multidict-6.0.4-cp39-cp39-win32.whl", hash = "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313"}, - {file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"}, - {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"}, -] - -[[package]] -name = "mypy" -version = "1.6.1" -description = "Optional static typing for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "mypy-1.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e5012e5cc2ac628177eaac0e83d622b2dd499e28253d4107a08ecc59ede3fc2c"}, - {file = "mypy-1.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d8fbb68711905f8912e5af474ca8b78d077447d8f3918997fecbf26943ff3cbb"}, - {file = "mypy-1.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21a1ad938fee7d2d96ca666c77b7c494c3c5bd88dff792220e1afbebb2925b5e"}, - {file = "mypy-1.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b96ae2c1279d1065413965c607712006205a9ac541895004a1e0d4f281f2ff9f"}, - {file = "mypy-1.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:40b1844d2e8b232ed92e50a4bd11c48d2daa351f9deee6c194b83bf03e418b0c"}, - {file = "mypy-1.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:81af8adaa5e3099469e7623436881eff6b3b06db5ef75e6f5b6d4871263547e5"}, - {file = "mypy-1.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8c223fa57cb154c7eab5156856c231c3f5eace1e0bed9b32a24696b7ba3c3245"}, - {file = "mypy-1.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8032e00ce71c3ceb93eeba63963b864bf635a18f6c0c12da6c13c450eedb183"}, - {file = "mypy-1.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4c46b51de523817a0045b150ed11b56f9fff55f12b9edd0f3ed35b15a2809de0"}, - {file = "mypy-1.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:19f905bcfd9e167159b3d63ecd8cb5e696151c3e59a1742e79bc3bcb540c42c7"}, - {file = "mypy-1.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:82e469518d3e9a321912955cc702d418773a2fd1e91c651280a1bda10622f02f"}, - {file = "mypy-1.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d4473c22cc296425bbbce7e9429588e76e05bc7342da359d6520b6427bf76660"}, - {file = "mypy-1.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59a0d7d24dfb26729e0a068639a6ce3500e31d6655df8557156c51c1cb874ce7"}, - {file = "mypy-1.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cfd13d47b29ed3bbaafaff7d8b21e90d827631afda134836962011acb5904b71"}, - {file = "mypy-1.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:eb4f18589d196a4cbe5290b435d135dee96567e07c2b2d43b5c4621b6501531a"}, - {file = "mypy-1.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:41697773aa0bf53ff917aa077e2cde7aa50254f28750f9b88884acea38a16169"}, - {file = "mypy-1.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7274b0c57737bd3476d2229c6389b2ec9eefeb090bbaf77777e9d6b1b5a9d143"}, - {file = "mypy-1.6.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbaf4662e498c8c2e352da5f5bca5ab29d378895fa2d980630656178bd607c46"}, - {file = "mypy-1.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bb8ccb4724f7d8601938571bf3f24da0da791fe2db7be3d9e79849cb64e0ae85"}, - {file = "mypy-1.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:68351911e85145f582b5aa6cd9ad666c8958bcae897a1bfda8f4940472463c45"}, - {file = "mypy-1.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:49ae115da099dcc0922a7a895c1eec82c1518109ea5c162ed50e3b3594c71208"}, - {file = "mypy-1.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b27958f8c76bed8edaa63da0739d76e4e9ad4ed325c814f9b3851425582a3cd"}, - {file = "mypy-1.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:925cd6a3b7b55dfba252b7c4561892311c5358c6b5a601847015a1ad4eb7d332"}, - {file = "mypy-1.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8f57e6b6927a49550da3d122f0cb983d400f843a8a82e65b3b380d3d7259468f"}, - {file = "mypy-1.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:a43ef1c8ddfdb9575691720b6352761f3f53d85f1b57d7745701041053deff30"}, - {file = "mypy-1.6.1-py3-none-any.whl", hash = "sha256:4cbe68ef919c28ea561165206a2dcb68591c50f3bcf777932323bc208d949cf1"}, - {file = "mypy-1.6.1.tar.gz", hash = "sha256:4d01c00d09a0be62a4ca3f933e315455bde83f37f892ba4b08ce92f3cf44bcc1"}, -] - -[package.dependencies] -mypy-extensions = ">=1.0.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=4.1.0" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -install-types = ["pip"] -reports = ["lxml"] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.5" -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - -[[package]] -name = "nbclassic" -version = "1.0.0" -description = "Jupyter Notebook as a Jupyter Server extension." -optional = true -python-versions = ">=3.7" -files = [ - {file = "nbclassic-1.0.0-py3-none-any.whl", hash = "sha256:f99e4769b4750076cd4235c044b61232110733322384a94a63791d2e7beacc66"}, - {file = "nbclassic-1.0.0.tar.gz", hash = "sha256:0ae11eb2319455d805596bf320336cda9554b41d99ab9a3c31bf8180bffa30e3"}, -] - -[package.dependencies] -argon2-cffi = "*" -ipykernel = "*" -ipython-genutils = "*" -jinja2 = "*" -jupyter-client = ">=6.1.1" -jupyter-core = ">=4.6.1" -jupyter-server = ">=1.8" -nbconvert = ">=5" -nbformat = "*" -nest-asyncio = ">=1.5" -notebook-shim = ">=0.2.3" -prometheus-client = "*" -pyzmq = ">=17" -Send2Trash = ">=1.8.0" -terminado = ">=0.8.3" -tornado = ">=6.1" -traitlets = ">=4.2.1" - -[package.extras] -docs = ["myst-parser", "nbsphinx", "sphinx", "sphinx-rtd-theme", "sphinxcontrib-github-alt"] -json-logging = ["json-logging"] -test = ["coverage", "nbval", "pytest", "pytest-cov", "pytest-jupyter", "pytest-playwright", "pytest-tornasync", "requests", "requests-unixsocket", "testpath"] - -[[package]] -name = "nbclient" -version = "0.8.0" -description = "A client library for executing notebooks. Formerly nbconvert's ExecutePreprocessor." -optional = true -python-versions = ">=3.8.0" -files = [ - {file = "nbclient-0.8.0-py3-none-any.whl", hash = "sha256:25e861299e5303a0477568557c4045eccc7a34c17fc08e7959558707b9ebe548"}, - {file = "nbclient-0.8.0.tar.gz", hash = "sha256:f9b179cd4b2d7bca965f900a2ebf0db4a12ebff2f36a711cb66861e4ae158e55"}, -] - -[package.dependencies] -jupyter-client = ">=6.1.12" -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" -nbformat = ">=5.1" -traitlets = ">=5.4" - -[package.extras] -dev = ["pre-commit"] -docs = ["autodoc-traits", "mock", "moto", "myst-parser", "nbclient[test]", "sphinx (>=1.7)", "sphinx-book-theme", "sphinxcontrib-spelling"] -test = ["flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "nbconvert (>=7.0.0)", "pytest (>=7.0)", "pytest-asyncio", "pytest-cov (>=4.0)", "testpath", "xmltodict"] - -[[package]] -name = "nbconvert" -version = "7.9.2" -description = "Converting Jupyter Notebooks" -optional = true -python-versions = ">=3.8" -files = [ - {file = "nbconvert-7.9.2-py3-none-any.whl", hash = "sha256:39fe4b8bdd1b0104fdd86fc8a43a9077ba64c720bda4c6132690d917a0a154ee"}, - {file = "nbconvert-7.9.2.tar.gz", hash = "sha256:e56cc7588acc4f93e2bb5a34ec69028e4941797b2bfaf6462f18a41d1cc258c9"}, -] - -[package.dependencies] -beautifulsoup4 = "*" -bleach = "!=5.0.0" -defusedxml = "*" -importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""} -jinja2 = ">=3.0" -jupyter-core = ">=4.7" -jupyterlab-pygments = "*" -markupsafe = ">=2.0" -mistune = ">=2.0.3,<4" -nbclient = ">=0.5.0" -nbformat = ">=5.7" -packaging = "*" -pandocfilters = ">=1.4.1" -pygments = ">=2.4.1" -tinycss2 = "*" -traitlets = ">=5.1" - -[package.extras] -all = ["nbconvert[docs,qtpdf,serve,test,webpdf]"] -docs = ["ipykernel", "ipython", "myst-parser", "nbsphinx (>=0.2.12)", "pydata-sphinx-theme", "sphinx (==5.0.2)", "sphinxcontrib-spelling"] -qtpdf = ["nbconvert[qtpng]"] -qtpng = ["pyqtwebengine (>=5.15)"] -serve = ["tornado (>=6.1)"] -test = ["flaky", "ipykernel", "ipywidgets (>=7)", "pytest", "pytest-dependency"] -webpdf = ["playwright"] - -[[package]] -name = "nbformat" -version = "5.9.2" -description = "The Jupyter Notebook format" -optional = true -python-versions = ">=3.8" -files = [ - {file = "nbformat-5.9.2-py3-none-any.whl", hash = "sha256:1c5172d786a41b82bcfd0c23f9e6b6f072e8fb49c39250219e4acfff1efe89e9"}, - {file = "nbformat-5.9.2.tar.gz", hash = "sha256:5f98b5ba1997dff175e77e0c17d5c10a96eaed2cbd1de3533d1fc35d5e111192"}, -] - -[package.dependencies] -fastjsonschema = "*" -jsonschema = ">=2.6" -jupyter-core = "*" -traitlets = ">=5.1" - -[package.extras] -docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] -test = ["pep440", "pre-commit", "pytest", "testpath"] - -[[package]] -name = "neoteroi-mkdocs" -version = "1.0.4" -description = "Plugins for MkDocs and Python Markdown" -optional = false -python-versions = ">=3.7" -files = [ - {file = "neoteroi_mkdocs-1.0.4-py3-none-any.whl", hash = "sha256:ed2b93a206173b10cd5635e050ce0d8e83df0e51cfd5ee6d41751bfcaa4fe197"}, - {file = "neoteroi_mkdocs-1.0.4.tar.gz", hash = "sha256:3825bcecc4b2c7755ef6adbf96d754762dafd76db508fdd3a23533c88e744228"}, -] - -[package.dependencies] -click = "*" -essentials-openapi = "*" -httpx = "*" -jinja2 = "*" -mkdocs = "*" -rich = "*" - -[[package]] -name = "nest-asyncio" -version = "1.5.8" -description = "Patch asyncio to allow nested event loops" -optional = true -python-versions = ">=3.5" -files = [ - {file = "nest_asyncio-1.5.8-py3-none-any.whl", hash = "sha256:accda7a339a70599cb08f9dd09a67e0c2ef8d8d6f4c07f96ab203f2ae254e48d"}, - {file = "nest_asyncio-1.5.8.tar.gz", hash = "sha256:25aa2ca0d2a5b5531956b9e273b45cf664cae2b145101d73b86b199978d48fdb"}, -] - -[[package]] -name = "nodeenv" -version = "1.8.0" -description = "Node.js virtual environment builder" -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" -files = [ - {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, - {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, -] - -[package.dependencies] -setuptools = "*" - -[[package]] -name = "notebook" -version = "6.5.6" -description = "A web-based notebook environment for interactive computing" -optional = true -python-versions = ">=3.7" -files = [ - {file = "notebook-6.5.6-py3-none-any.whl", hash = "sha256:c1e2eb2e3b6079a0552a04974883a48d04c3c05792170d64a4b23d707d453181"}, - {file = "notebook-6.5.6.tar.gz", hash = "sha256:b4625a4b7a597839dd3156b140d5ba2c7123761f98245a3290f67a8b8ee048d9"}, -] - -[package.dependencies] -argon2-cffi = "*" -ipykernel = "*" -ipython-genutils = "*" -jinja2 = "*" -jupyter-client = ">=5.3.4,<8" -jupyter-core = ">=4.6.1" -nbclassic = ">=0.4.7" -nbconvert = ">=5" -nbformat = "*" -nest-asyncio = ">=1.5" -prometheus-client = "*" -pyzmq = ">=17,<25" -Send2Trash = ">=1.8.0" -terminado = ">=0.8.3" -tornado = ">=6.1" -traitlets = ">=4.2.1" - -[package.extras] -docs = ["myst-parser", "nbsphinx", "sphinx", "sphinx-rtd-theme", "sphinxcontrib-github-alt"] -json-logging = ["json-logging"] -test = ["coverage", "nbval", "pytest", "pytest-cov", "requests", "requests-unixsocket", "selenium (==4.1.5)", "testpath"] - -[[package]] -name = "notebook-shim" -version = "0.2.3" -description = "A shim layer for notebook traits and config" -optional = true -python-versions = ">=3.7" -files = [ - {file = "notebook_shim-0.2.3-py3-none-any.whl", hash = "sha256:a83496a43341c1674b093bfcebf0fe8e74cbe7eda5fd2bbc56f8e39e1486c0c7"}, - {file = "notebook_shim-0.2.3.tar.gz", hash = "sha256:f69388ac283ae008cd506dda10d0288b09a017d822d5e8c7129a152cbd3ce7e9"}, -] - -[package.dependencies] -jupyter-server = ">=1.8,<3" - -[package.extras] -test = ["pytest", "pytest-console-scripts", "pytest-jupyter", "pytest-tornasync"] - -[[package]] -name = "overrides" -version = "7.4.0" -description = "A decorator to automatically detect mismatch when overriding a method." -optional = true -python-versions = ">=3.6" -files = [ - {file = "overrides-7.4.0-py3-none-any.whl", hash = "sha256:3ad24583f86d6d7a49049695efe9933e67ba62f0c7625d53c59fa832ce4b8b7d"}, - {file = "overrides-7.4.0.tar.gz", hash = "sha256:9502a3cca51f4fac40b5feca985b6703a5c1f6ad815588a7ca9e285b9dca6757"}, -] - -[[package]] -name = "packaging" -version = "23.2" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.7" -files = [ - {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, - {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, -] - -[[package]] -name = "paginate" -version = "0.5.6" -description = "Divides large result sets into pages for easier browsing" -optional = false -python-versions = "*" -files = [ - {file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"}, -] - -[[package]] -name = "pandocfilters" -version = "1.5.0" -description = "Utilities for writing pandoc filters in python" -optional = true -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "pandocfilters-1.5.0-py2.py3-none-any.whl", hash = "sha256:33aae3f25fd1a026079f5d27bdd52496f0e0803b3469282162bafdcbdf6ef14f"}, - {file = "pandocfilters-1.5.0.tar.gz", hash = "sha256:0b679503337d233b4339a817bfc8c50064e2eff681314376a47cb582305a7a38"}, -] - -[[package]] -name = "parso" -version = "0.8.3" -description = "A Python Parser" -optional = true -python-versions = ">=3.6" -files = [ - {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, - {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, -] - -[package.extras] -qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] -testing = ["docopt", "pytest (<6.0.0)"] - -[[package]] -name = "pathspec" -version = "0.11.2" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.7" -files = [ - {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, - {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, -] - -[[package]] -name = "pexpect" -version = "4.8.0" -description = "Pexpect allows easy control of interactive console applications." -optional = true -python-versions = "*" -files = [ - {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, - {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, -] - -[package.dependencies] -ptyprocess = ">=0.5" - -[[package]] -name = "pickleshare" -version = "0.7.5" -description = "Tiny 'shelve'-like database with concurrency support" -optional = true -python-versions = "*" -files = [ - {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, - {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, -] - -[[package]] -name = "pillow" -version = "9.5.0" -description = "Python Imaging Library (Fork)" -optional = true -python-versions = ">=3.7" -files = [ - {file = "Pillow-9.5.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:ace6ca218308447b9077c14ea4ef381ba0b67ee78d64046b3f19cf4e1139ad16"}, - {file = "Pillow-9.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3d403753c9d5adc04d4694d35cf0391f0f3d57c8e0030aac09d7678fa8030aa"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ba1b81ee69573fe7124881762bb4cd2e4b6ed9dd28c9c60a632902fe8db8b38"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe7e1c262d3392afcf5071df9afa574544f28eac825284596ac6db56e6d11062"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f36397bf3f7d7c6a3abdea815ecf6fd14e7fcd4418ab24bae01008d8d8ca15e"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:252a03f1bdddce077eff2354c3861bf437c892fb1832f75ce813ee94347aa9b5"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:85ec677246533e27770b0de5cf0f9d6e4ec0c212a1f89dfc941b64b21226009d"}, - {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b416f03d37d27290cb93597335a2f85ed446731200705b22bb927405320de903"}, - {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1781a624c229cb35a2ac31cc4a77e28cafc8900733a864870c49bfeedacd106a"}, - {file = "Pillow-9.5.0-cp310-cp310-win32.whl", hash = "sha256:8507eda3cd0608a1f94f58c64817e83ec12fa93a9436938b191b80d9e4c0fc44"}, - {file = "Pillow-9.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:d3c6b54e304c60c4181da1c9dadf83e4a54fd266a99c70ba646a9baa626819eb"}, - {file = "Pillow-9.5.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:7ec6f6ce99dab90b52da21cf0dc519e21095e332ff3b399a357c187b1a5eee32"}, - {file = "Pillow-9.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:560737e70cb9c6255d6dcba3de6578a9e2ec4b573659943a5e7e4af13f298f5c"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96e88745a55b88a7c64fa49bceff363a1a27d9a64e04019c2281049444a571e3"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9c206c29b46cfd343ea7cdfe1232443072bbb270d6a46f59c259460db76779a"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcc2c53c06f2ccb8976fb5c71d448bdd0a07d26d8e07e321c103416444c7ad1"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a0f9bb6c80e6efcde93ffc51256d5cfb2155ff8f78292f074f60f9e70b942d99"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8d935f924bbab8f0a9a28404422da8af4904e36d5c33fc6f677e4c4485515625"}, - {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fed1e1cf6a42577953abbe8e6cf2fe2f566daebde7c34724ec8803c4c0cda579"}, - {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c1170d6b195555644f0616fd6ed929dfcf6333b8675fcca044ae5ab110ded296"}, - {file = "Pillow-9.5.0-cp311-cp311-win32.whl", hash = "sha256:54f7102ad31a3de5666827526e248c3530b3a33539dbda27c6843d19d72644ec"}, - {file = "Pillow-9.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfa4561277f677ecf651e2b22dc43e8f5368b74a25a8f7d1d4a3a243e573f2d4"}, - {file = "Pillow-9.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:965e4a05ef364e7b973dd17fc765f42233415974d773e82144c9bbaaaea5d089"}, - {file = "Pillow-9.5.0-cp312-cp312-win32.whl", hash = "sha256:22baf0c3cf0c7f26e82d6e1adf118027afb325e703922c8dfc1d5d0156bb2eeb"}, - {file = "Pillow-9.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:432b975c009cf649420615388561c0ce7cc31ce9b2e374db659ee4f7d57a1f8b"}, - {file = "Pillow-9.5.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5d4ebf8e1db4441a55c509c4baa7a0587a0210f7cd25fcfe74dbbce7a4bd1906"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:375f6e5ee9620a271acb6820b3d1e94ffa8e741c0601db4c0c4d3cb0a9c224bf"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99eb6cafb6ba90e436684e08dad8be1637efb71c4f2180ee6b8f940739406e78"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfaaf10b6172697b9bceb9a3bd7b951819d1ca339a5ef294d1f1ac6d7f63270"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:763782b2e03e45e2c77d7779875f4432e25121ef002a41829d8868700d119392"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:35f6e77122a0c0762268216315bf239cf52b88865bba522999dc38f1c52b9b47"}, - {file = "Pillow-9.5.0-cp37-cp37m-win32.whl", hash = "sha256:aca1c196f407ec7cf04dcbb15d19a43c507a81f7ffc45b690899d6a76ac9fda7"}, - {file = "Pillow-9.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322724c0032af6692456cd6ed554bb85f8149214d97398bb80613b04e33769f6"}, - {file = "Pillow-9.5.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:a0aa9417994d91301056f3d0038af1199eb7adc86e646a36b9e050b06f526597"}, - {file = "Pillow-9.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8286396b351785801a976b1e85ea88e937712ee2c3ac653710a4a57a8da5d9c"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c830a02caeb789633863b466b9de10c015bded434deb3ec87c768e53752ad22a"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fbd359831c1657d69bb81f0db962905ee05e5e9451913b18b831febfe0519082"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8fc330c3370a81bbf3f88557097d1ea26cd8b019d6433aa59f71195f5ddebbf"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:7002d0797a3e4193c7cdee3198d7c14f92c0836d6b4a3f3046a64bd1ce8df2bf"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:229e2c79c00e85989a34b5981a2b67aa079fd08c903f0aaead522a1d68d79e51"}, - {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9adf58f5d64e474bed00d69bcd86ec4bcaa4123bfa70a65ce72e424bfb88ed96"}, - {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:662da1f3f89a302cc22faa9f14a262c2e3951f9dbc9617609a47521c69dd9f8f"}, - {file = "Pillow-9.5.0-cp38-cp38-win32.whl", hash = "sha256:6608ff3bf781eee0cd14d0901a2b9cc3d3834516532e3bd673a0a204dc8615fc"}, - {file = "Pillow-9.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:e49eb4e95ff6fd7c0c402508894b1ef0e01b99a44320ba7d8ecbabefddcc5569"}, - {file = "Pillow-9.5.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:482877592e927fd263028c105b36272398e3e1be3269efda09f6ba21fd83ec66"}, - {file = "Pillow-9.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3ded42b9ad70e5f1754fb7c2e2d6465a9c842e41d178f262e08b8c85ed8a1d8e"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c446d2245ba29820d405315083d55299a796695d747efceb5717a8b450324115"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aca1152d93dcc27dc55395604dcfc55bed5f25ef4c98716a928bacba90d33a3"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:608488bdcbdb4ba7837461442b90ea6f3079397ddc968c31265c1e056964f1ef"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:60037a8db8750e474af7ffc9faa9b5859e6c6d0a50e55c45576bf28be7419705"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:07999f5834bdc404c442146942a2ecadd1cb6292f5229f4ed3b31e0a108746b1"}, - {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a127ae76092974abfbfa38ca2d12cbeddcdeac0fb71f9627cc1135bedaf9d51a"}, - {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:489f8389261e5ed43ac8ff7b453162af39c3e8abd730af8363587ba64bb2e865"}, - {file = "Pillow-9.5.0-cp39-cp39-win32.whl", hash = "sha256:9b1af95c3a967bf1da94f253e56b6286b50af23392a886720f563c547e48e964"}, - {file = "Pillow-9.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:77165c4a5e7d5a284f10a6efaa39a0ae8ba839da344f20b111d62cc932fa4e5d"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:833b86a98e0ede388fa29363159c9b1a294b0905b5128baf01db683672f230f5"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaf305d6d40bd9632198c766fb64f0c1a83ca5b667f16c1e79e1661ab5060140"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0852ddb76d85f127c135b6dd1f0bb88dbb9ee990d2cd9aa9e28526c93e794fba"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:91ec6fe47b5eb5a9968c79ad9ed78c342b1f97a091677ba0e012701add857829"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cb841572862f629b99725ebaec3287fc6d275be9b14443ea746c1dd325053cbd"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c380b27d041209b849ed246b111b7c166ba36d7933ec6e41175fd15ab9eb1572"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c9af5a3b406a50e313467e3565fc99929717f780164fe6fbb7704edba0cebbe"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5671583eab84af046a397d6d0ba25343c00cd50bce03787948e0fff01d4fd9b1"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:84a6f19ce086c1bf894644b43cd129702f781ba5751ca8572f08aa40ef0ab7b7"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1e7723bd90ef94eda669a3c2c19d549874dd5badaeefabefd26053304abe5799"}, - {file = "Pillow-9.5.0.tar.gz", hash = "sha256:bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1"}, -] - -[package.extras] -docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] -tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] - -[[package]] -name = "platformdirs" -version = "3.11.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -optional = false -python-versions = ">=3.7" -files = [ - {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, - {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, -] - -[package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] - -[[package]] -name = "pluggy" -version = "1.3.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, - {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "pre-commit" -version = "3.5.0" -description = "A framework for managing and maintaining multi-language pre-commit hooks." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"}, - {file = "pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32"}, -] - -[package.dependencies] -cfgv = ">=2.0.0" -identify = ">=1.0.0" -nodeenv = ">=0.11.1" -pyyaml = ">=5.1" -virtualenv = ">=20.10.0" - -[[package]] -name = "prometheus-client" -version = "0.17.1" -description = "Python client for the Prometheus monitoring system." -optional = true -python-versions = ">=3.6" -files = [ - {file = "prometheus_client-0.17.1-py3-none-any.whl", hash = "sha256:e537f37160f6807b8202a6fc4764cdd19bac5480ddd3e0d463c3002b34462101"}, - {file = "prometheus_client-0.17.1.tar.gz", hash = "sha256:21e674f39831ae3f8acde238afd9a27a37d0d2fb5a28ea094f0ce25d2cbf2091"}, -] - -[package.extras] -twisted = ["twisted"] - -[[package]] -name = "prompt-toolkit" -version = "3.0.39" -description = "Library for building powerful interactive command lines in Python" -optional = true -python-versions = ">=3.7.0" -files = [ - {file = "prompt_toolkit-3.0.39-py3-none-any.whl", hash = "sha256:9dffbe1d8acf91e3de75f3b544e4842382fc06c6babe903ac9acb74dc6e08d88"}, - {file = "prompt_toolkit-3.0.39.tar.gz", hash = "sha256:04505ade687dc26dc4284b1ad19a83be2f2afe83e7a828ace0c72f3a1df72aac"}, -] - -[package.dependencies] -wcwidth = "*" - -[[package]] -name = "psutil" -version = "5.9.6" -description = "Cross-platform lib for process and system monitoring in Python." -optional = true -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" -files = [ - {file = "psutil-5.9.6-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:fb8a697f11b0f5994550555fcfe3e69799e5b060c8ecf9e2f75c69302cc35c0d"}, - {file = "psutil-5.9.6-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:91ecd2d9c00db9817a4b4192107cf6954addb5d9d67a969a4f436dbc9200f88c"}, - {file = "psutil-5.9.6-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:10e8c17b4f898d64b121149afb136c53ea8b68c7531155147867b7b1ac9e7e28"}, - {file = "psutil-5.9.6-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:18cd22c5db486f33998f37e2bb054cc62fd06646995285e02a51b1e08da97017"}, - {file = "psutil-5.9.6-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:ca2780f5e038379e520281e4c032dddd086906ddff9ef0d1b9dcf00710e5071c"}, - {file = "psutil-5.9.6-cp27-none-win32.whl", hash = "sha256:70cb3beb98bc3fd5ac9ac617a327af7e7f826373ee64c80efd4eb2856e5051e9"}, - {file = "psutil-5.9.6-cp27-none-win_amd64.whl", hash = "sha256:51dc3d54607c73148f63732c727856f5febec1c7c336f8f41fcbd6315cce76ac"}, - {file = "psutil-5.9.6-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c69596f9fc2f8acd574a12d5f8b7b1ba3765a641ea5d60fb4736bf3c08a8214a"}, - {file = "psutil-5.9.6-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92e0cc43c524834af53e9d3369245e6cc3b130e78e26100d1f63cdb0abeb3d3c"}, - {file = "psutil-5.9.6-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:748c9dd2583ed86347ed65d0035f45fa8c851e8d90354c122ab72319b5f366f4"}, - {file = "psutil-5.9.6-cp36-cp36m-win32.whl", hash = "sha256:3ebf2158c16cc69db777e3c7decb3c0f43a7af94a60d72e87b2823aebac3d602"}, - {file = "psutil-5.9.6-cp36-cp36m-win_amd64.whl", hash = "sha256:ff18b8d1a784b810df0b0fff3bcb50ab941c3b8e2c8de5726f9c71c601c611aa"}, - {file = "psutil-5.9.6-cp37-abi3-win32.whl", hash = "sha256:a6f01f03bf1843280f4ad16f4bde26b817847b4c1a0db59bf6419807bc5ce05c"}, - {file = "psutil-5.9.6-cp37-abi3-win_amd64.whl", hash = "sha256:6e5fb8dc711a514da83098bc5234264e551ad980cec5f85dabf4d38ed6f15e9a"}, - {file = "psutil-5.9.6-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:daecbcbd29b289aac14ece28eca6a3e60aa361754cf6da3dfb20d4d32b6c7f57"}, - {file = "psutil-5.9.6.tar.gz", hash = "sha256:e4b92ddcd7dd4cdd3f900180ea1e104932c7bce234fb88976e2a3b296441225a"}, -] - -[package.extras] -test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] - -[[package]] -name = "ptyprocess" -version = "0.7.0" -description = "Run a subprocess in a pseudo terminal" -optional = true -python-versions = "*" -files = [ - {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, - {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, -] - -[[package]] -name = "pure-eval" -version = "0.2.2" -description = "Safely evaluate AST nodes without side effects" -optional = true -python-versions = "*" -files = [ - {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, - {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, -] - -[package.extras] -tests = ["pytest"] - -[[package]] -name = "py" -version = "1.11.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -optional = true -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] - -[[package]] -name = "pycodestyle" -version = "2.11.1" -description = "Python style guide checker" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, - {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, -] - -[[package]] -name = "pycparser" -version = "2.21" -description = "C parser in Python" -optional = true -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, -] - -[[package]] -name = "pydantic" -version = "2.4.2" -description = "Data validation using Python type hints" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pydantic-2.4.2-py3-none-any.whl", hash = "sha256:bc3ddf669d234f4220e6e1c4d96b061abe0998185a8d7855c0126782b7abc8c1"}, - {file = "pydantic-2.4.2.tar.gz", hash = "sha256:94f336138093a5d7f426aac732dcfe7ab4eb4da243c88f891d65deb4a2556ee7"}, -] - -[package.dependencies] -annotated-types = ">=0.4.0" -pydantic-core = "2.10.1" -typing-extensions = ">=4.6.1" - -[package.extras] -email = ["email-validator (>=2.0.0)"] - -[[package]] -name = "pydantic-core" -version = "2.10.1" -description = "" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pydantic_core-2.10.1-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:d64728ee14e667ba27c66314b7d880b8eeb050e58ffc5fec3b7a109f8cddbd63"}, - {file = "pydantic_core-2.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:48525933fea744a3e7464c19bfede85df4aba79ce90c60b94d8b6e1eddd67096"}, - {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef337945bbd76cce390d1b2496ccf9f90b1c1242a3a7bc242ca4a9fc5993427a"}, - {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1392e0638af203cee360495fd2cfdd6054711f2db5175b6e9c3c461b76f5175"}, - {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0675ba5d22de54d07bccde38997e780044dcfa9a71aac9fd7d4d7a1d2e3e65f7"}, - {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:128552af70a64660f21cb0eb4876cbdadf1a1f9d5de820fed6421fa8de07c893"}, - {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f6e6aed5818c264412ac0598b581a002a9f050cb2637a84979859e70197aa9e"}, - {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ecaac27da855b8d73f92123e5f03612b04c5632fd0a476e469dfc47cd37d6b2e"}, - {file = "pydantic_core-2.10.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b3c01c2fb081fced3bbb3da78510693dc7121bb893a1f0f5f4b48013201f362e"}, - {file = "pydantic_core-2.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:92f675fefa977625105708492850bcbc1182bfc3e997f8eecb866d1927c98ae6"}, - {file = "pydantic_core-2.10.1-cp310-none-win32.whl", hash = "sha256:420a692b547736a8d8703c39ea935ab5d8f0d2573f8f123b0a294e49a73f214b"}, - {file = "pydantic_core-2.10.1-cp310-none-win_amd64.whl", hash = "sha256:0880e239827b4b5b3e2ce05e6b766a7414e5f5aedc4523be6b68cfbc7f61c5d0"}, - {file = "pydantic_core-2.10.1-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:073d4a470b195d2b2245d0343569aac7e979d3a0dcce6c7d2af6d8a920ad0bea"}, - {file = "pydantic_core-2.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:600d04a7b342363058b9190d4e929a8e2e715c5682a70cc37d5ded1e0dd370b4"}, - {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39215d809470f4c8d1881758575b2abfb80174a9e8daf8f33b1d4379357e417c"}, - {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eeb3d3d6b399ffe55f9a04e09e635554012f1980696d6b0aca3e6cf42a17a03b"}, - {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7a7902bf75779bc12ccfc508bfb7a4c47063f748ea3de87135d433a4cca7a2f"}, - {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3625578b6010c65964d177626fde80cf60d7f2e297d56b925cb5cdeda6e9925a"}, - {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:caa48fc31fc7243e50188197b5f0c4228956f97b954f76da157aae7f67269ae8"}, - {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:07ec6d7d929ae9c68f716195ce15e745b3e8fa122fc67698ac6498d802ed0fa4"}, - {file = "pydantic_core-2.10.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e6f31a17acede6a8cd1ae2d123ce04d8cca74056c9d456075f4f6f85de055607"}, - {file = "pydantic_core-2.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d8f1ebca515a03e5654f88411420fea6380fc841d1bea08effb28184e3d4899f"}, - {file = "pydantic_core-2.10.1-cp311-none-win32.whl", hash = "sha256:6db2eb9654a85ada248afa5a6db5ff1cf0f7b16043a6b070adc4a5be68c716d6"}, - {file = "pydantic_core-2.10.1-cp311-none-win_amd64.whl", hash = "sha256:4a5be350f922430997f240d25f8219f93b0c81e15f7b30b868b2fddfc2d05f27"}, - {file = "pydantic_core-2.10.1-cp311-none-win_arm64.whl", hash = "sha256:5fdb39f67c779b183b0c853cd6b45f7db84b84e0571b3ef1c89cdb1dfc367325"}, - {file = "pydantic_core-2.10.1-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:b1f22a9ab44de5f082216270552aa54259db20189e68fc12484873d926426921"}, - {file = "pydantic_core-2.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8572cadbf4cfa95fb4187775b5ade2eaa93511f07947b38f4cd67cf10783b118"}, - {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db9a28c063c7c00844ae42a80203eb6d2d6bbb97070cfa00194dff40e6f545ab"}, - {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2a35baa428181cb2270a15864ec6286822d3576f2ed0f4cd7f0c1708472aff"}, - {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05560ab976012bf40f25d5225a58bfa649bb897b87192a36c6fef1ab132540d7"}, - {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d6495008733c7521a89422d7a68efa0a0122c99a5861f06020ef5b1f51f9ba7c"}, - {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ac492c686defc8e6133e3a2d9eaf5261b3df26b8ae97450c1647286750b901"}, - {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8282bab177a9a3081fd3d0a0175a07a1e2bfb7fcbbd949519ea0980f8a07144d"}, - {file = "pydantic_core-2.10.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:aafdb89fdeb5fe165043896817eccd6434aee124d5ee9b354f92cd574ba5e78f"}, - {file = "pydantic_core-2.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f6defd966ca3b187ec6c366604e9296f585021d922e666b99c47e78738b5666c"}, - {file = "pydantic_core-2.10.1-cp312-none-win32.whl", hash = "sha256:7c4d1894fe112b0864c1fa75dffa045720a194b227bed12f4be7f6045b25209f"}, - {file = "pydantic_core-2.10.1-cp312-none-win_amd64.whl", hash = "sha256:5994985da903d0b8a08e4935c46ed8daf5be1cf217489e673910951dc533d430"}, - {file = "pydantic_core-2.10.1-cp312-none-win_arm64.whl", hash = "sha256:0d8a8adef23d86d8eceed3e32e9cca8879c7481c183f84ed1a8edc7df073af94"}, - {file = "pydantic_core-2.10.1-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:9badf8d45171d92387410b04639d73811b785b5161ecadabf056ea14d62d4ede"}, - {file = "pydantic_core-2.10.1-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:ebedb45b9feb7258fac0a268a3f6bec0a2ea4d9558f3d6f813f02ff3a6dc6698"}, - {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfe1090245c078720d250d19cb05d67e21a9cd7c257698ef139bc41cf6c27b4f"}, - {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e357571bb0efd65fd55f18db0a2fb0ed89d0bb1d41d906b138f088933ae618bb"}, - {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3dcd587b69bbf54fc04ca157c2323b8911033e827fffaecf0cafa5a892a0904"}, - {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c120c9ce3b163b985a3b966bb701114beb1da4b0468b9b236fc754783d85aa3"}, - {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15d6bca84ffc966cc9976b09a18cf9543ed4d4ecbd97e7086f9ce9327ea48891"}, - {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5cabb9710f09d5d2e9e2748c3e3e20d991a4c5f96ed8f1132518f54ab2967221"}, - {file = "pydantic_core-2.10.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:82f55187a5bebae7d81d35b1e9aaea5e169d44819789837cdd4720d768c55d15"}, - {file = "pydantic_core-2.10.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:1d40f55222b233e98e3921df7811c27567f0e1a4411b93d4c5c0f4ce131bc42f"}, - {file = "pydantic_core-2.10.1-cp37-none-win32.whl", hash = "sha256:14e09ff0b8fe6e46b93d36a878f6e4a3a98ba5303c76bb8e716f4878a3bee92c"}, - {file = "pydantic_core-2.10.1-cp37-none-win_amd64.whl", hash = "sha256:1396e81b83516b9d5c9e26a924fa69164156c148c717131f54f586485ac3c15e"}, - {file = "pydantic_core-2.10.1-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:6835451b57c1b467b95ffb03a38bb75b52fb4dc2762bb1d9dbed8de31ea7d0fc"}, - {file = "pydantic_core-2.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b00bc4619f60c853556b35f83731bd817f989cba3e97dc792bb8c97941b8053a"}, - {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fa467fd300a6f046bdb248d40cd015b21b7576c168a6bb20aa22e595c8ffcdd"}, - {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d99277877daf2efe074eae6338453a4ed54a2d93fb4678ddfe1209a0c93a2468"}, - {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa7db7558607afeccb33c0e4bf1c9a9a835e26599e76af6fe2fcea45904083a6"}, - {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aad7bd686363d1ce4ee930ad39f14e1673248373f4a9d74d2b9554f06199fb58"}, - {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:443fed67d33aa85357464f297e3d26e570267d1af6fef1c21ca50921d2976302"}, - {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:042462d8d6ba707fd3ce9649e7bf268633a41018d6a998fb5fbacb7e928a183e"}, - {file = "pydantic_core-2.10.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ecdbde46235f3d560b18be0cb706c8e8ad1b965e5c13bbba7450c86064e96561"}, - {file = "pydantic_core-2.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ed550ed05540c03f0e69e6d74ad58d026de61b9eaebebbaaf8873e585cbb18de"}, - {file = "pydantic_core-2.10.1-cp38-none-win32.whl", hash = "sha256:8cdbbd92154db2fec4ec973d45c565e767ddc20aa6dbaf50142676484cbff8ee"}, - {file = "pydantic_core-2.10.1-cp38-none-win_amd64.whl", hash = "sha256:9f6f3e2598604956480f6c8aa24a3384dbf6509fe995d97f6ca6103bb8c2534e"}, - {file = "pydantic_core-2.10.1-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:655f8f4c8d6a5963c9a0687793da37b9b681d9ad06f29438a3b2326d4e6b7970"}, - {file = "pydantic_core-2.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e570ffeb2170e116a5b17e83f19911020ac79d19c96f320cbfa1fa96b470185b"}, - {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64322bfa13e44c6c30c518729ef08fda6026b96d5c0be724b3c4ae4da939f875"}, - {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:485a91abe3a07c3a8d1e082ba29254eea3e2bb13cbbd4351ea4e5a21912cc9b0"}, - {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7c2b8eb9fc872e68b46eeaf835e86bccc3a58ba57d0eedc109cbb14177be531"}, - {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5cb87bdc2e5f620693148b5f8f842d293cae46c5f15a1b1bf7ceeed324a740c"}, - {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25bd966103890ccfa028841a8f30cebcf5875eeac8c4bde4fe221364c92f0c9a"}, - {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f323306d0556351735b54acbf82904fe30a27b6a7147153cbe6e19aaaa2aa429"}, - {file = "pydantic_core-2.10.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0c27f38dc4fbf07b358b2bc90edf35e82d1703e22ff2efa4af4ad5de1b3833e7"}, - {file = "pydantic_core-2.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f1365e032a477c1430cfe0cf2856679529a2331426f8081172c4a74186f1d595"}, - {file = "pydantic_core-2.10.1-cp39-none-win32.whl", hash = "sha256:a1c311fd06ab3b10805abb72109f01a134019739bd3286b8ae1bc2fc4e50c07a"}, - {file = "pydantic_core-2.10.1-cp39-none-win_amd64.whl", hash = "sha256:ae8a8843b11dc0b03b57b52793e391f0122e740de3df1474814c700d2622950a"}, - {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d43002441932f9a9ea5d6f9efaa2e21458221a3a4b417a14027a1d530201ef1b"}, - {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fcb83175cc4936a5425dde3356f079ae03c0802bbdf8ff82c035f8a54b333521"}, - {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:962ed72424bf1f72334e2f1e61b68f16c0e596f024ca7ac5daf229f7c26e4208"}, - {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2cf5bb4dd67f20f3bbc1209ef572a259027c49e5ff694fa56bed62959b41e1f9"}, - {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e544246b859f17373bed915182ab841b80849ed9cf23f1f07b73b7c58baee5fb"}, - {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c0877239307b7e69d025b73774e88e86ce82f6ba6adf98f41069d5b0b78bd1bf"}, - {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:53df009d1e1ba40f696f8995683e067e3967101d4bb4ea6f667931b7d4a01357"}, - {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a1254357f7e4c82e77c348dabf2d55f1d14d19d91ff025004775e70a6ef40ada"}, - {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:524ff0ca3baea164d6d93a32c58ac79eca9f6cf713586fdc0adb66a8cdeab96a"}, - {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f0ac9fb8608dbc6eaf17956bf623c9119b4db7dbb511650910a82e261e6600f"}, - {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:320f14bd4542a04ab23747ff2c8a778bde727158b606e2661349557f0770711e"}, - {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:63974d168b6233b4ed6a0046296803cb13c56637a7b8106564ab575926572a55"}, - {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:417243bf599ba1f1fef2bb8c543ceb918676954734e2dcb82bf162ae9d7bd514"}, - {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:dda81e5ec82485155a19d9624cfcca9be88a405e2857354e5b089c2a982144b2"}, - {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:14cfbb00959259e15d684505263d5a21732b31248a5dd4941f73a3be233865b9"}, - {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:631cb7415225954fdcc2a024119101946793e5923f6c4d73a5914d27eb3d3a05"}, - {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:bec7dd208a4182e99c5b6c501ce0b1f49de2802448d4056091f8e630b28e9a52"}, - {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:149b8a07712f45b332faee1a2258d8ef1fb4a36f88c0c17cb687f205c5dc6e7d"}, - {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d966c47f9dd73c2d32a809d2be529112d509321c5310ebf54076812e6ecd884"}, - {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7eb037106f5c6b3b0b864ad226b0b7ab58157124161d48e4b30c4a43fef8bc4b"}, - {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:154ea7c52e32dce13065dbb20a4a6f0cc012b4f667ac90d648d36b12007fa9f7"}, - {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e562617a45b5a9da5be4abe72b971d4f00bf8555eb29bb91ec2ef2be348cd132"}, - {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:f23b55eb5464468f9e0e9a9935ce3ed2a870608d5f534025cd5536bca25b1402"}, - {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:e9121b4009339b0f751955baf4543a0bfd6bc3f8188f8056b1a25a2d45099934"}, - {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:0523aeb76e03f753b58be33b26540880bac5aa54422e4462404c432230543f33"}, - {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e0e2959ef5d5b8dc9ef21e1a305a21a36e254e6a34432d00c72a92fdc5ecda5"}, - {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da01bec0a26befab4898ed83b362993c844b9a607a86add78604186297eb047e"}, - {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f2e9072d71c1f6cfc79a36d4484c82823c560e6f5599c43c1ca6b5cdbd54f881"}, - {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f36a3489d9e28fe4b67be9992a23029c3cec0babc3bd9afb39f49844a8c721c5"}, - {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f64f82cc3443149292b32387086d02a6c7fb39b8781563e0ca7b8d7d9cf72bd7"}, - {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b4a6db486ac8e99ae696e09efc8b2b9fea67b63c8f88ba7a1a16c24a057a0776"}, - {file = "pydantic_core-2.10.1.tar.gz", hash = "sha256:0f8682dbdd2f67f8e1edddcbffcc29f60a6182b4901c367fc8c1c40d30bb0a82"}, -] - -[package.dependencies] -typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" - -[[package]] -name = "pydantic-settings" -version = "2.0.3" -description = "Settings management using Pydantic" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pydantic_settings-2.0.3-py3-none-any.whl", hash = "sha256:ddd907b066622bd67603b75e2ff791875540dc485b7307c4fffc015719da8625"}, - {file = "pydantic_settings-2.0.3.tar.gz", hash = "sha256:962dc3672495aad6ae96a4390fac7e593591e144625e5112d359f8f67fb75945"}, -] - -[package.dependencies] -pydantic = ">=2.0.1" -python-dotenv = ">=0.21.0" - -[[package]] -name = "pyflakes" -version = "3.1.0" -description = "passive checker of Python programs" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pyflakes-3.1.0-py2.py3-none-any.whl", hash = "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774"}, - {file = "pyflakes-3.1.0.tar.gz", hash = "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"}, -] - -[[package]] -name = "pygments" -version = "2.16.1" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.7" -files = [ - {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, - {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, -] - -[package.extras] -plugins = ["importlib-metadata"] - -[[package]] -name = "pymdown-extensions" -version = "10.3.1" -description = "Extension pack for Python Markdown." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pymdown_extensions-10.3.1-py3-none-any.whl", hash = "sha256:8cba67beb2a1318cdaf742d09dff7c0fc4cafcc290147ade0f8fb7b71522711a"}, - {file = "pymdown_extensions-10.3.1.tar.gz", hash = "sha256:f6c79941498a458852853872e379e7bab63888361ba20992fc8b4f8a9b61735e"}, -] - -[package.dependencies] -markdown = ">=3.2" -pyyaml = "*" - -[package.extras] -extra = ["pygments (>=2.12)"] - -[[package]] -name = "pytest" -version = "7.4.3" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, - {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=0.12,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} - -[package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "python-dateutil" -version = "2.8.2" -description = "Extensions to the standard Python datetime module" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -files = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, -] - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "python-dotenv" -version = "1.0.0" -description = "Read key-value pairs from a .env file and set them as environment variables" -optional = false -python-versions = ">=3.8" -files = [ - {file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"}, - {file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"}, -] - -[package.extras] -cli = ["click (>=5.0)"] - -[[package]] -name = "python-json-logger" -version = "2.0.7" -description = "A python library adding a json log formatter" -optional = true -python-versions = ">=3.6" -files = [ - {file = "python-json-logger-2.0.7.tar.gz", hash = "sha256:23e7ec02d34237c5aa1e29a070193a4ea87583bb4e7f8fd06d3de8264c4b2e1c"}, - {file = "python_json_logger-2.0.7-py3-none-any.whl", hash = "sha256:f380b826a991ebbe3de4d897aeec42760035ac760345e57b812938dc8b35e2bd"}, -] - -[[package]] -name = "pywin32" -version = "306" -description = "Python for Window Extensions" -optional = true -python-versions = "*" -files = [ - {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, - {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, - {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, - {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, - {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, - {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, - {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, - {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, - {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, - {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, - {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, - {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, - {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, - {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, -] - -[[package]] -name = "pywinpty" -version = "2.0.12" -description = "Pseudo terminal support for Windows from Python." -optional = true -python-versions = ">=3.8" -files = [ - {file = "pywinpty-2.0.12-cp310-none-win_amd64.whl", hash = "sha256:21319cd1d7c8844fb2c970fb3a55a3db5543f112ff9cfcd623746b9c47501575"}, - {file = "pywinpty-2.0.12-cp311-none-win_amd64.whl", hash = "sha256:853985a8f48f4731a716653170cd735da36ffbdc79dcb4c7b7140bce11d8c722"}, - {file = "pywinpty-2.0.12-cp312-none-win_amd64.whl", hash = "sha256:1617b729999eb6713590e17665052b1a6ae0ad76ee31e60b444147c5b6a35dca"}, - {file = "pywinpty-2.0.12-cp38-none-win_amd64.whl", hash = "sha256:189380469ca143d06e19e19ff3fba0fcefe8b4a8cc942140a6b863aed7eebb2d"}, - {file = "pywinpty-2.0.12-cp39-none-win_amd64.whl", hash = "sha256:7520575b6546db23e693cbd865db2764097bd6d4ef5dc18c92555904cd62c3d4"}, - {file = "pywinpty-2.0.12.tar.gz", hash = "sha256:8197de460ae8ebb7f5d1701dfa1b5df45b157bb832e92acba316305e18ca00dd"}, -] - -[[package]] -name = "pyyaml" -version = "6.0.1" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.6" -files = [ - {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, - {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, - {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, - {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, - {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, - {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, - {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, - {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, - {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, -] - -[[package]] -name = "pyyaml-env-tag" -version = "0.1" -description = "A custom YAML tag for referencing environment variables in YAML files. " -optional = false -python-versions = ">=3.6" -files = [ - {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, - {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, -] - -[package.dependencies] -pyyaml = "*" - -[[package]] -name = "pyzmq" -version = "24.0.1" -description = "Python bindings for 0MQ" -optional = true -python-versions = ">=3.6" -files = [ - {file = "pyzmq-24.0.1-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:28b119ba97129d3001673a697b7cce47fe6de1f7255d104c2f01108a5179a066"}, - {file = "pyzmq-24.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bcbebd369493d68162cddb74a9c1fcebd139dfbb7ddb23d8f8e43e6c87bac3a6"}, - {file = "pyzmq-24.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae61446166983c663cee42c852ed63899e43e484abf080089f771df4b9d272ef"}, - {file = "pyzmq-24.0.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87f7ac99b15270db8d53f28c3c7b968612993a90a5cf359da354efe96f5372b4"}, - {file = "pyzmq-24.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dca7c3956b03b7663fac4d150f5e6d4f6f38b2462c1e9afd83bcf7019f17913"}, - {file = "pyzmq-24.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8c78bfe20d4c890cb5580a3b9290f700c570e167d4cdcc55feec07030297a5e3"}, - {file = "pyzmq-24.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:48f721f070726cd2a6e44f3c33f8ee4b24188e4b816e6dd8ba542c8c3bb5b246"}, - {file = "pyzmq-24.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:afe1f3bc486d0ce40abb0a0c9adb39aed3bbac36ebdc596487b0cceba55c21c1"}, - {file = "pyzmq-24.0.1-cp310-cp310-win32.whl", hash = "sha256:3e6192dbcefaaa52ed81be88525a54a445f4b4fe2fffcae7fe40ebb58bd06bfd"}, - {file = "pyzmq-24.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:86de64468cad9c6d269f32a6390e210ca5ada568c7a55de8e681ca3b897bb340"}, - {file = "pyzmq-24.0.1-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:838812c65ed5f7c2bd11f7b098d2e5d01685a3f6d1f82849423b570bae698c00"}, - {file = "pyzmq-24.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dfb992dbcd88d8254471760879d48fb20836d91baa90f181c957122f9592b3dc"}, - {file = "pyzmq-24.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7abddb2bd5489d30ffeb4b93a428130886c171b4d355ccd226e83254fcb6b9ef"}, - {file = "pyzmq-24.0.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94010bd61bc168c103a5b3b0f56ed3b616688192db7cd5b1d626e49f28ff51b3"}, - {file = "pyzmq-24.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8242543c522d84d033fe79be04cb559b80d7eb98ad81b137ff7e0a9020f00ace"}, - {file = "pyzmq-24.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ccb94342d13e3bf3ffa6e62f95b5e3f0bc6bfa94558cb37f4b3d09d6feb536ff"}, - {file = "pyzmq-24.0.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6640f83df0ae4ae1104d4c62b77e9ef39be85ebe53f636388707d532bee2b7b8"}, - {file = "pyzmq-24.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a180dbd5ea5d47c2d3b716d5c19cc3fb162d1c8db93b21a1295d69585bfddac1"}, - {file = "pyzmq-24.0.1-cp311-cp311-win32.whl", hash = "sha256:624321120f7e60336be8ec74a172ae7fba5c3ed5bf787cc85f7e9986c9e0ebc2"}, - {file = "pyzmq-24.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:1724117bae69e091309ffb8255412c4651d3f6355560d9af312d547f6c5bc8b8"}, - {file = "pyzmq-24.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:15975747462ec49fdc863af906bab87c43b2491403ab37a6d88410635786b0f4"}, - {file = "pyzmq-24.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b947e264f0e77d30dcbccbb00f49f900b204b922eb0c3a9f0afd61aaa1cedc3d"}, - {file = "pyzmq-24.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0ec91f1bad66f3ee8c6deb65fa1fe418e8ad803efedd69c35f3b5502f43bd1dc"}, - {file = "pyzmq-24.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:db03704b3506455d86ec72c3358a779e9b1d07b61220dfb43702b7b668edcd0d"}, - {file = "pyzmq-24.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:e7e66b4e403c2836ac74f26c4b65d8ac0ca1eef41dfcac2d013b7482befaad83"}, - {file = "pyzmq-24.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:7a23ccc1083c260fa9685c93e3b170baba45aeed4b524deb3f426b0c40c11639"}, - {file = "pyzmq-24.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:fa0ae3275ef706c0309556061185dd0e4c4cd3b7d6f67ae617e4e677c7a41e2e"}, - {file = "pyzmq-24.0.1-cp36-cp36m-win32.whl", hash = "sha256:f01de4ec083daebf210531e2cca3bdb1608dbbbe00a9723e261d92087a1f6ebc"}, - {file = "pyzmq-24.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:de4217b9eb8b541cf2b7fde4401ce9d9a411cc0af85d410f9d6f4333f43640be"}, - {file = "pyzmq-24.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:78068e8678ca023594e4a0ab558905c1033b2d3e806a0ad9e3094e231e115a33"}, - {file = "pyzmq-24.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77c2713faf25a953c69cf0f723d1b7dd83827b0834e6c41e3fb3bbc6765914a1"}, - {file = "pyzmq-24.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bb4af15f305056e95ca1bd086239b9ebc6ad55e9f49076d27d80027f72752f6"}, - {file = "pyzmq-24.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0f14cffd32e9c4c73da66db97853a6aeceaac34acdc0fae9e5bbc9370281864c"}, - {file = "pyzmq-24.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0108358dab8c6b27ff6b985c2af4b12665c1bc659648284153ee501000f5c107"}, - {file = "pyzmq-24.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d66689e840e75221b0b290b0befa86f059fb35e1ee6443bce51516d4d61b6b99"}, - {file = "pyzmq-24.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ae08ac90aa8fa14caafc7a6251bd218bf6dac518b7bff09caaa5e781119ba3f2"}, - {file = "pyzmq-24.0.1-cp37-cp37m-win32.whl", hash = "sha256:8421aa8c9b45ea608c205db9e1c0c855c7e54d0e9c2c2f337ce024f6843cab3b"}, - {file = "pyzmq-24.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:54d8b9c5e288362ec8595c1d98666d36f2070fd0c2f76e2b3c60fbad9bd76227"}, - {file = "pyzmq-24.0.1-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:acbd0a6d61cc954b9f535daaa9ec26b0a60a0d4353c5f7c1438ebc88a359a47e"}, - {file = "pyzmq-24.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:47b11a729d61a47df56346283a4a800fa379ae6a85870d5a2e1e4956c828eedc"}, - {file = "pyzmq-24.0.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:abe6eb10122f0d746a0d510c2039ae8edb27bc9af29f6d1b05a66cc2401353ff"}, - {file = "pyzmq-24.0.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:07bec1a1b22dacf718f2c0e71b49600bb6a31a88f06527dfd0b5aababe3fa3f7"}, - {file = "pyzmq-24.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0d945a85b70da97ae86113faf9f1b9294efe66bd4a5d6f82f2676d567338b66"}, - {file = "pyzmq-24.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1b7928bb7580736ffac5baf814097be342ba08d3cfdfb48e52773ec959572287"}, - {file = "pyzmq-24.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b946da90dc2799bcafa682692c1d2139b2a96ec3c24fa9fc6f5b0da782675330"}, - {file = "pyzmq-24.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c8840f064b1fb377cffd3efeaad2b190c14d4c8da02316dae07571252d20b31f"}, - {file = "pyzmq-24.0.1-cp38-cp38-win32.whl", hash = "sha256:4854f9edc5208f63f0841c0c667260ae8d6846cfa233c479e29fdc85d42ebd58"}, - {file = "pyzmq-24.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:42d4f97b9795a7aafa152a36fe2ad44549b83a743fd3e77011136def512e6c2a"}, - {file = "pyzmq-24.0.1-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:52afb0ac962963fff30cf1be775bc51ae083ef4c1e354266ab20e5382057dd62"}, - {file = "pyzmq-24.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bad8210ad4df68c44ff3685cca3cda448ee46e20d13edcff8909eba6ec01ca4"}, - {file = "pyzmq-24.0.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:dabf1a05318d95b1537fd61d9330ef4313ea1216eea128a17615038859da3b3b"}, - {file = "pyzmq-24.0.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5bd3d7dfd9cd058eb68d9a905dec854f86649f64d4ddf21f3ec289341386c44b"}, - {file = "pyzmq-24.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8012bce6836d3f20a6c9599f81dfa945f433dab4dbd0c4917a6fb1f998ab33d"}, - {file = "pyzmq-24.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c31805d2c8ade9b11feca4674eee2b9cce1fec3e8ddb7bbdd961a09dc76a80ea"}, - {file = "pyzmq-24.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:3104f4b084ad5d9c0cb87445cc8cfd96bba710bef4a66c2674910127044df209"}, - {file = "pyzmq-24.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:df0841f94928f8af9c7a1f0aaaffba1fb74607af023a152f59379c01c53aee58"}, - {file = "pyzmq-24.0.1-cp39-cp39-win32.whl", hash = "sha256:a435ef8a3bd95c8a2d316d6e0ff70d0db524f6037411652803e118871d703333"}, - {file = "pyzmq-24.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:2032d9cb994ce3b4cba2b8dfae08c7e25bc14ba484c770d4d3be33c27de8c45b"}, - {file = "pyzmq-24.0.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bb5635c851eef3a7a54becde6da99485eecf7d068bd885ac8e6d173c4ecd68b0"}, - {file = "pyzmq-24.0.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:83ea1a398f192957cb986d9206ce229efe0ee75e3c6635baff53ddf39bd718d5"}, - {file = "pyzmq-24.0.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:941fab0073f0a54dc33d1a0460cb04e0d85893cb0c5e1476c785000f8b359409"}, - {file = "pyzmq-24.0.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e8f482c44ccb5884bf3f638f29bea0f8dc68c97e38b2061769c4cb697f6140d"}, - {file = "pyzmq-24.0.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:613010b5d17906c4367609e6f52e9a2595e35d5cc27d36ff3f1b6fa6e954d944"}, - {file = "pyzmq-24.0.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:65c94410b5a8355cfcf12fd600a313efee46ce96a09e911ea92cf2acf6708804"}, - {file = "pyzmq-24.0.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:20e7eeb1166087db636c06cae04a1ef59298627f56fb17da10528ab52a14c87f"}, - {file = "pyzmq-24.0.1-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a2712aee7b3834ace51738c15d9ee152cc5a98dc7d57dd93300461b792ab7b43"}, - {file = "pyzmq-24.0.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a7c280185c4da99e0cc06c63bdf91f5b0b71deb70d8717f0ab870a43e376db8"}, - {file = "pyzmq-24.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:858375573c9225cc8e5b49bfac846a77b696b8d5e815711b8d4ba3141e6e8879"}, - {file = "pyzmq-24.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:80093b595921eed1a2cead546a683b9e2ae7f4a4592bb2ab22f70d30174f003a"}, - {file = "pyzmq-24.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f3f3154fde2b1ff3aa7b4f9326347ebc89c8ef425ca1db8f665175e6d3bd42f"}, - {file = "pyzmq-24.0.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abb756147314430bee5d10919b8493c0ccb109ddb7f5dfd2fcd7441266a25b75"}, - {file = "pyzmq-24.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44e706bac34e9f50779cb8c39f10b53a4d15aebb97235643d3112ac20bd577b4"}, - {file = "pyzmq-24.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:687700f8371643916a1d2c61f3fdaa630407dd205c38afff936545d7b7466066"}, - {file = "pyzmq-24.0.1.tar.gz", hash = "sha256:216f5d7dbb67166759e59b0479bca82b8acf9bed6015b526b8eb10143fb08e77"}, -] - -[package.dependencies] -cffi = {version = "*", markers = "implementation_name == \"pypy\""} -py = {version = "*", markers = "implementation_name == \"pypy\""} - -[[package]] -name = "referencing" -version = "0.30.2" -description = "JSON Referencing + Python" -optional = true -python-versions = ">=3.8" -files = [ - {file = "referencing-0.30.2-py3-none-any.whl", hash = "sha256:449b6669b6121a9e96a7f9e410b245d471e8d48964c67113ce9afe50c8dd7bdf"}, - {file = "referencing-0.30.2.tar.gz", hash = "sha256:794ad8003c65938edcdbc027f1933215e0d0ccc0291e3ce20a4d87432b59efc0"}, -] - -[package.dependencies] -attrs = ">=22.2.0" -rpds-py = ">=0.7.0" - -[[package]] -name = "regex" -version = "2023.10.3" -description = "Alternative regular expression module, to replace re." -optional = false -python-versions = ">=3.7" -files = [ - {file = "regex-2023.10.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4c34d4f73ea738223a094d8e0ffd6d2c1a1b4c175da34d6b0de3d8d69bee6bcc"}, - {file = "regex-2023.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8f4e49fc3ce020f65411432183e6775f24e02dff617281094ba6ab079ef0915"}, - {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cd1bccf99d3ef1ab6ba835308ad85be040e6a11b0977ef7ea8c8005f01a3c29"}, - {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:81dce2ddc9f6e8f543d94b05d56e70d03a0774d32f6cca53e978dc01e4fc75b8"}, - {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c6b4d23c04831e3ab61717a707a5d763b300213db49ca680edf8bf13ab5d91b"}, - {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c15ad0aee158a15e17e0495e1e18741573d04eb6da06d8b84af726cfc1ed02ee"}, - {file = "regex-2023.10.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6239d4e2e0b52c8bd38c51b760cd870069f0bdf99700a62cd509d7a031749a55"}, - {file = "regex-2023.10.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4a8bf76e3182797c6b1afa5b822d1d5802ff30284abe4599e1247be4fd6b03be"}, - {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9c727bbcf0065cbb20f39d2b4f932f8fa1631c3e01fcedc979bd4f51fe051c5"}, - {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3ccf2716add72f80714b9a63899b67fa711b654be3fcdd34fa391d2d274ce767"}, - {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:107ac60d1bfdc3edb53be75e2a52aff7481b92817cfdddd9b4519ccf0e54a6ff"}, - {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:00ba3c9818e33f1fa974693fb55d24cdc8ebafcb2e4207680669d8f8d7cca79a"}, - {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f0a47efb1dbef13af9c9a54a94a0b814902e547b7f21acb29434504d18f36e3a"}, - {file = "regex-2023.10.3-cp310-cp310-win32.whl", hash = "sha256:36362386b813fa6c9146da6149a001b7bd063dabc4d49522a1f7aa65b725c7ec"}, - {file = "regex-2023.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:c65a3b5330b54103e7d21cac3f6bf3900d46f6d50138d73343d9e5b2900b2353"}, - {file = "regex-2023.10.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90a79bce019c442604662d17bf69df99090e24cdc6ad95b18b6725c2988a490e"}, - {file = "regex-2023.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c7964c2183c3e6cce3f497e3a9f49d182e969f2dc3aeeadfa18945ff7bdd7051"}, - {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ef80829117a8061f974b2fda8ec799717242353bff55f8a29411794d635d964"}, - {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5addc9d0209a9afca5fc070f93b726bf7003bd63a427f65ef797a931782e7edc"}, - {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c148bec483cc4b421562b4bcedb8e28a3b84fcc8f0aa4418e10898f3c2c0eb9b"}, - {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d1f21af4c1539051049796a0f50aa342f9a27cde57318f2fc41ed50b0dbc4ac"}, - {file = "regex-2023.10.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b9ac09853b2a3e0d0082104036579809679e7715671cfbf89d83c1cb2a30f58"}, - {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ebedc192abbc7fd13c5ee800e83a6df252bec691eb2c4bedc9f8b2e2903f5e2a"}, - {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d8a993c0a0ffd5f2d3bda23d0cd75e7086736f8f8268de8a82fbc4bd0ac6791e"}, - {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:be6b7b8d42d3090b6c80793524fa66c57ad7ee3fe9722b258aec6d0672543fd0"}, - {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4023e2efc35a30e66e938de5aef42b520c20e7eda7bb5fb12c35e5d09a4c43f6"}, - {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d47840dc05e0ba04fe2e26f15126de7c755496d5a8aae4a08bda4dd8d646c54"}, - {file = "regex-2023.10.3-cp311-cp311-win32.whl", hash = "sha256:9145f092b5d1977ec8c0ab46e7b3381b2fd069957b9862a43bd383e5c01d18c2"}, - {file = "regex-2023.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:b6104f9a46bd8743e4f738afef69b153c4b8b592d35ae46db07fc28ae3d5fb7c"}, - {file = "regex-2023.10.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bff507ae210371d4b1fe316d03433ac099f184d570a1a611e541923f78f05037"}, - {file = "regex-2023.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:be5e22bbb67924dea15039c3282fa4cc6cdfbe0cbbd1c0515f9223186fc2ec5f"}, - {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a992f702c9be9c72fa46f01ca6e18d131906a7180950958f766c2aa294d4b41"}, - {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7434a61b158be563c1362d9071358f8ab91b8d928728cd2882af060481244c9e"}, - {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2169b2dcabf4e608416f7f9468737583ce5f0a6e8677c4efbf795ce81109d7c"}, - {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9e908ef5889cda4de038892b9accc36d33d72fb3e12c747e2799a0e806ec841"}, - {file = "regex-2023.10.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12bd4bc2c632742c7ce20db48e0d99afdc05e03f0b4c1af90542e05b809a03d9"}, - {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bc72c231f5449d86d6c7d9cc7cd819b6eb30134bb770b8cfdc0765e48ef9c420"}, - {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bce8814b076f0ce5766dc87d5a056b0e9437b8e0cd351b9a6c4e1134a7dfbda9"}, - {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:ba7cd6dc4d585ea544c1412019921570ebd8a597fabf475acc4528210d7c4a6f"}, - {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b0c7d2f698e83f15228ba41c135501cfe7d5740181d5903e250e47f617eb4292"}, - {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5a8f91c64f390ecee09ff793319f30a0f32492e99f5dc1c72bc361f23ccd0a9a"}, - {file = "regex-2023.10.3-cp312-cp312-win32.whl", hash = "sha256:ad08a69728ff3c79866d729b095872afe1e0557251da4abb2c5faff15a91d19a"}, - {file = "regex-2023.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:39cdf8d141d6d44e8d5a12a8569d5a227f645c87df4f92179bd06e2e2705e76b"}, - {file = "regex-2023.10.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4a3ee019a9befe84fa3e917a2dd378807e423d013377a884c1970a3c2792d293"}, - {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76066d7ff61ba6bf3cb5efe2428fc82aac91802844c022d849a1f0f53820502d"}, - {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe50b61bab1b1ec260fa7cd91106fa9fece57e6beba05630afe27c71259c59b"}, - {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fd88f373cb71e6b59b7fa597e47e518282455c2734fd4306a05ca219a1991b0"}, - {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ab05a182c7937fb374f7e946f04fb23a0c0699c0450e9fb02ef567412d2fa3"}, - {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dac37cf08fcf2094159922edc7a2784cfcc5c70f8354469f79ed085f0328ebdf"}, - {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e54ddd0bb8fb626aa1f9ba7b36629564544954fff9669b15da3610c22b9a0991"}, - {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3367007ad1951fde612bf65b0dffc8fd681a4ab98ac86957d16491400d661302"}, - {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:16f8740eb6dbacc7113e3097b0a36065a02e37b47c936b551805d40340fb9971"}, - {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:f4f2ca6df64cbdd27f27b34f35adb640b5d2d77264228554e68deda54456eb11"}, - {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:39807cbcbe406efca2a233884e169d056c35aa7e9f343d4e78665246a332f597"}, - {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7eece6fbd3eae4a92d7c748ae825cbc1ee41a89bb1c3db05b5578ed3cfcfd7cb"}, - {file = "regex-2023.10.3-cp37-cp37m-win32.whl", hash = "sha256:ce615c92d90df8373d9e13acddd154152645c0dc060871abf6bd43809673d20a"}, - {file = "regex-2023.10.3-cp37-cp37m-win_amd64.whl", hash = "sha256:0f649fa32fe734c4abdfd4edbb8381c74abf5f34bc0b3271ce687b23729299ed"}, - {file = "regex-2023.10.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9b98b7681a9437262947f41c7fac567c7e1f6eddd94b0483596d320092004533"}, - {file = "regex-2023.10.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:91dc1d531f80c862441d7b66c4505cd6ea9d312f01fb2f4654f40c6fdf5cc37a"}, - {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82fcc1f1cc3ff1ab8a57ba619b149b907072e750815c5ba63e7aa2e1163384a4"}, - {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7979b834ec7a33aafae34a90aad9f914c41fd6eaa8474e66953f3f6f7cbd4368"}, - {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef71561f82a89af6cfcbee47f0fabfdb6e63788a9258e913955d89fdd96902ab"}, - {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd829712de97753367153ed84f2de752b86cd1f7a88b55a3a775eb52eafe8a94"}, - {file = "regex-2023.10.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00e871d83a45eee2f8688d7e6849609c2ca2a04a6d48fba3dff4deef35d14f07"}, - {file = "regex-2023.10.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:706e7b739fdd17cb89e1fbf712d9dc21311fc2333f6d435eac2d4ee81985098c"}, - {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cc3f1c053b73f20c7ad88b0d1d23be7e7b3901229ce89f5000a8399746a6e039"}, - {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6f85739e80d13644b981a88f529d79c5bdf646b460ba190bffcaf6d57b2a9863"}, - {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:741ba2f511cc9626b7561a440f87d658aabb3d6b744a86a3c025f866b4d19e7f"}, - {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e77c90ab5997e85901da85131fd36acd0ed2221368199b65f0d11bca44549711"}, - {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:979c24cbefaf2420c4e377ecd1f165ea08cc3d1fbb44bdc51bccbbf7c66a2cb4"}, - {file = "regex-2023.10.3-cp38-cp38-win32.whl", hash = "sha256:58837f9d221744d4c92d2cf7201c6acd19623b50c643b56992cbd2b745485d3d"}, - {file = "regex-2023.10.3-cp38-cp38-win_amd64.whl", hash = "sha256:c55853684fe08d4897c37dfc5faeff70607a5f1806c8be148f1695be4a63414b"}, - {file = "regex-2023.10.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2c54e23836650bdf2c18222c87f6f840d4943944146ca479858404fedeb9f9af"}, - {file = "regex-2023.10.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:69c0771ca5653c7d4b65203cbfc5e66db9375f1078689459fe196fe08b7b4930"}, - {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ac965a998e1388e6ff2e9781f499ad1eaa41e962a40d11c7823c9952c77123e"}, - {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c0e8fae5b27caa34177bdfa5a960c46ff2f78ee2d45c6db15ae3f64ecadde14"}, - {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6c56c3d47da04f921b73ff9415fbaa939f684d47293f071aa9cbb13c94afc17d"}, - {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ef1e014eed78ab650bef9a6a9cbe50b052c0aebe553fb2881e0453717573f52"}, - {file = "regex-2023.10.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d29338556a59423d9ff7b6eb0cb89ead2b0875e08fe522f3e068b955c3e7b59b"}, - {file = "regex-2023.10.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9c6d0ced3c06d0f183b73d3c5920727268d2201aa0fe6d55c60d68c792ff3588"}, - {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:994645a46c6a740ee8ce8df7911d4aee458d9b1bc5639bc968226763d07f00fa"}, - {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:66e2fe786ef28da2b28e222c89502b2af984858091675044d93cb50e6f46d7af"}, - {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:11175910f62b2b8c055f2b089e0fedd694fe2be3941b3e2633653bc51064c528"}, - {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:06e9abc0e4c9ab4779c74ad99c3fc10d3967d03114449acc2c2762ad4472b8ca"}, - {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fb02e4257376ae25c6dd95a5aec377f9b18c09be6ebdefa7ad209b9137b73d48"}, - {file = "regex-2023.10.3-cp39-cp39-win32.whl", hash = "sha256:3b2c3502603fab52d7619b882c25a6850b766ebd1b18de3df23b2f939360e1bd"}, - {file = "regex-2023.10.3-cp39-cp39-win_amd64.whl", hash = "sha256:adbccd17dcaff65704c856bd29951c58a1bd4b2b0f8ad6b826dbd543fe740988"}, - {file = "regex-2023.10.3.tar.gz", hash = "sha256:3fef4f844d2290ee0ba57addcec17eec9e3df73f10a2748485dfd6a3a188cc0f"}, -] - -[[package]] -name = "requests" -version = "2.31.0" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.7" -files = [ - {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, - {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "rfc3339-validator" -version = "0.1.4" -description = "A pure python RFC3339 validator" -optional = true -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa"}, - {file = "rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b"}, -] - -[package.dependencies] -six = "*" - -[[package]] -name = "rfc3986-validator" -version = "0.1.1" -description = "Pure python rfc3986 validator" -optional = true -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9"}, - {file = "rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055"}, -] - -[[package]] -name = "rich" -version = "13.6.0" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "rich-13.6.0-py3-none-any.whl", hash = "sha256:2b38e2fe9ca72c9a00170a1a2d20c63c790d0e10ef1fe35eba76e1e7b1d7d245"}, - {file = "rich-13.6.0.tar.gz", hash = "sha256:5c14d22737e6d5084ef4771b62d5d4363165b403455a30a1c8ca39dc7b644bef"}, -] - -[package.dependencies] -markdown-it-py = ">=2.2.0" -pygments = ">=2.13.0,<3.0.0" - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] - -[[package]] -name = "rpds-py" -version = "0.10.6" -description = "Python bindings to Rust's persistent data structures (rpds)" -optional = true -python-versions = ">=3.8" -files = [ - {file = "rpds_py-0.10.6-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:6bdc11f9623870d75692cc33c59804b5a18d7b8a4b79ef0b00b773a27397d1f6"}, - {file = "rpds_py-0.10.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:26857f0f44f0e791f4a266595a7a09d21f6b589580ee0585f330aaccccb836e3"}, - {file = "rpds_py-0.10.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7f5e15c953ace2e8dde9824bdab4bec50adb91a5663df08d7d994240ae6fa31"}, - {file = "rpds_py-0.10.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61fa268da6e2e1cd350739bb61011121fa550aa2545762e3dc02ea177ee4de35"}, - {file = "rpds_py-0.10.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c48f3fbc3e92c7dd6681a258d22f23adc2eb183c8cb1557d2fcc5a024e80b094"}, - {file = "rpds_py-0.10.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0503c5b681566e8b722fe8c4c47cce5c7a51f6935d5c7012c4aefe952a35eed"}, - {file = "rpds_py-0.10.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:734c41f9f57cc28658d98270d3436dba65bed0cfc730d115b290e970150c540d"}, - {file = "rpds_py-0.10.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a5d7ed104d158c0042a6a73799cf0eb576dfd5fc1ace9c47996e52320c37cb7c"}, - {file = "rpds_py-0.10.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e3df0bc35e746cce42579826b89579d13fd27c3d5319a6afca9893a9b784ff1b"}, - {file = "rpds_py-0.10.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:73e0a78a9b843b8c2128028864901f55190401ba38aae685350cf69b98d9f7c9"}, - {file = "rpds_py-0.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5ed505ec6305abd2c2c9586a7b04fbd4baf42d4d684a9c12ec6110deefe2a063"}, - {file = "rpds_py-0.10.6-cp310-none-win32.whl", hash = "sha256:d97dd44683802000277bbf142fd9f6b271746b4846d0acaf0cefa6b2eaf2a7ad"}, - {file = "rpds_py-0.10.6-cp310-none-win_amd64.whl", hash = "sha256:b455492cab07107bfe8711e20cd920cc96003e0da3c1f91297235b1603d2aca7"}, - {file = "rpds_py-0.10.6-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:e8cdd52744f680346ff8c1ecdad5f4d11117e1724d4f4e1874f3a67598821069"}, - {file = "rpds_py-0.10.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66414dafe4326bca200e165c2e789976cab2587ec71beb80f59f4796b786a238"}, - {file = "rpds_py-0.10.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc435d059f926fdc5b05822b1be4ff2a3a040f3ae0a7bbbe672babb468944722"}, - {file = "rpds_py-0.10.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8e7f2219cb72474571974d29a191714d822e58be1eb171f229732bc6fdedf0ac"}, - {file = "rpds_py-0.10.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3953c6926a63f8ea5514644b7afb42659b505ece4183fdaaa8f61d978754349e"}, - {file = "rpds_py-0.10.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2bb2e4826be25e72013916eecd3d30f66fd076110de09f0e750163b416500721"}, - {file = "rpds_py-0.10.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bf347b495b197992efc81a7408e9a83b931b2f056728529956a4d0858608b80"}, - {file = "rpds_py-0.10.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:102eac53bb0bf0f9a275b438e6cf6904904908562a1463a6fc3323cf47d7a532"}, - {file = "rpds_py-0.10.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40f93086eef235623aa14dbddef1b9fb4b22b99454cb39a8d2e04c994fb9868c"}, - {file = "rpds_py-0.10.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e22260a4741a0e7a206e175232867b48a16e0401ef5bce3c67ca5b9705879066"}, - {file = "rpds_py-0.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f4e56860a5af16a0fcfa070a0a20c42fbb2012eed1eb5ceeddcc7f8079214281"}, - {file = "rpds_py-0.10.6-cp311-none-win32.whl", hash = "sha256:0774a46b38e70fdde0c6ded8d6d73115a7c39d7839a164cc833f170bbf539116"}, - {file = "rpds_py-0.10.6-cp311-none-win_amd64.whl", hash = "sha256:4a5ee600477b918ab345209eddafde9f91c0acd931f3776369585a1c55b04c57"}, - {file = "rpds_py-0.10.6-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:5ee97c683eaface61d38ec9a489e353d36444cdebb128a27fe486a291647aff6"}, - {file = "rpds_py-0.10.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0713631d6e2d6c316c2f7b9320a34f44abb644fc487b77161d1724d883662e31"}, - {file = "rpds_py-0.10.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5a53f5998b4bbff1cb2e967e66ab2addc67326a274567697379dd1e326bded7"}, - {file = "rpds_py-0.10.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6a555ae3d2e61118a9d3e549737bb4a56ff0cec88a22bd1dfcad5b4e04759175"}, - {file = "rpds_py-0.10.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:945eb4b6bb8144909b203a88a35e0a03d22b57aefb06c9b26c6e16d72e5eb0f0"}, - {file = "rpds_py-0.10.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:52c215eb46307c25f9fd2771cac8135d14b11a92ae48d17968eda5aa9aaf5071"}, - {file = "rpds_py-0.10.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1b3cd23d905589cb205710b3988fc8f46d4a198cf12862887b09d7aaa6bf9b9"}, - {file = "rpds_py-0.10.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64ccc28683666672d7c166ed465c09cee36e306c156e787acef3c0c62f90da5a"}, - {file = "rpds_py-0.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:516a611a2de12fbea70c78271e558f725c660ce38e0006f75139ba337d56b1f6"}, - {file = "rpds_py-0.10.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9ff93d3aedef11f9c4540cf347f8bb135dd9323a2fc705633d83210d464c579d"}, - {file = "rpds_py-0.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d858532212f0650be12b6042ff4378dc2efbb7792a286bee4489eaa7ba010586"}, - {file = "rpds_py-0.10.6-cp312-none-win32.whl", hash = "sha256:3c4eff26eddac49d52697a98ea01b0246e44ca82ab09354e94aae8823e8bda02"}, - {file = "rpds_py-0.10.6-cp312-none-win_amd64.whl", hash = "sha256:150eec465dbc9cbca943c8e557a21afdcf9bab8aaabf386c44b794c2f94143d2"}, - {file = "rpds_py-0.10.6-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:cf693eb4a08eccc1a1b636e4392322582db2a47470d52e824b25eca7a3977b53"}, - {file = "rpds_py-0.10.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4134aa2342f9b2ab6c33d5c172e40f9ef802c61bb9ca30d21782f6e035ed0043"}, - {file = "rpds_py-0.10.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e782379c2028a3611285a795b89b99a52722946d19fc06f002f8b53e3ea26ea9"}, - {file = "rpds_py-0.10.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f6da6d842195fddc1cd34c3da8a40f6e99e4a113918faa5e60bf132f917c247"}, - {file = "rpds_py-0.10.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4a9fe992887ac68256c930a2011255bae0bf5ec837475bc6f7edd7c8dfa254e"}, - {file = "rpds_py-0.10.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b788276a3c114e9f51e257f2a6f544c32c02dab4aa7a5816b96444e3f9ffc336"}, - {file = "rpds_py-0.10.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:caa1afc70a02645809c744eefb7d6ee8fef7e2fad170ffdeacca267fd2674f13"}, - {file = "rpds_py-0.10.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bddd4f91eede9ca5275e70479ed3656e76c8cdaaa1b354e544cbcf94c6fc8ac4"}, - {file = "rpds_py-0.10.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:775049dfa63fb58293990fc59473e659fcafd953bba1d00fc5f0631a8fd61977"}, - {file = "rpds_py-0.10.6-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:c6c45a2d2b68c51fe3d9352733fe048291e483376c94f7723458cfd7b473136b"}, - {file = "rpds_py-0.10.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0699ab6b8c98df998c3eacf51a3b25864ca93dab157abe358af46dc95ecd9801"}, - {file = "rpds_py-0.10.6-cp38-none-win32.whl", hash = "sha256:ebdab79f42c5961682654b851f3f0fc68e6cc7cd8727c2ac4ffff955154123c1"}, - {file = "rpds_py-0.10.6-cp38-none-win_amd64.whl", hash = "sha256:24656dc36f866c33856baa3ab309da0b6a60f37d25d14be916bd3e79d9f3afcf"}, - {file = "rpds_py-0.10.6-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:0898173249141ee99ffcd45e3829abe7bcee47d941af7434ccbf97717df020e5"}, - {file = "rpds_py-0.10.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9e9184fa6c52a74a5521e3e87badbf9692549c0fcced47443585876fcc47e469"}, - {file = "rpds_py-0.10.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5752b761902cd15073a527b51de76bbae63d938dc7c5c4ad1e7d8df10e765138"}, - {file = "rpds_py-0.10.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:99a57006b4ec39dbfb3ed67e5b27192792ffb0553206a107e4aadb39c5004cd5"}, - {file = "rpds_py-0.10.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09586f51a215d17efdb3a5f090d7cbf1633b7f3708f60a044757a5d48a83b393"}, - {file = "rpds_py-0.10.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e225a6a14ecf44499aadea165299092ab0cba918bb9ccd9304eab1138844490b"}, - {file = "rpds_py-0.10.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2039f8d545f20c4e52713eea51a275e62153ee96c8035a32b2abb772b6fc9e5"}, - {file = "rpds_py-0.10.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:34ad87a831940521d462ac11f1774edf867c34172010f5390b2f06b85dcc6014"}, - {file = "rpds_py-0.10.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dcdc88b6b01015da066da3fb76545e8bb9a6880a5ebf89e0f0b2e3ca557b3ab7"}, - {file = "rpds_py-0.10.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:25860ed5c4e7f5e10c496ea78af46ae8d8468e0be745bd233bab9ca99bfd2647"}, - {file = "rpds_py-0.10.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7854a207ef77319ec457c1eb79c361b48807d252d94348305db4f4b62f40f7f3"}, - {file = "rpds_py-0.10.6-cp39-none-win32.whl", hash = "sha256:e6fcc026a3f27c1282c7ed24b7fcac82cdd70a0e84cc848c0841a3ab1e3dea2d"}, - {file = "rpds_py-0.10.6-cp39-none-win_amd64.whl", hash = "sha256:e98c4c07ee4c4b3acf787e91b27688409d918212dfd34c872201273fdd5a0e18"}, - {file = "rpds_py-0.10.6-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:68fe9199184c18d997d2e4293b34327c0009a78599ce703e15cd9a0f47349bba"}, - {file = "rpds_py-0.10.6-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:3339eca941568ed52d9ad0f1b8eb9fe0958fa245381747cecf2e9a78a5539c42"}, - {file = "rpds_py-0.10.6-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a360cfd0881d36c6dc271992ce1eda65dba5e9368575663de993eeb4523d895f"}, - {file = "rpds_py-0.10.6-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:031f76fc87644a234883b51145e43985aa2d0c19b063e91d44379cd2786144f8"}, - {file = "rpds_py-0.10.6-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f36a9d751f86455dc5278517e8b65580eeee37d61606183897f122c9e51cef3"}, - {file = "rpds_py-0.10.6-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:052a832078943d2b2627aea0d19381f607fe331cc0eb5df01991268253af8417"}, - {file = "rpds_py-0.10.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:023574366002bf1bd751ebaf3e580aef4a468b3d3c216d2f3f7e16fdabd885ed"}, - {file = "rpds_py-0.10.6-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:defa2c0c68734f4a82028c26bcc85e6b92cced99866af118cd6a89b734ad8e0d"}, - {file = "rpds_py-0.10.6-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:879fb24304ead6b62dbe5034e7b644b71def53c70e19363f3c3be2705c17a3b4"}, - {file = "rpds_py-0.10.6-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:53c43e10d398e365da2d4cc0bcaf0854b79b4c50ee9689652cdc72948e86f487"}, - {file = "rpds_py-0.10.6-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:3777cc9dea0e6c464e4b24760664bd8831738cc582c1d8aacf1c3f546bef3f65"}, - {file = "rpds_py-0.10.6-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:40578a6469e5d1df71b006936ce95804edb5df47b520c69cf5af264d462f2cbb"}, - {file = "rpds_py-0.10.6-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:cf71343646756a072b85f228d35b1d7407da1669a3de3cf47f8bbafe0c8183a4"}, - {file = "rpds_py-0.10.6-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10f32b53f424fc75ff7b713b2edb286fdbfc94bf16317890260a81c2c00385dc"}, - {file = "rpds_py-0.10.6-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:81de24a1c51cfb32e1fbf018ab0bdbc79c04c035986526f76c33e3f9e0f3356c"}, - {file = "rpds_py-0.10.6-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac17044876e64a8ea20ab132080ddc73b895b4abe9976e263b0e30ee5be7b9c2"}, - {file = "rpds_py-0.10.6-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e8a78bd4879bff82daef48c14d5d4057f6856149094848c3ed0ecaf49f5aec2"}, - {file = "rpds_py-0.10.6-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78ca33811e1d95cac8c2e49cb86c0fb71f4d8409d8cbea0cb495b6dbddb30a55"}, - {file = "rpds_py-0.10.6-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c63c3ef43f0b3fb00571cff6c3967cc261c0ebd14a0a134a12e83bdb8f49f21f"}, - {file = "rpds_py-0.10.6-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:7fde6d0e00b2fd0dbbb40c0eeec463ef147819f23725eda58105ba9ca48744f4"}, - {file = "rpds_py-0.10.6-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:79edd779cfc46b2e15b0830eecd8b4b93f1a96649bcb502453df471a54ce7977"}, - {file = "rpds_py-0.10.6-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:9164ec8010327ab9af931d7ccd12ab8d8b5dc2f4c6a16cbdd9d087861eaaefa1"}, - {file = "rpds_py-0.10.6-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d29ddefeab1791e3c751e0189d5f4b3dbc0bbe033b06e9c333dca1f99e1d523e"}, - {file = "rpds_py-0.10.6-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:30adb75ecd7c2a52f5e76af50644b3e0b5ba036321c390b8e7ec1bb2a16dd43c"}, - {file = "rpds_py-0.10.6-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd609fafdcdde6e67a139898196698af37438b035b25ad63704fd9097d9a3482"}, - {file = "rpds_py-0.10.6-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eef672de005736a6efd565577101277db6057f65640a813de6c2707dc69f396"}, - {file = "rpds_py-0.10.6-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cf4393c7b41abbf07c88eb83e8af5013606b1cdb7f6bc96b1b3536b53a574b8"}, - {file = "rpds_py-0.10.6-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad857f42831e5b8d41a32437f88d86ead6c191455a3499c4b6d15e007936d4cf"}, - {file = "rpds_py-0.10.6-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d7360573f1e046cb3b0dceeb8864025aa78d98be4bb69f067ec1c40a9e2d9df"}, - {file = "rpds_py-0.10.6-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d08f63561c8a695afec4975fae445245386d645e3e446e6f260e81663bfd2e38"}, - {file = "rpds_py-0.10.6-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:f0f17f2ce0f3529177a5fff5525204fad7b43dd437d017dd0317f2746773443d"}, - {file = "rpds_py-0.10.6-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:442626328600bde1d09dc3bb00434f5374948838ce75c41a52152615689f9403"}, - {file = "rpds_py-0.10.6-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e9616f5bd2595f7f4a04b67039d890348ab826e943a9bfdbe4938d0eba606971"}, - {file = "rpds_py-0.10.6.tar.gz", hash = "sha256:4ce5a708d65a8dbf3748d2474b580d606b1b9f91b5c6ab2a316e0b0cf7a4ba50"}, -] - -[[package]] -name = "send2trash" -version = "1.8.2" -description = "Send file to trash natively under Mac OS X, Windows and Linux" -optional = true -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -files = [ - {file = "Send2Trash-1.8.2-py3-none-any.whl", hash = "sha256:a384719d99c07ce1eefd6905d2decb6f8b7ed054025bb0e618919f945de4f679"}, - {file = "Send2Trash-1.8.2.tar.gz", hash = "sha256:c132d59fa44b9ca2b1699af5c86f57ce9f4c5eb56629d5d55fbb7a35f84e2312"}, -] - -[package.extras] -nativelib = ["pyobjc-framework-Cocoa", "pywin32"] -objc = ["pyobjc-framework-Cocoa"] -win32 = ["pywin32"] - -[[package]] -name = "setuptools" -version = "68.2.2" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"}, - {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] - -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] - -[[package]] -name = "sniffio" -version = "1.3.0" -description = "Sniff out which async library your code is running under" -optional = false -python-versions = ">=3.7" -files = [ - {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, - {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, -] - -[[package]] -name = "soupsieve" -version = "2.5" -description = "A modern CSS selector implementation for Beautiful Soup." -optional = true -python-versions = ">=3.8" -files = [ - {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, - {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, -] - -[[package]] -name = "stack-data" -version = "0.6.3" -description = "Extract data from python stack frames and tracebacks for informative displays" -optional = true -python-versions = "*" -files = [ - {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, - {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, -] - -[package.dependencies] -asttokens = ">=2.1.0" -executing = ">=1.2.0" -pure-eval = "*" - -[package.extras] -tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] - -[[package]] -name = "terminado" -version = "0.17.1" -description = "Tornado websocket backend for the Xterm.js Javascript terminal emulator library." -optional = true -python-versions = ">=3.7" -files = [ - {file = "terminado-0.17.1-py3-none-any.whl", hash = "sha256:8650d44334eba354dd591129ca3124a6ba42c3d5b70df5051b6921d506fdaeae"}, - {file = "terminado-0.17.1.tar.gz", hash = "sha256:6ccbbcd3a4f8a25a5ec04991f39a0b8db52dfcd487ea0e578d977e6752380333"}, -] - -[package.dependencies] -ptyprocess = {version = "*", markers = "os_name != \"nt\""} -pywinpty = {version = ">=1.1.0", markers = "os_name == \"nt\""} -tornado = ">=6.1.0" - -[package.extras] -docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] -test = ["pre-commit", "pytest (>=7.0)", "pytest-timeout"] - -[[package]] -name = "tinycss2" -version = "1.2.1" -description = "A tiny CSS parser" -optional = true -python-versions = ">=3.7" -files = [ - {file = "tinycss2-1.2.1-py3-none-any.whl", hash = "sha256:2b80a96d41e7c3914b8cda8bc7f705a4d9c49275616e886103dd839dfc847847"}, - {file = "tinycss2-1.2.1.tar.gz", hash = "sha256:8cff3a8f066c2ec677c06dbc7b45619804a6938478d9d73c284b29d14ecb0627"}, -] - -[package.dependencies] -webencodings = ">=0.4" - -[package.extras] -doc = ["sphinx", "sphinx_rtd_theme"] -test = ["flake8", "isort", "pytest"] - -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] - -[[package]] -name = "tornado" -version = "6.3.3" -description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." -optional = true -python-versions = ">= 3.8" -files = [ - {file = "tornado-6.3.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:502fba735c84450974fec147340016ad928d29f1e91f49be168c0a4c18181e1d"}, - {file = "tornado-6.3.3-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:805d507b1f588320c26f7f097108eb4023bbaa984d63176d1652e184ba24270a"}, - {file = "tornado-6.3.3-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bd19ca6c16882e4d37368e0152f99c099bad93e0950ce55e71daed74045908f"}, - {file = "tornado-6.3.3-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ac51f42808cca9b3613f51ffe2a965c8525cb1b00b7b2d56828b8045354f76a"}, - {file = "tornado-6.3.3-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71a8db65160a3c55d61839b7302a9a400074c9c753040455494e2af74e2501f2"}, - {file = "tornado-6.3.3-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:ceb917a50cd35882b57600709dd5421a418c29ddc852da8bcdab1f0db33406b0"}, - {file = "tornado-6.3.3-cp38-abi3-musllinux_1_1_i686.whl", hash = "sha256:7d01abc57ea0dbb51ddfed477dfe22719d376119844e33c661d873bf9c0e4a16"}, - {file = "tornado-6.3.3-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:9dc4444c0defcd3929d5c1eb5706cbe1b116e762ff3e0deca8b715d14bf6ec17"}, - {file = "tornado-6.3.3-cp38-abi3-win32.whl", hash = "sha256:65ceca9500383fbdf33a98c0087cb975b2ef3bfb874cb35b8de8740cf7f41bd3"}, - {file = "tornado-6.3.3-cp38-abi3-win_amd64.whl", hash = "sha256:22d3c2fa10b5793da13c807e6fc38ff49a4f6e1e3868b0a6f4164768bb8e20f5"}, - {file = "tornado-6.3.3.tar.gz", hash = "sha256:e7d8db41c0181c80d76c982aacc442c0783a2c54d6400fe028954201a2e032fe"}, -] - -[[package]] -name = "traitlets" -version = "5.12.0" -description = "Traitlets Python configuration system" -optional = true -python-versions = ">=3.8" -files = [ - {file = "traitlets-5.12.0-py3-none-any.whl", hash = "sha256:81539f07f7aebcde2e4b5ab76727f53eabf18ad155c6ed7979a681411602fa47"}, - {file = "traitlets-5.12.0.tar.gz", hash = "sha256:833273bf645d8ce31dcb613c56999e2e055b1ffe6d09168a164bcd91c36d5d35"}, -] - -[package.extras] -docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] -test = ["argcomplete (>=3.0.3)", "mypy (>=1.6.0)", "pre-commit", "pytest (>=7.0,<7.5)", "pytest-mock", "pytest-mypy-testing"] - -[[package]] -name = "types-python-dateutil" -version = "2.8.19.14" -description = "Typing stubs for python-dateutil" -optional = true -python-versions = "*" -files = [ - {file = "types-python-dateutil-2.8.19.14.tar.gz", hash = "sha256:1f4f10ac98bb8b16ade9dbee3518d9ace017821d94b057a425b069f834737f4b"}, - {file = "types_python_dateutil-2.8.19.14-py3-none-any.whl", hash = "sha256:f977b8de27787639986b4e28963263fd0e5158942b3ecef91b9335c130cb1ce9"}, -] - -[[package]] -name = "types-requests" -version = "2.31.0.10" -description = "Typing stubs for requests" -optional = false -python-versions = ">=3.7" -files = [ - {file = "types-requests-2.31.0.10.tar.gz", hash = "sha256:dc5852a76f1eaf60eafa81a2e50aefa3d1f015c34cf0cba130930866b1b22a92"}, - {file = "types_requests-2.31.0.10-py3-none-any.whl", hash = "sha256:b32b9a86beffa876c0c3ac99a4cd3b8b51e973fb8e3bd4e0a6bb32c7efad80fc"}, -] - -[package.dependencies] -urllib3 = ">=2" - -[[package]] -name = "typing-extensions" -version = "4.8.0" -description = "Backported and Experimental Type Hints for Python 3.8+" -optional = false -python-versions = ">=3.8" -files = [ - {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, - {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, -] - -[[package]] -name = "uri-template" -version = "1.3.0" -description = "RFC 6570 URI Template Processor" -optional = true -python-versions = ">=3.7" -files = [ - {file = "uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7"}, - {file = "uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363"}, -] - -[package.extras] -dev = ["flake8", "flake8-annotations", "flake8-bandit", "flake8-bugbear", "flake8-commas", "flake8-comprehensions", "flake8-continuation", "flake8-datetimez", "flake8-docstrings", "flake8-import-order", "flake8-literal", "flake8-modern-annotations", "flake8-noqa", "flake8-pyproject", "flake8-requirements", "flake8-typechecking-import", "flake8-use-fstring", "mypy", "pep8-naming", "types-PyYAML"] - -[[package]] -name = "urllib3" -version = "2.0.7" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.7" -files = [ - {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"}, - {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, -] - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "virtualenv" -version = "20.24.6" -description = "Virtual Python Environment builder" -optional = false -python-versions = ">=3.7" -files = [ - {file = "virtualenv-20.24.6-py3-none-any.whl", hash = "sha256:520d056652454c5098a00c0f073611ccbea4c79089331f60bf9d7ba247bb7381"}, - {file = "virtualenv-20.24.6.tar.gz", hash = "sha256:02ece4f56fbf939dbbc33c0715159951d6bf14aaf5457b092e4548e1382455af"}, -] - -[package.dependencies] -distlib = ">=0.3.7,<1" -filelock = ">=3.12.2,<4" -platformdirs = ">=3.9.1,<4" - -[package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] - -[[package]] -name = "watchdog" -version = "3.0.0" -description = "Filesystem events monitoring" -optional = false -python-versions = ">=3.7" -files = [ - {file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:336adfc6f5cc4e037d52db31194f7581ff744b67382eb6021c868322e32eef41"}, - {file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a70a8dcde91be523c35b2bf96196edc5730edb347e374c7de7cd20c43ed95397"}, - {file = "watchdog-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:adfdeab2da79ea2f76f87eb42a3ab1966a5313e5a69a0213a3cc06ef692b0e96"}, - {file = "watchdog-3.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b57a1e730af3156d13b7fdddfc23dea6487fceca29fc75c5a868beed29177ae"}, - {file = "watchdog-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7ade88d0d778b1b222adebcc0927428f883db07017618a5e684fd03b83342bd9"}, - {file = "watchdog-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7e447d172af52ad204d19982739aa2346245cc5ba6f579d16dac4bfec226d2e7"}, - {file = "watchdog-3.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9fac43a7466eb73e64a9940ac9ed6369baa39b3bf221ae23493a9ec4d0022674"}, - {file = "watchdog-3.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8ae9cda41fa114e28faf86cb137d751a17ffd0316d1c34ccf2235e8a84365c7f"}, - {file = "watchdog-3.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:25f70b4aa53bd743729c7475d7ec41093a580528b100e9a8c5b5efe8899592fc"}, - {file = "watchdog-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4f94069eb16657d2c6faada4624c39464f65c05606af50bb7902e036e3219be3"}, - {file = "watchdog-3.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7c5f84b5194c24dd573fa6472685b2a27cc5a17fe5f7b6fd40345378ca6812e3"}, - {file = "watchdog-3.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3aa7f6a12e831ddfe78cdd4f8996af9cf334fd6346531b16cec61c3b3c0d8da0"}, - {file = "watchdog-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:233b5817932685d39a7896b1090353fc8efc1ef99c9c054e46c8002561252fb8"}, - {file = "watchdog-3.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:13bbbb462ee42ec3c5723e1205be8ced776f05b100e4737518c67c8325cf6100"}, - {file = "watchdog-3.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8f3ceecd20d71067c7fd4c9e832d4e22584318983cabc013dbf3f70ea95de346"}, - {file = "watchdog-3.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c9d8c8ec7efb887333cf71e328e39cffbf771d8f8f95d308ea4125bf5f90ba64"}, - {file = "watchdog-3.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0e06ab8858a76e1219e68c7573dfeba9dd1c0219476c5a44d5333b01d7e1743a"}, - {file = "watchdog-3.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:d00e6be486affb5781468457b21a6cbe848c33ef43f9ea4a73b4882e5f188a44"}, - {file = "watchdog-3.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:c07253088265c363d1ddf4b3cdb808d59a0468ecd017770ed716991620b8f77a"}, - {file = "watchdog-3.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:5113334cf8cf0ac8cd45e1f8309a603291b614191c9add34d33075727a967709"}, - {file = "watchdog-3.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:51f90f73b4697bac9c9a78394c3acbbd331ccd3655c11be1a15ae6fe289a8c83"}, - {file = "watchdog-3.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:ba07e92756c97e3aca0912b5cbc4e5ad802f4557212788e72a72a47ff376950d"}, - {file = "watchdog-3.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:d429c2430c93b7903914e4db9a966c7f2b068dd2ebdd2fa9b9ce094c7d459f33"}, - {file = "watchdog-3.0.0-py3-none-win32.whl", hash = "sha256:3ed7c71a9dccfe838c2f0b6314ed0d9b22e77d268c67e015450a29036a81f60f"}, - {file = "watchdog-3.0.0-py3-none-win_amd64.whl", hash = "sha256:4c9956d27be0bb08fc5f30d9d0179a855436e655f046d288e2bcc11adfae893c"}, - {file = "watchdog-3.0.0-py3-none-win_ia64.whl", hash = "sha256:5d9f3a10e02d7371cd929b5d8f11e87d4bad890212ed3901f9b4d68767bee759"}, - {file = "watchdog-3.0.0.tar.gz", hash = "sha256:4d98a320595da7a7c5a18fc48cb633c2e73cda78f93cac2ef42d42bf609a33f9"}, -] - -[package.extras] -watchmedo = ["PyYAML (>=3.10)"] - -[[package]] -name = "wcwidth" -version = "0.2.8" -description = "Measures the displayed width of unicode strings in a terminal" -optional = true -python-versions = "*" -files = [ - {file = "wcwidth-0.2.8-py2.py3-none-any.whl", hash = "sha256:77f719e01648ed600dfa5402c347481c0992263b81a027344f3e1ba25493a704"}, - {file = "wcwidth-0.2.8.tar.gz", hash = "sha256:8705c569999ffbb4f6a87c6d1b80f324bd6db952f5eb0b95bc07517f4c1813d4"}, -] - -[[package]] -name = "webcolors" -version = "1.13" -description = "A library for working with the color formats defined by HTML and CSS." -optional = true -python-versions = ">=3.7" -files = [ - {file = "webcolors-1.13-py3-none-any.whl", hash = "sha256:29bc7e8752c0a1bd4a1f03c14d6e6a72e93d82193738fa860cbff59d0fcc11bf"}, - {file = "webcolors-1.13.tar.gz", hash = "sha256:c225b674c83fa923be93d235330ce0300373d02885cef23238813b0d5668304a"}, -] - -[package.extras] -docs = ["furo", "sphinx", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-notfound-page", "sphinxext-opengraph"] -tests = ["pytest", "pytest-cov"] - -[[package]] -name = "webencodings" -version = "0.5.1" -description = "Character encoding aliases for legacy web content" -optional = true -python-versions = "*" -files = [ - {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, - {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, -] - -[[package]] -name = "websocket-client" -version = "1.6.4" -description = "WebSocket client for Python with low level API options" -optional = true -python-versions = ">=3.8" -files = [ - {file = "websocket-client-1.6.4.tar.gz", hash = "sha256:b3324019b3c28572086c4a319f91d1dcd44e6e11cd340232978c684a7650d0df"}, - {file = "websocket_client-1.6.4-py3-none-any.whl", hash = "sha256:084072e0a7f5f347ef2ac3d8698a5e0b4ffbfcab607628cadabc650fc9a83a24"}, -] - -[package.extras] -docs = ["Sphinx (>=6.0)", "sphinx-rtd-theme (>=1.1.0)"] -optional = ["python-socks", "wsaccel"] -test = ["websockets"] - -[[package]] -name = "websockets" -version = "11.0.3" -description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" -optional = false -python-versions = ">=3.7" -files = [ - {file = "websockets-11.0.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3ccc8a0c387629aec40f2fc9fdcb4b9d5431954f934da3eaf16cdc94f67dbfac"}, - {file = "websockets-11.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d67ac60a307f760c6e65dad586f556dde58e683fab03323221a4e530ead6f74d"}, - {file = "websockets-11.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84d27a4832cc1a0ee07cdcf2b0629a8a72db73f4cf6de6f0904f6661227f256f"}, - {file = "websockets-11.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffd7dcaf744f25f82190856bc26ed81721508fc5cbf2a330751e135ff1283564"}, - {file = "websockets-11.0.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7622a89d696fc87af8e8d280d9b421db5133ef5b29d3f7a1ce9f1a7bf7fcfa11"}, - {file = "websockets-11.0.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bceab846bac555aff6427d060f2fcfff71042dba6f5fca7dc4f75cac815e57ca"}, - {file = "websockets-11.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:54c6e5b3d3a8936a4ab6870d46bdd6ec500ad62bde9e44462c32d18f1e9a8e54"}, - {file = "websockets-11.0.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:41f696ba95cd92dc047e46b41b26dd24518384749ed0d99bea0a941ca87404c4"}, - {file = "websockets-11.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:86d2a77fd490ae3ff6fae1c6ceaecad063d3cc2320b44377efdde79880e11526"}, - {file = "websockets-11.0.3-cp310-cp310-win32.whl", hash = "sha256:2d903ad4419f5b472de90cd2d40384573b25da71e33519a67797de17ef849b69"}, - {file = "websockets-11.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:1d2256283fa4b7f4c7d7d3e84dc2ece74d341bce57d5b9bf385df109c2a1a82f"}, - {file = "websockets-11.0.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e848f46a58b9fcf3d06061d17be388caf70ea5b8cc3466251963c8345e13f7eb"}, - {file = "websockets-11.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa5003845cdd21ac0dc6c9bf661c5beddd01116f6eb9eb3c8e272353d45b3288"}, - {file = "websockets-11.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b58cbf0697721120866820b89f93659abc31c1e876bf20d0b3d03cef14faf84d"}, - {file = "websockets-11.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:660e2d9068d2bedc0912af508f30bbeb505bbbf9774d98def45f68278cea20d3"}, - {file = "websockets-11.0.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1f0524f203e3bd35149f12157438f406eff2e4fb30f71221c8a5eceb3617b6b"}, - {file = "websockets-11.0.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:def07915168ac8f7853812cc593c71185a16216e9e4fa886358a17ed0fd9fcf6"}, - {file = "websockets-11.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b30c6590146e53149f04e85a6e4fcae068df4289e31e4aee1fdf56a0dead8f97"}, - {file = "websockets-11.0.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:619d9f06372b3a42bc29d0cd0354c9bb9fb39c2cbc1a9c5025b4538738dbffaf"}, - {file = "websockets-11.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:01f5567d9cf6f502d655151645d4e8b72b453413d3819d2b6f1185abc23e82dd"}, - {file = "websockets-11.0.3-cp311-cp311-win32.whl", hash = "sha256:e1459677e5d12be8bbc7584c35b992eea142911a6236a3278b9b5ce3326f282c"}, - {file = "websockets-11.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:e7837cb169eca3b3ae94cc5787c4fed99eef74c0ab9506756eea335e0d6f3ed8"}, - {file = "websockets-11.0.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9f59a3c656fef341a99e3d63189852be7084c0e54b75734cde571182c087b152"}, - {file = "websockets-11.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2529338a6ff0eb0b50c7be33dc3d0e456381157a31eefc561771ee431134a97f"}, - {file = "websockets-11.0.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34fd59a4ac42dff6d4681d8843217137f6bc85ed29722f2f7222bd619d15e95b"}, - {file = "websockets-11.0.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:332d126167ddddec94597c2365537baf9ff62dfcc9db4266f263d455f2f031cb"}, - {file = "websockets-11.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6505c1b31274723ccaf5f515c1824a4ad2f0d191cec942666b3d0f3aa4cb4007"}, - {file = "websockets-11.0.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f467ba0050b7de85016b43f5a22b46383ef004c4f672148a8abf32bc999a87f0"}, - {file = "websockets-11.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9d9acd80072abcc98bd2c86c3c9cd4ac2347b5a5a0cae7ed5c0ee5675f86d9af"}, - {file = "websockets-11.0.3-cp37-cp37m-win32.whl", hash = "sha256:e590228200fcfc7e9109509e4d9125eace2042fd52b595dd22bbc34bb282307f"}, - {file = "websockets-11.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:b16fff62b45eccb9c7abb18e60e7e446998093cdcb50fed33134b9b6878836de"}, - {file = "websockets-11.0.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fb06eea71a00a7af0ae6aefbb932fb8a7df3cb390cc217d51a9ad7343de1b8d0"}, - {file = "websockets-11.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8a34e13a62a59c871064dfd8ffb150867e54291e46d4a7cf11d02c94a5275bae"}, - {file = "websockets-11.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4841ed00f1026dfbced6fca7d963c4e7043aa832648671b5138008dc5a8f6d99"}, - {file = "websockets-11.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a073fc9ab1c8aff37c99f11f1641e16da517770e31a37265d2755282a5d28aa"}, - {file = "websockets-11.0.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68b977f21ce443d6d378dbd5ca38621755f2063d6fdb3335bda981d552cfff86"}, - {file = "websockets-11.0.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1a99a7a71631f0efe727c10edfba09ea6bee4166a6f9c19aafb6c0b5917d09c"}, - {file = "websockets-11.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bee9fcb41db2a23bed96c6b6ead6489702c12334ea20a297aa095ce6d31370d0"}, - {file = "websockets-11.0.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4b253869ea05a5a073ebfdcb5cb3b0266a57c3764cf6fe114e4cd90f4bfa5f5e"}, - {file = "websockets-11.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1553cb82942b2a74dd9b15a018dce645d4e68674de2ca31ff13ebc2d9f283788"}, - {file = "websockets-11.0.3-cp38-cp38-win32.whl", hash = "sha256:f61bdb1df43dc9c131791fbc2355535f9024b9a04398d3bd0684fc16ab07df74"}, - {file = "websockets-11.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:03aae4edc0b1c68498f41a6772d80ac7c1e33c06c6ffa2ac1c27a07653e79d6f"}, - {file = "websockets-11.0.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:777354ee16f02f643a4c7f2b3eff8027a33c9861edc691a2003531f5da4f6bc8"}, - {file = "websockets-11.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8c82f11964f010053e13daafdc7154ce7385ecc538989a354ccc7067fd7028fd"}, - {file = "websockets-11.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3580dd9c1ad0701169e4d6fc41e878ffe05e6bdcaf3c412f9d559389d0c9e016"}, - {file = "websockets-11.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f1a3f10f836fab6ca6efa97bb952300b20ae56b409414ca85bff2ad241d2a61"}, - {file = "websockets-11.0.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df41b9bc27c2c25b486bae7cf42fccdc52ff181c8c387bfd026624a491c2671b"}, - {file = "websockets-11.0.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:279e5de4671e79a9ac877427f4ac4ce93751b8823f276b681d04b2156713b9dd"}, - {file = "websockets-11.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1fdf26fa8a6a592f8f9235285b8affa72748dc12e964a5518c6c5e8f916716f7"}, - {file = "websockets-11.0.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:69269f3a0b472e91125b503d3c0b3566bda26da0a3261c49f0027eb6075086d1"}, - {file = "websockets-11.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:97b52894d948d2f6ea480171a27122d77af14ced35f62e5c892ca2fae9344311"}, - {file = "websockets-11.0.3-cp39-cp39-win32.whl", hash = "sha256:c7f3cb904cce8e1be667c7e6fef4516b98d1a6a0635a58a57528d577ac18a128"}, - {file = "websockets-11.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c792ea4eabc0159535608fc5658a74d1a81020eb35195dd63214dcf07556f67e"}, - {file = "websockets-11.0.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f2e58f2c36cc52d41f2659e4c0cbf7353e28c8c9e63e30d8c6d3494dc9fdedcf"}, - {file = "websockets-11.0.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de36fe9c02995c7e6ae6efe2e205816f5f00c22fd1fbf343d4d18c3d5ceac2f5"}, - {file = "websockets-11.0.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ac56b661e60edd453585f4bd68eb6a29ae25b5184fd5ba51e97652580458998"}, - {file = "websockets-11.0.3-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e052b8467dd07d4943936009f46ae5ce7b908ddcac3fda581656b1b19c083d9b"}, - {file = "websockets-11.0.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:42cc5452a54a8e46a032521d7365da775823e21bfba2895fb7b77633cce031bb"}, - {file = "websockets-11.0.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e6316827e3e79b7b8e7d8e3b08f4e331af91a48e794d5d8b099928b6f0b85f20"}, - {file = "websockets-11.0.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8531fdcad636d82c517b26a448dcfe62f720e1922b33c81ce695d0edb91eb931"}, - {file = "websockets-11.0.3-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c114e8da9b475739dde229fd3bc6b05a6537a88a578358bc8eb29b4030fac9c9"}, - {file = "websockets-11.0.3-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e063b1865974611313a3849d43f2c3f5368093691349cf3c7c8f8f75ad7cb280"}, - {file = "websockets-11.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:92b2065d642bf8c0a82d59e59053dd2fdde64d4ed44efe4870fa816c1232647b"}, - {file = "websockets-11.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0ee68fe502f9031f19d495dae2c268830df2760c0524cbac5d759921ba8c8e82"}, - {file = "websockets-11.0.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcacf2c7a6c3a84e720d1bb2b543c675bf6c40e460300b628bab1b1efc7c034c"}, - {file = "websockets-11.0.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b67c6f5e5a401fc56394f191f00f9b3811fe843ee93f4a70df3c389d1adf857d"}, - {file = "websockets-11.0.3-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d5023a4b6a5b183dc838808087033ec5df77580485fc533e7dab2567851b0a4"}, - {file = "websockets-11.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ed058398f55163a79bb9f06a90ef9ccc063b204bb346c4de78efc5d15abfe602"}, - {file = "websockets-11.0.3-py3-none-any.whl", hash = "sha256:6681ba9e7f8f3b19440921e99efbb40fc89f26cd71bf539e45d8c8a25c976dc6"}, - {file = "websockets-11.0.3.tar.gz", hash = "sha256:88fc51d9a26b10fc331be344f1781224a375b78488fc343620184e95a4b27016"}, -] - -[[package]] -name = "yarl" -version = "1.9.2" -description = "Yet another URL library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8c2ad583743d16ddbdf6bb14b5cd76bf43b0d0006e918809d5d4ddf7bde8dd82"}, - {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:82aa6264b36c50acfb2424ad5ca537a2060ab6de158a5bd2a72a032cc75b9eb8"}, - {file = "yarl-1.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c0c77533b5ed4bcc38e943178ccae29b9bcf48ffd1063f5821192f23a1bd27b9"}, - {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee4afac41415d52d53a9833ebae7e32b344be72835bbb589018c9e938045a560"}, - {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bf345c3a4f5ba7f766430f97f9cc1320786f19584acc7086491f45524a551ac"}, - {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a96c19c52ff442a808c105901d0bdfd2e28575b3d5f82e2f5fd67e20dc5f4ea"}, - {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:891c0e3ec5ec881541f6c5113d8df0315ce5440e244a716b95f2525b7b9f3608"}, - {file = "yarl-1.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c3a53ba34a636a256d767c086ceb111358876e1fb6b50dfc4d3f4951d40133d5"}, - {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:566185e8ebc0898b11f8026447eacd02e46226716229cea8db37496c8cdd26e0"}, - {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2b0738fb871812722a0ac2154be1f049c6223b9f6f22eec352996b69775b36d4"}, - {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:32f1d071b3f362c80f1a7d322bfd7b2d11e33d2adf395cc1dd4df36c9c243095"}, - {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:e9fdc7ac0d42bc3ea78818557fab03af6181e076a2944f43c38684b4b6bed8e3"}, - {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:56ff08ab5df8429901ebdc5d15941b59f6253393cb5da07b4170beefcf1b2528"}, - {file = "yarl-1.9.2-cp310-cp310-win32.whl", hash = "sha256:8ea48e0a2f931064469bdabca50c2f578b565fc446f302a79ba6cc0ee7f384d3"}, - {file = "yarl-1.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:50f33040f3836e912ed16d212f6cc1efb3231a8a60526a407aeb66c1c1956dde"}, - {file = "yarl-1.9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:646d663eb2232d7909e6601f1a9107e66f9791f290a1b3dc7057818fe44fc2b6"}, - {file = "yarl-1.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aff634b15beff8902d1f918012fc2a42e0dbae6f469fce134c8a0dc51ca423bb"}, - {file = "yarl-1.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a83503934c6273806aed765035716216cc9ab4e0364f7f066227e1aaea90b8d0"}, - {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b25322201585c69abc7b0e89e72790469f7dad90d26754717f3310bfe30331c2"}, - {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22a94666751778629f1ec4280b08eb11815783c63f52092a5953faf73be24191"}, - {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ec53a0ea2a80c5cd1ab397925f94bff59222aa3cf9c6da938ce05c9ec20428d"}, - {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:159d81f22d7a43e6eabc36d7194cb53f2f15f498dbbfa8edc8a3239350f59fe7"}, - {file = "yarl-1.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:832b7e711027c114d79dffb92576acd1bd2decc467dec60e1cac96912602d0e6"}, - {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:95d2ecefbcf4e744ea952d073c6922e72ee650ffc79028eb1e320e732898d7e8"}, - {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d4e2c6d555e77b37288eaf45b8f60f0737c9efa3452c6c44626a5455aeb250b9"}, - {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:783185c75c12a017cc345015ea359cc801c3b29a2966c2655cd12b233bf5a2be"}, - {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:b8cc1863402472f16c600e3e93d542b7e7542a540f95c30afd472e8e549fc3f7"}, - {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:822b30a0f22e588b32d3120f6d41e4ed021806418b4c9f0bc3048b8c8cb3f92a"}, - {file = "yarl-1.9.2-cp311-cp311-win32.whl", hash = "sha256:a60347f234c2212a9f0361955007fcf4033a75bf600a33c88a0a8e91af77c0e8"}, - {file = "yarl-1.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:be6b3fdec5c62f2a67cb3f8c6dbf56bbf3f61c0f046f84645cd1ca73532ea051"}, - {file = "yarl-1.9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:38a3928ae37558bc1b559f67410df446d1fbfa87318b124bf5032c31e3447b74"}, - {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac9bb4c5ce3975aeac288cfcb5061ce60e0d14d92209e780c93954076c7c4367"}, - {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3da8a678ca8b96c8606bbb8bfacd99a12ad5dd288bc6f7979baddd62f71c63ef"}, - {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13414591ff516e04fcdee8dc051c13fd3db13b673c7a4cb1350e6b2ad9639ad3"}, - {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf74d08542c3a9ea97bb8f343d4fcbd4d8f91bba5ec9d5d7f792dbe727f88938"}, - {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e7221580dc1db478464cfeef9b03b95c5852cc22894e418562997df0d074ccc"}, - {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:494053246b119b041960ddcd20fd76224149cfea8ed8777b687358727911dd33"}, - {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:52a25809fcbecfc63ac9ba0c0fb586f90837f5425edfd1ec9f3372b119585e45"}, - {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:e65610c5792870d45d7b68c677681376fcf9cc1c289f23e8e8b39c1485384185"}, - {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:1b1bba902cba32cdec51fca038fd53f8beee88b77efc373968d1ed021024cc04"}, - {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:662e6016409828ee910f5d9602a2729a8a57d74b163c89a837de3fea050c7582"}, - {file = "yarl-1.9.2-cp37-cp37m-win32.whl", hash = "sha256:f364d3480bffd3aa566e886587eaca7c8c04d74f6e8933f3f2c996b7f09bee1b"}, - {file = "yarl-1.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6a5883464143ab3ae9ba68daae8e7c5c95b969462bbe42e2464d60e7e2698368"}, - {file = "yarl-1.9.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5610f80cf43b6202e2c33ba3ec2ee0a2884f8f423c8f4f62906731d876ef4fac"}, - {file = "yarl-1.9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b9a4e67ad7b646cd6f0938c7ebfd60e481b7410f574c560e455e938d2da8e0f4"}, - {file = "yarl-1.9.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:83fcc480d7549ccebe9415d96d9263e2d4226798c37ebd18c930fce43dfb9574"}, - {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fcd436ea16fee7d4207c045b1e340020e58a2597301cfbcfdbe5abd2356c2fb"}, - {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84e0b1599334b1e1478db01b756e55937d4614f8654311eb26012091be109d59"}, - {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3458a24e4ea3fd8930e934c129b676c27452e4ebda80fbe47b56d8c6c7a63a9e"}, - {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:838162460b3a08987546e881a2bfa573960bb559dfa739e7800ceeec92e64417"}, - {file = "yarl-1.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4e2d08f07a3d7d3e12549052eb5ad3eab1c349c53ac51c209a0e5991bbada78"}, - {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:de119f56f3c5f0e2fb4dee508531a32b069a5f2c6e827b272d1e0ff5ac040333"}, - {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:149ddea5abf329752ea5051b61bd6c1d979e13fbf122d3a1f9f0c8be6cb6f63c"}, - {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:674ca19cbee4a82c9f54e0d1eee28116e63bc6fd1e96c43031d11cbab8b2afd5"}, - {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:9b3152f2f5677b997ae6c804b73da05a39daa6a9e85a512e0e6823d81cdad7cc"}, - {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5415d5a4b080dc9612b1b63cba008db84e908b95848369aa1da3686ae27b6d2b"}, - {file = "yarl-1.9.2-cp38-cp38-win32.whl", hash = "sha256:f7a3d8146575e08c29ed1cd287068e6d02f1c7bdff8970db96683b9591b86ee7"}, - {file = "yarl-1.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:63c48f6cef34e6319a74c727376e95626f84ea091f92c0250a98e53e62c77c72"}, - {file = "yarl-1.9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:75df5ef94c3fdc393c6b19d80e6ef1ecc9ae2f4263c09cacb178d871c02a5ba9"}, - {file = "yarl-1.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c027a6e96ef77d401d8d5a5c8d6bc478e8042f1e448272e8d9752cb0aff8b5c8"}, - {file = "yarl-1.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3b078dbe227f79be488ffcfc7a9edb3409d018e0952cf13f15fd6512847f3f7"}, - {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59723a029760079b7d991a401386390c4be5bfec1e7dd83e25a6a0881859e716"}, - {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b03917871bf859a81ccb180c9a2e6c1e04d2f6a51d953e6a5cdd70c93d4e5a2a"}, - {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1012fa63eb6c032f3ce5d2171c267992ae0c00b9e164efe4d73db818465fac3"}, - {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a74dcbfe780e62f4b5a062714576f16c2f3493a0394e555ab141bf0d746bb955"}, - {file = "yarl-1.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c56986609b057b4839968ba901944af91b8e92f1725d1a2d77cbac6972b9ed1"}, - {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2c315df3293cd521033533d242d15eab26583360b58f7ee5d9565f15fee1bef4"}, - {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b7232f8dfbd225d57340e441d8caf8652a6acd06b389ea2d3222b8bc89cbfca6"}, - {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:53338749febd28935d55b41bf0bcc79d634881195a39f6b2f767870b72514caf"}, - {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:066c163aec9d3d073dc9ffe5dd3ad05069bcb03fcaab8d221290ba99f9f69ee3"}, - {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8288d7cd28f8119b07dd49b7230d6b4562f9b61ee9a4ab02221060d21136be80"}, - {file = "yarl-1.9.2-cp39-cp39-win32.whl", hash = "sha256:b124e2a6d223b65ba8768d5706d103280914d61f5cae3afbc50fc3dfcc016623"}, - {file = "yarl-1.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:61016e7d582bc46a5378ffdd02cd0314fb8ba52f40f9cf4d9a5e7dbef88dee18"}, - {file = "yarl-1.9.2.tar.gz", hash = "sha256:04ab9d4b9f587c06d801c2abfe9317b77cdf996c65a90d5e84ecc45010823571"}, -] - -[package.dependencies] -idna = ">=2.0" -multidict = ">=4.0" - -[[package]] -name = "zipp" -version = "3.17.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.8" -files = [ - {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, - {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] - -[extras] -all = ["Pillow", "jupyter-kernel-gateway"] -image-support = ["Pillow"] -local-support = ["jupyter-kernel-gateway"] - -[metadata] -lock-version = "2.0" -python-versions = "^3.9" -content-hash = "b57eeb76cac0f52b6053d7c9dee4b110183d9fb23923496db93e4ea5a67f1337" From 85456a9da8efe220655ec2d6f0135cb63cf9e3f3 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 29 Feb 2024 14:04:44 +0700 Subject: [PATCH 004/125] =?UTF-8?q?=F0=9F=9A=80=20Bump=20version,=20refine?= =?UTF-8?q?=20dependencies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index efc65ad..621979f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "codeboxapi" -version = "0.1.19" +version = "0.2.0" description = "CodeBox gives you an easy scalable and isolated python interpreter for your LLM Agents." keywords = [ "codebox", @@ -17,7 +17,6 @@ dependencies = [ "pydantic-settings>=2", "requests>=2", "aiohttp>=3.9", - "websockets>=12", ] readme = "README.md" requires-python = ">= 3.9" @@ -44,13 +43,16 @@ dev-dependencies = [ "mkdocs-material>=9", "types-requests>=2.31", "matplotlib>=3.8.2", + "jupyter-client>=8.6.0", + "ipykernel>=6.29.3", "pip", + "rich>=13.7.0", ] [project.optional-dependencies] -all = ["jupyter-kernel-gateway>=2.5, <3", "Pillow>=10"] -local_support = ["jupyter-kernel-gateway>=2.5, <3"] -image_support = ["Pillow>=10"] +local = ["jupyter-client", "ipykernel"] +image = ["Pillow"] +all = ["codeboxapi[local]", "codeboxapi[image]"] [tool.hatch.metadata] allow-direct-references = true From aa33be536cdc9a157ed25f93d29da99381e1d95f Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 29 Feb 2024 14:04:44 +0700 Subject: [PATCH 005/125] =?UTF-8?q?=F0=9F=93=A6=20Update=20dependencies=20?= =?UTF-8?q?versions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.lock | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/requirements.lock b/requirements.lock index 8f29825..bec0ae5 100644 --- a/requirements.lock +++ b/requirements.lock @@ -9,20 +9,41 @@ -e file:. aiohttp==3.9.3 + # via codeboxapi aiosignal==1.3.1 + # via aiohttp annotated-types==0.6.0 + # via pydantic attrs==23.2.0 + # via aiohttp certifi==2024.2.2 + # via requests charset-normalizer==3.3.2 + # via requests frozenlist==1.4.1 + # via aiohttp + # via aiosignal idna==3.6 + # via requests + # via yarl multidict==6.0.5 -pydantic==2.6.1 -pydantic-core==2.16.2 -pydantic-settings==2.1.0 + # via aiohttp + # via yarl +pydantic==2.6.2 + # via codeboxapi + # via pydantic-settings +pydantic-core==2.16.3 + # via pydantic +pydantic-settings==2.2.1 + # via codeboxapi python-dotenv==1.0.1 + # via pydantic-settings requests==2.31.0 -typing-extensions==4.9.0 -urllib3==2.2.0 -websockets==12.0 + # via codeboxapi +typing-extensions==4.10.0 + # via pydantic + # via pydantic-core +urllib3==2.2.1 + # via requests yarl==1.9.4 + # via aiohttp From 21ae710f4a21f0db67fdcdcd7633dcf8266e5bea Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 29 Feb 2024 14:04:44 +0700 Subject: [PATCH 006/125] =?UTF-8?q?=F0=9F=9A=9A=20Refactor=20import=20path?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/codeboxapi/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/codeboxapi/__init__.py b/src/codeboxapi/__init__.py index 7b85236..be59947 100644 --- a/src/codeboxapi/__init__.py +++ b/src/codeboxapi/__init__.py @@ -5,12 +5,10 @@ The package includes modules for configuring the client, setting the API key, and interacting with Codebox instances. """ -from codeboxapi.box.codebox import CodeBox -from codeboxapi.config import settings -from codeboxapi.utils import set_api_key +from .box import CodeBox +from .config import settings __all__ = [ "CodeBox", - "set_api_key", "settings", ] From 34f5155904d0753f5291a3230536ea7281da0c21 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 29 Feb 2024 14:04:44 +0700 Subject: [PATCH 007/125] =?UTF-8?q?=F0=9F=94=84=20Rename=20module=20import?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/codeboxapi/box/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/codeboxapi/box/__init__.py b/src/codeboxapi/box/__init__.py index b5a258d..da8dd20 100644 --- a/src/codeboxapi/box/__init__.py +++ b/src/codeboxapi/box/__init__.py @@ -5,9 +5,9 @@ `CodeBox` class is used to run code in a remote sandboxed environment. """ -from .basebox import BaseBox -from .codebox import CodeBox -from .localbox import LocalBox +from .base import BaseBox +from .local import LocalBox +from .remote import RemoteBox as CodeBox __all__ = [ "BaseBox", From f6391456ac3e7856a0e10dea1701087f138c3481 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 29 Feb 2024 14:04:44 +0700 Subject: [PATCH 008/125] =?UTF-8?q?=F0=9F=9A=9A=20Rename=20base.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/codeboxapi/box/base.py | 122 +++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 src/codeboxapi/box/base.py diff --git a/src/codeboxapi/box/base.py b/src/codeboxapi/box/base.py new file mode 100644 index 0000000..0c6cf22 --- /dev/null +++ b/src/codeboxapi/box/base.py @@ -0,0 +1,122 @@ +""" Abstract Base Class for Isolated Execution Environments (CodeBox's) """ + +from abc import ABC, abstractmethod +from datetime import datetime +from os import PathLike +from typing import List, Union + +from codeboxapi.schema import CodeBoxFile, CodeBoxOutput, CodeBoxStatus + + +class BaseBox(ABC): + """CodeBox Abstract Base Class""" + + def __init__(self, session_id: str = "local") -> None: + """Initialize the CodeBox instance""" + self.session_id = session_id + self.last_interaction = datetime.now() + + @abstractmethod + def start(self) -> CodeBoxStatus: + """Startup the CodeBox instance""" + + @abstractmethod + async def astart(self) -> CodeBoxStatus: + """Async Startup the CodeBox instance""" + + @abstractmethod + def status(self) -> CodeBoxStatus: + """Get the current status of the CodeBox instance""" + + @abstractmethod + async def astatus(self) -> CodeBoxStatus: + """Async Get the current status of the CodeBox instance""" + + @abstractmethod + def run(self, code: Union[str, PathLike]) -> CodeBoxOutput: + """Execute python code inside the CodeBox instance""" + + @abstractmethod + async def arun(self, code: Union[str, PathLike]) -> CodeBoxOutput: + """Async Execute python code inside the CodeBox instance""" + + # TODO: STREAMING + + # TODO: SHELL + + @abstractmethod + def upload(self, file_name: str, content: bytes) -> CodeBoxStatus: + """Upload a file as bytes to the CodeBox instance""" + + @abstractmethod + async def aupload(self, file_name: str, content: bytes) -> CodeBoxStatus: + """Async Upload a file as bytes to the CodeBox instance""" + + @abstractmethod + def download(self, file_name: str) -> CodeBoxFile: + """Download a file as CodeBoxFile schema""" + + @abstractmethod + async def adownload(self, file_name: str) -> CodeBoxFile: + """Async Download a file as CodeBoxFile schema""" + + @abstractmethod + def install(self, package_name: str) -> CodeBoxStatus: + """Install a python package to the venv""" + + @abstractmethod + async def ainstall(self, package_name: str) -> CodeBoxStatus: + """Async Install a python package to the venv""" + + @abstractmethod + def list_files(self) -> List[CodeBoxFile]: + """List all available files inside the CodeBox instance""" + + @abstractmethod + async def alist_files(self) -> List[CodeBoxFile]: + """Async List all available files inside the CodeBox instance""" + + @abstractmethod + def restart(self) -> CodeBoxStatus: + """Restart the jupyter kernel inside the CodeBox instance""" + + @abstractmethod + async def arestart(self) -> CodeBoxStatus: + """Async Restart the jupyter kernel inside the CodeBox instance""" + + @abstractmethod + def stop(self) -> CodeBoxStatus: + """Terminate the CodeBox instance""" + + @abstractmethod + async def astop(self) -> CodeBoxStatus: + """Async Terminate the CodeBox instance""" + + def _update(self) -> None: + self.last_interaction = datetime.now() + + def _resolve_pathlike(self, code: Union[str, PathLike]) -> str: + if isinstance(code, PathLike): + with open(code, "r", encoding="utf-8") as f: + return f.read() + return code + + def __enter__(self) -> "BaseBox": + self.start() + return self + + async def __aenter__(self) -> "BaseBox": + await self.astart() + return self + + def __exit__(self, exc_type, exc_value, traceback) -> None: + self.stop() + + async def __aexit__(self, exc_type, exc_value, traceback) -> None: + await self.astop() + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} id={self.session_id}>" + + def __str__(self) -> str: + return self.__repr__() From 1382529786f2514d796237cbe413e88d78b6e0a6 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 29 Feb 2024 14:04:44 +0700 Subject: [PATCH 009/125] =?UTF-8?q?=F0=9F=9A=80=20Add=20LocalBox=20impleme?= =?UTF-8?q?ntation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/codeboxapi/box/local.py | 324 ++++++++++++++++++++++++++++++++++++ 1 file changed, 324 insertions(+) create mode 100644 src/codeboxapi/box/local.py diff --git a/src/codeboxapi/box/local.py b/src/codeboxapi/box/local.py new file mode 100644 index 0000000..f846999 --- /dev/null +++ b/src/codeboxapi/box/local.py @@ -0,0 +1,324 @@ +""" +Local implementation of CodeBox. +This is useful for testing and development.c +In case you don't put an api_key, +this is the default CodeBox. +""" + +import asyncio +import os +import subprocess +from importlib.metadata import PackageNotFoundError, distribution +from os import PathLike +from queue import Queue +from threading import Thread +from typing import AsyncGenerator, Generator, List, Optional, Union + +from jupyter_client.manager import KernelManager +from rich import print + +from ..box import BaseBox +from ..config import settings +from ..schema import CodeBoxFile, CodeBoxOutput, CodeBoxStatus + + +class LocalBox(BaseBox): + """ + LocalBox is a CodeBox implementation that runs code locally. + This is useful for testing and development. + """ + + _instance: Optional["LocalBox"] = None + + def __new__(cls, *_, **__): + if not cls._instance: + cls._instance = super().__new__(cls) + else: + if settings.show_info: + print( + "INFO: Using a LocalBox which is not fully isolated\n" + " and not scalable across multiple users.\n" + " Make sure to use a CODEBOX_API_KEY in production.\n" + " Set envar CODEBOX_SHOW_INFO=False to not see this again.\n" + ) + return cls._instance + + def __init__(self, /, **kwargs) -> None: + os.environ["PYDEVD_DISABLE_FILE_VALIDATION"] = "1" + super().__init__(session_id=kwargs.pop("session_id", "local")) + self.kernel = KernelManager() + + def start(self) -> CodeBoxStatus: + self._check_installed() + os.makedirs(".codebox", exist_ok=True) + if not self.kernel.is_alive(): + self.kernel = KernelManager() + self.kernel.start_kernel() + return CodeBoxStatus(status="started") + + def _check_installed(self) -> None: + try: + distribution("jupyter-client") + except PackageNotFoundError: + print( + "Make sure 'jupyter-client' is installed " + "when using without a CODEBOX_API_KEY.\n" + "You can install it with 'pip install jupyter-client'.\n" + ) + raise + + async def astart(self) -> CodeBoxStatus: + self._check_installed() + os.makedirs(".codebox", exist_ok=True) + if not await self.kernel._async_is_alive(): + self.kernel = KernelManager() + await self.kernel._async_start_kernel() + return CodeBoxStatus(status="started") + + def status(self) -> CodeBoxStatus: + return CodeBoxStatus(status="running" if self.kernel.is_alive() else "stopped") + + async def astatus(self) -> CodeBoxStatus: + return CodeBoxStatus( + status="running" if await self.kernel._async_is_alive() else "stopped" + ) + + def run(self, code: Union[str, PathLike]) -> CodeBoxOutput: + code = self._resolve_pathlike(code) + + msg_stream = [] + self.kernel.client().execute_interactive( + code, output_hook=lambda msg: msg_stream.append(msg) + ) + return self._parse_messages(msg_stream) + + async def arun(self, code: Union[str, PathLike]) -> CodeBoxOutput: + code = self._resolve_pathlike(code) + + msg_stream = [] + await self.kernel.client()._async_execute_interactive( + code, output_hook=lambda msg: msg_stream.append(msg) + ) + return self._parse_messages(msg_stream) + + def stream_run( + self, code: Union[str, PathLike] + ) -> Generator[CodeBoxOutput, None, None]: + code = self._resolve_pathlike(code) + msg_queue = Queue[dict | None]() + + def output_hook(msg): + msg_queue.put(msg) + + def execute_code(): + self.kernel.client().execute_interactive(code, output_hook=output_hook) + # Signal the end of execution + msg_queue.put(None) + + # Start code execution in a separate thread + execution_thread = Thread(target=execute_code) + execution_thread.start() + + # Yield messages from the queue as they arrive + while True: + msg = msg_queue.get() # This will block until a message is available + if msg is None: + break # None is used as a signal to indicate the end of execution + yield self._parse_message(msg) + + # Wait for the execution thread to finish + execution_thread.join() + + async def astream_run( + self, code: Union[str, PathLike] + ) -> AsyncGenerator[CodeBoxOutput, None]: + code = self._resolve_pathlike(code) + msg_queue: asyncio.Queue = asyncio.Queue() + + async def output_hook(msg): + await msg_queue.put(msg) + + execution_task = asyncio.create_task( + self.kernel.client()._async_execute_interactive( + code, output_hook=output_hook + ) + ) + + try: + while not execution_task.done() or not msg_queue.empty(): + msg = await msg_queue.get() + yield self._parse_message(msg) + finally: + if not execution_task.done(): + execution_task.cancel() + try: + await execution_task + except asyncio.CancelledError: + pass + + def _parse_message(self, message: dict) -> CodeBoxOutput: + msg = message + if msg["msg_type"] == "stream": + return CodeBoxOutput(content=msg["content"]["text"].strip(), type="stream") + elif msg["msg_type"] == "execute_result": + CodeBoxOutput( + content=msg["content"]["data"]["text/plain"].strip(), type="text" + ) + elif msg["msg_type"] == "display_data": + if "image/png" in msg["content"]["data"]: + return CodeBoxOutput( + type="image/png", + content=msg["content"]["data"]["image/png"], + ) + if "text/plain" in msg["content"]["data"]: + return CodeBoxOutput(type="text", content=msg["data"]["text/plain"]) + + return CodeBoxOutput(type="error", content="Could not parse output") + elif msg["msg_type"] == "error": + error = f"{msg['content']['ename']}: " f"{msg['content']['evalue']}" + if settings.verbose: + print("Error:\n", error) + return CodeBoxOutput(type="error", content=error) + return CodeBoxOutput(type="empty", content="") + + def _parse_messages(self, messages: List[dict]) -> CodeBoxOutput: + result = "" + for msg in messages: + if msg["msg_type"] == "stream": + result += msg["content"]["text"].strip() + "\n" + elif msg["msg_type"] == "execute_result": + result += msg["content"]["data"]["text/plain"].strip() + "\n" + elif msg["msg_type"] == "display_data": + if "image/png" in msg["content"]["data"]: + return CodeBoxOutput( + type="image/png", + content=msg["content"]["data"]["image/png"], + ) + if "text/plain" in msg["content"]["data"]: + return CodeBoxOutput(type="text", content=msg["data"]["text/plain"]) + + return CodeBoxOutput(type="error", content="Could not parse output") + elif ( + msg["msg_type"] == "status" + and msg["content"]["execution_state"] == "idle" + ): + if len(result) > 500: + result = "[...]\n" + result[-500:] + return CodeBoxOutput( + type="text", content=result or "run successfully (no output)" + ) + elif msg["msg_type"] == "error": + error = f"{msg['content']['ename']}: " f"{msg['content']['evalue']}" + if settings.verbose: + print("Error:\n", error) + return CodeBoxOutput(type="error", content=error) + return CodeBoxOutput(type="error", content="No output") + + def shell(self, cmd: str) -> CodeBoxOutput: + try: + result = subprocess.run( + cmd, + shell=True, + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + return CodeBoxOutput(type="text", content=result.stdout) + except subprocess.CalledProcessError as e: + return CodeBoxOutput(type="error", content=e.stderr) + + async def ashell(self, cmd: str) -> CodeBoxOutput: + try: + process = await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, stderr = await process.communicate() + if process.returncode == 0: + return CodeBoxOutput(type="text", content=stdout.decode()) + else: + return CodeBoxOutput(type="error", content=stderr.decode()) + except Exception as e: + return CodeBoxOutput(type="error", content=str(e)) + + def shell_stream(self, cmd: str) -> Generator[CodeBoxOutput, None, None]: + process = subprocess.Popen( + cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True + ) + if process.stdout: + for line in process.stdout: + yield CodeBoxOutput(type="stream", content=line.strip()) + process.wait() + if process.returncode != 0: + yield CodeBoxOutput(type="error", content="Command execution failed") + + async def ashell_stream(self, cmd: str) -> AsyncGenerator[CodeBoxOutput, None]: + process = await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.STDOUT, + ) + if process.stdout: + async for line in process.stdout: + yield CodeBoxOutput(type="stream", content=line.decode().strip()) + await process.wait() + if process.returncode != 0: + yield CodeBoxOutput(type="error", content="Command execution failed") + + def upload(self, file_name: str, content: bytes) -> CodeBoxStatus: + os.makedirs(".codebox", exist_ok=True) + with open(os.path.join(".codebox", file_name), "wb") as f: + f.write(content) + + return CodeBoxStatus(status=f"{file_name} uploaded successfully") + + async def aupload(self, file_name: str, content: bytes) -> CodeBoxStatus: + return await asyncio.to_thread(self.upload, file_name, content) + + def download(self, file_name: str) -> CodeBoxFile: + with open(os.path.join(".codebox", file_name), "rb") as f: + content = f.read() + + return CodeBoxFile(name=file_name, content=content) + + async def adownload(self, file_name: str) -> CodeBoxFile: + return await asyncio.to_thread(self.download, file_name) + + def install(self, package_name: str) -> CodeBoxStatus: + self.run(f"!pip install -q {package_name}") + self.restart() + self.run(f"try:\n import {package_name}\nexcept:\n pass") + return CodeBoxStatus(status=f"{package_name} installed successfully") + + async def ainstall(self, package_name: str) -> CodeBoxStatus: + await self.arun(f"!pip install -q {package_name}") + await self.arestart() + await self.arun(f"try:\n import {package_name}\nexcept:\n pass") + return CodeBoxStatus(status=f"{package_name} installed successfully") + + def list_files(self) -> List[CodeBoxFile]: + return [ + CodeBoxFile(name=file_name, content=None) + for file_name in os.listdir(".codebox") + ] + + async def alist_files(self) -> List[CodeBoxFile]: + return await asyncio.to_thread(self.list_files) + + def restart(self) -> CodeBoxStatus: + self.kernel.restart_kernel() + return CodeBoxStatus(status="restarted") + + async def arestart(self) -> CodeBoxStatus: + await self.kernel._async_restart_kernel() + return CodeBoxStatus(status="restarted") + + def stop(self) -> CodeBoxStatus: + self.kernel.shutdown_kernel() + return CodeBoxStatus(status="stopped") + + async def astop(self) -> CodeBoxStatus: + await self.kernel._async_shutdown_kernel() + return CodeBoxStatus(status="stopped") From e85435543d5e2c31863b65a497e462c8d443c10d Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 29 Feb 2024 14:04:44 +0700 Subject: [PATCH 010/125] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20localb?= =?UTF-8?q?ox.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/codeboxapi/box/localbox.py | 639 --------------------------------- 1 file changed, 639 deletions(-) delete mode 100644 src/codeboxapi/box/localbox.py diff --git a/src/codeboxapi/box/localbox.py b/src/codeboxapi/box/localbox.py deleted file mode 100644 index b2956be..0000000 --- a/src/codeboxapi/box/localbox.py +++ /dev/null @@ -1,639 +0,0 @@ -""" -Local implementation of CodeBox. -This is useful for testing and development.c -In case you don't put an api_key, -this is the default CodeBox. -""" - -import asyncio -import json -import os -import signal -import subprocess -import sys -import time -from asyncio.subprocess import Process -from importlib.metadata import PackageNotFoundError, distribution -from pathlib import Path -from typing import List, Optional, Union -from uuid import uuid4 - -import aiohttp -import requests -from websockets.client import WebSocketClientProtocol -from websockets.client import connect as ws_connect -from websockets.exceptions import ConnectionClosedError -from websockets.sync.client import ClientConnection -from websockets.sync.client import connect as ws_connect_sync - -from codeboxapi.box import BaseBox -from codeboxapi.schema import CodeBoxFile, CodeBoxOutput, CodeBoxStatus - -from ..config import settings - - -class LocalBox(BaseBox): - """ - LocalBox is a CodeBox implementation that runs code locally. - This is useful for testing and development. - """ - - _instance: Optional["LocalBox"] = None - _jupyter_pids: List[int] = [] - - def __new__(cls, *args, **kwargs): - if not cls._instance: - cls._instance = super().__new__(cls) - else: - if settings.SHOW_INFO: - print( - "INFO: Using a LocalBox which is not fully isolated\n" - " and not scalable across multiple users.\n" - " Make sure to use a CODEBOX_API_KEY in production.\n" - " Set envar SHOW_INFO=False to not see this again.\n" - ) - return cls._instance - - def __init__(self, /, **kwargs) -> None: - super().__init__(session_id=kwargs.pop("session_id", None)) - self.port: int = 8888 - self.kernel_id: Optional[dict] = None - self.ws: Union[WebSocketClientProtocol, ClientConnection, None] = None - self.jupyter: Union[Process, subprocess.Popen, None] = None - self.aiohttp_session: Optional[aiohttp.ClientSession] = None - - def start(self) -> CodeBoxStatus: - self.session_id = uuid4() - os.makedirs(".codebox", exist_ok=True) - self._check_port() - if settings.VERBOSE: - print("Starting kernel...") - out = None - else: - out = subprocess.PIPE - self._check_installed() - try: - python = Path(sys.executable).absolute() - self.jupyter = subprocess.Popen( - [ - python, - "-m", - "jupyter", - "kernelgateway", - "--KernelGatewayApp.ip='0.0.0.0'", - f"--KernelGatewayApp.port={self.port}", - ], - stdout=out, - stderr=out, - cwd=".codebox", - ) - self._jupyter_pids.append(self.jupyter.pid) - except FileNotFoundError: - raise ModuleNotFoundError( - "Jupyter Kernel Gateway not found, please install it with:\n" - "`pip install jupyter_kernel_gateway`\n" - "to use the LocalBox." - ) - while True: - try: - response = requests.get(self.kernel_url, timeout=270) - if response.status_code == 200: - break - except requests.exceptions.ConnectionError: - pass - if settings.VERBOSE: - print("Waiting for kernel to start...") - time.sleep(1) - self._connect() - return CodeBoxStatus(status="started") - - def _connect(self) -> None: - # Implement retry logic for kernel connection - for attempt in range(5): - try: - response = requests.post( - f"{self.kernel_url}/kernels", - headers={"Content-Type": "application/json"}, - timeout=60, - ) - if response.status_code == 201: - self.kernel_id = response.json().get("id", None) - if self.kernel_id: - break - except requests.RequestException as e: - print(f"Could not connect to kernel. {e}") - time.sleep(5) # Wait for 5 seconds before retrying - - if self.kernel_id is None: - raise Exception("Could not start kernel after multiple attempts") - - # Connect to WebSocket with retry logic - for attempt in range(5): - try: - self.ws = ws_connect_sync( - f"{self.ws_url}/kernels/{self.kernel_id}/channels", - open_timeout=60, - close_timeout=60, - ) - break # Break the loop if connection is successful - except (ConnectionClosedError, TimeoutError) as e: - print(f"Attempt {attempt + 1}: WebSocket connection failed. Error: {e}") - time.sleep(5) # Wait for 5 seconds before retrying - - if not self.ws: - raise Exception("Could not connect to WebSocket after multiple attempts") - - def _check_port(self) -> None: - try: - response = requests.get(f"http://localhost:{self.port}", timeout=270) - except requests.exceptions.ConnectionError: - pass - else: - if response.status_code == 200: - self.port += 1 - self._check_port() - - def _check_installed(self) -> None: - try: - distribution("jupyter-kernel-gateway") - except PackageNotFoundError: - print( - "Make sure 'jupyter-kernel-gateway' is installed " - "when using without a CODEBOX_API_KEY.\n" - "You can install it with 'pip install jupyter-kernel-gateway'." - ) - raise - - async def astart(self) -> CodeBoxStatus: - self.session_id = uuid4() - os.makedirs(".codebox", exist_ok=True) - self.aiohttp_session = aiohttp.ClientSession() - await self._acheck_port() - if settings.VERBOSE: - print("Starting kernel...") - out = None - else: - out = asyncio.subprocess.PIPE - self._check_installed() - python = Path(sys.executable).absolute() - try: - self.jupyter = await asyncio.create_subprocess_exec( - python, - "-m", - "jupyter", - "kernelgateway", - "--KernelGatewayApp.ip='0.0.0.0'", - f"--KernelGatewayApp.port={self.port}", - stdout=out, - stderr=out, - cwd=".codebox", - ) - self._jupyter_pids.append(self.jupyter.pid) - except Exception as e: - print(e) - raise ModuleNotFoundError( - "Jupyter Kernel Gateway not found, please install it with:\n" - "`pip install jupyter_kernel_gateway`\n" - "to use the LocalBox." - ) - while True: - try: - response = await self.aiohttp_session.get(self.kernel_url) - if response.status == 200: - break - except aiohttp.ClientConnectorError: - pass - except aiohttp.ServerDisconnectedError: - pass - if settings.VERBOSE: - print("Waiting for kernel to start...") - await asyncio.sleep(1) - await self._aconnect() - return CodeBoxStatus(status="started") - - async def _aconnect(self) -> None: - if self.aiohttp_session is None: - timeout = aiohttp.ClientTimeout(total=270) - self.aiohttp_session = aiohttp.ClientSession(timeout=timeout) - - # Implement retry logic for kernel connection - for attempt in range(5): - try: - response = await self.aiohttp_session.post( - f"{self.kernel_url}/kernels", - headers={"Content-Type": "application/json"}, - ) - if response.status == 201: - self.kernel_id = (await response.json()).get("id", None) - if self.kernel_id: - break - except aiohttp.ClientError as e: - print(f"Attempt {attempt + 1}: Could not connect to kernel. Error: {e}") - await asyncio.sleep(5) # Wait for 5 seconds before retrying - - if self.kernel_id is None: - raise Exception("Could not start kernel after multiple attempts") - - # Connect to WebSocket with increased timeout and retry logic - for attempt in range(5): - try: - self.ws = await ws_connect( - f"{self.ws_url}/kernels/{self.kernel_id}/channels", - timeout=60, - open_timeout=60, - close_timeout=60, - ) - break # Break the loop if connection is successful - except asyncio.TimeoutError as e: - print( - f"Attempt {attempt + 1}: WebSocket connection timeout. Error: {e}" - ) - await asyncio.sleep(5) # Wait for 5 seconds before retrying - - if not self.ws: - raise Exception("Could not connect to WebSocket after multiple attempts") - - async def _acheck_port(self) -> None: - try: - if self.aiohttp_session is None: - self.aiohttp_session = aiohttp.ClientSession() - response = await self.aiohttp_session.get(f"http://localhost:{self.port}") - except aiohttp.ClientConnectorError: - pass - except aiohttp.ServerDisconnectedError: - pass - else: - if response.status == 200: - self.port += 1 - await self._acheck_port() - - def status(self) -> CodeBoxStatus: - if not self.kernel_id: - self._connect() - - return CodeBoxStatus( - status="running" - if self.kernel_id - and requests.get(self.kernel_url, timeout=270).status_code == 200 - else "stopped" - ) - - async def astatus(self) -> CodeBoxStatus: - if not self.kernel_id: - await self._aconnect() - return CodeBoxStatus( - status="running" - if self.kernel_id - and self.aiohttp_session - and (await self.aiohttp_session.get(self.kernel_url)).status == 200 - else "stopped" - ) - - def run( - self, - code: Optional[str] = None, - file_path: Optional[os.PathLike] = None, - retry=3, - ) -> CodeBoxOutput: - if not code and not file_path: - raise ValueError("Code or file_path must be specified!") - - if code and file_path: - raise ValueError("Can only specify code or the file to read_from!") - - if file_path: - with open(file_path, "r", encoding="utf-8") as f: - code = f.read() - - # run code in jupyter kernel - if retry <= 0: - raise RuntimeError("Could not connect to kernel") - if not self.ws: - self._connect() - if not self.ws: - raise RuntimeError("Jupyter not running. Make sure to start it first.") - - if settings.VERBOSE: - print("Running code:\n", code) - - # send code to kernel - self.ws.send( - json.dumps( - { - "header": { - "msg_id": (msg_id := uuid4().hex), - "msg_type": "execute_request", - }, - "parent_header": {}, - "metadata": {}, - "content": { - "code": code, - "silent": False, - "store_history": True, - "user_expressions": {}, - "allow_stdin": False, - "stop_on_error": True, - }, - "channel": "shell", - "buffers": [], - } - ) - ) - result = "" - while True: - try: - if isinstance(self.ws, WebSocketClientProtocol): - raise RuntimeError("Mixing asyncio and sync code is not supported") - received_msg = json.loads(self.ws.recv()) - except ConnectionClosedError: - self.start() - return self.run(code, file_path, retry - 1) - - msg_header = received_msg.get("header", {}) - msg_parent_header = received_msg.get("parent_header", {}) - msg_content = received_msg.get("content", {}) - msg_data = msg_content.get("data", {}) - - if ( - msg_header["msg_type"] == "stream" - and msg_parent_header["msg_id"] == msg_id - ): - msg = msg_content["text"].strip() - if "Requirement already satisfied:" in msg: - continue - result += msg + "\n" - if settings.VERBOSE: - print("Output:\n", result) - - elif ( - msg_header["msg_type"] == "execute_result" - and msg_parent_header["msg_id"] == msg_id - ): - result += msg_data["text/plain"].strip() + "\n" - if settings.VERBOSE: - print("Output:\n", result) - - elif msg_header["msg_type"] == "display_data": - if "image/png" in msg_data: - return CodeBoxOutput( - type="image/png", - content=msg_data["image/png"], - ) - if "text/plain" in msg_data: - return CodeBoxOutput( - type="text", - content=msg_data["text/plain"], - ) - return CodeBoxOutput( - type="error", - content="Could not parse output", - ) - elif ( - msg_header["msg_type"] == "status" - and msg_parent_header["msg_id"] == msg_id - and msg_content["execution_state"] == "idle" - ): - if len(result) > 500: - result = "[...]\n" + result[-500:] - return CodeBoxOutput( - type="text", content=result or "code run successfully (no output)" - ) - - elif ( - msg_header["msg_type"] == "error" - and msg_parent_header["msg_id"] == msg_id - ): - error = f"{msg_content['ename']}: " f"{msg_content['evalue']}" - if settings.VERBOSE: - print("Error:\n", error) - return CodeBoxOutput(type="error", content=error) - - async def arun( - self, - code: str, - file_path: Optional[os.PathLike] = None, - retry=3, - ) -> CodeBoxOutput: - if file_path: - raise NotImplementedError( - "Reading from file is not supported in async mode" - ) - - # run code in jupyter kernel - if retry <= 0: - raise RuntimeError("Could not connect to kernel") - if not self.ws: - await self._aconnect() - - if settings.VERBOSE: - print("Running code:\n", code) - - if not isinstance(self.ws, WebSocketClientProtocol): - raise RuntimeError("Mixing asyncio and sync code is not supported") - - await self.ws.send( - json.dumps( - { - "header": { - "msg_id": (msg_id := uuid4().hex), - "msg_type": "execute_request", - }, - "parent_header": {}, - "metadata": {}, - "content": { - "code": code, - "silent": False, - "store_history": True, - "user_expressions": {}, - "allow_stdin": False, - "stop_on_error": True, - }, - "channel": "shell", - "buffers": [], - } - ) - ) - result = "" - while True: - try: - received_msg = json.loads(await self.ws.recv()) - except ConnectionClosedError: - await self.astart() - return await self.arun(code, file_path, retry - 1) - - msg_header = received_msg.get("header", {}) - msg_parent_header = received_msg.get("parent_header", {}) - msg_content = received_msg.get("content", {}) - msg_data = msg_content.get("data", {}) - - if ( - msg_header["msg_type"] == "stream" - and msg_parent_header["msg_id"] == msg_id - ): - msg = msg_content["text"].strip() - if "Requirement already satisfied:" in msg: - continue - result += msg + "\n" - if settings.VERBOSE: - print("Output:\n", result) - - elif ( - msg_header["msg_type"] == "execute_result" - and msg_parent_header["msg_id"] == msg_id - ): - result += msg_data["text/plain"].strip() + "\n" - if settings.VERBOSE: - print("Output:\n", result) - - elif msg_header["msg_type"] == "display_data": - if "image/png" in msg_data: - return CodeBoxOutput( - type="image/png", - content=msg_data["image/png"], - ) - if "text/plain" in msg_data: - return CodeBoxOutput( - type="text", - content=msg_data["text/plain"], - ) - elif ( - msg_header["msg_type"] == "status" - and msg_parent_header["msg_id"] == msg_id - and msg_content["execution_state"] == "idle" - ): - if len(result) > 500: - result = "[...]\n" + result[-500:] - return CodeBoxOutput( - type="text", content=result or "code run successfully (no output)" - ) - - elif ( - msg_header["msg_type"] == "error" - and msg_parent_header["msg_id"] == msg_id - ): - error = f"{msg_content['ename']}: " f"{msg_content['evalue']}" - if settings.VERBOSE: - print("Error:\n", error) - return CodeBoxOutput(type="error", content=error) - - def upload(self, file_name: str, content: bytes) -> CodeBoxStatus: - os.makedirs(".codebox", exist_ok=True) - with open(os.path.join(".codebox", file_name), "wb") as f: - f.write(content) - - return CodeBoxStatus(status=f"{file_name} uploaded successfully") - - async def aupload(self, file_name: str, content: bytes) -> CodeBoxStatus: - return await asyncio.to_thread(self.upload, file_name, content) - - def download(self, file_name: str) -> CodeBoxFile: - with open(os.path.join(".codebox", file_name), "rb") as f: - content = f.read() - - return CodeBoxFile(name=file_name, content=content) - - async def adownload(self, file_name: str) -> CodeBoxFile: - return await asyncio.to_thread(self.download, file_name) - - def install(self, package_name: str) -> CodeBoxStatus: - self.run(f"!pip install -q {package_name}") - self.restart() - self.run(f"try:\n import {package_name}\nexcept:\n pass") - return CodeBoxStatus(status=f"{package_name} installed successfully") - - async def ainstall(self, package_name: str) -> CodeBoxStatus: - await self.arun(f"!pip install -q {package_name}") - await self.arestart() - await self.arun(f"try:\n import {package_name}\nexcept:\n pass") - return CodeBoxStatus(status=f"{package_name} installed successfully") - - def list_files(self) -> List[CodeBoxFile]: - return [ - CodeBoxFile(name=file_name, content=None) - for file_name in os.listdir(".codebox") - ] - - async def alist_files(self) -> List[CodeBoxFile]: - return await asyncio.to_thread(self.list_files) - - def restart(self) -> CodeBoxStatus: - # self.stop() - # self.start() - return CodeBoxStatus(status="restarted") - - async def arestart(self) -> CodeBoxStatus: - # await self.astop() - # await self.astart() - return CodeBoxStatus(status="restarted") - - def stop(self) -> CodeBoxStatus: - try: - if self.jupyter is not None: - if isinstance(self.jupyter, subprocess.Popen): - self.jupyter.terminate() - self.jupyter.wait() - self.jupyter = None - time.sleep(2) - elif isinstance(self.jupyter, Process): - self.jupyter.terminate() - self.jupyter = None - time.sleep(5) - else: - for pid in self._jupyter_pids: - os.kill(pid, signal.SIGTERM) - except ProcessLookupError: - pass - - if self.ws is not None: - try: - if isinstance(self.ws, ClientConnection): - self.ws.close() - else: - loop = asyncio.new_event_loop() - loop.run_until_complete(self.ws.close()) - except ConnectionClosedError: - pass - self.ws = None - - return CodeBoxStatus(status="stopped") - - async def astop(self) -> CodeBoxStatus: - if self.jupyter is not None: - self.jupyter.terminate() - await asyncio.create_subprocess_exec("kill", "-9", str(self.jupyter.pid)) - await asyncio.sleep(5) - self.jupyter = None - - if self.ws is not None: - try: - if isinstance(self.ws, WebSocketClientProtocol): - await self.ws.close() - else: - self.ws.close() - except ConnectionClosedError: - pass - self.ws = None - - if self.aiohttp_session is not None: - await self.aiohttp_session.close() - self.aiohttp_session = None - - return CodeBoxStatus(status="stopped") - - @property - def kernel_url(self) -> str: - """Return the url of the kernel.""" - return f"http://localhost:{self.port}/api" - - @property - def ws_url(self) -> str: - """Return the url of the websocket.""" - return f"ws://localhost:{self.port}/api" - - def __del__(self): - self.stop() - - if self.aiohttp_session is not None: - loop = asyncio.new_event_loop() - loop.run_until_complete(self.aiohttp_session.close()) - self.aiohttp_session = None From 6cecfacecd7d537f94d30cfca55f542cbe0ca60e Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 29 Feb 2024 14:04:44 +0700 Subject: [PATCH 011/125] =?UTF-8?q?=F0=9F=9A=9A=20Rename=20remote.py=20fil?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/codeboxapi/box/remote.py | 295 +++++++++++++++++++++++++++++++++++ 1 file changed, 295 insertions(+) create mode 100644 src/codeboxapi/box/remote.py diff --git a/src/codeboxapi/box/remote.py b/src/codeboxapi/box/remote.py new file mode 100644 index 0000000..92d709b --- /dev/null +++ b/src/codeboxapi/box/remote.py @@ -0,0 +1,295 @@ +""" +CodeBox API Wrapper +~~~~~~~~~~~~~~~~~~~ + +A basic wrapper for the CodeBox API. + +Usage +----- + +.. code-block:: python + + from codeboxapi import CodeBox + + with CodeBox() as codebox: + codebox.status() + codebox.run(code="print('Hello World!')") + codebox.install("python-package") + codebox.upload("test.txt", b"Hello World!") + codebox.list_files() + codebox.download("test.txt") + +.. code-block:: python + + from codeboxapi import CodeBox + + async with CodeBox() as codebox: + await codebox.astatus() + await codebox.arun(code="print('Hello World!')") + await codebox.ainstall("python-package") + await codebox.aupload("test.txt", b"Hello World!") + await codebox.alist_files() + await codebox.adownload("test.txt") + +""" + +from os import PathLike +from typing import Any, Dict, List, Optional, Union +from uuid import UUID, uuid4 + +from aiohttp import ClientSession + +from ..config import settings +from ..schema import CodeBoxFile, CodeBoxOutput, CodeBoxStatus +from ..utils import abase_request, base_request +from .base import BaseBox + + +class RemoteBox(BaseBox): + """ + Sandboxed Python Interpreter + """ + + def __new__(cls, *args, **kwargs): + if kwargs.pop("local", False) or settings.api_key == "local": + from .local import LocalBox + + return LocalBox(*args, **kwargs) + + return super().__new__(cls) + + def __init__(self, session_id: Optional[str] = None, **kwargs) -> None: + self._temp_id_cache = uuid4().hex + super().__init__(session_id or self._temp_id_cache, **kwargs) + self.aiohttp_session: Optional[ClientSession] = None + + @classmethod + def from_id(cls, session_id: Union[int, UUID, str], **kwargs) -> "RemoteBox": + return cls( + session_id=( + UUID(int=session_id).hex + if isinstance(session_id, int) + else session_id.hex + if isinstance(session_id, UUID) + else session_id + ), + **kwargs, + ) + + def codebox_request(self, method, endpoint, *args, **kwargs) -> Dict[str, Any]: + """General request to the CodeBox API""" + self._update() + # temp fix + session_id = UUID(self.session_id).int + return base_request( + method, f"/codebox/{session_id}" + endpoint, *args, **kwargs + ) + + async def acodebox_request( + self, method, endpoint, *args, **kwargs + ) -> Dict[str, Any]: + """General async request to the CodeBox API""" + self._update() + if self.aiohttp_session is None: + self.aiohttp_session = ClientSession() + # temp fix + session_id = UUID(self.session_id).int + return await abase_request( + self.aiohttp_session, + method, + f"/codebox/{session_id}" + endpoint, + *args, + **kwargs, + ) + + # def start(self) -> CodeBoxStatus: + # return self.status() + + # async def astart(self) -> CodeBoxStatus: + # return await self.astatus() + + def start(self) -> CodeBoxStatus: + if self.session_id != self._temp_id_cache: + return CodeBoxStatus(status="started") + self.session_id = UUID( + int=base_request( + method="GET", + endpoint="/codebox/start", + )["id"] + ).hex + return CodeBoxStatus(status="started") + + async def astart(self) -> CodeBoxStatus: + self.aiohttp_session = ClientSession() + if self.session_id != self._temp_id_cache: + return CodeBoxStatus(status="started") + self.session_id = UUID( + int=( + await abase_request( + self.aiohttp_session, method="GET", endpoint="/codebox/start" + ) + )["id"] + ).hex + return CodeBoxStatus(status="started") + + def status(self): + return CodeBoxStatus( + **self.codebox_request( + method="GET", + endpoint="/", + ) + ) + + async def astatus(self): + return CodeBoxStatus( + **await self.acodebox_request( + method="GET", + endpoint="/", + ) + ) + + def run(self, code: Union[str, PathLike]) -> CodeBoxOutput: + return CodeBoxOutput( + **self.codebox_request( + method="POST", + endpoint="/run", + body={"code": self._resolve_pathlike(code)}, + ) + ) + + async def arun(self, code: Union[str, PathLike]) -> CodeBoxOutput: + return CodeBoxOutput( + **await self.acodebox_request( + method="POST", + endpoint="/run", + body={"code": self._resolve_pathlike(code)}, + ) + ) + + # TODO: STREAMING + + # TODO: SHELL + + def upload(self, file_name: str, content: bytes) -> CodeBoxStatus: + return CodeBoxStatus( + **self.codebox_request( + method="POST", + endpoint="/upload", + files={"file": (file_name, content)}, + ) + ) + + async def aupload(self, file_name: str, content: bytes) -> CodeBoxStatus: + return CodeBoxStatus( + **await self.acodebox_request( + method="POST", + endpoint="/upload", + files={"file": (file_name, content)}, + ) + ) + + def download(self, file_name: str) -> CodeBoxFile: + return CodeBoxFile( + **self.codebox_request( + method="GET", + endpoint="/download", + body={"file_name": file_name}, + ) + ) + + async def adownload(self, file_name: str) -> CodeBoxFile: + return CodeBoxFile( + **await self.acodebox_request( + method="GET", + endpoint="/download", + body={"file_name": file_name}, + ) + ) + + def install(self, package_name: str) -> CodeBoxStatus: + return CodeBoxStatus( + **self.codebox_request( + method="POST", + endpoint="/install", + body={ + "package_name": package_name, + }, + ) + ) + + async def ainstall(self, package_name: str) -> CodeBoxStatus: + return CodeBoxStatus( + **await self.acodebox_request( + method="POST", + endpoint="/install", + body={ + "package_name": package_name, + }, + ) + ) + + def list_files(self) -> List[CodeBoxFile]: + return [ + CodeBoxFile(name=file_name, content=None) + for file_name in ( + self.codebox_request( + method="GET", + endpoint="/files", + ) + )["files"] + ] + + async def alist_files(self) -> List[CodeBoxFile]: + return [ + CodeBoxFile(name=file_name, content=None) + for file_name in ( + await self.acodebox_request( + method="GET", + endpoint="/files", + ) + )["files"] + ] + + def restart(self) -> CodeBoxStatus: + return CodeBoxStatus( + **self.codebox_request( + method="POST", + endpoint="/restart", + ) + ) + + async def arestart(self) -> CodeBoxStatus: + return CodeBoxStatus( + **await self.acodebox_request( + method="POST", + endpoint="/restart", + ) + ) + + def stop(self) -> CodeBoxStatus: + return CodeBoxStatus( + **self.codebox_request( + method="POST", + endpoint="/stop", + ) + ) + + async def astop(self) -> CodeBoxStatus: + status = CodeBoxStatus( + **await self.acodebox_request( + method="POST", + endpoint="/stop", + ) + ) + if self.aiohttp_session: + await self.aiohttp_session.close() + self.aiohttp_session = None + return status + + def __del__(self): + if self.aiohttp_session: + import asyncio + + loop = asyncio.new_event_loop() + loop.run_until_complete(self.aiohttp_session.close()) + self.aiohttp_session = None From c9ebbef127c9b7d344871af75f7d17e060bff911 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 29 Feb 2024 14:04:44 +0700 Subject: [PATCH 012/125] =?UTF-8?q?=F0=9F=94=A7=20Refactor=20config=20hand?= =?UTF-8?q?ling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/codeboxapi/config.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/codeboxapi/config.py b/src/codeboxapi/config.py index 02ebb2e..0b6fd85 100644 --- a/src/codeboxapi/config.py +++ b/src/codeboxapi/config.py @@ -3,26 +3,25 @@ Automatically loads environment variables from .env file """ -from typing import Optional - -from dotenv import load_dotenv from pydantic_settings import BaseSettings -# .env file -load_dotenv("./.env") - class CodeBoxSettings(BaseSettings): """ CodeBox API Config """ - VERBOSE: bool = False - SHOW_INFO: bool = True + verbose: bool = False + show_info: bool = True + + api_key: str = "local" + base_url: str = "https://codeboxapi.com/api/v1" + timeout: int = 20 - CODEBOX_API_KEY: Optional[str] = None - CODEBOX_BASE_URL: str = "https://codeboxapi.com/api/v1" - CODEBOX_TIMEOUT: int = 20 + class Config: + env_file = ".env" + env_prefix = "CODEBOX_" + ignore_extra = True settings = CodeBoxSettings() From 5f15c9bde7f722d93c20f9c3fbe64896705f5ff6 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 29 Feb 2024 14:04:44 +0700 Subject: [PATCH 013/125] =?UTF-8?q?=F0=9F=94=84=20Update=20error=20handlin?= =?UTF-8?q?g=20structure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/codeboxapi/errors.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/codeboxapi/errors.py b/src/codeboxapi/errors.py index d6d1232..eeeb9a9 100644 --- a/src/codeboxapi/errors.py +++ b/src/codeboxapi/errors.py @@ -12,18 +12,20 @@ class CodeBoxError(Exception): def __init__( self, http_status: int = 0, - json_body: dict = {}, + content: str = "error", headers: dict = {}, + body: dict = {}, **kwargs, ): super().__init__(**kwargs) self.http_status = http_status - self.json_body = json_body + self.content = content self.headers = headers + self.body = body def __str__(self): - return f"{self.http_status}: {self.json_body}" + return f"{self.http_status}: {self.content}" def __repr__(self): - return f"" + return f"" From 2ed3d4fe13e7a61d4ecd5b7b8f13cf4e59f75678 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 29 Feb 2024 14:04:44 +0700 Subject: [PATCH 014/125] =?UTF-8?q?=E2=9C=A8=20Add=20py.typed=20for=20mypy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/codeboxapi/py.typed | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/codeboxapi/py.typed diff --git a/src/codeboxapi/py.typed b/src/codeboxapi/py.typed new file mode 100644 index 0000000..e69de29 From 67bb9068fd026648914bb1ce83beb442e57a9721 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 29 Feb 2024 14:04:44 +0700 Subject: [PATCH 015/125] =?UTF-8?q?=F0=9F=94=84=20Refactor=20utils=20modul?= =?UTF-8?q?e=20paths?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/codeboxapi/utils.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/codeboxapi/utils.py b/src/codeboxapi/utils.py index 2e4ebab..a984330 100644 --- a/src/codeboxapi/utils.py +++ b/src/codeboxapi/utils.py @@ -8,8 +8,8 @@ from aiohttp import ClientResponse, ClientSession, FormData from aiohttp.payload import BytesIOPayload -from codeboxapi.config import settings -from codeboxapi.errors import CodeBoxError +from .config import settings +from .errors import CodeBoxError def build_request_data( @@ -23,9 +23,9 @@ def build_request_data( """ return { "method": method, - "url": settings.CODEBOX_BASE_URL + endpoint, + "url": settings.base_url + endpoint, "headers": { - "Authorization": f"Bearer {settings.CODEBOX_API_KEY}", + "Authorization": f"Bearer {settings.api_key}", }, "json": body, "files": files, @@ -50,7 +50,7 @@ def handle_response(response: requests.Response): if response.status_code != 200: raise CodeBoxError( http_status=response.status_code, - json_body=response.json(), + content=response.content.decode(), headers=dict(response.headers.items()), ) return handler(response) @@ -70,21 +70,26 @@ async def file_handler(r: ClientResponse) -> dict: "name": r.headers["Content-Disposition"].split("=")[1], } + async def text_handler(r: ClientResponse) -> dict: + return {"content": await r.text()} + async def default_handler(r: ClientResponse) -> dict: return {"content": await r.text()} handlers = { "application/json": json_handler, "application/octet-stream": file_handler, + "text/plain": text_handler, # Add other content type handlers here } handler = handlers.get( response.headers["Content-Type"].split(";")[0], default_handler ) if response.status != 200: + error_content = await handler(response) raise CodeBoxError( http_status=response.status, - json_body=await response.json(), + content=str(error_content), headers=dict(response.headers.items()), ) return await handler(response) @@ -139,4 +144,4 @@ def set_api_key(api_key: str) -> None: """ Manually set the CODEBOX_API_KEY. """ - settings.CODEBOX_API_KEY = api_key + settings.api_key = api_key From 630254d8eca45a99003ef8c8e72f5c13f2c18e70 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 29 Feb 2024 14:06:16 +0700 Subject: [PATCH 016/125] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Removed=20baseb?= =?UTF-8?q?ox.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/codeboxapi/box/basebox.py | 114 ---------------------------------- 1 file changed, 114 deletions(-) delete mode 100644 src/codeboxapi/box/basebox.py diff --git a/src/codeboxapi/box/basebox.py b/src/codeboxapi/box/basebox.py deleted file mode 100644 index d958547..0000000 --- a/src/codeboxapi/box/basebox.py +++ /dev/null @@ -1,114 +0,0 @@ -""" Abstract Base Class for Isolated Execution Environments (CodeBox's) """ - -from abc import ABC, abstractmethod -from datetime import datetime -from os import PathLike -from typing import List, Optional -from uuid import UUID - -from codeboxapi.schema import CodeBoxFile, CodeBoxOutput, CodeBoxStatus - - -class BaseBox(ABC): - """CodeBox Abstract Base Class""" - - def __init__(self, session_id: Optional[UUID] = None) -> None: - """Initialize the CodeBox instance""" - self.session_id = session_id - self.last_interaction = datetime.now() - - @abstractmethod - def start(self) -> CodeBoxStatus: - """Startup the CodeBox instance""" - - @abstractmethod - async def astart(self) -> CodeBoxStatus: - """Async Startup the CodeBox instance""" - - @abstractmethod - def status(self) -> CodeBoxStatus: - """Get the current status of the CodeBox instance""" - - @abstractmethod - async def astatus(self) -> CodeBoxStatus: - """Async Get the current status of the CodeBox instance""" - - @abstractmethod - def run( - self, code: Optional[str] = None, file_path: Optional[PathLike] = None - ) -> CodeBoxOutput: - """Execute python code inside the CodeBox instance""" - - @abstractmethod - async def arun( - self, code: str, file_path: Optional[PathLike] = None - ) -> CodeBoxOutput: - """Async Execute python code inside the CodeBox instance""" - - @abstractmethod - def upload(self, file_name: str, content: bytes) -> CodeBoxStatus: - """Upload a file as bytes to the CodeBox instance""" - - @abstractmethod - async def aupload(self, file_name: str, content: bytes) -> CodeBoxStatus: - """Async Upload a file as bytes to the CodeBox instance""" - - @abstractmethod - def download(self, file_name: str) -> CodeBoxFile: - """Download a file as CodeBoxFile schema""" - - @abstractmethod - async def adownload(self, file_name: str) -> CodeBoxFile: - """Async Download a file as CodeBoxFile schema""" - - @abstractmethod - def install(self, package_name: str) -> CodeBoxStatus: - """Install a python package to the venv""" - - @abstractmethod - async def ainstall(self, package_name: str) -> CodeBoxStatus: - """Async Install a python package to the venv""" - - @abstractmethod - def list_files(self) -> List[CodeBoxFile]: - """List all available files inside the CodeBox instance""" - - @abstractmethod - async def alist_files(self) -> List[CodeBoxFile]: - """Async List all available files inside the CodeBox instance""" - - @abstractmethod - def restart(self) -> CodeBoxStatus: - """Restart the jupyter kernel inside the CodeBox instance""" - - @abstractmethod - async def arestart(self) -> CodeBoxStatus: - """Async Restart the jupyter kernel inside the CodeBox instance""" - - @abstractmethod - def stop(self) -> CodeBoxStatus: - """Terminate the CodeBox instance""" - - @abstractmethod - async def astop(self) -> CodeBoxStatus: - """Async Terminate the CodeBox instance""" - - def __enter__(self) -> "BaseBox": - self.start() - return self - - async def __aenter__(self) -> "BaseBox": - await self.astart() - return self - - def __exit__(self, exc_type, exc_value, traceback) -> None: - self.stop() - - async def __aexit__(self, exc_type, exc_value, traceback) -> None: - await self.astop() - - def __repr__(self) -> str: - return f"<{self.__class__.__name__} id={self.session_id}>" - - def __str__(self) -> str: - return self.__repr__() From 78d5ac65d007d501400f9868f9a75fda1cbcb5ac Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 29 Feb 2024 14:06:16 +0700 Subject: [PATCH 017/125] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Removed=20codeb?= =?UTF-8?q?ox.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/codeboxapi/box/codebox.py | 318 ---------------------------------- 1 file changed, 318 deletions(-) delete mode 100644 src/codeboxapi/box/codebox.py diff --git a/src/codeboxapi/box/codebox.py b/src/codeboxapi/box/codebox.py deleted file mode 100644 index 77e63fb..0000000 --- a/src/codeboxapi/box/codebox.py +++ /dev/null @@ -1,318 +0,0 @@ -""" -CodeBox API Wrapper -~~~~~~~~~~~~~~~~~~~ - -A basic wrapper for the CodeBox API. - -Usage ------ - -.. code-block:: python - - from codeboxapi import CodeBox - - with CodeBox() as codebox: - codebox.status() - codebox.run(code="print('Hello World!')") - codebox.install("python-package") - codebox.upload("test.txt", b"Hello World!") - codebox.list_files() - codebox.download("test.txt") - -.. code-block:: python - - from codeboxapi import CodeBox - - async with CodeBox() as codebox: - await codebox.astatus() - await codebox.arun(code="print('Hello World!')") - await codebox.ainstall("python-package") - await codebox.aupload("test.txt", b"Hello World!") - await codebox.alist_files() - await codebox.adownload("test.txt") - -""" - -from datetime import datetime -from os import PathLike -from typing import Any, Dict, List, Optional, Union -from uuid import UUID - -from aiohttp import ClientSession - -from codeboxapi.box.basebox import BaseBox -from codeboxapi.config import settings -from codeboxapi.schema import CodeBoxFile, CodeBoxOutput, CodeBoxStatus -from codeboxapi.utils import abase_request, base_request - - -class CodeBox(BaseBox): - """ - Sandboxed Python Interpreter - """ - - def __new__(cls, *args, **kwargs): - if ( - kwargs.pop("local", False) - or settings.CODEBOX_API_KEY is None - or settings.CODEBOX_API_KEY == "local" - ): - from .localbox import LocalBox - - return LocalBox(*args, **kwargs) - - return super().__new__(cls) - - def __init__(self, /, **kwargs) -> None: - super().__init__() - self.session_id: Optional[UUID] = kwargs.pop("session_id", None) - self.aiohttp_session: Optional[ClientSession] = None - - @classmethod - def from_id(cls, session_id: Union[int, UUID], **kwargs) -> "CodeBox": - kwargs["session_id"] = ( - UUID(int=session_id) if isinstance(session_id, int) else session_id - ) - return cls(**kwargs) - - def _update(self) -> None: - """Update last interaction time""" - self.last_interaction = datetime.now() - - def codebox_request(self, method, endpoint, *args, **kwargs) -> Dict[str, Any]: - """Basic request to the CodeBox API""" - self._update() - if self.session_id is None: - raise RuntimeError("Make sure to start your CodeBox before using it.") - return base_request( - method, f"/codebox/{self.session_id.int}" + endpoint, *args, **kwargs - ) - - async def acodebox_request( - self, method, endpoint, *args, **kwargs - ) -> Dict[str, Any]: - """Basic async request to the CodeBox API""" - self._update() - if self.aiohttp_session is None: - self.aiohttp_session = ClientSession() - if self.session_id is None: - raise RuntimeError("Make sure to start your CodeBox before using it.") - return await abase_request( - self.aiohttp_session, - method, - f"/codebox/{self.session_id.int}" + endpoint, - *args, - **kwargs, - ) - - def start(self) -> CodeBoxStatus: - if self.session_id is not None: - if settings.VERBOSE: - print(f"{self} is already started!") - return CodeBoxStatus(status="started") - self.session_id = UUID( - int=base_request( - method="GET", - endpoint="/codebox/start", - )["id"] - ) - if settings.VERBOSE: - print(f"{self} started!") - return CodeBoxStatus(status="started") - - async def astart(self) -> CodeBoxStatus: - self.aiohttp_session = ClientSession() - if self.session_id is not None: - if settings.VERBOSE: - print(f"{self} is already started!") - return CodeBoxStatus(status="started") - self.session_id = UUID( - int=( - await abase_request( - self.aiohttp_session, method="GET", endpoint="/codebox/start" - ) - )["id"] - ) - if settings.VERBOSE: - print(f"{self} started!") - return CodeBoxStatus(status="started") - - def status(self): - return CodeBoxStatus( - **self.codebox_request( - method="GET", - endpoint="/", - ) - ) - - async def astatus(self): - return CodeBoxStatus( - **await self.acodebox_request( - method="GET", - endpoint="/", - ) - ) - - def run( - self, code: Optional[str] = None, file_path: Optional[PathLike] = None - ) -> CodeBoxOutput: - if not code and not file_path: # R0801 - raise ValueError("Code or file_path must be specified!") - - if code and file_path: - raise ValueError("Can only specify code or the file to read_from!") - - if file_path: - with open(file_path, "r", encoding="utf-8") as f: - code = f.read() - - return CodeBoxOutput( - **self.codebox_request( - method="POST", - endpoint="/run", - body={"code": code}, - ) - ) - - async def arun( - self, code: str, file_path: Optional[PathLike] = None - ) -> CodeBoxOutput: - if file_path: # TODO: Implement this - raise NotImplementedError( - "Reading from FilePath is not supported in async mode yet!" - ) - - return CodeBoxOutput( - **await self.acodebox_request( - method="POST", - endpoint="/run", - body={"code": code}, - ) - ) - - def upload(self, file_name: str, content: bytes) -> CodeBoxStatus: - return CodeBoxStatus( - **self.codebox_request( - method="POST", - endpoint="/upload", - files={"file": (file_name, content)}, - ) - ) - - async def aupload(self, file_name: str, content: bytes) -> CodeBoxStatus: - return CodeBoxStatus( - **await self.acodebox_request( - method="POST", - endpoint="/upload", - files={"file": (file_name, content)}, - ) - ) - - def download(self, file_name: str) -> CodeBoxFile: - return CodeBoxFile( - **self.codebox_request( - method="GET", - endpoint="/download", - body={"file_name": file_name}, - ) - ) - - async def adownload(self, file_name: str) -> CodeBoxFile: - return CodeBoxFile( - **await self.acodebox_request( - method="GET", - endpoint="/download", - body={"file_name": file_name}, - ) - ) - - def install(self, package_name: str) -> CodeBoxStatus: - return CodeBoxStatus( - **self.codebox_request( - method="POST", - endpoint="/install", - body={ - "package_name": package_name, - }, - ) - ) - - async def ainstall(self, package_name: str) -> CodeBoxStatus: - return CodeBoxStatus( - **await self.acodebox_request( - method="POST", - endpoint="/install", - body={ - "package_name": package_name, - }, - ) - ) - - def list_files(self) -> List[CodeBoxFile]: - return [ - CodeBoxFile(name=file_name, content=None) - for file_name in ( - self.codebox_request( - method="GET", - endpoint="/files", - ) - )["files"] - ] - - async def alist_files(self) -> List[CodeBoxFile]: - return [ - CodeBoxFile(name=file_name, content=None) - for file_name in ( - await self.acodebox_request( - method="GET", - endpoint="/files", - ) - )["files"] - ] - - def restart(self) -> CodeBoxStatus: - return CodeBoxStatus( - **self.codebox_request( - method="POST", - endpoint="/restart", - ) - ) - - async def arestart(self) -> CodeBoxStatus: - return CodeBoxStatus( - **await self.acodebox_request( - method="POST", - endpoint="/restart", - ) - ) - - def stop(self) -> CodeBoxStatus: - status = CodeBoxStatus( - **self.codebox_request( - method="POST", - endpoint="/stop", - ) - ) - self.session_id = None - return status - - async def astop(self) -> CodeBoxStatus: - status = CodeBoxStatus( - **await self.acodebox_request( - method="POST", - endpoint="/stop", - ) - ) - self.session_id = None - if self.aiohttp_session: - await self.aiohttp_session.close() - self.aiohttp_session = None - return status - - def __del__(self): - if self.aiohttp_session: - import asyncio - - loop = asyncio.new_event_loop() - loop.run_until_complete(self.aiohttp_session.close()) - self.aiohttp_session = None From 3e7c3ed297f6f3d9e8235b1133386aa12cc8809d Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sat, 2 Mar 2024 13:48:12 +0700 Subject: [PATCH 018/125] =?UTF-8?q?=E2=9C=A8=20Update=20ruff=20settings=20?= =?UTF-8?q?in=20pyproject.toml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 621979f..1e42cc1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,5 +63,9 @@ packages = ["src/codeboxapi"] [tool.pytest.ini_options] filterwarnings = "ignore::DeprecationWarning" -[tool.ruff] +[tool.ruff.lint] select = ["E", "F", "I"] +ignore = ["E701"] + +[tool.ruff.format] +preview = true From 5f4da64d7ca99bb5feb3b887ca57428c27f85bbf Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sat, 2 Mar 2024 13:48:12 +0700 Subject: [PATCH 019/125] =?UTF-8?q?=F0=9F=94=BC=20Upgrade=20pydantic=20to?= =?UTF-8?q?=202.6.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.lock b/requirements.lock index bec0ae5..014286f 100644 --- a/requirements.lock +++ b/requirements.lock @@ -29,7 +29,7 @@ idna==3.6 multidict==6.0.5 # via aiohttp # via yarl -pydantic==2.6.2 +pydantic==2.6.3 # via codeboxapi # via pydantic-settings pydantic-core==2.16.3 From d671fc1ad9f07833a64b16c8a01d958bbcd3faed Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sat, 2 Mar 2024 13:48:12 +0700 Subject: [PATCH 020/125] =?UTF-8?q?=F0=9F=94=A7=20Enhance=20LocalBox=20fun?= =?UTF-8?q?ctionality?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/codeboxapi/box/local.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/codeboxapi/box/local.py b/src/codeboxapi/box/local.py index f846999..e39c710 100644 --- a/src/codeboxapi/box/local.py +++ b/src/codeboxapi/box/local.py @@ -8,14 +8,15 @@ import asyncio import os import subprocess +from asyncio import sleep as asleep from importlib.metadata import PackageNotFoundError, distribution from os import PathLike from queue import Queue from threading import Thread +from time import sleep from typing import AsyncGenerator, Generator, List, Optional, Union from jupyter_client.manager import KernelManager -from rich import print from ..box import BaseBox from ..config import settings @@ -71,7 +72,9 @@ async def astart(self) -> CodeBoxStatus: self._check_installed() os.makedirs(".codebox", exist_ok=True) if not await self.kernel._async_is_alive(): - self.kernel = KernelManager() + self.kernel = KernelManager( + ip=os.getenv("LOCALHOST", "127.0.0.1"), + ) await self.kernel._async_start_kernel() return CodeBoxStatus(status="started") @@ -86,6 +89,9 @@ async def astatus(self) -> CodeBoxStatus: def run(self, code: Union[str, PathLike]) -> CodeBoxOutput: code = self._resolve_pathlike(code) + if settings.verbose: + print(f"\033[90m{code}\033[0m") + msg_stream = [] self.kernel.client().execute_interactive( code, output_hook=lambda msg: msg_stream.append(msg) @@ -309,10 +315,12 @@ async def alist_files(self) -> List[CodeBoxFile]: def restart(self) -> CodeBoxStatus: self.kernel.restart_kernel() + sleep(3) return CodeBoxStatus(status="restarted") async def arestart(self) -> CodeBoxStatus: await self.kernel._async_restart_kernel() + await asleep(3) return CodeBoxStatus(status="restarted") def stop(self) -> CodeBoxStatus: From 80a362da6c390884cdeb6d172c181c086b5cd784 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sat, 2 Mar 2024 13:48:13 +0700 Subject: [PATCH 021/125] =?UTF-8?q?=F0=9F=93=9D=20Update=20docstring=20for?= =?UTF-8?q?matting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/codeboxapi/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/codeboxapi/utils.py b/src/codeboxapi/utils.py index a984330..af5144a 100644 --- a/src/codeboxapi/utils.py +++ b/src/codeboxapi/utils.py @@ -1,4 +1,4 @@ -""" Utility functions for API requests """ +"""Utility functions for API requests""" import json from io import BytesIO From 2b7a55947d6837e10c8d06724a32255ce899624c Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sat, 2 Mar 2024 13:48:13 +0700 Subject: [PATCH 022/125] =?UTF-8?q?=F0=9F=94=87=20Disable=20test=5Fcodebox?= =?UTF-8?q?=20execution?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/general_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/general_test.py b/tests/general_test.py index acc21aa..bc08c2b 100644 --- a/tests/general_test.py +++ b/tests/general_test.py @@ -82,5 +82,5 @@ async def run_async(codebox: CodeBox) -> bool: if __name__ == "__main__": - test_codebox() + # test_codebox() test_localbox() From 4cb1b3ef73219b0836ca103ef0b4c10c2794f5dc Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sat, 2 Mar 2024 16:06:07 +0700 Subject: [PATCH 023/125] =?UTF-8?q?=F0=9F=93=81=20Use=20dynamic=20working?= =?UTF-8?q?=20directory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/codeboxapi/box/local.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/codeboxapi/box/local.py b/src/codeboxapi/box/local.py index e39c710..868152c 100644 --- a/src/codeboxapi/box/local.py +++ b/src/codeboxapi/box/local.py @@ -48,10 +48,11 @@ def __init__(self, /, **kwargs) -> None: os.environ["PYDEVD_DISABLE_FILE_VALIDATION"] = "1" super().__init__(session_id=kwargs.pop("session_id", "local")) self.kernel = KernelManager() + self.cwd = settings.default_working_dir def start(self) -> CodeBoxStatus: self._check_installed() - os.makedirs(".codebox", exist_ok=True) + os.makedirs(self.cwd, exist_ok=True) if not self.kernel.is_alive(): self.kernel = KernelManager() self.kernel.start_kernel() @@ -70,7 +71,7 @@ def _check_installed(self) -> None: async def astart(self) -> CodeBoxStatus: self._check_installed() - os.makedirs(".codebox", exist_ok=True) + os.makedirs(self.cwd, exist_ok=True) if not await self.kernel._async_is_alive(): self.kernel = KernelManager( ip=os.getenv("LOCALHOST", "127.0.0.1"), @@ -274,8 +275,8 @@ async def ashell_stream(self, cmd: str) -> AsyncGenerator[CodeBoxOutput, None]: yield CodeBoxOutput(type="error", content="Command execution failed") def upload(self, file_name: str, content: bytes) -> CodeBoxStatus: - os.makedirs(".codebox", exist_ok=True) - with open(os.path.join(".codebox", file_name), "wb") as f: + os.makedirs(self.cwd, exist_ok=True) + with open(os.path.join(self.cwd, file_name), "wb") as f: f.write(content) return CodeBoxStatus(status=f"{file_name} uploaded successfully") @@ -284,7 +285,7 @@ async def aupload(self, file_name: str, content: bytes) -> CodeBoxStatus: return await asyncio.to_thread(self.upload, file_name, content) def download(self, file_name: str) -> CodeBoxFile: - with open(os.path.join(".codebox", file_name), "rb") as f: + with open(os.path.join(self.cwd, file_name), "rb") as f: content = f.read() return CodeBoxFile(name=file_name, content=content) @@ -307,7 +308,7 @@ async def ainstall(self, package_name: str) -> CodeBoxStatus: def list_files(self) -> List[CodeBoxFile]: return [ CodeBoxFile(name=file_name, content=None) - for file_name in os.listdir(".codebox") + for file_name in os.listdir(self.cwd) ] async def alist_files(self) -> List[CodeBoxFile]: From cbffe53dd975cef866e2fc2c4aaa719b25637692 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sat, 2 Mar 2024 16:06:07 +0700 Subject: [PATCH 024/125] =?UTF-8?q?=E2=9C=A8=20Add=20default=5Fworking=5Fd?= =?UTF-8?q?ir=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/codeboxapi/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/codeboxapi/config.py b/src/codeboxapi/config.py index 0b6fd85..7ac0d0d 100644 --- a/src/codeboxapi/config.py +++ b/src/codeboxapi/config.py @@ -15,6 +15,7 @@ class CodeBoxSettings(BaseSettings): show_info: bool = True api_key: str = "local" + default_working_dir: str = ".codebox" base_url: str = "https://codeboxapi.com/api/v1" timeout: int = 20 From 4cf247d168c51f5d7265e2dd2793af6b9f047dc4 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sat, 2 Mar 2024 16:06:07 +0700 Subject: [PATCH 025/125] =?UTF-8?q?=E2=9C=85=20Enable=20test=5Fcodebox()?= =?UTF-8?q?=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/general_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/general_test.py b/tests/general_test.py index bc08c2b..82f6caa 100644 --- a/tests/general_test.py +++ b/tests/general_test.py @@ -82,5 +82,5 @@ async def run_async(codebox: CodeBox) -> bool: if __name__ == "__main__": - # test_codebox() - test_localbox() + test_codebox() + # test_localbox() From ed0d817558ff02c4bb7ebcde32b707865bc5ef69 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 3 Mar 2024 11:11:01 +0700 Subject: [PATCH 026/125] fix --- examples/localbox.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/examples/localbox.py b/examples/localbox.py index 204887e..633beda 100644 --- a/examples/localbox.py +++ b/examples/localbox.py @@ -1,6 +1,7 @@ -from codeboxapi.box import LocalBox +from codeboxapi import CodeBox -with LocalBox() as box: - box.run("print('Hello, world!')") - box.install("pandas") - v = box.run("pandas.__version__") +with CodeBox() as box: + v = box.install("pandas") + print(v) + r = box.run("import pandas; pandas.__version__") + print(r) From 140a336cff3d6f8e9b792f32cb3a036732f0b313 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sat, 23 Mar 2024 13:19:27 +0100 Subject: [PATCH 027/125] =?UTF-8?q?=F0=9F=94=A7=20fix=20ignoring=20extra?= =?UTF-8?q?=20env=20vars?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/codeboxapi/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/codeboxapi/config.py b/src/codeboxapi/config.py index 7ac0d0d..eccb435 100644 --- a/src/codeboxapi/config.py +++ b/src/codeboxapi/config.py @@ -22,7 +22,7 @@ class CodeBoxSettings(BaseSettings): class Config: env_file = ".env" env_prefix = "CODEBOX_" - ignore_extra = True + extra = "ignore" settings = CodeBoxSettings() From 58ef6e276bff47427152a36529d94e9283aafa3f Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sat, 23 Mar 2024 17:47:53 +0100 Subject: [PATCH 028/125] =?UTF-8?q?=F0=9F=94=A5=20Delete=20.vscode/setting?= =?UTF-8?q?s.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 12901f9..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "python.analysis.typeCheckingMode": "basic" -} From bb7fb710e96513f2da6472ffe34b8d0f1bbe3884 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sat, 23 Mar 2024 17:47:53 +0100 Subject: [PATCH 029/125] =?UTF-8?q?=E2=9C=A8=20Enhance=20codeboxapi=20with?= =?UTF-8?q?=20RemoteBox=20support=20and=20engine=20parameter=20in=20LocalB?= =?UTF-8?q?ox=20run=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/codeboxapi/__init__.py | 1 + src/codeboxapi/box/__init__.py | 2 ++ src/codeboxapi/box/local.py | 8 ++++++-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/codeboxapi/__init__.py b/src/codeboxapi/__init__.py index be59947..c0dd3ad 100644 --- a/src/codeboxapi/__init__.py +++ b/src/codeboxapi/__init__.py @@ -5,6 +5,7 @@ The package includes modules for configuring the client, setting the API key, and interacting with Codebox instances. """ + from .box import CodeBox from .config import settings diff --git a/src/codeboxapi/box/__init__.py b/src/codeboxapi/box/__init__.py index da8dd20..5c70f80 100644 --- a/src/codeboxapi/box/__init__.py +++ b/src/codeboxapi/box/__init__.py @@ -7,10 +7,12 @@ from .base import BaseBox from .local import LocalBox +from .remote import RemoteBox from .remote import RemoteBox as CodeBox __all__ = [ "BaseBox", "CodeBox", "LocalBox", + "RemoteBox", ] diff --git a/src/codeboxapi/box/local.py b/src/codeboxapi/box/local.py index 868152c..8d66e00 100644 --- a/src/codeboxapi/box/local.py +++ b/src/codeboxapi/box/local.py @@ -14,7 +14,7 @@ from queue import Queue from threading import Thread from time import sleep -from typing import AsyncGenerator, Generator, List, Optional, Union +from typing import AsyncGenerator, Generator, List, Literal, Optional, Union from jupyter_client.manager import KernelManager @@ -87,7 +87,11 @@ async def astatus(self) -> CodeBoxStatus: status="running" if await self.kernel._async_is_alive() else "stopped" ) - def run(self, code: Union[str, PathLike]) -> CodeBoxOutput: + def run( + self, + code: Union[str, PathLike], + engine: Literal["python", "shell"] = "python", + ) -> CodeBoxOutput: code = self._resolve_pathlike(code) if settings.verbose: From 32cedd4f26ed7d832a7f6ada0aa41bb759247308 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sat, 23 Mar 2024 17:47:53 +0100 Subject: [PATCH 030/125] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20tests?= =?UTF-8?q?=20to=20simplify=20asyncio.gather=20call?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/run_all_examples.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/run_all_examples.py b/tests/run_all_examples.py index 0d4257c..dd4ffe1 100644 --- a/tests/run_all_examples.py +++ b/tests/run_all_examples.py @@ -22,12 +22,10 @@ async def run_examples(): if os.environ.get("CODEBOX_API_KEY") is None: return print("Skipping remote examples because CODEBOX_API_KEY is not set") - await asyncio.gather( - *[ - asyncio.create_task(run_example(file)) - for file in list(Path("examples").glob("**/*.py")) - ] - ) + await asyncio.gather(*[ + asyncio.create_task(run_example(file)) + for file in list(Path("examples").glob("**/*.py")) + ]) async def run_examples_local(): From ec3881215eb329a7d916f5857d388e7b4e6ea7ef Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 25 Mar 2024 12:30:15 +0100 Subject: [PATCH 031/125] =?UTF-8?q?=F0=9F=93=A6=20Update=20pydantic=20vers?= =?UTF-8?q?ion=20from=202.6.3=20to=202.6.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.lock b/requirements.lock index 014286f..9f287e4 100644 --- a/requirements.lock +++ b/requirements.lock @@ -29,7 +29,7 @@ idna==3.6 multidict==6.0.5 # via aiohttp # via yarl -pydantic==2.6.3 +pydantic==2.6.4 # via codeboxapi # via pydantic-settings pydantic-core==2.16.3 From 7cbd8d77a3a02c72a3f00db4ab871aea370bee73 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 25 Mar 2024 12:30:15 +0100 Subject: [PATCH 032/125] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20Refactor=20Loca?= =?UTF-8?q?lBox=20class:=20remove=20Literal=20import,=20add=20IP=20configu?= =?UTF-8?q?ration=20for=20KernelManager,=20simplify=20run=20and=20install?= =?UTF-8?q?=20methods,=20and=20handle=20package=20installation=20errors=20?= =?UTF-8?q?more=20robustly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/codeboxapi/box/local.py | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/codeboxapi/box/local.py b/src/codeboxapi/box/local.py index 8d66e00..4370688 100644 --- a/src/codeboxapi/box/local.py +++ b/src/codeboxapi/box/local.py @@ -14,7 +14,7 @@ from queue import Queue from threading import Thread from time import sleep -from typing import AsyncGenerator, Generator, List, Literal, Optional, Union +from typing import AsyncGenerator, Generator, List, Optional, Union from jupyter_client.manager import KernelManager @@ -54,7 +54,9 @@ def start(self) -> CodeBoxStatus: self._check_installed() os.makedirs(self.cwd, exist_ok=True) if not self.kernel.is_alive(): - self.kernel = KernelManager() + self.kernel = KernelManager( + ip=os.getenv("LOCALHOST", "127.0.0.1"), + ) self.kernel.start_kernel() return CodeBoxStatus(status="started") @@ -87,11 +89,7 @@ async def astatus(self) -> CodeBoxStatus: status="running" if await self.kernel._async_is_alive() else "stopped" ) - def run( - self, - code: Union[str, PathLike], - engine: Literal["python", "shell"] = "python", - ) -> CodeBoxOutput: + def run(self, code: Union[str, PathLike]) -> CodeBoxOutput: code = self._resolve_pathlike(code) if settings.verbose: @@ -298,15 +296,27 @@ async def adownload(self, file_name: str) -> CodeBoxFile: return await asyncio.to_thread(self.download, file_name) def install(self, package_name: str) -> CodeBoxStatus: - self.run(f"!pip install -q {package_name}") + if "ERROR" in str(logs := self.run(f"!uv pip install {package_name}")): + return CodeBoxStatus(status="Error: " + logs.content) self.restart() - self.run(f"try:\n import {package_name}\nexcept:\n pass") + if "No module named" in str( + logs := self.run( + f"try:\n import {package_name}\nexcept Exception as e:\n print(e)" + ) + ): + return CodeBoxStatus(status="Error: " + logs.content) return CodeBoxStatus(status=f"{package_name} installed successfully") async def ainstall(self, package_name: str) -> CodeBoxStatus: - await self.arun(f"!pip install -q {package_name}") + if "ERROR" in str(logs := await self.arun(f"!uv pip install {package_name}")): + return CodeBoxStatus(status="Error: " + logs.content) await self.arestart() - await self.arun(f"try:\n import {package_name}\nexcept:\n pass") + if "No module named" in str( + logs := await self.arun( + f"try:\n import {package_name}\nexcept Exception as e:\n print(e)" + ) + ): + return CodeBoxStatus(status="Error: " + logs.content) return CodeBoxStatus(status=f"{package_name} installed successfully") def list_files(self) -> List[CodeBoxFile]: From 32baae43d1e95646b3240e8c3dbb601d31a872f0 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 25 Mar 2024 16:43:12 +0100 Subject: [PATCH 033/125] =?UTF-8?q?=F0=9F=90=9B=20Fix=20pip=20install=20co?= =?UTF-8?q?mmand=20in=20LocalBox=20install=20and=20ainstall=20methods?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/codeboxapi/box/local.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/codeboxapi/box/local.py b/src/codeboxapi/box/local.py index 4370688..5f38125 100644 --- a/src/codeboxapi/box/local.py +++ b/src/codeboxapi/box/local.py @@ -296,7 +296,7 @@ async def adownload(self, file_name: str) -> CodeBoxFile: return await asyncio.to_thread(self.download, file_name) def install(self, package_name: str) -> CodeBoxStatus: - if "ERROR" in str(logs := self.run(f"!uv pip install {package_name}")): + if "ERROR" in str(logs := self.run(f"!pip install {package_name}")): return CodeBoxStatus(status="Error: " + logs.content) self.restart() if "No module named" in str( @@ -308,7 +308,7 @@ def install(self, package_name: str) -> CodeBoxStatus: return CodeBoxStatus(status=f"{package_name} installed successfully") async def ainstall(self, package_name: str) -> CodeBoxStatus: - if "ERROR" in str(logs := await self.arun(f"!uv pip install {package_name}")): + if "ERROR" in str(logs := await self.arun(f"!pip install {package_name}")): return CodeBoxStatus(status="Error: " + logs.content) await self.arestart() if "No module named" in str( From ad3c331b96bb419c18b8e286886f62be140a7857 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 26 Mar 2024 16:20:29 +0100 Subject: [PATCH 034/125] =?UTF-8?q?=F0=9F=94=A7=20improve=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/general_test.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/general_test.py b/tests/general_test.py index 82f6caa..299453d 100644 --- a/tests/general_test.py +++ b/tests/general_test.py @@ -21,15 +21,17 @@ def run_sync(codebox: CodeBox) -> bool: assert codebox.status() == "running" - assert codebox.run("print('Hello World!')") == "Hello World!\n" + codebox.run("x = 'Hello World!'") + + assert codebox.run("print(x)") == "Hello World!\n" file_name = "test_file.txt" assert file_name in str(codebox.upload(file_name, b"Hello World!")) assert codebox.download(file_name).content == b"Hello World!" - package_name = "matplotlib" - assert package_name in str(codebox.install(package_name)) + assert "matplotlib" in str(codebox.install("matplotlib")) + assert ( "error" != codebox.run("import matplotlib; print(matplotlib.__version__)").type @@ -51,17 +53,18 @@ async def run_async(codebox: CodeBox) -> bool: try: assert await codebox.astart() == "started" + print(await codebox.astatus()) assert await codebox.astatus() == "running" - assert await codebox.arun("print('Hello World!')") == "Hello World!\n" + await codebox.arun("x = 'Hello World!'") + assert await codebox.arun("print(x)") == "Hello World!\n" file_name = "test_file.txt" assert file_name in str(await codebox.aupload(file_name, b"Hello World!")) assert (await codebox.adownload(file_name)).content == b"Hello World!" - package_name = "matplotlib" - assert package_name in str(await codebox.ainstall(package_name)) + assert "matplotlib" in str(await codebox.ainstall("matplotlib")) assert ( "error" != ( From c60a01e94e5c0b6020ab24f334838212f05aa8c9 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 27 Mar 2024 15:05:15 +0100 Subject: [PATCH 035/125] =?UTF-8?q?=F0=9F=94=A7=20Refactor=20RemoteBox=20c?= =?UTF-8?q?lass=20to=20use=20conditional=20assignment=20for=20aiohttp=5Fse?= =?UTF-8?q?ssion=20and=20add=20sleep=20logic=20to=20start=20and=20astart?= =?UTF-8?q?=20methods?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/codeboxapi/box/remote.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/codeboxapi/box/remote.py b/src/codeboxapi/box/remote.py index 92d709b..8b63099 100644 --- a/src/codeboxapi/box/remote.py +++ b/src/codeboxapi/box/remote.py @@ -33,7 +33,9 @@ """ +from asyncio import sleep as asleep from os import PathLike +from time import sleep from typing import Any, Dict, List, Optional, Union from uuid import UUID, uuid4 @@ -90,8 +92,7 @@ async def acodebox_request( ) -> Dict[str, Any]: """General async request to the CodeBox API""" self._update() - if self.aiohttp_session is None: - self.aiohttp_session = ClientSession() + self.aiohttp_session = self.aiohttp_session or ClientSession() # temp fix session_id = UUID(self.session_id).int return await abase_request( @@ -110,7 +111,13 @@ async def acodebox_request( def start(self) -> CodeBoxStatus: if self.session_id != self._temp_id_cache: - return CodeBoxStatus(status="started") + print( + "if self.session_id != self._temp_id_cache: " + f"{self.session_id} != {self._temp_id_cache}" + ) + while self.status().status == "starting": + sleep(1) + return self.status() self.session_id = UUID( int=base_request( method="GET", @@ -120,9 +127,15 @@ def start(self) -> CodeBoxStatus: return CodeBoxStatus(status="started") async def astart(self) -> CodeBoxStatus: - self.aiohttp_session = ClientSession() + self.aiohttp_session = self.aiohttp_session or ClientSession() if self.session_id != self._temp_id_cache: - return CodeBoxStatus(status="started") + print( + "if self.session_id != self._temp_id_cache: " + f"{self.session_id} != {self._temp_id_cache}" + ) + while self.status().status == "starting": + await asleep(1) + return await self.astatus() self.session_id = UUID( int=( await abase_request( From b8a2d406ad1fb213c0e777f856f4bcef3bd49ae4 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 27 Mar 2024 15:32:36 +0100 Subject: [PATCH 036/125] =?UTF-8?q?=F0=9F=A7=BD=20pre-commit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 29 +++++++---------------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 99aa3ad..ae6724d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,23 +1,8 @@ repos: - -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 - hooks: - - id: check-yaml - - id: end-of-file-fixer - - id: trailing-whitespace - -- repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.8.0 - hooks: - - id: mypy - args: [--ignore-missing-imports, --follow-imports=skip] - additional_dependencies: [types-requests] - -- repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.8 - hooks: - - id: ruff - args: [ --fix ] - - id: ruff-format - types_or: [ python, pyi, jupyter ] + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.8 + hooks: + - id: ruff + args: [--fix] + - id: ruff-format + types_or: [python, pyi, jupyter] From b7976faa2aa8978d22d540cc8176456f716fbd26 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 29 Apr 2024 16:15:19 +0200 Subject: [PATCH 037/125] fix --- src/codeboxapi/box/remote.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/codeboxapi/box/remote.py b/src/codeboxapi/box/remote.py index 8b63099..5aa74ce 100644 --- a/src/codeboxapi/box/remote.py +++ b/src/codeboxapi/box/remote.py @@ -133,7 +133,7 @@ async def astart(self) -> CodeBoxStatus: "if self.session_id != self._temp_id_cache: " f"{self.session_id} != {self._temp_id_cache}" ) - while self.status().status == "starting": + while (await self.astatus()).status == "starting": await asleep(1) return await self.astatus() self.session_id = UUID( From 6ea7d2f66e242e161f27d8fafddd281dec665d5c Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 2 May 2024 21:14:27 +0200 Subject: [PATCH 038/125] =?UTF-8?q?=F0=9F=94=A7=20Refactor=20RemoteBox=20m?= =?UTF-8?q?ethods=20and=20improve=20logging=20in=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/codeboxapi/box/remote.py | 13 ++++--------- tests/general_test.py | 5 ++++- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/codeboxapi/box/remote.py b/src/codeboxapi/box/remote.py index 5aa74ce..55cc47a 100644 --- a/src/codeboxapi/box/remote.py +++ b/src/codeboxapi/box/remote.py @@ -84,7 +84,10 @@ def codebox_request(self, method, endpoint, *args, **kwargs) -> Dict[str, Any]: # temp fix session_id = UUID(self.session_id).int return base_request( - method, f"/codebox/{session_id}" + endpoint, *args, **kwargs + method, + f"/codebox/{session_id}" + endpoint, + *args, + **kwargs, ) async def acodebox_request( @@ -111,10 +114,6 @@ async def acodebox_request( def start(self) -> CodeBoxStatus: if self.session_id != self._temp_id_cache: - print( - "if self.session_id != self._temp_id_cache: " - f"{self.session_id} != {self._temp_id_cache}" - ) while self.status().status == "starting": sleep(1) return self.status() @@ -129,10 +128,6 @@ def start(self) -> CodeBoxStatus: async def astart(self) -> CodeBoxStatus: self.aiohttp_session = self.aiohttp_session or ClientSession() if self.session_id != self._temp_id_cache: - print( - "if self.session_id != self._temp_id_cache: " - f"{self.session_id} != {self._temp_id_cache}" - ) while (await self.astatus()).status == "starting": await asleep(1) return await self.astatus() diff --git a/tests/general_test.py b/tests/general_test.py index 299453d..a5185a0 100644 --- a/tests/general_test.py +++ b/tests/general_test.py @@ -18,8 +18,10 @@ def test_localbox(): def run_sync(codebox: CodeBox) -> bool: try: assert codebox.start() == "started" + print(codebox.status()) assert codebox.status() == "running" + print(codebox.status()) codebox.run("x = 'Hello World!'") @@ -52,9 +54,10 @@ def run_sync(codebox: CodeBox) -> bool: async def run_async(codebox: CodeBox) -> bool: try: assert await codebox.astart() == "started" - print(await codebox.astatus()) + assert await codebox.astatus() == "running" + print(await codebox.astatus()) await codebox.arun("x = 'Hello World!'") assert await codebox.arun("print(x)") == "Hello World!\n" From 6f85896be36e21f2043b6026a98be888d223bfd5 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 9 May 2024 14:03:27 +0200 Subject: [PATCH 039/125] =?UTF-8?q?=F0=9F=94=A7=20Refactor=20shell=20comma?= =?UTF-8?q?nd=20execution=20to=20support=20multiple=20arguments=20and=20im?= =?UTF-8?q?prove=20package=20installation=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/codeboxapi/box/local.py | 54 ++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/src/codeboxapi/box/local.py b/src/codeboxapi/box/local.py index 5f38125..4a008d8 100644 --- a/src/codeboxapi/box/local.py +++ b/src/codeboxapi/box/local.py @@ -96,7 +96,7 @@ def run(self, code: Union[str, PathLike]) -> CodeBoxOutput: print(f"\033[90m{code}\033[0m") msg_stream = [] - self.kernel.client().execute_interactive( + self.kernel.client().execute_interactive( # type: ignore code, output_hook=lambda msg: msg_stream.append(msg) ) return self._parse_messages(msg_stream) @@ -223,10 +223,10 @@ def _parse_messages(self, messages: List[dict]) -> CodeBoxOutput: return CodeBoxOutput(type="error", content=error) return CodeBoxOutput(type="error", content="No output") - def shell(self, cmd: str) -> CodeBoxOutput: + def shell(self, *cmd: str) -> CodeBoxOutput: try: result = subprocess.run( - cmd, + " ".join(cmd), shell=True, check=True, stdout=subprocess.PIPE, @@ -237,10 +237,10 @@ def shell(self, cmd: str) -> CodeBoxOutput: except subprocess.CalledProcessError as e: return CodeBoxOutput(type="error", content=e.stderr) - async def ashell(self, cmd: str) -> CodeBoxOutput: + async def ashell(self, *cmd: str) -> CodeBoxOutput: try: process = await asyncio.create_subprocess_shell( - cmd, + " ".join(cmd), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) @@ -252,9 +252,13 @@ async def ashell(self, cmd: str) -> CodeBoxOutput: except Exception as e: return CodeBoxOutput(type="error", content=str(e)) - def shell_stream(self, cmd: str) -> Generator[CodeBoxOutput, None, None]: + def shell_stream(self, *cmd: str) -> Generator[CodeBoxOutput, None, None]: process = subprocess.Popen( - cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True + " ".join(cmd), + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, ) if process.stdout: for line in process.stdout: @@ -263,9 +267,9 @@ def shell_stream(self, cmd: str) -> Generator[CodeBoxOutput, None, None]: if process.returncode != 0: yield CodeBoxOutput(type="error", content="Command execution failed") - async def ashell_stream(self, cmd: str) -> AsyncGenerator[CodeBoxOutput, None]: + async def ashell_stream(self, *cmd: str) -> AsyncGenerator[CodeBoxOutput, None]: process = await asyncio.create_subprocess_shell( - cmd, + " ".join(cmd), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT, ) @@ -295,29 +299,41 @@ def download(self, file_name: str) -> CodeBoxFile: async def adownload(self, file_name: str) -> CodeBoxFile: return await asyncio.to_thread(self.download, file_name) - def install(self, package_name: str) -> CodeBoxStatus: - if "ERROR" in str(logs := self.run(f"!pip install {package_name}")): - return CodeBoxStatus(status="Error: " + logs.content) + def install(self, *package_names: str) -> CodeBoxStatus: + packages = " ".join(package_names) + if "bin/uv" in self.shell("which uv").content: + out = self.shell("uv pip install", packages) + else: + out = self.shell("pip install", packages) + + if out.type == "error": + return CodeBoxStatus(status="Error: " + out.content) self.restart() if "No module named" in str( logs := self.run( - f"try:\n import {package_name}\nexcept Exception as e:\n print(e)" + f"try:\n import {packages}\nexcept Exception as e:\n print(e)" ) ): return CodeBoxStatus(status="Error: " + logs.content) - return CodeBoxStatus(status=f"{package_name} installed successfully") + return CodeBoxStatus(status=f"{packages} installed successfully") - async def ainstall(self, package_name: str) -> CodeBoxStatus: - if "ERROR" in str(logs := await self.arun(f"!pip install {package_name}")): - return CodeBoxStatus(status="Error: " + logs.content) + async def ainstall(self, *package_names: str) -> CodeBoxStatus: + packages = " ".join(package_names) + if "bin/uv" in (await self.arun("which uv")).content: + out = await self.arun(f"uv pip install {packages}") + else: + out = await self.arun(f"pip install {packages}") + + if out.type == "error": + return CodeBoxStatus(status="Error: " + out.content) await self.arestart() if "No module named" in str( logs := await self.arun( - f"try:\n import {package_name}\nexcept Exception as e:\n print(e)" + f"try:\n import {packages}\nexcept Exception as e:\n print(e)" ) ): return CodeBoxStatus(status="Error: " + logs.content) - return CodeBoxStatus(status=f"{package_name} installed successfully") + return CodeBoxStatus(status=f"{packages} installed successfully") def list_files(self) -> List[CodeBoxFile]: return [ From 72094d76b544d489767f44f565b5fd3af1e59635 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 9 May 2024 14:03:27 +0200 Subject: [PATCH 040/125] =?UTF-8?q?=E2=9C=85=20Enhance=20test=20coverage?= =?UTF-8?q?=20for=20codebox=20API=20with=20additional=20assertions=20and?= =?UTF-8?q?=20print=20statements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/general_test.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/tests/general_test.py b/tests/general_test.py index a5185a0..365605e 100644 --- a/tests/general_test.py +++ b/tests/general_test.py @@ -19,18 +19,22 @@ def run_sync(codebox: CodeBox) -> bool: try: assert codebox.start() == "started" print(codebox.status()) + print("Started") assert codebox.status() == "running" print(codebox.status()) + print("Running") codebox.run("x = 'Hello World!'") - assert codebox.run("print(x)") == "Hello World!\n" + print("Printed") file_name = "test_file.txt" assert file_name in str(codebox.upload(file_name, b"Hello World!")) + print("Uploaded") assert codebox.download(file_name).content == b"Hello World!" + print("Downloaded") assert "matplotlib" in str(codebox.install("matplotlib")) @@ -38,15 +42,18 @@ def run_sync(codebox: CodeBox) -> bool: "error" != codebox.run("import matplotlib; print(matplotlib.__version__)").type ) + print("Installed") o = codebox.run( "import matplotlib.pyplot as plt;" "plt.plot([1, 2, 3, 4], [1, 4, 2, 3]); plt.show()" ) assert o.type == "image/png" + print("Plotted") finally: assert codebox.stop() == "stopped" + print("Stopped") return True @@ -55,17 +62,22 @@ async def run_async(codebox: CodeBox) -> bool: try: assert await codebox.astart() == "started" print(await codebox.astatus()) + print("Started") assert await codebox.astatus() == "running" print(await codebox.astatus()) + print("Running") await codebox.arun("x = 'Hello World!'") assert await codebox.arun("print(x)") == "Hello World!\n" + print("Printed") file_name = "test_file.txt" assert file_name in str(await codebox.aupload(file_name, b"Hello World!")) + print("Uploaded") assert (await codebox.adownload(file_name)).content == b"Hello World!" + print("Downloaded") assert "matplotlib" in str(await codebox.ainstall("matplotlib")) assert ( @@ -74,15 +86,18 @@ async def run_async(codebox: CodeBox) -> bool: await codebox.arun("import matplotlib; print(matplotlib.__version__)") ).type ) + print("Installed") o = await codebox.arun( "import matplotlib.pyplot as plt;" "plt.plot([1, 2, 3, 4], [1, 4, 2, 3]); plt.show()" ) assert o.type == "image/png" + print("Plotted") finally: assert await codebox.astop() == "stopped" + print("Stopped") return True From 171bbed712637e2d168ceee3f1771931c044af39 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 13 May 2024 12:20:02 -0600 Subject: [PATCH 041/125] =?UTF-8?q?=F0=9F=94=A7=20fix=20file=20io=20due=20?= =?UTF-8?q?to=20wrong=20cwd?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/codeboxapi/box/local.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/codeboxapi/box/local.py b/src/codeboxapi/box/local.py index 4a008d8..5ebd6a4 100644 --- a/src/codeboxapi/box/local.py +++ b/src/codeboxapi/box/local.py @@ -57,7 +57,7 @@ def start(self) -> CodeBoxStatus: self.kernel = KernelManager( ip=os.getenv("LOCALHOST", "127.0.0.1"), ) - self.kernel.start_kernel() + self.kernel.start_kernel(cwd=self.cwd) return CodeBoxStatus(status="started") def _check_installed(self) -> None: @@ -78,7 +78,7 @@ async def astart(self) -> CodeBoxStatus: self.kernel = KernelManager( ip=os.getenv("LOCALHOST", "127.0.0.1"), ) - await self.kernel._async_start_kernel() + await self.kernel._async_start_kernel(cwd=self.cwd) return CodeBoxStatus(status="started") def status(self) -> CodeBoxStatus: From 84e0b21965f30c49409f28ad6b4c94f0638c1d6e Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 28 May 2024 17:34:52 -0500 Subject: [PATCH 042/125] wip --- src/codeboxapi/box/__init__.py | 2 +- src/codeboxapi/box/base.py | 132 ++++++++++++++++----------------- src/codeboxapi/box/factory.py | 16 ++++ 3 files changed, 79 insertions(+), 71 deletions(-) create mode 100644 src/codeboxapi/box/factory.py diff --git a/src/codeboxapi/box/__init__.py b/src/codeboxapi/box/__init__.py index 5c70f80..4b0d2d6 100644 --- a/src/codeboxapi/box/__init__.py +++ b/src/codeboxapi/box/__init__.py @@ -8,7 +8,7 @@ from .base import BaseBox from .local import LocalBox from .remote import RemoteBox -from .remote import RemoteBox as CodeBox +from .factory import CodeBox __all__ = [ "BaseBox", diff --git a/src/codeboxapi/box/base.py b/src/codeboxapi/box/base.py index 0c6cf22..cfecfca 100644 --- a/src/codeboxapi/box/base.py +++ b/src/codeboxapi/box/base.py @@ -1,122 +1,114 @@ -""" Abstract Base Class for Isolated Execution Environments (CodeBox's) """ +"""Abstract Base Class for Isolated Execution Environments (CodeBox's)""" from abc import ABC, abstractmethod -from datetime import datetime +from dataclasses import dataclass from os import PathLike -from typing import List, Union +from typing import AsyncIterator, BinaryIO, Iterator, Literal, TypedDict -from codeboxapi.schema import CodeBoxFile, CodeBoxOutput, CodeBoxStatus +class ExecResult(TypedDict): + content: str + content_type: Literal["text", "image"] + error: str | None -class BaseBox(ABC): - """CodeBox Abstract Base Class""" - - def __init__(self, session_id: str = "local") -> None: - """Initialize the CodeBox instance""" - self.session_id = session_id - self.last_interaction = datetime.now() - @abstractmethod - def start(self) -> CodeBoxStatus: - """Startup the CodeBox instance""" - - @abstractmethod - async def astart(self) -> CodeBoxStatus: - """Async Startup the CodeBox instance""" +@dataclass +class MetaFile: ... - @abstractmethod - def status(self) -> CodeBoxStatus: - """Get the current status of the CodeBox instance""" - @abstractmethod - async def astatus(self) -> CodeBoxStatus: - """Async Get the current status of the CodeBox instance""" +class BaseBox(ABC): + """CodeBox Abstract Base Class""" @abstractmethod - def run(self, code: Union[str, PathLike]) -> CodeBoxOutput: + def exec( + self, + code: str | PathLike, + language: Literal["python", "bash"] = "python", + ) -> ExecResult: """Execute python code inside the CodeBox instance""" @abstractmethod - async def arun(self, code: Union[str, PathLike]) -> CodeBoxOutput: + async def aexec( + self, + code: str | PathLike, + language: Literal["python", "bash"] = "python", + ) -> ExecResult: """Async Execute python code inside the CodeBox instance""" - # TODO: STREAMING + @abstractmethod + def stream_exec( + self, + code: str | PathLike, + language: Literal["python", "bash"] = "python", + ) -> Iterator[ExecResult]: + """Stream Chunks of Execute python code inside the CodeBox instance""" - # TODO: SHELL + @abstractmethod + async def astream_exec( + self, + code: str | PathLike, + language: Literal["python", "bash"] = "python", + ) -> AsyncIterator[ExecResult]: + """Async Stream Chunks of Execute python code inside the CodeBox instance""" @abstractmethod - def upload(self, file_name: str, content: bytes) -> CodeBoxStatus: + def upload( + self, + file_name: str, + content: BinaryIO | bytes | str, + ) -> MetaFile: """Upload a file as bytes to the CodeBox instance""" @abstractmethod - async def aupload(self, file_name: str, content: bytes) -> CodeBoxStatus: + async def aupload( + self, + data: BinaryIO, + remote_file_path: str, + ) -> MetaFile: """Async Upload a file as bytes to the CodeBox instance""" @abstractmethod - def download(self, file_name: str) -> CodeBoxFile: + def download(self, remote_file_path: str) -> BinaryIO: """Download a file as CodeBoxFile schema""" @abstractmethod - async def adownload(self, file_name: str) -> CodeBoxFile: + async def adownload(self, remote_file_path: str) -> BinaryIO: """Async Download a file as CodeBoxFile schema""" @abstractmethod - def install(self, package_name: str) -> CodeBoxStatus: + def install( + self, + packages: list[str], + installer: Literal["pip", "apt"], + ) -> bool: """Install a python package to the venv""" @abstractmethod - async def ainstall(self, package_name: str) -> CodeBoxStatus: + async def ainstall( + self, + packages: str, + installer: Literal["pip", "apt"], + ) -> bool: """Async Install a python package to the venv""" @abstractmethod - def list_files(self) -> List[CodeBoxFile]: + def list_files(self) -> list[MetaFile]: """List all available files inside the CodeBox instance""" @abstractmethod - async def alist_files(self) -> List[CodeBoxFile]: + async def alist_files(self) -> list[MetaFile]: """Async List all available files inside the CodeBox instance""" @abstractmethod - def restart(self) -> CodeBoxStatus: + def restart(self) -> bool: """Restart the jupyter kernel inside the CodeBox instance""" @abstractmethod - async def arestart(self) -> CodeBoxStatus: + async def arestart(self) -> bool: """Async Restart the jupyter kernel inside the CodeBox instance""" - @abstractmethod - def stop(self) -> CodeBoxStatus: - """Terminate the CodeBox instance""" - - @abstractmethod - async def astop(self) -> CodeBoxStatus: - """Async Terminate the CodeBox instance""" - - def _update(self) -> None: - self.last_interaction = datetime.now() - - def _resolve_pathlike(self, code: Union[str, PathLike]) -> str: + def _resolve_pathlike(self, code: str | PathLike) -> str: if isinstance(code, PathLike): with open(code, "r", encoding="utf-8") as f: return f.read() return code - - def __enter__(self) -> "BaseBox": - self.start() - return self - - async def __aenter__(self) -> "BaseBox": - await self.astart() - return self - - def __exit__(self, exc_type, exc_value, traceback) -> None: - self.stop() - - async def __aexit__(self, exc_type, exc_value, traceback) -> None: - await self.astop() - - def __repr__(self) -> str: - return f"<{self.__class__.__name__} id={self.session_id}>" - - def __str__(self) -> str: - return self.__repr__() diff --git a/src/codeboxapi/box/factory.py b/src/codeboxapi/box/factory.py new file mode 100644 index 0000000..c1deddf --- /dev/null +++ b/src/codeboxapi/box/factory.py @@ -0,0 +1,16 @@ +from .base import BaseBox +from .local import LocalBox +from .remote import RemoteBox + + +def CodeBox( + session_id: str | None = None, + api_key: str = "local", +) -> BaseBox: + if session_id is None: + if api_key == "local": + return LocalBox() + else: + return RemoteBox() + else: + return RemoteBox(session_id) From 15a19a55aa3f7b8072685a4387c66d3b12bb1988 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 20 Jun 2024 19:28:46 -0700 Subject: [PATCH 043/125] wip --- .gitignore | 1 + docs/settings.md | 3 ++- pyproject.toml | 14 ++++---------- requirements.lock | 41 ++++++++++++++++++----------------------- roadmap.todo | 2 ++ 5 files changed, 27 insertions(+), 34 deletions(-) diff --git a/.gitignore b/.gitignore index d36542a..ddec77d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ site .python-version requirements-dev.lock +.aider* \ No newline at end of file diff --git a/docs/settings.md b/docs/settings.md index e5da440..4ee927a 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -5,6 +5,7 @@ The configuration settings are encapsulated within the `CodeBoxSettings` class, which inherits from Pydantic's `BaseSettings` class. `codeboxapi/config.py` + ```python class CodeBoxSettings(BaseSettings): ... @@ -22,7 +23,7 @@ class CodeBoxSettings(BaseSettings): ### CodeBox API Settings -- `CODEBOX_API_KEY: Optional[str] = None` +- `CODEBOX_API_KEY: str | None = None` The API key for CodeBox. - `CODEBOX_BASE_URL: str = "https://codeboxapi.com/api/v1"` diff --git a/pyproject.toml b/pyproject.toml index 75c5ebf..7beaa53 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,8 +13,8 @@ authors = [{ name = "Shroominic", email = "contact@shroominic.com" }] dependencies = [ "pydantic>=2", "pydantic-settings>=2", - "requests>=2", - "aiohttp>=3.9", + "httpx>=0.27.0", + "httpx-sse>=0.4.0", # todo check if really needed ] readme = "README.md" requires-python = ">= 3.9" @@ -35,16 +35,10 @@ dev-dependencies = [ "codeboxapi[all]", "mypy>=1.8", "ruff>=0.1", - "pytest>=7.4", - "pre-commit>=3.5", + "pytest-asyncio>=0.23.7", + # docs "neoteroi-mkdocs>=1", "mkdocs-material>=9", - "types-requests>=2.31", - "matplotlib>=3.8.2", - "jupyter-client>=8.6.0", - "ipykernel>=6.29.3", - "pip", - "rich>=13.7.0", ] [project.optional-dependencies] diff --git a/requirements.lock b/requirements.lock index fc66ac8..eff049f 100644 --- a/requirements.lock +++ b/requirements.lock @@ -6,29 +6,27 @@ # features: [] # all-features: false # with-sources: false +# generate-hashes: false -e file:. -aiohttp==3.9.5 - # via codeboxapi -aiosignal==1.3.1 - # via aiohttp annotated-types==0.6.0 # via pydantic -attrs==23.2.0 - # via aiohttp +anyio==4.4.0 + # via httpx certifi==2024.2.2 - # via requests -charset-normalizer==3.3.2 - # via requests -frozenlist==1.4.1 - # via aiohttp - # via aiosignal + # via httpcore + # via httpx +h11==0.14.0 + # via httpcore +httpcore==1.0.5 + # via httpx +httpx==0.27.0 + # via codeboxapi +httpx-sse==0.4.0 + # via codeboxapi idna==3.7 - # via requests - # via yarl -multidict==6.0.5 - # via aiohttp - # via yarl + # via anyio + # via httpx pydantic==2.7.1 # via codeboxapi # via pydantic-settings @@ -38,12 +36,9 @@ pydantic-settings==2.2.1 # via codeboxapi python-dotenv==1.0.1 # via pydantic-settings -requests==2.31.0 - # via codeboxapi +sniffio==1.3.1 + # via anyio + # via httpx typing-extensions==4.11.0 # via pydantic # via pydantic-core -urllib3==2.2.1 - # via requests -yarl==1.9.4 - # via aiohttp diff --git a/roadmap.todo b/roadmap.todo index d5ed364..29d14ae 100644 --- a/roadmap.todo +++ b/roadmap.todo @@ -19,3 +19,5 @@ [ ] - chromiumbox + vectorbox [ ] - gitbox + +[ ] - seperate .venv for localbox managed by uv From 26e7f1bea313acb79712f60e1fa9c2f8bab21217 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 20 Jun 2024 19:29:01 -0700 Subject: [PATCH 044/125] rm old examples --- examples/async_file_io.py | 44 --------------------- examples/async_plot_dataset.py | 72 ---------------------------------- examples/localbox.py | 7 ---- examples/session_restoring.py | 22 ----------- 4 files changed, 145 deletions(-) delete mode 100644 examples/async_file_io.py delete mode 100755 examples/async_plot_dataset.py delete mode 100644 examples/localbox.py delete mode 100644 examples/session_restoring.py diff --git a/examples/async_file_io.py b/examples/async_file_io.py deleted file mode 100644 index a4a0043..0000000 --- a/examples/async_file_io.py +++ /dev/null @@ -1,44 +0,0 @@ -import requests # type: ignore -from codeboxapi import CodeBox - - -async def main(): - async with CodeBox() as codebox: - # upload dataset csv - csv_bytes = requests.get( - "https://archive.ics.uci.edu/" - "ml/machine-learning-databases/iris/iris.data" - ).content - await codebox.aupload("iris.csv", csv_bytes) - - # install openpyxl for excel conversion - await codebox.ainstall("pandas") - await codebox.ainstall("openpyxl") - - # convert dataset csv to excel - output = await codebox.arun( - "import pandas as pd\n\n" - "df = pd.read_csv('iris.csv', header=None)\n\n" - "df.to_excel('iris.xlsx', index=False)\n" - "'iris.xlsx'" - ) - - if output.type == "image/png": - print("This should not happen") - - elif output.type == "error": - print("Error: ", output.content) - - else: - files = await codebox.alist_files() - print("Available files: ", files) - - file = files[0].name - content = await codebox.adownload(file) - print("Content: ", content) - - -if __name__ == "__main__": - import asyncio - - asyncio.run(main()) diff --git a/examples/async_plot_dataset.py b/examples/async_plot_dataset.py deleted file mode 100755 index c556489..0000000 --- a/examples/async_plot_dataset.py +++ /dev/null @@ -1,72 +0,0 @@ -import os - -import requests -from codeboxapi import CodeBox - - -async def main(): - async with CodeBox() as codebox: - # download the iris dataset - csv_bytes = requests.get( - "https://archive.ics.uci.edu/" - "ml/machine-learning-databases/iris/iris.data" - ).content - - # upload the dataset to the codebox - await codebox.aupload("iris.csv", csv_bytes) - - # install the required packages - await codebox.ainstall("matplotlib") - await codebox.ainstall("pandas") - - # dataset analysis code - code = ( - "import pandas as pd\n" - "import matplotlib.pyplot as plt\n\n" - "df = pd.read_csv('iris.csv', header=None)\n" - "df.columns = ['sepal_length', 'sepal_width'," - "'petal_length', 'petal_width', 'class']\n\n" - "color_dict = {'Iris-setosa': 0, 'Iris-versicolor': 1, " - "'Iris-virginica': 2}\n\n" - "df['color'] = df['class'].map(color_dict)\n\n" - "df.plot.scatter(x='sepal_length', y='sepal_width', " - "c='color', colormap='viridis')\n" - "plt.show()" - ) - - # run the code - output = await codebox.arun(code) - print(output.type) - - if output.type == "image/png" and os.environ.get("CODEBOX_TEST") == "False": - try: - from PIL import Image # type: ignore - except ImportError: - print( - "Please install it with " - '`pip install "codeboxapi[image_support]"`' - " to display images." - ) - exit(1) - - # Convert the image content ( bytes) into an image - import base64 - from io import BytesIO - - img_bytes = base64.b64decode(output.content) - img_buffer = BytesIO(img_bytes) - - # Display the image - img = Image.open(img_buffer) - img.show() - - elif output.type == "error": - # error output - print("Error:") - print(output.content) - - -if __name__ == "__main__": - import asyncio - - asyncio.run(main()) diff --git a/examples/localbox.py b/examples/localbox.py deleted file mode 100644 index 633beda..0000000 --- a/examples/localbox.py +++ /dev/null @@ -1,7 +0,0 @@ -from codeboxapi import CodeBox - -with CodeBox() as box: - v = box.install("pandas") - print(v) - r = box.run("import pandas; pandas.__version__") - print(r) diff --git a/examples/session_restoring.py b/examples/session_restoring.py deleted file mode 100644 index 5788026..0000000 --- a/examples/session_restoring.py +++ /dev/null @@ -1,22 +0,0 @@ -from codeboxapi import CodeBox - - -def session_restoring(): - session = CodeBox() - session.start() - - session_id = session.session_id - print(session_id) - assert session_id is not None - - session.run('hello = "Hello World!"') - - del session - - print(CodeBox.from_id(session_id=session_id).run("print(hello)")) - - CodeBox.from_id(session_id=session_id).stop() - - -if __name__ == "__main__": - session_restoring() From e812e698d4204cb2e40a83c0d1504208b5ac429a Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 8 Jul 2024 16:20:42 -0700 Subject: [PATCH 045/125] requests to httpx --- examples/file_io.py | 6 +++--- examples/plot_dataset.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/file_io.py b/examples/file_io.py index c42dcb9..edd36e6 100644 --- a/examples/file_io.py +++ b/examples/file_io.py @@ -1,10 +1,10 @@ -import requests +import httpx from codeboxapi import CodeBox with CodeBox() as codebox: # upload dataset csv - csv_bytes = requests.get( - "https://archive.ics.uci.edu/" "ml/machine-learning-databases/iris/iris.data" + csv_bytes = httpx.get( + "https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data" ).content codebox.upload("iris.csv", csv_bytes) diff --git a/examples/plot_dataset.py b/examples/plot_dataset.py index d60f3c6..0bca8cf 100644 --- a/examples/plot_dataset.py +++ b/examples/plot_dataset.py @@ -1,12 +1,12 @@ import os from pathlib import Path -import requests +import httpx from codeboxapi import CodeBox with CodeBox() as codebox: # download the iris dataset - csv_bytes = requests.get( + csv_bytes = httpx.get( "https://archive.ics.uci.edu/" "ml/machine-learning-databases/iris/iris.data" ).content From cc3c40a6f8c649a3bab93c66385290355a0cc129 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jul 2024 16:20:42 -0700 Subject: [PATCH 046/125] update deps and optional deps --- pyproject.toml | 53 +++++++++++++++++++++++++++++++++-------------- requirements.lock | 2 -- 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7beaa53..a4b923d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,12 +10,7 @@ keywords = [ "codeinterpreterapi", ] authors = [{ name = "Shroominic", email = "contact@shroominic.com" }] -dependencies = [ - "pydantic>=2", - "pydantic-settings>=2", - "httpx>=0.27.0", - "httpx-sse>=0.4.0", # todo check if really needed -] +dependencies = ["pydantic>=2", "pydantic-settings>=2", "httpx>=0.27"] readme = "README.md" requires-python = ">= 3.9" license = { text = "MIT" } @@ -33,18 +28,46 @@ build-backend = "hatchling.build" managed = true dev-dependencies = [ "codeboxapi[all]", - "mypy>=1.8", - "ruff>=0.1", - "pytest-asyncio>=0.23.7", - # docs - "neoteroi-mkdocs>=1", - "mkdocs-material>=9", + "codeboxapi[docs]", + "codeboxapi[pytest]", + "ruff", + "mypy", + "types-aiofiles>=24", + "fastapi>=0.111.0", ] [project.optional-dependencies] -local = ["jupyter-client", "ipykernel"] -image = ["Pillow"] -all = ["codeboxapi[local]", "codeboxapi[image]"] +docs = ["neoteroi-mkdocs", "mkdocs-material"] +pytest = ["pytest-asyncio"] +local = ["jupyter-client", "ipykernel", "uv", "aiofiles"] +vision = ["Pillow"] +data-science = [ + "codeboxapi[local]", + "codeboxapi[vision]", + "pandas", + "numpy", + "matplotlib", + "seaborn", + "scikit-learn", + "uv", + "bokeh", + "dash", + "matplotlib", + "networkx", + "numpy", + "openpyxl", + "pandas", + "pillow", + "plotly", + "python-docx", + "scikit-learn", + "scipy", + "seaborn", + "statsmodels", + "sympy", + "yfinance", +] +all = ["codeboxapi[data-science]"] [tool.hatch.metadata] allow-direct-references = true diff --git a/requirements.lock b/requirements.lock index eff049f..f22c58b 100644 --- a/requirements.lock +++ b/requirements.lock @@ -22,8 +22,6 @@ httpcore==1.0.5 # via httpx httpx==0.27.0 # via codeboxapi -httpx-sse==0.4.0 - # via codeboxapi idna==3.7 # via anyio # via httpx From a4fc6c0c497c3b24a05c0cb6a464ed2312033bb4 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 11 Jul 2024 20:15:15 -0700 Subject: [PATCH 047/125] =?UTF-8?q?=F0=9F=93=A6=20container?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/container/Dockerfile | 26 +++++++++++ src/container/api.py | 84 ++++++++++++++++++++++++++++++++++ src/container/build.sh | 7 +++ src/container/requirements.txt | 2 + 4 files changed, 119 insertions(+) create mode 100644 src/container/Dockerfile create mode 100644 src/container/api.py create mode 100755 src/container/build.sh create mode 100644 src/container/requirements.txt diff --git a/src/container/Dockerfile b/src/container/Dockerfile new file mode 100644 index 0000000..e379570 --- /dev/null +++ b/src/container/Dockerfile @@ -0,0 +1,26 @@ +FROM ghcr.io/astral-sh/uv as uv + +FROM --platform=arm64 python:3.12 as build + +ENV VIRTUAL_ENV=/.venv PATH="/.venv/bin:$PATH" + +COPY --from=uv /uv /uv +COPY requirements.txt . + +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=from=uv,source=/uv,target=/uv \ + /uv venv /.venv && /uv pip install -r requirements.txt + +FROM --platform=arm64 python:3.12-slim-bookworm as runtime + +RUN useradd -ms /bin/bash codebox + +COPY --chown=codebox:codebox api.py /api.py + +COPY --chown=codebox:codebox --from=build /.venv /.venv + +USER codebox + +ENV PATH="/.venv/bin:$PATH" + +CMD fastapi run /api.py --host 0.0.0.0 --port 8069 diff --git a/src/container/api.py b/src/container/api.py new file mode 100644 index 0000000..c7a3fe1 --- /dev/null +++ b/src/container/api.py @@ -0,0 +1,84 @@ +import asyncio +from contextlib import asynccontextmanager +from datetime import datetime, timedelta +from os import getenv +from typing import Annotated, AsyncGenerator, Literal + +from codeboxapi import CodeBox +from codeboxapi.codebox import CodeBoxFile +from codeboxapi.local import LocalBox +from fastapi import Depends, FastAPI, HTTPException, Path, UploadFile +from fastapi.responses import StreamingResponse +from pydantic import BaseModel + +TIMEOUT = float(getenv("TIMEOUT", "900")) + +last_interaction = datetime.utcnow() + +codebox = CodeBox.create(api_key="local") + + +@asynccontextmanager +async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]: + async def timeout(): + # todo maybe mode timeout into LocalBox + while last_interaction + timedelta(seconds=TIMEOUT) > datetime.utcnow(): + await asyncio.sleep(1) + exit(0) + + t = asyncio.create_task(timeout()) + yield + t.cancel() + + +app = FastAPI(title="Codebox API", lifespan=lifespan) + + +async def get_codebox() -> AsyncGenerator[CodeBox, None]: + global codebox, last_interaction + last_interaction = datetime.utcnow() + yield codebox + + +@app.get("/healthcheck") +async def healthcheck() -> dict[str, str]: + return {"status": "ok"} + + +class RunCodeInput(BaseModel): + code: str + + +@app.post("/exec") +async def exec( + codebox: Annotated[LocalBox, Depends(get_codebox)], + code: str, + language: Literal["python", "bash"], + timeout: int | None = None, + cwd: str | None = None, +) -> StreamingResponse: + async def event_stream() -> AsyncGenerator[str, None]: + async for chunk in codebox.astream_exec(code, language, timeout, cwd): + yield chunk.model_dump_json() + + return StreamingResponse(event_stream()) + + +@app.get("/download/{file_name}") +async def download( + codebox: Annotated[LocalBox, Depends(get_codebox)], + file_name: Annotated[str, Path()], + timeout: int | None = None, +) -> StreamingResponse: + return StreamingResponse(codebox.astream_download(file_name, timeout)) + + +@app.post("/upload") +async def upload( + file: UploadFile, + codebox: Annotated[LocalBox, Depends(get_codebox)], + timeout: int | None = None, +) -> CodeBoxFile: + if not file.filename: + raise HTTPException(status_code=400, detail="A file name is required") + return await codebox.aupload(file.filename, file.file, timeout) diff --git a/src/container/build.sh b/src/container/build.sh new file mode 100755 index 0000000..51d874c --- /dev/null +++ b/src/container/build.sh @@ -0,0 +1,7 @@ +TAG=${1:-latest} + +docker build -t codebox . + +docker tag codebox:latest shroominic/codebox:$TAG + +docker push shroominic/codebox:$TAG diff --git a/src/container/requirements.txt b/src/container/requirements.txt new file mode 100644 index 0000000..fa8304f --- /dev/null +++ b/src/container/requirements.txt @@ -0,0 +1,2 @@ +fastapi +codeboxapi[data-science] @ git+https://github.com/shroominic/codebox-api.git@v0.2 From c7ed76c1ad52c0acc9542df26b09042082ac58ef Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 11 Jul 2024 20:15:26 -0700 Subject: [PATCH 048/125] rm old stuff --- src/codeboxapi/box/__init__.py | 18 -- src/codeboxapi/box/base.py | 114 ----------- src/codeboxapi/box/factory.py | 16 -- src/codeboxapi/box/local.py | 363 --------------------------------- src/codeboxapi/box/remote.py | 303 --------------------------- src/codeboxapi/errors.py | 31 --- src/codeboxapi/schema.py | 60 ------ 7 files changed, 905 deletions(-) delete mode 100644 src/codeboxapi/box/__init__.py delete mode 100644 src/codeboxapi/box/base.py delete mode 100644 src/codeboxapi/box/factory.py delete mode 100644 src/codeboxapi/box/local.py delete mode 100644 src/codeboxapi/box/remote.py delete mode 100644 src/codeboxapi/errors.py delete mode 100644 src/codeboxapi/schema.py diff --git a/src/codeboxapi/box/__init__.py b/src/codeboxapi/box/__init__.py deleted file mode 100644 index 4b0d2d6..0000000 --- a/src/codeboxapi/box/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -""" -This module contains the box classes that are used to run code in a sandboxed -environment. The `BaseBox` class is the base class for all box classes. -The `LocalBox` class is used to run code in a local testing environment. The -`CodeBox` class is used to run code in a remote sandboxed environment. -""" - -from .base import BaseBox -from .local import LocalBox -from .remote import RemoteBox -from .factory import CodeBox - -__all__ = [ - "BaseBox", - "CodeBox", - "LocalBox", - "RemoteBox", -] diff --git a/src/codeboxapi/box/base.py b/src/codeboxapi/box/base.py deleted file mode 100644 index cfecfca..0000000 --- a/src/codeboxapi/box/base.py +++ /dev/null @@ -1,114 +0,0 @@ -"""Abstract Base Class for Isolated Execution Environments (CodeBox's)""" - -from abc import ABC, abstractmethod -from dataclasses import dataclass -from os import PathLike -from typing import AsyncIterator, BinaryIO, Iterator, Literal, TypedDict - - -class ExecResult(TypedDict): - content: str - content_type: Literal["text", "image"] - error: str | None - - -@dataclass -class MetaFile: ... - - -class BaseBox(ABC): - """CodeBox Abstract Base Class""" - - @abstractmethod - def exec( - self, - code: str | PathLike, - language: Literal["python", "bash"] = "python", - ) -> ExecResult: - """Execute python code inside the CodeBox instance""" - - @abstractmethod - async def aexec( - self, - code: str | PathLike, - language: Literal["python", "bash"] = "python", - ) -> ExecResult: - """Async Execute python code inside the CodeBox instance""" - - @abstractmethod - def stream_exec( - self, - code: str | PathLike, - language: Literal["python", "bash"] = "python", - ) -> Iterator[ExecResult]: - """Stream Chunks of Execute python code inside the CodeBox instance""" - - @abstractmethod - async def astream_exec( - self, - code: str | PathLike, - language: Literal["python", "bash"] = "python", - ) -> AsyncIterator[ExecResult]: - """Async Stream Chunks of Execute python code inside the CodeBox instance""" - - @abstractmethod - def upload( - self, - file_name: str, - content: BinaryIO | bytes | str, - ) -> MetaFile: - """Upload a file as bytes to the CodeBox instance""" - - @abstractmethod - async def aupload( - self, - data: BinaryIO, - remote_file_path: str, - ) -> MetaFile: - """Async Upload a file as bytes to the CodeBox instance""" - - @abstractmethod - def download(self, remote_file_path: str) -> BinaryIO: - """Download a file as CodeBoxFile schema""" - - @abstractmethod - async def adownload(self, remote_file_path: str) -> BinaryIO: - """Async Download a file as CodeBoxFile schema""" - - @abstractmethod - def install( - self, - packages: list[str], - installer: Literal["pip", "apt"], - ) -> bool: - """Install a python package to the venv""" - - @abstractmethod - async def ainstall( - self, - packages: str, - installer: Literal["pip", "apt"], - ) -> bool: - """Async Install a python package to the venv""" - - @abstractmethod - def list_files(self) -> list[MetaFile]: - """List all available files inside the CodeBox instance""" - - @abstractmethod - async def alist_files(self) -> list[MetaFile]: - """Async List all available files inside the CodeBox instance""" - - @abstractmethod - def restart(self) -> bool: - """Restart the jupyter kernel inside the CodeBox instance""" - - @abstractmethod - async def arestart(self) -> bool: - """Async Restart the jupyter kernel inside the CodeBox instance""" - - def _resolve_pathlike(self, code: str | PathLike) -> str: - if isinstance(code, PathLike): - with open(code, "r", encoding="utf-8") as f: - return f.read() - return code diff --git a/src/codeboxapi/box/factory.py b/src/codeboxapi/box/factory.py deleted file mode 100644 index c1deddf..0000000 --- a/src/codeboxapi/box/factory.py +++ /dev/null @@ -1,16 +0,0 @@ -from .base import BaseBox -from .local import LocalBox -from .remote import RemoteBox - - -def CodeBox( - session_id: str | None = None, - api_key: str = "local", -) -> BaseBox: - if session_id is None: - if api_key == "local": - return LocalBox() - else: - return RemoteBox() - else: - return RemoteBox(session_id) diff --git a/src/codeboxapi/box/local.py b/src/codeboxapi/box/local.py deleted file mode 100644 index 5ebd6a4..0000000 --- a/src/codeboxapi/box/local.py +++ /dev/null @@ -1,363 +0,0 @@ -""" -Local implementation of CodeBox. -This is useful for testing and development.c -In case you don't put an api_key, -this is the default CodeBox. -""" - -import asyncio -import os -import subprocess -from asyncio import sleep as asleep -from importlib.metadata import PackageNotFoundError, distribution -from os import PathLike -from queue import Queue -from threading import Thread -from time import sleep -from typing import AsyncGenerator, Generator, List, Optional, Union - -from jupyter_client.manager import KernelManager - -from ..box import BaseBox -from ..config import settings -from ..schema import CodeBoxFile, CodeBoxOutput, CodeBoxStatus - - -class LocalBox(BaseBox): - """ - LocalBox is a CodeBox implementation that runs code locally. - This is useful for testing and development. - """ - - _instance: Optional["LocalBox"] = None - - def __new__(cls, *_, **__): - if not cls._instance: - cls._instance = super().__new__(cls) - else: - if settings.show_info: - print( - "INFO: Using a LocalBox which is not fully isolated\n" - " and not scalable across multiple users.\n" - " Make sure to use a CODEBOX_API_KEY in production.\n" - " Set envar CODEBOX_SHOW_INFO=False to not see this again.\n" - ) - return cls._instance - - def __init__(self, /, **kwargs) -> None: - os.environ["PYDEVD_DISABLE_FILE_VALIDATION"] = "1" - super().__init__(session_id=kwargs.pop("session_id", "local")) - self.kernel = KernelManager() - self.cwd = settings.default_working_dir - - def start(self) -> CodeBoxStatus: - self._check_installed() - os.makedirs(self.cwd, exist_ok=True) - if not self.kernel.is_alive(): - self.kernel = KernelManager( - ip=os.getenv("LOCALHOST", "127.0.0.1"), - ) - self.kernel.start_kernel(cwd=self.cwd) - return CodeBoxStatus(status="started") - - def _check_installed(self) -> None: - try: - distribution("jupyter-client") - except PackageNotFoundError: - print( - "Make sure 'jupyter-client' is installed " - "when using without a CODEBOX_API_KEY.\n" - "You can install it with 'pip install jupyter-client'.\n" - ) - raise - - async def astart(self) -> CodeBoxStatus: - self._check_installed() - os.makedirs(self.cwd, exist_ok=True) - if not await self.kernel._async_is_alive(): - self.kernel = KernelManager( - ip=os.getenv("LOCALHOST", "127.0.0.1"), - ) - await self.kernel._async_start_kernel(cwd=self.cwd) - return CodeBoxStatus(status="started") - - def status(self) -> CodeBoxStatus: - return CodeBoxStatus(status="running" if self.kernel.is_alive() else "stopped") - - async def astatus(self) -> CodeBoxStatus: - return CodeBoxStatus( - status="running" if await self.kernel._async_is_alive() else "stopped" - ) - - def run(self, code: Union[str, PathLike]) -> CodeBoxOutput: - code = self._resolve_pathlike(code) - - if settings.verbose: - print(f"\033[90m{code}\033[0m") - - msg_stream = [] - self.kernel.client().execute_interactive( # type: ignore - code, output_hook=lambda msg: msg_stream.append(msg) - ) - return self._parse_messages(msg_stream) - - async def arun(self, code: Union[str, PathLike]) -> CodeBoxOutput: - code = self._resolve_pathlike(code) - - msg_stream = [] - await self.kernel.client()._async_execute_interactive( - code, output_hook=lambda msg: msg_stream.append(msg) - ) - return self._parse_messages(msg_stream) - - def stream_run( - self, code: Union[str, PathLike] - ) -> Generator[CodeBoxOutput, None, None]: - code = self._resolve_pathlike(code) - msg_queue = Queue[dict | None]() - - def output_hook(msg): - msg_queue.put(msg) - - def execute_code(): - self.kernel.client().execute_interactive(code, output_hook=output_hook) - # Signal the end of execution - msg_queue.put(None) - - # Start code execution in a separate thread - execution_thread = Thread(target=execute_code) - execution_thread.start() - - # Yield messages from the queue as they arrive - while True: - msg = msg_queue.get() # This will block until a message is available - if msg is None: - break # None is used as a signal to indicate the end of execution - yield self._parse_message(msg) - - # Wait for the execution thread to finish - execution_thread.join() - - async def astream_run( - self, code: Union[str, PathLike] - ) -> AsyncGenerator[CodeBoxOutput, None]: - code = self._resolve_pathlike(code) - msg_queue: asyncio.Queue = asyncio.Queue() - - async def output_hook(msg): - await msg_queue.put(msg) - - execution_task = asyncio.create_task( - self.kernel.client()._async_execute_interactive( - code, output_hook=output_hook - ) - ) - - try: - while not execution_task.done() or not msg_queue.empty(): - msg = await msg_queue.get() - yield self._parse_message(msg) - finally: - if not execution_task.done(): - execution_task.cancel() - try: - await execution_task - except asyncio.CancelledError: - pass - - def _parse_message(self, message: dict) -> CodeBoxOutput: - msg = message - if msg["msg_type"] == "stream": - return CodeBoxOutput(content=msg["content"]["text"].strip(), type="stream") - elif msg["msg_type"] == "execute_result": - CodeBoxOutput( - content=msg["content"]["data"]["text/plain"].strip(), type="text" - ) - elif msg["msg_type"] == "display_data": - if "image/png" in msg["content"]["data"]: - return CodeBoxOutput( - type="image/png", - content=msg["content"]["data"]["image/png"], - ) - if "text/plain" in msg["content"]["data"]: - return CodeBoxOutput(type="text", content=msg["data"]["text/plain"]) - - return CodeBoxOutput(type="error", content="Could not parse output") - elif msg["msg_type"] == "error": - error = f"{msg['content']['ename']}: " f"{msg['content']['evalue']}" - if settings.verbose: - print("Error:\n", error) - return CodeBoxOutput(type="error", content=error) - return CodeBoxOutput(type="empty", content="") - - def _parse_messages(self, messages: List[dict]) -> CodeBoxOutput: - result = "" - for msg in messages: - if msg["msg_type"] == "stream": - result += msg["content"]["text"].strip() + "\n" - elif msg["msg_type"] == "execute_result": - result += msg["content"]["data"]["text/plain"].strip() + "\n" - elif msg["msg_type"] == "display_data": - if "image/png" in msg["content"]["data"]: - return CodeBoxOutput( - type="image/png", - content=msg["content"]["data"]["image/png"], - ) - if "text/plain" in msg["content"]["data"]: - return CodeBoxOutput(type="text", content=msg["data"]["text/plain"]) - - return CodeBoxOutput(type="error", content="Could not parse output") - elif ( - msg["msg_type"] == "status" - and msg["content"]["execution_state"] == "idle" - ): - if len(result) > 500: - result = "[...]\n" + result[-500:] - return CodeBoxOutput( - type="text", content=result or "run successfully (no output)" - ) - elif msg["msg_type"] == "error": - error = f"{msg['content']['ename']}: " f"{msg['content']['evalue']}" - if settings.verbose: - print("Error:\n", error) - return CodeBoxOutput(type="error", content=error) - return CodeBoxOutput(type="error", content="No output") - - def shell(self, *cmd: str) -> CodeBoxOutput: - try: - result = subprocess.run( - " ".join(cmd), - shell=True, - check=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - ) - return CodeBoxOutput(type="text", content=result.stdout) - except subprocess.CalledProcessError as e: - return CodeBoxOutput(type="error", content=e.stderr) - - async def ashell(self, *cmd: str) -> CodeBoxOutput: - try: - process = await asyncio.create_subprocess_shell( - " ".join(cmd), - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - stdout, stderr = await process.communicate() - if process.returncode == 0: - return CodeBoxOutput(type="text", content=stdout.decode()) - else: - return CodeBoxOutput(type="error", content=stderr.decode()) - except Exception as e: - return CodeBoxOutput(type="error", content=str(e)) - - def shell_stream(self, *cmd: str) -> Generator[CodeBoxOutput, None, None]: - process = subprocess.Popen( - " ".join(cmd), - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - ) - if process.stdout: - for line in process.stdout: - yield CodeBoxOutput(type="stream", content=line.strip()) - process.wait() - if process.returncode != 0: - yield CodeBoxOutput(type="error", content="Command execution failed") - - async def ashell_stream(self, *cmd: str) -> AsyncGenerator[CodeBoxOutput, None]: - process = await asyncio.create_subprocess_shell( - " ".join(cmd), - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.STDOUT, - ) - if process.stdout: - async for line in process.stdout: - yield CodeBoxOutput(type="stream", content=line.decode().strip()) - await process.wait() - if process.returncode != 0: - yield CodeBoxOutput(type="error", content="Command execution failed") - - def upload(self, file_name: str, content: bytes) -> CodeBoxStatus: - os.makedirs(self.cwd, exist_ok=True) - with open(os.path.join(self.cwd, file_name), "wb") as f: - f.write(content) - - return CodeBoxStatus(status=f"{file_name} uploaded successfully") - - async def aupload(self, file_name: str, content: bytes) -> CodeBoxStatus: - return await asyncio.to_thread(self.upload, file_name, content) - - def download(self, file_name: str) -> CodeBoxFile: - with open(os.path.join(self.cwd, file_name), "rb") as f: - content = f.read() - - return CodeBoxFile(name=file_name, content=content) - - async def adownload(self, file_name: str) -> CodeBoxFile: - return await asyncio.to_thread(self.download, file_name) - - def install(self, *package_names: str) -> CodeBoxStatus: - packages = " ".join(package_names) - if "bin/uv" in self.shell("which uv").content: - out = self.shell("uv pip install", packages) - else: - out = self.shell("pip install", packages) - - if out.type == "error": - return CodeBoxStatus(status="Error: " + out.content) - self.restart() - if "No module named" in str( - logs := self.run( - f"try:\n import {packages}\nexcept Exception as e:\n print(e)" - ) - ): - return CodeBoxStatus(status="Error: " + logs.content) - return CodeBoxStatus(status=f"{packages} installed successfully") - - async def ainstall(self, *package_names: str) -> CodeBoxStatus: - packages = " ".join(package_names) - if "bin/uv" in (await self.arun("which uv")).content: - out = await self.arun(f"uv pip install {packages}") - else: - out = await self.arun(f"pip install {packages}") - - if out.type == "error": - return CodeBoxStatus(status="Error: " + out.content) - await self.arestart() - if "No module named" in str( - logs := await self.arun( - f"try:\n import {packages}\nexcept Exception as e:\n print(e)" - ) - ): - return CodeBoxStatus(status="Error: " + logs.content) - return CodeBoxStatus(status=f"{packages} installed successfully") - - def list_files(self) -> List[CodeBoxFile]: - return [ - CodeBoxFile(name=file_name, content=None) - for file_name in os.listdir(self.cwd) - ] - - async def alist_files(self) -> List[CodeBoxFile]: - return await asyncio.to_thread(self.list_files) - - def restart(self) -> CodeBoxStatus: - self.kernel.restart_kernel() - sleep(3) - return CodeBoxStatus(status="restarted") - - async def arestart(self) -> CodeBoxStatus: - await self.kernel._async_restart_kernel() - await asleep(3) - return CodeBoxStatus(status="restarted") - - def stop(self) -> CodeBoxStatus: - self.kernel.shutdown_kernel() - return CodeBoxStatus(status="stopped") - - async def astop(self) -> CodeBoxStatus: - await self.kernel._async_shutdown_kernel() - return CodeBoxStatus(status="stopped") diff --git a/src/codeboxapi/box/remote.py b/src/codeboxapi/box/remote.py deleted file mode 100644 index 55cc47a..0000000 --- a/src/codeboxapi/box/remote.py +++ /dev/null @@ -1,303 +0,0 @@ -""" -CodeBox API Wrapper -~~~~~~~~~~~~~~~~~~~ - -A basic wrapper for the CodeBox API. - -Usage ------ - -.. code-block:: python - - from codeboxapi import CodeBox - - with CodeBox() as codebox: - codebox.status() - codebox.run(code="print('Hello World!')") - codebox.install("python-package") - codebox.upload("test.txt", b"Hello World!") - codebox.list_files() - codebox.download("test.txt") - -.. code-block:: python - - from codeboxapi import CodeBox - - async with CodeBox() as codebox: - await codebox.astatus() - await codebox.arun(code="print('Hello World!')") - await codebox.ainstall("python-package") - await codebox.aupload("test.txt", b"Hello World!") - await codebox.alist_files() - await codebox.adownload("test.txt") - -""" - -from asyncio import sleep as asleep -from os import PathLike -from time import sleep -from typing import Any, Dict, List, Optional, Union -from uuid import UUID, uuid4 - -from aiohttp import ClientSession - -from ..config import settings -from ..schema import CodeBoxFile, CodeBoxOutput, CodeBoxStatus -from ..utils import abase_request, base_request -from .base import BaseBox - - -class RemoteBox(BaseBox): - """ - Sandboxed Python Interpreter - """ - - def __new__(cls, *args, **kwargs): - if kwargs.pop("local", False) or settings.api_key == "local": - from .local import LocalBox - - return LocalBox(*args, **kwargs) - - return super().__new__(cls) - - def __init__(self, session_id: Optional[str] = None, **kwargs) -> None: - self._temp_id_cache = uuid4().hex - super().__init__(session_id or self._temp_id_cache, **kwargs) - self.aiohttp_session: Optional[ClientSession] = None - - @classmethod - def from_id(cls, session_id: Union[int, UUID, str], **kwargs) -> "RemoteBox": - return cls( - session_id=( - UUID(int=session_id).hex - if isinstance(session_id, int) - else session_id.hex - if isinstance(session_id, UUID) - else session_id - ), - **kwargs, - ) - - def codebox_request(self, method, endpoint, *args, **kwargs) -> Dict[str, Any]: - """General request to the CodeBox API""" - self._update() - # temp fix - session_id = UUID(self.session_id).int - return base_request( - method, - f"/codebox/{session_id}" + endpoint, - *args, - **kwargs, - ) - - async def acodebox_request( - self, method, endpoint, *args, **kwargs - ) -> Dict[str, Any]: - """General async request to the CodeBox API""" - self._update() - self.aiohttp_session = self.aiohttp_session or ClientSession() - # temp fix - session_id = UUID(self.session_id).int - return await abase_request( - self.aiohttp_session, - method, - f"/codebox/{session_id}" + endpoint, - *args, - **kwargs, - ) - - # def start(self) -> CodeBoxStatus: - # return self.status() - - # async def astart(self) -> CodeBoxStatus: - # return await self.astatus() - - def start(self) -> CodeBoxStatus: - if self.session_id != self._temp_id_cache: - while self.status().status == "starting": - sleep(1) - return self.status() - self.session_id = UUID( - int=base_request( - method="GET", - endpoint="/codebox/start", - )["id"] - ).hex - return CodeBoxStatus(status="started") - - async def astart(self) -> CodeBoxStatus: - self.aiohttp_session = self.aiohttp_session or ClientSession() - if self.session_id != self._temp_id_cache: - while (await self.astatus()).status == "starting": - await asleep(1) - return await self.astatus() - self.session_id = UUID( - int=( - await abase_request( - self.aiohttp_session, method="GET", endpoint="/codebox/start" - ) - )["id"] - ).hex - return CodeBoxStatus(status="started") - - def status(self): - return CodeBoxStatus( - **self.codebox_request( - method="GET", - endpoint="/", - ) - ) - - async def astatus(self): - return CodeBoxStatus( - **await self.acodebox_request( - method="GET", - endpoint="/", - ) - ) - - def run(self, code: Union[str, PathLike]) -> CodeBoxOutput: - return CodeBoxOutput( - **self.codebox_request( - method="POST", - endpoint="/run", - body={"code": self._resolve_pathlike(code)}, - ) - ) - - async def arun(self, code: Union[str, PathLike]) -> CodeBoxOutput: - return CodeBoxOutput( - **await self.acodebox_request( - method="POST", - endpoint="/run", - body={"code": self._resolve_pathlike(code)}, - ) - ) - - # TODO: STREAMING - - # TODO: SHELL - - def upload(self, file_name: str, content: bytes) -> CodeBoxStatus: - return CodeBoxStatus( - **self.codebox_request( - method="POST", - endpoint="/upload", - files={"file": (file_name, content)}, - ) - ) - - async def aupload(self, file_name: str, content: bytes) -> CodeBoxStatus: - return CodeBoxStatus( - **await self.acodebox_request( - method="POST", - endpoint="/upload", - files={"file": (file_name, content)}, - ) - ) - - def download(self, file_name: str) -> CodeBoxFile: - return CodeBoxFile( - **self.codebox_request( - method="GET", - endpoint="/download", - body={"file_name": file_name}, - ) - ) - - async def adownload(self, file_name: str) -> CodeBoxFile: - return CodeBoxFile( - **await self.acodebox_request( - method="GET", - endpoint="/download", - body={"file_name": file_name}, - ) - ) - - def install(self, package_name: str) -> CodeBoxStatus: - return CodeBoxStatus( - **self.codebox_request( - method="POST", - endpoint="/install", - body={ - "package_name": package_name, - }, - ) - ) - - async def ainstall(self, package_name: str) -> CodeBoxStatus: - return CodeBoxStatus( - **await self.acodebox_request( - method="POST", - endpoint="/install", - body={ - "package_name": package_name, - }, - ) - ) - - def list_files(self) -> List[CodeBoxFile]: - return [ - CodeBoxFile(name=file_name, content=None) - for file_name in ( - self.codebox_request( - method="GET", - endpoint="/files", - ) - )["files"] - ] - - async def alist_files(self) -> List[CodeBoxFile]: - return [ - CodeBoxFile(name=file_name, content=None) - for file_name in ( - await self.acodebox_request( - method="GET", - endpoint="/files", - ) - )["files"] - ] - - def restart(self) -> CodeBoxStatus: - return CodeBoxStatus( - **self.codebox_request( - method="POST", - endpoint="/restart", - ) - ) - - async def arestart(self) -> CodeBoxStatus: - return CodeBoxStatus( - **await self.acodebox_request( - method="POST", - endpoint="/restart", - ) - ) - - def stop(self) -> CodeBoxStatus: - return CodeBoxStatus( - **self.codebox_request( - method="POST", - endpoint="/stop", - ) - ) - - async def astop(self) -> CodeBoxStatus: - status = CodeBoxStatus( - **await self.acodebox_request( - method="POST", - endpoint="/stop", - ) - ) - if self.aiohttp_session: - await self.aiohttp_session.close() - self.aiohttp_session = None - return status - - def __del__(self): - if self.aiohttp_session: - import asyncio - - loop = asyncio.new_event_loop() - loop.run_until_complete(self.aiohttp_session.close()) - self.aiohttp_session = None diff --git a/src/codeboxapi/errors.py b/src/codeboxapi/errors.py deleted file mode 100644 index eeeb9a9..0000000 --- a/src/codeboxapi/errors.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -CodeBox API -Error Classes -""" - - -class CodeBoxError(Exception): - """ - Represents an api error returned from the CodeBox API. - """ - - def __init__( - self, - http_status: int = 0, - content: str = "error", - headers: dict = {}, - body: dict = {}, - **kwargs, - ): - super().__init__(**kwargs) - - self.http_status = http_status - self.content = content - self.headers = headers - self.body = body - - def __str__(self): - return f"{self.http_status}: {self.content}" - - def __repr__(self): - return f"" diff --git a/src/codeboxapi/schema.py b/src/codeboxapi/schema.py deleted file mode 100644 index 850a823..0000000 --- a/src/codeboxapi/schema.py +++ /dev/null @@ -1,60 +0,0 @@ -""" -This file contains the schema for the CodeBox API. -It is used to validate the data returned from the API. -It also helps with type hinting and provides a nice -interface for interacting with the API. -""" - -from typing import Optional - -from pydantic import BaseModel - - -class CodeBoxStatus(BaseModel): - """ - Represents the status of a CodeBox instance. - """ - - status: str - - def __str__(self): - return self.status - - def __repr__(self): - return f"Status({self.status})" - - def __eq__(self, other): - return self.__str__() == other.__str__() - - -class CodeBoxOutput(BaseModel): - """ - Represents the code execution output of a CodeBox instance. - """ - - type: str - content: str - - def __str__(self): - return self.content - - def __repr__(self): - return f"{self.type}({self.content})" - - def __eq__(self, other): - return self.__str__() == other.__str__() - - -class CodeBoxFile(BaseModel): - """ - Represents a file returned from a CodeBox instance. - """ - - name: str - content: Optional[bytes] = None - - def __str__(self): - return self.name - - def __repr__(self): - return f"File({self.name})" From 680e09132a81419f6b4ac7e1151fcfae6f42fc13 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 11 Jul 2024 20:15:40 -0700 Subject: [PATCH 049/125] new improved base class --- src/codeboxapi/codebox.py | 346 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 346 insertions(+) create mode 100644 src/codeboxapi/codebox.py diff --git a/src/codeboxapi/codebox.py b/src/codeboxapi/codebox.py new file mode 100644 index 0000000..b50b2b4 --- /dev/null +++ b/src/codeboxapi/codebox.py @@ -0,0 +1,346 @@ +""" +CodeBox API +~~~~~~~~~~~ + +The main class for the CodeBox API. + +Usage +----- + +.. code-block:: python + + from codeboxapi import CodeBox + + codebox.healthcheck() + codebox.exec("print('Hello World!')") + codebox.install("matplotlib") + codebox.upload("test.txt", "This is test file content!") + codebox.files() + codebox.download("test.txt") + +.. code-block:: python + + from codeboxapi import CodeBox + + await codebox.healthcheck() + await codebox.exec("print('Hello World!')") + await codebox.install("matplotlib") + await codebox.upload("test.txt", "This is test file content!") + await codebox.files() + await codebox.download("test.txt") + +""" + +from abc import ABC, abstractmethod +from functools import partial, wraps +from os import PathLike +from typing import ( + Any, + AsyncGenerator, + AsyncIterator, + BinaryIO, + Callable, + Coroutine, + Generator, + Iterator, + Literal, + ParamSpec, + TypeVar, +) +from warnings import warn + +import anyio +from pydantic import BaseModel + +from . import utils + + +class ExecChunk(BaseModel): + type: Literal["text", "image", "stream", "error"] + content: str + + +class ExecResult(BaseModel): + content: list[ExecChunk] + + @property + def text(self) -> str: + return "".join( + chunk.content + for chunk in self.content + if chunk.type == "text" or chunk.type == "stream" + ) + + @property + def images(self) -> list[str]: + return [chunk.content for chunk in self.content if chunk.type == "image"] + + @property + def errors(self) -> list[str]: + return [chunk.content for chunk in self.content if chunk.type == "error"] + + +# todo move somewhere more clean +class CodeBoxOutput(BaseModel): + """Deprecated CodeBoxOutput class""" + + content: str + type: Literal["stdout", "stderr", "error"] + + +class CodeBoxFile(BaseModel): + remote_path: str + size: int + codebox: "CodeBox" + _content: bytes | None = None + + @property + def name(self) -> str: + return self.remote_path.split("/")[-1] + + @property + def content(self) -> bytes: + return self._content or b"".join(self.codebox.stream_download(self.remote_path)) + + @property + async def acontent(self) -> bytes: + return self._content or b"".join([ + chunk async for chunk in self.codebox.astream_download(self.remote_path) + ]) + + def save(self, path: str) -> None: + with open(path, "wb") as f: + for chunk in self.codebox.stream_download(self.remote_path): + f.write(chunk) + + async def asave(self, path: str) -> None: + import aiofiles + + async with aiofiles.open(path, "wb") as f: + async for chunk in self.codebox.astream_download(self.remote_path): + await f.write(chunk) + + +T = TypeVar("T") +P = ParamSpec("P") + + +def deprecated(message: str) -> Callable[[Callable[P, T]], Callable[P, T]]: + def decorator(func: Callable[P, T]) -> Callable[P, T]: + @wraps(func) + def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: + warn( + f"{func.__name__} is deprecated. {message}", + DeprecationWarning, + stacklevel=2, + ) + return func(*args, **kwargs) + + return wrapper + + return decorator + + +class CodeBox(ABC): + """CodeBox Abstract Base Class""" + + @classmethod + def create( + cls, + api_key: str | None = None, + factory_id: str | None = None, + ) -> "CodeBox": + """ + Creates a CodeBox session + """ + from .local import LocalBox + from .remote import RemoteBox + + if api_key == "local": + return LocalBox() + + if api_key == "docker": + # return DockerBox() + raise NotImplementedError("DockerBox is not implemented yet") + + return RemoteBox(factory_id, api_key) + + # SYNC + + def exec( + self, + code: str | PathLike, + language: Literal["python", "bash"] = "python", + timeout: float | None = None, + cwd: str | None = None, + ) -> ExecResult: + """Execute python code inside the CodeBox instance""" + # todo think about if this maybe better not scripted + return utils.flatten_exec_result(self.stream_exec(code, language, timeout, cwd)) + + @abstractmethod + def stream_exec( + self, + code: str | PathLike, + language: Literal["python", "bash"] = "python", + timeout: float | None = None, + cwd: str | None = None, + ) -> Generator[ExecChunk, None, None]: + """Stream Chunks of Execute python code inside the CodeBox instance""" + + @abstractmethod + def upload( + self, + remote_file_path: str, + content: BinaryIO | bytes | str, + timeout: float | None = None, + ) -> CodeBoxFile: + """Upload a file to the CodeBox instance""" + + @abstractmethod + def stream_download( + self, + remote_file_path: str, + timeout: float | None = None, + ) -> Iterator[bytes]: + """Download a file as open BinaryIO. Make sure to close the file after use.""" + + # ASYNC + + async def aexec( + self, + code: str | PathLike, + language: Literal[ + "python", "bash" + ] = "python", # todo differentiate python and jupyter + timeout: float | None = None, + cwd: str | None = None, + ) -> ExecResult: + """Async Execute python code inside the CodeBox instance""" + return await utils.async_flatten_exec_result( + self.astream_exec(code, language, timeout, cwd) + ) + + @abstractmethod + def astream_exec( + self, + code: str | PathLike, + language: Literal["python", "bash"] = "python", + timeout: float | None = None, + cwd: str | None = None, + ) -> AsyncGenerator[ExecChunk, None]: + """Async Stream Chunks of Execute python code inside the CodeBox instance""" + + @abstractmethod + async def aupload( + self, + remote_file_path: str, + content: BinaryIO | bytes | str, + timeout: float | None = None, + ) -> CodeBoxFile: + """Async Upload a file to the CodeBox instance""" + + async def adownload( + self, + remote_file_path: str, + timeout: float | None = None, + ) -> CodeBoxFile: + return next( + f for f in (await self.alist_files()) if f.remote_path == remote_file_path + ) + + @abstractmethod + def astream_download( + self, + remote_file_path: str, + timeout: float | None = None, + ) -> AsyncIterator[bytes]: + """Async Download a file as BinaryIO. Make sure to close the file after use.""" + + # SCRIPTED METHODS + + async def ahealthcheck(self) -> Literal["healthy", "error"]: + health = (await self.aexec("echo 'ok'", language="bash")).text + if health == "ok": + return "healthy" + return "error" + + async def ainstall(self, *packages: str) -> str: + # todo make sure it always uses the correct python venv + await self.aexec( + "uv pip install " + " ".join(packages), + language="bash", + ) + return " ".join(packages) + " installed successfully" + + async def alist_files(self) -> list[CodeBoxFile]: + files = ( + await self.aexec( + "find . -type f -exec du -h {} + | awk '{print $2, $1}' | sort", + language="bash", + ) + ).text.splitlines() + return [ + CodeBoxFile(remote_path=parts[0], size=int(parts[1]), codebox=self) + for file in files + if (parts := file.split()) and len(parts) == 2 + ] + + async def alist_packages(self) -> list[str]: + return (await self.aexec("uv pip list", language="bash")).text.splitlines() + + async def alist_variables(self) -> list[str]: + return (await self.aexec("%who")).text.splitlines() + + async def arestart(self) -> None: + """Restart the Jupyter kernel""" + await self.aexec(r"%restart") + + # DEPRECATED + + @deprecated( + "There is no need anymore to explicitly start a CodeBox instance.\n" + "When calling any method you will get assigned a new session.\n" + "The `.start` method is deprecated. Use `.healthcheck` instead." + ) + async def astart(self) -> Literal["started", "error"]: + return "started" if await self.ahealthcheck() == "healthy" else "error" + + @deprecated( + "The `.stop` method is deprecated. " + "The session will be closed automatically after the last interaction.\n" + "(default timeout: 15 minutes)" + ) + async def astop(self) -> Literal["stopped"]: + return "stopped" + + @deprecated( + "The `.run` method is deprecated. Use `.exec` instead.", + ) + async def arun(self, code: str) -> CodeBoxOutput: + exec_result = await self.aexec(code, language="python") + return CodeBoxOutput(type="stdout", content=exec_result.text) + + @deprecated( + "The `.status` method is deprecated. Use `.healthcheck` instead.", + ) + async def astatus(self) -> Literal["started", "running", "stopped"]: + return "running" if await self.ahealthcheck() == "healthy" else "stopped" + + # SYNCIFY + + def __init__(self): + def syncify(async_func: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P, T]: + return partial(anyio.run, async_func) + + self.healthcheck = syncify(self.ahealthcheck) + self.list_files = syncify(self.alist_files) + self.list_packages = syncify(self.alist_packages) + self.list_variables = syncify(self.alist_variables) + self.download = syncify(self.adownload) + self.restart = syncify(self.arestart) + self.install = syncify(self.ainstall) + self.start = syncify(self.astart) + self.stop = syncify(self.astop) + self.run = syncify(self.arun) + self.status = syncify(self.astatus) From 130a6c2513f26141f400a282ae6dd9c8595b2cdf Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 11 Jul 2024 20:15:51 -0700 Subject: [PATCH 050/125] new improved local box --- src/codeboxapi/local.py | 240 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 240 insertions(+) create mode 100644 src/codeboxapi/local.py diff --git a/src/codeboxapi/local.py b/src/codeboxapi/local.py new file mode 100644 index 0000000..7fbee26 --- /dev/null +++ b/src/codeboxapi/local.py @@ -0,0 +1,240 @@ +""" +Local implementation of CodeBox. +This is useful for testing and development.c +In case you don't put an api_key, +this is the default CodeBox. +""" + +import asyncio +import os +import subprocess +from os import PathLike +from queue import Queue +from threading import Thread +from typing import ( + AsyncGenerator, + AsyncIterator, + BinaryIO, + Generator, + Iterator, + Literal, + Self, + Union, +) + +from jupyter_client.manager import KernelManager + +from . import utils +from .codebox import CodeBox, CodeBoxFile, ExecChunk +from .config import settings + + +# todo implement inactivity timeout to close kernel after 10 minutes of last method call +class LocalBox(CodeBox): + """ + LocalBox is a CodeBox implementation that runs code locally. + This is useful for testing and development. + """ + + _instance: Self | None = None + + def __new__(cls, *_, **__): + if not cls._instance: + cls._instance = super().__new__(cls) + else: + if settings.debug: + print( + "INFO: Using a LocalBox which is not fully isolated\n" + " and not scalable across multiple parallel users.\n" + " Make sure to use a CODEBOX_API_KEY in production.\n" + " Set envar CODEBOX_DEBUG=False to not see this again.\n" + ) + return cls._instance + + def __init__(self, /, **kwargs) -> None: + super().__init__() + os.environ["PYDEVD_DISABLE_FILE_VALIDATION"] = "1" + self.kernel = KernelManager() + self.cwd = settings.default_working_dir + # startup + utils.check_installed("jupyter-client") + os.makedirs(self.cwd, exist_ok=True) + if not self.kernel.is_alive(): + self.kernel = KernelManager(ip=os.getenv("LOCALHOST", "127.0.0.1")) + self.kernel.start_kernel(cwd=self.cwd) + + def stream_exec( + self, + code: str | PathLike, + language: Literal["python", "bash"] = "python", + timeout: float | None = None, + cwd: str | None = None, + ) -> Generator[ExecChunk, None, None]: + """ + Creates a Generator that streams chunks of the output of the code execution + """ + code = utils.resolve_pathlike(code) + + if language == "python": + msg_queue: Queue[dict | None] = Queue() + + def output_hook(msg): + msg_queue.put(msg) + + def execute_code(): + self.kernel.client().execute_interactive(code, output_hook=output_hook) + msg_queue.put(None) + + execution_thread = Thread(target=execute_code) + execution_thread.start() + + while True: + msg = msg_queue.get() + if msg is None: + break + yield utils.parse_message(msg) + + execution_thread.join() + + elif language == "bash": + with utils.raise_timeout(timeout): + process = subprocess.Popen( + code, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + if process.stdout: + for line in process.stdout: + yield ExecChunk(type="stream", content=line.strip()) + process.wait() + if process.returncode != 0: + yield ExecChunk(type="error", content="Command execution failed") + else: + raise ValueError(f"Unsupported language: {language}") + + def upload( + self, + file_name: str, + content: BinaryIO | bytes | str, + timeout: float | None = None, + ) -> CodeBoxFile: + with utils.raise_timeout(timeout): + file_path = os.path.join(self.cwd, file_name) + with open(file_path, "wb") as file: + if isinstance(content, str): + file.write(content.encode()) + elif isinstance(content, BinaryIO): + while chunk := content.read(8192): + file.write(chunk) + else: + file.write(content) + file_size = os.path.getsize(file_path) + return CodeBoxFile( + remote_path=file_path, + size=file_size, + codebox=self, + ) + + def stream_download( + self, + file_name: str, + timeout: float | None = None, + ) -> Iterator[bytes]: + with utils.raise_timeout(timeout): + with open(os.path.join(self.cwd, file_name), "rb") as file: + yield file.read() + + async def astream_exec( + self, + code: Union[str, PathLike], + language: Literal["python", "bash"] = "python", + timeout: float | None = None, + cwd: str | None = None, + ) -> AsyncGenerator[ExecChunk, None]: + code = utils.resolve_pathlike(code) + + if language == "python": + msg_queue: asyncio.Queue = asyncio.Queue() + + async def output_hook(msg): + await msg_queue.put(msg) + + execution_task = asyncio.create_task( + self.kernel.client()._async_execute_interactive( + code, output_hook=output_hook, timeout=timeout + ) + ) + + try: + while not execution_task.done() or not msg_queue.empty(): + msg = await msg_queue.get() + yield utils.parse_message(msg) + finally: + if not execution_task.done(): + execution_task.cancel() + try: + await execution_task + except asyncio.CancelledError: + pass + + elif language == "bash": + async with asyncio.timeout(timeout): + process = await asyncio.create_subprocess_shell( + code, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.STDOUT, + cwd=cwd, + ) + if process.stdout: + async for line in process.stdout: + yield ExecChunk(type="stream", content=line.decode().strip()) + await process.wait() + if process.returncode != 0: + yield ExecChunk(type="error", content="Command execution failed") + + else: + raise ValueError(f"Unsupported language: {language}") + + async def aupload( + self, + file_name: str, + content: BinaryIO | bytes | str, + timeout: float | None = None, + ) -> CodeBoxFile: + import aiofiles + + async with asyncio.timeout(timeout): + file_path = os.path.join(self.cwd, file_name) + async with aiofiles.open(file_path, "wb") as file: + if isinstance(content, str): + await file.write(content.encode()) + elif isinstance(content, BinaryIO): + while chunk := content.read(8192): + await file.write(chunk) + else: + await file.write(content) + + file_size = await aiofiles.os.path.getsize(file_path) + return CodeBoxFile( + remote_path=file_path, + size=file_size, + codebox=self, + ) + + async def astream_download( + self, + remote_file_path: str, + timeout: float | None = None, + ) -> AsyncIterator[bytes]: + import aiofiles + + async with asyncio.timeout(timeout): + async with aiofiles.open( + os.path.join(self.cwd, remote_file_path), "rb" + ) as f: + yield await f.read() + + def __del__(self): + self.kernel.shutdown_kernel() From 66cabfa02cb68af7b189bf3044338cdf5f486304 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 11 Jul 2024 20:15:57 -0700 Subject: [PATCH 051/125] new improved remote box --- src/codeboxapi/remote.py | 114 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 src/codeboxapi/remote.py diff --git a/src/codeboxapi/remote.py b/src/codeboxapi/remote.py new file mode 100644 index 0000000..0ece202 --- /dev/null +++ b/src/codeboxapi/remote.py @@ -0,0 +1,114 @@ +from json import loads +from os import PathLike +from typing import AsyncGenerator, AsyncIterator, BinaryIO, Generator, Iterator, Literal +from uuid import uuid4 + +import anyio +import httpx + +from . import utils +from .codebox import CodeBox, CodeBoxFile, ExecChunk +from .config import settings + + +class RemoteBox(CodeBox): + """ + Sandboxed Python Interpreter + """ + + def __new__(cls, *args, **kwargs): + if kwargs.pop("local", False) or settings.api_key == "local": + from .local import LocalBox + + return LocalBox(*args, **kwargs) + return super().__new__(cls) + + def __init__( + self, + factory_id: str | None = None, + api_key: str | None = None, + ) -> None: + super().__init__() + self.session_id = uuid4().hex + self.factory_id = factory_id + self.api_key = api_key or settings.api_key + self.aclient = httpx.AsyncClient( + base_url=f"{settings.base_url}/codebox/{self.session_id}" + ) + + def stream_exec( + self, + code: str | PathLike, + language: Literal["python", "bash"] = "python", + timeout: float | None = None, + cwd: str | None = None, + ) -> Generator[ExecChunk, None, None]: + async_gen = self.astream_exec(code, language, timeout, cwd) + return (chunk for chunk in anyio.run(utils.collect_async_gen, async_gen)) + + def upload( + self, + file_name: str, + content: BinaryIO | bytes | str, + timeout: float | None = None, + ) -> CodeBoxFile: + return anyio.run(self.aupload, file_name, content, timeout) + + def stream_download( + self, + remote_file_path: str, + timeout: float | None = None, + ) -> Iterator[bytes]: + return anyio.run( + utils.collect_async_gen, self.astream_download(remote_file_path, timeout) + ) + + async def astream_exec( + self, + code: str | PathLike, + language: Literal["python", "bash"] = "python", + timeout: float | None = None, + cwd: str | None = None, + ) -> AsyncGenerator[ExecChunk, None]: + code = utils.resolve_pathlike(code) + async with self.aclient.stream( + method="POST", + url="/stream", + timeout=timeout, + params={"code": code, "language": language, "cwd": cwd}, + ) as response: + async for chunk in response.aiter_text(): + yield ExecChunk(**loads(chunk)) + + async def aupload( + self, + file_name: str, + content: BinaryIO | bytes | str, + timeout: float | None = None, + ) -> CodeBoxFile: + if isinstance(content, str): + content = content.encode("utf-8") + return CodeBoxFile( + **( + await self.aclient.post( + url="/upload", + files={"file": (file_name, content)}, + timeout=timeout, + ) + ).json(), + codebox=self, + ) + + async def astream_download( + self, + remote_file_path: str, + timeout: float | None = None, + ) -> AsyncIterator[bytes]: + async with self.aclient.stream( + method="GET", + url="/download", + timeout=timeout, + params={"file_name": remote_file_path}, + ) as response: + async for chunk in response.aiter_bytes(): + yield chunk From 3fb255fdba13c31a036d1482f7bb892aeb3e2f00 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 11 Jul 2024 20:16:19 -0700 Subject: [PATCH 052/125] improvements --- src/codeboxapi/__init__.py | 2 +- src/codeboxapi/config.py | 5 +- src/codeboxapi/utils.py | 285 ++++++++++++++----------------------- 3 files changed, 110 insertions(+), 182 deletions(-) diff --git a/src/codeboxapi/__init__.py b/src/codeboxapi/__init__.py index c0dd3ad..e5db11e 100644 --- a/src/codeboxapi/__init__.py +++ b/src/codeboxapi/__init__.py @@ -6,7 +6,7 @@ and interacting with Codebox instances. """ -from .box import CodeBox +from .codebox import CodeBox from .config import settings __all__ = [ diff --git a/src/codeboxapi/config.py b/src/codeboxapi/config.py index eccb435..6db725b 100644 --- a/src/codeboxapi/config.py +++ b/src/codeboxapi/config.py @@ -11,13 +11,12 @@ class CodeBoxSettings(BaseSettings): CodeBox API Config """ - verbose: bool = False + debug: bool = False show_info: bool = True api_key: str = "local" default_working_dir: str = ".codebox" - base_url: str = "https://codeboxapi.com/api/v1" - timeout: int = 20 + base_url: str = "https://codeboxapi.com/api/v2" class Config: env_file = ".env" diff --git a/src/codeboxapi/utils.py b/src/codeboxapi/utils.py index 65c6dd1..cfca39c 100644 --- a/src/codeboxapi/utils.py +++ b/src/codeboxapi/utils.py @@ -1,193 +1,122 @@ -"""Utility functions for API requests""" +import signal +from contextlib import contextmanager +from functools import reduce +from importlib.metadata import PackageNotFoundError, distribution +from os import PathLike +from typing import AsyncIterator, Iterator, TypeVar + +from .codebox import ExecChunk, ExecResult +from .config import settings -import json -from asyncio import sleep as asleep -from io import BytesIO -from time import sleep -from typing import Optional -import requests -from aiohttp import ClientError, ClientResponse, ClientSession, FormData -from aiohttp.payload import BytesIOPayload +def resolve_pathlike(file: str | PathLike) -> str: + if isinstance(file, PathLike): + with open(file, "r") as f: + return f.read() + return file -from .config import settings -from .errors import CodeBoxError +T = TypeVar("T") -def build_request_data( - method: str, - endpoint: str, - body: Optional[dict] = None, - files: Optional[dict] = None, -) -> dict: - """ - Builds a request data dictionary for the requests library. - """ - return { - "method": method, - "url": settings.base_url + endpoint, - "headers": { - "Authorization": f"Bearer {settings.api_key}", - }, - "json": body, - "files": files, - } - - -def handle_response(response: requests.Response): - """ - Handles a response from the requests library. - """ - handlers = { - "application/json": lambda r: json.loads(r.content.decode()), - "application/octet-stream": lambda r: { - "content": BytesIO(r.content).read(), - "name": r.headers["Content-Disposition"].split("=")[1], - }, - # Add other content type handlers here - } - handler = handlers.get( - response.headers["Content-Type"].split(";")[0], lambda r: r.content.decode() - ) - if response.status_code != 200: - raise CodeBoxError( - http_status=response.status_code, - content=response.content.decode(), - headers=dict(response.headers.items()), - ) - return handler(response) +async def collect_async_gen(async_gen: AsyncIterator[T]) -> Iterator[T]: + return iter([item async for item in async_gen]) -async def handle_response_async(response: ClientResponse) -> dict: - """ - Handles a response from the aiohttp library. - """ - async def json_handler(r: ClientResponse) -> dict: - return json.loads(await r.text()) - - async def file_handler(r: ClientResponse) -> dict: - return { - "content": await r.read(), - "name": r.headers["Content-Disposition"].split("=")[1], - } - - async def text_handler(r: ClientResponse) -> dict: - return {"content": await r.text()} - - async def default_handler(r: ClientResponse) -> dict: - return {"content": await r.text()} - - handlers = { - "application/json": json_handler, - "application/octet-stream": file_handler, - "text/plain": text_handler, - # Add other content type handlers here - } - if response.status != 200: - raise CodeBoxError( - http_status=response.status, - content=(await response.content.read()).decode(), - headers=dict(response.headers.items()), - ) - handler = handlers.get( - response.headers["Content-Type"].split(";")[0], default_handler - ) - return await handler(response) - - -def base_request( - method: str, - endpoint: str, - body: Optional[dict] = None, - files: Optional[dict] = None, - retries: int = 3, - backoff_factor: float = 0.3, -) -> dict: +def reduce_bytes(async_gen: Iterator[bytes]) -> bytes: + return reduce(lambda x, y: x + y, async_gen) + + +def flatten_exec_result(result: ExecResult | Iterator[ExecChunk]) -> ExecResult: + if not isinstance(result, ExecResult): + result = ExecResult(content=[c for c in result]) + # todo + # remove empty text chunks + # merge text chunks + # remove empty stream chunks + # merge stream chunks + # remove empty error chunks + # merge error chunks + # ... + return result + + +async def async_flatten_exec_result(async_gen: AsyncIterator[ExecChunk]) -> ExecResult: + return flatten_exec_result(await collect_async_gen(async_gen)) + + +def parse_message(msg: dict) -> ExecChunk: """ - Makes a request to the CodeBox API with retry logic. - - Args: - - method: HTTP method as a string. - - endpoint: API endpoint as a string. - - body: Optional dictionary containing the JSON body. - - files: Optional dictionary containing file data. - - retries: Maximum number of retries on failure. - - backoff_factor: Multiplier for delay between retries (exponential backoff). - - Returns: - - A dictionary response from the API. + Parse a message from the Jupyter kernel. + The message is a dictionary which is a part of the message stream. + The output is a chunk of the execution result. """ - request_data = build_request_data(method, endpoint, body, files) - for attempt in range(retries): - try: - response = requests.request(**request_data, timeout=270) - return handle_response(response) - except requests.RequestException as e: - if attempt < retries - 1: - sleep_time = backoff_factor * (2**attempt) - sleep(sleep_time) - else: - raise e - raise CodeBoxError(http_status=500, content="Request Failed. Max retries exceeded") - - -async def abase_request( - session: ClientSession, - method: str, - endpoint: str, - body: Optional[dict] = None, - files: Optional[dict] = None, - retries: int = 3, - backoff_factor: float = 0.3, -) -> dict: + if msg["msg_type"] == "stream": + return ExecChunk(type="stream", content=msg["content"]["text"]) + elif msg["msg_type"] == "execute_result": + return ExecChunk(type="text", content=msg["content"]["data"]["text/plain"]) + elif msg["msg_type"] == "display_data": + if "image/png" in msg["content"]["data"]: + return ExecChunk(type="image", content=msg["content"]["data"]["image/png"]) + if "text/plain" in msg["content"]["data"]: + return ExecChunk(type="text", content=msg["data"]["text/plain"]) + return ExecChunk(type="error", content="Could not parse output") + elif msg["msg_type"] == "status" and msg["content"]["execution_state"] == "idle": + return ExecChunk(type="text", content="") + elif msg["msg_type"] == "error": + return ExecChunk( + type="error", + content=msg["content"]["ename"] + ": " + msg["content"]["evalue"], + ) + else: + return ExecChunk( + type="error", content="Could not parse output: Unsupported message type" + ) + + +def parse_messages(messages: list[dict]) -> ExecResult: """ - Makes an asynchronous request to the CodeBox API with retry functionality. - - Args: - - session: The aiohttp ClientSession. - - method: HTTP method as a string. - - endpoint: API endpoint as a string. - - body: Optional dictionary containing the JSON body. - - files: Optional dictionary containing file data. - - retries: Maximum number of retries on failure. - - backoff_factor: Multiplier for delay between retries (exponential backoff). - - Returns: - - A dictionary response from the API. + Parse a list of messages from the Jupyter kernel. + The output is a list of chunks of the execution result. """ - request_data = build_request_data(method, endpoint, body, files) - if files is not None: - data = FormData() - for key, file_tuple in files.items(): - filename, fileobject = file_tuple[ - :2 - ] # Get the filename and fileobject from the tuple - payload = BytesIOPayload(BytesIO(fileobject)) - data.add_field( - key, payload, filename=filename - ) # Use the filename from the tuple - request_data.pop("files") - request_data.pop("json") - request_data["data"] = data + chunks = [] + for msg in messages: + if chunk := parse_message(msg): + chunks.append(chunk) else: - request_data.pop("files") - - for attempt in range(retries): - try: - response = await session.request(**request_data) - return await handle_response_async(response) - except ClientError as e: - if attempt < retries - 1: - sleep_time = backoff_factor * (2**attempt) - await asleep(sleep_time) - else: - raise e - raise CodeBoxError(http_status=500, content="Request Failed. Max retries exceeded") - - -def set_api_key(api_key: str) -> None: + chunks.append( + ExecChunk(type="text", content="/* exec successful - no output */") + ) + return ExecResult(content=chunks) + + +def check_installed(package: str) -> None: """ - Manually set the CODEBOX_API_KEY. + Check if the given package is installed. """ - settings.api_key = api_key + try: + distribution(package) + except PackageNotFoundError: + if settings.debug: + print( + f"\nMake sure '{package}' is installed " + "when using without a CODEBOX_API_KEY.\n" + f"You can install it with 'pip install {package}'.\n" + ) + raise + + +@contextmanager +def raise_timeout(timeout: float | None = None): + def timeout_handler(signum, frame): + raise TimeoutError("Execution timed out") + + if timeout is not None: + signal.signal(signal.SIGALRM, timeout_handler) + signal.alarm(int(timeout)) + + try: + yield + finally: + if timeout is not None: + signal.alarm(0) From 5bb590e23a75e3054af2dc3529f4fee54be25042 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 11 Jul 2024 20:16:22 -0700 Subject: [PATCH 053/125] wip --- src/codeboxapi/docker.py.todo | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/codeboxapi/docker.py.todo diff --git a/src/codeboxapi/docker.py.todo b/src/codeboxapi/docker.py.todo new file mode 100644 index 0000000..e69de29 From 64a3652f956ead90cf6455cb3fd76a72d7a997e3 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 12 Jul 2024 16:20:42 -0700 Subject: [PATCH 054/125] =?UTF-8?q?=F0=9F=90=8B=20docker=20box?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/codeboxapi/docker.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/codeboxapi/docker.py diff --git a/src/codeboxapi/docker.py b/src/codeboxapi/docker.py new file mode 100644 index 0000000..35c09ae --- /dev/null +++ b/src/codeboxapi/docker.py @@ -0,0 +1,40 @@ +import socket +import subprocess + +import httpx + +from .remote import RemoteBox + + +def get_free_port(port_or_range: int | tuple[int, int]) -> int: + if isinstance(port_or_range, int): + port = port_or_range + else: + start, end = port_or_range + port = start + + while port <= (end if isinstance(port_or_range, tuple) else port): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + try: + s.bind(("localhost", port)) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + return port + except OSError: + port += 1 + + raise OSError("No free ports available on the specified port or range.") + + +class DockerBox(RemoteBox): + def __init__( + self, + port_or_range: int | tuple[int, int] = 8069, + image: str = "shroominic/codebox:latest", + **_: bool, + ) -> None: + self.port = get_free_port(port_or_range) + subprocess.run( + ["docker", "run", "-d", "--rm", "-p", f"{self.port}:8069", image], + check=True, + ) + self.aclient = httpx.AsyncClient(base_url=f"http://localhost:{self.port}") From d04f046898c69e76447968c05fbb9e364908a089 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sat, 13 Jul 2024 16:20:42 -0700 Subject: [PATCH 055/125] =?UTF-8?q?=F0=9F=94=A7=20dev=20setup=20script?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/tasks.json | 14 ++++++++++++++ scripts/dev-setup.sh | 23 +++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 .vscode/tasks.json create mode 100644 scripts/dev-setup.sh diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..29fe4d4 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,14 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "shell", + "command": "bash ./scripts/dev-setup.sh", + "group": "build", + "label": "dev-setup", + "runOptions": { + "runOn": "default" + } + } + ] +} \ No newline at end of file diff --git a/scripts/dev-setup.sh b/scripts/dev-setup.sh new file mode 100644 index 0000000..3af3f57 --- /dev/null +++ b/scripts/dev-setup.sh @@ -0,0 +1,23 @@ +ruff lint . --fix + +# check if uv is installed or install it +if ! command -v uv &> /dev/null +then + echo "uv not found, installing..." + pip install uv +else + echo "uv is already installed" +fi + +# check if venv exists or create it +if [ ! -d ".venv" ]; then + echo "Creating virtual environment..." + uv venv +else + echo "Virtual environment already exists" +fi + +# Install dependencies +echo "Installing dependencies..." +uv pip install -r pyproject.toml + From 08c84e5bc0930fea5c1c6004a396633a8a424a93 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 14 Jul 2024 16:20:42 -0700 Subject: [PATCH 056/125] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20build=20script?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/build.sh | 10 ++++++++++ src/container/build.sh | 7 ------- 2 files changed, 10 insertions(+), 7 deletions(-) create mode 100755 scripts/build.sh delete mode 100755 src/container/build.sh diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..ea468f9 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,10 @@ +TAG=${1:-latest} + +rye build --wheel -c -q + +docker build -t codebox . + +# todo move container to seperate codeboxapi account +docker tag codebox:latest shroominic/codebox:$TAG + +# docker push shroominic/codebox:$TAG diff --git a/src/container/build.sh b/src/container/build.sh deleted file mode 100755 index 51d874c..0000000 --- a/src/container/build.sh +++ /dev/null @@ -1,7 +0,0 @@ -TAG=${1:-latest} - -docker build -t codebox . - -docker tag codebox:latest shroominic/codebox:$TAG - -docker push shroominic/codebox:$TAG From 6765cdddc28fd8cbe892a18d5d0d9deb78c9084e Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jul 2024 16:20:42 -0700 Subject: [PATCH 057/125] =?UTF-8?q?=F0=9F=A7=AA=20v0.1=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/{general_test.py => test_v01_general.py} | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) rename tests/{general_test.py => test_v01_general.py} (99%) diff --git a/tests/general_test.py b/tests/test_v01_general.py similarity index 99% rename from tests/general_test.py rename to tests/test_v01_general.py index 365605e..116266f 100644 --- a/tests/general_test.py +++ b/tests/test_v01_general.py @@ -80,6 +80,7 @@ async def run_async(codebox: CodeBox) -> bool: print("Downloaded") assert "matplotlib" in str(await codebox.ainstall("matplotlib")) + assert ( "error" != ( @@ -104,4 +105,4 @@ async def run_async(codebox: CodeBox) -> bool: if __name__ == "__main__": test_codebox() - # test_localbox() + test_localbox() From 1ca5d6b7f19380eddfe0e5ba47d71b36415bcca0 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 17 Jul 2024 13:29:14 -0700 Subject: [PATCH 058/125] =?UTF-8?q?=F0=9F=93=9D=20simplify=20deps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 15 ++++++++++++--- requirements.lock | 16 +--------------- src/container/requirements.txt | 2 -- 3 files changed, 13 insertions(+), 20 deletions(-) delete mode 100644 src/container/requirements.txt diff --git a/pyproject.toml b/pyproject.toml index a4b923d..b130a36 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,9 +8,14 @@ keywords = [ "codebox-api", "language model", "codeinterpreterapi", + "code sandbox", + "codebox", + "agent infrastructure", + "agents", + "agent sandbox", ] authors = [{ name = "Shroominic", email = "contact@shroominic.com" }] -dependencies = ["pydantic>=2", "pydantic-settings>=2", "httpx>=0.27"] +dependencies = ["httpx>=0.27"] readme = "README.md" requires-python = ">= 3.9" license = { text = "MIT" } @@ -30,10 +35,10 @@ dev-dependencies = [ "codeboxapi[all]", "codeboxapi[docs]", "codeboxapi[pytest]", + "codeboxapi[serve]", "ruff", "mypy", "types-aiofiles>=24", - "fastapi>=0.111.0", ] [project.optional-dependencies] @@ -41,6 +46,7 @@ docs = ["neoteroi-mkdocs", "mkdocs-material"] pytest = ["pytest-asyncio"] local = ["jupyter-client", "ipykernel", "uv", "aiofiles"] vision = ["Pillow"] +serve = ["fastapi"] data-science = [ "codeboxapi[local]", "codeboxapi[vision]", @@ -67,7 +73,10 @@ data-science = [ "sympy", "yfinance", ] -all = ["codeboxapi[data-science]"] +all = ["codeboxapi[data-science]", "codeboxapi[serve]"] + +[project.scripts] +codeboxapi-serve = "codeboxapi.api:serve" [tool.hatch.metadata] allow-direct-references = true diff --git a/requirements.lock b/requirements.lock index f22c58b..7a47e04 100644 --- a/requirements.lock +++ b/requirements.lock @@ -9,11 +9,9 @@ # generate-hashes: false -e file:. -annotated-types==0.6.0 - # via pydantic anyio==4.4.0 # via httpx -certifi==2024.2.2 +certifi==2024.7.4 # via httpcore # via httpx h11==0.14.0 @@ -25,18 +23,6 @@ httpx==0.27.0 idna==3.7 # via anyio # via httpx -pydantic==2.7.1 - # via codeboxapi - # via pydantic-settings -pydantic-core==2.18.2 - # via pydantic -pydantic-settings==2.2.1 - # via codeboxapi -python-dotenv==1.0.1 - # via pydantic-settings sniffio==1.3.1 # via anyio # via httpx -typing-extensions==4.11.0 - # via pydantic - # via pydantic-core diff --git a/src/container/requirements.txt b/src/container/requirements.txt deleted file mode 100644 index fa8304f..0000000 --- a/src/container/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -fastapi -codeboxapi[data-science] @ git+https://github.com/shroominic/codebox-api.git@v0.2 From bf2a01989df6ff2aa8894317ef2f4f6cad057d6d Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 17 Jul 2024 13:29:37 -0700 Subject: [PATCH 059/125] =?UTF-8?q?=F0=9F=90=8B=F0=9F=93=A6=20container=20?= =?UTF-8?q?api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 17 ++++++++++ src/{container => codeboxapi}/api.py | 51 ++++++++++++++-------------- src/container/Dockerfile | 26 -------------- 3 files changed, 43 insertions(+), 51 deletions(-) create mode 100644 Dockerfile rename src/{container => codeboxapi}/api.py (59%) delete mode 100644 src/container/Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b4a4b38 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM python:slim-bookworm as build + +COPY README.md pyproject.toml src / + +RUN pip install -e .[all] + +FROM python:slim-bookworm as runtime + +RUN useradd -ms /bin/bash codebox-user + +COPY --chown=codebox-user:codebox-user --from=build /usr/local/ /usr/local/ + +USER codebox-user + +ENV PATH="/usr/local/bin:$PATH" + +CMD codeboxapi-serve diff --git a/src/container/api.py b/src/codeboxapi/api.py similarity index 59% rename from src/container/api.py rename to src/codeboxapi/api.py index c7a3fe1..e7bea65 100644 --- a/src/container/api.py +++ b/src/codeboxapi/api.py @@ -4,25 +4,21 @@ from os import getenv from typing import Annotated, AsyncGenerator, Literal -from codeboxapi import CodeBox -from codeboxapi.codebox import CodeBoxFile -from codeboxapi.local import LocalBox -from fastapi import Depends, FastAPI, HTTPException, Path, UploadFile +from fastapi import Body, Depends, FastAPI, HTTPException, UploadFile from fastapi.responses import StreamingResponse -from pydantic import BaseModel -TIMEOUT = float(getenv("TIMEOUT", "900")) +from .local import LocalBox +from .utils import CodeBoxFile +codebox = LocalBox() last_interaction = datetime.utcnow() -codebox = CodeBox.create(api_key="local") - @asynccontextmanager async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]: async def timeout(): - # todo maybe mode timeout into LocalBox - while last_interaction + timedelta(seconds=TIMEOUT) > datetime.utcnow(): + timeout_secs = float(getenv("CODEBOX_TIMEOUT", "900")) + while last_interaction + timedelta(seconds=timeout_secs) > datetime.utcnow(): await asyncio.sleep(1) exit(0) @@ -34,41 +30,40 @@ async def timeout(): app = FastAPI(title="Codebox API", lifespan=lifespan) -async def get_codebox() -> AsyncGenerator[CodeBox, None]: +async def get_codebox() -> AsyncGenerator[LocalBox, None]: global codebox, last_interaction last_interaction = datetime.utcnow() yield codebox -@app.get("/healthcheck") +@app.get("/") async def healthcheck() -> dict[str, str]: return {"status": "ok"} -class RunCodeInput(BaseModel): - code: str - - @app.post("/exec") async def exec( - codebox: Annotated[LocalBox, Depends(get_codebox)], - code: str, - language: Literal["python", "bash"], + code: Annotated[str, Body()], + kernel: Literal["ipython", "bash"] = "ipython", timeout: int | None = None, cwd: str | None = None, + codebox: LocalBox = Depends(get_codebox), ) -> StreamingResponse: + print("code", code) + async def event_stream() -> AsyncGenerator[str, None]: - async for chunk in codebox.astream_exec(code, language, timeout, cwd): - yield chunk.model_dump_json() + async for chunk in codebox.astream_exec(code, kernel, timeout, cwd): + print("chunk", chunk) + yield chunk.__str__() return StreamingResponse(event_stream()) @app.get("/download/{file_name}") async def download( - codebox: Annotated[LocalBox, Depends(get_codebox)], - file_name: Annotated[str, Path()], + file_name: str, timeout: int | None = None, + codebox: LocalBox = Depends(get_codebox), ) -> StreamingResponse: return StreamingResponse(codebox.astream_download(file_name, timeout)) @@ -76,9 +71,15 @@ async def download( @app.post("/upload") async def upload( file: UploadFile, - codebox: Annotated[LocalBox, Depends(get_codebox)], timeout: int | None = None, -) -> CodeBoxFile: + codebox: LocalBox = Depends(get_codebox), +) -> "CodeBoxFile": if not file.filename: raise HTTPException(status_code=400, detail="A file name is required") return await codebox.aupload(file.filename, file.file, timeout) + + +def serve(): + import uvicorn + + uvicorn.run(app, host="0.0.0.0", port=getenv("CODEBOX_PORT", 8069)) diff --git a/src/container/Dockerfile b/src/container/Dockerfile deleted file mode 100644 index e379570..0000000 --- a/src/container/Dockerfile +++ /dev/null @@ -1,26 +0,0 @@ -FROM ghcr.io/astral-sh/uv as uv - -FROM --platform=arm64 python:3.12 as build - -ENV VIRTUAL_ENV=/.venv PATH="/.venv/bin:$PATH" - -COPY --from=uv /uv /uv -COPY requirements.txt . - -RUN --mount=type=cache,target=/root/.cache/uv \ - --mount=from=uv,source=/uv,target=/uv \ - /uv venv /.venv && /uv pip install -r requirements.txt - -FROM --platform=arm64 python:3.12-slim-bookworm as runtime - -RUN useradd -ms /bin/bash codebox - -COPY --chown=codebox:codebox api.py /api.py - -COPY --chown=codebox:codebox --from=build /.venv /.venv - -USER codebox - -ENV PATH="/.venv/bin:$PATH" - -CMD fastapi run /api.py --host 0.0.0.0 --port 8069 From a8e1db630ab315b632162185f03f8ca546ee5c03 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 17 Jul 2024 13:29:53 -0700 Subject: [PATCH 060/125] rm --- src/codeboxapi/config.py | 27 --------------------------- src/codeboxapi/docker.py.todo | 0 2 files changed, 27 deletions(-) delete mode 100644 src/codeboxapi/config.py delete mode 100644 src/codeboxapi/docker.py.todo diff --git a/src/codeboxapi/config.py b/src/codeboxapi/config.py deleted file mode 100644 index 6db725b..0000000 --- a/src/codeboxapi/config.py +++ /dev/null @@ -1,27 +0,0 @@ -""" -CodeBox API Config: -Automatically loads environment variables from .env file -""" - -from pydantic_settings import BaseSettings - - -class CodeBoxSettings(BaseSettings): - """ - CodeBox API Config - """ - - debug: bool = False - show_info: bool = True - - api_key: str = "local" - default_working_dir: str = ".codebox" - base_url: str = "https://codeboxapi.com/api/v2" - - class Config: - env_file = ".env" - env_prefix = "CODEBOX_" - extra = "ignore" - - -settings = CodeBoxSettings() diff --git a/src/codeboxapi/docker.py.todo b/src/codeboxapi/docker.py.todo deleted file mode 100644 index e69de29..0000000 From a741ad6105af001144fe895158c70c9c419ffb23 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 17 Jul 2024 13:30:30 -0700 Subject: [PATCH 061/125] =?UTF-8?q?=F0=9F=93=82=20put=20everything=20in=20?= =?UTF-8?q?utils=20to=20look=20clean?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/codeboxapi/utils.py | 211 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 197 insertions(+), 14 deletions(-) diff --git a/src/codeboxapi/utils.py b/src/codeboxapi/utils.py index cfca39c..4937625 100644 --- a/src/codeboxapi/utils.py +++ b/src/codeboxapi/utils.py @@ -1,12 +1,149 @@ import signal from contextlib import contextmanager -from functools import reduce +from dataclasses import dataclass +from functools import partial, reduce, wraps from importlib.metadata import PackageNotFoundError, distribution -from os import PathLike -from typing import AsyncIterator, Iterator, TypeVar +from os import PathLike, getenv +from typing import ( + TYPE_CHECKING, + Any, + AsyncGenerator, + Callable, + Coroutine, + Generator, + Iterator, + Literal, + NoReturn, + ParamSpec, + TypeVar, +) +from warnings import warn -from .codebox import ExecChunk, ExecResult -from .config import settings +import anyio +from anyio._core._eventloop import threadlocals + +if TYPE_CHECKING: + from .codebox import CodeBox + + +@dataclass +class ExecChunk: + type: Literal["text", "image", "stream", "error"] + content: str + + @classmethod + def decode(cls, text: str) -> "ExecChunk": + type, content = text.split(";\n") + assert type in ["text", "image", "stream", "error"] + return cls(type=type, content=content) # type: ignore[arg-type] + + def __str__(self) -> str: + return f"{self.type};\n{self.content}" + + +@dataclass +class ExecResult: + chunks: list[ExecChunk] + + @property + def text(self) -> str: + return "".join( + chunk.content + for chunk in self.chunks + if chunk.type == "text" or chunk.type == "stream" + ) + + @property + def images(self) -> list[str]: + return [chunk.content for chunk in self.chunks if chunk.type == "image"] + + @property + def errors(self) -> list[str]: + return [chunk.content for chunk in self.chunks if chunk.type == "error"] + + +# todo move somewhere more clean +@dataclass +class CodeBoxOutput: + """Deprecated CodeBoxOutput class""" + + content: str + type: Literal["stdout", "stderr", "image/png", "error"] + + def __str__(self) -> str: + return self.content + + def __eq__(self, other: object) -> bool: + if isinstance(other, str): + return self.content == other + if isinstance(other, CodeBoxOutput): + return self.content == other.content and self.type == other.type + return False + + +@dataclass +class CodeBoxFile: + remote_path: str + size: int + codebox_id: str + _content: bytes | None = None + + @property + def codebox(self) -> "CodeBox": + from .codebox import CodeBox + + return CodeBox( + self.codebox_id, + codebox_cwd="../.codebox", # type: ignore + ) + + @property + def name(self) -> str: + return self.remote_path.split("/")[-1] + + @property + def content(self) -> bytes: + return self._content or b"".join(self.codebox.stream_download(self.remote_path)) + + @property + async def acontent(self) -> bytes: + return self._content or b"".join([ + chunk async for chunk in self.codebox.astream_download(self.remote_path) + ]) + + def save(self, path: str) -> None: + with open(path, "wb") as f: + for chunk in self.codebox.stream_download(self.remote_path): + f.write(chunk) + + async def asave(self, path: str) -> None: + import aiofiles + + async with aiofiles.open(path, "wb") as f: + async for chunk in self.codebox.astream_download(self.remote_path): + await f.write(chunk) + + +T = TypeVar("T") +P = ParamSpec("P") + + +def deprecated(message: str) -> Callable[[Callable[P, T]], Callable[P, T]]: + def decorator(func: Callable[P, T]) -> Callable[P, T]: + @wraps(func) + def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: + if getenv("IGNORE_DEPRECATION_WARNINGS", "false").lower() == "true": + return func(*args, **kwargs) + warn( + f"{func.__name__} is deprecated. {message}", + DeprecationWarning, + stacklevel=2, + ) + return func(*args, **kwargs) + + return wrapper + + return decorator def resolve_pathlike(file: str | PathLike) -> str: @@ -16,11 +153,21 @@ def resolve_pathlike(file: str | PathLike) -> str: return file -T = TypeVar("T") +IT = TypeVar("IT") -async def collect_async_gen(async_gen: AsyncIterator[T]) -> Iterator[T]: - return iter([item async for item in async_gen]) +def _syncify_generator( + async_gen: AsyncGenerator[IT, None], +) -> Generator[IT, None, None]: + # todo is this even possible? + while True: + try: + if not getattr(threadlocals, "current_async_backend", None): + yield anyio.run(async_gen.__anext__) + else: + yield anyio.from_thread.run(async_gen.__anext__) + except StopAsyncIteration: + break def reduce_bytes(async_gen: Iterator[bytes]) -> bytes: @@ -29,8 +176,8 @@ def reduce_bytes(async_gen: Iterator[bytes]) -> bytes: def flatten_exec_result(result: ExecResult | Iterator[ExecChunk]) -> ExecResult: if not isinstance(result, ExecResult): - result = ExecResult(content=[c for c in result]) - # todo + result = ExecResult(chunks=[c for c in result]) + # todo todo todo todo todo todo # remove empty text chunks # merge text chunks # remove empty stream chunks @@ -41,8 +188,36 @@ def flatten_exec_result(result: ExecResult | Iterator[ExecChunk]) -> ExecResult: return result -async def async_flatten_exec_result(async_gen: AsyncIterator[ExecChunk]) -> ExecResult: - return flatten_exec_result(await collect_async_gen(async_gen)) +async def async_flatten_exec_result( + async_gen: AsyncGenerator[ExecChunk, None], +) -> ExecResult: + # todo todo todo todo todo todo + # remove empty text chunks + # merge text chunks + # remove empty stream chunks + # merge stream chunks + # remove empty error chunks + # merge error chunks + # ... + return ExecResult(chunks=[c async for c in async_gen]) + + +def syncify(async_function: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P, T]: + """ + Take an async function and create a regular one that receives the same keyword and + positional arguments, and that when called, calls the original async function in + the main async loop from the worker thread using `anyio.to_thread.run()`. + """ + + @wraps(async_function) + def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: + partial_f = partial(async_function, *args, **kwargs) + + if not getattr(threadlocals, "current_async_backend", None): + return anyio.run(partial_f) + return anyio.from_thread.run(partial_f) + + return wrapper def parse_message(msg: dict) -> ExecChunk: @@ -87,7 +262,7 @@ def parse_messages(messages: list[dict]) -> ExecResult: chunks.append( ExecChunk(type="text", content="/* exec successful - no output */") ) - return ExecResult(content=chunks) + return ExecResult(chunks=chunks) def check_installed(package: str) -> None: @@ -97,7 +272,7 @@ def check_installed(package: str) -> None: try: distribution(package) except PackageNotFoundError: - if settings.debug: + if getenv("DEBUG", "false").lower() == "true": print( f"\nMake sure '{package}' is installed " "when using without a CODEBOX_API_KEY.\n" @@ -106,6 +281,10 @@ def check_installed(package: str) -> None: raise +def debug_mode() -> bool: + return getenv("DEBUG", "false").lower() == "true" + + @contextmanager def raise_timeout(timeout: float | None = None): def timeout_handler(signum, frame): @@ -120,3 +299,7 @@ def timeout_handler(signum, frame): finally: if timeout is not None: signal.alarm(0) + + +def raise_error(message: str) -> NoReturn: + raise Exception(message) From 676ce27a9d012e7418142f51a6ecc3c050e67af3 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 17 Jul 2024 13:31:01 -0700 Subject: [PATCH 062/125] =?UTF-8?q?=E2=9C=A8=E2=9C=A8insanly=20improved=20?= =?UTF-8?q?codebox=20base=20class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/codeboxapi/__init__.py | 6 +- src/codeboxapi/codebox.py | 326 +++++++++++++++++-------------------- 2 files changed, 152 insertions(+), 180 deletions(-) diff --git a/src/codeboxapi/__init__.py b/src/codeboxapi/__init__.py index e5db11e..c3ee53c 100644 --- a/src/codeboxapi/__init__.py +++ b/src/codeboxapi/__init__.py @@ -7,9 +7,5 @@ """ from .codebox import CodeBox -from .config import settings -__all__ = [ - "CodeBox", - "settings", -] +__all__ = ["CodeBox"] diff --git a/src/codeboxapi/codebox.py b/src/codeboxapi/codebox.py index b50b2b4..70777c7 100644 --- a/src/codeboxapi/codebox.py +++ b/src/codeboxapi/codebox.py @@ -11,184 +11,100 @@ from codeboxapi import CodeBox + codebox = CodeBox.create(api_key="local") + codebox.healthcheck() codebox.exec("print('Hello World!')") - codebox.install("matplotlib") codebox.upload("test.txt", "This is test file content!") - codebox.files() + codebox.exec("!pip install matplotlib", kernel="bash") + codebox.list_files() codebox.download("test.txt") .. code-block:: python from codeboxapi import CodeBox - await codebox.healthcheck() - await codebox.exec("print('Hello World!')") - await codebox.install("matplotlib") - await codebox.upload("test.txt", "This is test file content!") - await codebox.files() - await codebox.download("test.txt") + codebox = CodeBox.create(api_key="local") + + await codebox.ahealthcheck() + await codebox.aexec("print('Hello World!')") + await codebox.ainstall("matplotlib") + await codebox.aupload("test.txt", "This is test file content!") + await codebox.alist_files() + await codebox.adownload("test.txt") """ -from abc import ABC, abstractmethod -from functools import partial, wraps +from importlib import import_module from os import PathLike -from typing import ( - Any, - AsyncGenerator, - AsyncIterator, - BinaryIO, - Callable, - Coroutine, - Generator, - Iterator, - Literal, - ParamSpec, - TypeVar, -) -from warnings import warn +from typing import Any, AsyncGenerator, BinaryIO, Generator, Literal import anyio -from pydantic import BaseModel - -from . import utils - - -class ExecChunk(BaseModel): - type: Literal["text", "image", "stream", "error"] - content: str - - -class ExecResult(BaseModel): - content: list[ExecChunk] - - @property - def text(self) -> str: - return "".join( - chunk.content - for chunk in self.content - if chunk.type == "text" or chunk.type == "stream" - ) - - @property - def images(self) -> list[str]: - return [chunk.content for chunk in self.content if chunk.type == "image"] - - @property - def errors(self) -> list[str]: - return [chunk.content for chunk in self.content if chunk.type == "error"] - - -# todo move somewhere more clean -class CodeBoxOutput(BaseModel): - """Deprecated CodeBoxOutput class""" - - content: str - type: Literal["stdout", "stderr", "error"] - - -class CodeBoxFile(BaseModel): - remote_path: str - size: int - codebox: "CodeBox" - _content: bytes | None = None - - @property - def name(self) -> str: - return self.remote_path.split("/")[-1] - - @property - def content(self) -> bytes: - return self._content or b"".join(self.codebox.stream_download(self.remote_path)) - - @property - async def acontent(self) -> bytes: - return self._content or b"".join([ - chunk async for chunk in self.codebox.astream_download(self.remote_path) - ]) - - def save(self, path: str) -> None: - with open(path, "wb") as f: - for chunk in self.codebox.stream_download(self.remote_path): - f.write(chunk) - - async def asave(self, path: str) -> None: - import aiofiles - - async with aiofiles.open(path, "wb") as f: - async for chunk in self.codebox.astream_download(self.remote_path): - await f.write(chunk) - - -T = TypeVar("T") -P = ParamSpec("P") - - -def deprecated(message: str) -> Callable[[Callable[P, T]], Callable[P, T]]: - def decorator(func: Callable[P, T]) -> Callable[P, T]: - @wraps(func) - def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: - warn( - f"{func.__name__} is deprecated. {message}", - DeprecationWarning, - stacklevel=2, - ) - return func(*args, **kwargs) - - return wrapper - - return decorator +from .utils import ( + CodeBoxFile, + CodeBoxOutput, + ExecChunk, + ExecResult, + async_flatten_exec_result, + deprecated, + flatten_exec_result, + syncify, +) -class CodeBox(ABC): - """CodeBox Abstract Base Class""" - @classmethod - def create( +class CodeBox: + def __new__( cls, - api_key: str | None = None, - factory_id: str | None = None, + session_id: str | None = None, + api_key: str | Literal["local", "docker"] = "local", + factory_id: str | Literal["default"] = "default", + **kwargs: Any, ) -> "CodeBox": """ Creates a CodeBox session """ - from .local import LocalBox - from .remote import RemoteBox - if api_key == "local": - return LocalBox() + return super().__new__(import_module("codeboxapi.local").LocalBox) if api_key == "docker": - # return DockerBox() - raise NotImplementedError("DockerBox is not implemented yet") + return super().__new__(import_module("codeboxapi.docker").DockerBox) + + return super().__new__(import_module("codeboxapi.remote").RemoteBox) - return RemoteBox(factory_id, api_key) + def __init__( + self, + session_id: str | None = None, + api_key: str | Literal["local", "docker"] = "local", + factory_id: str | Literal["default"] = "default", + **_: bool, + ) -> None: + self.session_id = session_id or "local" + self.api_key = api_key + self.factory_id = factory_id # SYNC def exec( self, code: str | PathLike, - language: Literal["python", "bash"] = "python", + kernel: Literal["ipython", "bash"] = "ipython", timeout: float | None = None, cwd: str | None = None, ) -> ExecResult: """Execute python code inside the CodeBox instance""" - # todo think about if this maybe better not scripted - return utils.flatten_exec_result(self.stream_exec(code, language, timeout, cwd)) + return flatten_exec_result(self.stream_exec(code, kernel, timeout, cwd)) - @abstractmethod def stream_exec( self, code: str | PathLike, - language: Literal["python", "bash"] = "python", + kernel: Literal["ipython", "bash"] = "ipython", timeout: float | None = None, cwd: str | None = None, ) -> Generator[ExecChunk, None, None]: """Stream Chunks of Execute python code inside the CodeBox instance""" + raise NotImplementedError("Abstract method, please use a subclass.") - @abstractmethod def upload( self, remote_file_path: str, @@ -196,42 +112,40 @@ def upload( timeout: float | None = None, ) -> CodeBoxFile: """Upload a file to the CodeBox instance""" + return syncify(self.aupload)(remote_file_path, content, timeout) - @abstractmethod def stream_download( self, remote_file_path: str, timeout: float | None = None, - ) -> Iterator[bytes]: + ) -> Generator[bytes, None, None]: """Download a file as open BinaryIO. Make sure to close the file after use.""" + raise NotImplementedError("Abstract method, please use a subclass.") # ASYNC async def aexec( self, code: str | PathLike, - language: Literal[ - "python", "bash" - ] = "python", # todo differentiate python and jupyter + kernel: Literal["ipython", "bash"] = "ipython", timeout: float | None = None, cwd: str | None = None, ) -> ExecResult: """Async Execute python code inside the CodeBox instance""" - return await utils.async_flatten_exec_result( - self.astream_exec(code, language, timeout, cwd) + return await async_flatten_exec_result( + self.astream_exec(code, kernel, timeout, cwd) ) - @abstractmethod def astream_exec( self, code: str | PathLike, - language: Literal["python", "bash"] = "python", + kernel: Literal["ipython", "bash"] = "ipython", timeout: float | None = None, cwd: str | None = None, ) -> AsyncGenerator[ExecChunk, None]: """Async Stream Chunks of Execute python code inside the CodeBox instance""" + raise NotImplementedError("Abstract method, please use a subclass.") - @abstractmethod async def aupload( self, remote_file_path: str, @@ -239,37 +153,39 @@ async def aupload( timeout: float | None = None, ) -> CodeBoxFile: """Async Upload a file to the CodeBox instance""" + raise NotImplementedError("Abstract method, please use a subclass.") async def adownload( self, remote_file_path: str, timeout: float | None = None, ) -> CodeBoxFile: - return next( + return [ f for f in (await self.alist_files()) if f.remote_path == remote_file_path - ) + ][0] - @abstractmethod def astream_download( self, remote_file_path: str, timeout: float | None = None, - ) -> AsyncIterator[bytes]: + ) -> AsyncGenerator[bytes, None]: """Async Download a file as BinaryIO. Make sure to close the file after use.""" + raise NotImplementedError("Abstract method, please use a subclass.") - # SCRIPTED METHODS + # HELPER METHODS async def ahealthcheck(self) -> Literal["healthy", "error"]: - health = (await self.aexec("echo 'ok'", language="bash")).text - if health == "ok": - return "healthy" - return "error" + return ( + "healthy" + if "ok" in (await self.aexec("echo ok", kernel="bash")).text + else "error" + ) async def ainstall(self, *packages: str) -> str: # todo make sure it always uses the correct python venv await self.aexec( "uv pip install " + " ".join(packages), - language="bash", + kernel="bash", ) return " ".join(packages) + " installed successfully" @@ -277,17 +193,28 @@ async def alist_files(self) -> list[CodeBoxFile]: files = ( await self.aexec( "find . -type f -exec du -h {} + | awk '{print $2, $1}' | sort", - language="bash", + kernel="bash", ) ).text.splitlines() return [ - CodeBoxFile(remote_path=parts[0], size=int(parts[1]), codebox=self) + CodeBoxFile( + remote_path=parts[0].removeprefix("./"), + size=self._parse_size(parts[1]), + codebox_id=self.session_id, + ) for file in files - if (parts := file.split()) and len(parts) == 2 + if (parts := file.split(" ")) and len(parts) == 2 ] + def _parse_size(self, size_str: str) -> int: + """Convert human-readable size to bytes.""" + units = {"K": 1024, "M": 1024**2, "G": 1024**3, "T": 1024**4} + number = float(size_str[:-1]) + unit = size_str[-1].upper() + return int(number * units.get(unit, 1)) + async def alist_packages(self) -> list[str]: - return (await self.aexec("uv pip list", language="bash")).text.splitlines() + return (await self.aexec("uv pip list", kernel="bash")).text.splitlines() async def alist_variables(self) -> list[str]: return (await self.aexec("%who")).text.splitlines() @@ -296,6 +223,45 @@ async def arestart(self) -> None: """Restart the Jupyter kernel""" await self.aexec(r"%restart") + async def akeep_alive(self, minutes: int = 15) -> None: + """Keep the CodeBox instance alive for a certain amount of minutes""" + + async def ping(cb: CodeBox, d: int) -> None: + for _ in range(d): + await cb.ahealthcheck() + await anyio.sleep(60) + + async with anyio.create_task_group() as tg: + tg.start_soon(ping, self, minutes) + + # SYNCIFY + + def download( + self, remote_file_path: str, timeout: float | None = None + ) -> CodeBoxFile: + return syncify(self.adownload)(remote_file_path, timeout) + + def healthcheck(self) -> str: + return syncify(self.ahealthcheck)() + + def install(self, *packages: str) -> str: + return syncify(self.ainstall)(*packages) + + def list_files(self) -> list[CodeBoxFile]: + return syncify(self.alist_files)() + + def list_packages(self) -> list[str]: + return syncify(self.alist_packages)() + + def list_variables(self) -> list[str]: + return syncify(self.alist_variables)() + + def restart(self) -> None: + return syncify(self.arestart)() + + def keep_alive(self, minutes: int = 15) -> None: + return syncify(self.akeep_alive)(minutes) + # DEPRECATED @deprecated( @@ -304,7 +270,7 @@ async def arestart(self) -> None: "The `.start` method is deprecated. Use `.healthcheck` instead." ) async def astart(self) -> Literal["started", "error"]: - return "started" if await self.ahealthcheck() == "healthy" else "error" + return "started" if (await self.ahealthcheck()) == "healthy" else "error" @deprecated( "The `.stop` method is deprecated. " @@ -317,8 +283,10 @@ async def astop(self) -> Literal["stopped"]: @deprecated( "The `.run` method is deprecated. Use `.exec` instead.", ) - async def arun(self, code: str) -> CodeBoxOutput: - exec_result = await self.aexec(code, language="python") + async def arun(self, code: str | PathLike) -> CodeBoxOutput: + exec_result = await self.aexec(code, kernel="ipython") + if exec_result.images: + return CodeBoxOutput(type="image/png", content=exec_result.images[0]) return CodeBoxOutput(type="stdout", content=exec_result.text) @deprecated( @@ -327,20 +295,28 @@ async def arun(self, code: str) -> CodeBoxOutput: async def astatus(self) -> Literal["started", "running", "stopped"]: return "running" if await self.ahealthcheck() == "healthy" else "stopped" - # SYNCIFY + @deprecated( + "The `.start` method is deprecated. Use `.healthcheck` instead.", + ) + def start(self) -> Literal["started", "error"]: + return syncify(self.astart)() - def __init__(self): - def syncify(async_func: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P, T]: - return partial(anyio.run, async_func) - - self.healthcheck = syncify(self.ahealthcheck) - self.list_files = syncify(self.alist_files) - self.list_packages = syncify(self.alist_packages) - self.list_variables = syncify(self.alist_variables) - self.download = syncify(self.adownload) - self.restart = syncify(self.arestart) - self.install = syncify(self.ainstall) - self.start = syncify(self.astart) - self.stop = syncify(self.astop) - self.run = syncify(self.arun) - self.status = syncify(self.astatus) + @deprecated( + "The `.stop` method is deprecated. " + "The session will be closed automatically after the last interaction.\n" + "(default timeout: 15 minutes)" + ) + def stop(self) -> Literal["stopped"]: + return syncify(self.astop)() + + @deprecated( + "The `.run` method is deprecated. Use `.exec` instead.", + ) + def run(self, code: str | PathLike) -> CodeBoxOutput: + return syncify(self.arun)(code) + + @deprecated( + "The `.status` method is deprecated. Use `.healthcheck` instead.", + ) + def status(self) -> Literal["started", "running", "stopped"]: + return syncify(self.astatus)() From 847ef13550df11e4566535595654753309d5f619 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 17 Jul 2024 13:31:17 -0700 Subject: [PATCH 063/125] =?UTF-8?q?=E2=9C=A8=20way=20better=20local=20box?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/codeboxapi/local.py | 351 ++++++++++++++++++++-------------------- 1 file changed, 174 insertions(+), 177 deletions(-) diff --git a/src/codeboxapi/local.py b/src/codeboxapi/local.py index 7fbee26..54a0887 100644 --- a/src/codeboxapi/local.py +++ b/src/codeboxapi/local.py @@ -6,211 +6,202 @@ """ import asyncio +import base64 +import io import os +import re import subprocess -from os import PathLike -from queue import Queue -from threading import Thread -from typing import ( - AsyncGenerator, - AsyncIterator, - BinaryIO, - Generator, - Iterator, - Literal, - Self, - Union, -) - -from jupyter_client.manager import KernelManager - -from . import utils +import sys +import typing as t +from io import StringIO +from typing import Generator + +from IPython.core.interactiveshell import InteractiveShell + from .codebox import CodeBox, CodeBoxFile, ExecChunk -from .config import settings +from .utils import check_installed, raise_timeout, resolve_pathlike -# todo implement inactivity timeout to close kernel after 10 minutes of last method call class LocalBox(CodeBox): """ - LocalBox is a CodeBox implementation that runs code locally. + LocalBox is a CodeBox implementation that runs code locally using IPython. This is useful for testing and development. """ - _instance: Self | None = None - - def __new__(cls, *_, **__): - if not cls._instance: - cls._instance = super().__new__(cls) - else: - if settings.debug: - print( - "INFO: Using a LocalBox which is not fully isolated\n" - " and not scalable across multiple parallel users.\n" - " Make sure to use a CODEBOX_API_KEY in production.\n" - " Set envar CODEBOX_DEBUG=False to not see this again.\n" - ) - return cls._instance - - def __init__(self, /, **kwargs) -> None: - super().__init__() - os.environ["PYDEVD_DISABLE_FILE_VALIDATION"] = "1" - self.kernel = KernelManager() - self.cwd = settings.default_working_dir - # startup - utils.check_installed("jupyter-client") - os.makedirs(self.cwd, exist_ok=True) - if not self.kernel.is_alive(): - self.kernel = KernelManager(ip=os.getenv("LOCALHOST", "127.0.0.1")) - self.kernel.start_kernel(cwd=self.cwd) + def __new__(cls, *args, **kwargs) -> "LocalBox": + # This is a hack to ignore the CodeBox.__new__ factory method. + return object.__new__(cls) - def stream_exec( - self, - code: str | PathLike, - language: Literal["python", "bash"] = "python", - timeout: float | None = None, - cwd: str | None = None, - ) -> Generator[ExecChunk, None, None]: - """ - Creates a Generator that streams chunks of the output of the code execution - """ - code = utils.resolve_pathlike(code) - - if language == "python": - msg_queue: Queue[dict | None] = Queue() - - def output_hook(msg): - msg_queue.put(msg) - - def execute_code(): - self.kernel.client().execute_interactive(code, output_hook=output_hook) - msg_queue.put(None) - - execution_thread = Thread(target=execute_code) - execution_thread.start() - - while True: - msg = msg_queue.get() - if msg is None: - break - yield utils.parse_message(msg) - - execution_thread.join() - - elif language == "bash": - with utils.raise_timeout(timeout): - process = subprocess.Popen( - code, - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - ) - if process.stdout: - for line in process.stdout: - yield ExecChunk(type="stream", content=line.strip()) - process.wait() - if process.returncode != 0: - yield ExecChunk(type="error", content="Command execution failed") - else: - raise ValueError(f"Unsupported language: {language}") - - def upload( + def __init__( self, - file_name: str, - content: BinaryIO | bytes | str, - timeout: float | None = None, - ) -> CodeBoxFile: - with utils.raise_timeout(timeout): - file_path = os.path.join(self.cwd, file_name) - with open(file_path, "wb") as file: - if isinstance(content, str): - file.write(content.encode()) - elif isinstance(content, BinaryIO): - while chunk := content.read(8192): - file.write(chunk) - else: - file.write(content) - file_size = os.path.getsize(file_path) - return CodeBoxFile( - remote_path=file_path, - size=file_size, - codebox=self, - ) + session_id: str | None = None, + codebox_cwd: str = ".codebox", + **kwargs, + ) -> None: + self.session_id = session_id or "" + os.makedirs(codebox_cwd, exist_ok=True) + self.cwd = os.path.abspath(codebox_cwd) + os.chdir(self.cwd) + check_installed("ipython") + self.shell = InteractiveShell.instance() + self.shell.enable_gui = lambda x: None # type: ignore + self._patch_matplotlib_show() + + def _patch_matplotlib_show(self) -> None: + import matplotlib.pyplot as plt + + def custom_show(close=True): + fig = plt.gcf() + buf = io.BytesIO() + fig.savefig(buf, format="png") + buf.seek(0) + img_str = base64.b64encode(buf.getvalue()).decode("utf-8") + print(f"") + if close: + plt.close(fig) + + plt.show = custom_show - def stream_download( + def stream_exec( self, - file_name: str, + code: str | os.PathLike, + kernel: t.Literal["ipython", "bash"] = "ipython", timeout: float | None = None, - ) -> Iterator[bytes]: - with utils.raise_timeout(timeout): - with open(os.path.join(self.cwd, file_name), "rb") as file: - yield file.read() + cwd: str | None = None, + ) -> t.Generator[ExecChunk, None, None]: + code = resolve_pathlike(code) + + old_stdout = sys.stdout + old_stderr = sys.stderr + redirected_output = sys.stdout = StringIO() + redirected_error = sys.stderr = StringIO() + + try: + if kernel == "ipython": + result = self.shell.run_cell(code) + output = redirected_output.getvalue() + error = redirected_error.getvalue() + + if "" + image_matches = re.findall(image_pattern, output) + for img_str in image_matches: + yield ExecChunk(type="image", content=img_str) + elif output: + yield ExecChunk(type="text", content=output) + if error: + yield ExecChunk(type="error", content=error) + if result.result is not None: + yield ExecChunk(type="text", content=str(result.result)) + + elif kernel == "bash": + try: + process = subprocess.Popen( + code, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + cwd=cwd, + ) + try: + stdout, stderr = process.communicate(timeout=timeout) + if stdout: + yield ExecChunk(type="text", content=stdout) + if stderr: + yield ExecChunk(type="error", content=stderr) + if process.returncode != 0: + yield ExecChunk( + type="error", + content="Command failed with " + f"exit code {process.returncode}", + ) + except subprocess.TimeoutExpired: + process.kill() + yield ExecChunk(type="error", content="Command timed out") + except Exception as e: + yield ExecChunk(type="error", content=str(e)) + else: + raise ValueError(f"Unsupported kernel: {kernel}") + finally: + sys.stdout = old_stdout + sys.stderr = old_stderr async def astream_exec( self, - code: Union[str, PathLike], - language: Literal["python", "bash"] = "python", + code: str | os.PathLike, + kernel: t.Literal["ipython", "bash"] = "ipython", timeout: float | None = None, cwd: str | None = None, - ) -> AsyncGenerator[ExecChunk, None]: - code = utils.resolve_pathlike(code) - - if language == "python": - msg_queue: asyncio.Queue = asyncio.Queue() - - async def output_hook(msg): - await msg_queue.put(msg) - - execution_task = asyncio.create_task( - self.kernel.client()._async_execute_interactive( - code, output_hook=output_hook, timeout=timeout + ) -> t.AsyncGenerator[ExecChunk, None]: + code = resolve_pathlike(code) + + old_stdout = sys.stdout + old_stderr = sys.stderr + redirected_output = sys.stdout = StringIO() + redirected_error = sys.stderr = StringIO() + + try: + if kernel == "ipython": + result = await self.shell.run_cell_async( + code, store_history=False, silent=True ) - ) - - try: - while not execution_task.done() or not msg_queue.empty(): - msg = await msg_queue.get() - yield utils.parse_message(msg) - finally: - if not execution_task.done(): - execution_task.cancel() - try: - await execution_task - except asyncio.CancelledError: - pass - - elif language == "bash": - async with asyncio.timeout(timeout): - process = await asyncio.create_subprocess_shell( - code, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.STDOUT, - cwd=cwd, - ) - if process.stdout: - async for line in process.stdout: - yield ExecChunk(type="stream", content=line.decode().strip()) - await process.wait() - if process.returncode != 0: - yield ExecChunk(type="error", content="Command execution failed") - - else: - raise ValueError(f"Unsupported language: {language}") + output = redirected_output.getvalue() + error = redirected_error.getvalue() + if "" + image_matches = re.findall(image_pattern, output) + for img_str in image_matches: + yield ExecChunk(type="image", content=img_str) + elif output: + yield ExecChunk(type="text", content=output) + if error: + yield ExecChunk(type="error", content=error) + if result.result is not None: + yield ExecChunk(type="text", content=str(result.result)) + elif kernel == "bash": + try: + process = await asyncio.create_subprocess_shell( + code, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + cwd=cwd, + ) + stdout, stderr = await asyncio.wait_for( + process.communicate(), timeout=timeout + ) + if stdout: + yield ExecChunk(type="text", content=stdout.decode()) + if stderr: + yield ExecChunk(type="error", content=stderr.decode()) + if process.returncode != 0: + yield ExecChunk( + type="error", + content="Command failed with " + f"exit code {process.returncode}", + ) + except asyncio.TimeoutError: + yield ExecChunk(type="error", content="Command timed out") + else: + raise ValueError(f"Unsupported kernel: {kernel}") + finally: + sys.stdout = old_stdout + sys.stderr = old_stderr async def aupload( self, file_name: str, - content: BinaryIO | bytes | str, + content: t.BinaryIO | bytes | str, timeout: float | None = None, ) -> CodeBoxFile: - import aiofiles + import aiofiles.os async with asyncio.timeout(timeout): file_path = os.path.join(self.cwd, file_name) async with aiofiles.open(file_path, "wb") as file: if isinstance(content, str): await file.write(content.encode()) - elif isinstance(content, BinaryIO): + elif isinstance(content, t.BinaryIO): while chunk := content.read(8192): await file.write(chunk) else: @@ -220,14 +211,23 @@ async def aupload( return CodeBoxFile( remote_path=file_path, size=file_size, - codebox=self, + codebox_id=self.session_id, ) + def stream_download( + self, + remote_file_path: str, + timeout: float | None = None, + ) -> Generator[bytes, None, None]: + with raise_timeout(timeout): + with open(os.path.join(self.cwd, remote_file_path), "rb") as f: + yield f.read() + async def astream_download( self, remote_file_path: str, timeout: float | None = None, - ) -> AsyncIterator[bytes]: + ) -> t.AsyncGenerator[bytes, None]: import aiofiles async with asyncio.timeout(timeout): @@ -235,6 +235,3 @@ async def astream_download( os.path.join(self.cwd, remote_file_path), "rb" ) as f: yield await f.read() - - def __del__(self): - self.kernel.shutdown_kernel() From 46fed727cb8649ab6dbaf39b6d1bb62651a3cc61 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 17 Jul 2024 13:31:40 -0700 Subject: [PATCH 064/125] =?UTF-8?q?=E2=9C=A8=20=E2=8F=AB=20way=20simpler?= =?UTF-8?q?=20remotebox?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/codeboxapi/remote.py | 95 +++++++++++++++++++++------------------- 1 file changed, 50 insertions(+), 45 deletions(-) diff --git a/src/codeboxapi/remote.py b/src/codeboxapi/remote.py index 0ece202..722e438 100644 --- a/src/codeboxapi/remote.py +++ b/src/codeboxapi/remote.py @@ -1,14 +1,11 @@ -from json import loads -from os import PathLike -from typing import AsyncGenerator, AsyncIterator, BinaryIO, Generator, Iterator, Literal +from os import PathLike, getenv +from typing import AsyncGenerator, BinaryIO, Generator, Literal from uuid import uuid4 -import anyio import httpx -from . import utils from .codebox import CodeBox, CodeBoxFile, ExecChunk -from .config import settings +from .utils import raise_error, resolve_pathlike class RemoteBox(CodeBox): @@ -16,69 +13,63 @@ class RemoteBox(CodeBox): Sandboxed Python Interpreter """ - def __new__(cls, *args, **kwargs): - if kwargs.pop("local", False) or settings.api_key == "local": - from .local import LocalBox - - return LocalBox(*args, **kwargs) - return super().__new__(cls) + def __new__(cls) -> "RemoteBox": + # This is a hack to ignore the CodeBox.__new__ factory method. + return object.__new__(cls) def __init__( self, - factory_id: str | None = None, - api_key: str | None = None, + session_id: str | None = None, + api_key: str | Literal["local", "docker"] = "local", + factory_id: str | Literal["default"] = "default", + base_url: str = "https://codeboxapi.com/api/v2", + _new: bool = False, ) -> None: - super().__init__() - self.session_id = uuid4().hex + self.session_id = session_id or uuid4().hex self.factory_id = factory_id - self.api_key = api_key or settings.api_key - self.aclient = httpx.AsyncClient( - base_url=f"{settings.base_url}/codebox/{self.session_id}" + self.api_key = ( + api_key + or getenv("CODEBOX_API_KEY") + or raise_error("CODEBOX_API_KEY is required") ) + self.base_url = f"{base_url}/codebox/{self.session_id}" + self.headers = {"Factory-Id": self.factory_id} if self.factory_id else None + self.client = httpx.Client(base_url=self.base_url, headers=self.headers) + self.aclient = httpx.AsyncClient(base_url=self.base_url, headers=self.headers) def stream_exec( self, code: str | PathLike, - language: Literal["python", "bash"] = "python", + kernel: Literal["ipython", "bash"] = "ipython", timeout: float | None = None, cwd: str | None = None, ) -> Generator[ExecChunk, None, None]: - async_gen = self.astream_exec(code, language, timeout, cwd) - return (chunk for chunk in anyio.run(utils.collect_async_gen, async_gen)) - - def upload( - self, - file_name: str, - content: BinaryIO | bytes | str, - timeout: float | None = None, - ) -> CodeBoxFile: - return anyio.run(self.aupload, file_name, content, timeout) - - def stream_download( - self, - remote_file_path: str, - timeout: float | None = None, - ) -> Iterator[bytes]: - return anyio.run( - utils.collect_async_gen, self.astream_download(remote_file_path, timeout) - ) + code = resolve_pathlike(code) + with self.client.stream( + method="POST", + url="/stream", + timeout=timeout, + params={"code": code, "kernel": kernel, "cwd": cwd}, + ) as response: + for chunk in response.iter_text(): + yield ExecChunk.decode(chunk) async def astream_exec( self, code: str | PathLike, - language: Literal["python", "bash"] = "python", + kernel: Literal["ipython", "bash"] = "ipython", timeout: float | None = None, cwd: str | None = None, ) -> AsyncGenerator[ExecChunk, None]: - code = utils.resolve_pathlike(code) + code = resolve_pathlike(code) async with self.aclient.stream( method="POST", url="/stream", timeout=timeout, - params={"code": code, "language": language, "cwd": cwd}, + params={"code": code, "kernel": kernel, "cwd": cwd}, ) as response: async for chunk in response.aiter_text(): - yield ExecChunk(**loads(chunk)) + yield ExecChunk.decode(chunk) async def aupload( self, @@ -96,14 +87,28 @@ async def aupload( timeout=timeout, ) ).json(), - codebox=self, + codebox_id=self.session_id, ) + def stream_download( + self, + remote_file_path: str, + timeout: float | None = None, + ) -> Generator[bytes, None, None]: + with self.client.stream( + method="GET", + url="/download", + timeout=timeout, + params={"file_name": remote_file_path}, + ) as response: + for chunk in response.iter_bytes(): + yield chunk + async def astream_download( self, remote_file_path: str, timeout: float | None = None, - ) -> AsyncIterator[bytes]: + ) -> AsyncGenerator[bytes, None]: async with self.aclient.stream( method="GET", url="/download", From 7f0413f8a36d34bc0e5c7bec3e45afb3f146f1f0 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 18 Jul 2024 04:20:00 +0200 Subject: [PATCH 065/125] =?UTF-8?q?=F0=9F=94=A7=20fix=20api+remote=20box?= =?UTF-8?q?=20sync?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/codeboxapi/api.py | 29 ++++++++++++++++++----------- src/codeboxapi/remote.py | 4 ++-- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/codeboxapi/api.py b/src/codeboxapi/api.py index e7bea65..df3d8a3 100644 --- a/src/codeboxapi/api.py +++ b/src/codeboxapi/api.py @@ -2,10 +2,11 @@ from contextlib import asynccontextmanager from datetime import datetime, timedelta from os import getenv -from typing import Annotated, AsyncGenerator, Literal +from typing import AsyncGenerator, Literal -from fastapi import Body, Depends, FastAPI, HTTPException, UploadFile +from fastapi import Depends, FastAPI, HTTPException, UploadFile from fastapi.responses import StreamingResponse +from pydantic import BaseModel from .local import LocalBox from .utils import CodeBoxFile @@ -41,19 +42,21 @@ async def healthcheck() -> dict[str, str]: return {"status": "ok"} +class ExecBody(BaseModel): + code: str + kernel: Literal["ipython", "bash"] = "ipython" + timeout: int | None = None + cwd: str | None = None + + @app.post("/exec") async def exec( - code: Annotated[str, Body()], - kernel: Literal["ipython", "bash"] = "ipython", - timeout: int | None = None, - cwd: str | None = None, - codebox: LocalBox = Depends(get_codebox), + exec: ExecBody, codebox: LocalBox = Depends(get_codebox) ) -> StreamingResponse: - print("code", code) - async def event_stream() -> AsyncGenerator[str, None]: - async for chunk in codebox.astream_exec(code, kernel, timeout, cwd): - print("chunk", chunk) + async for chunk in codebox.astream_exec( + exec.code, exec.kernel, exec.timeout, exec.cwd + ): yield chunk.__str__() return StreamingResponse(event_stream()) @@ -83,3 +86,7 @@ def serve(): import uvicorn uvicorn.run(app, host="0.0.0.0", port=getenv("CODEBOX_PORT", 8069)) + + +if __name__ == "__main__": + serve() diff --git a/src/codeboxapi/remote.py b/src/codeboxapi/remote.py index 722e438..a9a7754 100644 --- a/src/codeboxapi/remote.py +++ b/src/codeboxapi/remote.py @@ -49,7 +49,7 @@ def stream_exec( method="POST", url="/stream", timeout=timeout, - params={"code": code, "kernel": kernel, "cwd": cwd}, + json={"code": code, "kernel": kernel, "cwd": cwd}, ) as response: for chunk in response.iter_text(): yield ExecChunk.decode(chunk) @@ -66,7 +66,7 @@ async def astream_exec( method="POST", url="/stream", timeout=timeout, - params={"code": code, "kernel": kernel, "cwd": cwd}, + json={"code": code, "kernel": kernel, "cwd": cwd}, ) as response: async for chunk in response.aiter_text(): yield ExecChunk.decode(chunk) From f99eb64cea141cd24c993d860de3154acf9c7fad Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 19 Jul 2024 04:20:00 +0200 Subject: [PATCH 066/125] =?UTF-8?q?=E2=9B=93=EF=B8=8F=20fix=20ipython=20st?= =?UTF-8?q?reaming?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/codeboxapi/codebox.py | 16 ++- src/codeboxapi/local.py | 296 ++++++++++++++++++++++++-------------- 2 files changed, 202 insertions(+), 110 deletions(-) diff --git a/src/codeboxapi/codebox.py b/src/codeboxapi/codebox.py index 70777c7..eb24b24 100644 --- a/src/codeboxapi/codebox.py +++ b/src/codeboxapi/codebox.py @@ -92,7 +92,7 @@ def exec( timeout: float | None = None, cwd: str | None = None, ) -> ExecResult: - """Execute python code inside the CodeBox instance""" + """Execute code inside the CodeBox instance""" return flatten_exec_result(self.stream_exec(code, kernel, timeout, cwd)) def stream_exec( @@ -102,7 +102,7 @@ def stream_exec( timeout: float | None = None, cwd: str | None = None, ) -> Generator[ExecChunk, None, None]: - """Stream Chunks of Execute python code inside the CodeBox instance""" + """Executes the code and streams the result.""" raise NotImplementedError("Abstract method, please use a subclass.") def upload( @@ -216,8 +216,12 @@ def _parse_size(self, size_str: str) -> int: async def alist_packages(self) -> list[str]: return (await self.aexec("uv pip list", kernel="bash")).text.splitlines() - async def alist_variables(self) -> list[str]: - return (await self.aexec("%who")).text.splitlines() + async def ashow_variables(self) -> dict[str, str]: + vars = [ + line.strip() for line in (await self.aexec("%who")).text.strip().split("\t") + ] + # todo remove that splitting thing when Out[0] thing is fixed + return {v: (await self.aexec(v)).text for v in vars} async def arestart(self) -> None: """Restart the Jupyter kernel""" @@ -253,8 +257,8 @@ def list_files(self) -> list[CodeBoxFile]: def list_packages(self) -> list[str]: return syncify(self.alist_packages)() - def list_variables(self) -> list[str]: - return syncify(self.alist_variables)() + def show_variables(self) -> dict[str, str]: + return syncify(self.ashow_variables)() def restart(self) -> None: return syncify(self.arestart)() diff --git a/src/codeboxapi/local.py b/src/codeboxapi/local.py index 54a0887..9dc14f5 100644 --- a/src/codeboxapi/local.py +++ b/src/codeboxapi/local.py @@ -1,6 +1,6 @@ """ Local implementation of CodeBox. -This is useful for testing and development.c +This is useful for testing and development. In case you don't put an api_key, this is the default CodeBox. """ @@ -12,14 +12,22 @@ import re import subprocess import sys +import threading +import time import typing as t -from io import StringIO -from typing import Generator +from queue import Queue -from IPython.core.interactiveshell import InteractiveShell +from IPython.core.interactiveshell import ExecutionResult, InteractiveShell from .codebox import CodeBox, CodeBoxFile, ExecChunk -from .utils import check_installed, raise_timeout, resolve_pathlike +from .utils import check_installed, raise_timeout, resolve_pathlike, run_inside + + +def _print(*text, stdout): + _stdout = sys.stdout + sys.stdout = stdout + print(*text, flush=True) + sys.stdout = _stdout class LocalBox(CodeBox): @@ -41,13 +49,15 @@ def __init__( self.session_id = session_id or "" os.makedirs(codebox_cwd, exist_ok=True) self.cwd = os.path.abspath(codebox_cwd) - os.chdir(self.cwd) check_installed("ipython") self.shell = InteractiveShell.instance() self.shell.enable_gui = lambda x: None # type: ignore self._patch_matplotlib_show() def _patch_matplotlib_show(self) -> None: + import matplotlib + + matplotlib.use("Agg") import matplotlib.pyplot as plt def custom_show(close=True): @@ -69,63 +79,116 @@ def stream_exec( timeout: float | None = None, cwd: str | None = None, ) -> t.Generator[ExecChunk, None, None]: - code = resolve_pathlike(code) - - old_stdout = sys.stdout - old_stderr = sys.stderr - redirected_output = sys.stdout = StringIO() - redirected_error = sys.stderr = StringIO() + with raise_timeout(timeout): + code = resolve_pathlike(code) - try: if kernel == "ipython": - result = self.shell.run_cell(code) - output = redirected_output.getvalue() - error = redirected_error.getvalue() - - if "" - image_matches = re.findall(image_pattern, output) - for img_str in image_matches: - yield ExecChunk(type="image", content=img_str) - elif output: - yield ExecChunk(type="text", content=output) - if error: - yield ExecChunk(type="error", content=error) - if result.result is not None: - yield ExecChunk(type="text", content=str(result.result)) - - elif kernel == "bash": - try: - process = subprocess.Popen( - code, - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - cwd=cwd, + with run_inside(cwd or self.cwd): + old_stdout, old_stderr = sys.stdout, sys.stderr + temp_output, temp_error = sys.stdout, sys.stderr = ( + io.StringIO(), + io.StringIO(), ) try: - stdout, stderr = process.communicate(timeout=timeout) - if stdout: - yield ExecChunk(type="text", content=stdout) - if stderr: - yield ExecChunk(type="error", content=stderr) - if process.returncode != 0: + queue = Queue[ExecChunk | None]() + _result: list[ExecutionResult] = [] + + def _run_cell(c: str, result: list[ExecutionResult]) -> None: + time.sleep(0.0001) + result.append(self.shell.run_cell(c)) + + run_cell = threading.Thread( + target=_run_cell, args=(code, _result) + ) + + def stream_chunks(_out: io.StringIO, _err: io.StringIO) -> None: + while run_cell.is_alive(): + time.sleep(0.0001) + if output := _out.getvalue(): + # todo make this more efficient? + sys.stdout = _out = io.StringIO() + + if "" + ) + image_matches = re.findall( + image_pattern, output + ) + for img_str in image_matches: + queue.put( + ExecChunk(type="image", content=img_str) + ) + output = re.sub(image_pattern, "", output) + + if output: + if output.startswith("Out["): + # todo better disable logging somehow + output = re.sub(r"Out[(.*?)]: ", "", output) + queue.put( + ExecChunk(type="text", content=output) + ) + + if error := _err.getvalue(): + # todo make this more efficient? + sys.stderr = _err = io.StringIO() + queue.put(ExecChunk(type="error", content=error)) + + queue.put(None) + + stream = threading.Thread( + target=stream_chunks, args=(temp_output, temp_error) + ) + + run_cell.start() + stream.start() + + while True: + time.sleep(0.001) + if queue.qsize() > 0: + if chunk := queue.get(): + yield chunk + else: + break + + result = _result[0] + if result.error_before_exec: + yield ExecChunk( + type="error", + content=str(result.error_before_exec).replace( + "\\n", "\n" + ), + ) + elif result.error_in_exec: yield ExecChunk( type="error", - content="Command failed with " - f"exit code {process.returncode}", + content=str(result.error_in_exec).replace("\\n", "\n"), ) - except subprocess.TimeoutExpired: - process.kill() - yield ExecChunk(type="error", content="Command timed out") - except Exception as e: - yield ExecChunk(type="error", content=str(e)) + elif result.result is not None: + yield ExecChunk(type="text", content=str(result.result)) + + finally: + sys.stdout = old_stdout + sys.stderr = old_stderr + + elif kernel == "bash": + # todo maybe implement using queue + process = subprocess.Popen( + code, + cwd=cwd or self.cwd, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + if process.stdout: + for c in process.stdout: + yield ExecChunk(content=c.decode(), type="text") + if process.stderr: + for c in process.stderr: + yield ExecChunk(content=c.decode(), type="error") + else: raise ValueError(f"Unsupported kernel: {kernel}") - finally: - sys.stdout = old_stdout - sys.stderr = old_stderr async def astream_exec( self, @@ -134,59 +197,82 @@ async def astream_exec( timeout: float | None = None, cwd: str | None = None, ) -> t.AsyncGenerator[ExecChunk, None]: - code = resolve_pathlike(code) - - old_stdout = sys.stdout - old_stderr = sys.stderr - redirected_output = sys.stdout = StringIO() - redirected_error = sys.stderr = StringIO() + async with asyncio.timeout(timeout): + code = resolve_pathlike(code) - try: if kernel == "ipython": - result = await self.shell.run_cell_async( - code, store_history=False, silent=True - ) - output = redirected_output.getvalue() - error = redirected_error.getvalue() - if "" - image_matches = re.findall(image_pattern, output) - for img_str in image_matches: - yield ExecChunk(type="image", content=img_str) - elif output: - yield ExecChunk(type="text", content=output) - if error: - yield ExecChunk(type="error", content=error) - if result.result is not None: - yield ExecChunk(type="text", content=str(result.result)) - elif kernel == "bash": - try: - process = await asyncio.create_subprocess_shell( - code, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - cwd=cwd, - ) - stdout, stderr = await asyncio.wait_for( - process.communicate(), timeout=timeout + with run_inside(cwd or self.cwd): + old_stdout, old_stderr = sys.stdout, sys.stderr + temp_output, temp_error = sys.stdout, sys.stderr = ( + io.StringIO(), + io.StringIO(), ) - if stdout: - yield ExecChunk(type="text", content=stdout.decode()) - if stderr: - yield ExecChunk(type="error", content=stderr.decode()) - if process.returncode != 0: - yield ExecChunk( - type="error", - content="Command failed with " - f"exit code {process.returncode}", + + try: + run_cell = asyncio.create_task( + asyncio.to_thread(self.shell.run_cell, code) ) - except asyncio.TimeoutError: - yield ExecChunk(type="error", content="Command timed out") + + while not run_cell.done(): + await asyncio.sleep(0.001) + if output := temp_output.getvalue(): + # todo make this more efficient? + sys.stdout = temp_output = io.StringIO() + + if "" + ) + image_matches = re.findall(image_pattern, output) + for img_str in image_matches: + yield ExecChunk(type="image", content=img_str) + output = re.sub(image_pattern, "", output) + + if output: + if output.startswith("Out["): + # todo better disable logging somehow + output = re.sub( + r"Out\[(.*?)\]: ", "", output.strip() + ) + yield ExecChunk(type="text", content=output) + + if error := temp_error.getvalue(): + sys.stderr = temp_error = io.StringIO() + yield ExecChunk(type="error", content=error) + + result = await run_cell + if result.error_before_exec: + yield ExecChunk( + type="error", content=str(result.error_before_exec) + ) + elif result.error_in_exec: + yield ExecChunk( + type="error", content=str(result.error_in_exec) + ) + elif result.result is not None: + yield ExecChunk(type="text", content=str(result.result)) + finally: + sys.stdout = old_stdout + sys.stderr = old_stderr + + elif kernel == "bash": + process = await asyncio.create_subprocess_shell( + code, + cwd=cwd or self.cwd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + + # todo yield at the same time and not after each other + if process.stdout: + async for chunk in process.stdout: + yield ExecChunk(content=chunk.decode(), type="text") + + if process.stderr: + async for err in process.stderr: + yield ExecChunk(content=err.decode(), type="error") else: raise ValueError(f"Unsupported kernel: {kernel}") - finally: - sys.stdout = old_stdout - sys.stderr = old_stderr async def aupload( self, @@ -218,10 +304,11 @@ def stream_download( self, remote_file_path: str, timeout: float | None = None, - ) -> Generator[bytes, None, None]: + ) -> t.Generator[bytes, None, None]: with raise_timeout(timeout): with open(os.path.join(self.cwd, remote_file_path), "rb") as f: - yield f.read() + while chunk := f.read(8192): + yield chunk async def astream_download( self, @@ -234,4 +321,5 @@ async def astream_download( async with aiofiles.open( os.path.join(self.cwd, remote_file_path), "rb" ) as f: - yield await f.read() + while chunk := await f.read(8192): + yield chunk From 90477b66a1f988690869ee2d9bc957d2afa3f703 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 21 Jul 2024 04:20:00 +0200 Subject: [PATCH 067/125] =?UTF-8?q?=F0=9F=90=8B=20simplify=20docker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/Dockerfile b/Dockerfile index b4a4b38..f3fa0ce 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,8 @@ -FROM python:slim-bookworm as build +FROM shroominic/python-uv:3.10 COPY README.md pyproject.toml src / -RUN pip install -e .[all] +RUN uv pip install --no-cache-dir -e .[all] && \ + rm -rf README.md pyproject.toml src -FROM python:slim-bookworm as runtime - -RUN useradd -ms /bin/bash codebox-user - -COPY --chown=codebox-user:codebox-user --from=build /usr/local/ /usr/local/ - -USER codebox-user - -ENV PATH="/usr/local/bin:$PATH" - -CMD codeboxapi-serve +CMD ["/.venv/bin/python", "-m", "codeboxapi.api"] From 6aa1aecf855746552b25cd84ae7da2532272d422 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jul 2024 00:44:10 +0200 Subject: [PATCH 068/125] rm anyio ref --- requirements.lock | 2 -- 1 file changed, 2 deletions(-) diff --git a/requirements.lock b/requirements.lock index 7a47e04..651b43f 100644 --- a/requirements.lock +++ b/requirements.lock @@ -21,8 +21,6 @@ httpcore==1.0.5 httpx==0.27.0 # via codeboxapi idna==3.7 - # via anyio # via httpx sniffio==1.3.1 - # via anyio # via httpx From 511c47bad25504d2e8fc45a46ee69010d8d39cf7 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jul 2024 00:46:29 +0200 Subject: [PATCH 069/125] =?UTF-8?q?=F0=9F=93=9C=20update=20roadmap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- roadmap.todo | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/roadmap.todo b/roadmap.todo index 29d14ae..9baea17 100644 --- a/roadmap.todo +++ b/roadmap.todo @@ -1,23 +1,15 @@ -[ ] - gpu support - [ ] - different python versions -[ ] - node/bun engine - -[ ] - metadata kw storage - -[ ] - different container sizes (cpu/ram) - -[ ] - variable timeout +[ ] - gpu support -[ ] - request only mode with dynamic filesystem +[ ] - js kernel -[ ] - general dynamic filesystem +[ ] - metadata for kw storage -[ ] - requirements on startup +[ ] - request only mode -[ ] - chromiumbox + vectorbox +[ ] - s3 filesystems -[ ] - gitbox +[ ] - add pypi requirements to factory -[ ] - seperate .venv for localbox managed by uv +[ ] - visual-chromium-box From 4c0a5dd3192f46b7edf3ee0dc5a11de11818b32a Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jul 2024 00:48:40 +0200 Subject: [PATCH 070/125] =?UTF-8?q?=F0=9F=A7=AA=20finally=20good=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/run_all_examples.py | 47 ---- tests/{test_v01_general.py => test_v01.py} | 0 tests/test_v02.py | 303 +++++++++++++++++++++ 3 files changed, 303 insertions(+), 47 deletions(-) delete mode 100644 tests/run_all_examples.py rename tests/{test_v01_general.py => test_v01.py} (100%) create mode 100644 tests/test_v02.py diff --git a/tests/run_all_examples.py b/tests/run_all_examples.py deleted file mode 100644 index dd4ffe1..0000000 --- a/tests/run_all_examples.py +++ /dev/null @@ -1,47 +0,0 @@ -import asyncio -import os -import sys -from pathlib import Path - -from dotenv import load_dotenv - - -async def run_example(file: Path, local: bool = False): - process = await asyncio.create_subprocess_exec( - Path(sys.executable).absolute(), - file.absolute(), - env={"CODEBOX_API_KEY": "local" if local else os.environ["CODEBOX_API_KEY"]}, - ) - await process.wait() - - if process.returncode != 0: - raise Exception(f"Example {file} failed with return code {process.returncode}") - - -async def run_examples(): - if os.environ.get("CODEBOX_API_KEY") is None: - return print("Skipping remote examples because CODEBOX_API_KEY is not set") - - await asyncio.gather(*[ - asyncio.create_task(run_example(file)) - for file in list(Path("examples").glob("**/*.py")) - ]) - - -async def run_examples_local(): - for file in list(Path("examples").glob("**/*.py")): - await run_example(file, local=True) - - -# TODO: fix using pytest -def run_all_examples(): - """Integration test for running the examples.""" - load_dotenv() - os.environ["CODEBOX_TEST"] = "True" - # TODO: Use ENV variable to reuse the same remote codebox - asyncio.run(run_examples()) - asyncio.run(run_examples_local()) - - -if __name__ == "__main__": - run_all_examples() diff --git a/tests/test_v01_general.py b/tests/test_v01.py similarity index 100% rename from tests/test_v01_general.py rename to tests/test_v01.py diff --git a/tests/test_v02.py b/tests/test_v02.py new file mode 100644 index 0000000..ed04d33 --- /dev/null +++ b/tests/test_v02.py @@ -0,0 +1,303 @@ +import asyncio +from typing import Literal + +import pytest +from codeboxapi import CodeBox +from codeboxapi.utils import CodeBoxFile, ExecChunk, ExecResult + + +@pytest.fixture( + scope="session", params=["local"] +) # , "docker", getenv("CODEBOX_API_KEY")]) +def codebox(request): + return CodeBox() # api_key=request.param) + + +@pytest.mark.parametrize("method", ["sync", "async"]) +def test_codebox_lifecycle(codebox: CodeBox, method: Literal["sync", "async"]): + if method == "sync": + _test_sync_lifecycle(codebox) + else: + asyncio.run(_test_async_lifecycle(codebox)) + + +def _test_sync_lifecycle(codebox: CodeBox): + assert codebox.healthcheck() == "healthy", "CodeBox should be healthy" + + result = codebox.exec("print('Hello World!')") + assert isinstance(result, ExecResult), "Exec should return an ExecResult" + assert result.text.strip() == "Hello World!", "Exec should print 'Hello World!'" + assert not result.errors, "Exec should not produce errors" + + file_name = "test_file.txt" + file_content = b"Hello World!" + uploaded_file = codebox.upload(file_name, file_content) + assert isinstance(uploaded_file, CodeBoxFile), "Upload should return a CodeBoxFile" + assert uploaded_file.name == file_name, "Uploaded file should have correct name" + assert uploaded_file.size == len( + file_content + ), "Uploaded file should have correct size" + + downloaded_file = codebox.download(file_name) + assert isinstance( + downloaded_file, CodeBoxFile + ), "Download should return a CodeBoxFile" + assert ( + downloaded_file.content == file_content + ), "Downloaded content should match uploaded content" + + install_result = codebox.install("matplotlib") + assert "matplotlib" in install_result, "Matplotlib should be installed successfully" + + exec_result = codebox.exec("import matplotlib; print(matplotlib.__version__)") + assert exec_result.errors == [], "Importing matplotlib should not produce errors" + assert exec_result.text.strip() != "", "Matplotlib version should be printed" + + plot_result = codebox.exec( + "import matplotlib.pyplot as plt; " + "plt.figure(figsize=(10, 5)); " + "plt.plot([1, 2, 3, 4], [1, 4, 2, 3]); " + "plt.title('Test Plot'); " + "plt.xlabel('X-axis'); " + "plt.ylabel('Y-axis'); " + "plt.show()" + ) + assert plot_result.images, "Plot execution should produce an image" + assert ( + len(plot_result.images) == 1 + ), "Plot execution should produce exactly one image" + + +async def _test_async_lifecycle(codebox: CodeBox): + assert await codebox.ahealthcheck() == "healthy", "CodeBox should be healthy" + + result = await codebox.aexec("print('Hello World!')") + assert isinstance(result, ExecResult), "Exec should return an ExecResult" + assert result.text.strip() == "Hello World!", "Exec should print 'Hello World!'" + assert not result.errors, "Exec should not produce errors" + + file_name = "test_file.txt" + file_content = b"Hello World!" + uploaded_file = await codebox.aupload(file_name, file_content) + assert isinstance(uploaded_file, CodeBoxFile), "Upload should return a CodeBoxFile" + assert uploaded_file.name == file_name, "Uploaded file should have correct name" + assert uploaded_file.size == len( + file_content + ), "Uploaded file should have correct size" + + downloaded_file = await codebox.adownload(file_name) + assert isinstance( + downloaded_file, CodeBoxFile + ), "Download should return a CodeBoxFile" + assert ( + await downloaded_file.acontent == file_content + ), "Downloaded content should match uploaded content" + + install_result = await codebox.ainstall("matplotlib") + assert "matplotlib" in install_result, "Matplotlib should be installed successfully" + + exec_result = await codebox.aexec( + "import matplotlib; print(matplotlib.__version__)" + ) + assert exec_result.errors == [], "Importing matplotlib should not produce errors" + assert exec_result.text.strip() != "", "Matplotlib version should be printed" + + plot_result = await codebox.aexec( + "import matplotlib.pyplot as plt; " + "plt.figure(figsize=(10, 5)); " + "plt.plot([1, 2, 3, 4], [1, 4, 2, 3]); " + "plt.title('Test Plot'); " + "plt.xlabel('X-axis'); " + "plt.ylabel('Y-axis'); " + "plt.show()" + ) + assert plot_result.images, "Plot execution should produce an image" + assert ( + len(plot_result.images) == 1 + ), "Plot execution should produce exactly one image" + + +@pytest.mark.parametrize("method", ["sync", "async"]) +def test_list_operations(codebox: CodeBox, method: Literal["sync", "async"]): + if method == "sync": + _test_sync_list_operations(codebox) + else: + asyncio.run(_test_async_list_operations(codebox)) + + +def _test_sync_list_operations(codebox: CodeBox): + codebox.exec("x = 1; y = 'test'; z = [1, 2, 3]") + variables = codebox.show_variables() + assert "x" in variables.keys(), "Variable 'x' should be listed" + assert "1" in variables["x"], "Variable 'x' should contain value '1'" + assert "y" in variables.keys(), "Variable 'y' should be listed" + assert "test" in variables["y"], "Variable 'y' should contain value 'test'" + assert "z" in variables.keys(), "Variable 'z' should be listed" + assert "[1, 2, 3]" in variables["z"], "Variable 'z' should contain value '[1, 2, 3]" + + files = codebox.list_files() + assert isinstance(files, list), "list_files should return a list" + assert all( + isinstance(f, CodeBoxFile) for f in files + ), "All items in list_files should be CodeBoxFile instances" + + packages = codebox.list_packages() + assert isinstance(packages, list), "list_packages should return a list" + assert len(packages) > 0, "There should be at least one package installed" + assert any( + "matplotlib" in pkg for pkg in packages + ), "Matplotlib should be in the list of packages" + + +async def _test_async_list_operations(codebox: CodeBox): + await codebox.aexec("x = 1; y = 'test'; z = [1, 2, 3]") + variables = await codebox.ashow_variables() + assert "x" in variables.keys(), "Variable 'x' should be listed" + assert "1" in variables["x"], "Variable 'x' should contain value '1'" + assert "y" in variables.keys(), "Variable 'y' should be listed" + assert "test" in variables["y"], "Variable 'y' should contain value 'test'" + assert "z" in variables.keys(), "Variable 'z' should be listed" + assert ( + "[1, 2, 3]" in variables["z"] + ), "Variable 'z' should contain value '[1, 2, 3]'" + + files = await codebox.alist_files() + assert isinstance(files, list), "list_files should return a list" + assert all( + isinstance(f, CodeBoxFile) for f in files + ), "All items in list_files should be CodeBoxFile instances" + + packages = await codebox.alist_packages() + assert isinstance(packages, list), "list_packages should return a list" + assert len(packages) > 0, "There should be at least one package installed" + assert any( + "matplotlib" in pkg for pkg in packages + ), "Matplotlib should be in the list of packages" + + +@pytest.mark.parametrize("method", ["sync", "async"]) +def test_stream_exec(codebox: CodeBox, method: Literal["sync", "async"]): + if method == "sync": + _test_sync_stream_exec(codebox) + else: + asyncio.run(_test_async_stream_exec(codebox)) + + +def _test_sync_stream_exec(codebox: CodeBox): + chunks = list( + codebox.stream_exec( + "import time;\nfor i in range(3): time.sleep(0.01); print(i)" + ) + ) + assert ( + len(chunks) == 3 + ), "iterating over stream_exec should produce 3 chunks (ipython)" + assert all( + isinstance(chunk, ExecChunk) for chunk in chunks + ), "All items should be ExecChunk instances (ipython)" + assert all( + chunk.type == "text" for chunk in chunks + ), "All chunks should be of type 'text' (ipython)" + assert [chunk.content.strip() for chunk in chunks] == [ + "0", + "1", + "2", + ], "Chunks should contain correct content (ipython)" + chunks = list( + codebox.stream_exec( + "python -c 'import time\nfor i in range(3): time.sleep(0.01); print(i)'", + kernel="bash", + ) + ) + assert len(chunks) == 3, "iterating over stream_exec should produce 3 chunks (bash)" + assert all( + isinstance(chunk, ExecChunk) for chunk in chunks + ), "All items should be ExecChunk instances (bash)" + assert all( + chunk.type == "text" for chunk in chunks + ), "All chunks should be of type 'text' (bash)" + print(chunks) + assert [chunk.content.strip() for chunk in chunks] == [ + "0", + "1", + "2", + ], "Chunks should contain correct content (bash)" + + +async def _test_async_stream_exec(codebox: CodeBox): + chunks = [ + chunk + async for chunk in codebox.astream_exec( + "import time;\nfor i in range(3): time.sleep(0.01); print(i)" + ) + ] + assert len(chunks) == 3, "Stream should produce 3 chunks" + assert all( + isinstance(chunk, ExecChunk) for chunk in chunks + ), "All items should be ExecChunk instances" + assert all( + chunk.type == "text" for chunk in chunks + ), "All chunks should be of type 'text'" + assert [chunk.content.strip() for chunk in chunks] == [ + "0", + "1", + "2", + ], "Chunks should contain correct content" + + +@pytest.mark.parametrize("method", ["sync", "async"]) +def test_error_handling(codebox: CodeBox, method: Literal["sync", "async"]): + if method == "sync": + _test_sync_error_handling(codebox) + else: + asyncio.run(_test_async_error_handling(codebox)) + + +def _test_sync_error_handling(codebox: CodeBox): + result = codebox.exec("1/0") + assert result.errors, "Execution should produce an error" + error = result.errors[0].lower() + assert ( + "division" in error and "zero" in error + ), "Error should be a ZeroDivisionError" + + +async def _test_async_error_handling(codebox: CodeBox): + result = await codebox.aexec("1/0") + assert result.errors, "Execution should produce an error" + error = result.errors[0].lower() + assert ( + "division" in error and "zero" in error + ), "Error should be a ZeroDivisionError" + + +@pytest.mark.parametrize("method", ["sync", "async"]) +def test_bash_commands(codebox: CodeBox, method: Literal["sync", "async"]): + if method == "sync": + _test_bash_commands(codebox) + else: + asyncio.run(_test_async_bash_commands(codebox)) + + +def _test_bash_commands(codebox: CodeBox): + result = codebox.exec("echo ok", kernel="bash") + assert "ok" in result.text, "Execution should contain 'ok'" + result = codebox.exec('echo print("Hello!") > test.py', kernel="bash") + assert result.text.strip() == "", "Execution result should be empty" + assert "test.py" in [file.remote_path for file in codebox.list_files()] + result = codebox.exec("python test.py", kernel="bash") + assert result.text.strip() == "Hello!", "Execution result should be 'Hello!'" + + +async def _test_async_bash_commands(codebox: CodeBox): + result = await codebox.aexec("echo ok", kernel="bash") + assert "ok" in result.text, "Execution should contain 'ok'" + result = await codebox.aexec("echo 'print(\"Hello!\")' > test.py", kernel="bash") + assert result.text.strip() == "", "Execution result should be empty" + assert "test.py" in [file.remote_path for file in await codebox.alist_files()] + result = await codebox.aexec("python test.py", kernel="bash") + assert result.text.strip() == "Hello!", "Execution result should be 'Hello!'" + + +if __name__ == "__main__": + pytest.main([__file__]) From 0d737d585ff8c33e84a999fd82d310b7a1846f0c Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jul 2024 00:49:46 +0200 Subject: [PATCH 071/125] cleanup --- src/codeboxapi/utils.py | 72 ++++++++++------------------------------- 1 file changed, 17 insertions(+), 55 deletions(-) diff --git a/src/codeboxapi/utils.py b/src/codeboxapi/utils.py index 4937625..c906cf5 100644 --- a/src/codeboxapi/utils.py +++ b/src/codeboxapi/utils.py @@ -1,9 +1,9 @@ +import os import signal from contextlib import contextmanager from dataclasses import dataclass from functools import partial, reduce, wraps from importlib.metadata import PackageNotFoundError, distribution -from os import PathLike, getenv from typing import ( TYPE_CHECKING, Any, @@ -92,10 +92,7 @@ class CodeBoxFile: def codebox(self) -> "CodeBox": from .codebox import CodeBox - return CodeBox( - self.codebox_id, - codebox_cwd="../.codebox", # type: ignore - ) + return CodeBox(self.codebox_id) @property def name(self) -> str: @@ -132,7 +129,7 @@ def deprecated(message: str) -> Callable[[Callable[P, T]], Callable[P, T]]: def decorator(func: Callable[P, T]) -> Callable[P, T]: @wraps(func) def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: - if getenv("IGNORE_DEPRECATION_WARNINGS", "false").lower() == "true": + if os.getenv("IGNORE_DEPRECATION_WARNINGS", "false").lower() == "true": return func(*args, **kwargs) warn( f"{func.__name__} is deprecated. {message}", @@ -146,8 +143,8 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: return decorator -def resolve_pathlike(file: str | PathLike) -> str: - if isinstance(file, PathLike): +def resolve_pathlike(file: str | os.PathLike) -> str: + if isinstance(file, os.PathLike): with open(file, "r") as f: return f.read() return file @@ -220,51 +217,6 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: return wrapper -def parse_message(msg: dict) -> ExecChunk: - """ - Parse a message from the Jupyter kernel. - The message is a dictionary which is a part of the message stream. - The output is a chunk of the execution result. - """ - if msg["msg_type"] == "stream": - return ExecChunk(type="stream", content=msg["content"]["text"]) - elif msg["msg_type"] == "execute_result": - return ExecChunk(type="text", content=msg["content"]["data"]["text/plain"]) - elif msg["msg_type"] == "display_data": - if "image/png" in msg["content"]["data"]: - return ExecChunk(type="image", content=msg["content"]["data"]["image/png"]) - if "text/plain" in msg["content"]["data"]: - return ExecChunk(type="text", content=msg["data"]["text/plain"]) - return ExecChunk(type="error", content="Could not parse output") - elif msg["msg_type"] == "status" and msg["content"]["execution_state"] == "idle": - return ExecChunk(type="text", content="") - elif msg["msg_type"] == "error": - return ExecChunk( - type="error", - content=msg["content"]["ename"] + ": " + msg["content"]["evalue"], - ) - else: - return ExecChunk( - type="error", content="Could not parse output: Unsupported message type" - ) - - -def parse_messages(messages: list[dict]) -> ExecResult: - """ - Parse a list of messages from the Jupyter kernel. - The output is a list of chunks of the execution result. - """ - chunks = [] - for msg in messages: - if chunk := parse_message(msg): - chunks.append(chunk) - else: - chunks.append( - ExecChunk(type="text", content="/* exec successful - no output */") - ) - return ExecResult(chunks=chunks) - - def check_installed(package: str) -> None: """ Check if the given package is installed. @@ -272,7 +224,7 @@ def check_installed(package: str) -> None: try: distribution(package) except PackageNotFoundError: - if getenv("DEBUG", "false").lower() == "true": + if os.getenv("DEBUG", "false").lower() == "true": print( f"\nMake sure '{package}' is installed " "when using without a CODEBOX_API_KEY.\n" @@ -282,7 +234,7 @@ def check_installed(package: str) -> None: def debug_mode() -> bool: - return getenv("DEBUG", "false").lower() == "true" + return os.getenv("DEBUG", "false").lower() == "true" @contextmanager @@ -301,5 +253,15 @@ def timeout_handler(signum, frame): signal.alarm(0) +@contextmanager +def run_inside(directory: str): + old_cwd = os.getcwd() + os.chdir(directory) + try: + yield + finally: + os.chdir(old_cwd) + + def raise_error(message: str) -> NoReturn: raise Exception(message) From d383be3fbbb68093193d1815dca0a91cd77c5523 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jul 2024 00:50:28 +0200 Subject: [PATCH 072/125] =?UTF-8?q?=F0=9F=90=8B=20docker=20box?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/codeboxapi/docker.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/codeboxapi/docker.py b/src/codeboxapi/docker.py index 35c09ae..67dd073 100644 --- a/src/codeboxapi/docker.py +++ b/src/codeboxapi/docker.py @@ -37,4 +37,9 @@ def __init__( ["docker", "run", "-d", "--rm", "-p", f"{self.port}:8069", image], check=True, ) - self.aclient = httpx.AsyncClient(base_url=f"http://localhost:{self.port}") + self.base_url = f"http://localhost:{self.port}" + self.client = httpx.Client(base_url=self.base_url) + self.aclient = httpx.AsyncClient(base_url=self.base_url) + + # def healthcheck(self) -> str: + # return self.client.get("/").text From 2fc61f012a20096867c880a14cd4621a61dc098c Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jul 2024 00:50:54 +0200 Subject: [PATCH 073/125] cici --- .github/workflows/auto-tests.yml | 22 ++++++++++------------ .github/workflows/code-check.yml | 18 +++++++++--------- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/.github/workflows/auto-tests.yml b/.github/workflows/auto-tests.yml index 4196f51..c591369 100644 --- a/.github/workflows/auto-tests.yml +++ b/.github/workflows/auto-tests.yml @@ -2,21 +2,19 @@ name: 🔁 Pytest ⏳x15 on: schedule: - - cron: '*/15 * * * *' + - cron: "*/60 * * * *" jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: eifinger/setup-rye@v1 - with: - enable_cache: true - cache_prefix: 'venv-codeboxapi' - - name: Sync rye - run: rye sync - - name: Run Pytest - env: - CODEBOX_API_KEY: ${{ secrets.CODEBOX_API_KEY }} - run: rye run pytest + - uses: actions/checkout@v2 + - uses: eifinger/setup-rye@v1 + with: + enable_cache: true + cache_prefix: "venv-codeboxapi" + - run: rye sync + - run: rye run pytest + env: + CODEBOX_API_KEY: ${{ secrets.CODEBOX_API_KEY }} diff --git a/.github/workflows/code-check.yml b/.github/workflows/code-check.yml index 7954a61..454eed9 100644 --- a/.github/workflows/code-check.yml +++ b/.github/workflows/code-check.yml @@ -3,23 +3,23 @@ name: ☑️ CodeCheck on: [push] jobs: - pre-commit: - strategy: - matrix: - python-version: ['3.9', '3.10', '3.11'] - runs-on: ubuntu-latest - steps: + pre-commit: + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11"] + runs-on: ubuntu-latest + steps: - uses: actions/checkout@v2 - uses: eifinger/setup-rye@v1 with: enable-cache: true - cache-prefix: 'venv-codeboxapi' + cache-prefix: "venv-codeboxapi" - name: pin version run: rye pin ${{ matrix.python-version }} - name: Sync rye run: rye sync - - name: Run pre-commit - run: rye run pre-commit run --all-files + - name: Run ruff + run: rye run ruff - name: Run tests env: CODEBOX_API_KEY: ${{ secrets.CODEBOX_API_KEY }} From 410e3865d259000b4c74b983b4aad26c2f0ad780 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 23 Jul 2024 04:20:00 +0200 Subject: [PATCH 074/125] fix test01 --- tests/test_v01.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_v01.py b/tests/test_v01.py index 116266f..eb0bf06 100644 --- a/tests/test_v01.py +++ b/tests/test_v01.py @@ -1,16 +1,17 @@ import asyncio +import os from codeboxapi import CodeBox def test_codebox(): - codebox = CodeBox() + codebox = CodeBox(api_key=os.getenv("CODEBOX_API_KEY")) assert run_sync(codebox), "Failed to run sync codebox remotely" assert asyncio.run(run_async(codebox)), "Failed to run async codebox remotely" def test_localbox(): - codebox = CodeBox(local=True) + codebox = CodeBox(api_key="local") assert run_sync(codebox), "Failed to run sync codebox locally" assert asyncio.run(run_async(codebox)), "Failed to run async codebox locally" From badd0e299044e9b2bc161cbf73142f6c0c86abdc Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 24 Jul 2024 04:20:00 +0200 Subject: [PATCH 075/125] rm pre commit --- .pre-commit-config.yaml | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index ae6724d..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,8 +0,0 @@ -repos: - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.8 - hooks: - - id: ruff - args: [--fix] - - id: ruff-format - types_or: [python, pyi, jupyter] From ed25b29af084f549153cbb92458095f9ad9cc232 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 25 Jul 2024 17:31:27 +0200 Subject: [PATCH 076/125] =?UTF-8?q?=F0=9F=90=8B=20improve=20docker=20build?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index f3fa0ce..c6a9962 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,15 @@ -FROM shroominic/python-uv:3.10 +FROM ghcr.io/astral-sh/uv as uv +FROM --platform=arm64 python:3.11 as build + +ENV VIRTUAL_ENV=/.venv PATH="/.venv/bin:$PATH" + +COPY --from=uv /uv /uv COPY README.md pyproject.toml src / -RUN uv pip install --no-cache-dir -e .[all] && \ - rm -rf README.md pyproject.toml src +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=from=uv,source=/uv,target=/uv \ + /uv venv /.venv && /uv pip install -e .[all] \ + && rm -rf README.md pyproject.toml src CMD ["/.venv/bin/python", "-m", "codeboxapi.api"] From 7dc17117577be912b0516738e857787e10daf9ad Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 25 Jul 2024 17:33:15 +0200 Subject: [PATCH 077/125] update deps --- requirements.lock | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements.lock b/requirements.lock index 651b43f..7a47e04 100644 --- a/requirements.lock +++ b/requirements.lock @@ -21,6 +21,8 @@ httpcore==1.0.5 httpx==0.27.0 # via codeboxapi idna==3.7 + # via anyio # via httpx sniffio==1.3.1 + # via anyio # via httpx From 4f5ff810135addbc55ae78952ca45baa1c7764ca Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 25 Jul 2024 17:33:36 +0200 Subject: [PATCH 078/125] =?UTF-8?q?=F0=9F=90=8B=20build=20tag=20push?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/build.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/scripts/build.sh b/scripts/build.sh index ea468f9..aa1aaa9 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -1,10 +1,8 @@ TAG=${1:-latest} -rye build --wheel -c -q - docker build -t codebox . # todo move container to seperate codeboxapi account docker tag codebox:latest shroominic/codebox:$TAG -# docker push shroominic/codebox:$TAG +docker push shroominic/codebox:$TAG From 5ecb69e6b7421ea19da050694caea9d4dc7b2ccf Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 25 Jul 2024 17:34:29 +0200 Subject: [PATCH 079/125] =?UTF-8?q?=F0=9F=93=A6=20examples=20wip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/custom_factory.py.todo | 0 examples/docker.py | 9 +++ examples/file_io.py | 57 ++++++++-------- examples/langchain_agent.py.todo | 0 examples/local_docker.py.todo | 0 examples/plot_dataset.py | 86 +++++++++++++------------ examples/simple_codeinterpreter.py.todo | 0 7 files changed, 82 insertions(+), 70 deletions(-) create mode 100644 examples/custom_factory.py.todo create mode 100644 examples/docker.py create mode 100644 examples/langchain_agent.py.todo create mode 100644 examples/local_docker.py.todo create mode 100644 examples/simple_codeinterpreter.py.todo diff --git a/examples/custom_factory.py.todo b/examples/custom_factory.py.todo new file mode 100644 index 0000000..e69de29 diff --git a/examples/docker.py b/examples/docker.py new file mode 100644 index 0000000..0c6cf4b --- /dev/null +++ b/examples/docker.py @@ -0,0 +1,9 @@ +from codeboxapi.docker import DockerBox + +codebox = DockerBox() + +assert codebox.healthcheck() + +r = codebox.exec("import matplotlib.pyplot as plt; plt.plot([1, 2, 3]); plt.show()") + +print(r.text) diff --git a/examples/file_io.py b/examples/file_io.py index edd36e6..0bdc503 100644 --- a/examples/file_io.py +++ b/examples/file_io.py @@ -1,34 +1,35 @@ import httpx from codeboxapi import CodeBox -with CodeBox() as codebox: - # upload dataset csv - csv_bytes = httpx.get( - "https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data" - ).content - codebox.upload("iris.csv", csv_bytes) +codebox = CodeBox() - # install openpyxl for excel conversion - codebox.install("pandas") - codebox.install("openpyxl") +# upload dataset csv +csv_bytes = httpx.get( + "https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data" +).content +codebox.upload("iris.csv", csv_bytes) - # convert dataset csv to excel - output = codebox.run( - "import pandas as pd\n\n" - "df = pd.read_csv('iris.csv', header=None)\n\n" - "df.to_excel('iris.xlsx', index=False)\n" - "'iris.xlsx'" - ) +# install openpyxl for excel conversion +codebox.install("pandas") +codebox.install("openpyxl") - # check output type - if output.type == "image/png": - print("This should not happen") - elif output.type == "error": - print("Error: ", output.content) - else: - # all files inside the codebox - for file in codebox.list_files(): - print("File: ", file.name) - print("Content is None: ", file.content is None) - content = codebox.download(file.name) - print("Content: ", content) +# convert dataset csv to excel +output = codebox.run( + "import pandas as pd\n\n" + "df = pd.read_csv('iris.csv', header=None)\n\n" + "df.to_excel('iris.xlsx', index=False)\n" + "'iris.xlsx'" +) + +# check output type +if output.type == "image/png": + print("This should not happen") +elif output.type == "error": + print("Error: ", output.content) +else: + # all files inside the codebox + for file in codebox.list_files(): + print("File: ", file.name) + print("Content is None: ", file.content is None) + content = codebox.download(file.name) + print("Content: ", content) diff --git a/examples/langchain_agent.py.todo b/examples/langchain_agent.py.todo new file mode 100644 index 0000000..e69de29 diff --git a/examples/local_docker.py.todo b/examples/local_docker.py.todo new file mode 100644 index 0000000..e69de29 diff --git a/examples/plot_dataset.py b/examples/plot_dataset.py index 0bca8cf..9075993 100644 --- a/examples/plot_dataset.py +++ b/examples/plot_dataset.py @@ -4,45 +4,47 @@ import httpx from codeboxapi import CodeBox -with CodeBox() as codebox: - # download the iris dataset - csv_bytes = httpx.get( - "https://archive.ics.uci.edu/" "ml/machine-learning-databases/iris/iris.data" - ).content - - # upload the dataset to the codebox - o = codebox.upload("iris.csv", csv_bytes) - - # dataset analysis code - file_path = Path("examples/assets/dataset_code.txt") - - # run the code - output = codebox.run(code=file_path) - print(output.type) - - if output.type == "image/png" and os.environ.get("CODEBOX_TEST") == "False": - try: - from PIL import Image # type: ignore - except ImportError: - print( - "Please install it with " - '`pip install "codeboxapi[image_support]"`' - " to display images." - ) - exit(1) - - # Convert the image content ( bytes) into an image - import base64 - from io import BytesIO - - img_bytes = base64.b64decode(output.content) - img_buffer = BytesIO(img_bytes) - - # Display the image - img = Image.open(img_buffer) - img.show() - - elif output.type == "error": - # error output - print("Error:") - print(output.content) +codebox = CodeBox(api_key="local") + +# download the iris dataset +csv_bytes = httpx.get( + "https://archive.ics.uci.edu/" "ml/machine-learning-databases/iris/iris.data" +).content + +# upload the dataset to the codebox +o = codebox.upload("iris.csv", csv_bytes) + +# dataset analysis code +file_path = Path("../../examples/assets/dataset_code.txt") + +# run the code +output = codebox.run(code=file_path) +print(output) +print(output.type) + +if output.type == "image" and os.environ.get("CODEBOX_TEST") == "False": + try: + from PIL import Image # type: ignore + except ImportError: + print( + "Please install it with " + '`pip install "codeboxapi[image_support]"`' + " to display images." + ) + exit(1) + + # Convert the image content ( bytes) into an image + import base64 + from io import BytesIO + + img_bytes = base64.b64decode(output.content) + img_buffer = BytesIO(img_bytes) + + # Display the image + img = Image.open(img_buffer) + img.show() + +elif output.type == "error": + # error output + print("Error:") + print(output.content) diff --git a/examples/simple_codeinterpreter.py.todo b/examples/simple_codeinterpreter.py.todo new file mode 100644 index 0000000..e69de29 From 176368f1be86a874018015585e1a5f8c43561081 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 25 Jul 2024 17:34:47 +0200 Subject: [PATCH 080/125] testing fix wip --- tests/test_v02.py | 80 ++++++++++++++--------------------------------- 1 file changed, 24 insertions(+), 56 deletions(-) diff --git a/tests/test_v02.py b/tests/test_v02.py index ed04d33..2446e9e 100644 --- a/tests/test_v02.py +++ b/tests/test_v02.py @@ -1,5 +1,4 @@ -import asyncio -from typing import Literal +import os import pytest from codeboxapi import CodeBox @@ -7,21 +6,18 @@ @pytest.fixture( - scope="session", params=["local"] -) # , "docker", getenv("CODEBOX_API_KEY")]) + scope="session", + params=[ + "local", + "docker", + # os.getenv("CODEBOX_API_KEY"), + ], +) def codebox(request): - return CodeBox() # api_key=request.param) + return CodeBox(api_key=request.param) # api_key=request.param) -@pytest.mark.parametrize("method", ["sync", "async"]) -def test_codebox_lifecycle(codebox: CodeBox, method: Literal["sync", "async"]): - if method == "sync": - _test_sync_lifecycle(codebox) - else: - asyncio.run(_test_async_lifecycle(codebox)) - - -def _test_sync_lifecycle(codebox: CodeBox): +def test_sync_codebox_lifecycle(codebox: CodeBox): assert codebox.healthcheck() == "healthy", "CodeBox should be healthy" result = codebox.exec("print('Hello World!')") @@ -68,7 +64,8 @@ def _test_sync_lifecycle(codebox: CodeBox): ), "Plot execution should produce exactly one image" -async def _test_async_lifecycle(codebox: CodeBox): +@pytest.mark.asyncio +async def test_async_codebox_lifecycle(codebox: CodeBox): assert await codebox.ahealthcheck() == "healthy", "CodeBox should be healthy" result = await codebox.aexec("print('Hello World!')") @@ -117,15 +114,7 @@ async def _test_async_lifecycle(codebox: CodeBox): ), "Plot execution should produce exactly one image" -@pytest.mark.parametrize("method", ["sync", "async"]) -def test_list_operations(codebox: CodeBox, method: Literal["sync", "async"]): - if method == "sync": - _test_sync_list_operations(codebox) - else: - asyncio.run(_test_async_list_operations(codebox)) - - -def _test_sync_list_operations(codebox: CodeBox): +def test_sync_list_operations(codebox: CodeBox): codebox.exec("x = 1; y = 'test'; z = [1, 2, 3]") variables = codebox.show_variables() assert "x" in variables.keys(), "Variable 'x' should be listed" @@ -149,7 +138,8 @@ def _test_sync_list_operations(codebox: CodeBox): ), "Matplotlib should be in the list of packages" -async def _test_async_list_operations(codebox: CodeBox): +@pytest.mark.asyncio +async def test_async_list_operations(codebox: CodeBox): await codebox.aexec("x = 1; y = 'test'; z = [1, 2, 3]") variables = await codebox.ashow_variables() assert "x" in variables.keys(), "Variable 'x' should be listed" @@ -175,15 +165,7 @@ async def _test_async_list_operations(codebox: CodeBox): ), "Matplotlib should be in the list of packages" -@pytest.mark.parametrize("method", ["sync", "async"]) -def test_stream_exec(codebox: CodeBox, method: Literal["sync", "async"]): - if method == "sync": - _test_sync_stream_exec(codebox) - else: - asyncio.run(_test_async_stream_exec(codebox)) - - -def _test_sync_stream_exec(codebox: CodeBox): +def test_sync_stream_exec(codebox: CodeBox): chunks = list( codebox.stream_exec( "import time;\nfor i in range(3): time.sleep(0.01); print(i)" @@ -216,7 +198,6 @@ def _test_sync_stream_exec(codebox: CodeBox): assert all( chunk.type == "text" for chunk in chunks ), "All chunks should be of type 'text' (bash)" - print(chunks) assert [chunk.content.strip() for chunk in chunks] == [ "0", "1", @@ -224,7 +205,8 @@ def _test_sync_stream_exec(codebox: CodeBox): ], "Chunks should contain correct content (bash)" -async def _test_async_stream_exec(codebox: CodeBox): +@pytest.mark.asyncio +async def test_async_stream_exec(codebox: CodeBox): chunks = [ chunk async for chunk in codebox.astream_exec( @@ -245,15 +227,7 @@ async def _test_async_stream_exec(codebox: CodeBox): ], "Chunks should contain correct content" -@pytest.mark.parametrize("method", ["sync", "async"]) -def test_error_handling(codebox: CodeBox, method: Literal["sync", "async"]): - if method == "sync": - _test_sync_error_handling(codebox) - else: - asyncio.run(_test_async_error_handling(codebox)) - - -def _test_sync_error_handling(codebox: CodeBox): +def test_sync_error_handling(codebox: CodeBox): result = codebox.exec("1/0") assert result.errors, "Execution should produce an error" error = result.errors[0].lower() @@ -262,7 +236,8 @@ def _test_sync_error_handling(codebox: CodeBox): ), "Error should be a ZeroDivisionError" -async def _test_async_error_handling(codebox: CodeBox): +@pytest.mark.asyncio +async def test_async_error_handling(codebox: CodeBox): result = await codebox.aexec("1/0") assert result.errors, "Execution should produce an error" error = result.errors[0].lower() @@ -271,15 +246,7 @@ async def _test_async_error_handling(codebox: CodeBox): ), "Error should be a ZeroDivisionError" -@pytest.mark.parametrize("method", ["sync", "async"]) -def test_bash_commands(codebox: CodeBox, method: Literal["sync", "async"]): - if method == "sync": - _test_bash_commands(codebox) - else: - asyncio.run(_test_async_bash_commands(codebox)) - - -def _test_bash_commands(codebox: CodeBox): +def test_sync_bash_commands(codebox: CodeBox): result = codebox.exec("echo ok", kernel="bash") assert "ok" in result.text, "Execution should contain 'ok'" result = codebox.exec('echo print("Hello!") > test.py', kernel="bash") @@ -289,7 +256,8 @@ def _test_bash_commands(codebox: CodeBox): assert result.text.strip() == "Hello!", "Execution result should be 'Hello!'" -async def _test_async_bash_commands(codebox: CodeBox): +@pytest.mark.asyncio +async def test_async_bash_commands(codebox: CodeBox): result = await codebox.aexec("echo ok", kernel="bash") assert "ok" in result.text, "Execution should contain 'ok'" result = await codebox.aexec("echo 'print(\"Hello!\")' > test.py", kernel="bash") From 8c2fbffa5e1fa34ce243ac1e7d6da26b922dfe2d Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 25 Jul 2024 17:35:48 +0200 Subject: [PATCH 081/125] rm syncify due to many async errors --- src/codeboxapi/utils.py | 42 ++--------------------------------------- 1 file changed, 2 insertions(+), 40 deletions(-) diff --git a/src/codeboxapi/utils.py b/src/codeboxapi/utils.py index c906cf5..0025f2f 100644 --- a/src/codeboxapi/utils.py +++ b/src/codeboxapi/utils.py @@ -2,15 +2,12 @@ import signal from contextlib import contextmanager from dataclasses import dataclass -from functools import partial, reduce, wraps +from functools import reduce, wraps from importlib.metadata import PackageNotFoundError, distribution from typing import ( TYPE_CHECKING, - Any, AsyncGenerator, Callable, - Coroutine, - Generator, Iterator, Literal, NoReturn, @@ -19,9 +16,6 @@ ) from warnings import warn -import anyio -from anyio._core._eventloop import threadlocals - if TYPE_CHECKING: from .codebox import CodeBox @@ -33,7 +27,7 @@ class ExecChunk: @classmethod def decode(cls, text: str) -> "ExecChunk": - type, content = text.split(";\n") + type, content = text.split(";\n", 1) assert type in ["text", "image", "stream", "error"] return cls(type=type, content=content) # type: ignore[arg-type] @@ -153,20 +147,6 @@ def resolve_pathlike(file: str | os.PathLike) -> str: IT = TypeVar("IT") -def _syncify_generator( - async_gen: AsyncGenerator[IT, None], -) -> Generator[IT, None, None]: - # todo is this even possible? - while True: - try: - if not getattr(threadlocals, "current_async_backend", None): - yield anyio.run(async_gen.__anext__) - else: - yield anyio.from_thread.run(async_gen.__anext__) - except StopAsyncIteration: - break - - def reduce_bytes(async_gen: Iterator[bytes]) -> bytes: return reduce(lambda x, y: x + y, async_gen) @@ -199,24 +179,6 @@ async def async_flatten_exec_result( return ExecResult(chunks=[c async for c in async_gen]) -def syncify(async_function: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P, T]: - """ - Take an async function and create a regular one that receives the same keyword and - positional arguments, and that when called, calls the original async function in - the main async loop from the worker thread using `anyio.to_thread.run()`. - """ - - @wraps(async_function) - def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: - partial_f = partial(async_function, *args, **kwargs) - - if not getattr(threadlocals, "current_async_backend", None): - return anyio.run(partial_f) - return anyio.from_thread.run(partial_f) - - return wrapper - - def check_installed(package: str) -> None: """ Check if the given package is installed. From a845e2512b9346f77a9367c47fa60451c665b6d2 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 25 Jul 2024 17:36:33 +0200 Subject: [PATCH 082/125] =?UTF-8?q?=F0=9F=90=8B=F0=9F=93=A6=20fix=20docker?= =?UTF-8?q?box?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/codeboxapi/docker.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/codeboxapi/docker.py b/src/codeboxapi/docker.py index 67dd073..a7bd01d 100644 --- a/src/codeboxapi/docker.py +++ b/src/codeboxapi/docker.py @@ -30,16 +30,34 @@ def __init__( self, port_or_range: int | tuple[int, int] = 8069, image: str = "shroominic/codebox:latest", - **_: bool, + timeout: float = 1, # minutes + **_, ) -> None: self.port = get_free_port(port_or_range) subprocess.run( - ["docker", "run", "-d", "--rm", "-p", f"{self.port}:8069", image], + [ + "docker", + "run", + "-d", + "--rm", + "-e", + f"CODEBOX_TIMEOUT={timeout}", + "-p", + f"{self.port}:8069", + image, + ], check=True, ) + self.session_id = str(self.port) self.base_url = f"http://localhost:{self.port}" self.client = httpx.Client(base_url=self.base_url) self.aclient = httpx.AsyncClient(base_url=self.base_url) + self._wait_for_startup() - # def healthcheck(self) -> str: - # return self.client.get("/").text + def _wait_for_startup(self) -> None: + while True: + try: + self.client.get("/") + break + except httpx.HTTPError: + pass From 1cf0ef8a95c462779ad0d0f8da2bd337277f5c7e Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jul 2024 15:52:47 +0200 Subject: [PATCH 083/125] =?UTF-8?q?=F0=9F=94=A7=20tiny=20fixes=20and=20imp?= =?UTF-8?q?rovements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/codeboxapi/codebox.py | 71 +++++++++++----------- src/codeboxapi/local.py | 122 ++++++++++++++++++++++++-------------- src/codeboxapi/remote.py | 88 +++++++++++++++++++-------- src/codeboxapi/utils.py | 58 ++++++++++++++---- 4 files changed, 228 insertions(+), 111 deletions(-) diff --git a/src/codeboxapi/codebox.py b/src/codeboxapi/codebox.py index eb24b24..cb02b95 100644 --- a/src/codeboxapi/codebox.py +++ b/src/codeboxapi/codebox.py @@ -35,9 +35,9 @@ """ +import os +import typing as t from importlib import import_module -from os import PathLike -from typing import Any, AsyncGenerator, BinaryIO, Generator, Literal import anyio @@ -57,9 +57,9 @@ class CodeBox: def __new__( cls, session_id: str | None = None, - api_key: str | Literal["local", "docker"] = "local", - factory_id: str | Literal["default"] = "default", - **kwargs: Any, + api_key: str | t.Literal["local", "docker"] = "local", + factory_id: str | t.Literal["default"] = "default", + **kwargs: t.Any, ) -> "CodeBox": """ Creates a CodeBox session @@ -75,8 +75,8 @@ def __new__( def __init__( self, session_id: str | None = None, - api_key: str | Literal["local", "docker"] = "local", - factory_id: str | Literal["default"] = "default", + api_key: str | t.Literal["local", "docker"] = "local", + factory_id: str | t.Literal["default"] = "default", **_: bool, ) -> None: self.session_id = session_id or "local" @@ -87,8 +87,8 @@ def __init__( def exec( self, - code: str | PathLike, - kernel: Literal["ipython", "bash"] = "ipython", + code: str | os.PathLike, + kernel: t.Literal["ipython", "bash"] = "ipython", timeout: float | None = None, cwd: str | None = None, ) -> ExecResult: @@ -97,18 +97,18 @@ def exec( def stream_exec( self, - code: str | PathLike, - kernel: Literal["ipython", "bash"] = "ipython", + code: str | os.PathLike, + kernel: t.Literal["ipython", "bash"] = "ipython", timeout: float | None = None, cwd: str | None = None, - ) -> Generator[ExecChunk, None, None]: + ) -> t.Generator[ExecChunk, None, None]: """Executes the code and streams the result.""" raise NotImplementedError("Abstract method, please use a subclass.") def upload( self, remote_file_path: str, - content: BinaryIO | bytes | str, + content: t.BinaryIO | bytes | str, timeout: float | None = None, ) -> CodeBoxFile: """Upload a file to the CodeBox instance""" @@ -118,7 +118,7 @@ def stream_download( self, remote_file_path: str, timeout: float | None = None, - ) -> Generator[bytes, None, None]: + ) -> t.Generator[bytes, None, None]: """Download a file as open BinaryIO. Make sure to close the file after use.""" raise NotImplementedError("Abstract method, please use a subclass.") @@ -126,8 +126,8 @@ def stream_download( async def aexec( self, - code: str | PathLike, - kernel: Literal["ipython", "bash"] = "ipython", + code: str | os.PathLike, + kernel: t.Literal["ipython", "bash"] = "ipython", timeout: float | None = None, cwd: str | None = None, ) -> ExecResult: @@ -138,18 +138,18 @@ async def aexec( def astream_exec( self, - code: str | PathLike, - kernel: Literal["ipython", "bash"] = "ipython", + code: str | os.PathLike, + kernel: t.Literal["ipython", "bash"] = "ipython", timeout: float | None = None, cwd: str | None = None, - ) -> AsyncGenerator[ExecChunk, None]: + ) -> t.AsyncGenerator[ExecChunk, None]: """Async Stream Chunks of Execute python code inside the CodeBox instance""" raise NotImplementedError("Abstract method, please use a subclass.") async def aupload( self, remote_file_path: str, - content: BinaryIO | bytes | str, + content: t.BinaryIO | bytes | str, timeout: float | None = None, ) -> CodeBoxFile: """Async Upload a file to the CodeBox instance""" @@ -168,13 +168,13 @@ def astream_download( self, remote_file_path: str, timeout: float | None = None, - ) -> AsyncGenerator[bytes, None]: + ) -> t.AsyncGenerator[bytes, None]: """Async Download a file as BinaryIO. Make sure to close the file after use.""" raise NotImplementedError("Abstract method, please use a subclass.") # HELPER METHODS - async def ahealthcheck(self) -> Literal["healthy", "error"]: + async def ahealthcheck(self) -> t.Literal["healthy", "error"]: return ( "healthy" if "ok" in (await self.aexec("echo ok", kernel="bash")).text @@ -209,9 +209,12 @@ async def alist_files(self) -> list[CodeBoxFile]: def _parse_size(self, size_str: str) -> int: """Convert human-readable size to bytes.""" units = {"K": 1024, "M": 1024**2, "G": 1024**3, "T": 1024**4} - number = float(size_str[:-1]) - unit = size_str[-1].upper() - return int(number * units.get(unit, 1)) + try: + number = float(size_str[:-1]) + unit = size_str[-1].upper() + return int(number * units.get(unit, 1)) + except ValueError: + return -1 async def alist_packages(self) -> list[str]: return (await self.aexec("uv pip list", kernel="bash")).text.splitlines() @@ -273,7 +276,7 @@ def keep_alive(self, minutes: int = 15) -> None: "When calling any method you will get assigned a new session.\n" "The `.start` method is deprecated. Use `.healthcheck` instead." ) - async def astart(self) -> Literal["started", "error"]: + async def astart(self) -> t.Literal["started", "error"]: return "started" if (await self.ahealthcheck()) == "healthy" else "error" @deprecated( @@ -281,28 +284,30 @@ async def astart(self) -> Literal["started", "error"]: "The session will be closed automatically after the last interaction.\n" "(default timeout: 15 minutes)" ) - async def astop(self) -> Literal["stopped"]: + async def astop(self) -> t.Literal["stopped"]: return "stopped" @deprecated( "The `.run` method is deprecated. Use `.exec` instead.", ) - async def arun(self, code: str | PathLike) -> CodeBoxOutput: + async def arun(self, code: str | os.PathLike) -> CodeBoxOutput: exec_result = await self.aexec(code, kernel="ipython") if exec_result.images: return CodeBoxOutput(type="image/png", content=exec_result.images[0]) + if exec_result.errors: + return CodeBoxOutput(type="stderr", content=exec_result.errors[0]) return CodeBoxOutput(type="stdout", content=exec_result.text) @deprecated( "The `.status` method is deprecated. Use `.healthcheck` instead.", ) - async def astatus(self) -> Literal["started", "running", "stopped"]: + async def astatus(self) -> t.Literal["started", "running", "stopped"]: return "running" if await self.ahealthcheck() == "healthy" else "stopped" @deprecated( "The `.start` method is deprecated. Use `.healthcheck` instead.", ) - def start(self) -> Literal["started", "error"]: + def start(self) -> t.Literal["started", "error"]: return syncify(self.astart)() @deprecated( @@ -310,17 +315,17 @@ def start(self) -> Literal["started", "error"]: "The session will be closed automatically after the last interaction.\n" "(default timeout: 15 minutes)" ) - def stop(self) -> Literal["stopped"]: + def stop(self) -> t.Literal["stopped"]: return syncify(self.astop)() @deprecated( "The `.run` method is deprecated. Use `.exec` instead.", ) - def run(self, code: str | PathLike) -> CodeBoxOutput: + def run(self, code: str | os.PathLike) -> CodeBoxOutput: return syncify(self.arun)(code) @deprecated( "The `.status` method is deprecated. Use `.healthcheck` instead.", ) - def status(self) -> Literal["started", "running", "stopped"]: + def status(self) -> t.Literal["started", "running", "stopped"]: return syncify(self.astatus)() diff --git a/src/codeboxapi/local.py b/src/codeboxapi/local.py index 9dc14f5..0b54132 100644 --- a/src/codeboxapi/local.py +++ b/src/codeboxapi/local.py @@ -12,6 +12,7 @@ import re import subprocess import sys +import tempfile as tmpf import threading import time import typing as t @@ -23,13 +24,6 @@ from .utils import check_installed, raise_timeout, resolve_pathlike, run_inside -def _print(*text, stdout): - _stdout = sys.stdout - sys.stdout = stdout - print(*text, flush=True) - sys.stdout = _stdout - - class LocalBox(CodeBox): """ LocalBox is a CodeBox implementation that runs code locally using IPython. @@ -89,21 +83,19 @@ def stream_exec( io.StringIO(), io.StringIO(), ) - try: - queue = Queue[ExecChunk | None]() - _result: list[ExecutionResult] = [] + queue = Queue[ExecChunk | None]() + _result: list[ExecutionResult] = [] - def _run_cell(c: str, result: list[ExecutionResult]) -> None: - time.sleep(0.0001) - result.append(self.shell.run_cell(c)) + def _run_cell(c: str, result: list[ExecutionResult]) -> None: + time.sleep(0.001) + result.append(self.shell.run_cell(c)) - run_cell = threading.Thread( - target=_run_cell, args=(code, _result) - ) + run_cell = threading.Thread(target=_run_cell, args=(code, _result)) + try: def stream_chunks(_out: io.StringIO, _err: io.StringIO) -> None: while run_cell.is_alive(): - time.sleep(0.0001) + time.sleep(0.001) if output := _out.getvalue(): # todo make this more efficient? sys.stdout = _out = io.StringIO() @@ -117,7 +109,7 @@ def stream_chunks(_out: io.StringIO, _err: io.StringIO) -> None: ) for img_str in image_matches: queue.put( - ExecChunk(type="image", content=img_str) + ExecChunk(type="img", content=img_str) ) output = re.sub(image_pattern, "", output) @@ -125,14 +117,12 @@ def stream_chunks(_out: io.StringIO, _err: io.StringIO) -> None: if output.startswith("Out["): # todo better disable logging somehow output = re.sub(r"Out[(.*?)]: ", "", output) - queue.put( - ExecChunk(type="text", content=output) - ) + queue.put(ExecChunk(type="txt", content=output)) if error := _err.getvalue(): # todo make this more efficient? sys.stderr = _err = io.StringIO() - queue.put(ExecChunk(type="error", content=error)) + queue.put(ExecChunk(type="err", content=error)) queue.put(None) @@ -154,22 +144,23 @@ def stream_chunks(_out: io.StringIO, _err: io.StringIO) -> None: result = _result[0] if result.error_before_exec: yield ExecChunk( - type="error", + type="err", content=str(result.error_before_exec).replace( "\\n", "\n" ), ) elif result.error_in_exec: yield ExecChunk( - type="error", + type="err", content=str(result.error_in_exec).replace("\\n", "\n"), ) elif result.result is not None: - yield ExecChunk(type="text", content=str(result.result)) + yield ExecChunk(type="txt", content=str(result.result)) finally: sys.stdout = old_stdout sys.stderr = old_stderr + run_cell._stop() # type: ignore elif kernel == "bash": # todo maybe implement using queue @@ -182,10 +173,10 @@ def stream_chunks(_out: io.StringIO, _err: io.StringIO) -> None: ) if process.stdout: for c in process.stdout: - yield ExecChunk(content=c.decode(), type="text") + yield ExecChunk(content=c.decode(), type="txt") if process.stderr: for c in process.stderr: - yield ExecChunk(content=c.decode(), type="error") + yield ExecChunk(content=c.decode(), type="err") else: raise ValueError(f"Unsupported kernel: {kernel}") @@ -208,11 +199,10 @@ async def astream_exec( io.StringIO(), ) + run_cell = asyncio.create_task( + asyncio.to_thread(self.shell.run_cell, code) + ) try: - run_cell = asyncio.create_task( - asyncio.to_thread(self.shell.run_cell, code) - ) - while not run_cell.done(): await asyncio.sleep(0.001) if output := temp_output.getvalue(): @@ -225,7 +215,7 @@ async def astream_exec( ) image_matches = re.findall(image_pattern, output) for img_str in image_matches: - yield ExecChunk(type="image", content=img_str) + yield ExecChunk(type="img", content=img_str) output = re.sub(image_pattern, "", output) if output: @@ -234,26 +224,27 @@ async def astream_exec( output = re.sub( r"Out\[(.*?)\]: ", "", output.strip() ) - yield ExecChunk(type="text", content=output) + yield ExecChunk(type="txt", content=output) if error := temp_error.getvalue(): sys.stderr = temp_error = io.StringIO() - yield ExecChunk(type="error", content=error) + yield ExecChunk(type="err", content=error) result = await run_cell if result.error_before_exec: yield ExecChunk( - type="error", content=str(result.error_before_exec) + type="err", content=str(result.error_before_exec) ) elif result.error_in_exec: yield ExecChunk( - type="error", content=str(result.error_in_exec) + type="err", content=str(result.error_in_exec) ) elif result.result is not None: - yield ExecChunk(type="text", content=str(result.result)) + yield ExecChunk(type="txt", content=str(result.result)) finally: sys.stdout = old_stdout sys.stderr = old_stderr + run_cell.cancel() elif kernel == "bash": process = await asyncio.create_subprocess_shell( @@ -266,18 +257,44 @@ async def astream_exec( # todo yield at the same time and not after each other if process.stdout: async for chunk in process.stdout: - yield ExecChunk(content=chunk.decode(), type="text") + yield ExecChunk(content=chunk.decode(), type="txt") if process.stderr: async for err in process.stderr: - yield ExecChunk(content=err.decode(), type="error") + yield ExecChunk(content=err.decode(), type="err") else: raise ValueError(f"Unsupported kernel: {kernel}") + def upload( + self, + remote_file_path: str, + content: t.BinaryIO | bytes | str, + timeout: float | None = None, + ) -> CodeBoxFile: + with raise_timeout(timeout): + file_path = os.path.join(self.cwd, remote_file_path) + with open(file_path, "wb") as file: + if isinstance(content, str): + file.write(content.encode()) + elif isinstance(content, bytes): + file.write(content) + elif isinstance(content, tmpf.SpooledTemporaryFile): + file.write(content.read()) + elif isinstance(content, (t.BinaryIO, io.BytesIO)): + file.write(content.read()) + else: + raise TypeError("Unsupported content type") + file_size = os.path.getsize(file_path) + return CodeBoxFile( + remote_path=file_path, + size=file_size, + codebox_id=self.session_id, + ) + async def aupload( self, file_name: str, - content: t.BinaryIO | bytes | str, + content: t.BinaryIO | bytes | str | tmpf.SpooledTemporaryFile, timeout: float | None = None, ) -> CodeBoxFile: import aiofiles.os @@ -287,11 +304,28 @@ async def aupload( async with aiofiles.open(file_path, "wb") as file: if isinstance(content, str): await file.write(content.encode()) - elif isinstance(content, t.BinaryIO): - while chunk := content.read(8192): - await file.write(chunk) - else: + elif isinstance(content, tmpf.SpooledTemporaryFile): + await file.write(content.read()) + elif isinstance(content, (t.BinaryIO, io.BytesIO)): + try: + while chunk := content.read(8192): + await file.write(chunk) + except ValueError as e: + if "I/O operation on closed file" in str(e): + # If the file is closed, we can't reopen it + # Instead, we'll raise a more informative error + raise ValueError( + "The provided file object is closed and cannot be read" + ) from e + else: + raise + elif isinstance(content, bytes): await file.write(content) + else: + from builtins import print as do_not_remote_print_lol + + do_not_remote_print_lol(type(content), content.__dict__) + raise TypeError("Unsupported content type") file_size = await aiofiles.os.path.getsize(file_path) return CodeBoxFile( diff --git a/src/codeboxapi/remote.py b/src/codeboxapi/remote.py index a9a7754..8388bfb 100644 --- a/src/codeboxapi/remote.py +++ b/src/codeboxapi/remote.py @@ -2,6 +2,7 @@ from typing import AsyncGenerator, BinaryIO, Generator, Literal from uuid import uuid4 +import anyio import httpx from .codebox import CodeBox, CodeBoxFile, ExecChunk @@ -22,8 +23,7 @@ def __init__( session_id: str | None = None, api_key: str | Literal["local", "docker"] = "local", factory_id: str | Literal["default"] = "default", - base_url: str = "https://codeboxapi.com/api/v2", - _new: bool = False, + base_url: str | None = None, ) -> None: self.session_id = session_id or uuid4().hex self.factory_id = factory_id @@ -32,10 +32,16 @@ def __init__( or getenv("CODEBOX_API_KEY") or raise_error("CODEBOX_API_KEY is required") ) - self.base_url = f"{base_url}/codebox/{self.session_id}" - self.headers = {"Factory-Id": self.factory_id} if self.factory_id else None - self.client = httpx.Client(base_url=self.base_url, headers=self.headers) - self.aclient = httpx.AsyncClient(base_url=self.base_url, headers=self.headers) + self.base_url = base_url or getenv( + "CODEBOX_BASE_URL", "https://codeboxapi.com/api/v2" + ) + self.url = f"{self.base_url}/codebox/{self.session_id}" + self.headers = { + "Factory-Id": self.factory_id, + "Authorization": f"Bearer {self.api_key}", + } + self.client = httpx.Client(base_url=self.url, headers=self.headers) + self.aclient = httpx.AsyncClient(base_url=self.url, headers=self.headers) def stream_exec( self, @@ -47,12 +53,20 @@ def stream_exec( code = resolve_pathlike(code) with self.client.stream( method="POST", - url="/stream", + url="/exec", timeout=timeout, json={"code": code, "kernel": kernel, "cwd": cwd}, ) as response: + response.raise_for_status() + img_buffer = "" for chunk in response.iter_text(): - yield ExecChunk.decode(chunk) + if chunk.startswith("img;") and not chunk.endswith("=="): + img_buffer += chunk + elif img_buffer and chunk.endswith("=="): + yield ExecChunk.decode(img_buffer + chunk) + img_buffer = "" + else: + yield ExecChunk.decode(chunk) async def astream_exec( self, @@ -62,14 +76,44 @@ async def astream_exec( cwd: str | None = None, ) -> AsyncGenerator[ExecChunk, None]: code = resolve_pathlike(code) - async with self.aclient.stream( - method="POST", - url="/stream", + try: + async with self.aclient.stream( + method="POST", + url="/exec", + timeout=timeout, + json={"code": code, "kernel": kernel, "cwd": cwd}, + ) as response: + response.raise_for_status() + img_buffer = "" + async for chunk in response.aiter_text(): + if chunk.startswith("img;") and not chunk.endswith("=="): + img_buffer += chunk + elif img_buffer and chunk.endswith("=="): + yield ExecChunk.decode(img_buffer + chunk) + img_buffer = "" + else: + yield ExecChunk.decode(chunk) + except RuntimeError as e: + if "loop is closed" not in str(e): + raise e + await anyio.sleep(0.1) + async for c in self.astream_exec(code, kernel, timeout, cwd): + yield c + + def upload( + self, + remote_file_path: str, + content: BinaryIO | bytes | str, + timeout: float | None = None, + ) -> CodeBoxFile: + if isinstance(content, str): + content = content.encode("utf-8") + response = self.client.post( + url="/upload", + files={"file": (remote_file_path, content)}, timeout=timeout, - json={"code": code, "kernel": kernel, "cwd": cwd}, - ) as response: - async for chunk in response.aiter_text(): - yield ExecChunk.decode(chunk) + ) + return CodeBoxFile(**response.json()) async def aupload( self, @@ -79,16 +123,12 @@ async def aupload( ) -> CodeBoxFile: if isinstance(content, str): content = content.encode("utf-8") - return CodeBoxFile( - **( - await self.aclient.post( - url="/upload", - files={"file": (file_name, content)}, - timeout=timeout, - ) - ).json(), - codebox_id=self.session_id, + response = await self.aclient.post( + url="/upload", + files={"file": (file_name, content)}, + timeout=timeout, ) + return CodeBoxFile(**response.json()) def stream_download( self, diff --git a/src/codeboxapi/utils.py b/src/codeboxapi/utils.py index 0025f2f..a9773f7 100644 --- a/src/codeboxapi/utils.py +++ b/src/codeboxapi/utils.py @@ -2,12 +2,14 @@ import signal from contextlib import contextmanager from dataclasses import dataclass -from functools import reduce, wraps +from functools import partial, reduce, wraps from importlib.metadata import PackageNotFoundError, distribution from typing import ( TYPE_CHECKING, + Any, AsyncGenerator, Callable, + Coroutine, Iterator, Literal, NoReturn, @@ -16,19 +18,40 @@ ) from warnings import warn +import anyio +from anyio._core._eventloop import threadlocals + if TYPE_CHECKING: from .codebox import CodeBox +# import sys +# def _print(*text, stdout): +# _stdout = sys.stdout +# sys.stdout = stdout +# print(*text, flush=True) +# sys.stdout = _stdout + + @dataclass class ExecChunk: - type: Literal["text", "image", "stream", "error"] + """ + A chunk of output from an execution. + The type is one of: + - txt: text output + - img: image output + - stm: stream output + - err: error output + """ + + type: Literal["txt", "img", "stm", "err"] content: str @classmethod def decode(cls, text: str) -> "ExecChunk": - type, content = text.split(";\n", 1) - assert type in ["text", "image", "stream", "error"] + type, content = text[:3], text[5:] + print(f"Decoding chunk: {type=}, {content=}") + assert type in ["txt", "img", "stm", "err"] return cls(type=type, content=content) # type: ignore[arg-type] def __str__(self) -> str: @@ -44,16 +67,16 @@ def text(self) -> str: return "".join( chunk.content for chunk in self.chunks - if chunk.type == "text" or chunk.type == "stream" + if chunk.type == "txt" or chunk.type == "stm" ) @property def images(self) -> list[str]: - return [chunk.content for chunk in self.chunks if chunk.type == "image"] + return [chunk.content for chunk in self.chunks if chunk.type == "img"] @property def errors(self) -> list[str]: - return [chunk.content for chunk in self.chunks if chunk.type == "error"] + return [chunk.content for chunk in self.chunks if chunk.type == "err"] # todo move somewhere more clean @@ -144,9 +167,6 @@ def resolve_pathlike(file: str | os.PathLike) -> str: return file -IT = TypeVar("IT") - - def reduce_bytes(async_gen: Iterator[bytes]) -> bytes: return reduce(lambda x, y: x + y, async_gen) @@ -179,6 +199,24 @@ async def async_flatten_exec_result( return ExecResult(chunks=[c async for c in async_gen]) +def syncify(async_function: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P, T]: + """ + Take an async function and create a regular one that receives the same keyword and + positional arguments, and that when called, calls the original async function in + the main async loop from the worker thread using `anyio.to_thread.run()`. + """ + + @wraps(async_function) + def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: + partial_f = partial(async_function, *args, **kwargs) + + if not getattr(threadlocals, "current_async_backend", None): + return anyio.run(partial_f) + return anyio.from_thread.run(partial_f) + + return wrapper + + def check_installed(package: str) -> None: """ Check if the given package is installed. From 718e41f19ecd4895a777e35cb999f3b4f5e69a0f Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jul 2024 15:53:43 +0200 Subject: [PATCH 084/125] =?UTF-8?q?=F0=9F=A7=AA=20fix=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_v02.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/test_v02.py b/tests/test_v02.py index 2446e9e..8b6ae33 100644 --- a/tests/test_v02.py +++ b/tests/test_v02.py @@ -9,8 +9,8 @@ scope="session", params=[ "local", - "docker", - # os.getenv("CODEBOX_API_KEY"), + # "docker", + os.getenv("CODEBOX_API_KEY"), ], ) def codebox(request): @@ -178,8 +178,8 @@ def test_sync_stream_exec(codebox: CodeBox): isinstance(chunk, ExecChunk) for chunk in chunks ), "All items should be ExecChunk instances (ipython)" assert all( - chunk.type == "text" for chunk in chunks - ), "All chunks should be of type 'text' (ipython)" + chunk.type == "txt" for chunk in chunks + ), "All chunks should be of type 'txt' (ipython)" assert [chunk.content.strip() for chunk in chunks] == [ "0", "1", @@ -196,8 +196,8 @@ def test_sync_stream_exec(codebox: CodeBox): isinstance(chunk, ExecChunk) for chunk in chunks ), "All items should be ExecChunk instances (bash)" assert all( - chunk.type == "text" for chunk in chunks - ), "All chunks should be of type 'text' (bash)" + chunk.type == "txt" for chunk in chunks + ), "All chunks should be of type 'txt' (bash)" assert [chunk.content.strip() for chunk in chunks] == [ "0", "1", @@ -218,8 +218,8 @@ async def test_async_stream_exec(codebox: CodeBox): isinstance(chunk, ExecChunk) for chunk in chunks ), "All items should be ExecChunk instances" assert all( - chunk.type == "text" for chunk in chunks - ), "All chunks should be of type 'text'" + chunk.type == "txt" for chunk in chunks + ), "All chunks should be of type 'txt'" assert [chunk.content.strip() for chunk in chunks] == [ "0", "1", @@ -249,7 +249,7 @@ async def test_async_error_handling(codebox: CodeBox): def test_sync_bash_commands(codebox: CodeBox): result = codebox.exec("echo ok", kernel="bash") assert "ok" in result.text, "Execution should contain 'ok'" - result = codebox.exec('echo print("Hello!") > test.py', kernel="bash") + result = codebox.exec("echo \"print('Hello!')\" > test.py", kernel="bash") assert result.text.strip() == "", "Execution result should be empty" assert "test.py" in [file.remote_path for file in codebox.list_files()] result = codebox.exec("python test.py", kernel="bash") From bf2cc519fe26e67f226a3923c17d378627846692 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Aug 2024 19:07:44 +0200 Subject: [PATCH 085/125] one bug further lol --- Dockerfile | 2 +- src/codeboxapi/api.py | 33 +++++++++++++++++++++------------ src/codeboxapi/docker.py | 2 +- src/codeboxapi/remote.py | 23 +++-------------------- 4 files changed, 26 insertions(+), 34 deletions(-) diff --git a/Dockerfile b/Dockerfile index c6a9962..1a8fb86 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM ghcr.io/astral-sh/uv as uv -FROM --platform=arm64 python:3.11 as build +FROM --platform=amd64 python:3.11 as build ENV VIRTUAL_ENV=/.venv PATH="/.venv/bin:$PATH" diff --git a/src/codeboxapi/api.py b/src/codeboxapi/api.py index df3d8a3..3c6e7d3 100644 --- a/src/codeboxapi/api.py +++ b/src/codeboxapi/api.py @@ -2,9 +2,10 @@ from contextlib import asynccontextmanager from datetime import datetime, timedelta from os import getenv +from tempfile import SpooledTemporaryFile from typing import AsyncGenerator, Literal -from fastapi import Depends, FastAPI, HTTPException, UploadFile +from fastapi import Body, Depends, FastAPI, HTTPException, UploadFile from fastapi.responses import StreamingResponse from pydantic import BaseModel @@ -18,8 +19,9 @@ @asynccontextmanager async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]: async def timeout(): - timeout_secs = float(getenv("CODEBOX_TIMEOUT", "900")) - while last_interaction + timedelta(seconds=timeout_secs) > datetime.utcnow(): + if (_timeout := getenv("CODEBOX_TIMEOUT", "90")).lower() == "none": + return + while last_interaction + timedelta(seconds=float(_timeout)) > datetime.utcnow(): await asyncio.sleep(1) exit(0) @@ -28,18 +30,14 @@ async def timeout(): t.cancel() -app = FastAPI(title="Codebox API", lifespan=lifespan) - - async def get_codebox() -> AsyncGenerator[LocalBox, None]: global codebox, last_interaction last_interaction = datetime.utcnow() yield codebox -@app.get("/") -async def healthcheck() -> dict[str, str]: - return {"status": "ok"} +app = FastAPI(title="Codebox API", lifespan=lifespan) +app.get("/")(lambda: {"status": "ok"}) class ExecBody(BaseModel): @@ -62,7 +60,7 @@ async def event_stream() -> AsyncGenerator[str, None]: return StreamingResponse(event_stream()) -@app.get("/download/{file_name}") +@app.get("/files/download/{file_name}") async def download( file_name: str, timeout: int | None = None, @@ -71,7 +69,7 @@ async def download( return StreamingResponse(codebox.astream_download(file_name, timeout)) -@app.post("/upload") +@app.post("/files/upload") async def upload( file: UploadFile, timeout: int | None = None, @@ -79,13 +77,24 @@ async def upload( ) -> "CodeBoxFile": if not file.filename: raise HTTPException(status_code=400, detail="A file name is required") + if isinstance(file.file, SpooledTemporaryFile): + file.file = file.file return await codebox.aupload(file.filename, file.file, timeout) +@app.post("/code/execute") +async def deprecated_exec( + body: dict = Body(), codebox: LocalBox = Depends(get_codebox) +) -> dict: + """deprecated: use /exec instead""" + ex = await codebox.aexec(body["properties"]["code"]) + return {"properties": {"stdout": ex.text, "stderr": ex.errors, "result": ex.text}} + + def serve(): import uvicorn - uvicorn.run(app, host="0.0.0.0", port=getenv("CODEBOX_PORT", 8069)) + uvicorn.run(app, host="0.0.0.0", port=8000) if __name__ == "__main__": diff --git a/src/codeboxapi/docker.py b/src/codeboxapi/docker.py index a7bd01d..ac42c09 100644 --- a/src/codeboxapi/docker.py +++ b/src/codeboxapi/docker.py @@ -43,7 +43,7 @@ def __init__( "-e", f"CODEBOX_TIMEOUT={timeout}", "-p", - f"{self.port}:8069", + f"{self.port}:8000", image, ], check=True, diff --git a/src/codeboxapi/remote.py b/src/codeboxapi/remote.py index 8388bfb..e28edf9 100644 --- a/src/codeboxapi/remote.py +++ b/src/codeboxapi/remote.py @@ -100,21 +100,6 @@ async def astream_exec( async for c in self.astream_exec(code, kernel, timeout, cwd): yield c - def upload( - self, - remote_file_path: str, - content: BinaryIO | bytes | str, - timeout: float | None = None, - ) -> CodeBoxFile: - if isinstance(content, str): - content = content.encode("utf-8") - response = self.client.post( - url="/upload", - files={"file": (remote_file_path, content)}, - timeout=timeout, - ) - return CodeBoxFile(**response.json()) - async def aupload( self, file_name: str, @@ -124,7 +109,7 @@ async def aupload( if isinstance(content, str): content = content.encode("utf-8") response = await self.aclient.post( - url="/upload", + url="/files/upload", files={"file": (file_name, content)}, timeout=timeout, ) @@ -137,9 +122,8 @@ def stream_download( ) -> Generator[bytes, None, None]: with self.client.stream( method="GET", - url="/download", + url=f"/files/download/{remote_file_path}", timeout=timeout, - params={"file_name": remote_file_path}, ) as response: for chunk in response.iter_bytes(): yield chunk @@ -151,9 +135,8 @@ async def astream_download( ) -> AsyncGenerator[bytes, None]: async with self.aclient.stream( method="GET", - url="/download", + url=f"/files/download/{remote_file_path}", timeout=timeout, - params={"file_name": remote_file_path}, ) as response: async for chunk in response.aiter_bytes(): yield chunk From ad8b052efe236940b5c88873ef9f5d7cd20fa6d7 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 23 Oct 2024 04:20:00 +0800 Subject: [PATCH 086/125] fix container --- Dockerfile | 7 ++++++- pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1a8fb86..34394e9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM ghcr.io/astral-sh/uv as uv -FROM --platform=amd64 python:3.11 as build +FROM --platform=amd64 python:3.11-slim as build ENV VIRTUAL_ENV=/.venv PATH="/.venv/bin:$PATH" @@ -12,4 +12,9 @@ RUN --mount=type=cache,target=/root/.cache/uv \ /uv venv /.venv && /uv pip install -e .[all] \ && rm -rf README.md pyproject.toml src +# FROM --platform=amd64 python:3.11-slim as runtime + +ENV PORT=8069 +EXPOSE $PORT + CMD ["/.venv/bin/python", "-m", "codeboxapi.api"] diff --git a/pyproject.toml b/pyproject.toml index b130a36..1850e26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ docs = ["neoteroi-mkdocs", "mkdocs-material"] pytest = ["pytest-asyncio"] local = ["jupyter-client", "ipykernel", "uv", "aiofiles"] vision = ["Pillow"] -serve = ["fastapi"] +serve = ["fastapi[standard]"] data-science = [ "codeboxapi[local]", "codeboxapi[vision]", From db8d9d40322ca16ce76f4fd4e8bf87ff5150b95f Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 24 Oct 2024 10:09:46 +0800 Subject: [PATCH 087/125] fix timeout, port, file download --- src/codeboxapi/api.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/codeboxapi/api.py b/src/codeboxapi/api.py index 3c6e7d3..0d8beb2 100644 --- a/src/codeboxapi/api.py +++ b/src/codeboxapi/api.py @@ -1,12 +1,11 @@ import asyncio from contextlib import asynccontextmanager from datetime import datetime, timedelta -from os import getenv -from tempfile import SpooledTemporaryFile +from os import getenv, path from typing import AsyncGenerator, Literal from fastapi import Body, Depends, FastAPI, HTTPException, UploadFile -from fastapi.responses import StreamingResponse +from fastapi.responses import FileResponse, StreamingResponse from pydantic import BaseModel from .local import LocalBox @@ -19,9 +18,9 @@ @asynccontextmanager async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]: async def timeout(): - if (_timeout := getenv("CODEBOX_TIMEOUT", "90")).lower() == "none": + if (_timeout := getenv("CODEBOX_TIMEOUT", "15")).lower() == "none": return - while last_interaction + timedelta(seconds=float(_timeout)) > datetime.utcnow(): + while last_interaction + timedelta(minutes=float(_timeout)) > datetime.utcnow(): await asyncio.sleep(1) exit(0) @@ -65,8 +64,12 @@ async def download( file_name: str, timeout: int | None = None, codebox: LocalBox = Depends(get_codebox), -) -> StreamingResponse: - return StreamingResponse(codebox.astream_download(file_name, timeout)) +) -> FileResponse: + async with asyncio.timeout(timeout): + file_path = path.join(codebox.cwd, file_name) + return FileResponse( + path=file_path, media_type="application/octet-stream", filename=file_name + ) @app.post("/files/upload") @@ -77,8 +80,6 @@ async def upload( ) -> "CodeBoxFile": if not file.filename: raise HTTPException(status_code=400, detail="A file name is required") - if isinstance(file.file, SpooledTemporaryFile): - file.file = file.file return await codebox.aupload(file.filename, file.file, timeout) @@ -94,7 +95,7 @@ async def deprecated_exec( def serve(): import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8000) + uvicorn.run(app, host="0.0.0.0", port=int(getenv("PORT", 8069))) if __name__ == "__main__": From 01f96664413b117912a9236f8b046d8a48c023c8 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 24 Oct 2024 10:10:06 +0800 Subject: [PATCH 088/125] simplify example --- examples/big_upload.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/examples/big_upload.py b/examples/big_upload.py index bcccebe..8a73fe3 100644 --- a/examples/big_upload.py +++ b/examples/big_upload.py @@ -19,19 +19,20 @@ def download_file_from_url(url: str) -> None: print(codebox.run(f"download_file_from_url('{url}')")) -with CodeBox() as codebox: - url_upload( - codebox, - "https://codeboxapistorage.blob.core.windows.net/bucket/data-test.arrow", - ) - print(codebox.list_files()) +codebox = CodeBox() - url_upload( - codebox, - "https://codeboxapistorage.blob.core.windows.net/bucket/data-train.arrow", - ) - print(codebox.list_files()) +url_upload( + codebox, + "https://codeboxapistorage.blob.core.windows.net/bucket/data-test.arrow", +) +print(codebox.list_files()) + +url_upload( + codebox, + "https://codeboxapistorage.blob.core.windows.net/bucket/data-train.arrow", +) +print(codebox.list_files()) - codebox.run("import os") - print(codebox.run("print(os.listdir())")) - print(codebox.run("print([(f, os.path.getsize(f)) for f in os.listdir('.')])")) +codebox.run("import os") +print(codebox.run("print(os.listdir())")) +print(codebox.run("print([(f, os.path.getsize(f)) for f in os.listdir('.')])")) From 0d8216b90999fb19ae91073fdc8d150d7c54135c Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 24 Oct 2024 10:24:56 +0800 Subject: [PATCH 089/125] fix CodeBoxFile --- src/codeboxapi/utils.py | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/src/codeboxapi/utils.py b/src/codeboxapi/utils.py index a9773f7..48c4735 100644 --- a/src/codeboxapi/utils.py +++ b/src/codeboxapi/utils.py @@ -50,7 +50,6 @@ class ExecChunk: @classmethod def decode(cls, text: str) -> "ExecChunk": type, content = text[:3], text[5:] - print(f"Decoding chunk: {type=}, {content=}") assert type in ["txt", "img", "stm", "err"] return cls(type=type, content=content) # type: ignore[arg-type] @@ -102,14 +101,34 @@ def __eq__(self, other: object) -> bool: class CodeBoxFile: remote_path: str size: int - codebox_id: str + codebox_id: str | None = None + codebox_api_key: str | None = None + codebox_factory_id: str | None = None _content: bytes | None = None @property def codebox(self) -> "CodeBox": from .codebox import CodeBox - return CodeBox(self.codebox_id) + if self.codebox_id is None: + raise ValueError("CodeBox ID is not set") + if self.codebox_api_key is None: + raise ValueError("CodeBox API key is not set") + if self.codebox_factory_id is None: + raise ValueError("CodeBox factory ID is not set") + if self.codebox_api_key == "docker": + from .docker import DockerBox + + return DockerBox( + port_or_range=int(self.codebox_id), + image=self.codebox_factory_id, + start_container=False, + ) + return CodeBox( + session_id=self.codebox_id, + api_key=self.codebox_api_key, + factory_id=self.codebox_factory_id, + ) @property def name(self) -> str: @@ -125,6 +144,16 @@ async def acontent(self) -> bytes: chunk async for chunk in self.codebox.astream_download(self.remote_path) ]) + @classmethod + def from_path(cls, path: str) -> "CodeBoxFile": + with open(path, "rb") as f: + return cls( + remote_path=path, + size=os.path.getsize(path), + codebox_id=None, + _content=f.read(), + ) + def save(self, path: str) -> None: with open(path, "wb") as f: for chunk in self.codebox.stream_download(self.remote_path): From f8b96fbd5c0a78cef9b88ee3034eaf36c61c47af Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 24 Oct 2024 10:25:34 +0800 Subject: [PATCH 090/125] fix codebox docker --- src/codeboxapi/docker.py | 43 ++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/src/codeboxapi/docker.py b/src/codeboxapi/docker.py index ac42c09..1f8e386 100644 --- a/src/codeboxapi/docker.py +++ b/src/codeboxapi/docker.py @@ -1,5 +1,6 @@ import socket import subprocess +import time import httpx @@ -30,28 +31,36 @@ def __init__( self, port_or_range: int | tuple[int, int] = 8069, image: str = "shroominic/codebox:latest", - timeout: float = 1, # minutes + timeout: float = 3, # minutes + start_container: bool = True, **_, ) -> None: - self.port = get_free_port(port_or_range) - subprocess.run( - [ - "docker", - "run", - "-d", - "--rm", - "-e", - f"CODEBOX_TIMEOUT={timeout}", - "-p", - f"{self.port}:8000", - image, - ], - check=True, - ) + if start_container: + self.port = get_free_port(port_or_range) + subprocess.run( + [ + "docker", + "run", + "-d", + "--rm", + "-e", + f"CODEBOX_TIMEOUT={timeout}", + "-p", + f"{self.port}:8069", + image, + ], + check=True, + ) + else: + assert isinstance(port_or_range, int) + self.port = port_or_range self.session_id = str(self.port) self.base_url = f"http://localhost:{self.port}" self.client = httpx.Client(base_url=self.base_url) self.aclient = httpx.AsyncClient(base_url=self.base_url) + self.api_key = "docker" + self.factory_id = image + self.session_id = str(self.port) self._wait_for_startup() def _wait_for_startup(self) -> None: @@ -60,4 +69,4 @@ def _wait_for_startup(self) -> None: self.client.get("/") break except httpx.HTTPError: - pass + time.sleep(1) From 83fffe7535eee7f90f690c38e33615051d7b81ea Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 24 Oct 2024 10:26:07 +0800 Subject: [PATCH 091/125] remote box fixes --- src/codeboxapi/remote.py | 52 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/src/codeboxapi/remote.py b/src/codeboxapi/remote.py index e28edf9..1bd30d9 100644 --- a/src/codeboxapi/remote.py +++ b/src/codeboxapi/remote.py @@ -14,19 +14,20 @@ class RemoteBox(CodeBox): Sandboxed Python Interpreter """ - def __new__(cls) -> "RemoteBox": + def __new__(cls, *args, **kwargs) -> "RemoteBox": # This is a hack to ignore the CodeBox.__new__ factory method. return object.__new__(cls) def __init__( self, session_id: str | None = None, - api_key: str | Literal["local", "docker"] = "local", - factory_id: str | Literal["default"] = "default", + api_key: str | Literal["local", "docker"] | None = None, + factory_id: str | Literal["default"] | None = None, base_url: str | None = None, ) -> None: self.session_id = session_id or uuid4().hex - self.factory_id = factory_id + self.factory_id = factory_id or getenv("CODEBOX_FACTORY_ID", "default") + assert self.factory_id is not None self.api_key = ( api_key or getenv("CODEBOX_API_KEY") @@ -60,11 +61,15 @@ def stream_exec( response.raise_for_status() img_buffer = "" for chunk in response.iter_text(): + # todo check for better solutions + # why did I implemented my own streaming protocol? if chunk.startswith("img;") and not chunk.endswith("=="): img_buffer += chunk elif img_buffer and chunk.endswith("=="): yield ExecChunk.decode(img_buffer + chunk) img_buffer = "" + elif img_buffer: + img_buffer += chunk else: yield ExecChunk.decode(chunk) @@ -91,6 +96,8 @@ async def astream_exec( elif img_buffer and chunk.endswith("=="): yield ExecChunk.decode(img_buffer + chunk) img_buffer = "" + elif img_buffer: + img_buffer += chunk else: yield ExecChunk.decode(chunk) except RuntimeError as e: @@ -100,6 +107,31 @@ async def astream_exec( async for c in self.astream_exec(code, kernel, timeout, cwd): yield c + def upload( + self, + file_name: str, + content: BinaryIO | bytes | str, + timeout: float | None = None, + ) -> CodeBoxFile: + if isinstance(content, str): + content = content.encode("utf-8") + response = self.client.post( + url="/files/upload", + files={"file": (file_name, content)}, + timeout=timeout, + ) + json = response.json() + # todo fix: this is bad code + del json["codebox_id"] + del json["codebox_api_key"] + del json["codebox_factory_id"] + return CodeBoxFile( + **json, + codebox_api_key=self.api_key, + codebox_factory_id=self.factory_id, + codebox_id=self.session_id, + ) + async def aupload( self, file_name: str, @@ -113,7 +145,17 @@ async def aupload( files={"file": (file_name, content)}, timeout=timeout, ) - return CodeBoxFile(**response.json()) + json = response.json() + # todo this is a sign of bad design + del json["codebox_id"] + del json["codebox_api_key"] + del json["codebox_factory_id"] + return CodeBoxFile( + **json, + codebox_api_key=self.api_key, + codebox_factory_id=self.factory_id, + codebox_id=self.session_id, + ) def stream_download( self, From 15d19fe7e3ccd663c79607d2a24d9c2505df2352 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 24 Oct 2024 10:26:46 +0800 Subject: [PATCH 092/125] fix default values --- src/codeboxapi/codebox.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/codeboxapi/codebox.py b/src/codeboxapi/codebox.py index cb02b95..24fb0f5 100644 --- a/src/codeboxapi/codebox.py +++ b/src/codeboxapi/codebox.py @@ -57,13 +57,14 @@ class CodeBox: def __new__( cls, session_id: str | None = None, - api_key: str | t.Literal["local", "docker"] = "local", - factory_id: str | t.Literal["default"] = "default", - **kwargs: t.Any, + api_key: str | t.Literal["local", "docker"] | None = None, + factory_id: str | t.Literal["default"] | None = None, ) -> "CodeBox": """ Creates a CodeBox session """ + api_key = api_key or os.getenv("CODEBOX_API_KEY", "local") + factory_id = factory_id or os.getenv("CODEBOX_FACTORY_ID", "default") if api_key == "local": return super().__new__(import_module("codeboxapi.local").LocalBox) @@ -75,13 +76,13 @@ def __new__( def __init__( self, session_id: str | None = None, - api_key: str | t.Literal["local", "docker"] = "local", - factory_id: str | t.Literal["default"] = "default", + api_key: str | t.Literal["local", "docker"] | None = None, + factory_id: str | t.Literal["default"] | None = None, **_: bool, ) -> None: self.session_id = session_id or "local" - self.api_key = api_key - self.factory_id = factory_id + self.api_key = api_key or os.getenv("CODEBOX_API_KEY", "local") + self.factory_id = factory_id or os.getenv("CODEBOX_FACTORY_ID", "default") # SYNC @@ -112,7 +113,7 @@ def upload( timeout: float | None = None, ) -> CodeBoxFile: """Upload a file to the CodeBox instance""" - return syncify(self.aupload)(remote_file_path, content, timeout) + raise NotImplementedError("Abstract method, please use a subclass.") def stream_download( self, @@ -201,6 +202,8 @@ async def alist_files(self) -> list[CodeBoxFile]: remote_path=parts[0].removeprefix("./"), size=self._parse_size(parts[1]), codebox_id=self.session_id, + codebox_api_key=self.api_key, + codebox_factory_id=self.factory_id, ) for file in files if (parts := file.split(" ")) and len(parts) == 2 From 4d02bf939595829f1a6c9f340e008fde347b0c50 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 24 Oct 2024 10:28:43 +0800 Subject: [PATCH 093/125] rm prints, fix async, fix str comparison --- tests/test_v01.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tests/test_v01.py b/tests/test_v01.py index 629075a..aad2e17 100644 --- a/tests/test_v01.py +++ b/tests/test_v01.py @@ -19,15 +19,13 @@ def test_localbox(): def run_sync(codebox: CodeBox) -> bool: try: assert codebox.start() == "started" - print(codebox.status()) print("Started") assert codebox.status() == "running" - print(codebox.status()) print("Running") codebox.run("x = 'Hello World!'") - assert codebox.run("print(x)") == "Hello World!\n" + assert codebox.run("print(x)").content == "Hello World!" print("Printed") file_name = "test_file.txt" @@ -67,15 +65,13 @@ def run_sync(codebox: CodeBox) -> bool: async def run_async(codebox: CodeBox) -> bool: try: assert await codebox.astart() == "started" - print(await codebox.astatus()) print("Started") assert await codebox.astatus() == "running" - print(await codebox.astatus()) print("Running") await codebox.arun("x = 'Hello World!'") - assert await codebox.arun("print(x)") == "Hello World!\n" + assert (await codebox.arun("print(x)")).content == "Hello World!" print("Printed") file_name = "test_file.txt" @@ -86,7 +82,7 @@ async def run_async(codebox: CodeBox) -> bool: await codebox.arun("import os;\nprint(os.listdir(os.getcwd())); ") ) - assert file_name in str(codebox.list_files()) + assert file_name in str(await codebox.alist_files()) assert (await codebox.adownload(file_name)).content == b"Hello World!" print("Downloaded") From 533c2d5a16a1af0c1fe5623a10365643266ff428 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 25 Oct 2024 04:20:00 +0800 Subject: [PATCH 094/125] fix testv01 --- tests/test_v01.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_v01.py b/tests/test_v01.py index aad2e17..170cee5 100644 --- a/tests/test_v01.py +++ b/tests/test_v01.py @@ -25,7 +25,7 @@ def run_sync(codebox: CodeBox) -> bool: print("Running") codebox.run("x = 'Hello World!'") - assert codebox.run("print(x)").content == "Hello World!" + assert codebox.run("print(x)") == "Hello World!\n" print("Printed") file_name = "test_file.txt" @@ -71,7 +71,7 @@ async def run_async(codebox: CodeBox) -> bool: print("Running") await codebox.arun("x = 'Hello World!'") - assert (await codebox.arun("print(x)")).content == "Hello World!" + assert (await codebox.arun("print(x)")) == "Hello World!\n" print("Printed") file_name = "test_file.txt" From 6e421340f1a83a785d73e73016e518020ab80229 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sat, 26 Oct 2024 15:33:38 +0800 Subject: [PATCH 095/125] improve docker build tagging --- scripts/build.sh | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/scripts/build.sh b/scripts/build.sh index aa1aaa9..d225a8f 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -1,8 +1,16 @@ -TAG=${1:-latest} +if [ -z "$1" ]; then + echo "Error: No version supplied" + echo "Usage: $0 " + exit 1 +fi + +VERSION=$1 docker build -t codebox . # todo move container to seperate codeboxapi account -docker tag codebox:latest shroominic/codebox:$TAG +docker tag codebox:latest shroominic/codebox:$VERSION +docker tag codebox:latest shroominic/codebox:latest -docker push shroominic/codebox:$TAG +docker push shroominic/codebox:$VERSION +docker push shroominic/codebox:latest From b742c5c952321c09fc664b066dff7196d447baaa Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sat, 26 Oct 2024 15:34:12 +0800 Subject: [PATCH 096/125] improve protocol --- src/codeboxapi/api.py | 3 ++- src/codeboxapi/remote.py | 41 +++++++++++++++++++--------------------- src/codeboxapi/utils.py | 27 ++------------------------ 3 files changed, 23 insertions(+), 48 deletions(-) diff --git a/src/codeboxapi/api.py b/src/codeboxapi/api.py index 0d8beb2..dda935b 100644 --- a/src/codeboxapi/api.py +++ b/src/codeboxapi/api.py @@ -54,7 +54,8 @@ async def event_stream() -> AsyncGenerator[str, None]: async for chunk in codebox.astream_exec( exec.code, exec.kernel, exec.timeout, exec.cwd ): - yield chunk.__str__() + # protocol is content + yield f"<{chunk.type}>{chunk.content}" return StreamingResponse(event_stream()) diff --git a/src/codeboxapi/remote.py b/src/codeboxapi/remote.py index 1bd30d9..5002d18 100644 --- a/src/codeboxapi/remote.py +++ b/src/codeboxapi/remote.py @@ -1,3 +1,4 @@ +import re from os import PathLike, getenv from typing import AsyncGenerator, BinaryIO, Generator, Literal from uuid import uuid4 @@ -59,19 +60,16 @@ def stream_exec( json={"code": code, "kernel": kernel, "cwd": cwd}, ) as response: response.raise_for_status() - img_buffer = "" + buffer = "" for chunk in response.iter_text(): - # todo check for better solutions - # why did I implemented my own streaming protocol? - if chunk.startswith("img;") and not chunk.endswith("=="): - img_buffer += chunk - elif img_buffer and chunk.endswith("=="): - yield ExecChunk.decode(img_buffer + chunk) - img_buffer = "" - elif img_buffer: - img_buffer += chunk - else: - yield ExecChunk.decode(chunk) + buffer += chunk + while match := re.match( + r"<(txt|img|err)>(.*?)", buffer, re.DOTALL + ): + _, end = match.span() + t, c = match.groups() + yield ExecChunk(type=t, content=c) # type: ignore[arg-type] + buffer = buffer[end:] async def astream_exec( self, @@ -89,17 +87,16 @@ async def astream_exec( json={"code": code, "kernel": kernel, "cwd": cwd}, ) as response: response.raise_for_status() - img_buffer = "" + buffer = "" async for chunk in response.aiter_text(): - if chunk.startswith("img;") and not chunk.endswith("=="): - img_buffer += chunk - elif img_buffer and chunk.endswith("=="): - yield ExecChunk.decode(img_buffer + chunk) - img_buffer = "" - elif img_buffer: - img_buffer += chunk - else: - yield ExecChunk.decode(chunk) + buffer += chunk + while match := re.match( + r"<(txt|img|err)>(.*?)", buffer, re.DOTALL + ): + _, end = match.span() + t, c = match.groups() + yield ExecChunk(type=t, content=c) # type: ignore[arg-type] + buffer = buffer[end:] except RuntimeError as e: if "loop is closed" not in str(e): raise e diff --git a/src/codeboxapi/utils.py b/src/codeboxapi/utils.py index 48c4735..1420b20 100644 --- a/src/codeboxapi/utils.py +++ b/src/codeboxapi/utils.py @@ -25,14 +25,6 @@ from .codebox import CodeBox -# import sys -# def _print(*text, stdout): -# _stdout = sys.stdout -# sys.stdout = stdout -# print(*text, flush=True) -# sys.stdout = _stdout - - @dataclass class ExecChunk: """ @@ -40,22 +32,12 @@ class ExecChunk: The type is one of: - txt: text output - img: image output - - stm: stream output - err: error output """ - type: Literal["txt", "img", "stm", "err"] + type: Literal["txt", "img", "err"] content: str - @classmethod - def decode(cls, text: str) -> "ExecChunk": - type, content = text[:3], text[5:] - assert type in ["txt", "img", "stm", "err"] - return cls(type=type, content=content) # type: ignore[arg-type] - - def __str__(self) -> str: - return f"{self.type};\n{self.content}" - @dataclass class ExecResult: @@ -63,11 +45,7 @@ class ExecResult: @property def text(self) -> str: - return "".join( - chunk.content - for chunk in self.chunks - if chunk.type == "txt" or chunk.type == "stm" - ) + return "".join(chunk.content for chunk in self.chunks if chunk.type == "txt") @property def images(self) -> list[str]: @@ -150,7 +128,6 @@ def from_path(cls, path: str) -> "CodeBoxFile": return cls( remote_path=path, size=os.path.getsize(path), - codebox_id=None, _content=f.read(), ) From 27cd7f6b6559956ac6862a5596d66d8a511edf83 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sat, 26 Oct 2024 15:35:12 +0800 Subject: [PATCH 097/125] fix todo --- src/codeboxapi/codebox.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/codeboxapi/codebox.py b/src/codeboxapi/codebox.py index 24fb0f5..aa63829 100644 --- a/src/codeboxapi/codebox.py +++ b/src/codeboxapi/codebox.py @@ -223,10 +223,7 @@ async def alist_packages(self) -> list[str]: return (await self.aexec("uv pip list", kernel="bash")).text.splitlines() async def ashow_variables(self) -> dict[str, str]: - vars = [ - line.strip() for line in (await self.aexec("%who")).text.strip().split("\t") - ] - # todo remove that splitting thing when Out[0] thing is fixed + vars = [line.strip() for line in (await self.aexec("%who")).text.strip()] return {v: (await self.aexec(v)).text for v in vars} async def arestart(self) -> None: From 1ea5578b394796c18ff1ccc10974beab7d152344 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sat, 26 Oct 2024 15:36:48 +0800 Subject: [PATCH 098/125] fix image matching --- src/codeboxapi/local.py | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/codeboxapi/local.py b/src/codeboxapi/local.py index 0b54132..75ead9d 100644 --- a/src/codeboxapi/local.py +++ b/src/codeboxapi/local.py @@ -16,6 +16,7 @@ import threading import time import typing as t +from builtins import print as important_print from queue import Queue from IPython.core.interactiveshell import ExecutionResult, InteractiveShell @@ -23,6 +24,8 @@ from .codebox import CodeBox, CodeBoxFile, ExecChunk from .utils import check_installed, raise_timeout, resolve_pathlike, run_inside +IMAGE_PATTERN = r"(.*?)" + class LocalBox(CodeBox): """ @@ -40,6 +43,8 @@ def __init__( codebox_cwd: str = ".codebox", **kwargs, ) -> None: + self.api_key = "local" + self.factory_id = "local" self.session_id = session_id or "" os.makedirs(codebox_cwd, exist_ok=True) self.cwd = os.path.abspath(codebox_cwd) @@ -60,7 +65,7 @@ def custom_show(close=True): fig.savefig(buf, format="png") buf.seek(0) img_str = base64.b64encode(buf.getvalue()).decode("utf-8") - print(f"") + important_print(IMAGE_PATTERN.replace("(.*?)", img_str)) if close: plt.close(fig) @@ -100,23 +105,22 @@ def stream_chunks(_out: io.StringIO, _err: io.StringIO) -> None: # todo make this more efficient? sys.stdout = _out = io.StringIO() - if "" - ) + if "" in output: image_matches = re.findall( - image_pattern, output + IMAGE_PATTERN, output ) for img_str in image_matches: queue.put( ExecChunk(type="img", content=img_str) ) - output = re.sub(image_pattern, "", output) + output = re.sub(IMAGE_PATTERN, "", output) if output: if output.startswith("Out["): # todo better disable logging somehow - output = re.sub(r"Out[(.*?)]: ", "", output) + output = re.sub( + r"Out\[(.*?)\]: ", "", output.strip() + ) queue.put(ExecChunk(type="txt", content=output)) if error := _err.getvalue(): @@ -209,14 +213,11 @@ async def astream_exec( # todo make this more efficient? sys.stdout = temp_output = io.StringIO() - if "" - ) - image_matches = re.findall(image_pattern, output) + if "" in output: + image_matches = re.findall(IMAGE_PATTERN, output) for img_str in image_matches: yield ExecChunk(type="img", content=img_str) - output = re.sub(image_pattern, "", output) + output = re.sub(IMAGE_PATTERN, "", output) if output: if output.startswith("Out["): @@ -322,9 +323,7 @@ async def aupload( elif isinstance(content, bytes): await file.write(content) else: - from builtins import print as do_not_remote_print_lol - - do_not_remote_print_lol(type(content), content.__dict__) + print(type(content), content.__dict__) raise TypeError("Unsupported content type") file_size = await aiofiles.os.path.getsize(file_path) From 625b19ae1f95acfa4f6b9baa7306ab05dce2f2a2 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 27 Oct 2024 21:38:36 +0800 Subject: [PATCH 099/125] skip docker tests if not running --- tests/test_v02.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_v02.py b/tests/test_v02.py index 8b6ae33..96f023a 100644 --- a/tests/test_v02.py +++ b/tests/test_v02.py @@ -9,11 +9,13 @@ scope="session", params=[ "local", - # "docker", + "docker", os.getenv("CODEBOX_API_KEY"), ], ) def codebox(request): + if os.system("docker ps > /dev/null 2>&1") != 0: + pytest.skip("Docker is not running") return CodeBox(api_key=request.param) # api_key=request.param) From f008b56acafdabfd754acc5d71dbd182ab030216 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 28 Oct 2024 12:46:01 +0800 Subject: [PATCH 100/125] migrate v0.2 --- examples/parallel.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/parallel.py b/examples/parallel.py index 3853fcb..a21ed40 100644 --- a/examples/parallel.py +++ b/examples/parallel.py @@ -8,11 +8,11 @@ async def main(): async def spawn_codebox(): - async with CodeBox() as codebox: - await codebox.arun("a = 'Hello World!'") - a = await codebox.arun("a") - assert a == "Hello World!" - print("Success!") + codebox = CodeBox(api_key="local") + await codebox.arun("a = 'Hello World!'") + a = await codebox.arun("a") + assert a == "Hello World!" + print("Success!") if __name__ == "__main__": From bc8a96b4350d7a743cadee9128538aefb9991ccf Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 28 Oct 2024 13:09:31 +0800 Subject: [PATCH 101/125] fix types from CodeBoxFile to RemoteFile --- examples/file_io.py | 6 +- src/codeboxapi/__init__.py | 3 +- src/codeboxapi/api.py | 9 +- src/codeboxapi/codebox.py | 56 ++++++------- src/codeboxapi/local.py | 26 +++--- src/codeboxapi/remote.py | 66 ++++++--------- src/codeboxapi/types.py | 120 +++++++++++++++++++++++++++ src/codeboxapi/utils.py | 166 +++++-------------------------------- tests/test_v01.py | 4 +- tests/test_v02.py | 41 ++++----- 10 files changed, 227 insertions(+), 270 deletions(-) create mode 100644 src/codeboxapi/types.py diff --git a/examples/file_io.py b/examples/file_io.py index 0bdc503..9026e1a 100644 --- a/examples/file_io.py +++ b/examples/file_io.py @@ -29,7 +29,5 @@ else: # all files inside the codebox for file in codebox.list_files(): - print("File: ", file.name) - print("Content is None: ", file.content is None) - content = codebox.download(file.name) - print("Content: ", content) + print("File: ", file.path) + print("Content: ", file.get_content()) diff --git a/src/codeboxapi/__init__.py b/src/codeboxapi/__init__.py index c3ee53c..12ade69 100644 --- a/src/codeboxapi/__init__.py +++ b/src/codeboxapi/__init__.py @@ -7,5 +7,6 @@ """ from .codebox import CodeBox +from .types import ExecChunk, ExecResult, RemoteFile -__all__ = ["CodeBox"] +__all__ = ["CodeBox", "ExecChunk", "ExecResult", "RemoteFile"] diff --git a/src/codeboxapi/api.py b/src/codeboxapi/api.py index dda935b..9e50f8c 100644 --- a/src/codeboxapi/api.py +++ b/src/codeboxapi/api.py @@ -9,7 +9,6 @@ from pydantic import BaseModel from .local import LocalBox -from .utils import CodeBoxFile codebox = LocalBox() last_interaction = datetime.utcnow() @@ -78,10 +77,10 @@ async def upload( file: UploadFile, timeout: int | None = None, codebox: LocalBox = Depends(get_codebox), -) -> "CodeBoxFile": - if not file.filename: - raise HTTPException(status_code=400, detail="A file name is required") - return await codebox.aupload(file.filename, file.file, timeout) +) -> None: + if file.filename: + await codebox.aupload(file.filename, file.file, timeout) + raise HTTPException(status_code=400, detail="A file name is required") @app.post("/code/execute") diff --git a/src/codeboxapi/codebox.py b/src/codeboxapi/codebox.py index aa63829..5cce1d6 100644 --- a/src/codeboxapi/codebox.py +++ b/src/codeboxapi/codebox.py @@ -41,16 +41,10 @@ import anyio -from .utils import ( - CodeBoxFile, - CodeBoxOutput, - ExecChunk, - ExecResult, - async_flatten_exec_result, - deprecated, - flatten_exec_result, - syncify, -) +from .utils import async_flatten_exec_result, deprecated, flatten_exec_result, syncify + +if t.TYPE_CHECKING: + from .types import CodeBoxOutput, ExecChunk, ExecResult, RemoteFile class CodeBox: @@ -92,7 +86,7 @@ def exec( kernel: t.Literal["ipython", "bash"] = "ipython", timeout: float | None = None, cwd: str | None = None, - ) -> ExecResult: + ) -> "ExecResult": """Execute code inside the CodeBox instance""" return flatten_exec_result(self.stream_exec(code, kernel, timeout, cwd)) @@ -102,7 +96,7 @@ def stream_exec( kernel: t.Literal["ipython", "bash"] = "ipython", timeout: float | None = None, cwd: str | None = None, - ) -> t.Generator[ExecChunk, None, None]: + ) -> t.Generator["ExecChunk", None, None]: """Executes the code and streams the result.""" raise NotImplementedError("Abstract method, please use a subclass.") @@ -111,7 +105,7 @@ def upload( remote_file_path: str, content: t.BinaryIO | bytes | str, timeout: float | None = None, - ) -> CodeBoxFile: + ) -> "RemoteFile": """Upload a file to the CodeBox instance""" raise NotImplementedError("Abstract method, please use a subclass.") @@ -131,7 +125,7 @@ async def aexec( kernel: t.Literal["ipython", "bash"] = "ipython", timeout: float | None = None, cwd: str | None = None, - ) -> ExecResult: + ) -> "ExecResult": """Async Execute python code inside the CodeBox instance""" return await async_flatten_exec_result( self.astream_exec(code, kernel, timeout, cwd) @@ -143,7 +137,7 @@ def astream_exec( kernel: t.Literal["ipython", "bash"] = "ipython", timeout: float | None = None, cwd: str | None = None, - ) -> t.AsyncGenerator[ExecChunk, None]: + ) -> t.AsyncGenerator["ExecChunk", None]: """Async Stream Chunks of Execute python code inside the CodeBox instance""" raise NotImplementedError("Abstract method, please use a subclass.") @@ -152,7 +146,7 @@ async def aupload( remote_file_path: str, content: t.BinaryIO | bytes | str, timeout: float | None = None, - ) -> CodeBoxFile: + ) -> "RemoteFile": """Async Upload a file to the CodeBox instance""" raise NotImplementedError("Abstract method, please use a subclass.") @@ -160,10 +154,8 @@ async def adownload( self, remote_file_path: str, timeout: float | None = None, - ) -> CodeBoxFile: - return [ - f for f in (await self.alist_files()) if f.remote_path == remote_file_path - ][0] + ) -> "RemoteFile": + return [f for f in (await self.alist_files()) if f.path in remote_file_path][0] def astream_download( self, @@ -190,7 +182,9 @@ async def ainstall(self, *packages: str) -> str: ) return " ".join(packages) + " installed successfully" - async def alist_files(self) -> list[CodeBoxFile]: + async def alist_files(self) -> list["RemoteFile"]: + from .types import RemoteFile + files = ( await self.aexec( "find . -type f -exec du -h {} + | awk '{print $2, $1}' | sort", @@ -198,12 +192,10 @@ async def alist_files(self) -> list[CodeBoxFile]: ) ).text.splitlines() return [ - CodeBoxFile( - remote_path=parts[0].removeprefix("./"), - size=self._parse_size(parts[1]), - codebox_id=self.session_id, - codebox_api_key=self.api_key, - codebox_factory_id=self.factory_id, + RemoteFile( + path=parts[0].removeprefix("./"), + remote=self, + _size=self._parse_size(parts[1]), ) for file in files if (parts := file.split(" ")) and len(parts) == 2 @@ -245,7 +237,7 @@ async def ping(cb: CodeBox, d: int) -> None: def download( self, remote_file_path: str, timeout: float | None = None - ) -> CodeBoxFile: + ) -> "RemoteFile": return syncify(self.adownload)(remote_file_path, timeout) def healthcheck(self) -> str: @@ -254,7 +246,7 @@ def healthcheck(self) -> str: def install(self, *packages: str) -> str: return syncify(self.ainstall)(*packages) - def list_files(self) -> list[CodeBoxFile]: + def list_files(self) -> list["RemoteFile"]: return syncify(self.alist_files)() def list_packages(self) -> list[str]: @@ -290,7 +282,9 @@ async def astop(self) -> t.Literal["stopped"]: @deprecated( "The `.run` method is deprecated. Use `.exec` instead.", ) - async def arun(self, code: str | os.PathLike) -> CodeBoxOutput: + async def arun(self, code: str | os.PathLike) -> "CodeBoxOutput": + from .types import CodeBoxOutput + exec_result = await self.aexec(code, kernel="ipython") if exec_result.images: return CodeBoxOutput(type="image/png", content=exec_result.images[0]) @@ -321,7 +315,7 @@ def stop(self) -> t.Literal["stopped"]: @deprecated( "The `.run` method is deprecated. Use `.exec` instead.", ) - def run(self, code: str | os.PathLike) -> CodeBoxOutput: + def run(self, code: str | os.PathLike) -> "CodeBoxOutput": return syncify(self.arun)(code) @deprecated( diff --git a/src/codeboxapi/local.py b/src/codeboxapi/local.py index 75ead9d..abdd85c 100644 --- a/src/codeboxapi/local.py +++ b/src/codeboxapi/local.py @@ -21,7 +21,8 @@ from IPython.core.interactiveshell import ExecutionResult, InteractiveShell -from .codebox import CodeBox, CodeBoxFile, ExecChunk +from .codebox import CodeBox +from .types import ExecChunk, RemoteFile from .utils import check_installed, raise_timeout, resolve_pathlike, run_inside IMAGE_PATTERN = r"(.*?)" @@ -271,7 +272,9 @@ def upload( remote_file_path: str, content: t.BinaryIO | bytes | str, timeout: float | None = None, - ) -> CodeBoxFile: + ) -> "RemoteFile": + from .types import RemoteFile + with raise_timeout(timeout): file_path = os.path.join(self.cwd, remote_file_path) with open(file_path, "wb") as file: @@ -285,21 +288,18 @@ def upload( file.write(content.read()) else: raise TypeError("Unsupported content type") - file_size = os.path.getsize(file_path) - return CodeBoxFile( - remote_path=file_path, - size=file_size, - codebox_id=self.session_id, - ) + return RemoteFile(path=remote_file_path, remote=self) async def aupload( self, file_name: str, content: t.BinaryIO | bytes | str | tmpf.SpooledTemporaryFile, timeout: float | None = None, - ) -> CodeBoxFile: + ) -> RemoteFile: import aiofiles.os + from .types import RemoteFile + async with asyncio.timeout(timeout): file_path = os.path.join(self.cwd, file_name) async with aiofiles.open(file_path, "wb") as file: @@ -325,13 +325,7 @@ async def aupload( else: print(type(content), content.__dict__) raise TypeError("Unsupported content type") - - file_size = await aiofiles.os.path.getsize(file_path) - return CodeBoxFile( - remote_path=file_path, - size=file_size, - codebox_id=self.session_id, - ) + return RemoteFile(path=file_path, remote=self) def stream_download( self, diff --git a/src/codeboxapi/remote.py b/src/codeboxapi/remote.py index 5002d18..bec7462 100644 --- a/src/codeboxapi/remote.py +++ b/src/codeboxapi/remote.py @@ -1,12 +1,13 @@ import re +import typing as t from os import PathLike, getenv -from typing import AsyncGenerator, BinaryIO, Generator, Literal from uuid import uuid4 import anyio import httpx -from .codebox import CodeBox, CodeBoxFile, ExecChunk +from .codebox import CodeBox +from .types import ExecChunk, RemoteFile from .utils import raise_error, resolve_pathlike @@ -22,8 +23,8 @@ def __new__(cls, *args, **kwargs) -> "RemoteBox": def __init__( self, session_id: str | None = None, - api_key: str | Literal["local", "docker"] | None = None, - factory_id: str | Literal["default"] | None = None, + api_key: str | t.Literal["local", "docker"] | None = None, + factory_id: str | t.Literal["default"] | None = None, base_url: str | None = None, ) -> None: self.session_id = session_id or uuid4().hex @@ -48,10 +49,10 @@ def __init__( def stream_exec( self, code: str | PathLike, - kernel: Literal["ipython", "bash"] = "ipython", + kernel: t.Literal["ipython", "bash"] = "ipython", timeout: float | None = None, cwd: str | None = None, - ) -> Generator[ExecChunk, None, None]: + ) -> t.Generator[ExecChunk, None, None]: code = resolve_pathlike(code) with self.client.stream( method="POST", @@ -74,10 +75,10 @@ def stream_exec( async def astream_exec( self, code: str | PathLike, - kernel: Literal["ipython", "bash"] = "ipython", + kernel: t.Literal["ipython", "bash"] = "ipython", timeout: float | None = None, cwd: str | None = None, - ) -> AsyncGenerator[ExecChunk, None]: + ) -> t.AsyncGenerator[ExecChunk, None]: code = resolve_pathlike(code) try: async with self.aclient.stream( @@ -107,58 +108,43 @@ async def astream_exec( def upload( self, file_name: str, - content: BinaryIO | bytes | str, + content: t.BinaryIO | bytes | str, timeout: float | None = None, - ) -> CodeBoxFile: + ) -> "RemoteFile": + from .types import RemoteFile + if isinstance(content, str): content = content.encode("utf-8") - response = self.client.post( + self.client.post( url="/files/upload", files={"file": (file_name, content)}, timeout=timeout, - ) - json = response.json() - # todo fix: this is bad code - del json["codebox_id"] - del json["codebox_api_key"] - del json["codebox_factory_id"] - return CodeBoxFile( - **json, - codebox_api_key=self.api_key, - codebox_factory_id=self.factory_id, - codebox_id=self.session_id, - ) + ).raise_for_status() + return RemoteFile(path=file_name, remote=self) async def aupload( self, - file_name: str, - content: BinaryIO | bytes | str, + remote_file_path: str, + content: t.BinaryIO | bytes | str, timeout: float | None = None, - ) -> CodeBoxFile: + ) -> "RemoteFile": + from .types import RemoteFile + if isinstance(content, str): content = content.encode("utf-8") response = await self.aclient.post( url="/files/upload", - files={"file": (file_name, content)}, + files={"file": (remote_file_path, content)}, timeout=timeout, ) - json = response.json() - # todo this is a sign of bad design - del json["codebox_id"] - del json["codebox_api_key"] - del json["codebox_factory_id"] - return CodeBoxFile( - **json, - codebox_api_key=self.api_key, - codebox_factory_id=self.factory_id, - codebox_id=self.session_id, - ) + response.raise_for_status() + return RemoteFile(path=remote_file_path, remote=self) def stream_download( self, remote_file_path: str, timeout: float | None = None, - ) -> Generator[bytes, None, None]: + ) -> t.Generator[bytes, None, None]: with self.client.stream( method="GET", url=f"/files/download/{remote_file_path}", @@ -171,7 +157,7 @@ async def astream_download( self, remote_file_path: str, timeout: float | None = None, - ) -> AsyncGenerator[bytes, None]: + ) -> t.AsyncGenerator[bytes, None]: async with self.aclient.stream( method="GET", url=f"/files/download/{remote_file_path}", diff --git a/src/codeboxapi/types.py b/src/codeboxapi/types.py new file mode 100644 index 0000000..38492ba --- /dev/null +++ b/src/codeboxapi/types.py @@ -0,0 +1,120 @@ +import typing as t +from dataclasses import dataclass + +import aiofiles + +from .codebox import CodeBox + + +@dataclass +class RemoteFile: + path: str + remote: CodeBox + _size: int | None = None + _content: bytes | None = None + + @property + def name(self) -> str: + return self.path.split("/")[-1] + + def get_content(self) -> bytes: + if self._content is None: + self._content = b"".join(self.remote.stream_download(self.path)) + return self._content + + async def aget_content(self) -> bytes: + if self._content is None: + self._content = b"" + async for chunk in self.remote.astream_download(self.path): + self._content += chunk + return self._content + + def get_size(self) -> int: + if self._size is None: + self._size = len(self.get_content()) + return self._size + + async def aget_size(self) -> int: + if self._size is None: + self._size = len(await self.aget_content()) + return self._size + + def save(self, local_path: str) -> None: + with open(local_path, "wb") as f: + for chunk in self.remote.stream_download(self.path): + f.write(chunk) + + async def asave(self, local_path: str) -> None: + async with aiofiles.open(local_path, "wb") as f: + async for chunk in self.remote.astream_download(self.path): + await f.write(chunk) + + +@dataclass +class ExecChunk: + """ + A chunk of output from an execution. + The type is one of: + - txt: text output + - img: image output + - err: error output + """ + + type: t.Literal["txt", "img", "err"] + content: str + + +@dataclass +class ExecResult: + chunks: list[ExecChunk] + + @property + def text(self) -> str: + return "".join(chunk.content for chunk in self.chunks if chunk.type == "txt") + + @property + def images(self) -> list[str]: + return [chunk.content for chunk in self.chunks if chunk.type == "img"] + + @property + def errors(self) -> list[str]: + return [chunk.content for chunk in self.chunks if chunk.type == "err"] + + +@dataclass +class CodeBoxOutput: + """Deprecated CodeBoxOutput class""" + + content: str + type: t.Literal["stdout", "stderr", "image/png", "error"] + + def __str__(self) -> str: + return self.content + + def __eq__(self, other: object) -> bool: + if isinstance(other, str): + return self.content == other + if isinstance(other, CodeBoxOutput): + return self.content == other.content and self.type == other.type + return False + + +class CodeBoxFile: + """Deprecated CodeBoxFile class""" + + def __init__(self, name: str, content: bytes | None = None) -> None: + from .utils import deprecated + + deprecated( + "The CodeBoxFile class is deprecated. Use RemoteFile for file handling " + "or plain bytes for content instead." + )(lambda: None)() + self.name = name + self.content = content + + @classmethod + def from_path(cls, path: str) -> "CodeBoxFile": + import os + + with open(path, "rb") as f: + return cls(name=os.path.basename(path), content=f.read()) diff --git a/src/codeboxapi/utils.py b/src/codeboxapi/utils.py index 1420b20..48cc9e0 100644 --- a/src/codeboxapi/utils.py +++ b/src/codeboxapi/utils.py @@ -1,155 +1,23 @@ import os import signal +import typing as t from contextlib import contextmanager -from dataclasses import dataclass from functools import partial, reduce, wraps from importlib.metadata import PackageNotFoundError, distribution -from typing import ( - TYPE_CHECKING, - Any, - AsyncGenerator, - Callable, - Coroutine, - Iterator, - Literal, - NoReturn, - ParamSpec, - TypeVar, -) from warnings import warn import anyio from anyio._core._eventloop import threadlocals -if TYPE_CHECKING: - from .codebox import CodeBox +if t.TYPE_CHECKING: + from .types import ExecChunk, ExecResult - -@dataclass -class ExecChunk: - """ - A chunk of output from an execution. - The type is one of: - - txt: text output - - img: image output - - err: error output - """ - - type: Literal["txt", "img", "err"] - content: str - - -@dataclass -class ExecResult: - chunks: list[ExecChunk] - - @property - def text(self) -> str: - return "".join(chunk.content for chunk in self.chunks if chunk.type == "txt") - - @property - def images(self) -> list[str]: - return [chunk.content for chunk in self.chunks if chunk.type == "img"] - - @property - def errors(self) -> list[str]: - return [chunk.content for chunk in self.chunks if chunk.type == "err"] - - -# todo move somewhere more clean -@dataclass -class CodeBoxOutput: - """Deprecated CodeBoxOutput class""" - - content: str - type: Literal["stdout", "stderr", "image/png", "error"] - - def __str__(self) -> str: - return self.content - - def __eq__(self, other: object) -> bool: - if isinstance(other, str): - return self.content == other - if isinstance(other, CodeBoxOutput): - return self.content == other.content and self.type == other.type - return False +T = t.TypeVar("T") +P = t.ParamSpec("P") -@dataclass -class CodeBoxFile: - remote_path: str - size: int - codebox_id: str | None = None - codebox_api_key: str | None = None - codebox_factory_id: str | None = None - _content: bytes | None = None - - @property - def codebox(self) -> "CodeBox": - from .codebox import CodeBox - - if self.codebox_id is None: - raise ValueError("CodeBox ID is not set") - if self.codebox_api_key is None: - raise ValueError("CodeBox API key is not set") - if self.codebox_factory_id is None: - raise ValueError("CodeBox factory ID is not set") - if self.codebox_api_key == "docker": - from .docker import DockerBox - - return DockerBox( - port_or_range=int(self.codebox_id), - image=self.codebox_factory_id, - start_container=False, - ) - return CodeBox( - session_id=self.codebox_id, - api_key=self.codebox_api_key, - factory_id=self.codebox_factory_id, - ) - - @property - def name(self) -> str: - return self.remote_path.split("/")[-1] - - @property - def content(self) -> bytes: - return self._content or b"".join(self.codebox.stream_download(self.remote_path)) - - @property - async def acontent(self) -> bytes: - return self._content or b"".join([ - chunk async for chunk in self.codebox.astream_download(self.remote_path) - ]) - - @classmethod - def from_path(cls, path: str) -> "CodeBoxFile": - with open(path, "rb") as f: - return cls( - remote_path=path, - size=os.path.getsize(path), - _content=f.read(), - ) - - def save(self, path: str) -> None: - with open(path, "wb") as f: - for chunk in self.codebox.stream_download(self.remote_path): - f.write(chunk) - - async def asave(self, path: str) -> None: - import aiofiles - - async with aiofiles.open(path, "wb") as f: - async for chunk in self.codebox.astream_download(self.remote_path): - await f.write(chunk) - - -T = TypeVar("T") -P = ParamSpec("P") - - -def deprecated(message: str) -> Callable[[Callable[P, T]], Callable[P, T]]: - def decorator(func: Callable[P, T]) -> Callable[P, T]: +def deprecated(message: str) -> t.Callable[[t.Callable[P, T]], t.Callable[P, T]]: + def decorator(func: t.Callable[P, T]) -> t.Callable[P, T]: @wraps(func) def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: if os.getenv("IGNORE_DEPRECATION_WARNINGS", "false").lower() == "true": @@ -173,11 +41,15 @@ def resolve_pathlike(file: str | os.PathLike) -> str: return file -def reduce_bytes(async_gen: Iterator[bytes]) -> bytes: +def reduce_bytes(async_gen: t.Iterator[bytes]) -> bytes: return reduce(lambda x, y: x + y, async_gen) -def flatten_exec_result(result: ExecResult | Iterator[ExecChunk]) -> ExecResult: +def flatten_exec_result( + result: "ExecResult" | t.Iterator["ExecChunk"], +) -> "ExecResult": + from .types import ExecResult + if not isinstance(result, ExecResult): result = ExecResult(chunks=[c for c in result]) # todo todo todo todo todo todo @@ -192,8 +64,8 @@ def flatten_exec_result(result: ExecResult | Iterator[ExecChunk]) -> ExecResult: async def async_flatten_exec_result( - async_gen: AsyncGenerator[ExecChunk, None], -) -> ExecResult: + async_gen: t.AsyncGenerator["ExecChunk", None], +) -> "ExecResult": # todo todo todo todo todo todo # remove empty text chunks # merge text chunks @@ -202,10 +74,14 @@ async def async_flatten_exec_result( # remove empty error chunks # merge error chunks # ... + from .types import ExecResult + return ExecResult(chunks=[c async for c in async_gen]) -def syncify(async_function: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P, T]: +def syncify( + async_function: t.Callable[P, t.Coroutine[t.Any, t.Any, T]], +) -> t.Callable[P, T]: """ Take an async function and create a regular one that receives the same keyword and positional arguments, and that when called, calls the original async function in @@ -269,5 +145,5 @@ def run_inside(directory: str): os.chdir(old_cwd) -def raise_error(message: str) -> NoReturn: +def raise_error(message: str) -> t.NoReturn: raise Exception(message) diff --git a/tests/test_v01.py b/tests/test_v01.py index 170cee5..845ee72 100644 --- a/tests/test_v01.py +++ b/tests/test_v01.py @@ -37,7 +37,7 @@ def run_sync(codebox: CodeBox) -> bool: ) assert file_name in str(codebox.list_files()) - assert codebox.download(file_name).content == b"Hello World!" + assert codebox.download(file_name).get_content() == b"Hello World!" print("Downloaded") assert "matplotlib" in str(codebox.install("matplotlib")) @@ -84,7 +84,7 @@ async def run_async(codebox: CodeBox) -> bool: assert file_name in str(await codebox.alist_files()) - assert (await codebox.adownload(file_name)).content == b"Hello World!" + assert (await codebox.adownload(file_name)).get_content() == b"Hello World!" print("Downloaded") assert "matplotlib" in str(await codebox.ainstall("matplotlib")) diff --git a/tests/test_v02.py b/tests/test_v02.py index 96f023a..590d92a 100644 --- a/tests/test_v02.py +++ b/tests/test_v02.py @@ -1,8 +1,7 @@ import os import pytest -from codeboxapi import CodeBox -from codeboxapi.utils import CodeBoxFile, ExecChunk, ExecResult +from codeboxapi import CodeBox, ExecChunk, ExecResult, RemoteFile @pytest.fixture( @@ -29,19 +28,14 @@ def test_sync_codebox_lifecycle(codebox: CodeBox): file_name = "test_file.txt" file_content = b"Hello World!" - uploaded_file = codebox.upload(file_name, file_content) - assert isinstance(uploaded_file, CodeBoxFile), "Upload should return a CodeBoxFile" - assert uploaded_file.name == file_name, "Uploaded file should have correct name" - assert uploaded_file.size == len( - file_content - ), "Uploaded file should have correct size" + codebox.upload(file_name, file_content) downloaded_file = codebox.download(file_name) assert isinstance( - downloaded_file, CodeBoxFile - ), "Download should return a CodeBoxFile" + downloaded_file, RemoteFile + ), "Download should return a RemoteFile" assert ( - downloaded_file.content == file_content + downloaded_file.get_content() == file_content ), "Downloaded content should match uploaded content" install_result = codebox.install("matplotlib") @@ -77,19 +71,14 @@ async def test_async_codebox_lifecycle(codebox: CodeBox): file_name = "test_file.txt" file_content = b"Hello World!" - uploaded_file = await codebox.aupload(file_name, file_content) - assert isinstance(uploaded_file, CodeBoxFile), "Upload should return a CodeBoxFile" - assert uploaded_file.name == file_name, "Uploaded file should have correct name" - assert uploaded_file.size == len( - file_content - ), "Uploaded file should have correct size" + await codebox.aupload(file_name, file_content) downloaded_file = await codebox.adownload(file_name) assert isinstance( - downloaded_file, CodeBoxFile - ), "Download should return a CodeBoxFile" + downloaded_file, RemoteFile + ), "Download should return a RemoteFile" assert ( - await downloaded_file.acontent == file_content + downloaded_file.get_content() == file_content ), "Downloaded content should match uploaded content" install_result = await codebox.ainstall("matplotlib") @@ -129,8 +118,8 @@ def test_sync_list_operations(codebox: CodeBox): files = codebox.list_files() assert isinstance(files, list), "list_files should return a list" assert all( - isinstance(f, CodeBoxFile) for f in files - ), "All items in list_files should be CodeBoxFile instances" + isinstance(f, RemoteFile) for f in files + ), "All items in list_files should be RemoteFile instances" packages = codebox.list_packages() assert isinstance(packages, list), "list_packages should return a list" @@ -156,8 +145,8 @@ async def test_async_list_operations(codebox: CodeBox): files = await codebox.alist_files() assert isinstance(files, list), "list_files should return a list" assert all( - isinstance(f, CodeBoxFile) for f in files - ), "All items in list_files should be CodeBoxFile instances" + isinstance(f, RemoteFile) for f in files + ), "All items in list_files should be RemoteFile instances" packages = await codebox.alist_packages() assert isinstance(packages, list), "list_packages should return a list" @@ -253,7 +242,7 @@ def test_sync_bash_commands(codebox: CodeBox): assert "ok" in result.text, "Execution should contain 'ok'" result = codebox.exec("echo \"print('Hello!')\" > test.py", kernel="bash") assert result.text.strip() == "", "Execution result should be empty" - assert "test.py" in [file.remote_path for file in codebox.list_files()] + assert "test.py" in [file.path for file in codebox.list_files()] result = codebox.exec("python test.py", kernel="bash") assert result.text.strip() == "Hello!", "Execution result should be 'Hello!'" @@ -264,7 +253,7 @@ async def test_async_bash_commands(codebox: CodeBox): assert "ok" in result.text, "Execution should contain 'ok'" result = await codebox.aexec("echo 'print(\"Hello!\")' > test.py", kernel="bash") assert result.text.strip() == "", "Execution result should be empty" - assert "test.py" in [file.remote_path for file in await codebox.alist_files()] + assert "test.py" in [file.path for file in await codebox.alist_files()] result = await codebox.aexec("python test.py", kernel="bash") assert result.text.strip() == "Hello!", "Execution result should be 'Hello!'" From 1732905323035724ee854e6467466579f60e814f Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 28 Oct 2024 14:28:45 +0800 Subject: [PATCH 102/125] rm unused func --- src/codeboxapi/utils.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/codeboxapi/utils.py b/src/codeboxapi/utils.py index 48cc9e0..620eeb7 100644 --- a/src/codeboxapi/utils.py +++ b/src/codeboxapi/utils.py @@ -115,10 +115,6 @@ def check_installed(package: str) -> None: raise -def debug_mode() -> bool: - return os.getenv("DEBUG", "false").lower() == "true" - - @contextmanager def raise_timeout(timeout: float | None = None): def timeout_handler(signum, frame): From 16a63ac8fb168ffadf9ccb18a0181eeb5e3326e0 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 28 Oct 2024 14:29:14 +0800 Subject: [PATCH 103/125] upgrade to v0.2 --- tests/parametric_test.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/parametric_test.py b/tests/parametric_test.py index 872532b..ba6efc4 100644 --- a/tests/parametric_test.py +++ b/tests/parametric_test.py @@ -3,10 +3,10 @@ from typing import Callable import pytest -from codeboxapi import CodeBox -from codeboxapi.schema import CodeBoxFile, CodeBoxOutput +from codeboxapi import CodeBox, RemoteFile +from codeboxapi.types import CodeBoxFile, CodeBoxOutput -AssertFunctionType = Callable[[CodeBoxOutput, list[CodeBoxFile]], bool] +AssertFunctionType = Callable[[CodeBoxOutput, list[RemoteFile]], bool] code_1 = """ import pandas as pd @@ -121,7 +121,9 @@ async def test_boxes_async( packages: list[str], capsys: pytest.CaptureFixture, ) -> None: - codeboxes = [CodeBox(local=local) for _ in range(num_samples)] + codeboxes = [ + CodeBox(api_key="local" if local else None) for _ in range(num_samples) + ] start_time = time.perf_counter() tasks = [ @@ -158,12 +160,10 @@ async def run_async( [file.name for file in files] + [file.name for file in orginal_files] ) == set([file.name for file in codebox_files]) - assert all( - [ - package_name in str(await codebox.ainstall(package_name)) - for package_name in packages - ] - ) + assert all([ + package_name in str(await codebox.ainstall(package_name)) + for package_name in packages + ]) output: CodeBoxOutput = await codebox.arun(code) codebox_files_output = await codebox.alist_files() From e9488b1778598bc26040e33b0aee2bdd887bdb0d Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 28 Oct 2024 14:29:57 +0800 Subject: [PATCH 104/125] add delay measurement --- tests/test_v02.py | 127 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 97 insertions(+), 30 deletions(-) diff --git a/tests/test_v02.py b/tests/test_v02.py index 590d92a..21d5253 100644 --- a/tests/test_v02.py +++ b/tests/test_v02.py @@ -1,4 +1,5 @@ import os +import time import pytest from codeboxapi import CodeBox, ExecChunk, ExecResult, RemoteFile @@ -157,65 +158,131 @@ async def test_async_list_operations(codebox: CodeBox): def test_sync_stream_exec(codebox: CodeBox): - chunks = list( - codebox.stream_exec( - "import time;\nfor i in range(3): time.sleep(0.01); print(i)" - ) - ) + chunks: list[tuple[ExecChunk, float]] = [] + t0 = time.perf_counter() + for chunk in codebox.stream_exec( + "import time;\nfor i in range(3): time.sleep(0.01); print(i)" + ): + chunks.append((chunk, time.perf_counter() - t0)) + assert ( len(chunks) == 3 ), "iterating over stream_exec should produce 3 chunks (ipython)" assert all( - isinstance(chunk, ExecChunk) for chunk in chunks + isinstance(chunk[0], ExecChunk) for chunk in chunks ), "All items should be ExecChunk instances (ipython)" assert all( - chunk.type == "txt" for chunk in chunks + chunk[0].type == "txt" for chunk in chunks ), "All chunks should be of type 'txt' (ipython)" - assert [chunk.content.strip() for chunk in chunks] == [ + assert [chunk[0].content.strip() for chunk in chunks] == [ "0", "1", "2", ], "Chunks should contain correct content (ipython)" - chunks = list( - codebox.stream_exec( - "python -c 'import time\nfor i in range(3): time.sleep(0.01); print(i)'", - kernel="bash", - ) - ) + # Verify chunks arrive with delay + assert all( + chunks[i][1] < chunks[i + 1][1] for i in range(len(chunks) - 1) + ), "Chunks should arrive with delay (ipython)" + # Verify delay is approximately 0.01s + assert all( + abs(chunks[i + 1][1] - chunks[i][1] - 0.01) < 0.005 + for i in range(len(chunks) - 1) + ), "Delay between chunks should be approximately 0.01s (ipython)" + + chunks = [] + t0 = time.perf_counter() + for chunk in codebox.stream_exec( + "python -u -c 'import time\nfor i in range(3): time.sleep(0.01); print(i)'", + kernel="bash", + ): + chunks.append((chunk, time.perf_counter() - t0)) + assert len(chunks) == 3, "iterating over stream_exec should produce 3 chunks (bash)" assert all( - isinstance(chunk, ExecChunk) for chunk in chunks + isinstance(chunk[0], ExecChunk) for chunk in chunks ), "All items should be ExecChunk instances (bash)" assert all( - chunk.type == "txt" for chunk in chunks + chunk[0].type == "txt" for chunk in chunks ), "All chunks should be of type 'txt' (bash)" - assert [chunk.content.strip() for chunk in chunks] == [ + assert [chunk[0].content.strip() for chunk in chunks] == [ "0", "1", "2", ], "Chunks should contain correct content (bash)" + # Verify chunks arrive with delay + assert all( + chunks[i][1] < chunks[i + 1][1] for i in range(len(chunks) - 1) + ), "Chunks should arrive with delay (bash)" + # Verify delay is approximately 0.01s + assert all( + abs(chunks[i + 1][1] - chunks[i][1] - 0.01) < 0.005 + for i in range(len(chunks) - 1) + ), "Delay between chunks should be approximately 0.01s (bash)" @pytest.mark.asyncio async def test_async_stream_exec(codebox: CodeBox): - chunks = [ - chunk - async for chunk in codebox.astream_exec( - "import time;\nfor i in range(3): time.sleep(0.01); print(i)" - ) - ] - assert len(chunks) == 3, "Stream should produce 3 chunks" + chunks: list[tuple[ExecChunk, float]] = [] + t0 = time.perf_counter() + async for chunk in codebox.astream_exec( + "import time;\nfor i in range(3): time.sleep(0.01); print(i)" + ): + chunks.append((chunk, time.perf_counter() - t0)) + + assert ( + len(chunks) == 3 + ), "iterating over stream_exec should produce 3 chunks (ipython)" + assert all( + isinstance(chunk[0], ExecChunk) for chunk in chunks + ), "All items should be ExecChunk instances (ipython)" + assert all( + chunk[0].type == "txt" for chunk in chunks + ), "All chunks should be of type 'txt' (ipython)" + assert [chunk[0].content.strip() for chunk in chunks] == [ + "0", + "1", + "2", + ], "Chunks should contain correct content (ipython)" + # Verify chunks arrive with delay assert all( - isinstance(chunk, ExecChunk) for chunk in chunks - ), "All items should be ExecChunk instances" + chunks[i][1] < chunks[i + 1][1] for i in range(len(chunks) - 1) + ), "Chunks should arrive with delay (ipython)" + # Verify delay is approximately 0.03s + assert all( - chunk.type == "txt" for chunk in chunks - ), "All chunks should be of type 'txt'" - assert [chunk.content.strip() for chunk in chunks] == [ + abs(chunks[i + 1][1] - chunks[i][1] - 0.01) < 0.005 + for i in range(len(chunks) - 1) + ), "Delay between chunks should be approximately 0.01s (ipython)" + + chunks = [] + t0 = time.perf_counter() + async for chunk in codebox.astream_exec( + "python -u -c 'import time\nfor i in range(3): time.sleep(0.01); print(i)'", + kernel="bash", + ): + chunks.append((chunk, time.perf_counter() - t0)) + + assert len(chunks) == 3, "iterating over stream_exec should produce 3 chunks (bash)" + assert all( + isinstance(chunk[0], ExecChunk) for chunk in chunks + ), "All items should be ExecChunk instances (bash)" + assert all( + chunk[0].type == "txt" for chunk in chunks + ), "All chunks should be of type 'txt' (bash)" + assert [chunk[0].content.strip() for chunk in chunks] == [ "0", "1", "2", - ], "Chunks should contain correct content" + ], "Chunks should contain correct content (bash)" + # Verify chunks arrive with delay + assert all( + chunks[i][1] < chunks[i + 1][1] for i in range(len(chunks) - 1) + ), "Chunks should arrive with delay (bash)" + # Verify delay is approximately 0.01s + assert all( + abs(chunks[i + 1][1] - chunks[i][1] - 0.01) < 0.005 + for i in range(len(chunks) - 1) + ), "Delay between chunks should be approximately 0.01s (bash)" def test_sync_error_handling(codebox: CodeBox): From 88ba047a40f82b1b04dcf6d461bf75f7e2cfb4af Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 29 Oct 2024 17:04:17 +0800 Subject: [PATCH 105/125] final api fixes --- src/codeboxapi/api.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/codeboxapi/api.py b/src/codeboxapi/api.py index 9e50f8c..0b69b02 100644 --- a/src/codeboxapi/api.py +++ b/src/codeboxapi/api.py @@ -1,6 +1,6 @@ import asyncio from contextlib import asynccontextmanager -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from os import getenv, path from typing import AsyncGenerator, Literal @@ -11,7 +11,7 @@ from .local import LocalBox codebox = LocalBox() -last_interaction = datetime.utcnow() +last_interaction = datetime.now(UTC) @asynccontextmanager @@ -19,7 +19,7 @@ async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]: async def timeout(): if (_timeout := getenv("CODEBOX_TIMEOUT", "15")).lower() == "none": return - while last_interaction + timedelta(minutes=float(_timeout)) > datetime.utcnow(): + while last_interaction + timedelta(minutes=float(_timeout)) > datetime.now(UTC): await asyncio.sleep(1) exit(0) @@ -30,7 +30,7 @@ async def timeout(): async def get_codebox() -> AsyncGenerator[LocalBox, None]: global codebox, last_interaction - last_interaction = datetime.utcnow() + last_interaction = datetime.now(UTC) yield codebox @@ -52,9 +52,8 @@ async def exec( async def event_stream() -> AsyncGenerator[str, None]: async for chunk in codebox.astream_exec( exec.code, exec.kernel, exec.timeout, exec.cwd - ): - # protocol is content - yield f"<{chunk.type}>{chunk.content}" + ): # protocol is content + yield f"<{chunk.type}>{chunk.content}\n" return StreamingResponse(event_stream()) @@ -78,9 +77,10 @@ async def upload( timeout: int | None = None, codebox: LocalBox = Depends(get_codebox), ) -> None: - if file.filename: - await codebox.aupload(file.filename, file.file, timeout) - raise HTTPException(status_code=400, detail="A file name is required") + if not file.filename: + raise HTTPException(status_code=400, detail="A file name is required") + async with asyncio.timeout(timeout): + await codebox.aupload(file.filename, file.file) @app.post("/code/execute") From 7710a9c5e68f9763968547be44de398793146e35 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 29 Oct 2024 17:04:28 +0800 Subject: [PATCH 106/125] hide docker logs --- src/codeboxapi/docker.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/codeboxapi/docker.py b/src/codeboxapi/docker.py index 1f8e386..da69af7 100644 --- a/src/codeboxapi/docker.py +++ b/src/codeboxapi/docker.py @@ -50,6 +50,8 @@ def __init__( image, ], check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, ) else: assert isinstance(port_or_range, int) From 268638818e4c8bdad6667e503ac605e2b7d1b602 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 29 Oct 2024 17:13:08 +0800 Subject: [PATCH 107/125] fix test skipping and typo --- tests/test_v02.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_v02.py b/tests/test_v02.py index 21d5253..ce9f9a9 100644 --- a/tests/test_v02.py +++ b/tests/test_v02.py @@ -14,7 +14,7 @@ ], ) def codebox(request): - if os.system("docker ps > /dev/null 2>&1") != 0: + if request.param == "docker" and os.system("docker ps > /dev/null 2>&1") != 0: pytest.skip("Docker is not running") return CodeBox(api_key=request.param) # api_key=request.param) @@ -247,8 +247,7 @@ async def test_async_stream_exec(codebox: CodeBox): assert all( chunks[i][1] < chunks[i + 1][1] for i in range(len(chunks) - 1) ), "Chunks should arrive with delay (ipython)" - # Verify delay is approximately 0.03s - + # Verify delay is approximately 0.01s assert all( abs(chunks[i + 1][1] - chunks[i][1] - 0.01) < 0.005 for i in range(len(chunks) - 1) From c46f0b30d6e5816d62d1acf62f177df7270da08f Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 29 Oct 2024 17:13:17 +0800 Subject: [PATCH 108/125] rm latest tag --- scripts/build.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/build.sh b/scripts/build.sh index d225a8f..5cd1503 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -9,8 +9,8 @@ VERSION=$1 docker build -t codebox . # todo move container to seperate codeboxapi account -docker tag codebox:latest shroominic/codebox:$VERSION -docker tag codebox:latest shroominic/codebox:latest +docker tag codebox shroominic/codebox:$VERSION +docker tag codebox shroominic/codebox:latest docker push shroominic/codebox:$VERSION docker push shroominic/codebox:latest From e114e517c706fa63b0d07a7314ea332bcd06c41b Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 30 Oct 2024 13:27:04 +0800 Subject: [PATCH 109/125] basic usage examples --- examples/async_example.py | 37 ++++++++++++++++++ examples/getting_started.py | 76 +++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 examples/async_example.py create mode 100644 examples/getting_started.py diff --git a/examples/async_example.py b/examples/async_example.py new file mode 100644 index 0000000..24e277c --- /dev/null +++ b/examples/async_example.py @@ -0,0 +1,37 @@ +from codeboxapi import CodeBox + +codebox = CodeBox() + + +async def async_examples(): + # 1. Async Code Execution + result = await codebox.aexec("print('Async Hello!')") + print(result.text) + + # 2. Async File Operations + await codebox.aupload("async_file.txt", b"Async content") + + downloaded = await codebox.adownload("async_file.txt") + print("File content:", downloaded.get_content()) + + # 3. All Sync Methods are also available Async + await codebox.ainstall("requests") + + # 4. Async Streaming + async for chunk in codebox.astream_exec(""" + for i in range(3): + print(f"Async chunk {i}") + import time + time.sleep(1) + """): + print(chunk.content, end="") + + # 5. Async Streaming Download + async for chunk in codebox.astream_download("async_file.txt"): + print(chunk.content) + + +if __name__ == "__main__": + import asyncio + + asyncio.run(async_examples()) diff --git a/examples/getting_started.py b/examples/getting_started.py new file mode 100644 index 0000000..b99392d --- /dev/null +++ b/examples/getting_started.py @@ -0,0 +1,76 @@ +from codeboxapi import CodeBox + +# Initialize CodeBox +codebox = CodeBox(api_key="local") # or get your API key at https://codeboxapi.com + +# Basic Examples +# ------------- + +# 1. Simple Code Execution +result = codebox.exec("print('Hello World!')") +print(result.text) # Output: Hello World! + +# 2. File Operations +# Upload a file +codebox.upload("example.txt", b"Hello from CodeBox!") + +# Download a file +downloaded = codebox.download("example.txt") +content = downloaded.get_content() # Returns b"Hello from CodeBox!" + +# List files +files = codebox.list_files() # Returns list[RemoteFile] + +# 3. Package Management +# Install packages +codebox.install("pandas") + +# List installed packages +packages = codebox.list_packages() + +# 4. Variable Management +# Execute code that creates variables +codebox.exec(""" +x = 42 +data = [1, 2, 3] +name = "Alice" +""") + +# Show all variables +variables = codebox.show_variables() +print(variables) # Shows dict with all variables and their values + +# 5. Plotting with Matplotlib +plot_code = """ +import matplotlib.pyplot as plt +plt.figure(figsize=(10, 5)) +plt.plot([1, 2, 3, 4], [1, 4, 2, 3]) +plt.title('My Plot') +plt.show() +""" +result = codebox.exec(plot_code) +# result.images will contain the plot as bytes + +# 6. Streaming Output +# Useful for long-running operations +for chunk in codebox.stream_exec(""" +for i in range(5): + print(f"Processing item {i}") + import time + time.sleep(1) +"""): + print(chunk.content, end="") + +# 7. Bash Commands +# Execute bash commands +codebox.exec("ls -la", kernel="bash") +codebox.exec("pwd", kernel="bash") + +# Create and run Python scripts via bash +codebox.exec("echo \"print('Running from file')\" > script.py", kernel="bash") +codebox.exec("python script.py", kernel="bash") + +# 8. Error Handling +result = codebox.exec("1/0") +if result.errors: + print("Error occurred:", result.errors[0]) From 0194d229e0d7edabc6730a07614a7ceeead23876 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 30 Oct 2024 13:27:12 +0800 Subject: [PATCH 110/125] fix async example --- examples/async_example.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/async_example.py b/examples/async_example.py index 24e277c..e9fcdd4 100644 --- a/examples/async_example.py +++ b/examples/async_example.py @@ -28,7 +28,8 @@ async def async_examples(): # 5. Async Streaming Download async for chunk in codebox.astream_download("async_file.txt"): - print(chunk.content) + assert isinstance(chunk, bytes) + print(chunk.decode()) if __name__ == "__main__": From d47f1b3286f52137c33870777194bfd397d5ee07 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 30 Oct 2024 13:37:07 +0800 Subject: [PATCH 111/125] fix example --- examples/plot_dataset.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/plot_dataset.py b/examples/plot_dataset.py index 9075993..e7fba20 100644 --- a/examples/plot_dataset.py +++ b/examples/plot_dataset.py @@ -12,10 +12,10 @@ ).content # upload the dataset to the codebox -o = codebox.upload("iris.csv", csv_bytes) +codebox.upload("iris.csv", csv_bytes) # dataset analysis code -file_path = Path("../../examples/assets/dataset_code.txt") +file_path = Path("examples/assets/dataset_code.txt") # run the code output = codebox.run(code=file_path) From d8e5d83985d8722580a3e148b87a66fa5d78a4e8 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 30 Oct 2024 13:37:14 +0800 Subject: [PATCH 112/125] rm example --- examples/docker.py | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 examples/docker.py diff --git a/examples/docker.py b/examples/docker.py deleted file mode 100644 index 0c6cf4b..0000000 --- a/examples/docker.py +++ /dev/null @@ -1,9 +0,0 @@ -from codeboxapi.docker import DockerBox - -codebox = DockerBox() - -assert codebox.healthcheck() - -r = codebox.exec("import matplotlib.pyplot as plt; plt.plot([1, 2, 3]); plt.show()") - -print(r.text) From 67debe4b366f34ee6a0b91c8a7f67d818949ca16 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 30 Oct 2024 13:42:58 +0800 Subject: [PATCH 113/125] parallel exec example with docker --- examples/docker_parallel_execution.py | 84 +++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 examples/docker_parallel_execution.py diff --git a/examples/docker_parallel_execution.py b/examples/docker_parallel_execution.py new file mode 100644 index 0000000..9ed911f --- /dev/null +++ b/examples/docker_parallel_execution.py @@ -0,0 +1,84 @@ +import asyncio +import time +from pathlib import Path + +from codeboxapi import CodeBox + + +async def train_model(codebox: CodeBox, data_split: int) -> dict: + """Train a model on a subset of data.""" + + file = Path("examples/assets/advertising.csv") + assert file.exists(), "Dataset file does not exist" + + # Upload dataset + await codebox.aupload(file.name, file.read_bytes()) + + # Install required packages + await codebox.ainstall("pandas") + await codebox.ainstall("scikit-learn") + + # Training code with different data splits + code = f""" +import pandas as pd +from sklearn.model_selection import train_test_split +from sklearn.linear_model import LinearRegression +from sklearn.metrics import mean_squared_error, r2_score + +# Load and prepare data +data = pd.read_csv('advertising.csv') +X = data[['TV', 'Radio', 'Newspaper']] +y = data['Sales'] + +# Split with different random states for different data subsets +X_train, X_test, y_train, y_test = train_test_split( + X, y, test_size=0.3, random_state={data_split} +) + +# Train model +model = LinearRegression() +model.fit(X_train, y_train) + +# Evaluate +y_pred = model.predict(X_test) +mse = mean_squared_error(y_test, y_pred) +r2 = r2_score(y_test, y_pred) + +print(f"Split {data_split}:") +print(f"MSE: {{mse:.4f}}") +print(f"R2: {{r2:.4f}}") +print(f"Coefficients: {{model.coef_.tolist()}}") +""" + result = await codebox.aexec(code) + return {"split": data_split, "output": result.text, "errors": result.errors} + + +async def main(): + # Create multiple Docker instances + num_parallel = 4 + codeboxes = [CodeBox(api_key="docker") for _ in range(num_parallel)] + + # Create tasks for different data splits + tasks = [] + for i, codebox in enumerate(codeboxes): + task = asyncio.create_task(train_model(codebox, i)) + tasks.append(task) + + # Execute and time the parallel processing + start_time = time.perf_counter() + results = await asyncio.gather(*tasks) + end_time = time.perf_counter() + + # Print results + print(f"\nParallel execution completed in {end_time - start_time:.2f} seconds\n") + for result in results: + if not result["errors"]: + print(f"Results for {result['split']}:") + print(result["output"]) + print("-" * 50) + else: + print(f"Error in split {result['split']}:", result["errors"]) + + +if __name__ == "__main__": + asyncio.run(main()) From eec6072ffad0a74e3924e9ece0656e66c3bcb675 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 30 Oct 2024 13:46:23 +0800 Subject: [PATCH 114/125] fix sub inits --- src/codeboxapi/codebox.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/codeboxapi/codebox.py b/src/codeboxapi/codebox.py index 5cce1d6..770f996 100644 --- a/src/codeboxapi/codebox.py +++ b/src/codeboxapi/codebox.py @@ -60,12 +60,11 @@ def __new__( api_key = api_key or os.getenv("CODEBOX_API_KEY", "local") factory_id = factory_id or os.getenv("CODEBOX_FACTORY_ID", "default") if api_key == "local": - return super().__new__(import_module("codeboxapi.local").LocalBox) + return import_module("codeboxapi.local").LocalBox() if api_key == "docker": - return super().__new__(import_module("codeboxapi.docker").DockerBox) - - return super().__new__(import_module("codeboxapi.remote").RemoteBox) + return import_module("codeboxapi.docker").DockerBox() + return import_module("codeboxapi.remote").RemoteBox() def __init__( self, From b32c2c0e5c67ae7be44936f0d37e2610fce657bd Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 30 Oct 2024 13:56:59 +0800 Subject: [PATCH 115/125] update examples --- .../{big_upload.py => big_upload_from_url.py} | 12 +++--- examples/{file_io.py => file_conversion.py} | 10 ++--- examples/parallel.py | 19 ---------- examples/plot_dataset.py | 38 ++++++------------- 4 files changed, 22 insertions(+), 57 deletions(-) rename examples/{big_upload.py => big_upload_from_url.py} (71%) rename examples/{file_io.py => file_conversion.py} (80%) delete mode 100644 examples/parallel.py diff --git a/examples/big_upload.py b/examples/big_upload_from_url.py similarity index 71% rename from examples/big_upload.py rename to examples/big_upload_from_url.py index 8a73fe3..750a1eb 100644 --- a/examples/big_upload.py +++ b/examples/big_upload_from_url.py @@ -1,8 +1,8 @@ from codeboxapi import CodeBox -def url_upload(codebox, url: str) -> None: - codebox.run( +def url_upload(codebox: CodeBox, url: str) -> None: + codebox.exec( """ import requests @@ -16,7 +16,7 @@ def download_file_from_url(url: str) -> None: file.write(chunk) """ ) - print(codebox.run(f"download_file_from_url('{url}')")) + print(codebox.exec(f"download_file_from_url('{url}')")) codebox = CodeBox() @@ -33,6 +33,6 @@ def download_file_from_url(url: str) -> None: ) print(codebox.list_files()) -codebox.run("import os") -print(codebox.run("print(os.listdir())")) -print(codebox.run("print([(f, os.path.getsize(f)) for f in os.listdir('.')])")) +codebox.exec("import os") +print(codebox.exec("print(os.listdir())")) +print(codebox.exec("print([(f, os.path.getsize(f)) for f in os.listdir('.')])")) diff --git a/examples/file_io.py b/examples/file_conversion.py similarity index 80% rename from examples/file_io.py rename to examples/file_conversion.py index 9026e1a..10a75c0 100644 --- a/examples/file_io.py +++ b/examples/file_conversion.py @@ -14,7 +14,7 @@ codebox.install("openpyxl") # convert dataset csv to excel -output = codebox.run( +output = codebox.exec( "import pandas as pd\n\n" "df = pd.read_csv('iris.csv', header=None)\n\n" "df.to_excel('iris.xlsx', index=False)\n" @@ -22,12 +22,12 @@ ) # check output type -if output.type == "image/png": +if output.images: print("This should not happen") -elif output.type == "error": - print("Error: ", output.content) +elif output.errors: + print("Error: ", output.errors) else: # all files inside the codebox for file in codebox.list_files(): print("File: ", file.path) - print("Content: ", file.get_content()) + print("File Size: ", file.get_size()) diff --git a/examples/parallel.py b/examples/parallel.py deleted file mode 100644 index a21ed40..0000000 --- a/examples/parallel.py +++ /dev/null @@ -1,19 +0,0 @@ -import asyncio - -from codeboxapi import CodeBox - - -async def main(): - await asyncio.gather(*(spawn_codebox() for _ in range(10))) - - -async def spawn_codebox(): - codebox = CodeBox(api_key="local") - await codebox.arun("a = 'Hello World!'") - a = await codebox.arun("a") - assert a == "Hello World!" - print("Success!") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/examples/plot_dataset.py b/examples/plot_dataset.py index e7fba20..a293cf8 100644 --- a/examples/plot_dataset.py +++ b/examples/plot_dataset.py @@ -1,50 +1,34 @@ -import os +import base64 +from io import BytesIO from pathlib import Path import httpx from codeboxapi import CodeBox +from PIL import Image codebox = CodeBox(api_key="local") # download the iris dataset -csv_bytes = httpx.get( +iris_csv_bytes = httpx.get( "https://archive.ics.uci.edu/" "ml/machine-learning-databases/iris/iris.data" ).content # upload the dataset to the codebox -codebox.upload("iris.csv", csv_bytes) +codebox.upload("iris.csv", iris_csv_bytes) # dataset analysis code file_path = Path("examples/assets/dataset_code.txt") # run the code -output = codebox.run(code=file_path) -print(output) -print(output.type) - -if output.type == "image" and os.environ.get("CODEBOX_TEST") == "False": - try: - from PIL import Image # type: ignore - except ImportError: - print( - "Please install it with " - '`pip install "codeboxapi[image_support]"`' - " to display images." - ) - exit(1) - - # Convert the image content ( bytes) into an image - import base64 - from io import BytesIO - - img_bytes = base64.b64decode(output.content) +output = codebox.exec(file_path) + +if output.images: + img_bytes = base64.b64decode(output.images[0]) img_buffer = BytesIO(img_bytes) # Display the image img = Image.open(img_buffer) img.show() -elif output.type == "error": - # error output - print("Error:") - print(output.content) +elif output.errors: + print("Error:", output.errors) From 274476605e4882ae02b7e184189c876c2d8abd12 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 30 Oct 2024 14:16:45 +0800 Subject: [PATCH 116/125] =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=94=AC=20imp?= =?UTF-8?q?rove=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/conftest.py | 20 +++++ tests/parametric_test.py | 175 --------------------------------------- tests/test_v01.py | 30 +------ tests/test_v02.py | 30 +++---- 4 files changed, 39 insertions(+), 216 deletions(-) create mode 100644 tests/conftest.py delete mode 100644 tests/parametric_test.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..b773253 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,20 @@ +import os + +import pytest +from codeboxapi import CodeBox + +LOCALBOX = CodeBox(api_key="local") + + +@pytest.fixture( + scope="session", + params=["local", "docker", os.getenv("CODEBOX_API_KEY")], +) +def codebox(request: pytest.FixtureRequest) -> CodeBox: + if request.param == "local": + return LOCALBOX + + if request.param == "docker" and os.system("docker ps > /dev/null 2>&1") != 0: + pytest.skip("Docker is not running") + + return CodeBox(api_key=request.param) diff --git a/tests/parametric_test.py b/tests/parametric_test.py deleted file mode 100644 index ba6efc4..0000000 --- a/tests/parametric_test.py +++ /dev/null @@ -1,175 +0,0 @@ -import asyncio -import time -from typing import Callable - -import pytest -from codeboxapi import CodeBox, RemoteFile -from codeboxapi.types import CodeBoxFile, CodeBoxOutput - -AssertFunctionType = Callable[[CodeBoxOutput, list[RemoteFile]], bool] - -code_1 = """ -import pandas as pd -# Read the CSV file -df = pd.read_csv('iris.csv') - -# Save the DataFrame to an Excel file -df.to_excel('iris.xlsx', index=False) -""" - - -def assert_function_1(_, files): - return any(".xlsx" in file.name for file in files) - - -code_2 = """ -import pandas as pd -from sklearn.model_selection import train_test_split -from sklearn.linear_model import LinearRegression -from sklearn.metrics import mean_squared_error - -# Load the dataset -data = pd.read_csv('advertising.csv') - -# Split the data into features (X) and target (y) -X = data[['TV']] -y = data['Sales'] - -# Split the data into train and test sets -X_train, X_test, y_train, y_test = train_test_split( - X, y, test_size=0.3, random_state=42 -) - -# Train the model -model = LinearRegression() -model.fit(X_train, y_train) - -# Make predictions on the test set -y_pred = model.predict(X_test) - -# Calculate Mean Squared Error -mse = mean_squared_error(y_test, y_pred) - -mse -""" - - -def assert_function_2(output, _): - # np.float64(5.179525402166653)\n - if "np.float64" in output.content: - return 4.0 <= float(output.content.split("(")[1].split(")")[0]) <= 7.0 - return 4.0 <= float(output.content) <= 7.0 - - -# Helper function to build parameters with defaults -def param(code, assert_function, files=[], num_samples=2, local=False, packages=[]): - return ( - code, - assert_function, - files, - num_samples, - local, - packages, - ) - - -@pytest.mark.asyncio -@pytest.mark.parametrize( - "code, assert_function, files, num_samples, local, packages", - [ - param( - code_1, - assert_function_1, - files=[CodeBoxFile.from_path("examples/assets/iris.csv")], - ), - param( - code_1, - assert_function_1, - files=[CodeBoxFile.from_path("examples/assets/iris.csv")], - num_samples=1, - local=True, - packages=["pandas", "openpyxl"], - ), - param( - code_2, - assert_function_2, - files=[CodeBoxFile.from_path("examples/assets/advertising.csv")], - ), - param( - code_2, - assert_function_2, - files=[CodeBoxFile.from_path("examples/assets/advertising.csv")], - num_samples=10, - ), # For remote CodeBox, the time taken to run 10 samples - # should be around the same as 2 samples (the above case). - param( - code_2, - assert_function_2, - files=[CodeBoxFile.from_path("examples/assets/advertising.csv")], - num_samples=1, - local=True, - packages=["pandas", "scikit-learn"], - ), - ], -) -async def test_boxes_async( - code: str, - assert_function: AssertFunctionType, - files: list[CodeBoxFile], - num_samples: int, - local: bool, - packages: list[str], - capsys: pytest.CaptureFixture, -) -> None: - codeboxes = [ - CodeBox(api_key="local" if local else None) for _ in range(num_samples) - ] - - start_time = time.perf_counter() - tasks = [ - run_async(codebox, code, assert_function, files, packages) - for codebox in codeboxes - ] - results = await asyncio.gather(*tasks) - end_time = time.perf_counter() - with capsys.disabled(): - print(f"Time taken: {end_time - start_time:.2f} seconds") - - assert all(results), "Failed to run codeboxes" - - -async def run_async( - codebox: CodeBox, - code: str, - assert_function: AssertFunctionType, - files: list[CodeBoxFile], - packages: list[str], -) -> bool: - try: - assert await codebox.astart() == "started" - - assert await codebox.astatus() == "running" - - orginal_files = await codebox.alist_files() - for file in files: - assert file.content is not None - await codebox.aupload(file.name, file.content) - - codebox_files = await codebox.alist_files() - assert set( - [file.name for file in files] + [file.name for file in orginal_files] - ) == set([file.name for file in codebox_files]) - - assert all([ - package_name in str(await codebox.ainstall(package_name)) - for package_name in packages - ]) - - output: CodeBoxOutput = await codebox.arun(code) - codebox_files_output = await codebox.alist_files() - assert assert_function(output, codebox_files_output) - - finally: - assert await codebox.astop() == "stopped" - - return True diff --git a/tests/test_v01.py b/tests/test_v01.py index 845ee72..dfe960f 100644 --- a/tests/test_v01.py +++ b/tests/test_v01.py @@ -1,22 +1,8 @@ -import asyncio -import os - +import pytest from codeboxapi import CodeBox -def test_codebox(): - codebox = CodeBox(api_key=os.getenv("CODEBOX_API_KEY")) - assert run_sync(codebox), "Failed to run sync codebox remotely" - assert asyncio.run(run_async(codebox)), "Failed to run async codebox remotely" - - -def test_localbox(): - codebox = CodeBox(api_key="local") - assert run_sync(codebox), "Failed to run sync codebox locally" - assert asyncio.run(run_async(codebox)), "Failed to run async codebox locally" - - -def run_sync(codebox: CodeBox) -> bool: +def test_sync(codebox: CodeBox) -> None: try: assert codebox.start() == "started" print("Started") @@ -59,10 +45,9 @@ def run_sync(codebox: CodeBox) -> bool: assert codebox.stop() == "stopped" print("Stopped") - return True - -async def run_async(codebox: CodeBox) -> bool: +@pytest.mark.asyncio +async def test_async(codebox: CodeBox) -> None: try: assert await codebox.astart() == "started" print("Started") @@ -107,10 +92,3 @@ async def run_async(codebox: CodeBox) -> bool: finally: assert await codebox.astop() == "stopped" print("Stopped") - - return True - - -if __name__ == "__main__": - test_codebox() - test_localbox() diff --git a/tests/test_v02.py b/tests/test_v02.py index ce9f9a9..4738f12 100644 --- a/tests/test_v02.py +++ b/tests/test_v02.py @@ -1,24 +1,9 @@ -import os import time import pytest from codeboxapi import CodeBox, ExecChunk, ExecResult, RemoteFile -@pytest.fixture( - scope="session", - params=[ - "local", - "docker", - os.getenv("CODEBOX_API_KEY"), - ], -) -def codebox(request): - if request.param == "docker" and os.system("docker ps > /dev/null 2>&1") != 0: - pytest.skip("Docker is not running") - return CodeBox(api_key=request.param) # api_key=request.param) - - def test_sync_codebox_lifecycle(codebox: CodeBox): assert codebox.healthcheck() == "healthy", "CodeBox should be healthy" @@ -248,6 +233,7 @@ async def test_async_stream_exec(codebox: CodeBox): chunks[i][1] < chunks[i + 1][1] for i in range(len(chunks) - 1) ), "Chunks should arrive with delay (ipython)" # Verify delay is approximately 0.01s + print([abs(chunks[i + 1][1] - chunks[i][1] - 0.01) for i in range(len(chunks) - 1)]) assert all( abs(chunks[i + 1][1] - chunks[i][1] - 0.01) < 0.005 for i in range(len(chunks) - 1) @@ -324,5 +310,19 @@ async def test_async_bash_commands(codebox: CodeBox): assert result.text.strip() == "Hello!", "Execution result should be 'Hello!'" +def test_local_box_singleton(): + from codeboxapi.local import LocalBox + + with pytest.raises(RuntimeError) as exc_info: + _ = LocalBox() + + assert "Only one LocalBox instance can exist at a time" in str(exc_info.value) + + with pytest.raises(RuntimeError) as exc_info: + _ = CodeBox(api_key="local") + + assert "codeboxapi.com" in str(exc_info.value) + + if __name__ == "__main__": pytest.main([__file__]) From a45f814a580a6492e204cf11850eccb44c1ebc05 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 30 Oct 2024 14:16:59 +0800 Subject: [PATCH 117/125] force singelton for localbox --- src/codeboxapi/local.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/codeboxapi/local.py b/src/codeboxapi/local.py index abdd85c..515a84c 100644 --- a/src/codeboxapi/local.py +++ b/src/codeboxapi/local.py @@ -32,11 +32,27 @@ class LocalBox(CodeBox): """ LocalBox is a CodeBox implementation that runs code locally using IPython. This is useful for testing and development. + + Only one instance can exist at a time. For parallel execution, use: + - DockerBox for local parallel execution + - Get an API key at codeboxapi.com for hosted parallel execution """ + _instance: t.Optional["LocalBox"] = None + def __new__(cls, *args, **kwargs) -> "LocalBox": - # This is a hack to ignore the CodeBox.__new__ factory method. - return object.__new__(cls) + if cls._instance: + raise RuntimeError( + "Only one LocalBox instance can exist at a time.\n" + "For parallel execution use:\n" + "- Use DockerBox for local parallel execution\n" + "- Get an API key at https://codeboxapi.com for secure remote execution" + ) + + # This is a hack to ignore the CodeBox.__new__ factory method + instance = object.__new__(cls) + cls._instance = instance + return instance def __init__( self, From 3169dbd98c37d777dd1054308529a0f8ef440ee5 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 30 Oct 2024 14:23:49 +0800 Subject: [PATCH 118/125] add chunk timing example --- examples/stream_chunk_timing.py | 44 +++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 examples/stream_chunk_timing.py diff --git a/examples/stream_chunk_timing.py b/examples/stream_chunk_timing.py new file mode 100644 index 0000000..7a08422 --- /dev/null +++ b/examples/stream_chunk_timing.py @@ -0,0 +1,44 @@ +import asyncio +import time + +from codeboxapi import CodeBox, ExecChunk + + +def sync_stream_exec(cb: CodeBox) -> None: + chunks: list[tuple[ExecChunk, float]] = [] + t0 = time.perf_counter() + for chunk in cb.stream_exec( + "import time;\nfor i in range(3): time.sleep(1); print(i)" + ): + chunks.append((chunk, time.perf_counter() - t0)) + + for chunk, t in chunks: + print(f"{t:.5f}: {chunk}") + + +async def async_stream_exec(cb: CodeBox) -> None: + chunks: list[tuple[ExecChunk, float]] = [] + t0 = time.perf_counter() + async for chunk in cb.astream_exec( + "import time;\nfor i in range(3): time.sleep(1); print(i)" + ): + chunks.append((chunk, time.perf_counter() - t0)) + + for chunk, t in chunks: + print(f"{t:.5f}: {chunk}") + + +print("remote") +cb = CodeBox() +sync_stream_exec(cb) +asyncio.run(async_stream_exec(cb)) + +print("local") +local = CodeBox(api_key="local") +sync_stream_exec(local) +asyncio.run(async_stream_exec(local)) + +print("docker") +docker = CodeBox(api_key="docker") +sync_stream_exec(docker) +asyncio.run(async_stream_exec(docker)) From 901b32ad5ad0368c5124c03d5750a07a9ddff5e5 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 30 Oct 2024 15:00:35 +0800 Subject: [PATCH 119/125] fix chunk timing for remote box --- tests/test_v02.py | 117 ++++++++++++++++++++++++++-------------------- 1 file changed, 67 insertions(+), 50 deletions(-) diff --git a/tests/test_v02.py b/tests/test_v02.py index 4738f12..2701dc2 100644 --- a/tests/test_v02.py +++ b/tests/test_v02.py @@ -145,129 +145,146 @@ async def test_async_list_operations(codebox: CodeBox): def test_sync_stream_exec(codebox: CodeBox): chunks: list[tuple[ExecChunk, float]] = [] t0 = time.perf_counter() + sleep = 0.5 + if codebox.api_key == "local": + sleep = 0.01 + if codebox.api_key == "docker": + sleep = 0.05 for chunk in codebox.stream_exec( - "import time;\nfor i in range(3): time.sleep(0.01); print(i)" + f"import time;\nfor i in range(3): time.sleep({sleep}); print(i)" ): chunks.append((chunk, time.perf_counter() - t0)) - assert ( - len(chunks) == 3 - ), "iterating over stream_exec should produce 3 chunks (ipython)" + assert len(chunks) == 3, "iterating over stream_exec should produce 3 chunks" assert all( isinstance(chunk[0], ExecChunk) for chunk in chunks - ), "All items should be ExecChunk instances (ipython)" + ), "All items should be ExecChunk instances" assert all( chunk[0].type == "txt" for chunk in chunks - ), "All chunks should be of type 'txt' (ipython)" + ), "All chunks should be of type 'txt'" assert [chunk[0].content.strip() for chunk in chunks] == [ "0", "1", "2", - ], "Chunks should contain correct content (ipython)" + ], "Chunks should contain correct content" # Verify chunks arrive with delay assert all( chunks[i][1] < chunks[i + 1][1] for i in range(len(chunks) - 1) - ), "Chunks should arrive with delay (ipython)" - # Verify delay is approximately 0.01s - assert all( - abs(chunks[i + 1][1] - chunks[i][1] - 0.01) < 0.005 - for i in range(len(chunks) - 1) - ), "Delay between chunks should be approximately 0.01s (ipython)" + ), "Chunks should arrive with delay" + # Verify chunks don't arrive all at once + assert any( + chunks[i + 1][1] - chunks[i][1] > 0.005 for i in range(len(chunks) - 1) + ), "At least some chunks should have noticeable delay between them" + +@pytest.mark.asyncio +async def test_sync_stream_exec_ipython(codebox: CodeBox): chunks = [] t0 = time.perf_counter() + sleep = 0.5 + if codebox.api_key == "local": + sleep = 0.01 + if codebox.api_key == "docker": + sleep = 0.05 for chunk in codebox.stream_exec( - "python -u -c 'import time\nfor i in range(3): time.sleep(0.01); print(i)'", + f"python -u -c 'import time\nfor i in range(3): time.sleep({sleep}); print(i)'", kernel="bash", ): chunks.append((chunk, time.perf_counter() - t0)) - assert len(chunks) == 3, "iterating over stream_exec should produce 3 chunks (bash)" + assert len(chunks) == 3, "iterating over stream_exec should produce 3 chunks" assert all( isinstance(chunk[0], ExecChunk) for chunk in chunks - ), "All items should be ExecChunk instances (bash)" + ), "All items should be ExecChunk instances" assert all( chunk[0].type == "txt" for chunk in chunks - ), "All chunks should be of type 'txt' (bash)" + ), "All chunks should be of type 'txt'" assert [chunk[0].content.strip() for chunk in chunks] == [ "0", "1", "2", - ], "Chunks should contain correct content (bash)" + ], "Chunks should contain correct content" # Verify chunks arrive with delay assert all( chunks[i][1] < chunks[i + 1][1] for i in range(len(chunks) - 1) - ), "Chunks should arrive with delay (bash)" - # Verify delay is approximately 0.01s - assert all( - abs(chunks[i + 1][1] - chunks[i][1] - 0.01) < 0.005 - for i in range(len(chunks) - 1) - ), "Delay between chunks should be approximately 0.01s (bash)" + ), "Chunks should arrive with delay" + # Verify chunks don't arrive all at once + assert any( + chunks[i + 1][1] - chunks[i][1] > 0.005 for i in range(len(chunks) - 1) + ), "At least some chunks should have noticeable delay between them" @pytest.mark.asyncio -async def test_async_stream_exec(codebox: CodeBox): +async def test_async_stream_exec_ipython(codebox: CodeBox): chunks: list[tuple[ExecChunk, float]] = [] t0 = time.perf_counter() + sleep = 0.5 + if codebox.api_key == "local": + sleep = 0.01 + if codebox.api_key == "docker": + sleep = 0.05 async for chunk in codebox.astream_exec( - "import time;\nfor i in range(3): time.sleep(0.01); print(i)" + f"import time;\nfor i in range(3): time.sleep({sleep}); print(i)", ): chunks.append((chunk, time.perf_counter() - t0)) - assert ( - len(chunks) == 3 - ), "iterating over stream_exec should produce 3 chunks (ipython)" + assert len(chunks) == 3, "iterating over stream_exec should produce 3 chunks" assert all( isinstance(chunk[0], ExecChunk) for chunk in chunks - ), "All items should be ExecChunk instances (ipython)" + ), "All items should be ExecChunk instances" assert all( chunk[0].type == "txt" for chunk in chunks - ), "All chunks should be of type 'txt' (ipython)" + ), "All chunks should be of type 'txt'" assert [chunk[0].content.strip() for chunk in chunks] == [ "0", "1", "2", - ], "Chunks should contain correct content (ipython)" + ], "Chunks should contain correct content" # Verify chunks arrive with delay assert all( chunks[i][1] < chunks[i + 1][1] for i in range(len(chunks) - 1) - ), "Chunks should arrive with delay (ipython)" - # Verify delay is approximately 0.01s - print([abs(chunks[i + 1][1] - chunks[i][1] - 0.01) for i in range(len(chunks) - 1)]) - assert all( - abs(chunks[i + 1][1] - chunks[i][1] - 0.01) < 0.005 - for i in range(len(chunks) - 1) - ), "Delay between chunks should be approximately 0.01s (ipython)" + ), "Chunks should arrive with delay" + # Verify chunks don't arrive all at once + assert any( + chunks[i + 1][1] - chunks[i][1] > 0.005 for i in range(len(chunks) - 1) + ), "At least some chunks should have noticeable delay between them" + +@pytest.mark.asyncio +async def test_async_stream_exec_bash(codebox: CodeBox): chunks = [] t0 = time.perf_counter() + sleep = 0.5 + if codebox.api_key == "local": + sleep = 0.01 + if codebox.api_key == "docker": + sleep = 0.05 async for chunk in codebox.astream_exec( - "python -u -c 'import time\nfor i in range(3): time.sleep(0.01); print(i)'", + f"python -u -c 'import time\nfor i in range(3): time.sleep({sleep}); print(i)'", kernel="bash", ): chunks.append((chunk, time.perf_counter() - t0)) - assert len(chunks) == 3, "iterating over stream_exec should produce 3 chunks (bash)" + assert len(chunks) == 3, "iterating over stream_exec should produce 3 chunks" assert all( isinstance(chunk[0], ExecChunk) for chunk in chunks - ), "All items should be ExecChunk instances (bash)" + ), "All items should be ExecChunk instances" assert all( chunk[0].type == "txt" for chunk in chunks - ), "All chunks should be of type 'txt' (bash)" + ), "All chunks should be of type 'txt'" assert [chunk[0].content.strip() for chunk in chunks] == [ "0", "1", "2", - ], "Chunks should contain correct content (bash)" + ], "Chunks should contain correct content" # Verify chunks arrive with delay assert all( chunks[i][1] < chunks[i + 1][1] for i in range(len(chunks) - 1) - ), "Chunks should arrive with delay (bash)" - # Verify delay is approximately 0.01s - assert all( - abs(chunks[i + 1][1] - chunks[i][1] - 0.01) < 0.005 - for i in range(len(chunks) - 1) - ), "Delay between chunks should be approximately 0.01s (bash)" + ), "Chunks should arrive with delay" + # Verify chunks don't arrive all at once + assert any( + chunks[i + 1][1] - chunks[i][1] > 0.005 for i in range(len(chunks) - 1) + ), "At least some chunks should have noticeable delay between them" def test_sync_error_handling(codebox: CodeBox): From 3cb7c53fb64528401a2167bc1c5f7533f16be424 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 30 Oct 2024 15:01:54 +0800 Subject: [PATCH 120/125] rm \n --- src/codeboxapi/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/codeboxapi/api.py b/src/codeboxapi/api.py index 0b69b02..6c4c559 100644 --- a/src/codeboxapi/api.py +++ b/src/codeboxapi/api.py @@ -53,7 +53,7 @@ async def event_stream() -> AsyncGenerator[str, None]: async for chunk in codebox.astream_exec( exec.code, exec.kernel, exec.timeout, exec.cwd ): # protocol is content - yield f"<{chunk.type}>{chunk.content}\n" + yield f"<{chunk.type}>{chunk.content}" return StreamingResponse(event_stream()) From 59ea8ee8292230a90adc53253b086408123f894e Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 30 Oct 2024 15:34:57 +0800 Subject: [PATCH 121/125] fix print formatting in example --- examples/getting_started.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/examples/getting_started.py b/examples/getting_started.py index b99392d..419719d 100644 --- a/examples/getting_started.py +++ b/examples/getting_started.py @@ -17,9 +17,11 @@ # Download a file downloaded = codebox.download("example.txt") content = downloaded.get_content() # Returns b"Hello from CodeBox!" +print("Content:\n", content, sep="") # List files -files = codebox.list_files() # Returns list[RemoteFile] +files = codebox.list_files() +print("\nFiles:\n", "\n".join(f.__repr__() for f in files), sep="") # 3. Package Management # Install packages @@ -27,6 +29,7 @@ # List installed packages packages = codebox.list_packages() +print("\nFirst 10 packages:\n", "\n".join(packages[:10]), sep="") # 4. Variable Management # Execute code that creates variables @@ -38,7 +41,7 @@ # Show all variables variables = codebox.show_variables() -print(variables) # Shows dict with all variables and their values +print("\nVariables:\n", "\n".join(f"{k}={v}" for k, v in variables.items()), sep="") # 5. Plotting with Matplotlib plot_code = """ @@ -59,6 +62,8 @@ import time time.sleep(1) """): + # will not print when using "local" as api_key + # due to stdout being captured in the background print(chunk.content, end="") # 7. Bash Commands @@ -73,4 +78,4 @@ # 8. Error Handling result = codebox.exec("1/0") if result.errors: - print("Error occurred:", result.errors[0]) + print("\nError occurred:", result.errors[0]) From 0e10acf142c4125a208de5c5e7f58d698b5cbb1e Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 30 Oct 2024 15:35:06 +0800 Subject: [PATCH 122/125] add str repr --- src/codeboxapi/types.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/codeboxapi/types.py b/src/codeboxapi/types.py index 38492ba..0350564 100644 --- a/src/codeboxapi/types.py +++ b/src/codeboxapi/types.py @@ -49,6 +49,14 @@ async def asave(self, local_path: str) -> None: async for chunk in self.remote.astream_download(self.path): await f.write(chunk) + def __str__(self) -> str: + return self.name + + def __repr__(self) -> str: + if self._size is None: + return f"RemoteFile({self.path})" + return f"RemoteFile({self.path}, {self._size} bytes)" + @dataclass class ExecChunk: From b7b56519355d68081958596a98de88335f4b6ef4 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 30 Oct 2024 15:35:22 +0800 Subject: [PATCH 123/125] fix output formatting of extra methods --- src/codeboxapi/codebox.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/codeboxapi/codebox.py b/src/codeboxapi/codebox.py index 770f996..32d36ed 100644 --- a/src/codeboxapi/codebox.py +++ b/src/codeboxapi/codebox.py @@ -211,11 +211,18 @@ def _parse_size(self, size_str: str) -> int: return -1 async def alist_packages(self) -> list[str]: - return (await self.aexec("uv pip list", kernel="bash")).text.splitlines() + return ( + await self.aexec( + "uv pip list | tail -n +3 | cut -d ' ' -f 1", + kernel="bash", + ) + ).text.splitlines() async def ashow_variables(self) -> dict[str, str]: - vars = [line.strip() for line in (await self.aexec("%who")).text.strip()] - return {v: (await self.aexec(v)).text for v in vars} + vars = [ + line.strip() for line in (await self.aexec("%who")).text.strip().split() + ] + return {v: (await self.aexec(f"print({v}, end='')")).text for v in vars} async def arestart(self) -> None: """Restart the Jupyter kernel""" From 1cce923c2473a3782790b1ecabd13c445a11de22 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 30 Oct 2024 15:35:36 +0800 Subject: [PATCH 124/125] fix test chunk sep sleep time --- tests/test_v02.py | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/tests/test_v02.py b/tests/test_v02.py index 2701dc2..bc6a37a 100644 --- a/tests/test_v02.py +++ b/tests/test_v02.py @@ -145,11 +145,7 @@ async def test_async_list_operations(codebox: CodeBox): def test_sync_stream_exec(codebox: CodeBox): chunks: list[tuple[ExecChunk, float]] = [] t0 = time.perf_counter() - sleep = 0.5 - if codebox.api_key == "local": - sleep = 0.01 - if codebox.api_key == "docker": - sleep = 0.05 + sleep = 0.01 if codebox.api_key == "local" else 1 for chunk in codebox.stream_exec( f"import time;\nfor i in range(3): time.sleep({sleep}); print(i)" ): @@ -181,11 +177,7 @@ def test_sync_stream_exec(codebox: CodeBox): async def test_sync_stream_exec_ipython(codebox: CodeBox): chunks = [] t0 = time.perf_counter() - sleep = 0.5 - if codebox.api_key == "local": - sleep = 0.01 - if codebox.api_key == "docker": - sleep = 0.05 + sleep = 0.01 if codebox.api_key == "local" else 1 for chunk in codebox.stream_exec( f"python -u -c 'import time\nfor i in range(3): time.sleep({sleep}); print(i)'", kernel="bash", @@ -218,11 +210,7 @@ async def test_sync_stream_exec_ipython(codebox: CodeBox): async def test_async_stream_exec_ipython(codebox: CodeBox): chunks: list[tuple[ExecChunk, float]] = [] t0 = time.perf_counter() - sleep = 0.5 - if codebox.api_key == "local": - sleep = 0.01 - if codebox.api_key == "docker": - sleep = 0.05 + sleep = 0.01 if codebox.api_key == "local" else 1 async for chunk in codebox.astream_exec( f"import time;\nfor i in range(3): time.sleep({sleep}); print(i)", ): @@ -254,11 +242,7 @@ async def test_async_stream_exec_ipython(codebox: CodeBox): async def test_async_stream_exec_bash(codebox: CodeBox): chunks = [] t0 = time.perf_counter() - sleep = 0.5 - if codebox.api_key == "local": - sleep = 0.01 - if codebox.api_key == "docker": - sleep = 0.05 + sleep = 0.01 if codebox.api_key == "local" else 1 async for chunk in codebox.astream_exec( f"python -u -c 'import time\nfor i in range(3): time.sleep({sleep}); print(i)'", kernel="bash", From 341394cc0777f262b4b9f0cd2ed6cffe05b9a6a4 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 30 Oct 2024 15:51:52 +0800 Subject: [PATCH 125/125] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20fix=20CICD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/code-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code-check.yml b/.github/workflows/code-check.yml index 454eed9..78152b5 100644 --- a/.github/workflows/code-check.yml +++ b/.github/workflows/code-check.yml @@ -19,7 +19,7 @@ jobs: - name: Sync rye run: rye sync - name: Run ruff - run: rye run ruff + run: rye run ruff check - name: Run tests env: CODEBOX_API_KEY: ${{ secrets.CODEBOX_API_KEY }}