diff --git a/backend/poetry.lock b/backend/poetry.lock index 59c72bcd..094f29c6 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -2074,8 +2074,6 @@ groups = ["fe_test"] files = [ {file = "pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860"}, {file = "pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad"}, - {file = "pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0"}, - {file = "pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b"}, {file = "pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50"}, {file = "pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae"}, {file = "pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9"}, @@ -2085,8 +2083,6 @@ files = [ {file = "pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f"}, {file = "pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722"}, {file = "pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288"}, - {file = "pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d"}, - {file = "pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494"}, {file = "pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58"}, {file = "pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f"}, {file = "pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e"}, @@ -2096,8 +2092,6 @@ files = [ {file = "pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd"}, {file = "pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4"}, {file = "pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69"}, - {file = "pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d"}, - {file = "pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6"}, {file = "pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7"}, {file = "pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024"}, {file = "pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809"}, @@ -2110,8 +2104,6 @@ files = [ {file = "pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f"}, {file = "pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c"}, {file = "pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd"}, - {file = "pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e"}, - {file = "pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1"}, {file = "pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805"}, {file = "pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8"}, {file = "pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2"}, @@ -2121,8 +2113,6 @@ files = [ {file = "pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580"}, {file = "pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e"}, {file = "pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d"}, - {file = "pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced"}, - {file = "pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c"}, {file = "pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8"}, {file = "pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59"}, {file = "pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe"}, @@ -2132,8 +2122,6 @@ files = [ {file = "pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e"}, {file = "pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12"}, {file = "pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a"}, - {file = "pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632"}, - {file = "pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673"}, {file = "pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027"}, {file = "pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77"}, {file = "pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874"}, @@ -2143,8 +2131,6 @@ files = [ {file = "pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6"}, {file = "pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae"}, {file = "pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653"}, - {file = "pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6"}, - {file = "pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36"}, {file = "pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b"}, {file = "pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477"}, {file = "pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50"}, @@ -2154,8 +2140,6 @@ files = [ {file = "pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa"}, {file = "pillow-11.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f"}, {file = "pillow-11.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081"}, - {file = "pillow-11.3.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4"}, - {file = "pillow-11.3.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc"}, {file = "pillow-11.3.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06"}, {file = "pillow-11.3.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a"}, {file = "pillow-11.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978"}, @@ -2165,15 +2149,11 @@ files = [ {file = "pillow-11.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb"}, {file = "pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967"}, {file = "pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe"}, - {file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c"}, - {file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25"}, {file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27"}, {file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a"}, {file = "pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f"}, {file = "pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6"}, {file = "pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438"}, - {file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3"}, - {file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c"}, {file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361"}, {file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7"}, {file = "pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8"}, @@ -2658,13 +2638,6 @@ optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, - {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, - {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"}, - {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"}, - {file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"}, - {file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"}, - {file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"}, {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}, @@ -3545,6 +3518,85 @@ typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\"" docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] +[[package]] +name = "websockets" +version = "15.0.1" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b"}, + {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205"}, + {file = "websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c"}, + {file = "websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256"}, + {file = "websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf"}, + {file = "websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85"}, + {file = "websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597"}, + {file = "websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9"}, + {file = "websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4"}, + {file = "websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa"}, + {file = "websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880"}, + {file = "websockets-15.0.1-cp39-cp39-win32.whl", hash = "sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411"}, + {file = "websockets-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123"}, + {file = "websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f"}, + {file = "websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee"}, +] + [[package]] name = "xarray" version = "2025.6.1" @@ -3797,4 +3849,4 @@ json-timeline-db = [] [metadata] lock-version = "2.1" python-versions = "^3.10,<3.13" -content-hash = "ada3ad4a699bb7bea678a2e7bfd04407aaf5dbf73e5663114de6822a219a4e25" +content-hash = "a1d1edecb6bb4a2b42effd1aae40b6e89ba2b8f936fe723f787480a8f176b3ba" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 7af8ea7f..3d6b88cb 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -18,6 +18,7 @@ tomli = { version = "^2.0.1", python = "<3.11" } tomli-w = "^1.0.0" requests = "^2.32.4" jsonpatch = "^1.33" +websockets = "^15.0.1" qualibrate-config = "^0.1.8,<0.2.0" # TODO: replace after publishing timeline-db # json-timeline-database = { path = "../../json-timeline-database", optional = true, develop = true } diff --git a/backend/qualibrate_app/api/core/lifespan.py b/backend/qualibrate_app/api/core/lifespan.py new file mode 100644 index 00000000..4e585939 --- /dev/null +++ b/backend/qualibrate_app/api/core/lifespan.py @@ -0,0 +1,15 @@ +import asyncio +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager + +from fastapi import FastAPI + +from qualibrate_app.api.sockets.tasks import update_snapshot_history_required + +__all__ = ["app_lifespan"] + + +@asynccontextmanager +async def app_lifespan(app: FastAPI) -> AsyncIterator[None]: + await asyncio.gather(update_snapshot_history_required()) + yield diff --git a/backend/qualibrate_app/api/core/schemas/update_history.py b/backend/qualibrate_app/api/core/schemas/update_history.py new file mode 100644 index 00000000..4d183703 --- /dev/null +++ b/backend/qualibrate_app/api/core/schemas/update_history.py @@ -0,0 +1,12 @@ +from pydantic import BaseModel, computed_field + +from qualibrate_app.api.core.types import IdType + + +class UpdateHistoryRequired(BaseModel): + saved_id: IdType | None + latest_id: IdType | None + + @computed_field + def update_required(self) -> bool: + return self.latest_id != self.saved_id diff --git a/backend/qualibrate_app/api/core/socket/__init__.py b/backend/qualibrate_app/api/core/socket/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/qualibrate_app/api/core/socket/periodic_tasks.py b/backend/qualibrate_app/api/core/socket/periodic_tasks.py new file mode 100644 index 00000000..f68daadc --- /dev/null +++ b/backend/qualibrate_app/api/core/socket/periodic_tasks.py @@ -0,0 +1,81 @@ +import asyncio +from collections.abc import Callable, Coroutine +from functools import wraps +from typing import Any, TypeAlias + +from fastapi.concurrency import run_in_threadpool + +NoArgsNoReturnFuncT: TypeAlias = Callable[[], None] +NoArgsNoReturnAsyncFuncT: TypeAlias = Callable[[], Coroutine[Any, Any, None]] +ExcArgNoReturnFuncT: TypeAlias = Callable[[Exception], None] +ExcArgNoReturnAsyncFuncT: TypeAlias = Callable[ + [Exception], Coroutine[Any, Any, None] +] +NoArgsNoReturnAnyFuncT: TypeAlias = ( + NoArgsNoReturnFuncT | NoArgsNoReturnAsyncFuncT +) +ExcArgNoReturnAnyFuncT: TypeAlias = ( + ExcArgNoReturnFuncT | ExcArgNoReturnAsyncFuncT +) + +__all__ = ["repeat_every"] + + +async def _handle_func(func: NoArgsNoReturnAnyFuncT) -> None: + if asyncio.iscoroutinefunction(func): + await func() + else: + await run_in_threadpool(func) + + +async def _handle_exc( + exc: Exception, on_exception: ExcArgNoReturnAnyFuncT | None +) -> None: + if on_exception is None: + return + if asyncio.iscoroutinefunction(on_exception): + await on_exception(exc) + else: + await run_in_threadpool(on_exception, exc) + + +def repeat_every( + *, + seconds: float, + on_exception: Callable[[Exception], None] | None = None, +) -> Callable[[NoArgsNoReturnAnyFuncT], NoArgsNoReturnAsyncFuncT]: + """ + This function returns a decorator that modifies a function so it is + periodically re-executed after its first call. + The function it decorates should accept no arguments and return nothing. + + Parameters + ---------- + seconds: float + The number of seconds to wait between repeated calls + on_exception: Optional[Callable[[Exception], None]] (default None) + A function to call when an exception is raised by the decorated + function. + """ + + def decorator(func: NoArgsNoReturnAnyFuncT) -> NoArgsNoReturnAsyncFuncT: + """ + Converts the decorated function into a repeated, + periodically-called version of itself. + """ + + @wraps(func) + async def wrapped() -> None: + async def loop() -> None: + while True: + try: + await _handle_func(func) + except Exception as exc: + await _handle_exc(exc, on_exception) + await asyncio.sleep(seconds) + + asyncio.ensure_future(loop()) + + return wrapped + + return decorator diff --git a/backend/qualibrate_app/api/core/socket/ws_manager.py b/backend/qualibrate_app/api/core/socket/ws_manager.py new file mode 100644 index 00000000..c9dccefd --- /dev/null +++ b/backend/qualibrate_app/api/core/socket/ws_manager.py @@ -0,0 +1,72 @@ +from collections import defaultdict +from collections.abc import Hashable +from typing import Any, Generic, TypeVar + +from fastapi import WebSocket + + +class SocketConnectionManagerList: + def __init__(self) -> None: + self.active_connections: list[WebSocket] = [] + + async def connect(self, websocket: WebSocket) -> None: + await websocket.accept() + self.active_connections.append(websocket) + + def disconnect(self, websocket: WebSocket) -> None: + if websocket in self.active_connections: + self.active_connections.remove(websocket) + + async def broadcast(self, message: Any) -> None: + disconnected: list[WebSocket] = [] + for connection in self.active_connections: + try: + await connection.send_json(message) + except Exception: + disconnected.append(connection) + for conn in disconnected: + self.disconnect(conn) + + @property + def any_subscriber(self) -> bool: + return len(self.active_connections) > 0 + + +KT = TypeVar("KT", bound=Hashable) + + +class SocketConnectionManagerMapping(Generic[KT]): + def __init__(self) -> None: + self.active_connections: dict[KT, list[WebSocket]] = defaultdict(list) + + async def connect(self, key: KT, websocket: WebSocket) -> None: + await websocket.accept() + self.active_connections[key].append(websocket) + + def disconnect(self, key: KT, websocket: WebSocket) -> None: + if key in self.active_connections and websocket in self.active_connections[key]: + self.active_connections[key].remove(websocket) + + async def broadcast(self, key: KT, message: Any) -> None: + if not self.any_subscriber_for(key): + return + disconnected: list[WebSocket] = [] + for connection in self.active_connections[key]: + try: + await connection.send_json(message) + except Exception: + disconnected.append(connection) + for conn in disconnected: + self.disconnect(key, conn) + + @property + def any_subscriber(self) -> bool: + return len(self.active_connections) > 0 and any( + map(len, self.active_connections.values()) + ) + + def any_subscriber_for(self, key: KT) -> bool: + return ( + key in self.active_connections + and len(self.active_connections[key]) > 0 + ) diff --git a/backend/qualibrate_app/api/core/socket/ws_managers.py b/backend/qualibrate_app/api/core/socket/ws_managers.py new file mode 100644 index 00000000..a4ab47ed --- /dev/null +++ b/backend/qualibrate_app/api/core/socket/ws_managers.py @@ -0,0 +1,14 @@ +from functools import cache + +from qualibrate_app.api.core.socket.ws_manager import ( + SocketConnectionManagerList, +) + +__all__ = ["get_need_to_update_snapshots_history_socket_manager"] + + +@cache +def get_need_to_update_snapshots_history_socket_manager() -> ( + SocketConnectionManagerList +): + return SocketConnectionManagerList() diff --git a/backend/qualibrate_app/api/sockets/__init__.py b/backend/qualibrate_app/api/sockets/__init__.py new file mode 100644 index 00000000..000c9e3e --- /dev/null +++ b/backend/qualibrate_app/api/sockets/__init__.py @@ -0,0 +1,6 @@ +from fastapi import APIRouter + +from .common import common_ws_router + +base_ws_router = APIRouter() +base_ws_router.include_router(common_ws_router) diff --git a/backend/qualibrate_app/api/sockets/common.py b/backend/qualibrate_app/api/sockets/common.py new file mode 100644 index 00000000..e1782ea6 --- /dev/null +++ b/backend/qualibrate_app/api/sockets/common.py @@ -0,0 +1,30 @@ +from typing import Annotated + +from fastapi import APIRouter, Depends +from starlette.websockets import WebSocket, WebSocketDisconnect + +from qualibrate_app.api.core.socket.ws_manager import ( + SocketConnectionManagerList, +) +from qualibrate_app.api.core.socket.ws_managers import ( + get_need_to_update_snapshots_history_socket_manager, +) + +common_ws_router = APIRouter() + + +@common_ws_router.websocket("/update_snapshots_history_required") +async def run_status_subscribe( + websocket: WebSocket, + *, + manager: Annotated[ + SocketConnectionManagerList, + Depends(get_need_to_update_snapshots_history_socket_manager), + ], +) -> None: + await manager.connect(websocket) + try: + while True: + await websocket.receive_text() + except WebSocketDisconnect: + manager.disconnect(websocket) diff --git a/backend/qualibrate_app/api/sockets/tasks.py b/backend/qualibrate_app/api/sockets/tasks.py new file mode 100644 index 00000000..b0a0e4a3 --- /dev/null +++ b/backend/qualibrate_app/api/sockets/tasks.py @@ -0,0 +1,46 @@ +import logging + +from qualibrate_app.api.core.domain.local_storage.utils.node_utils import ( + find_nodes_ids_by_filter, +) +from qualibrate_app.api.core.schemas.update_history import UpdateHistoryRequired +from qualibrate_app.api.core.socket.periodic_tasks import repeat_every +from qualibrate_app.api.core.socket.ws_managers import ( + get_need_to_update_snapshots_history_socket_manager, +) +from qualibrate_app.api.core.types import IdType +from qualibrate_app.config import get_config_path, get_settings + +__all__ = ["update_snapshot_history_required"] + + +def _on_exc(exc: Exception) -> None: + logging.exception( + "Exception occurred while running periodic task", exc_info=exc + ) + + +previous_snapshot_id: IdType | None = None + + +@repeat_every(seconds=1, on_exception=_on_exc) +async def update_snapshot_history_required() -> None: + manager = get_need_to_update_snapshots_history_socket_manager() + if not manager.any_subscriber: + return + settings = get_settings(get_config_path()) + id = next( + find_nodes_ids_by_filter( + settings.storage.location, + project_name=settings.project, + descending=True, + ), + None, + ) + global previous_snapshot_id + update_required = UpdateHistoryRequired( + latest_id=id, + saved_id=previous_snapshot_id, + ) + previous_snapshot_id = id + await manager.broadcast(update_required.model_dump(mode="json")) diff --git a/backend/qualibrate_app/app.py b/backend/qualibrate_app/app.py index 13f5aad7..985d56e9 100644 --- a/backend/qualibrate_app/app.py +++ b/backend/qualibrate_app/app.py @@ -4,9 +4,11 @@ from starlette.middleware.cors import CORSMiddleware from qualibrate_app.api.__main__ import api_router +from qualibrate_app.api.core.lifespan import app_lifespan from qualibrate_app.api.exceptions.classes.base import QualibrateException from qualibrate_app.api.exceptions.handler import qualibrate_exception_handler from qualibrate_app.api.middleware.process_time import ProcessTimeMiddleware +from qualibrate_app.api.sockets import base_ws_router from qualibrate_app.config.resolvers import ( get_config_path, get_default_static_files_path, @@ -20,6 +22,7 @@ app = FastAPI( + lifespan=app_lifespan, title="Qualibrate", openapi_url="/app_openapi.json", docs_url="/app_docs", @@ -44,6 +47,7 @@ app.add_middleware(ProcessTimeMiddleware) app.include_router(api_router, prefix="/api") +app.include_router(base_ws_router, prefix="/ws") static_files_path = ( _settings.app.static_site_files diff --git a/frontend/src/modules/Project/components/Project/ProjectActions.tsx b/frontend/src/modules/Project/components/Project/ProjectActions.tsx index 09ddd7c0..a78fccc2 100644 --- a/frontend/src/modules/Project/components/Project/ProjectActions.tsx +++ b/frontend/src/modules/Project/components/Project/ProjectActions.tsx @@ -7,8 +7,7 @@ import cyKeys from "../../../../utils/cyKeys"; import { NODES_KEY } from "../../../AppRoutes"; import { useRootDispatch } from "../../../../stores"; import { setActivePage } from "../../../../stores/NavigationStore"; -import { useSelector } from "react-redux"; -import { getReset, clearData } from "../../../../stores/SnapshotsStore"; +import { clearData, fetchGitgraphSnapshots } from "../../../../stores/SnapshotsStore"; interface ProjectActionsProps { isCurrentProject: boolean; @@ -18,15 +17,15 @@ interface ProjectActionsProps { const ProjectActions: React.FC = ({ isCurrentProject, projectName, selectedProject }) => { const dispatch = useRootDispatch(); - const reset = useSelector(getReset); const handleSubmit = useCallback(() => { if (!selectedProject) return; dispatch(selectActiveProject(selectedProject)); dispatch(clearData()); + dispatch(fetchGitgraphSnapshots(true)); dispatch(setActivePage(NODES_KEY)); - }, [ selectedProject, reset ]); + }, [ selectedProject ]); return (
diff --git a/frontend/src/services/webSocketRoutes.ts b/frontend/src/services/webSocketRoutes.ts index dad8fff3..417b2128 100644 --- a/frontend/src/services/webSocketRoutes.ts +++ b/frontend/src/services/webSocketRoutes.ts @@ -1,2 +1,3 @@ export const WS_GET_STATUS = "execution/ws/run_status"; export const WS_EXECUTION_HISTORY = "execution/ws/workflow_execution_history"; +export const WS_SNAPSHOT_INFO = "ws/update_snapshots_history_required"; diff --git a/frontend/src/stores/SnapshotsStore/SnapshotsStore.ts b/frontend/src/stores/SnapshotsStore/SnapshotsStore.ts index 79a93836..cde189e9 100644 --- a/frontend/src/stores/SnapshotsStore/SnapshotsStore.ts +++ b/frontend/src/stores/SnapshotsStore/SnapshotsStore.ts @@ -16,14 +16,13 @@ interface SnapshotsState { result: object | undefined; firstId: string; secondId: string; - reset: boolean; } const initialState: SnapshotsState = { trackLatestSidePanel: true, trackPreviousSnapshot: true, totalPages: 0, - pageNumber: 0, + pageNumber: 1, allSnapshots: [], selectedSnapshotId: undefined, latestSnapshotId: undefined, @@ -34,7 +33,6 @@ const initialState: SnapshotsState = { result: {}, firstId: "0", secondId: "0", - reset: false, }; export const SnapshotsSlice = createSlice({ @@ -79,7 +77,6 @@ export const SnapshotsSlice = createSlice({ state.jsonData = undefined; state.result = undefined; state.diffData = undefined; - state.reset = true; }, setResult: (state, action) => { state.result = action.payload; @@ -90,9 +87,6 @@ export const SnapshotsSlice = createSlice({ setSecondId: (state, action) => { state.secondId = action.payload; }, - setReset: (state, action) => { - state.reset = action.payload; - }, } }); diff --git a/frontend/src/stores/SnapshotsStore/actions.ts b/frontend/src/stores/SnapshotsStore/actions.ts index 5e54ad73..a5a5d473 100644 --- a/frontend/src/stores/SnapshotsStore/actions.ts +++ b/frontend/src/stores/SnapshotsStore/actions.ts @@ -1,6 +1,6 @@ import { RootDispatch, RootState } from ".."; import { SnapshotsSlice } from "./SnapshotsStore"; -import { getAllSnapshots, getSecondId, getSelectedSnapshotId, getTrackLatestSidePanel, getTrackPreviousSnapshot } from "./selectors"; +import { getPageNumber, getSecondId, getSelectedSnapshotId, getTrackLatestSidePanel, getTrackPreviousSnapshot } from "./selectors"; import { fetchAllSnapshots, fetchSnapshotDiff, fetchSnapshotJsonData, fetchSnapshotResults } from "./utils"; export const { @@ -19,7 +19,6 @@ export const { setResult, setFirstId, setSecondId, - setReset, } = SnapshotsSlice.actions; export const fetchOneSnapshot = (snapshotId: number, snapshotId2?: number, updateResult = true, fetchUpdate = false) => @@ -47,15 +46,15 @@ export const fetchOneSnapshot = (snapshotId: number, snapshotId2?: number, updat } }; -export const fetchGitgraphSnapshots = (firstTime: boolean, page: number) => +export const fetchGitgraphSnapshots = (firstTime: boolean) => async (dispatch: RootDispatch, getState: () => RootState) => { const trackLatestSidePanel = getTrackLatestSidePanel(getState()); const trackPreviousSnapshot = getTrackPreviousSnapshot(getState()); const secondId = getSecondId(getState()); const selectedSnapshotId = getSelectedSnapshotId(getState()); + const page = getPageNumber(getState()); const resAllSnapshots = await fetchAllSnapshots(page); - dispatch(setAllSnapshots([])); if (resAllSnapshots && resAllSnapshots?.isOk) { const items = resAllSnapshots.result?.items; dispatch(setTotalPages(resAllSnapshots.result?.total_pages ?? 1)); @@ -78,29 +77,8 @@ export const fetchGitgraphSnapshots = (firstTime: boolean, page: number) => } else { if (selectedSnapshotId) { dispatch(fetchOneSnapshot(selectedSnapshotId)); - dispatch(setReset(false)); } } } } }; - -// ----------------------------------------------------------- -// PERIODICAL FETCH ALL SNAPSHOTS -export const intervalFetch = (page: number) => - async (dispatch: RootDispatch, getState: () => RootState) => { - const allSnapshots = getAllSnapshots(getState()); - const resAllSnapshots = await fetchAllSnapshots(page); - if (resAllSnapshots) { - dispatch(setTotalPages(resAllSnapshots.result?.total_pages as number)); - dispatch(setPageNumber(resAllSnapshots.result?.page as number)); - const newMaxId = resAllSnapshots.result?.items[0]?.id; - const odlMaxId = allSnapshots ? allSnapshots[0]?.id : 0; - console.log(`Max snapshot ID - previous=${odlMaxId}, latest=${newMaxId}`); - if (newMaxId !== odlMaxId! && resAllSnapshots.result?.items?.length !== 0) { - dispatch(setReset(true)); - } else { - dispatch(setReset(false)); - } - } - }; diff --git a/frontend/src/stores/SnapshotsStore/hooks.ts b/frontend/src/stores/SnapshotsStore/hooks.ts index 87a6d9b8..24518545 100644 --- a/frontend/src/stores/SnapshotsStore/hooks.ts +++ b/frontend/src/stores/SnapshotsStore/hooks.ts @@ -1,38 +1,25 @@ import { useEffect } from "react"; import { useRootDispatch } from ".."; -import { fetchGitgraphSnapshots, intervalFetch, setAllSnapshots } from "./actions"; +import { fetchGitgraphSnapshots } from "./actions"; import { useSelector } from "react-redux"; -import { getAllSnapshots, getPageNumber, getReset } from "./selectors"; +import { getPageNumber } from "./selectors"; +import { getIsSnapshotUpdateRequired } from "../WebSocketStore"; export const useInitSnapshots = () => { const dispatch = useRootDispatch(); const pageNumber = useSelector(getPageNumber); - const allSnapshots = useSelector(getAllSnapshots); - const reset = useSelector(getReset); + const isUpdateRequired = useSelector(getIsSnapshotUpdateRequired); useEffect(() => { - dispatch(setAllSnapshots([])); - dispatch(fetchGitgraphSnapshots(true, pageNumber)); + dispatch(fetchGitgraphSnapshots(true)); }, [pageNumber]); - // ----------------------------------------------------------- - // ----------------------------------------------------------- // TODO Add lastSelectedId! in state useEffect(() => { - const checkInterval = setInterval(() => dispatch(intervalFetch(pageNumber)), 1000); - return () => clearInterval(checkInterval); - }, [allSnapshots, pageNumber]); - // ----------------------------------------------------------- - - // ----------------------------------------------------------- - // PERIODICAL FETCH ALL SNAPSHOTS - useEffect(() => { - if (reset) { - // setAllSnapshots([]); - const updateFn = setTimeout(() => dispatch(fetchGitgraphSnapshots(false, pageNumber)), 2); - return () => clearTimeout(updateFn); + if (isUpdateRequired) { + dispatch(fetchGitgraphSnapshots(false)); } - }, [reset, pageNumber]); + }, [isUpdateRequired, pageNumber]); // ----------------------------------------------------------- }; diff --git a/frontend/src/stores/SnapshotsStore/index.ts b/frontend/src/stores/SnapshotsStore/index.ts index 3268c2fe..332cdbda 100644 --- a/frontend/src/stores/SnapshotsStore/index.ts +++ b/frontend/src/stores/SnapshotsStore/index.ts @@ -17,10 +17,8 @@ export { setResult, setFirstId, setSecondId, - setReset, fetchOneSnapshot, fetchGitgraphSnapshots, - intervalFetch, } from "./actions"; export { getSnapshotsState, @@ -38,6 +36,5 @@ export { getResult, getFirstId, getSecondId, - getReset, } from "./selectors"; export { useInitSnapshots } from "./hooks"; \ No newline at end of file diff --git a/frontend/src/stores/SnapshotsStore/selectors.ts b/frontend/src/stores/SnapshotsStore/selectors.ts index 5e2e3581..577c1e0c 100644 --- a/frontend/src/stores/SnapshotsStore/selectors.ts +++ b/frontend/src/stores/SnapshotsStore/selectors.ts @@ -72,8 +72,3 @@ export const getSecondId = createSelector( getSnapshotsState, (state) => state.secondId ); - -export const getReset = createSelector( - getSnapshotsState, - (state) => state.reset -); diff --git a/frontend/src/stores/WebSocketStore/WebSocketStore.ts b/frontend/src/stores/WebSocketStore/WebSocketStore.ts index ab218d79..5778be2f 100644 --- a/frontend/src/stores/WebSocketStore/WebSocketStore.ts +++ b/frontend/src/stores/WebSocketStore/WebSocketStore.ts @@ -95,6 +95,27 @@ export type HistoryType = { items: Measurement[]; }; +/** + * Real-time info about latest executed and latest saved snapshot id and a flag that indicates whether snapshots array + * should be updated or not (by doing API request to the BE). + * + * @property saved_id - id of the last saved snapshot + * @property latest_id - id of the last executed node + * @property update_required - flag that indicates if all snapshot array should be updated (because of the new ones) + * + * @remarks + * Pushed via `/execution/ws/workflow_execution_history` WebSocket endpoint. + * Used by timeline components to display calibration history and trends. + * + * @see Data for previous executed nodes details + * + */ +export type SnapshotType = { + saved_id: number; + latest_id: number; + update_required: boolean; +}; + /** * WebSocket redux state interface providing real-time calibration data and operations. * @@ -104,6 +125,7 @@ export type HistoryType = { interface WebSocketState { runStatus: RunStatusType | null; history: HistoryType | null; + snapshotInfo: SnapshotType | null; showConnectionErrorDialog: boolean; connectionLostAt: number | null; connectionLostSeconds: number; @@ -112,6 +134,7 @@ interface WebSocketState { const initialState: WebSocketState = { runStatus: null, history: null, + snapshotInfo: null, showConnectionErrorDialog: false, connectionLostAt: null, connectionLostSeconds: 0, @@ -127,6 +150,9 @@ export const webSocketSlice = createSlice({ setHistory: (state, action) => { state.history = action.payload; }, + setSnapshotInfo: (state, action) => { + state.snapshotInfo = action.payload; + }, setShowConnectionErrorDialog: (state, action) => { state.showConnectionErrorDialog = action.payload; }, diff --git a/frontend/src/stores/WebSocketStore/actions.ts b/frontend/src/stores/WebSocketStore/actions.ts index 39f86be7..1f932731 100644 --- a/frontend/src/stores/WebSocketStore/actions.ts +++ b/frontend/src/stores/WebSocketStore/actions.ts @@ -7,6 +7,7 @@ import { RunStatusType, webSocketSlice } from "./WebSocketStore"; export const { setRunStatus, setHistory, + setSnapshotInfo, setShowConnectionErrorDialog, setConnectionLostAt, setConnectionLostSeconds, diff --git a/frontend/src/stores/WebSocketStore/hooks.ts b/frontend/src/stores/WebSocketStore/hooks.ts index c8806c75..3754b844 100644 --- a/frontend/src/stores/WebSocketStore/hooks.ts +++ b/frontend/src/stores/WebSocketStore/hooks.ts @@ -5,13 +5,14 @@ import { handleShowConnectionErrorDialog, setConnectionLostSeconds, setHistory, + setSnapshotInfo, } from "./actions"; import { useRootDispatch } from ".."; import { useSelector } from "react-redux"; import { getConnectionLostAt, getShowConnectionErrorDialog } from "./selectors"; import { setAllMeasurements } from "../GraphStores/GraphStatus"; -import { HistoryType, RunStatusType } from "./WebSocketStore"; -import { WS_EXECUTION_HISTORY, WS_GET_STATUS } from "../../services/webSocketRoutes"; +import { HistoryType, RunStatusType, SnapshotType } from "./WebSocketStore"; +import { WS_EXECUTION_HISTORY, WS_GET_STATUS, WS_SNAPSHOT_INFO } from "../../services/webSocketRoutes"; import WebSocketService from "../../services/WebSocketService"; export const useInitWebSocket = () => { @@ -21,6 +22,7 @@ export const useInitWebSocket = () => { const runStatusWS = useRef | null>(null); const historyWS = useRef | null>(null); + const snapshotInfoWS = useRef | null>(null); const flattenHistory = (history: HistoryType) => { if (!history?.items) return []; @@ -61,6 +63,7 @@ export const useInitWebSocket = () => { const host = process.env.WS_BASE_URL || location; const runStatusUrl = `${protocol}://${host}${WS_GET_STATUS}`; const historyUrl = `${protocol}://${host}${WS_EXECUTION_HISTORY}`; + const snapshotInfoUrl = `${protocol}://${host}${WS_SNAPSHOT_INFO}`; runStatusWS.current = new WebSocketService( runStatusUrl, @@ -74,6 +77,12 @@ export const useInitWebSocket = () => { () => dispatch(handleHideConnectionErrorDialog()), () => dispatch(handleShowConnectionErrorDialog()) ); + snapshotInfoWS.current = new WebSocketService( + snapshotInfoUrl, + (snapshotInfo) => dispatch(setSnapshotInfo(snapshotInfo)), + () => dispatch(handleHideConnectionErrorDialog()), + () => dispatch(handleShowConnectionErrorDialog()) + ); if (runStatusWS.current && !runStatusWS.current.isConnected()) { runStatusWS.current.connect(); @@ -81,6 +90,9 @@ export const useInitWebSocket = () => { if (historyWS.current && !historyWS.current.isConnected()) { historyWS.current.connect(); } + if (snapshotInfoWS.current && !snapshotInfoWS.current.isConnected()) { + snapshotInfoWS.current.connect(); + } // Cleanup function: disconnect WebSockets when component unmounts // Prevents memory leaks and dangling connections @@ -91,15 +103,20 @@ export const useInitWebSocket = () => { if (historyWS.current && historyWS.current.isConnected()) { historyWS.current.disconnect(); } + if (snapshotInfoWS.current && snapshotInfoWS.current.isConnected()) { + snapshotInfoWS.current.disconnect(); + } }; }, []); // Methods for sending data through WebSocket. const sendRunStatus = (data: RunStatusType) => runStatusWS.current?.send(data); const sendHistory = (data: HistoryType) => historyWS.current?.send(data); + const sendSnapshotInfo = (data: SnapshotType) => snapshotInfoWS.current?.send(data); return { sendRunStatus, sendHistory, + sendSnapshotInfo, }; }; diff --git a/frontend/src/stores/WebSocketStore/index.ts b/frontend/src/stores/WebSocketStore/index.ts index cc7164ec..88c29754 100644 --- a/frontend/src/stores/WebSocketStore/index.ts +++ b/frontend/src/stores/WebSocketStore/index.ts @@ -3,6 +3,7 @@ export type { HistoryType, RunStatusType } from "./WebSocketStore"; export { setRunStatus, setHistory, + setSnapshotInfo, setShowConnectionErrorDialog, setConnectionLostAt, setConnectionLostSeconds, @@ -38,5 +39,6 @@ export { getRunStatusNodeId, getRunStatusNodeCurrentAction, getRunStatusNodeTimeRemaining, + getIsSnapshotUpdateRequired, } from "./selectors"; export { useInitWebSocket } from "./hooks"; \ No newline at end of file diff --git a/frontend/src/stores/WebSocketStore/selectors.ts b/frontend/src/stores/WebSocketStore/selectors.ts index 668547e0..405a9e23 100644 --- a/frontend/src/stores/WebSocketStore/selectors.ts +++ b/frontend/src/stores/WebSocketStore/selectors.ts @@ -47,6 +47,20 @@ export const getRunResultNodeError = createSelector( (runStatusState) => runStatusState?.node?.run_results?.error ); +/** + * shapshotInfo selectors + */ + +export const getSnapshotInfo = createSelector( + getWebSocketState, + (webSocketState) => webSocketState.snapshotInfo, +); + +export const getIsSnapshotUpdateRequired = createSelector( + getSnapshotInfo, + (snapshotInfo) => snapshotInfo?.update_required +); + /** * graph selectors */ diff --git a/frontend/tests/e2e/workflow1.test.ts b/frontend/tests/e2e/workflow1.test.ts index c7c66091..72a6562b 100644 --- a/frontend/tests/e2e/workflow1.test.ts +++ b/frontend/tests/e2e/workflow1.test.ts @@ -12,14 +12,13 @@ test("Workflow1 - Running a Calibration Node", async ({ page }, testInfo) => { // 1. Navigate to the Application // Open http://127.0.0.1:8001/ in your web browser. - await page.goto("http://localhost:8001/", { waitUntil: 'load' }); + await page.goto("http://localhost:8001/", { waitUntil: "load" }); expect(page.url()).toBe("http://localhost:8001/"); // page loaded successfully // 1a. Verify Project Page and go to project (node) page try { await expect(page.getByTestId("project-wrapper-demo_project")).toBeVisible({ timeout: 2000 }); - } catch { - } + } catch {} try { const lets_start_button = page.getByTestId("lets-start-button-demo_project"); await expect(lets_start_button).toBeVisible({ timeout: 1000 }); // Project page loaded sucessfully @@ -30,7 +29,7 @@ test("Workflow1 - Running a Calibration Node", async ({ page }, testInfo) => { // 2. Verify Calibration Nodes // Check that at least one calibration node (e.g., test_cal) is displayed in the Node Library. - await expect(page.getByTestId("nodes-and-job-wrapper")).toBeVisible({ timeout: 1000 }); // Node page loaded sucessfully + await expect(page.getByTestId("nodes-and-job-wrapper")).toBeVisible({ timeout: 10000 }); // Node page loaded sucessfully await expect(page.getByTestId("nodes-page-wrapper")).toBeVisible(); // Node page loaded sucessfully // await expect(page.getByTestId('title-wrapper')).toBeVisible(); // title wrapper is visible // await expect(page.getByTestId('title-wrapper')).toContainText('Run calibration node'); // title is correct @@ -95,7 +94,7 @@ test("Workflow1 - Running a Calibration Node", async ({ page }, testInfo) => { const screenshotPathStep4 = `screenshot-after-step4-${Date.now()}.png`; await page.screenshot({ path: screenshotPathStep4 }); - await testInfo.attach('screenshot-after-step4', { path: screenshotPathStep4, contentType: 'image/png' }); + await testInfo.attach("screenshot-after-step4", { path: screenshotPathStep4, contentType: "image/png" }); // 5. Run the Calibration Node // Click the Run button for test_cal. @@ -106,7 +105,7 @@ test("Workflow1 - Running a Calibration Node", async ({ page }, testInfo) => { const screenshotPathStep5 = `screenshot-after-step5-${Date.now()}.png`; await page.screenshot({ path: screenshotPathStep5 }); - await testInfo.attach('screenshot-after-step5', { path: screenshotPathStep5, contentType: 'image/png' }); + await testInfo.attach("screenshot-after-step5", { path: screenshotPathStep5, contentType: "image/png" }); // Verify: // The running node section should be visible (even after node completes) @@ -119,7 +118,7 @@ test("Workflow1 - Running a Calibration Node", async ({ page }, testInfo) => { const screenshotPathFinished = `screenshot-after-finished-${Date.now()}.png`; await page.screenshot({ path: screenshotPathFinished }); - await testInfo.attach('screenshot-after-step6', { path: screenshotPathFinished, contentType: 'image/png' }); + await testInfo.attach("screenshot-after-step6", { path: screenshotPathFinished, contentType: "image/png" }); // Verify parameters are displayed (they should match what we set earlier) await expect(page.getByTestId("parameter-item-resonator")).toBeVisible(); @@ -158,7 +157,10 @@ test("Workflow1 - Running a Calibration Node", async ({ page }, testInfo) => { await expect(page.getByTestId("state-title")).toBeVisible(); // Check if state updates exist (they're conditional based on whether the node produces state updates) - const stateUpdatesExist = await page.getByTestId("state-updates-top-wrapper").isVisible().catch(() => false); + const stateUpdatesExist = await page + .getByTestId("state-updates-top-wrapper") + .isVisible() + .catch(() => false); if (stateUpdatesExist) { // If state updates exist, verify their structure @@ -166,9 +168,9 @@ test("Workflow1 - Running a Calibration Node", async ({ page }, testInfo) => { await expect(page.getByTestId("update-all-button")).toBeVisible(); const ch1 = page.getByTestId("state-update-wrapper-#/channels/ch1/intermediate_frequency"); - const ch2 = page.getByTestId("state-update-wrapper-#/channels/ch2/intermediate_frequency"); + // const ch2 = page.getByTestId("state-update-wrapper-#/channels/ch2/intermediate_frequency"); await expect(ch1).toBeVisible(); - await expect(ch2).toBeVisible(); + // await expect(ch2).toBeVisible(); // Update the state value for ch1 to 20000000 await expect(ch1.getByTestId("value-container")).toContainText("100000000"); @@ -182,15 +184,15 @@ test("Workflow1 - Running a Calibration Node", async ({ page }, testInfo) => { await expect(ch1.getByTestId("update-after-icon")).toBeVisible(); // Update the state value for ch2 to [1,2,4,5] - await expect(ch2.getByTestId("value-input")).toBeVisible(); - await expect(ch2.getByTestId("value-container")).toContainText("80000000"); - await expect(ch2.getByTestId("value-input")).toHaveValue("70000000"); - ch2.getByTestId("value-input").click(); - ch2.getByTestId("value-input").fill("[1,2,4,5]"); - await resonatorField.click(); // Clicking (anywhere) away from input feild to spawn undo button - await expect(ch2.getByTestId("undo-icon-wrapper")).toBeVisible(); - await expect(ch2.getByTestId("update-before-icon")).toBeVisible(); - ch2.getByTestId("update-before-icon").click(); // Click the icon to update the state - await expect(ch2.getByTestId("update-after-icon")).toBeVisible(); + // await expect(ch2.getByTestId("value-input")).toBeVisible(); + // await expect(ch2.getByTestId("value-container")).toContainText("80000000"); + // await expect(ch2.getByTestId("value-input")).toHaveValue("70000000"); + // ch2.getByTestId("value-input").click(); + // ch2.getByTestId("value-input").fill("[1,2,4,5]"); + // await resonatorField.click(); // Clicking (anywhere) away from input feild to spawn undo button + // await expect(ch2.getByTestId("undo-icon-wrapper")).toBeVisible(); + // await expect(ch2.getByTestId("update-before-icon")).toBeVisible(); + // ch2.getByTestId("update-before-icon").click(); // Click the icon to update the state + // await expect(ch2.getByTestId("update-after-icon")).toBeVisible(); } }); diff --git a/frontend/tests/setup.ts b/frontend/tests/setup.ts index 70c8b761..b1595307 100644 --- a/frontend/tests/setup.ts +++ b/frontend/tests/setup.ts @@ -1,5 +1,5 @@ -import { test } from '@playwright/test'; -import { execSync, spawn } from 'child_process'; +import { test } from "@playwright/test"; +import { spawn } from "child_process"; let serverProcess: any; @@ -8,52 +8,51 @@ test.beforeEach(async () => { // await context.clearPermissions(); // console.log('Cleared browser state'); - console.log('Starting Qualibrate server...'); + console.log("Starting Qualibrate server..."); - const fs = require('fs'); + const fs = require("fs"); - const filePath = '.venv/bin/activate'; + const filePath = ".venv/bin/activate"; if (fs.existsSync(filePath)) { - - console.log('Spawning QUAlibrate from virtual Environment .venv.'); - serverProcess = spawn('bash', ['-c', 'source .venv/bin/activate && qualibrate start']); - + console.log("Spawning QUAlibrate from virtual Environment .venv."); + serverProcess = spawn("bash", ["-c", "source .venv/bin/activate && qualibrate start"]); } else { - console.log('No virtual environment found. Spawning QUAlibrate.'); - serverProcess = spawn('bash', ['-c', 'qualibrate start']); + console.log("No virtual environment found. Spawning QUAlibrate."); + serverProcess = spawn("bash", ["-c", "qualibrate start"]); } - // Log server output for debugging - serverProcess.stdout.on('data', (data) => console.log(`SERVER: ${data}`)); - serverProcess.stderr.on('data', (data) => console.error(`SERVER ERR: ${data}`)); + // @ts-ignore + serverProcess.stdout.on("data", (data) => console.log(`SERVER: ${data}`)); + // @ts-ignore + serverProcess.stderr.on("data", (data) => console.error(`SERVER ERR: ${data}`)); // Poll for server readiness let serverReady = false; const maxRetries = 10; for (let i = 0; i < maxRetries; i++) { try { - const response = await fetch('http://127.0.0.1:8001'); // Replace with a valid health endpoint + const response = await fetch("http://127.0.0.1:8001"); // Replace with a valid health endpoint if (response.ok) { serverReady = true; break; } } catch { - console.log('Waiting for server to be ready...'); + console.log("Waiting for server to be ready..."); await new Promise((resolve) => setTimeout(resolve, 2000)); } } if (!serverReady) { - throw new Error('Server did not become ready in time.'); + throw new Error("Server did not become ready in time."); } }); test.afterEach(async () => { - console.log('Stopping Qualibrate server...'); + console.log("Stopping Qualibrate server..."); if (serverProcess) { - serverProcess.kill('SIGTERM'); + serverProcess.kill("SIGTERM"); } // Wait to ensure the process stops cleanly