diff --git a/poetry.lock b/poetry.lock index fd77d36..1080843 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,16 @@ # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +[[package]] +name = "aiofiles" +version = "23.2.1" +description = "File support for asyncio." +optional = false +python-versions = ">=3.7" +files = [ + {file = "aiofiles-23.2.1-py3-none-any.whl", hash = "sha256:19297512c647d4b27a2cf7c34caa7e405c0d60b5560618a29a9fe027b18b0107"}, + {file = "aiofiles-23.2.1.tar.gz", hash = "sha256:84ec2218d8419404abcb9f0c02df3f34c6e0a68ed41072acfb1cef5cbc29051a"}, +] + [[package]] name = "asttokens" version = "2.4.1" @@ -54,70 +65,6 @@ files = [ {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, ] -[[package]] -name = "cffi" -version = "1.16.0" -description = "Foreign Function Interface for Python calling C code." -optional = false -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 = "charset-normalizer" version = "3.3.2" @@ -438,13 +385,13 @@ typing = ["typing-extensions (>=4.8)"] [[package]] name = "flask" -version = "3.0.0" +version = "3.0.1" description = "A simple framework for building complex web applications." optional = false python-versions = ">=3.8" files = [ - {file = "flask-3.0.0-py3-none-any.whl", hash = "sha256:21128f47e4e3b9d597a3e8521a329bf56909b690fcc3fa3e477725aa81367638"}, - {file = "flask-3.0.0.tar.gz", hash = "sha256:cfadcdb638b609361d29ec22360d6070a77d7463dcb3ab08d2c2f2f168845f58"}, + {file = "flask-3.0.1-py3-none-any.whl", hash = "sha256:ca631a507f6dfe6c278ae20112cea3ff54ff2216390bf8880f6b035a5354af13"}, + {file = "flask-3.0.1.tar.gz", hash = "sha256:6489f51bb3666def6f314e15f19d50a1869a19ae0e8c9a3641ffe66c77d42403"}, ] [package.dependencies] @@ -458,38 +405,6 @@ Werkzeug = ">=3.0.0" async = ["asgiref (>=3.2)"] dotenv = ["python-dotenv"] -[[package]] -name = "flask-cors" -version = "4.0.0" -description = "A Flask extension adding a decorator for CORS support" -optional = false -python-versions = "*" -files = [ - {file = "Flask-Cors-4.0.0.tar.gz", hash = "sha256:f268522fcb2f73e2ecdde1ef45e2fd5c71cc48fe03cffb4b441c6d1b40684eb0"}, - {file = "Flask_Cors-4.0.0-py2.py3-none-any.whl", hash = "sha256:bc3492bfd6368d27cfe79c7821df5a8a319e1a6d5eab277a3794be19bdc51783"}, -] - -[package.dependencies] -Flask = ">=0.9" - -[[package]] -name = "flask-sock" -version = "0.7.0" -description = "WebSocket support for Flask" -optional = false -python-versions = ">=3.6" -files = [ - {file = "flask-sock-0.7.0.tar.gz", hash = "sha256:e023b578284195a443b8d8bdb4469e6a6acf694b89aeb51315b1a34fcf427b7d"}, - {file = "flask_sock-0.7.0-py3-none-any.whl", hash = "sha256:caac4d679392aaf010d02fabcf73d52019f5bdaf1c9c131ec5a428cb3491204a"}, -] - -[package.dependencies] -flask = ">=2" -simple-websocket = ">=0.5.1" - -[package.extras] -docs = ["sphinx"] - [[package]] name = "fonttools" version = "4.47.2" @@ -590,68 +505,6 @@ smb = ["smbprotocol"] ssh = ["paramiko"] tqdm = ["tqdm"] -[[package]] -name = "gevent" -version = "23.9.1" -description = "Coroutine-based network library" -optional = false -python-versions = ">=3.8" -files = [ - {file = "gevent-23.9.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:a3c5e9b1f766a7a64833334a18539a362fb563f6c4682f9634dea72cbe24f771"}, - {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b101086f109168b23fa3586fccd1133494bdb97f86920a24dc0b23984dc30b69"}, - {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36a549d632c14684bcbbd3014a6ce2666c5f2a500f34d58d32df6c9ea38b6535"}, - {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:272cffdf535978d59c38ed837916dfd2b5d193be1e9e5dcc60a5f4d5025dd98a"}, - {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcb8612787a7f4626aa881ff15ff25439561a429f5b303048f0fca8a1c781c39"}, - {file = "gevent-23.9.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:d57737860bfc332b9b5aa438963986afe90f49645f6e053140cfa0fa1bdae1ae"}, - {file = "gevent-23.9.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5f3c781c84794926d853d6fb58554dc0dcc800ba25c41d42f6959c344b4db5a6"}, - {file = "gevent-23.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:dbb22a9bbd6a13e925815ce70b940d1578dbe5d4013f20d23e8a11eddf8d14a7"}, - {file = "gevent-23.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:707904027d7130ff3e59ea387dddceedb133cc742b00b3ffe696d567147a9c9e"}, - {file = "gevent-23.9.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:45792c45d60f6ce3d19651d7fde0bc13e01b56bb4db60d3f32ab7d9ec467374c"}, - {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e24c2af9638d6c989caffc691a039d7c7022a31c0363da367c0d32ceb4a0648"}, - {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e1ead6863e596a8cc2a03e26a7a0981f84b6b3e956101135ff6d02df4d9a6b07"}, - {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65883ac026731ac112184680d1f0f1e39fa6f4389fd1fc0bf46cc1388e2599f9"}, - {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf7af500da05363e66f122896012acb6e101a552682f2352b618e541c941a011"}, - {file = "gevent-23.9.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:c3e5d2fa532e4d3450595244de8ccf51f5721a05088813c1abd93ad274fe15e7"}, - {file = "gevent-23.9.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c84d34256c243b0a53d4335ef0bc76c735873986d478c53073861a92566a8d71"}, - {file = "gevent-23.9.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ada07076b380918829250201df1d016bdafb3acf352f35e5693b59dceee8dd2e"}, - {file = "gevent-23.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:921dda1c0b84e3d3b1778efa362d61ed29e2b215b90f81d498eb4d8eafcd0b7a"}, - {file = "gevent-23.9.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ed7a048d3e526a5c1d55c44cb3bc06cfdc1947d06d45006cc4cf60dedc628904"}, - {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c1abc6f25f475adc33e5fc2dbcc26a732608ac5375d0d306228738a9ae14d3b"}, - {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4368f341a5f51611411ec3fc62426f52ac3d6d42eaee9ed0f9eebe715c80184e"}, - {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:52b4abf28e837f1865a9bdeef58ff6afd07d1d888b70b6804557e7908032e599"}, - {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52e9f12cd1cda96603ce6b113d934f1aafb873e2c13182cf8e86d2c5c41982ea"}, - {file = "gevent-23.9.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:de350fde10efa87ea60d742901e1053eb2127ebd8b59a7d3b90597eb4e586599"}, - {file = "gevent-23.9.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fde6402c5432b835fbb7698f1c7f2809c8d6b2bd9d047ac1f5a7c1d5aa569303"}, - {file = "gevent-23.9.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:dd6c32ab977ecf7c7b8c2611ed95fa4aaebd69b74bf08f4b4960ad516861517d"}, - {file = "gevent-23.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:455e5ee8103f722b503fa45dedb04f3ffdec978c1524647f8ba72b4f08490af1"}, - {file = "gevent-23.9.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:7ccf0fd378257cb77d91c116e15c99e533374a8153632c48a3ecae7f7f4f09fe"}, - {file = "gevent-23.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d163d59f1be5a4c4efcdd13c2177baaf24aadf721fdf2e1af9ee54a998d160f5"}, - {file = "gevent-23.9.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:7532c17bc6c1cbac265e751b95000961715adef35a25d2b0b1813aa7263fb397"}, - {file = "gevent-23.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:78eebaf5e73ff91d34df48f4e35581ab4c84e22dd5338ef32714264063c57507"}, - {file = "gevent-23.9.1-cp38-cp38-win32.whl", hash = "sha256:f632487c87866094546a74eefbca2c74c1d03638b715b6feb12e80120960185a"}, - {file = "gevent-23.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:62d121344f7465e3739989ad6b91f53a6ca9110518231553fe5846dbe1b4518f"}, - {file = "gevent-23.9.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:bf456bd6b992eb0e1e869e2fd0caf817f0253e55ca7977fd0e72d0336a8c1c6a"}, - {file = "gevent-23.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43daf68496c03a35287b8b617f9f91e0e7c0d042aebcc060cadc3f049aadd653"}, - {file = "gevent-23.9.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:7c28e38dcde327c217fdafb9d5d17d3e772f636f35df15ffae2d933a5587addd"}, - {file = "gevent-23.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fae8d5b5b8fa2a8f63b39f5447168b02db10c888a3e387ed7af2bd1b8612e543"}, - {file = "gevent-23.9.1-cp39-cp39-win32.whl", hash = "sha256:2c7b5c9912378e5f5ccf180d1fdb1e83f42b71823483066eddbe10ef1a2fcaa2"}, - {file = "gevent-23.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:a2898b7048771917d85a1d548fd378e8a7b2ca963db8e17c6d90c76b495e0e2b"}, - {file = "gevent-23.9.1.tar.gz", hash = "sha256:72c002235390d46f94938a96920d8856d4ffd9ddf62a303a0d7c118894097e34"}, -] - -[package.dependencies] -cffi = {version = ">=1.12.2", markers = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""} -greenlet = {version = ">=3.0rc3", markers = "platform_python_implementation == \"CPython\" and python_version >= \"3.11\""} -"zope.event" = "*" -"zope.interface" = "*" - -[package.extras] -dnspython = ["dnspython (>=1.16.0,<2.0)", "idna"] -docs = ["furo", "repoze.sphinx.autointerface", "sphinx", "sphinxcontrib-programoutput", "zope.schema"] -monitor = ["psutil (>=5.7.0)"] -recommended = ["cffi (>=1.12.2)", "dnspython (>=1.16.0,<2.0)", "idna", "psutil (>=5.7.0)"] -test = ["cffi (>=1.12.2)", "coverage (>=5.0)", "dnspython (>=1.16.0,<2.0)", "idna", "objgraph", "psutil (>=5.7.0)", "requests", "setuptools"] - [[package]] name = "ghp-import" version = "2.1.0" @@ -670,85 +523,74 @@ python-dateutil = ">=2.8.1" dev = ["flake8", "markdown", "twine", "wheel"] [[package]] -name = "greenlet" -version = "3.0.3" -description = "Lightweight in-process concurrent programming" +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 = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, - {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, - {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, - {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, - {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, - {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, - {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, - {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, - {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, - {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, - {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, - {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, - {file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"}, - {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"}, - {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"}, - {file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"}, - {file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"}, - {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"}, - {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"}, - {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"}, - {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"}, - {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"}, - {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, - {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, - {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, - {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, - {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, - {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "h2" +version = "4.1.0" +description = "HTTP/2 State-Machine based protocol implementation" +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "h2-4.1.0-py3-none-any.whl", hash = "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d"}, + {file = "h2-4.1.0.tar.gz", hash = "sha256:a83aca08fbe7aacb79fec788c9c0bac936343560ed9ec18b82a13a12c28d2abb"}, +] + +[package.dependencies] +hpack = ">=4.0,<5" +hyperframe = ">=6.0,<7" + +[[package]] +name = "hpack" +version = "4.0.0" +description = "Pure-Python HPACK header compression" +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c"}, + {file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"}, ] +[[package]] +name = "hypercorn" +version = "0.16.0" +description = "A ASGI Server based on Hyper libraries and inspired by Gunicorn" +optional = false +python-versions = ">=3.8" +files = [ + {file = "hypercorn-0.16.0-py3-none-any.whl", hash = "sha256:929e45c4acde3fbf7c58edf55336d30a009d2b4cb1f1eb96e6a515d61b663f58"}, + {file = "hypercorn-0.16.0.tar.gz", hash = "sha256:3b17d1dcf4992c1f262d9f9dd799c374125d0b9a8e40e1e2d11e2938b0adfe03"}, +] + +[package.dependencies] +h11 = "*" +h2 = ">=3.1.0" +priority = "*" +wsproto = ">=0.14.0" + [package.extras] -docs = ["Sphinx", "furo"] -test = ["objgraph", "psutil"] +docs = ["pydata_sphinx_theme", "sphinxcontrib_mermaid"] +h3 = ["aioquic (>=0.9.0,<1.0)"] +trio = ["exceptiongroup (>=1.1.0)", "trio (>=0.22.0)"] +uvloop = ["uvloop"] [[package]] -name = "h11" -version = "0.14.0" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +name = "hyperframe" +version = "6.0.1" +description = "HTTP/2 framing layer for Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6.1" files = [ - {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, + {file = "hyperframe-6.0.1-py3-none-any.whl", hash = "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15"}, + {file = "hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914"}, ] [[package]] @@ -1859,6 +1701,17 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "priority" +version = "2.0.0" +description = "A pure-Python implementation of the HTTP/2 priority tree" +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "priority-2.0.0-py3-none-any.whl", hash = "sha256:6f8eefce5f3ad59baf2c080a664037bb4725cd0a790d53d59ab4059288faf6aa"}, + {file = "priority-2.0.0.tar.gz", hash = "sha256:c965d54f1b8d0d0b19479db3924c7c36cf672dbf2aec92d43fbdaf4492ba18c0"}, +] + [[package]] name = "prompt-toolkit" version = "3.0.43" @@ -1898,17 +1751,6 @@ files = [ [package.extras] tests = ["pytest"] -[[package]] -name = "pycparser" -version = "2.21" -description = "C parser in Python" -optional = false -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 = "pygments" version = "2.17.2" @@ -1976,6 +1818,24 @@ pluggy = ">=0.12,<2.0" [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-asyncio" +version = "0.23.3" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-asyncio-0.23.3.tar.gz", hash = "sha256:af313ce900a62fbe2b1aed18e37ad757f1ef9940c6b6a88e2954de38d6b1fb9f"}, + {file = "pytest_asyncio-0.23.3-py3-none-any.whl", hash = "sha256:37a9d912e8338ee7b4a3e917381d1c95bfc8682048cb0fbc35baba316ec1faba"}, +] + +[package.dependencies] +pytest = ">=7.0.0" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + [[package]] name = "pytest-cov" version = "4.1.0" @@ -2106,6 +1966,32 @@ files = [ [package.dependencies] pyyaml = "*" +[[package]] +name = "quart" +version = "0.19.4" +description = "A Python ASGI web microframework with the same API as Flask" +optional = false +python-versions = ">=3.8" +files = [ + {file = "quart-0.19.4-py3-none-any.whl", hash = "sha256:959da9371b44b6f48d952661863f8f64e68a893481ef3f2ef45b177629dc0928"}, + {file = "quart-0.19.4.tar.gz", hash = "sha256:22ff186cf164955a7bf7483ff42a739a9fad3b119041846b15dc9597ec74c85c"}, +] + +[package.dependencies] +aiofiles = "*" +blinker = ">=1.6" +click = ">=8.0.0" +flask = ">=3.0.0" +hypercorn = ">=0.11.2" +itsdangerous = "*" +jinja2 = "*" +markupsafe = "*" +werkzeug = ">=3.0.0" + +[package.extras] +docs = ["pydata_sphinx_theme"] +dotenv = ["python-dotenv"] + [[package]] name = "rapidfuzz" version = "3.6.1" @@ -2513,22 +2399,6 @@ dev = ["flake8", "flit", "mypy", "pandas-stubs", "pre-commit", "pytest", "pytest docs = ["ipykernel", "nbconvert", "numpydoc", "pydata_sphinx_theme (==0.10.0rc2)", "pyyaml", "sphinx (<6.0.0)", "sphinx-copybutton", "sphinx-design", "sphinx-issues"] stats = ["scipy (>=1.7)", "statsmodels (>=0.12)"] -[[package]] -name = "setuptools" -version = "69.0.3" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"}, - {file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "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 = "simple-websocket" version = "1.0.0" @@ -2868,78 +2738,7 @@ files = [ [package.dependencies] h11 = ">=0.9.0,<1" -[[package]] -name = "zope-event" -version = "5.0" -description = "Very basic event publishing system" -optional = false -python-versions = ">=3.7" -files = [ - {file = "zope.event-5.0-py3-none-any.whl", hash = "sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26"}, - {file = "zope.event-5.0.tar.gz", hash = "sha256:bac440d8d9891b4068e2b5a2c5e2c9765a9df762944bda6955f96bb9b91e67cd"}, -] - -[package.dependencies] -setuptools = "*" - -[package.extras] -docs = ["Sphinx"] -test = ["zope.testrunner"] - -[[package]] -name = "zope-interface" -version = "6.1" -description = "Interfaces for Python" -optional = false -python-versions = ">=3.7" -files = [ - {file = "zope.interface-6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:43b576c34ef0c1f5a4981163b551a8781896f2a37f71b8655fd20b5af0386abb"}, - {file = "zope.interface-6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:67be3ca75012c6e9b109860820a8b6c9a84bfb036fbd1076246b98e56951ca92"}, - {file = "zope.interface-6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b9bc671626281f6045ad61d93a60f52fd5e8209b1610972cf0ef1bbe6d808e3"}, - {file = "zope.interface-6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbe81def9cf3e46f16ce01d9bfd8bea595e06505e51b7baf45115c77352675fd"}, - {file = "zope.interface-6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dc998f6de015723196a904045e5a2217f3590b62ea31990672e31fbc5370b41"}, - {file = "zope.interface-6.1-cp310-cp310-win_amd64.whl", hash = "sha256:239a4a08525c080ff833560171d23b249f7f4d17fcbf9316ef4159f44997616f"}, - {file = "zope.interface-6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9ffdaa5290422ac0f1688cb8adb1b94ca56cee3ad11f29f2ae301df8aecba7d1"}, - {file = "zope.interface-6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34c15ca9248f2e095ef2e93af2d633358c5f048c49fbfddf5fdfc47d5e263736"}, - {file = "zope.interface-6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b012d023b4fb59183909b45d7f97fb493ef7a46d2838a5e716e3155081894605"}, - {file = "zope.interface-6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:97806e9ca3651588c1baaebb8d0c5ee3db95430b612db354c199b57378312ee8"}, - {file = "zope.interface-6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fddbab55a2473f1d3b8833ec6b7ac31e8211b0aa608df5ab09ce07f3727326de"}, - {file = "zope.interface-6.1-cp311-cp311-win_amd64.whl", hash = "sha256:a0da79117952a9a41253696ed3e8b560a425197d4e41634a23b1507efe3273f1"}, - {file = "zope.interface-6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8bb9c990ca9027b4214fa543fd4025818dc95f8b7abce79d61dc8a2112b561a"}, - {file = "zope.interface-6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b51b64432eed4c0744241e9ce5c70dcfecac866dff720e746d0a9c82f371dfa7"}, - {file = "zope.interface-6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa6fd016e9644406d0a61313e50348c706e911dca29736a3266fc9e28ec4ca6d"}, - {file = "zope.interface-6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c8cf55261e15590065039696607f6c9c1aeda700ceee40c70478552d323b3ff"}, - {file = "zope.interface-6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e30506bcb03de8983f78884807e4fd95d8db6e65b69257eea05d13d519b83ac0"}, - {file = "zope.interface-6.1-cp312-cp312-win_amd64.whl", hash = "sha256:e33e86fd65f369f10608b08729c8f1c92ec7e0e485964670b4d2633a4812d36b"}, - {file = "zope.interface-6.1-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:2f8d89721834524a813f37fa174bac074ec3d179858e4ad1b7efd4401f8ac45d"}, - {file = "zope.interface-6.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13b7d0f2a67eb83c385880489dbb80145e9d344427b4262c49fbf2581677c11c"}, - {file = "zope.interface-6.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef43ee91c193f827e49599e824385ec7c7f3cd152d74cb1dfe02cb135f264d83"}, - {file = "zope.interface-6.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e441e8b7d587af0414d25e8d05e27040d78581388eed4c54c30c0c91aad3a379"}, - {file = "zope.interface-6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f89b28772fc2562ed9ad871c865f5320ef761a7fcc188a935e21fe8b31a38ca9"}, - {file = "zope.interface-6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:70d2cef1bf529bff41559be2de9d44d47b002f65e17f43c73ddefc92f32bf00f"}, - {file = "zope.interface-6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ad54ed57bdfa3254d23ae04a4b1ce405954969c1b0550cc2d1d2990e8b439de1"}, - {file = "zope.interface-6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef467d86d3cfde8b39ea1b35090208b0447caaabd38405420830f7fd85fbdd56"}, - {file = "zope.interface-6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6af47f10cfc54c2ba2d825220f180cc1e2d4914d783d6fc0cd93d43d7bc1c78b"}, - {file = "zope.interface-6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9559138690e1bd4ea6cd0954d22d1e9251e8025ce9ede5d0af0ceae4a401e43"}, - {file = "zope.interface-6.1-cp38-cp38-win_amd64.whl", hash = "sha256:964a7af27379ff4357dad1256d9f215047e70e93009e532d36dcb8909036033d"}, - {file = "zope.interface-6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:387545206c56b0315fbadb0431d5129c797f92dc59e276b3ce82db07ac1c6179"}, - {file = "zope.interface-6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:57d0a8ce40ce440f96a2c77824ee94bf0d0925e6089df7366c2272ccefcb7941"}, - {file = "zope.interface-6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ebc4d34e7620c4f0da7bf162c81978fce0ea820e4fa1e8fc40ee763839805f3"}, - {file = "zope.interface-6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a804abc126b33824a44a7aa94f06cd211a18bbf31898ba04bd0924fbe9d282d"}, - {file = "zope.interface-6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f294a15f7723fc0d3b40701ca9b446133ec713eafc1cc6afa7b3d98666ee1ac"}, - {file = "zope.interface-6.1-cp39-cp39-win_amd64.whl", hash = "sha256:a41f87bb93b8048fe866fa9e3d0c51e27fe55149035dcf5f43da4b56732c0a40"}, - {file = "zope.interface-6.1.tar.gz", hash = "sha256:2fdc7ccbd6eb6b7df5353012fbed6c3c5d04ceaca0038f75e601060e95345309"}, -] - -[package.dependencies] -setuptools = "*" - -[package.extras] -docs = ["Sphinx", "repoze.sphinx.autointerface", "sphinx-rtd-theme"] -test = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] -testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] - [metadata] lock-version = "2.0" python-versions = "^3.11,<3.12" -content-hash = "88c56122fc328480edfe017b7d83e1c770168efd9c0dbe79a9f5d64193e6dc30" +content-hash = "dee1ad0ab18dd32d10a560cff1b04137e3608c4adba4829e2ccc31bbc62ab9ff" diff --git a/pyproject.toml b/pyproject.toml index cc00160..2dcdd36 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,15 +18,15 @@ safe-ds-runner = "safeds_runner.main:main" [tool.poetry.dependencies] python = "^3.11,<3.12" safe-ds = ">=0.17,<0.18" -flask = "^3.0.0" -flask-cors = "^4.0.0" -flask-sock = "^0.7.0" -gevent = "^23.9.1" +hypercorn = "^0.16.0" +quart = "^0.19.4" [tool.poetry.dev-dependencies] pytest = "^7.4.4" pytest-cov = "^4.1.0" pytest-timeout = "^2.2.0" +pytest-asyncio = "^0.23.3" +simple-websocket = "^1.0.0" [tool.poetry.group.docs.dependencies] mkdocs = "^1.4.3" diff --git a/src/safeds_runner/server/main.py b/src/safeds_runner/server/main.py index 1987cb9..1f69adb 100644 --- a/src/safeds_runner/server/main.py +++ b/src/safeds_runner/server/main.py @@ -2,7 +2,7 @@ import logging -from safeds_runner.server.pipeline_manager import PipelineManager +from safeds_runner.server.server import SafeDsServer def start_server(port: int) -> None: @@ -14,15 +14,6 @@ def start_server(port: int) -> None: builtins.print = functools.partial(print, flush=True) # type: ignore[assignment] logging.getLogger().setLevel(logging.DEBUG) - # Startup early, so our multiprocessing setup works - app_pipeline_manager = PipelineManager() - app_pipeline_manager.startup() - from gevent.monkey import patch_all - # Patch WebSockets to work in parallel - patch_all() - - from safeds_runner.server.server import SafeDsServer - - safeds_server = SafeDsServer(app_pipeline_manager) # pragma: no cover + safeds_server = SafeDsServer() safeds_server.listen(port) # pragma: no cover diff --git a/src/safeds_runner/server/pipeline_manager.py b/src/safeds_runner/server/pipeline_manager.py index c9a6487..2e223d4 100644 --- a/src/safeds_runner/server/pipeline_manager.py +++ b/src/safeds_runner/server/pipeline_manager.py @@ -1,5 +1,6 @@ """Module that contains the infrastructure for pipeline execution in child processes.""" +import asyncio import json import logging import multiprocessing @@ -12,7 +13,6 @@ from pathlib import Path from typing import Any -import simple_websocket import stack_data from safeds_runner.server.messages import ( @@ -41,7 +41,7 @@ class PipelineManager: def __init__(self) -> None: """Create a new PipelineManager object, which is lazily started, when needed.""" self._placeholder_map: dict = {} - self._websocket_target: list[simple_websocket.Server] = [] + self._websocket_target: list[asyncio.Queue] = [] @cached_property def _multiprocessing_manager(self) -> SyncManager: @@ -55,10 +55,7 @@ def _messages_queue(self) -> queue.Queue[Message]: @cached_property def _messages_queue_thread(self) -> threading.Thread: - return threading.Thread( - target=self._handle_queue_messages, - daemon=True, - ) + return threading.Thread(target=self._handle_queue_messages, daemon=True, args=(asyncio.get_event_loop(),)) @cached_property def _memoization_map(self) -> MemoizationMap: @@ -79,11 +76,16 @@ def startup(self) -> None: if not self._messages_queue_thread.is_alive(): self._messages_queue_thread.start() - def _handle_queue_messages(self) -> None: + def _handle_queue_messages(self, event_loop: asyncio.AbstractEventLoop) -> None: """ Relay messages from pipeline processes to the currently connected websocket endpoint. Should be used in a dedicated thread. + + Parameters + ---------- + event_loop : asyncio.AbstractEventLoop + Event Loop that handles websocket connections. """ try: while self._messages_queue is not None: @@ -91,31 +93,31 @@ def _handle_queue_messages(self) -> None: message_encoded = json.dumps(message.to_dict()) # only send messages to the same connection once for connection in set(self._websocket_target): - connection.send(message_encoded) + asyncio.run_coroutine_threadsafe(connection.put(message_encoded), event_loop) except BaseException as error: # noqa: BLE001 # pragma: no cover logging.warning("Message queue terminated: %s", error.__repr__()) # pragma: no cover - def connect(self, websocket_connection: simple_websocket.Server) -> None: + def connect(self, websocket_connection_queue: asyncio.Queue) -> None: """ - Add a websocket connection to relay event messages to, which are occurring during pipeline execution. + Add a websocket connection queue to relay event messages to, which are occurring during pipeline execution. Parameters ---------- - websocket_connection : simple_websocket.Server - New websocket connection. + websocket_connection_queue : asyncio.Queue + Message Queue for a websocket connection. """ - self._websocket_target.append(websocket_connection) + self._websocket_target.append(websocket_connection_queue) - def disconnect(self, websocket_connection: simple_websocket.Server) -> None: + def disconnect(self, websocket_connection_queue: asyncio.Queue) -> None: """ - Remove a websocket target connection to no longer receive messages. + Remove a websocket target connection queue to no longer receive messages. Parameters ---------- - websocket_connection : simple_websocket.Server - Websocket connection to be removed. + websocket_connection_queue : asyncio.Queue + Message Queue for a websocket connection to be removed. """ - self._websocket_target.remove(websocket_connection) + self._websocket_target.remove(websocket_connection_queue) def execute_pipeline( self, diff --git a/src/safeds_runner/server/server.py b/src/safeds_runner/server/server.py index 47afdc1..b033936 100644 --- a/src/safeds_runner/server/server.py +++ b/src/safeds_runner/server/server.py @@ -1,15 +1,12 @@ """Module containing the server, endpoints and utility functions.""" +import asyncio import json import logging import sys -import flask.app -import flask_sock -import simple_websocket -from flask import Flask -from flask_cors import CORS -from flask_sock import Sock +import hypercorn.asyncio +import quart.app from safeds_runner.server import messages from safeds_runner.server.json_encoder import SafeDsEncoder @@ -22,63 +19,27 @@ from safeds_runner.server.pipeline_manager import PipelineManager -def create_flask_app(testing: bool = False) -> flask.app.App: +def create_flask_app() -> quart.app.Quart: """ - Create a flask app, that handles all requests. - - Parameters - ---------- - testing : bool - Whether the app should run in a testing context. - - Returns - ------- - flask.app.App - Flask app. - """ - flask_app = Flask(__name__) - # Websocket Configuration - flask_app.config["SOCK_SERVER_OPTIONS"] = {"ping_interval": 25} - flask_app.config["TESTING"] = testing - - # Allow access from VSCode extension - CORS(flask_app, resources={r"/*": {"origins": "vscode-webview://*"}}) - return flask_app - - -def create_flask_websocket(flask_app: flask.app.App) -> flask_sock.Sock: - """ - Create a flask websocket extension. - - Parameters - ---------- - flask_app: flask.app.App - Flask App Instance. + Create a quart app, that handles all requests. Returns ------- - flask_sock.Sock - Websocket extension for the provided flask app. + quart.app.Quart + App. """ - return Sock(flask_app) + return quart.app.Quart(__name__) class SafeDsServer: """Server containing the flask app, websocket handler and endpoints.""" - def __init__(self, app_pipeline_manager: PipelineManager) -> None: - """ - Create a new server object. - - Parameters - ---------- - app_pipeline_manager : PipelineManager - Manager responsible for executing pipelines sent to this server. - """ - self.app_pipeline_manager = app_pipeline_manager + def __init__(self) -> None: + """Create a new server object.""" + self.app_pipeline_manager = PipelineManager() self.app = create_flask_app() - self.sock = create_flask_websocket(self.app) - self.sock.route("/WSMain")(lambda ws: self._ws_main(ws, self.app_pipeline_manager)) + self.app.config["pipeline_manager"] = self.app_pipeline_manager + self.app.websocket("/WSMain")(SafeDsServer.ws_main) def listen(self, port: int) -> None: """ @@ -90,13 +51,21 @@ def listen(self, port: int) -> None: Port to listen on """ logging.info("Starting Safe-DS Runner on port %s", str(port)) + serve_config = hypercorn.config.Config() # Only bind to host=127.0.0.1. Connections from other devices should not be accepted - from gevent.pywsgi import WSGIServer + serve_config.bind = f"127.0.0.1:{port}" + serve_config.websocket_ping_interval = 25.0 + event_loop = asyncio.get_event_loop() + event_loop.run_until_complete(hypercorn.asyncio.serve(self.app, serve_config)) + event_loop.run_forever() # pragma: no cover - WSGIServer(("127.0.0.1", port), self.app, spawn=8).serve_forever() + @staticmethod + async def ws_main() -> None: + """Handle websocket requests to the WSMain endpoint and delegates with the required objects.""" + await SafeDsServer._ws_main(quart.websocket, quart.current_app.config["pipeline_manager"]) @staticmethod - def _ws_main(ws: simple_websocket.Server, pipeline_manager: PipelineManager) -> None: + async def _ws_main(ws: quart.Websocket, pipeline_manager: PipelineManager) -> None: """ Handle websocket requests to the WSMain endpoint. @@ -104,27 +73,34 @@ def _ws_main(ws: simple_websocket.Server, pipeline_manager: PipelineManager) -> Parameters ---------- - ws : simple_websocket.Server - Websocket Connection, provided by flask. + ws : quart.Websocket + Connection pipeline_manager : PipelineManager - Manager used to execute pipelines on, and retrieve placeholders from + Pipeline Manager """ logging.debug("Request to WSRunProgram") - pipeline_manager.connect(ws) + output_queue: asyncio.Queue = asyncio.Queue() + pipeline_manager.connect(output_queue) + foreground_handler = asyncio.create_task(SafeDsServer._ws_main_foreground(ws, pipeline_manager, output_queue)) + background_handler = asyncio.create_task(SafeDsServer._ws_main_background(ws, output_queue)) + await asyncio.gather(foreground_handler, background_handler) + + @staticmethod + async def _ws_main_foreground( + ws: quart.Websocket, + pipeline_manager: PipelineManager, + output_queue: asyncio.Queue, + ) -> None: while True: # This would be a JSON message - received_message: str = ws.receive() - if received_message is None: - logging.debug("Received EOF, closing connection") - pipeline_manager.disconnect(ws) - ws.close() - return + received_message: str = await ws.receive() logging.debug("Received Message: %s", received_message) received_object, error_detail, error_short = parse_validate_message(received_message) if received_object is None: logging.error(error_detail) - pipeline_manager.disconnect(ws) - ws.close(message=error_short) + await output_queue.put(None) + pipeline_manager.disconnect(output_queue) + await ws.close(code=1000, reason=error_short) return match received_object.type: case "shutdown": @@ -135,8 +111,9 @@ def _ws_main(ws: simple_websocket.Server, pipeline_manager: PipelineManager) -> program_data, invalid_message = messages.validate_program_message_data(received_object.data) if program_data is None: logging.error("Invalid message data specified in: %s (%s)", received_message, invalid_message) - pipeline_manager.disconnect(ws) - ws.close(None, invalid_message) + await output_queue.put(None) + pipeline_manager.disconnect(output_queue) + await ws.close(code=1000, reason=invalid_message) return # This should only be called from the extension as it is a security risk pipeline_manager.execute_pipeline(program_data, received_object.id) @@ -147,8 +124,9 @@ def _ws_main(ws: simple_websocket.Server, pipeline_manager: PipelineManager) -> ) if placeholder_query_data is None: logging.error("Invalid message data specified in: %s (%s)", received_message, invalid_message) - pipeline_manager.disconnect(ws) - ws.close(None, invalid_message) + await output_queue.put(None) + pipeline_manager.disconnect(output_queue) + await ws.close(code=1000, reason=invalid_message) return placeholder_type, placeholder_value = pipeline_manager.get_placeholder( received_object.id, @@ -157,8 +135,8 @@ def _ws_main(ws: simple_websocket.Server, pipeline_manager: PipelineManager) -> # send back a value message if placeholder_type is not None: try: - broadcast_message( - [ws], + await send_message( + ws, Message( message_type_placeholder_value, received_object.id, @@ -171,8 +149,8 @@ def _ws_main(ws: simple_websocket.Server, pipeline_manager: PipelineManager) -> ) except TypeError as _encoding_error: # if the value can't be encoded send back that the value exists but is not displayable - broadcast_message( - [ws], + await send_message( + ws, Message( message_type_placeholder_value, received_object.id, @@ -186,8 +164,8 @@ def _ws_main(ws: simple_websocket.Server, pipeline_manager: PipelineManager) -> else: # Send back empty type / value, to communicate that no placeholder exists (yet) # Use name from query to allow linking a response to a request on the peer - broadcast_message( - [ws], + await send_message( + ws, Message( message_type_placeholder_value, received_object.id, @@ -198,18 +176,25 @@ def _ws_main(ws: simple_websocket.Server, pipeline_manager: PipelineManager) -> if received_object.type not in messages.message_types: logging.warning("Invalid message type: %s", received_object.type) + @staticmethod + async def _ws_main_background(ws: quart.Websocket, output_queue: asyncio.Queue) -> None: + while True: + encoded_message = await output_queue.get() + if encoded_message is None: + return + await ws.send(encoded_message) + -def broadcast_message(connections: list[simple_websocket.Server], message: Message) -> None: +async def send_message(connection: quart.Websocket, message: Message) -> None: """ - Send any message to all the provided connections (to the VS Code extension). + Send a message to the provided websocket connection (to the VS Code extension). Parameters ---------- - connections : list[simple_websocket.Server] - List of Websocket connections that should receive the message. + connection : quart.Websocket + Connection that should receive the message. message : Message Object that will be sent. """ message_encoded = json.dumps(message.to_dict(), cls=SafeDsEncoder) - for connection in connections: - connection.send(message_encoded) + await connection.send(message_encoded) diff --git a/tests/safeds_runner/server/test_websocket_mock.py b/tests/safeds_runner/server/test_websocket_mock.py index f25892f..ddc1003 100644 --- a/tests/safeds_runner/server/test_websocket_mock.py +++ b/tests/safeds_runner/server/test_websocket_mock.py @@ -1,15 +1,16 @@ +import asyncio import json import logging import multiprocessing import os import sys -import threading import time import typing import pytest import safeds_runner.server.main import simple_websocket +from quart.testing.connections import WebsocketDisconnectError from safeds.data.tabular.containers import Table from safeds_runner.server.json_encoder import SafeDsEncoder from safeds_runner.server.messages import ( @@ -23,45 +24,115 @@ message_type_placeholder_value, message_type_runtime_error, message_type_runtime_progress, + parse_validate_message, + validate_placeholder_query_message_data, + validate_program_message_data, ) -from safeds_runner.server.pipeline_manager import PipelineManager from safeds_runner.server.server import SafeDsServer -app_pipeline_manager = PipelineManager() - -class MockWebsocketConnection: - def __init__(self, messages: list[str]): - self.messages = messages - self.received: list[str] = [] - self.close_reason: int | None = None - self.close_message: str | None = None - self.condition_variable = threading.Condition(lock=threading.Lock()) - - def send(self, msg: str) -> None: - with self.condition_variable: - self.received.append(msg) - self.condition_variable.notify_all() - - def receive(self) -> str | None: - if len(self.messages) == 0: - return None - return self.messages.pop(0) - - def close(self, reason: int | None = None, message: str | None = None) -> None: - self.close_reason = reason - self.close_message = message - - def wait_for_messages(self, wait_for_messages: int = 1) -> None: - while True: - with self.condition_variable: - if len(self.received) >= wait_for_messages: - return - self.condition_variable.wait(1.0) # this should not be needed, but it seems the process can get stuck - - def get_next_received_message(self) -> str: - with self.condition_variable: - return self.received.pop(0) +@pytest.mark.parametrize( + argnames="websocket_message", + argvalues=[ + "", + json.dumps({"id": "a", "data": "b"}), + json.dumps({"type": "a", "data": "b"}), + json.dumps({"type": "b", "id": "123"}), + json.dumps({"type": {"program": "2"}, "id": "123", "data": "a"}), + json.dumps({"type": "c", "id": {"": "1233"}, "data": "a"}), + json.dumps({"type": "program", "id": "1234", "data": "a"}), + json.dumps({"type": "placeholder_query", "id": "123", "data": "abc"}), + json.dumps({"type": "placeholder_query", "id": "123", "data": {"a": "v"}}), + json.dumps({"type": "placeholder_query", "id": "123", "data": {"name": "v", "window": {"begin": "a"}}}), + json.dumps({"type": "placeholder_query", "id": "123", "data": {"name": "v", "window": {"size": "a"}}}), + json.dumps({ + "type": "program", + "id": "1234", + "data": {"main": {"modulepath": "1", "module": "2", "pipeline": "3"}}, + }), + json.dumps({"type": "program", "id": "1234", "data": {"code": {"": {"entry": ""}}}}), + json.dumps({ + "type": "program", + "id": "1234", + "data": {"code": {"": {"entry": ""}}, "main": {"modulepath": "1", "module": "2"}}, + }), + json.dumps({ + "type": "program", + "id": "1234", + "data": {"code": {"": {"entry": ""}}, "main": {"modulepath": "1", "pipeline": "3"}}, + }), + json.dumps({ + "type": "program", + "id": "1234", + "data": {"code": {"": {"entry": ""}}, "main": {"module": "2", "pipeline": "3"}}, + }), + json.dumps({ + "type": "program", + "id": "1234", + "data": { + "code": {"": {"entry": ""}}, + "main": {"modulepath": "1", "module": "2", "pipeline": "3", "other": "4"}, + }, + }), + json.dumps({ + "type": "program", + "id": "1234", + "data": { + "code": {"": {"entry": ""}}, + "main": {"modulepath": "1", "module": "2", "pipeline": "3", "other": {"4": "a"}}, + }, + }), + json.dumps({ + "type": "program", + "id": "1234", + "data": {"code": "a", "main": {"modulepath": "1", "module": "2", "pipeline": "3"}}, + }), + json.dumps({ + "type": "program", + "id": "1234", + "data": {"code": {"": "a"}, "main": {"modulepath": "1", "module": "2", "pipeline": "3"}}, + }), + json.dumps({ + "type": "program", + "id": "1234", + "data": {"code": {"": {"a": {"b": "c"}}}, "main": {"modulepath": "1", "module": "2", "pipeline": "3"}}, + }), + ], + ids=[ + "no_json", + "any_no_type", + "any_no_id", + "any_no_data", + "any_invalid_type", + "any_invalid_id", + "program_invalid_data", + "placeholder_query_invalid_data1", + "placeholder_query_invalid_data2", + "placeholder_query_invalid_data3", + "placeholder_query_invalid_data4", + "program_no_code", + "program_no_main", + "program_invalid_main1", + "program_invalid_main2", + "program_invalid_main3", + "program_invalid_main4", + "program_invalid_main5", + "program_invalid_code1", + "program_invalid_code2", + "program_invalid_code3", + ], +) +@pytest.mark.asyncio() +async def test_should_fail_message_validation_ws(websocket_message: str) -> None: + test_client = SafeDsServer().app.test_client() + async with test_client.websocket("/WSMain") as test_websocket: + await test_websocket.send(websocket_message) + disconnected = False + try: + _result = await test_websocket.receive() + except WebsocketDisconnectError as _disconnect: + disconnected = True + assert disconnected @pytest.mark.parametrize( @@ -73,17 +144,25 @@ def get_next_received_message(self) -> str: (json.dumps({"type": "b", "id": "123"}), "Invalid Message: no data"), (json.dumps({"type": {"program": "2"}, "id": "123", "data": "a"}), "Invalid Message: invalid type"), (json.dumps({"type": "c", "id": {"": "1233"}, "data": "a"}), "Invalid Message: invalid id"), + ], + ids=[ + "no_json", + "any_no_type", + "any_no_id", + "any_no_data", + "any_invalid_type", + "any_invalid_id", + ], +) +def test_should_fail_message_validation_reason_general(websocket_message: str, exception_message: str) -> None: + received_object, error_detail, error_short = parse_validate_message(websocket_message) + assert error_short == exception_message + + +@pytest.mark.parametrize( + argnames="websocket_message,exception_message", + argvalues=[ (json.dumps({"type": "program", "id": "1234", "data": "a"}), "Message data is not a JSON object"), - (json.dumps({"type": "placeholder_query", "id": "123", "data": "abc"}), "Message data is not a JSON object"), - (json.dumps({"type": "placeholder_query", "id": "123", "data": {"a": "v"}}), "No 'name' parameter given"), - ( - json.dumps({"type": "placeholder_query", "id": "123", "data": {"name": "v", "window": {"begin": "a"}}}), - "Invalid 'window'.'begin' parameter given", - ), - ( - json.dumps({"type": "placeholder_query", "id": "123", "data": {"name": "v", "window": {"size": "a"}}}), - "Invalid 'window'.'size' parameter given", - ), ( json.dumps({ "type": "program", @@ -168,17 +247,7 @@ def get_next_received_message(self) -> str: ), ], ids=[ - "no_json", - "any_no_type", - "any_no_id", - "any_no_data", - "any_invalid_type", - "any_invalid_id", "program_invalid_data", - "placeholder_query_invalid_data1", - "placeholder_query_invalid_data2", - "placeholder_query_invalid_data3", - "placeholder_query_invalid_data4", "program_no_code", "program_no_main", "program_invalid_main1", @@ -191,11 +260,42 @@ def get_next_received_message(self) -> str: "program_invalid_code3", ], ) -def test_should_fail_message_validation(websocket_message: str, exception_message: str) -> None: - mock_connection = MockWebsocketConnection([websocket_message]) - app_pipeline_manager.connect(mock_connection) - SafeDsServer._ws_main(mock_connection, app_pipeline_manager) - assert str(mock_connection.close_message) == exception_message +def test_should_fail_message_validation_reason_program(websocket_message: str, exception_message: str) -> None: + received_object, error_detail, error_short = parse_validate_message(websocket_message) + assert received_object is not None + program_data, invalid_message = validate_program_message_data(received_object.data) + assert invalid_message == exception_message + + +@pytest.mark.parametrize( + argnames="websocket_message,exception_message", + argvalues=[ + (json.dumps({"type": "placeholder_query", "id": "123", "data": "abc"}), "Message data is not a JSON object"), + (json.dumps({"type": "placeholder_query", "id": "123", "data": {"a": "v"}}), "No 'name' parameter given"), + ( + json.dumps({"type": "placeholder_query", "id": "123", "data": {"name": "v", "window": {"begin": "a"}}}), + "Invalid 'window'.'begin' parameter given", + ), + ( + json.dumps({"type": "placeholder_query", "id": "123", "data": {"name": "v", "window": {"size": "a"}}}), + "Invalid 'window'.'size' parameter given", + ), + ], + ids=[ + "placeholder_query_invalid_data1", + "placeholder_query_invalid_data2", + "placeholder_query_invalid_data3", + "placeholder_query_invalid_data4", + ], +) +def test_should_fail_message_validation_reason_placeholder_query( + websocket_message: str, + exception_message: str, +) -> None: + received_object, error_detail, error_short = parse_validate_message(websocket_message) + assert received_object is not None + program_data, invalid_message = validate_placeholder_query_message_data(received_object.data) + assert invalid_message == exception_message @pytest.mark.skipif( @@ -206,52 +306,48 @@ def test_should_fail_message_validation(websocket_message: str, exception_messag ), ) @pytest.mark.parametrize( - argnames="messages,expected_response_runtime_error", + argnames="message,expected_response_runtime_error", argvalues=[ ( - [ - json.dumps({ - "type": "program", - "id": "abcdefgh", - "data": { - "code": { - "": { - "gen_test_a": "def pipe():\n\traise Exception('Test Exception')\n", - "gen_test_a_pipe": ( - "from gen_test_a import pipe\n\nif __name__ == '__main__':\n\tpipe()" - ), - }, + json.dumps({ + "type": "program", + "id": "abcdefgh", + "data": { + "code": { + "": { + "gen_test_a": "def pipe():\n\traise Exception('Test Exception')\n", + "gen_test_a_pipe": "from gen_test_a import pipe\n\nif __name__ == '__main__':\n\tpipe()", }, - "main": {"modulepath": "", "module": "test_a", "pipeline": "pipe"}, }, - }), - ], + "main": {"modulepath": "", "module": "test_a", "pipeline": "pipe"}, + }, + }), Message(message_type_runtime_error, "abcdefgh", {"message": "Test Exception"}), ), ], ids=["raise_exception"], ) -def test_should_execute_pipeline_return_exception( - messages: list[str], +@pytest.mark.asyncio() +async def test_should_execute_pipeline_return_exception( + message: str, expected_response_runtime_error: Message, ) -> None: - mock_connection = MockWebsocketConnection(messages) - app_pipeline_manager.connect(mock_connection) - SafeDsServer._ws_main(mock_connection, app_pipeline_manager) - mock_connection.wait_for_messages(1) - exception_message = Message.from_dict(json.loads(mock_connection.get_next_received_message())) - - assert exception_message.type == expected_response_runtime_error.type - assert exception_message.id == expected_response_runtime_error.id - assert isinstance(exception_message.data, dict) - assert exception_message.data["message"] == expected_response_runtime_error.data["message"] - assert isinstance(exception_message.data["backtrace"], list) - assert len(exception_message.data["backtrace"]) > 0 - for frame in exception_message.data["backtrace"]: - assert "file" in frame - assert isinstance(frame["file"], str) - assert "line" in frame - assert isinstance(frame["line"], int) + test_client = SafeDsServer().app.test_client() + async with test_client.websocket("/WSMain") as test_websocket: + await test_websocket.send(message) + received_message = await test_websocket.receive() + exception_message = Message.from_dict(json.loads(received_message)) + assert exception_message.type == expected_response_runtime_error.type + assert exception_message.id == expected_response_runtime_error.id + assert isinstance(exception_message.data, dict) + assert exception_message.data["message"] == expected_response_runtime_error.data["message"] + assert isinstance(exception_message.data["backtrace"], list) + assert len(exception_message.data["backtrace"]) > 0 + for frame in exception_message.data["backtrace"]: + assert "file" in frame + assert isinstance(frame["file"], str) + assert "line" in frame + assert isinstance(frame["line"], int) @pytest.mark.skipif( @@ -287,7 +383,7 @@ def test_should_execute_pipeline_return_exception( }, }), ], - 2, + 3, [ # Query Placeholder json.dumps({"type": "placeholder_query", "id": "abcdefg", "data": {"name": "value1", "window": {}}}), @@ -325,26 +421,31 @@ def test_should_execute_pipeline_return_exception( ], ids=["query_valid_query_invalid"], ) -def test_should_execute_pipeline_return_valid_placeholder( +@pytest.mark.asyncio() +async def test_should_execute_pipeline_return_valid_placeholder( initial_messages: list[str], initial_execution_message_wait: int, appended_messages: list[str], expected_responses: list[Message], ) -> None: # Initial execution - mock_connection = MockWebsocketConnection(initial_messages) - app_pipeline_manager.connect(mock_connection) - SafeDsServer._ws_main(mock_connection, app_pipeline_manager) - # Wait for at least enough messages to successfully execute pipeline - mock_connection.wait_for_messages(initial_execution_message_wait) - # Now send queries - mock_connection.messages.extend(appended_messages) - SafeDsServer._ws_main(mock_connection, app_pipeline_manager) - # And compare with expected responses - while len(expected_responses) > 0: - mock_connection.wait_for_messages(1) - next_message = Message.from_dict(json.loads(mock_connection.get_next_received_message())) - assert next_message == expected_responses.pop(0) + test_client = SafeDsServer().app.test_client() + async with test_client.websocket("/WSMain") as test_websocket: + for message in initial_messages: + await test_websocket.send(message) + # Wait for at least enough messages to successfully execute pipeline + for _ in range(initial_execution_message_wait): + received_message = await test_websocket.receive() + next_message = Message.from_dict(json.loads(received_message)) + assert next_message == expected_responses.pop(0) + # Now send queries + for message in appended_messages: + await test_websocket.send(message) + # And compare with expected responses + while len(expected_responses) > 0: + received_message = await test_websocket.receive() + next_message = Message.from_dict(json.loads(received_message)) + assert next_message == expected_responses.pop(0) @pytest.mark.skipif( @@ -410,13 +511,15 @@ def test_should_execute_pipeline_return_valid_placeholder( ], ids=["progress_message_done", "invalid_message_invalid_placeholder_query"], ) -def test_should_successfully_execute_simple_flow(messages: list[str], expected_response: Message) -> None: - mock_connection = MockWebsocketConnection(messages) - app_pipeline_manager.connect(mock_connection) - SafeDsServer._ws_main(mock_connection, app_pipeline_manager) - mock_connection.wait_for_messages(1) - query_result_invalid = Message.from_dict(json.loads(mock_connection.get_next_received_message())) - assert query_result_invalid == expected_response +@pytest.mark.asyncio() +async def test_should_successfully_execute_simple_flow(messages: list[str], expected_response: Message) -> None: + test_client = SafeDsServer().app.test_client() + async with test_client.websocket("/WSMain") as test_websocket: + for message in messages: + await test_websocket.send(message) + received_message = await test_websocket.receive() + query_result_invalid = Message.from_dict(json.loads(received_message)) + assert query_result_invalid == expected_response @pytest.mark.skipif( @@ -443,10 +546,14 @@ def test_should_shut_itself_down(messages: list[str]) -> None: def helper_should_shut_itself_down_run_in_subprocess(sub_messages: list[str]) -> None: - mock_connection = MockWebsocketConnection(sub_messages) - pipeline_manager = PipelineManager() - pipeline_manager.startup() - SafeDsServer._ws_main(mock_connection, pipeline_manager) + asyncio.get_event_loop().run_until_complete(helper_should_shut_itself_down_run_in_subprocess_async(sub_messages)) + + +async def helper_should_shut_itself_down_run_in_subprocess_async(sub_messages: list[str]) -> None: + test_client = SafeDsServer().app.test_client() + async with test_client.websocket("/WSMain") as test_websocket: + for message in sub_messages: + await test_websocket.send(message) @pytest.mark.timeout(45) @@ -581,3 +688,72 @@ def helper_should_accept_at_least_2_parallel_connections_in_subprocess_server( def test_windowed_placeholder(query: MessageQueryInformation, type_: str, value: typing.Any, result: str) -> None: message = create_placeholder_value(query, type_, value) assert json.dumps(message, cls=SafeDsEncoder) == result + + +@pytest.mark.parametrize( + argnames="query,expected_response", + argvalues=[ + ( + json.dumps({ + "type": "program", + "id": "abcdefgh", + "data": { + "code": { + "": { + "gen_test_a": "def pipe():\n\tpass\n", + "gen_test_a_pipe": "from gen_test_a import pipe\n\nif __name__ == '__main__':\n\tpipe()", + }, + }, + "main": {"modulepath": "", "module": "test_a", "pipeline": "pipe"}, + }, + }), + Message(message_type_runtime_progress, "abcdefgh", "done"), + ), + ], + ids=["at_least_a_message_without_crashing"], +) +@pytest.mark.timeout(45) +def test_should_accept_at_least_a_message_without_crashing_in_subprocess( + query: str, + expected_response: Message, +) -> None: + port = 6000 + server_output_pipes_stderr_r, server_output_pipes_stderr_w = multiprocessing.Pipe() + process = multiprocessing.Process( + target=helper_should_accept_at_least_a_message_without_crashing_in_subprocess_server, + args=(port, server_output_pipes_stderr_w), + ) + process.start() + while process.is_alive(): + if not server_output_pipes_stderr_r.poll(0.1): + continue + process_line = str(server_output_pipes_stderr_r.recv()).strip() + # Wait for first line of log + if process_line.startswith("INFO:root:Starting Safe-DS Runner"): + break + client1 = None + for _i in range(10): + try: + client1 = simple_websocket.Client.connect(f"ws://127.0.0.1:{port}/WSMain") + break + except ConnectionRefusedError as e: + logging.warning("Connection refused: %s", e) + time.sleep(0.5) + if client1 is not None and client1.connected: + client1.send(query) + received_message = client1.receive() + received_message_validated = Message.from_dict(json.loads(received_message)) + assert received_message_validated == expected_response + client1.send('{"id": "", "type": "shutdown", "data": ""}') + process.join(5) + if process.is_alive(): + process.kill() + + +def helper_should_accept_at_least_a_message_without_crashing_in_subprocess_server( + port: int, + pipe: multiprocessing.connection.Connection, +) -> None: + sys.stderr.write = lambda value: pipe.send(value) # type: ignore[method-assign, assignment] + sys.stdout.write = lambda value: pipe.send(value) # type: ignore[method-assign, assignment] + safeds_runner.server.main.start_server(port)