diff --git a/poetry.lock b/poetry.lock index 95edbbaef..b4244e6b1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "alfred-cli" @@ -54,41 +54,52 @@ files = [ [[package]] name = "anyio" -version = "4.6.2.post1" +version = "4.7.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.9" files = [ - {file = "anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d"}, - {file = "anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c"}, + {file = "anyio-4.7.0-py3-none-any.whl", hash = "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352"}, + {file = "anyio-4.7.0.tar.gz", hash = "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48"}, ] [package.dependencies] exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" sniffio = ">=1.1" -typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] -doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] +doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] trio = ["trio (>=0.26.1)"] +[[package]] +name = "async-timeout" +version = "5.0.1" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, + {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, +] + [[package]] name = "attrs" -version = "24.2.0" +version = "24.3.0" description = "Classes Without Boilerplate" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, - {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, + {file = "attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308"}, + {file = "attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff"}, ] [package.extras] benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] @@ -109,13 +120,13 @@ cryptography = "*" [[package]] name = "certifi" -version = "2024.8.30" +version = "2024.12.14" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, - {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, + {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, + {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, ] [[package]] @@ -349,7 +360,6 @@ files = [ {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb"}, {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b"}, {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543"}, - {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:60eb32934076fa07e4316b7b2742fa52cbb190b42c2df2863dbc4230a0a9b385"}, {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e"}, {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e"}, {file = "cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053"}, @@ -360,7 +370,6 @@ files = [ {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289"}, {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7"}, {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c"}, - {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9abcc2e083cbe8dde89124a47e5e53ec38751f0d7dfd36801008f316a127d7ba"}, {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64"}, {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285"}, {file = "cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417"}, @@ -414,13 +423,13 @@ test = ["pytest (>=6)"] [[package]] name = "fastapi" -version = "0.115.5" +version = "0.115.6" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.8" files = [ - {file = "fastapi-0.115.5-py3-none-any.whl", hash = "sha256:596b95adbe1474da47049e802f9a65ab2ffa9c2b07e7efee70eb8a66c9f2f796"}, - {file = "fastapi-0.115.5.tar.gz", hash = "sha256:0e7a4d0dc0d01c68df21887cce0945e72d3c48b9f4f79dfe7a7d53aa08fbb289"}, + {file = "fastapi-0.115.6-py3-none-any.whl", hash = "sha256:e9240b29e36fa8f4bb7290316988e90c381e5092e0cbe84e7818cc3713bcf305"}, + {file = "fastapi-0.115.6.tar.gz", hash = "sha256:9ec46f7addc14ea472958a96aae5b5de65f39721a46aaf5705c480d9a8b76654"}, ] [package.dependencies] @@ -476,13 +485,13 @@ trio = ["trio (>=0.22.0,<1.0)"] [[package]] name = "httpx" -version = "0.28.0" +version = "0.28.1" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" files = [ - {file = "httpx-0.28.0-py3-none-any.whl", hash = "sha256:dc0b419a0cfeb6e8b34e85167c0da2671206f5095f1baa9663d23bcfd6b535fc"}, - {file = "httpx-0.28.0.tar.gz", hash = "sha256:0858d3bab51ba7e386637f22a61d8ccddaeec5f3fe4209da3a6168dbb91573e0"}, + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, ] [package.dependencies] @@ -711,13 +720,13 @@ files = [ [[package]] name = "narwhals" -version = "1.14.3" +version = "1.18.4" description = "Extremely lightweight compatibility layer between dataframe libraries" optional = false python-versions = ">=3.8" files = [ - {file = "narwhals-1.14.3-py3-none-any.whl", hash = "sha256:49bfcd80387e1377a353c5274f63d12bade364fd2feac45742be9c0f69c60ae9"}, - {file = "narwhals-1.14.3.tar.gz", hash = "sha256:3a9484c69c36bbc44a28a36627eede502946e60d6e0401b4b351202b42b18419"}, + {file = "narwhals-1.18.4-py3-none-any.whl", hash = "sha256:c6bb6b6fba59caeab28a7d6ec1e79ab0040c75baef2e4152199ad1a9c266ef96"}, + {file = "narwhals-1.18.4.tar.gz", hash = "sha256:b1da4e2e4ab185824781760319ac1ec8ee2944a929795064c3a64ffff16b00c4"}, ] [package.extras] @@ -727,6 +736,7 @@ modin = ["modin"] pandas = ["pandas (>=0.25.3)"] polars = ["polars (>=0.20.3)"] pyarrow = ["pyarrow (>=11.0.0)"] +pyspark = ["pyspark (>=3.3.0)"] [[package]] name = "numpy" @@ -1029,13 +1039,13 @@ files = [ [[package]] name = "pydantic" -version = "2.10.2" +version = "2.10.3" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.10.2-py3-none-any.whl", hash = "sha256:cfb96e45951117c3024e6b67b25cdc33a3cb7b2fa62e239f7af1378358a1d99e"}, - {file = "pydantic-2.10.2.tar.gz", hash = "sha256:2bc2d7f17232e0841cbba4641e65ba1eb6fafb3a08de3a091ff3ce14a197c4fa"}, + {file = "pydantic-2.10.3-py3-none-any.whl", hash = "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d"}, + {file = "pydantic-2.10.3.tar.gz", hash = "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9"}, ] [package.dependencies] @@ -1215,13 +1225,13 @@ six = ">=1.5" [[package]] name = "python-multipart" -version = "0.0.18" +version = "0.0.19" description = "A streaming multipart parser for Python" optional = false python-versions = ">=3.8" files = [ - {file = "python_multipart-0.0.18-py3-none-any.whl", hash = "sha256:efe91480f485f6a361427a541db4796f9e1591afc0fb8e7a4ba06bfbc6708996"}, - {file = "python_multipart-0.0.18.tar.gz", hash = "sha256:7a68db60c8bfb82e460637fa4750727b45af1d5e2ed215593f917f64694d34fe"}, + {file = "python_multipart-0.0.19-py3-none-any.whl", hash = "sha256:f8d5b0b9c618575bf9df01c684ded1d94a338839bdd8223838afacfb4bb2082d"}, + {file = "python_multipart-0.0.19.tar.gz", hash = "sha256:905502ef39050557b7a6af411f454bc19526529ca46ae6831508438890ce12cc"}, ] [[package]] @@ -1235,6 +1245,24 @@ files = [ {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, ] +[[package]] +name = "redis" +version = "5.2.1" +description = "Python client for Redis database and key-value store" +optional = false +python-versions = ">=3.8" +files = [ + {file = "redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4"}, + {file = "redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f"}, +] + +[package.dependencies] +async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\""} + +[package.extras] +hiredis = ["hiredis (>=3.0.0)"] +ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==23.2.1)", "requests (>=2.31.0)"] + [[package]] name = "referencing" version = "0.35.1" @@ -1273,101 +1301,114 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rpds-py" -version = "0.21.0" +version = "0.22.3" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.9" files = [ - {file = "rpds_py-0.21.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a017f813f24b9df929674d0332a374d40d7f0162b326562daae8066b502d0590"}, - {file = "rpds_py-0.21.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:20cc1ed0bcc86d8e1a7e968cce15be45178fd16e2ff656a243145e0b439bd250"}, - {file = "rpds_py-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad116dda078d0bc4886cb7840e19811562acdc7a8e296ea6ec37e70326c1b41c"}, - {file = "rpds_py-0.21.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:808f1ac7cf3b44f81c9475475ceb221f982ef548e44e024ad5f9e7060649540e"}, - {file = "rpds_py-0.21.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de552f4a1916e520f2703ec474d2b4d3f86d41f353e7680b597512ffe7eac5d0"}, - {file = "rpds_py-0.21.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:efec946f331349dfc4ae9d0e034c263ddde19414fe5128580f512619abed05f1"}, - {file = "rpds_py-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b80b4690bbff51a034bfde9c9f6bf9357f0a8c61f548942b80f7b66356508bf5"}, - {file = "rpds_py-0.21.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:085ed25baac88953d4283e5b5bd094b155075bb40d07c29c4f073e10623f9f2e"}, - {file = "rpds_py-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:daa8efac2a1273eed2354397a51216ae1e198ecbce9036fba4e7610b308b6153"}, - {file = "rpds_py-0.21.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:95a5bad1ac8a5c77b4e658671642e4af3707f095d2b78a1fdd08af0dfb647624"}, - {file = "rpds_py-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3e53861b29a13d5b70116ea4230b5f0f3547b2c222c5daa090eb7c9c82d7f664"}, - {file = "rpds_py-0.21.0-cp310-none-win32.whl", hash = "sha256:ea3a6ac4d74820c98fcc9da4a57847ad2cc36475a8bd9683f32ab6d47a2bd682"}, - {file = "rpds_py-0.21.0-cp310-none-win_amd64.whl", hash = "sha256:b8f107395f2f1d151181880b69a2869c69e87ec079c49c0016ab96860b6acbe5"}, - {file = "rpds_py-0.21.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5555db3e618a77034954b9dc547eae94166391a98eb867905ec8fcbce1308d95"}, - {file = "rpds_py-0.21.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:97ef67d9bbc3e15584c2f3c74bcf064af36336c10d2e21a2131e123ce0f924c9"}, - {file = "rpds_py-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ab2c2a26d2f69cdf833174f4d9d86118edc781ad9a8fa13970b527bf8236027"}, - {file = "rpds_py-0.21.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4e8921a259f54bfbc755c5bbd60c82bb2339ae0324163f32868f63f0ebb873d9"}, - {file = "rpds_py-0.21.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a7ff941004d74d55a47f916afc38494bd1cfd4b53c482b77c03147c91ac0ac3"}, - {file = "rpds_py-0.21.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5145282a7cd2ac16ea0dc46b82167754d5e103a05614b724457cffe614f25bd8"}, - {file = "rpds_py-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de609a6f1b682f70bb7163da745ee815d8f230d97276db049ab447767466a09d"}, - {file = "rpds_py-0.21.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40c91c6e34cf016fa8e6b59d75e3dbe354830777fcfd74c58b279dceb7975b75"}, - {file = "rpds_py-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d2132377f9deef0c4db89e65e8bb28644ff75a18df5293e132a8d67748397b9f"}, - {file = "rpds_py-0.21.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0a9e0759e7be10109645a9fddaaad0619d58c9bf30a3f248a2ea57a7c417173a"}, - {file = "rpds_py-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9e20da3957bdf7824afdd4b6eeb29510e83e026473e04952dca565170cd1ecc8"}, - {file = "rpds_py-0.21.0-cp311-none-win32.whl", hash = "sha256:f71009b0d5e94c0e86533c0b27ed7cacc1239cb51c178fd239c3cfefefb0400a"}, - {file = "rpds_py-0.21.0-cp311-none-win_amd64.whl", hash = "sha256:e168afe6bf6ab7ab46c8c375606298784ecbe3ba31c0980b7dcbb9631dcba97e"}, - {file = "rpds_py-0.21.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:30b912c965b2aa76ba5168fd610087bad7fcde47f0a8367ee8f1876086ee6d1d"}, - {file = "rpds_py-0.21.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ca9989d5d9b1b300bc18e1801c67b9f6d2c66b8fd9621b36072ed1df2c977f72"}, - {file = "rpds_py-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f54e7106f0001244a5f4cf810ba8d3f9c542e2730821b16e969d6887b664266"}, - {file = "rpds_py-0.21.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fed5dfefdf384d6fe975cc026886aece4f292feaf69d0eeb716cfd3c5a4dd8be"}, - {file = "rpds_py-0.21.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:590ef88db231c9c1eece44dcfefd7515d8bf0d986d64d0caf06a81998a9e8cab"}, - {file = "rpds_py-0.21.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f983e4c2f603c95dde63df633eec42955508eefd8d0f0e6d236d31a044c882d7"}, - {file = "rpds_py-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b229ce052ddf1a01c67d68166c19cb004fb3612424921b81c46e7ea7ccf7c3bf"}, - {file = "rpds_py-0.21.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ebf64e281a06c904a7636781d2e973d1f0926a5b8b480ac658dc0f556e7779f4"}, - {file = "rpds_py-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:998a8080c4495e4f72132f3d66ff91f5997d799e86cec6ee05342f8f3cda7dca"}, - {file = "rpds_py-0.21.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:98486337f7b4f3c324ab402e83453e25bb844f44418c066623db88e4c56b7c7b"}, - {file = "rpds_py-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a78d8b634c9df7f8d175451cfeac3810a702ccb85f98ec95797fa98b942cea11"}, - {file = "rpds_py-0.21.0-cp312-none-win32.whl", hash = "sha256:a58ce66847711c4aa2ecfcfaff04cb0327f907fead8945ffc47d9407f41ff952"}, - {file = "rpds_py-0.21.0-cp312-none-win_amd64.whl", hash = "sha256:e860f065cc4ea6f256d6f411aba4b1251255366e48e972f8a347cf88077b24fd"}, - {file = "rpds_py-0.21.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ee4eafd77cc98d355a0d02f263efc0d3ae3ce4a7c24740010a8b4012bbb24937"}, - {file = "rpds_py-0.21.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:688c93b77e468d72579351a84b95f976bd7b3e84aa6686be6497045ba84be560"}, - {file = "rpds_py-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c38dbf31c57032667dd5a2f0568ccde66e868e8f78d5a0d27dcc56d70f3fcd3b"}, - {file = "rpds_py-0.21.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2d6129137f43f7fa02d41542ffff4871d4aefa724a5fe38e2c31a4e0fd343fb0"}, - {file = "rpds_py-0.21.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:520ed8b99b0bf86a176271f6fe23024323862ac674b1ce5b02a72bfeff3fff44"}, - {file = "rpds_py-0.21.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aaeb25ccfb9b9014a10eaf70904ebf3f79faaa8e60e99e19eef9f478651b9b74"}, - {file = "rpds_py-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af04ac89c738e0f0f1b913918024c3eab6e3ace989518ea838807177d38a2e94"}, - {file = "rpds_py-0.21.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b9b76e2afd585803c53c5b29e992ecd183f68285b62fe2668383a18e74abe7a3"}, - {file = "rpds_py-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5afb5efde74c54724e1a01118c6e5c15e54e642c42a1ba588ab1f03544ac8c7a"}, - {file = "rpds_py-0.21.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:52c041802a6efa625ea18027a0723676a778869481d16803481ef6cc02ea8cb3"}, - {file = "rpds_py-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee1e4fc267b437bb89990b2f2abf6c25765b89b72dd4a11e21934df449e0c976"}, - {file = "rpds_py-0.21.0-cp313-none-win32.whl", hash = "sha256:0c025820b78817db6a76413fff6866790786c38f95ea3f3d3c93dbb73b632202"}, - {file = "rpds_py-0.21.0-cp313-none-win_amd64.whl", hash = "sha256:320c808df533695326610a1b6a0a6e98f033e49de55d7dc36a13c8a30cfa756e"}, - {file = "rpds_py-0.21.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:2c51d99c30091f72a3c5d126fad26236c3f75716b8b5e5cf8effb18889ced928"}, - {file = "rpds_py-0.21.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cbd7504a10b0955ea287114f003b7ad62330c9e65ba012c6223dba646f6ffd05"}, - {file = "rpds_py-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6dcc4949be728ede49e6244eabd04064336012b37f5c2200e8ec8eb2988b209c"}, - {file = "rpds_py-0.21.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f414da5c51bf350e4b7960644617c130140423882305f7574b6cf65a3081cecb"}, - {file = "rpds_py-0.21.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9afe42102b40007f588666bc7de82451e10c6788f6f70984629db193849dced1"}, - {file = "rpds_py-0.21.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b929c2bb6e29ab31f12a1117c39f7e6d6450419ab7464a4ea9b0b417174f044"}, - {file = "rpds_py-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8404b3717da03cbf773a1d275d01fec84ea007754ed380f63dfc24fb76ce4592"}, - {file = "rpds_py-0.21.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e12bb09678f38b7597b8346983d2323a6482dcd59e423d9448108c1be37cac9d"}, - {file = "rpds_py-0.21.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:58a0e345be4b18e6b8501d3b0aa540dad90caeed814c515e5206bb2ec26736fd"}, - {file = "rpds_py-0.21.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:c3761f62fcfccf0864cc4665b6e7c3f0c626f0380b41b8bd1ce322103fa3ef87"}, - {file = "rpds_py-0.21.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c2b2f71c6ad6c2e4fc9ed9401080badd1469fa9889657ec3abea42a3d6b2e1ed"}, - {file = "rpds_py-0.21.0-cp39-none-win32.whl", hash = "sha256:b21747f79f360e790525e6f6438c7569ddbfb1b3197b9e65043f25c3c9b489d8"}, - {file = "rpds_py-0.21.0-cp39-none-win_amd64.whl", hash = "sha256:0626238a43152918f9e72ede9a3b6ccc9e299adc8ade0d67c5e142d564c9a83d"}, - {file = "rpds_py-0.21.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6b4ef7725386dc0762857097f6b7266a6cdd62bfd209664da6712cb26acef035"}, - {file = "rpds_py-0.21.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:6bc0e697d4d79ab1aacbf20ee5f0df80359ecf55db33ff41481cf3e24f206919"}, - {file = "rpds_py-0.21.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da52d62a96e61c1c444f3998c434e8b263c384f6d68aca8274d2e08d1906325c"}, - {file = "rpds_py-0.21.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:98e4fe5db40db87ce1c65031463a760ec7906ab230ad2249b4572c2fc3ef1f9f"}, - {file = "rpds_py-0.21.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30bdc973f10d28e0337f71d202ff29345320f8bc49a31c90e6c257e1ccef4333"}, - {file = "rpds_py-0.21.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:faa5e8496c530f9c71f2b4e1c49758b06e5f4055e17144906245c99fa6d45356"}, - {file = "rpds_py-0.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32eb88c30b6a4f0605508023b7141d043a79b14acb3b969aa0b4f99b25bc7d4a"}, - {file = "rpds_py-0.21.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a89a8ce9e4e75aeb7fa5d8ad0f3fecdee813802592f4f46a15754dcb2fd6b061"}, - {file = "rpds_py-0.21.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:241e6c125568493f553c3d0fdbb38c74babf54b45cef86439d4cd97ff8feb34d"}, - {file = "rpds_py-0.21.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:3b766a9f57663396e4f34f5140b3595b233a7b146e94777b97a8413a1da1be18"}, - {file = "rpds_py-0.21.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:af4a644bf890f56e41e74be7d34e9511e4954894d544ec6b8efe1e21a1a8da6c"}, - {file = "rpds_py-0.21.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3e30a69a706e8ea20444b98a49f386c17b26f860aa9245329bab0851ed100677"}, - {file = "rpds_py-0.21.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:031819f906bb146561af051c7cef4ba2003d28cff07efacef59da973ff7969ba"}, - {file = "rpds_py-0.21.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b876f2bc27ab5954e2fd88890c071bd0ed18b9c50f6ec3de3c50a5ece612f7a6"}, - {file = "rpds_py-0.21.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc5695c321e518d9f03b7ea6abb5ea3af4567766f9852ad1560f501b17588c7b"}, - {file = "rpds_py-0.21.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4de1da871b5c0fd5537b26a6fc6814c3cc05cabe0c941db6e9044ffbb12f04a"}, - {file = "rpds_py-0.21.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:878f6fea96621fda5303a2867887686d7a198d9e0f8a40be100a63f5d60c88c9"}, - {file = "rpds_py-0.21.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8eeec67590e94189f434c6d11c426892e396ae59e4801d17a93ac96b8c02a6c"}, - {file = "rpds_py-0.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ff2eba7f6c0cb523d7e9cff0903f2fe1feff8f0b2ceb6bd71c0e20a4dcee271"}, - {file = "rpds_py-0.21.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a429b99337062877d7875e4ff1a51fe788424d522bd64a8c0a20ef3021fdb6ed"}, - {file = "rpds_py-0.21.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:d167e4dbbdac48bd58893c7e446684ad5d425b407f9336e04ab52e8b9194e2ed"}, - {file = "rpds_py-0.21.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:4eb2de8a147ffe0626bfdc275fc6563aa7bf4b6db59cf0d44f0ccd6ca625a24e"}, - {file = "rpds_py-0.21.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e78868e98f34f34a88e23ee9ccaeeec460e4eaf6db16d51d7a9b883e5e785a5e"}, - {file = "rpds_py-0.21.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4991ca61656e3160cdaca4851151fd3f4a92e9eba5c7a530ab030d6aee96ec89"}, - {file = "rpds_py-0.21.0.tar.gz", hash = "sha256:ed6378c9d66d0de903763e7706383d60c33829581f0adff47b6535f1802fa6db"}, + {file = "rpds_py-0.22.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:6c7b99ca52c2c1752b544e310101b98a659b720b21db00e65edca34483259967"}, + {file = "rpds_py-0.22.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:be2eb3f2495ba669d2a985f9b426c1797b7d48d6963899276d22f23e33d47e37"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70eb60b3ae9245ddea20f8a4190bd79c705a22f8028aaf8bbdebe4716c3fab24"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4041711832360a9b75cfb11b25a6a97c8fb49c07b8bd43d0d02b45d0b499a4ff"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64607d4cbf1b7e3c3c8a14948b99345eda0e161b852e122c6bb71aab6d1d798c"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e69b0a0e2537f26d73b4e43ad7bc8c8efb39621639b4434b76a3de50c6966e"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc27863442d388870c1809a87507727b799c8460573cfbb6dc0eeaef5a11b5ec"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e79dd39f1e8c3504be0607e5fc6e86bb60fe3584bec8b782578c3b0fde8d932c"}, + {file = "rpds_py-0.22.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e0fa2d4ec53dc51cf7d3bb22e0aa0143966119f42a0c3e4998293a3dd2856b09"}, + {file = "rpds_py-0.22.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fda7cb070f442bf80b642cd56483b5548e43d366fe3f39b98e67cce780cded00"}, + {file = "rpds_py-0.22.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cff63a0272fcd259dcc3be1657b07c929c466b067ceb1c20060e8d10af56f5bf"}, + {file = "rpds_py-0.22.3-cp310-cp310-win32.whl", hash = "sha256:9bd7228827ec7bb817089e2eb301d907c0d9827a9e558f22f762bb690b131652"}, + {file = "rpds_py-0.22.3-cp310-cp310-win_amd64.whl", hash = "sha256:9beeb01d8c190d7581a4d59522cd3d4b6887040dcfc744af99aa59fef3e041a8"}, + {file = "rpds_py-0.22.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d20cfb4e099748ea39e6f7b16c91ab057989712d31761d3300d43134e26e165f"}, + {file = "rpds_py-0.22.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:68049202f67380ff9aa52f12e92b1c30115f32e6895cd7198fa2a7961621fc5a"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb4f868f712b2dd4bcc538b0a0c1f63a2b1d584c925e69a224d759e7070a12d5"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bc51abd01f08117283c5ebf64844a35144a0843ff7b2983e0648e4d3d9f10dbb"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f3cec041684de9a4684b1572fe28c7267410e02450f4561700ca5a3bc6695a2"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7ef9d9da710be50ff6809fed8f1963fecdfecc8b86656cadfca3bc24289414b0"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59f4a79c19232a5774aee369a0c296712ad0e77f24e62cad53160312b1c1eaa1"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a60bce91f81ddaac922a40bbb571a12c1070cb20ebd6d49c48e0b101d87300d"}, + {file = "rpds_py-0.22.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e89391e6d60251560f0a8f4bd32137b077a80d9b7dbe6d5cab1cd80d2746f648"}, + {file = "rpds_py-0.22.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e3fb866d9932a3d7d0c82da76d816996d1667c44891bd861a0f97ba27e84fc74"}, + {file = "rpds_py-0.22.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1352ae4f7c717ae8cba93421a63373e582d19d55d2ee2cbb184344c82d2ae55a"}, + {file = "rpds_py-0.22.3-cp311-cp311-win32.whl", hash = "sha256:b0b4136a252cadfa1adb705bb81524eee47d9f6aab4f2ee4fa1e9d3cd4581f64"}, + {file = "rpds_py-0.22.3-cp311-cp311-win_amd64.whl", hash = "sha256:8bd7c8cfc0b8247c8799080fbff54e0b9619e17cdfeb0478ba7295d43f635d7c"}, + {file = "rpds_py-0.22.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:27e98004595899949bd7a7b34e91fa7c44d7a97c40fcaf1d874168bb652ec67e"}, + {file = "rpds_py-0.22.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1978d0021e943aae58b9b0b196fb4895a25cc53d3956b8e35e0b7682eefb6d56"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:655ca44a831ecb238d124e0402d98f6212ac527a0ba6c55ca26f616604e60a45"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:feea821ee2a9273771bae61194004ee2fc33f8ec7db08117ef9147d4bbcbca8e"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22bebe05a9ffc70ebfa127efbc429bc26ec9e9b4ee4d15a740033efda515cf3d"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3af6e48651c4e0d2d166dc1b033b7042ea3f871504b6805ba5f4fe31581d8d38"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67ba3c290821343c192f7eae1d8fd5999ca2dc99994114643e2f2d3e6138b15"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:02fbb9c288ae08bcb34fb41d516d5eeb0455ac35b5512d03181d755d80810059"}, + {file = "rpds_py-0.22.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f56a6b404f74ab372da986d240e2e002769a7d7102cc73eb238a4f72eec5284e"}, + {file = "rpds_py-0.22.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0a0461200769ab3b9ab7e513f6013b7a97fdeee41c29b9db343f3c5a8e2b9e61"}, + {file = "rpds_py-0.22.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8633e471c6207a039eff6aa116e35f69f3156b3989ea3e2d755f7bc41754a4a7"}, + {file = "rpds_py-0.22.3-cp312-cp312-win32.whl", hash = "sha256:593eba61ba0c3baae5bc9be2f5232430453fb4432048de28399ca7376de9c627"}, + {file = "rpds_py-0.22.3-cp312-cp312-win_amd64.whl", hash = "sha256:d115bffdd417c6d806ea9069237a4ae02f513b778e3789a359bc5856e0404cc4"}, + {file = "rpds_py-0.22.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ea7433ce7e4bfc3a85654aeb6747babe3f66eaf9a1d0c1e7a4435bbdf27fea84"}, + {file = "rpds_py-0.22.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6dd9412824c4ce1aca56c47b0991e65bebb7ac3f4edccfd3f156150c96a7bf25"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20070c65396f7373f5df4005862fa162db5d25d56150bddd0b3e8214e8ef45b4"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0b09865a9abc0ddff4e50b5ef65467cd94176bf1e0004184eb915cbc10fc05c5"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3453e8d41fe5f17d1f8e9c383a7473cd46a63661628ec58e07777c2fff7196dc"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5d36399a1b96e1a5fdc91e0522544580dbebeb1f77f27b2b0ab25559e103b8b"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:009de23c9c9ee54bf11303a966edf4d9087cd43a6003672e6aa7def643d06518"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1aef18820ef3e4587ebe8b3bc9ba6e55892a6d7b93bac6d29d9f631a3b4befbd"}, + {file = "rpds_py-0.22.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f60bd8423be1d9d833f230fdbccf8f57af322d96bcad6599e5a771b151398eb2"}, + {file = "rpds_py-0.22.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:62d9cfcf4948683a18a9aff0ab7e1474d407b7bab2ca03116109f8464698ab16"}, + {file = "rpds_py-0.22.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9253fc214112405f0afa7db88739294295f0e08466987f1d70e29930262b4c8f"}, + {file = "rpds_py-0.22.3-cp313-cp313-win32.whl", hash = "sha256:fb0ba113b4983beac1a2eb16faffd76cb41e176bf58c4afe3e14b9c681f702de"}, + {file = "rpds_py-0.22.3-cp313-cp313-win_amd64.whl", hash = "sha256:c58e2339def52ef6b71b8f36d13c3688ea23fa093353f3a4fee2556e62086ec9"}, + {file = "rpds_py-0.22.3-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f82a116a1d03628a8ace4859556fb39fd1424c933341a08ea3ed6de1edb0283b"}, + {file = "rpds_py-0.22.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3dfcbc95bd7992b16f3f7ba05af8a64ca694331bd24f9157b49dadeeb287493b"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59259dc58e57b10e7e18ce02c311804c10c5a793e6568f8af4dead03264584d1"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5725dd9cc02068996d4438d397e255dcb1df776b7ceea3b9cb972bdb11260a83"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99b37292234e61325e7a5bb9689e55e48c3f5f603af88b1642666277a81f1fbd"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:27b1d3b3915a99208fee9ab092b8184c420f2905b7d7feb4aeb5e4a9c509b8a1"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f612463ac081803f243ff13cccc648578e2279295048f2a8d5eb430af2bae6e3"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f73d3fef726b3243a811121de45193c0ca75f6407fe66f3f4e183c983573e130"}, + {file = "rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3f21f0495edea7fdbaaa87e633a8689cd285f8f4af5c869f27bc8074638ad69c"}, + {file = "rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1e9663daaf7a63ceccbbb8e3808fe90415b0757e2abddbfc2e06c857bf8c5e2b"}, + {file = "rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a76e42402542b1fae59798fab64432b2d015ab9d0c8c47ba7addddbaf7952333"}, + {file = "rpds_py-0.22.3-cp313-cp313t-win32.whl", hash = "sha256:69803198097467ee7282750acb507fba35ca22cc3b85f16cf45fb01cb9097730"}, + {file = "rpds_py-0.22.3-cp313-cp313t-win_amd64.whl", hash = "sha256:f5cf2a0c2bdadf3791b5c205d55a37a54025c6e18a71c71f82bb536cf9a454bf"}, + {file = "rpds_py-0.22.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:378753b4a4de2a7b34063d6f95ae81bfa7b15f2c1a04a9518e8644e81807ebea"}, + {file = "rpds_py-0.22.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3445e07bf2e8ecfeef6ef67ac83de670358abf2996916039b16a218e3d95e97e"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b2513ba235829860b13faa931f3b6846548021846ac808455301c23a101689d"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eaf16ae9ae519a0e237a0f528fd9f0197b9bb70f40263ee57ae53c2b8d48aeb3"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:583f6a1993ca3369e0f80ba99d796d8e6b1a3a2a442dd4e1a79e652116413091"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4617e1915a539a0d9a9567795023de41a87106522ff83fbfaf1f6baf8e85437e"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c150c7a61ed4a4f4955a96626574e9baf1adf772c2fb61ef6a5027e52803543"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fa4331c200c2521512595253f5bb70858b90f750d39b8cbfd67465f8d1b596d"}, + {file = "rpds_py-0.22.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:214b7a953d73b5e87f0ebece4a32a5bd83c60a3ecc9d4ec8f1dca968a2d91e99"}, + {file = "rpds_py-0.22.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f47ad3d5f3258bd7058d2d506852217865afefe6153a36eb4b6928758041d831"}, + {file = "rpds_py-0.22.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f276b245347e6e36526cbd4a266a417796fc531ddf391e43574cf6466c492520"}, + {file = "rpds_py-0.22.3-cp39-cp39-win32.whl", hash = "sha256:bbb232860e3d03d544bc03ac57855cd82ddf19c7a07651a7c0fdb95e9efea8b9"}, + {file = "rpds_py-0.22.3-cp39-cp39-win_amd64.whl", hash = "sha256:cfbc454a2880389dbb9b5b398e50d439e2e58669160f27b60e5eca11f68ae17c"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d48424e39c2611ee1b84ad0f44fb3b2b53d473e65de061e3f460fc0be5f1939d"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:24e8abb5878e250f2eb0d7859a8e561846f98910326d06c0d51381fed59357bd"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b232061ca880db21fa14defe219840ad9b74b6158adb52ddf0e87bead9e8493"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac0a03221cdb5058ce0167ecc92a8c89e8d0decdc9e99a2ec23380793c4dcb96"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb0c341fa71df5a4595f9501df4ac5abfb5a09580081dffbd1ddd4654e6e9123"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf9db5488121b596dbfc6718c76092fda77b703c1f7533a226a5a9f65248f8ad"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b8db6b5b2d4491ad5b6bdc2bc7c017eec108acbf4e6785f42a9eb0ba234f4c9"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b3d504047aba448d70cf6fa22e06cb09f7cbd761939fdd47604f5e007675c24e"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:e61b02c3f7a1e0b75e20c3978f7135fd13cb6cf551bf4a6d29b999a88830a338"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:e35ba67d65d49080e8e5a1dd40101fccdd9798adb9b050ff670b7d74fa41c566"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:26fd7cac7dd51011a245f29a2cc6489c4608b5a8ce8d75661bb4a1066c52dfbe"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:177c7c0fce2855833819c98e43c262007f42ce86651ffbb84f37883308cb0e7d"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bb47271f60660803ad11f4c61b42242b8c1312a31c98c578f79ef9387bbde21c"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:70fb28128acbfd264eda9bf47015537ba3fe86e40d046eb2963d75024be4d055"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44d61b4b7d0c2c9ac019c314e52d7cbda0ae31078aabd0f22e583af3e0d79723"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f0e260eaf54380380ac3808aa4ebe2d8ca28b9087cf411649f96bad6900c728"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b25bc607423935079e05619d7de556c91fb6adeae9d5f80868dde3468657994b"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fb6116dfb8d1925cbdb52595560584db42a7f664617a1f7d7f6e32f138cdf37d"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a63cbdd98acef6570c62b92a1e43266f9e8b21e699c363c0fef13bd530799c11"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2b8f60e1b739a74bab7e01fcbe3dddd4657ec685caa04681df9d562ef15b625f"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2e8b55d8517a2fda8d95cb45d62a5a8bbf9dd0ad39c5b25c8833efea07b880ca"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:2de29005e11637e7a2361fa151f780ff8eb2543a0da1413bb951e9f14b699ef3"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:666ecce376999bf619756a24ce15bb14c5bfaf04bf00abc7e663ce17c3f34fe7"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:5246b14ca64a8675e0a7161f7af68fe3e910e6b90542b4bfb5439ba752191df6"}, + {file = "rpds_py-0.22.3.tar.gz", hash = "sha256:e32fee8ab45d3c2db6da19a5323bc3362237c8b653c70194414b892fd06a080d"}, ] [[package]] @@ -1409,13 +1450,13 @@ files = [ [[package]] name = "six" -version = "1.16.0" +version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] [[package]] @@ -1516,13 +1557,13 @@ files = [ [[package]] name = "types-python-dateutil" -version = "2.9.0.20241003" +version = "2.9.0.20241206" description = "Typing stubs for python-dateutil" optional = false python-versions = ">=3.8" files = [ - {file = "types-python-dateutil-2.9.0.20241003.tar.gz", hash = "sha256:58cb85449b2a56d6684e41aeefb4c4280631246a0da1a719bdbe6f3fb0317446"}, - {file = "types_python_dateutil-2.9.0.20241003-py3-none-any.whl", hash = "sha256:250e1d8e80e7bbc3a6c99b907762711d1a1cdd00e978ad39cb5940f6f0a87f3d"}, + {file = "types_python_dateutil-2.9.0.20241206-py3-none-any.whl", hash = "sha256:e248a4bc70a486d3e3ec84d0dc30eec3a5f979d6e7ee4123ae043eedbb987f53"}, + {file = "types_python_dateutil-2.9.0.20241206.tar.gz", hash = "sha256:18f493414c26ffba692a72369fea7a154c502646301ebfe3d56a04b3767284cb"}, ] [[package]] @@ -1591,13 +1632,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "uvicorn" -version = "0.32.1" +version = "0.34.0" description = "The lightning-fast ASGI server." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "uvicorn-0.32.1-py3-none-any.whl", hash = "sha256:82ad92fd58da0d12af7482ecdb5f2470a04c9c9a53ced65b9bbb4a205377602e"}, - {file = "uvicorn-0.32.1.tar.gz", hash = "sha256:ee9519c246a72b1c084cea8d3b44ed6026e78a4a309cbedae9c37e4cb9fbb175"}, + {file = "uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4"}, + {file = "uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9"}, ] [package.dependencies] @@ -1758,7 +1799,10 @@ pydantic = ">=1.9.0,<3" sniffio = "*" typing-extensions = ">=4.7,<5" +[extras] +redis = [] + [metadata] lock-version = "2.0" python-versions = ">=3.9.2, <4.0" -content-hash = "ddd67e80eb5e5990db930710506e771990d96c0af8647ec2c0c9e15cefdfd7a5" +content-hash = "eef09bbf43e87914f27d85100a012608014f55be3d4e89c5b70f4d0810579a10" diff --git a/pyproject.toml b/pyproject.toml index ab7e2d602..9a7dd4371 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,6 @@ websockets = ">= 12, < 13" writer-sdk = ">= 1.5.0, < 2" python-multipart = ">=0.0.7, < 1" - [tool.poetry.group.build] optional = true @@ -68,6 +67,11 @@ pytest-asyncio = ">= 0.23.4, < 1" ruff = "^0.3.4" types-requests = "^2.31.0.20240406" +[tool.poetry.group.redis] +optional = true + +[tool.poetry.group.redis.dependencies] +redis = "^5.2.1" [tool.poetry.group.dev.dependencies] types-python-dateutil = "^2.9.0.20240316" @@ -75,6 +79,8 @@ types-python-dateutil = "^2.9.0.20240316" [tool.poetry.scripts] writer = 'writer.command_line:main' +[tool.poetry.extras] +redis = ["redis"] [tool.ruff] exclude = [ diff --git a/src/ui/src/builder/BuilderEmbeddedCodeEditor.vue b/src/ui/src/builder/BuilderEmbeddedCodeEditor.vue index 4a2e3c923..7c348f1ac 100644 --- a/src/ui/src/builder/BuilderEmbeddedCodeEditor.vue +++ b/src/ui/src/builder/BuilderEmbeddedCodeEditor.vue @@ -59,6 +59,7 @@ onMounted(() => { editor = monaco.editor.create(editorContainerEl.value, { value: modelValue.value, language: props.language, + readOnly: props.disabled, ...VARIANTS_SETTINGS[props.variant], }); editor.getModel().onDidChangeContent(() => { @@ -84,11 +85,11 @@ onUnmounted(() => { .BuilderEmbeddedCodeEditor { height: 100%; width: 100%; - min-height: 200px; + min-height: 100px; } .editorContainer { - min-height: 200px; + min-height: 100px; width: 100%; height: 100%; overflow: hidden; diff --git a/src/ui/src/builder/BuilderTooltip.vue b/src/ui/src/builder/BuilderTooltip.vue index 889ec1e87..cbca66472 100644 --- a/src/ui/src/builder/BuilderTooltip.vue +++ b/src/ui/src/builder/BuilderTooltip.vue @@ -37,7 +37,9 @@ const position = ref<{ }>({ top: 0, left: 0 }); async function setUpAndShowTooltip() { + if (!trackedElement) return; tooltipText.value = trackedElement.dataset.writerTooltip; + if (!tooltipText.value) return; const gapPx = trackedElement.dataset.writerTooltipGap ? parseInt(trackedElement.dataset.writerTooltipGap) : DEFAULT_GAP_PX; diff --git a/src/ui/src/builder/settings/BuilderSettingsAPICode.vue b/src/ui/src/builder/settings/BuilderSettingsAPICode.vue new file mode 100644 index 000000000..9586c4b1d --- /dev/null +++ b/src/ui/src/builder/settings/BuilderSettingsAPICode.vue @@ -0,0 +1,132 @@ + + + + + diff --git a/src/ui/src/builder/settings/BuilderSettingsMain.vue b/src/ui/src/builder/settings/BuilderSettingsMain.vue index 68ddd2564..fd98b7f47 100644 --- a/src/ui/src/builder/settings/BuilderSettingsMain.vue +++ b/src/ui/src/builder/settings/BuilderSettingsMain.vue @@ -22,6 +22,7 @@ + Execute via API
@@ -42,6 +43,7 @@ import BuilderSettingsBinding from "./BuilderSettingsBinding.vue"; import BuilderSettingsVisibility from "./BuilderSettingsVisibility.vue"; import BuilderCopyText from "../BuilderCopyText.vue"; import BuilderAsyncLoader from "../BuilderAsyncLoader.vue"; +import BuilderSettingsAPICode from "./BuilderSettingsAPICode.vue"; const BuilderSettingsHandlers = defineAsyncComponent({ loader: () => import("./BuilderSettingsHandlers.vue"), diff --git a/src/ui/src/builder/sidebar/BuilderSidebarComponentTreeBranch.vue b/src/ui/src/builder/sidebar/BuilderSidebarComponentTreeBranch.vue index a3e168119..75808ddfb 100644 --- a/src/ui/src/builder/sidebar/BuilderSidebarComponentTreeBranch.vue +++ b/src/ui/src/builder/sidebar/BuilderSidebarComponentTreeBranch.vue @@ -141,7 +141,7 @@ const name = computed(() => { return component.value.content?.["element"]; } if (type == "workflows_workflow") { - return component.value.content?.["key"] ?? "Workflow"; + return component.value.content?.["key"] || "Workflow"; } return def.value?.name ?? `Unknown (${component.value.type})`; }); diff --git a/src/ui/src/core/index.ts b/src/ui/src/core/index.ts index 8368d99ea..7a1e33f70 100644 --- a/src/ui/src/core/index.ts +++ b/src/ui/src/core/index.ts @@ -333,6 +333,35 @@ export function generateCore() { sendFrontendMessage("event", messagePayload, callback, true); } + /** + * Sends a message to be hashed in the backend using the relevant keys. + * Due to security reasons, it works only in edit mode. + * + * @param message Messaged to be hashed + * @returns The hashed message + */ + async function hashMessage(message: string):Promise { + return new Promise((resolve, reject) => { + const messageCallback = (r: { + ok: boolean; + payload?: Record; + }) => { + if (!r.ok) { + reject("Couldn't connect to the server."); + return; + } + resolve(r.payload?.message); + }; + + sendFrontendMessage( + "hashRequest", + { message }, + messageCallback, + ); + }); + + } + async function sendCodeSaveRequest(newCode: string): Promise { const messageData = { code: newCode, @@ -572,6 +601,7 @@ export function generateCore() { addMailSubscription, init, forwardEvent, + hashMessage, runCode: readonly(runCode), sendCodeSaveRequest, sendCodeUpdate, diff --git a/src/writer/app_runner.py b/src/writer/app_runner.py index 616093a6e..5531a59ce 100644 --- a/src/writer/app_runner.py +++ b/src/writer/app_runner.py @@ -17,7 +17,7 @@ from pydantic import ValidationError from watchdog.observers.polling import PollingObserver -from writer import VERSION, audit_and_fix, core_ui, wf_project +from writer import VERSION, audit_and_fix, core_ui, crypto, wf_project from writer.core import ( Config, EventHandlerRegistry, @@ -36,6 +36,9 @@ ComponentUpdateRequestPayload, EventRequest, EventResponsePayload, + HashRequest, + HashRequestPayload, + HashRequestResponsePayload, InitSessionRequest, InitSessionRequestPayload, InitSessionResponsePayload, @@ -199,7 +202,7 @@ def _handle_event(self, session: WriterSession, event: WriterEvent) -> EventResp session.session_state.clear_mail() return res_payload - + def _handle_state_enquiry(self, session: WriterSession) -> StateEnquiryResponsePayload: import traceback as tb @@ -236,7 +239,13 @@ def _handle_state_content(self, session: WriterSession) -> StateContentResponseP tb.format_exc()) return StateContentResponsePayload(state=serialized_state) - + + def _handle_hash_request(self, req_payload: HashRequestPayload) -> HashRequestResponsePayload: + res_payload = HashRequestResponsePayload( + message=crypto.get_hash(req_payload.message) + ) + return res_payload + def _handle_component_update(self, session: WriterSession, payload: ComponentUpdateRequestPayload) -> None: import writer ingest_bmc_component_tree(writer.base_component_tree, payload.components) @@ -303,6 +312,14 @@ def _handle_message(self, session_id: str, request: AppProcessServerRequest) -> payload=None ) + if self.mode == "edit" and type == "hashRequest": + hash_request_payload = HashRequestPayload.model_validate(request.payload) + return AppProcessServerResponse( + status="ok", + status_message=None, + payload=self._handle_hash_request(hash_request_payload) + ) + if self.mode == "edit" and type == "componentUpdate": cu_req_payload = ComponentUpdateRequestPayload.parse_obj( request.payload) @@ -745,6 +762,12 @@ async def handle_event(self, session_id: str, event: WriterEvent) -> AppProcessS payload=event )) + async def handle_hash_request(self, session_id: str, payload: HashRequestPayload) -> AppProcessServerResponse: + return await self.dispatch_message(session_id, HashRequest( + type="hashRequest", + payload=payload + )) + async def handle_state_enquiry(self, session_id: str) -> AppProcessServerResponse: return await self.dispatch_message(session_id, StateEnquiryRequest( type="stateEnquiry" diff --git a/src/writer/blocks/foreach.py b/src/writer/blocks/foreach.py index b5268d625..b8f56a82c 100644 --- a/src/writer/blocks/foreach.py +++ b/src/writer/blocks/foreach.py @@ -22,19 +22,12 @@ def register(cls, type: str): }, "items": { "name": "Items", - "desc": "The item value will be passed in the execution environment and will be available at @{item}, its id at @{itemId}.", + "desc": "The item value will be passed in the execution environment and will be available at @{payload.item}, its id at @{payload.itemId}.", "default": "{}", "init": '{ "fr": "France", "pl": "Poland" }', "type": "Object", "control": "Textarea" - }, - "executionEnv": { - "name": "Execution environment", - "desc": "You can add other values to the execution environment.", - "default": "{}", - "type": "Object", - "control": "Textarea" - }, + } }, "outs": { "success": { @@ -52,14 +45,20 @@ def register(cls, type: str): )) def _run_workflow_for_item(self, workflow_key, base_execution_environment, item_id, item): - expanded_execution_environment = base_execution_environment | { "itemId": item_id, "item": item } + expanded_execution_environment = base_execution_environment | { + "payload": { + "itemId": item_id, + "item": item + } + } return self.runner.run_workflow_by_key(workflow_key, expanded_execution_environment) def run(self): try: workflow_key = self._get_field("workflowKey") items = self._get_field("items", as_json=True) - base_execution_environment = self._get_field("executionEnv", as_json=True) + base_execution_environment = self.execution_environment + std_items = items result = None if isinstance(items, list): diff --git a/src/writer/blocks/httprequest.py b/src/writer/blocks/httprequest.py index 6d403734c..de2642236 100644 --- a/src/writer/blocks/httprequest.py +++ b/src/writer/blocks/httprequest.py @@ -1,3 +1,5 @@ +import re + import requests from writer.abstract import register_abstract_template @@ -7,6 +9,8 @@ class HTTPRequest(WorkflowBlock): + CONTROL_CHARS = re.compile(r"[\x00-\x1f\x7f]") + @classmethod def register(cls, type: str): super(HTTPRequest, cls).register(type) @@ -64,6 +68,12 @@ def register(cls, type: str): } )) + def _clean_json_string(self, s: str) -> str: + + """ Remove control characters, which aren't tolerated by JSON loads() strict mode, from string.""" + + return HTTPRequest.CONTROL_CHARS.sub("", s) + def run(self): import json @@ -71,7 +81,7 @@ def run(self): method = self._get_field("method", False, "GET") url = self._get_field("url") headers = self._get_field("headers", True) - body = self._get_field("body") + body = self._clean_json_string(self._get_field("body")) req = requests.request(method, url, headers=headers, data=body) content_type = req.headers.get("Content-Type") diff --git a/src/writer/blocks/runworkflow.py b/src/writer/blocks/runworkflow.py index e9042cc42..00e1189ec 100644 --- a/src/writer/blocks/runworkflow.py +++ b/src/writer/blocks/runworkflow.py @@ -19,11 +19,11 @@ def register(cls, type: str): "name": "Workflow key", "type": "Text", }, - "executionEnv": { - "name": "Execution environment", - "desc": "Values passed will be available using the template syntax i.e. @{my_var}", + "payload": { + "name": "Payload", + "desc": "The value specified will be available using the template syntax i.e. @{payload}", "default": "{}", - "type": "Object", + "type": "Text", "control": "Textarea" }, }, @@ -45,9 +45,9 @@ def register(cls, type: str): def run(self): try: workflow_key = self._get_field("workflowKey") - execution_environment = self._get_field("executionEnv", as_json=True) - - return_value = self.runner.run_workflow_by_key(workflow_key, execution_environment) + payload = self._get_field("payload") + expanded_execution_environment = self.execution_environment | { "payload": payload } + return_value = self.runner.run_workflow_by_key(workflow_key, expanded_execution_environment) self.result = return_value self.outcome = "success" except BaseException as e: diff --git a/src/writer/command_line.py b/src/writer/command_line.py index f7ff8c315..a460fc398 100644 --- a/src/writer/command_line.py +++ b/src/writer/command_line.py @@ -21,8 +21,9 @@ def main(): @main.command() @click.option('--host', default="127.0.0.1", help="Host to run the app on") @click.option('--port', default=None, help="Port to run the app on") +@click.option("--enable-jobs-api", help="Set this flag to enable the Jobs API, allowing you to execute jobs without user interaction.", is_flag=True) @click.argument('path') -def run(path: str, host: str, port: Optional[int]): +def run(path: str, host: str, port: Optional[int], enable_jobs_api: bool): """Run the app from PATH folder in run mode.""" abs_path = os.path.abspath(path) @@ -35,7 +36,7 @@ def run(path: str, host: str, port: Optional[int]): raise click.ClickException(f"There’s no Writer Framework project at this location : {abs_path}") writer.serve.serve( - abs_path, mode="run", port=port, host=host, enable_server_setup=True) + abs_path, mode="run", port=port, host=host, enable_server_setup=True, enable_jobs_api=enable_jobs_api) @main.command() @click.option('--host', default="127.0.0.1", help="Host to run the app on") @@ -43,6 +44,7 @@ def run(path: str, host: str, port: Optional[int]): @click.option('--enable-remote-edit', help="Set this flag to allow non-local requests in edit mode.", is_flag=True) @click.option('--enable-server-setup', help="Set this flag to enable server setup hook in edit mode.", is_flag=True) @click.option("--no-interactive", help="Set the flask to ask the app to run without asking anything to the user", is_flag=True) +@click.option("--enable-jobs-api", help="Set this flag to enable the Jobs API, allowing you to execute jobs without user interaction.", is_flag=True) @click.argument('path') def edit( path: str, @@ -50,7 +52,8 @@ def edit( host: str, enable_remote_edit: bool, enable_server_setup: bool, - no_interactive: bool + no_interactive: bool, + enable_jobs_api: bool, ): """Run the app from PATH folder in edit mode.""" abs_path = os.path.abspath(path) @@ -70,7 +73,7 @@ def edit( writer.serve.serve( abs_path, mode="edit", port=port, host=host, - enable_remote_edit=enable_remote_edit, enable_server_setup=enable_server_setup) + enable_remote_edit=enable_remote_edit, enable_server_setup=enable_server_setup, enable_jobs_api=enable_jobs_api) @main.command() @click.argument('path') diff --git a/src/writer/core.py b/src/writer/core.py index 9c0074340..74f40cd99 100644 --- a/src/writer/core.py +++ b/src/writer/core.py @@ -1240,6 +1240,8 @@ def transform(self, ev: WriterEvent) -> None: custom_event_name = ev.type[3:] func_name = "_transform_" + custom_event_name.replace("-", "_") if not hasattr(self, func_name): + if ev.isSafe: + return ev.payload = {} raise ValueError( "No payload transformer available for custom event type.") @@ -1605,9 +1607,9 @@ def fn(payload, context, session): "session": session } if workflow_key: - self.workflow_runner.run_workflow_by_key(workflow_key, execution_environment) + return self.workflow_runner.run_workflow_by_key(workflow_key, execution_environment) elif workflow_id: - self.workflow_runner.run_workflow(workflow_id, execution_environment, "Workflow execution triggered on demand") + return self.workflow_runner.run_workflow(workflow_id, execution_environment, "Workflow execution triggered on demand") return fn def _get_handler_callable(self, handler: str) -> Optional[Callable]: diff --git a/src/writer/crypto.py b/src/writer/crypto.py new file mode 100644 index 000000000..c55e255de --- /dev/null +++ b/src/writer/crypto.py @@ -0,0 +1,23 @@ + +import hashlib +import os + +from fastapi import HTTPException, Request + +HASH_SALT = "a9zHYfIeL0" + +def get_hash(message: str): + base_hash = os.getenv("WRITER_SECRET_KEY") + if not base_hash: + raise ValueError("Environment variable WRITER_SECRET_KEY needs to be set up in" + \ + "order to enable operations which require hash generation, such as creating async jobs.") + assert HASH_SALT + combined = base_hash + HASH_SALT + message + return hashlib.sha256(combined.encode()).hexdigest() + +def verify_message_authorization_signature(message: str, request: Request): + auth_header = request.headers.get("Authorization") + if not auth_header: + raise HTTPException(status_code=401, detail="Unauthorized. Token not specified.") + if auth_header != f"Bearer {get_hash(message)}": + raise HTTPException(status_code=403, detail="Forbidden. Incorrect token.") \ No newline at end of file diff --git a/src/writer/evaluator.py b/src/writer/evaluator.py index 1d8ccf685..c4100ac27 100644 --- a/src/writer/evaluator.py +++ b/src/writer/evaluator.py @@ -32,7 +32,7 @@ def __init__(self, state: "WriterState", component_tree: "ComponentTree"): def evaluate_field(self, instance_path: InstancePath, field_key: str, as_json=False, default_field_value="", base_context={}) -> Any: def decode_json(text): try: - return json.loads(text) + return json.loads(text, strict=False) except json.JSONDecodeError as exception: raise WriterConfigurationError("Error decoding JSON. " + str(exception)) from exception diff --git a/src/writer/serve.py b/src/writer/serve.py index 459069d1b..caa310726 100644 --- a/src/writer/serve.py +++ b/src/writer/serve.py @@ -2,6 +2,7 @@ import html import importlib.util import io +import json import logging import mimetypes import os @@ -9,26 +10,29 @@ import pathlib import socket import textwrap +import time import typing from contextlib import asynccontextmanager from importlib.machinery import ModuleSpec -from typing import Any, Callable, Dict, List, Literal, Optional, Set, Tuple, Union, cast +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type, Union, cast from urllib.parse import urlsplit import uvicorn from fastapi import FastAPI, HTTPException, Request, Response -from fastapi.responses import FileResponse +from fastapi.responses import FileResponse, JSONResponse from fastapi.routing import Mount from fastapi.staticfiles import StaticFiles from pydantic import ValidationError from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState -from writer import VERSION, abstract +from writer import VERSION, abstract, crypto from writer.app_runner import AppRunner from writer.ss_types import ( AppProcessServerResponse, ComponentUpdateRequestPayload, EventResponsePayload, + HashRequestPayload, + HashRequestResponsePayload, InitRequestBody, InitResponseBodyEdit, InitResponseBodyRun, @@ -50,13 +54,16 @@ class JobVault: + SCHEMES:List[str] = [] + job_vault_implementations: List[Type["JobVault"]] = [] + def __init__(self): self.counter = 0 self.vault = {} def generate_job_id(self): self.counter += 1 - return self.counter + return str(self.counter) def set(self, job_id: str, value: Any): self.vault[job_id] = value @@ -64,6 +71,71 @@ def set(self, job_id: str, value: Any): def get(self, job_id: str): return self.vault.get(job_id) + @classmethod + def register(cls, klass: Type["JobVault"]): + cls.job_vault_implementations.insert(0, klass) + + @classmethod + def _get_matching_implementation(cls, connection_string): + for job_vault_implementation in cls.job_vault_implementations: + for scheme in job_vault_implementation.SCHEMES: + if connection_string.startswith(scheme): + return job_vault_implementation + + @classmethod + def create_vault(cls): + connection_string = os.getenv("WRITER_PERSISTENT_STORE") + if not connection_string: + return cls() + + matching_implementation = cls._get_matching_implementation(connection_string) + if not matching_implementation: + supported_schemes = [scheme for implementation in JobVault.job_vault_implementations for scheme in implementation.SCHEMES] + supported_schemes_msg = ", ".join(supported_schemes) + logging.error(f"No matching implementation found for { connection_string }. Falling back to in-memory JobVault. \ + Supported schemes: {supported_schemes_msg}.") + return cls() + + try: + return matching_implementation() + except Exception as e: + logging.error(f"There was an error connecting to { connection_string }. Falling back to in-memory JobVault. {repr(e)}") + return cls() + + +class RedisJobVault(JobVault): + + SCHEMES = ["redis://", "rediss://", "redis-socket://", "redis-sentinel://"] + DEFAULT_TTL = 86400 + + def __init__(self): + import redis # type: ignore + super().__init__() + redis_connection_string = os.getenv("WRITER_PERSISTENT_STORE") + self.redis_client = redis.from_url(redis_connection_string, decode_responses=True, socket_timeout=30) + self.counter_key = "job_counter" + if not self.redis_client.exists(self.counter_key): + self.redis_client.set(self.counter_key, 0) + + def generate_job_id(self): + job_id = self.redis_client.incr(self.counter_key) + return str(job_id) + + def set(self, job_id: str, value: Any): + ttl = RedisJobVault.DEFAULT_TTL + env_ttl = os.getenv("WRITER_PERSISTENT_STORE_TTL") + if env_ttl is not None: + ttl = int(env_ttl) + json_str = json.dumps(value) + self.redis_client.set(f"job:{job_id}", json_str, ex=ttl) + + def get(self, job_id: str): + json_str = self.redis_client.get(f"job:{job_id}") + if not json_str: + return None + return json.loads(json_str) + + class WriterState(typing.Protocol): app_runner: AppRunner writer_app: bool @@ -87,7 +159,8 @@ def get_asgi_app( enable_remote_edit: bool = False, enable_server_setup: bool = True, on_load: Optional[Callable] = None, - on_shutdown: Optional[Callable] = None + on_shutdown: Optional[Callable] = None, + enable_jobs_api: bool = False ) -> WriterFastAPI: """ Builds an ASGI server that can be injected into another ASGI application @@ -139,7 +212,6 @@ async def lifespan(asgi_app: FastAPI): """ app.state.writer_app = True app.state.app_runner = app_runner - app.state.job_vault = JobVault() def _get_extension_paths() -> List[str]: extensions_path = pathlib.Path(user_app_path) / "extensions" @@ -242,6 +314,115 @@ async def init(initBody: InitRequestBody, request: Request, response: Response) if serve_mode == "edit": return _get_edit_starter_pack(app_response.payload) + # Jobs + + async def _get_payload_as_json(request: Request): + payload = None + body = await request.body() + if not body: + return None + try: + payload = await request.json() + except json.JSONDecodeError: + raise HTTPException(status_code=400, detail="Cannot parse the payload.") + return payload + + @app.post("/api/job/workflow/{workflow_key}") + async def create_workflow_job(workflow_key: str, request: Request, response: Response): + if not enable_jobs_api: + raise HTTPException(status_code=404) + + crypto.verify_message_authorization_signature(f"create_job_{workflow_key}", request) + + def serialize_result(data): + if isinstance(data, list): + return [serialize_result(item) for item in data] + if isinstance(data, dict): + return {k : serialize_result(v) for k, v in data.items()} + if isinstance(data, (str, int, float, bool, type(None))): + return data + try: + return json.loads(json.dumps(data)) + except (TypeError, OverflowError): + return f"Can't be displayed. Value of type: {str(type(data))}." + + def update_job(job_id: str, job_info: dict): + current_job_info = app.state.job_vault.get(job_id) + if not current_job_info: + raise RuntimeError("Job not found.") + merged_info = current_job_info | { "finished_at": int(time.time()) } | job_info + app.state.job_vault.set(job_id, merged_info) + + def job_done_callback(task: asyncio.Task, job_id: str): + try: + apsr: Optional[AppProcessServerResponse] = task.result() + if apsr is None or apsr.status != "ok": + update_job(job_id, {"status": "error"}) + return + result = None + if apsr.payload and apsr.payload.result: + result = apsr.payload.result.get("result") + update_job(job_id, { + "status": "complete", + "result": serialize_result(result) + }) + except Exception as e: + update_job(job_id, {"status": "error"}) + raise e + + app_response = await app_runner.init_session(InitSessionRequestPayload( + cookies=dict(request.cookies), + headers=dict(request.headers), + proposedSessionId=None + )) + + if not app_response or not app_response.payload: + raise HTTPException(status_code=500, detail="Cannot initialize session.") + session_id = app_response.payload.sessionId + is_session_ok = await app_runner.check_session(session_id) + if not is_session_ok: + raise HTTPException(status_code=500, detail="Cannot initialize session.") + + loop = asyncio.get_running_loop() + task = loop.create_task(app_runner.handle_event( + session_id, WriterEvent( + type="wf-builtin-run", + isSafe=True, + handler=f"$runWorkflow_{workflow_key}", + payload=await _get_payload_as_json(request) + ))) + + job_id = app.state.job_vault.generate_job_id() + app.state.job_vault.set(job_id, { + "id": job_id, + "status": "in progress", + "created_at": int(time.time()) + }) + task.add_done_callback(lambda t: job_done_callback(t, job_id)) + return { + "id": job_id, + "token": crypto.get_hash(f"get_job_{job_id}") + } + + @app.get("/api/job/{job_id}") + async def get_workflow_job(job_id: str, request: Request, response: Response): + if not enable_jobs_api: + raise HTTPException(status_code=404) + + crypto.verify_message_authorization_signature(f"get_job_{job_id}", request) + job = app.state.job_vault.get(job_id) + + if not job: + return JSONResponse(status_code=404, content={ + "id": job_id, + "status": "not found" + }) + + status_code = 200 + if job.get("status") == "error": + status_code = 400 + + return JSONResponse(status_code=status_code, content=job) # Streaming @@ -301,6 +482,9 @@ async def _stream_incoming_requests(websocket: WebSocket, session_id: str): elif req_message.type == "stateEnquiry": new_task = asyncio.create_task( _handle_state_enquiry_message(websocket, session_id, req_message)) + elif serve_mode == "edit" and req_message.type == "hashRequest": + new_task = asyncio.create_task( + _handle_hash_request(websocket, session_id, req_message)) elif serve_mode == "edit": new_task = asyncio.create_task( _handle_incoming_edit_message(websocket, session_id, req_message)) @@ -392,6 +576,21 @@ async def _handle_state_enquiry_message(websocket: WebSocket, session_id: str, r response.payload = res_payload await websocket.send_json(response.model_dump()) + async def _handle_hash_request(websocket: WebSocket, session_id: str, req_message: WriterWebsocketIncoming): + response = WriterWebsocketOutgoing( + messageType=f"{req_message.type}Response", + trackingId=req_message.trackingId, + payload=None + ) + apsr: Optional[AppProcessServerResponse] = None + apsr = await app_runner.handle_hash_request(session_id, HashRequestPayload( + message=req_message.payload.get("message", "") + )) + if apsr is not None and apsr.payload is not None: + response.payload = typing.cast( + HashRequestResponsePayload, apsr.payload).model_dump() + await websocket.send_json(response.model_dump()) + async def _stream_outgoing_announcements(websocket: WebSocket): """ @@ -489,10 +688,14 @@ async def stream(websocket: WebSocket): ) ) + JobVault.register(RedisJobVault) + # Return if enable_server_setup is True: _execute_server_setup_hook(user_app_path) + app.state.job_vault = JobVault.create_vault() + return app @@ -529,7 +732,7 @@ def register_auth( ): auth.register(app, callback=callback, unauthorized_action=unauthorized_action) -def serve(app_path: str, mode: ServeMode, port: Optional[int], host, enable_remote_edit=False, enable_server_setup=False): +def serve(app_path: str, mode: ServeMode, port: Optional[int], host, enable_remote_edit=False, enable_server_setup=False, enable_jobs_api=False): """ Initialises the web server. """ print_init_message() @@ -551,7 +754,7 @@ def on_load(): port = _next_localhost_available_port(mode_allowed_ports[mode]) enable_server_setup = mode == "run" or enable_server_setup - app = get_asgi_app(app_path, mode, enable_remote_edit, on_load=on_load, enable_server_setup=enable_server_setup) + app = get_asgi_app(app_path, mode, enable_remote_edit, on_load=on_load, enable_server_setup=enable_server_setup, enable_jobs_api=enable_jobs_api) log_level = "warning" uvicorn.run(app, host=host, port=port, log_level=log_level, ws_max_size=MAX_WEBSOCKET_MESSAGE_SIZE) diff --git a/src/writer/ss_types.py b/src/writer/ss_types.py index 3bd05ba76..efa801cf6 100644 --- a/src/writer/ss_types.py +++ b/src/writer/ss_types.py @@ -26,7 +26,7 @@ def read(self) -> Any: ServeMode = Literal["run", "edit"] MessageType = Literal["sessionInit", "componentUpdate", "event", "codeUpdate", "codeSave", "checkSession", - "keepAlive", "stateEnquiry", "setUserinfo", "stateContent"] + "keepAlive", "stateEnquiry", "setUserinfo", "stateContent", "hashRequest"] class AbstractTemplate(BaseModel): @@ -120,6 +120,13 @@ class StateContentRequest(AppProcessServerRequest): type: Literal["stateContent"] +class HashRequestPayload(BaseModel): + message: str + +class HashRequest(AppProcessServerRequest): + type: Literal["hashRequest"] + payload: HashRequestPayload + AppProcessServerRequestPacket = Tuple[int, Optional[str], AppProcessServerRequest] @@ -171,6 +178,13 @@ class StateEnquiryResponse(AppProcessServerResponse): payload: Optional[StateEnquiryResponsePayload] +class HashRequestResponsePayload(BaseModel): + message: str + +class HashRequestResponse(AppProcessServerRequest): + type: Literal["hashRequest"] + payload: HashRequestResponsePayload + AppProcessServerResponsePacket = Tuple[int, Optional[str], AppProcessServerResponse] diff --git a/tests/backend/blocks/conftest.py b/tests/backend/blocks/conftest.py index d41f61e6f..fd84d077d 100644 --- a/tests/backend/blocks/conftest.py +++ b/tests/backend/blocks/conftest.py @@ -27,16 +27,17 @@ def run_branch(self, component_id: str, base_outcome_id: str, execution_environm return f"Branch run {component_id} {base_outcome_id}" def run_workflow_by_key(self, workflow_key: str, execution_environment: Dict): - if "env_injection_test" in execution_environment: - return execution_environment.get("env_injection_test") + payload = execution_environment.get("payload") + if "env_injection_test" in payload: + return payload.get("env_injection_test") if workflow_key == "workflow1": return 1 if workflow_key == "workflowDict": return { "a": "b" } if workflow_key == "duplicator": - return execution_environment.get("item") * 2 + return payload.get("item") * 2 if workflow_key == "showId": - return execution_environment.get("itemId") + return payload.get("itemId") if workflow_key == "boom": return 1/0 raise ValueError("Workflow not found.") diff --git a/tests/backend/blocks/test_httprequest.py b/tests/backend/blocks/test_httprequest.py index 3d3388ff0..9aab07316 100644 --- a/tests/backend/blocks/test_httprequest.py +++ b/tests/backend/blocks/test_httprequest.py @@ -71,8 +71,8 @@ def test_actual_request_with_bad_path(session, runner): assert block.outcome == "responseError" -def test_patched_request(session, runner): - requests.request = fake_request +def test_patched_request(session, runner, monkeypatch): + monkeypatch.setattr("requests.request", fake_request) session.add_fake_component({ "url": "https://www.duck.com" }) @@ -82,8 +82,8 @@ def test_patched_request(session, runner): assert block.result.get("body") == "Ducks are birds." -def test_patched_request_to_nowhere(session, runner): - requests.request = fake_request +def test_patched_request_to_nowhere(session, runner, monkeypatch): + monkeypatch.setattr("requests.request", fake_request) session.add_fake_component({ "url": "https://www.cat.com" }) @@ -93,8 +93,8 @@ def test_patched_request_to_nowhere(session, runner): assert block.outcome == "connectionError" -def test_patched_request_with_json(session, runner): - requests.request = fake_request +def test_patched_request_with_json(session, runner, monkeypatch): + monkeypatch.setattr("requests.request", fake_request) session.add_fake_component({ "url": "https://www.elephant.com", "method": "POST", @@ -107,8 +107,8 @@ def test_patched_request_with_json(session, runner): assert block.result.get("body").get("request_body") == "Posting the elephant." -def test_patched_request_with_json_and_bad_path(session, runner): - requests.request = fake_request +def test_patched_request_with_json_and_bad_path(session, runner, monkeypatch): + monkeypatch.setattr("requests.request", fake_request) session.add_fake_component({ "url": "https://www.elephant.com/history", "method": "POST", diff --git a/tests/backend/blocks/test_runworkflow.py b/tests/backend/blocks/test_runworkflow.py index 2e3a0d90d..5bef1f214 100644 --- a/tests/backend/blocks/test_runworkflow.py +++ b/tests/backend/blocks/test_runworkflow.py @@ -1,3 +1,5 @@ +import json + import pytest from writer.blocks.runworkflow import RunWorkflow @@ -12,9 +14,10 @@ def test_workflow_that_does_not_exist(session, runner): assert block.outcome == "error" def test_duplicator(session, runner): + session.session_state["item_dict"] = json.loads('{"item": 23}') session.add_fake_component({ "workflowKey": "duplicator", - "executionEnv": '{"item": 23}' + "payload": "@{item_dict}" }) block = RunWorkflow("fake_id", runner, {}) block.run() diff --git a/tests/backend/blocks/test_writernocodeapp.py b/tests/backend/blocks/test_writernocodeapp.py index 9c8f576cd..65c4cd2e0 100644 --- a/tests/backend/blocks/test_writernocodeapp.py +++ b/tests/backend/blocks/test_writernocodeapp.py @@ -15,7 +15,6 @@ def fake_generate_content(application_id, app_inputs): def test_call_nocode_app(monkeypatch, session, runner): monkeypatch.setattr("writer.ai.apps.generate_content", fake_generate_content) - writer.ai.apps.generate_content = fake_generate_content session.add_fake_component({ "appId": "123", "appInputs": json.dumps({ diff --git a/tests/backend/test_serve.py b/tests/backend/test_serve.py index 901813bb1..163c66eb2 100644 --- a/tests/backend/test_serve.py +++ b/tests/backend/test_serve.py @@ -1,4 +1,6 @@ import mimetypes +import time +from typing import Any import fastapi import fastapi.testclient @@ -6,6 +8,7 @@ import writer.abstract import writer.serve from fastapi import FastAPI +from writer import crypto from tests.backend import test_app_dir, test_multiapp_dir @@ -213,4 +216,97 @@ def test_feature_flags(self): "Content-Type": "application/json" }) feature_flags = res.json().get("featureFlags") - assert feature_flags == ["flag_one", "flag_two"] \ No newline at end of file + assert feature_flags == ["workflows", "flag_one", "flag_two"] + + def test_create_workflow_job_api(self, monkeypatch): + asgi_app: fastapi.FastAPI = writer.serve.get_asgi_app( + test_app_dir, "run", enable_jobs_api=True) + monkeypatch.setenv("WRITER_SECRET_KEY", "abc") + workflow_key = "workflow2" + + with fastapi.testclient.TestClient(asgi_app) as client: + create_job_token = crypto.get_hash(f"create_job_{workflow_key}") + res = client.post(f"/api/job/workflow/{workflow_key}", json={ + "proposedSessionId": None + }, headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {create_job_token}" + }) + time.sleep(1) + job_id = res.json().get("id") + get_job_token = res.json().get("token") + res = client.get(f"/api/job/{job_id}", headers={ + "Authorization": f"Bearer {get_job_token}" + }) + assert res.json().get("result") == "987127" + + def test_create_workflow_job_api_incorrect_token(self, monkeypatch): + asgi_app: fastapi.FastAPI = writer.serve.get_asgi_app( + test_app_dir, "run", enable_jobs_api=True) + monkeypatch.setenv("WRITER_SECRET_KEY", "abc") + workflow_key = "workflow2" + + with fastapi.testclient.TestClient(asgi_app) as client: + create_job_token = crypto.get_hash("not_the_right_message") + res = client.post(f"/api/job/workflow/{workflow_key}", json={ + "proposedSessionId": None + }, headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {create_job_token}" + }) + assert res.status_code == 403 + + def test_create_workflow_job_api_incorrect_token_for_get(self, monkeypatch): + asgi_app: fastapi.FastAPI = writer.serve.get_asgi_app( + test_app_dir, "run", enable_jobs_api=True) + monkeypatch.setenv("WRITER_SECRET_KEY", "abc") + workflow_key = "workflow2" + + with fastapi.testclient.TestClient(asgi_app) as client: + create_job_token = crypto.get_hash("not_the_right_message") + res = client.post(f"/api/job/workflow/{workflow_key}", json={ + "proposedSessionId": None + }, headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {create_job_token}" + }) + time.sleep(1) + job_id = res.json().get("id") + get_job_token = "not_the_right_job_token" + res = client.get(f"/api/job/{job_id}", headers={ + "Authorization": f"Bearer {get_job_token}" + }) + assert res.status_code == 403 + + + def test_create_workflow_job_api_custom_job_vault(self, monkeypatch): + monkeypatch.setenv("WRITER_SECRET_KEY", "abc") + monkeypatch.setenv("WRITER_PERSISTENT_STORE", "testjobvault://doesn'tmatter") + workflow_key = "workflow2" + + class TestJobVault(writer.serve.JobVault): + SCHEMES = ["testjobvault://"] + def get(self, job_id: str): + value = self.vault[job_id] + if "result" in value: + value["result"] = "Powered by TestJobVault - " + str(value["result"]) + return value + + writer.serve.JobVault.register(TestJobVault) + asgi_app: fastapi.FastAPI = writer.serve.get_asgi_app( + test_app_dir, "run", enable_jobs_api=True) + with fastapi.testclient.TestClient(asgi_app) as client: + create_job_token = crypto.get_hash(f"create_job_{workflow_key}") + res = client.post(f"/api/job/workflow/{workflow_key}", json={ + "proposedSessionId": None + }, headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {create_job_token}" + }) + time.sleep(1) + job_id = res.json().get("id") + get_job_token = res.json().get("token") + res = client.get(f"/api/job/{job_id}", headers={ + "Authorization": f"Bearer {get_job_token}" + }) + assert res.json().get("result") == "Powered by TestJobVault - 987127" \ No newline at end of file diff --git a/tests/backend/testapp/.wf/components-workflows_workflow-0-hywgzgfetx6rpiqz.jsonl b/tests/backend/testapp/.wf/components-workflows_workflow-0-hywgzgfetx6rpiqz.jsonl index 2572997d8..f2458507a 100644 --- a/tests/backend/testapp/.wf/components-workflows_workflow-0-hywgzgfetx6rpiqz.jsonl +++ b/tests/backend/testapp/.wf/components-workflows_workflow-0-hywgzgfetx6rpiqz.jsonl @@ -1,5 +1,5 @@ {"id": "hywgzgfetx6rpiqz", "type": "workflows_workflow", "content": {}, "handlers": {}, "isCodeManaged": false, "parentId": "workflows_root", "position": 0} -{"id": "rbh725i69ilo6gsr", "type": "workflows_setstate", "content": {"element": "test", "value": "test"}, "handlers": {}, "outs": [{"outId": "success", "toNodeId": "f052suq3dgzb5np7"}, {"outId": "success", "toNodeId": "f052suq3dgzb5np7"}], "parentId": "hywgzgfetx6rpiqz", "position": 0, "x": 248, "y": 204} -{"id": "mw5rz7ay5p8pg2fm", "type": "workflows_writerclassification", "content": {}, "handlers": {}, "parentId": "hywgzgfetx6rpiqz", "position": 1, "x": 935, "y": 153} -{"id": "3cy7f577x6xsijiq", "type": "workflows_runworkflow", "content": {}, "handlers": {}, "parentId": "hywgzgfetx6rpiqz", "position": 2, "x": 645, "y": 328} -{"id": "c0v1pnroo32gfsye", "type": "workflows_httprequest", "content": {}, "handlers": {}, "parentId": "hywgzgfetx6rpiqz", "position": 3, "x": 497, "y": 80} +{"id": "rbh725i69ilo6gsr", "type": "workflows_setstate", "content": {"element": "test", "value": "test"}, "handlers": {}, "isCodeManaged": false, "outs": [{"outId": "success", "toNodeId": "f052suq3dgzb5np7"}, {"outId": "success", "toNodeId": "f052suq3dgzb5np7"}], "parentId": "hywgzgfetx6rpiqz", "position": 0, "x": 248, "y": 204} +{"id": "mw5rz7ay5p8pg2fm", "type": "workflows_writerclassification", "content": {}, "handlers": {}, "isCodeManaged": false, "parentId": "hywgzgfetx6rpiqz", "position": 1, "x": 935, "y": 153} +{"id": "3cy7f577x6xsijiq", "type": "workflows_runworkflow", "content": {}, "handlers": {}, "isCodeManaged": false, "parentId": "hywgzgfetx6rpiqz", "position": 2, "x": 645, "y": 328} +{"id": "c0v1pnroo32gfsye", "type": "workflows_httprequest", "content": {}, "handlers": {}, "isCodeManaged": false, "parentId": "hywgzgfetx6rpiqz", "position": 3, "x": 497, "y": 80} diff --git a/tests/backend/testapp/.wf/components-workflows_workflow-1-8ffkuce0ermsm9dr.jsonl b/tests/backend/testapp/.wf/components-workflows_workflow-1-8ffkuce0ermsm9dr.jsonl new file mode 100644 index 000000000..c2ec51bee --- /dev/null +++ b/tests/backend/testapp/.wf/components-workflows_workflow-1-8ffkuce0ermsm9dr.jsonl @@ -0,0 +1,2 @@ +{"id": "8ffkuce0ermsm9dr", "type": "workflows_workflow", "content": {"key": "workflow2"}, "handlers": {}, "parentId": "workflows_root", "position": 1} +{"id": "6ymlyaewhil88bck", "type": "workflows_returnvalue", "content": {"value": "987127"}, "handlers": {}, "parentId": "8ffkuce0ermsm9dr", "position": 0, "x": 408, "y": 172} diff --git a/tests/backend/testapp/.wf/metadata.json b/tests/backend/testapp/.wf/metadata.json index e47807980..8792752a3 100644 --- a/tests/backend/testapp/.wf/metadata.json +++ b/tests/backend/testapp/.wf/metadata.json @@ -1,3 +1,3 @@ { - "writer_version": "0.8.0rc1" + "writer_version": "0.8.3rc1" } \ No newline at end of file diff --git a/tests/backend/testapp/main.py b/tests/backend/testapp/main.py index 9542bfcac..6e08476ba 100644 --- a/tests/backend/testapp/main.py +++ b/tests/backend/testapp/main.py @@ -10,6 +10,7 @@ import writer.core from writer import WriterUIManager +writer.Config.feature_flags.append("workflows") writer.Config.feature_flags.append("flag_one") writer.Config.feature_flags.append("flag_two") diff --git a/tests/e2e/presets/workflows/.wf/components-workflows_workflow-0-auxjfi7lssb268ly.jsonl b/tests/e2e/presets/workflows/.wf/components-workflows_workflow-0-auxjfi7lssb268ly.jsonl index 4216ffb42..e1d0ebeda 100644 --- a/tests/e2e/presets/workflows/.wf/components-workflows_workflow-0-auxjfi7lssb268ly.jsonl +++ b/tests/e2e/presets/workflows/.wf/components-workflows_workflow-0-auxjfi7lssb268ly.jsonl @@ -1,4 +1,4 @@ {"id": "auxjfi7lssb268ly", "type": "workflows_workflow", "content": {"key": "handle_object"}, "handlers": {}, "isCodeManaged": false, "parentId": "workflows_root", "position": 0} {"id": "8y56lmia3wu99jhl", "type": "workflows_parsejson", "content": {"plainText": "{\"color\": \"@{payload}\", \"object\": \"@{context.item.object}\"}"}, "handlers": {}, "isCodeManaged": false, "outs": [{"toNodeId": "xy6vdzh2pm55alc0", "outId": "success"}], "parentId": "auxjfi7lssb268ly", "position": 0, "x": 150, "y": 319} {"id": "xy6vdzh2pm55alc0", "type": "workflows_setstate", "content": {"alias": "Save the JSON", "element": "json_e2e", "value": "@{result}"}, "handlers": {}, "isCodeManaged": false, "outs": [{"toNodeId": "mve8ssvtk0pvw5yf", "outId": "success"}], "parentId": "auxjfi7lssb268ly", "position": 1, "x": 537, "y": 321} -{"id": "mve8ssvtk0pvw5yf", "type": "workflows_returnvalue", "content": {"alias": "", "value": "@{json_e2e}"}, "handlers": {}, "isCodeManaged": false, "parentId": "auxjfi7lssb268ly", "position": 2, "x": 876, "y": 317} +{"id": "mve8ssvtk0pvw5yf", "type": "workflows_returnvalue", "content": {"alias": "", "value": "@{json_e2e}"}, "handlers": {}, "isCodeManaged": false, "parentId": "auxjfi7lssb268ly", "position": 2, "x": 885, "y": 331} diff --git a/tests/e2e/presets/workflows/.wf/components-workflows_workflow-1-n20uom1t17z7c1h8.jsonl b/tests/e2e/presets/workflows/.wf/components-workflows_workflow-1-n20uom1t17z7c1h8.jsonl new file mode 100644 index 000000000..d6dc3cbaf --- /dev/null +++ b/tests/e2e/presets/workflows/.wf/components-workflows_workflow-1-n20uom1t17z7c1h8.jsonl @@ -0,0 +1,2 @@ +{"id": "n20uom1t17z7c1h8", "type": "workflows_workflow", "content": {"key": "repeat_payload"}, "handlers": {}, "isCodeManaged": false, "parentId": "workflows_root", "position": 1} +{"id": "5rwx9ukywrkz2f8t", "type": "workflows_returnvalue", "content": {"alias": "Repeat payload", "value": "@{payload}"}, "handlers": {}, "isCodeManaged": false, "parentId": "n20uom1t17z7c1h8", "position": 0, "x": 371, "y": 270} diff --git a/tests/e2e/presets/workflows/.wf/components-workflows_workflow-2-bjhk2qqylt0ijn50.jsonl b/tests/e2e/presets/workflows/.wf/components-workflows_workflow-2-bjhk2qqylt0ijn50.jsonl new file mode 100644 index 000000000..bbcb3c43c --- /dev/null +++ b/tests/e2e/presets/workflows/.wf/components-workflows_workflow-2-bjhk2qqylt0ijn50.jsonl @@ -0,0 +1,3 @@ +{"id": "bjhk2qqylt0ijn50", "type": "workflows_workflow", "content": {}, "handlers": {}, "isCodeManaged": false, "parentId": "workflows_root", "position": 2} +{"id": "60xs6i5w0ckdiymh", "type": "workflows_runworkflow", "content": {"payload": "blue", "workflowKey": "repeat_payload"}, "handlers": {}, "isCodeManaged": false, "outs": [{"toNodeId": "htzhnqlqe1u02l3o", "outId": "success"}], "parentId": "bjhk2qqylt0ijn50", "position": 0, "x": 244, "y": 282} +{"id": "htzhnqlqe1u02l3o", "type": "workflows_returnvalue", "content": {"value": "@{result}"}, "handlers": {}, "isCodeManaged": false, "parentId": "bjhk2qqylt0ijn50", "position": 1, "x": 622, "y": 287} diff --git a/tests/e2e/presets/workflows/.wf/metadata.json b/tests/e2e/presets/workflows/.wf/metadata.json index da9c77c7c..8792752a3 100644 --- a/tests/e2e/presets/workflows/.wf/metadata.json +++ b/tests/e2e/presets/workflows/.wf/metadata.json @@ -1,3 +1,3 @@ { - "writer_version": "0.8.0rc3" + "writer_version": "0.8.3rc1" } \ No newline at end of file diff --git a/tests/e2e/tests/workflows.spec.ts b/tests/e2e/tests/workflows.spec.ts index e579b960c..8854efd19 100644 --- a/tests/e2e/tests/workflows.spec.ts +++ b/tests/e2e/tests/workflows.spec.ts @@ -22,7 +22,7 @@ test.describe("Workflows", () => { { object: "cup", color: "pink" }, ]; - inputData.forEach(({ object, color }) => { + for (const { object, color } of inputData) { test(`Test context and payload in Workflows for ${object} ${color}`, async ({ page, }) => { @@ -41,16 +41,14 @@ test.describe("Workflows", () => { `.BuilderModal [data-automation-key="return-value"]`, ); const expectedTexts = ["color", color, "object", object]; - expectedTexts.forEach( - async (text) => await expect(resultsLocator).toContainText(text), - ); - expectedTexts.forEach( - async (text) => await expect(returnValueLocator).toContainText(text), - ); + for (const text of expectedTexts) { + await expect(resultsLocator).toContainText(text); + await expect(returnValueLocator).toContainText(text); + } }); - }); + } - test("Create workflow and run workflow handle_object from it", async ({ + test("Create workflow and run workflow repeat_payload from it", async ({ page, }) => { await page.locator(`[data-automation-action="set-mode-workflows"]`).click(); @@ -75,16 +73,9 @@ test.describe("Workflows", () => { const returnValueBlock = page.locator(`.WorkflowsNode.wf-type-workflows_returnvalue`); await runWorkflowBlock.click(); - await page.locator(`.BuilderFieldsText[data-automation-key="workflowKey"] input`).fill("handle_object"); - const executionEnv = { - "payload": "blue", - "context": { - "item": { - "object": "bottle" - } - } - }; - await page.locator(`.BuilderFieldsObject[data-automation-key="executionEnv"] textarea`).fill(JSON.stringify(executionEnv)); + await page.locator(`.BuilderFieldsText[data-automation-key="workflowKey"] input`).fill("repeat_payload"); + const payload = "blue"; + await page.locator(`.BuilderFieldsText[data-automation-key="payload"] textarea`).fill(payload); await page.locator(`[data-automation-action="collapse-settings"]`).click(); await runWorkflowBlock.locator(".ball.success").dragTo(returnValueBlock); @@ -96,16 +87,13 @@ test.describe("Workflows", () => { await page.locator(`[data-automation-action="toggle-panel"][data-automation-key="log"]`).click(); const rowsLocator = page.locator(".BuilderPanelSwitcher div.row"); - await expect(rowsLocator).toHaveCount(5); + await expect(rowsLocator).toHaveCount(3); const rowLocator = rowsLocator.filter({ hasText: "Return value" }).first();; await rowLocator.getByRole("button", { name: "Details" }).click(); await expect(page.locator(".BuilderModal")).toBeVisible(); const returnValueLocator = page.locator( `.BuilderModal [data-automation-key="return-value"]`, ); - const expectedTexts = ["color", "blue", "object", "bottle"]; - expectedTexts.forEach( - async (text) => await expect(returnValueLocator).toContainText(text), - ); + await expect(returnValueLocator).toContainText("blue"); }); }); \ No newline at end of file