From 562ad8e8b56792c7afda16c76784d78702d7eb56 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Mon, 22 Apr 2024 20:28:18 +0200 Subject: [PATCH] refactor: use `pydantic` for message validation (#91) ### Summary of Changes Use `pydantic` to easily describe valid messages. --------- Co-authored-by: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com> --- poetry.lock | 123 ++++++++- pyproject.toml | 3 +- src/safeds_runner/server/_messages.py | 253 ++++-------------- src/safeds_runner/server/_pipeline_manager.py | 10 +- src/safeds_runner/server/_server.py | 28 +- .../safeds_runner/server/test_memoization.py | 16 +- .../server/test_websocket_mock.py | 217 ++++++--------- 7 files changed, 274 insertions(+), 376 deletions(-) diff --git a/poetry.lock b/poetry.lock index 71e7882..e1804c6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -11,6 +11,17 @@ files = [ {file = "aiofiles-23.2.1.tar.gz", hash = "sha256:84ec2218d8419404abcb9f0c02df3f34c6e0a68ed41072acfb1cef5cbc29051a"}, ] +[[package]] +name = "annotated-types" +version = "0.6.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, + {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, +] + [[package]] name = "apipkg" version = "3.0.2" @@ -1798,6 +1809,116 @@ files = [ [package.extras] tests = ["pytest"] +[[package]] +name = "pydantic" +version = "2.7.0" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.7.0-py3-none-any.whl", hash = "sha256:9dee74a271705f14f9a1567671d144a851c675b072736f0a7b2608fd9e495352"}, + {file = "pydantic-2.7.0.tar.gz", hash = "sha256:b5ecdd42262ca2462e2624793551e80911a1e989f462910bb81aef974b4bb383"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.18.1" +typing-extensions = ">=4.6.1" + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.18.1" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.18.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ee9cf33e7fe14243f5ca6977658eb7d1042caaa66847daacbd2117adb258b226"}, + {file = "pydantic_core-2.18.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6b7bbb97d82659ac8b37450c60ff2e9f97e4eb0f8a8a3645a5568b9334b08b50"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df4249b579e75094f7e9bb4bd28231acf55e308bf686b952f43100a5a0be394c"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d0491006a6ad20507aec2be72e7831a42efc93193d2402018007ff827dc62926"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ae80f72bb7a3e397ab37b53a2b49c62cc5496412e71bc4f1277620a7ce3f52b"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58aca931bef83217fca7a390e0486ae327c4af9c3e941adb75f8772f8eeb03a1"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1be91ad664fc9245404a789d60cba1e91c26b1454ba136d2a1bf0c2ac0c0505a"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:667880321e916a8920ef49f5d50e7983792cf59f3b6079f3c9dac2b88a311d17"}, + {file = "pydantic_core-2.18.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f7054fdc556f5421f01e39cbb767d5ec5c1139ea98c3e5b350e02e62201740c7"}, + {file = "pydantic_core-2.18.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:030e4f9516f9947f38179249778709a460a3adb516bf39b5eb9066fcfe43d0e6"}, + {file = "pydantic_core-2.18.1-cp310-none-win32.whl", hash = "sha256:2e91711e36e229978d92642bfc3546333a9127ecebb3f2761372e096395fc649"}, + {file = "pydantic_core-2.18.1-cp310-none-win_amd64.whl", hash = "sha256:9a29726f91c6cb390b3c2338f0df5cd3e216ad7a938762d11c994bb37552edb0"}, + {file = "pydantic_core-2.18.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9ece8a49696669d483d206b4474c367852c44815fca23ac4e48b72b339807f80"}, + {file = "pydantic_core-2.18.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a5d83efc109ceddb99abd2c1316298ced2adb4570410defe766851a804fcd5b"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7973c381283783cd1043a8c8f61ea5ce7a3a58b0369f0ee0ee975eaf2f2a1b"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54c7375c62190a7845091f521add19b0f026bcf6ae674bdb89f296972272e86d"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd63cec4e26e790b70544ae5cc48d11b515b09e05fdd5eff12e3195f54b8a586"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:561cf62c8a3498406495cfc49eee086ed2bb186d08bcc65812b75fda42c38294"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68717c38a68e37af87c4da20e08f3e27d7e4212e99e96c3d875fbf3f4812abfc"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d5728e93d28a3c63ee513d9ffbac9c5989de8c76e049dbcb5bfe4b923a9739d"}, + {file = "pydantic_core-2.18.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f0f17814c505f07806e22b28856c59ac80cee7dd0fbb152aed273e116378f519"}, + {file = "pydantic_core-2.18.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d816f44a51ba5175394bc6c7879ca0bd2be560b2c9e9f3411ef3a4cbe644c2e9"}, + {file = "pydantic_core-2.18.1-cp311-none-win32.whl", hash = "sha256:09f03dfc0ef8c22622eaa8608caa4a1e189cfb83ce847045eca34f690895eccb"}, + {file = "pydantic_core-2.18.1-cp311-none-win_amd64.whl", hash = "sha256:27f1009dc292f3b7ca77feb3571c537276b9aad5dd4efb471ac88a8bd09024e9"}, + {file = "pydantic_core-2.18.1-cp311-none-win_arm64.whl", hash = "sha256:48dd883db92e92519201f2b01cafa881e5f7125666141a49ffba8b9facc072b0"}, + {file = "pydantic_core-2.18.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b6b0e4912030c6f28bcb72b9ebe4989d6dc2eebcd2a9cdc35fefc38052dd4fe8"}, + {file = "pydantic_core-2.18.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3202a429fe825b699c57892d4371c74cc3456d8d71b7f35d6028c96dfecad31"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3982b0a32d0a88b3907e4b0dc36809fda477f0757c59a505d4e9b455f384b8b"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25595ac311f20e5324d1941909b0d12933f1fd2171075fcff763e90f43e92a0d"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14fe73881cf8e4cbdaded8ca0aa671635b597e42447fec7060d0868b52d074e6"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca976884ce34070799e4dfc6fbd68cb1d181db1eefe4a3a94798ddfb34b8867f"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684d840d2c9ec5de9cb397fcb3f36d5ebb6fa0d94734f9886032dd796c1ead06"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:54764c083bbe0264f0f746cefcded6cb08fbbaaf1ad1d78fb8a4c30cff999a90"}, + {file = "pydantic_core-2.18.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:201713f2f462e5c015b343e86e68bd8a530a4f76609b33d8f0ec65d2b921712a"}, + {file = "pydantic_core-2.18.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fd1a9edb9dd9d79fbeac1ea1f9a8dd527a6113b18d2e9bcc0d541d308dae639b"}, + {file = "pydantic_core-2.18.1-cp312-none-win32.whl", hash = "sha256:d5e6b7155b8197b329dc787356cfd2684c9d6a6b1a197f6bbf45f5555a98d411"}, + {file = "pydantic_core-2.18.1-cp312-none-win_amd64.whl", hash = "sha256:9376d83d686ec62e8b19c0ac3bf8d28d8a5981d0df290196fb6ef24d8a26f0d6"}, + {file = "pydantic_core-2.18.1-cp312-none-win_arm64.whl", hash = "sha256:c562b49c96906b4029b5685075fe1ebd3b5cc2601dfa0b9e16c2c09d6cbce048"}, + {file = "pydantic_core-2.18.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:3e352f0191d99fe617371096845070dee295444979efb8f27ad941227de6ad09"}, + {file = "pydantic_core-2.18.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0295d52b012cbe0d3059b1dba99159c3be55e632aae1999ab74ae2bd86a33d7"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56823a92075780582d1ffd4489a2e61d56fd3ebb4b40b713d63f96dd92d28144"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd3f79e17b56741b5177bcc36307750d50ea0698df6aa82f69c7db32d968c1c2"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38a5024de321d672a132b1834a66eeb7931959c59964b777e8f32dbe9523f6b1"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2ce426ee691319d4767748c8e0895cfc56593d725594e415f274059bcf3cb76"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2adaeea59849ec0939af5c5d476935f2bab4b7f0335b0110f0f069a41024278e"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9b6431559676a1079eac0f52d6d0721fb8e3c5ba43c37bc537c8c83724031feb"}, + {file = "pydantic_core-2.18.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:85233abb44bc18d16e72dc05bf13848a36f363f83757541f1a97db2f8d58cfd9"}, + {file = "pydantic_core-2.18.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:641a018af4fe48be57a2b3d7a1f0f5dbca07c1d00951d3d7463f0ac9dac66622"}, + {file = "pydantic_core-2.18.1-cp38-none-win32.whl", hash = "sha256:63d7523cd95d2fde0d28dc42968ac731b5bb1e516cc56b93a50ab293f4daeaad"}, + {file = "pydantic_core-2.18.1-cp38-none-win_amd64.whl", hash = "sha256:907a4d7720abfcb1c81619863efd47c8a85d26a257a2dbebdb87c3b847df0278"}, + {file = "pydantic_core-2.18.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:aad17e462f42ddbef5984d70c40bfc4146c322a2da79715932cd8976317054de"}, + {file = "pydantic_core-2.18.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:94b9769ba435b598b547c762184bcfc4783d0d4c7771b04a3b45775c3589ca44"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80e0e57cc704a52fb1b48f16d5b2c8818da087dbee6f98d9bf19546930dc64b5"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:76b86e24039c35280ceee6dce7e62945eb93a5175d43689ba98360ab31eebc4a"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12a05db5013ec0ca4a32cc6433f53faa2a014ec364031408540ba858c2172bb0"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:250ae39445cb5475e483a36b1061af1bc233de3e9ad0f4f76a71b66231b07f88"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a32204489259786a923e02990249c65b0f17235073149d0033efcebe80095570"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6395a4435fa26519fd96fdccb77e9d00ddae9dd6c742309bd0b5610609ad7fb2"}, + {file = "pydantic_core-2.18.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2533ad2883f001efa72f3d0e733fb846710c3af6dcdd544fe5bf14fa5fe2d7db"}, + {file = "pydantic_core-2.18.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b560b72ed4816aee52783c66854d96157fd8175631f01ef58e894cc57c84f0f6"}, + {file = "pydantic_core-2.18.1-cp39-none-win32.whl", hash = "sha256:582cf2cead97c9e382a7f4d3b744cf0ef1a6e815e44d3aa81af3ad98762f5a9b"}, + {file = "pydantic_core-2.18.1-cp39-none-win_amd64.whl", hash = "sha256:ca71d501629d1fa50ea7fa3b08ba884fe10cefc559f5c6c8dfe9036c16e8ae89"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e178e5b66a06ec5bf51668ec0d4ac8cfb2bdcb553b2c207d58148340efd00143"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:72722ce529a76a4637a60be18bd789d8fb871e84472490ed7ddff62d5fed620d"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fe0c1ce5b129455e43f941f7a46f61f3d3861e571f2905d55cdbb8b5c6f5e2c"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4284c621f06a72ce2cb55f74ea3150113d926a6eb78ab38340c08f770eb9b4d"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a0c3e718f4e064efde68092d9d974e39572c14e56726ecfaeebbe6544521f47"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2027493cc44c23b598cfaf200936110433d9caa84e2c6cf487a83999638a96ac"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:76909849d1a6bffa5a07742294f3fa1d357dc917cb1fe7b470afbc3a7579d539"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ee7ccc7fb7e921d767f853b47814c3048c7de536663e82fbc37f5eb0d532224b"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ee2794111c188548a4547eccc73a6a8527fe2af6cf25e1a4ebda2fd01cdd2e60"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a139fe9f298dc097349fb4f28c8b81cc7a202dbfba66af0e14be5cfca4ef7ce5"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d074b07a10c391fc5bbdcb37b2f16f20fcd9e51e10d01652ab298c0d07908ee2"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c69567ddbac186e8c0aadc1f324a60a564cfe25e43ef2ce81bcc4b8c3abffbae"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf1c7b78cddb5af00971ad5294a4583188bda1495b13760d9f03c9483bb6203"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2684a94fdfd1b146ff10689c6e4e815f6a01141781c493b97342cdc5b06f4d5d"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:73c1bc8a86a5c9e8721a088df234265317692d0b5cd9e86e975ce3bc3db62a59"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e60defc3c15defb70bb38dd605ff7e0fae5f6c9c7cbfe0ad7868582cb7e844a6"}, + {file = "pydantic_core-2.18.1.tar.gz", hash = "sha256:de9d3e8717560eb05e28739d1b35e4eac2e458553a52a301e51352a7ffc86a35"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + [[package]] name = "pygments" version = "2.17.2" @@ -2977,4 +3098,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11,<3.13" -content-hash = "485bb1a340b5cbea2dfdf201144c82429fd6bb0d67430e2056e8d6d2d9131218" +content-hash = "fa80aeb341aef3b6f8cabeda1bbe47d5a45c710de41ebf6f89f010ae7a3a4073" diff --git a/pyproject.toml b/pyproject.toml index e0bbf13..da6fb52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,8 +19,9 @@ safe-ds-runner = "safeds_runner.main:main" python = "^3.11,<3.13" safe-ds = ">=0.20,<0.22" hypercorn = "^0.16.0" -quart = "^0.19.4" psutil = "^5.9.8" +pydantic = "^2.7.0" +quart = "^0.19.4" [tool.poetry.dev-dependencies] pytest = "^8.1.1" diff --git a/src/safeds_runner/server/_messages.py b/src/safeds_runner/server/_messages.py index bae1efa..5738bd9 100644 --- a/src/safeds_runner/server/_messages.py +++ b/src/safeds_runner/server/_messages.py @@ -7,6 +7,8 @@ from dataclasses import dataclass from typing import Any +from pydantic import BaseModel, ConfigDict, Field + message_type_program = "program" message_type_placeholder_query = "placeholder_query" message_type_placeholder_type = "placeholder_type" @@ -74,10 +76,25 @@ def to_dict(self) -> dict[str, Any]: return dataclasses.asdict(self) -@dataclass(frozen=True) -class MessageDataProgram: +class ProgramMessage(BaseModel): + """ + A message object for a `program` message. + + Parameters + ---------- + data : ProgramMessageData + Data of the program message. + """ + + id: str + data: ProgramMessageData + + model_config = ConfigDict(extra="forbid") + + +class ProgramMessageData(BaseModel): """ - Message data for a program message. + Message data for a `program` message. Parameters ---------- @@ -85,51 +102,20 @@ class MessageDataProgram: A dictionary containing the code needed for executed, in a virtual filesystem. Keys of the outer dictionary are the module path, keys of the inner dictionary are the module name. The values of the inner dictionary is the python code for each module. - main : ProgramMainInformation + main : ProgramMessageMainInformation Information where the main pipeline (the pipeline to be executed) is located. cwd: Current working directory to use for execution. If not set, the default working directory is used. """ code: dict[str, dict[str, str]] - main: ProgramMainInformation + main: ProgramMessageMainInformation cwd: str | None = None - @staticmethod - def from_dict(d: dict[str, Any]) -> MessageDataProgram: - """ - Create a new MessageDataProgram object from a dictionary. - - Parameters - ---------- - d : dict[str, Any] - Dictionary which should contain all needed fields. - - Returns - ------- - MessageDataProgram - Dataclass which contains information copied from the provided dictionary. - """ - return MessageDataProgram( - d["code"], - ProgramMainInformation.from_dict(d["main"]), - d.get("cwd"), - ) - - def to_dict(self) -> dict[str, Any]: - """ - Convert this dataclass to a dictionary. - - Returns - ------- - dict[str, Any] - Dictionary containing all the fields which are part of this dataclass. - """ - return dataclasses.asdict(self) # pragma: no cover + model_config = ConfigDict(extra="forbid") -@dataclass(frozen=True) -class ProgramMainInformation: +class ProgramMessageMainInformation(BaseModel): """ Information that can be used to locate a pipeline. @@ -147,37 +133,26 @@ class ProgramMainInformation: module: str pipeline: str - @staticmethod - def from_dict(d: dict[str, Any]) -> ProgramMainInformation: - """ - Create a new ProgramMainInformation object from a dictionary. + model_config = ConfigDict(extra="forbid") - Parameters - ---------- - d : dict[str, Any] - Dictionary which should contain all needed fields. - Returns - ------- - ProgramMainInformation - Dataclass which contains information copied from the provided dictionary. - """ - return ProgramMainInformation(**d) +class QueryMessage(BaseModel): + """ + A message object for a `placeholder_query` message. - def to_dict(self) -> dict[str, Any]: - """ - Convert this dataclass to a dictionary. + Parameters + ---------- + data : QueryMessageData + Data of the placeholder query message. + """ - Returns - ------- - dict[str, Any] - Dictionary containing all the fields which are part of this dataclass. - """ - return dataclasses.asdict(self) # pragma: no cover + id: str + data: QueryMessageData + model_config = ConfigDict(extra="forbid") -@dataclass(frozen=True) -class QueryWindow: + +class QueryMessageWindow(BaseModel): """ Information that is used to create a subset of the data of a placeholder. @@ -192,37 +167,10 @@ class QueryWindow: begin: int | None = None size: int | None = None - @staticmethod - def from_dict(d: dict[str, Any]) -> QueryWindow: - """ - Create a new QueryWindow object from a dictionary. - - Parameters - ---------- - d : dict[str, Any] - Dictionary which should contain all needed fields. - - Returns - ------- - QueryWindow - Dataclass which contains information copied from the provided dictionary. - """ - return QueryWindow(**d) - - def to_dict(self) -> dict[str, Any]: - """ - Convert this dataclass to a dictionary. - - Returns - ------- - dict[str, Any] - Dictionary containing all the fields which are part of this dataclass. - """ - return dataclasses.asdict(self) # pragma: no cover + model_config = ConfigDict(extra="forbid") -@dataclass(frozen=True) -class MessageQueryInformation: +class QueryMessageData(BaseModel): """ Information used to query a placeholder with optional window bounds. Only complex types like tables are affected by window bounds. @@ -230,40 +178,14 @@ class MessageQueryInformation: ---------- name : str Placeholder name that is queried. - window : QueryWindow + window : QueryMessageWindow Window bounds for requesting only a subset of the available data. """ name: str - window: QueryWindow = dataclasses.field(default_factory=QueryWindow) - - @staticmethod - def from_dict(d: dict[str, Any]) -> MessageQueryInformation: - """ - Create a new MessageQueryInformation object from a dictionary. - - Parameters - ---------- - d : dict[str, Any] - Dictionary which should contain all needed fields. - - Returns - ------- - MessageQueryInformation - Dataclass which contains information copied from the provided dictionary. - """ - return MessageQueryInformation(name=d["name"], window=QueryWindow.from_dict(d["window"])) + window: QueryMessageWindow = Field(default_factory=QueryMessageWindow) - def to_dict(self) -> dict[str, Any]: - """ - Convert this dataclass to a dictionary. - - Returns - ------- - dict[str, Any] - Dictionary containing all the fields which are part of this dataclass. - """ - return dataclasses.asdict(self) # pragma: no cover + model_config = ConfigDict(extra="forbid") def create_placeholder_description(name: str, type_: str) -> dict[str, str]: @@ -285,7 +207,7 @@ def create_placeholder_description(name: str, type_: str) -> dict[str, str]: return {"name": name, "type": type_} -def create_placeholder_value(placeholder_query: MessageQueryInformation, type_: str, value: Any) -> dict[str, Any]: +def create_placeholder_value(placeholder_query: QueryMessageData, type_: str, value: Any) -> dict[str, Any]: """ Create the message data of a placeholder value message containing name, type and the actual value. @@ -294,7 +216,7 @@ def create_placeholder_value(placeholder_query: MessageQueryInformation, type_: Parameters ---------- - placeholder_query : MessageQueryInformation + placeholder_query : QueryMessageData Query of the placeholder. type_ : str Type of the placeholder. @@ -388,87 +310,4 @@ def parse_validate_message(message: str) -> tuple[Message | None, str | None, st elif not isinstance(message_dict["id"], str): return None, f"Message id is not a string: {message}", "Invalid Message: invalid id" else: - return Message.from_dict(message_dict), None, None - - -def validate_program_message_data(message_data: dict[str, Any] | str) -> tuple[MessageDataProgram | None, str | None]: - """ - Validate the message data of a program message. - - Parameters - ---------- - message_data : dict[str, Any] | str - Message data dictionary or string. - - Returns - ------- - tuple[MessageDataProgram | None, str | None] - A tuple containing either a validated message data object or an error message. - """ - if not isinstance(message_data, dict): - return None, "Message data is not a JSON object" - - cwd = message_data.get("cwd", None) - - if "code" not in message_data: - return None, "No 'code' parameter given" - elif "main" not in message_data: - return None, "No 'main' parameter given" - elif ( - not isinstance(message_data["main"], dict) - or "modulepath" not in message_data["main"] - or "module" not in message_data["main"] - or "pipeline" not in message_data["main"] - or len(message_data["main"]) != 3 - ): - return None, "Invalid 'main' parameter given" - elif not isinstance(message_data["code"], dict): - return None, "Invalid 'code' parameter given" - elif cwd is not None and not isinstance(cwd, str): - return None, "Invalid 'cwd' parameter given" - else: - code: dict = message_data["code"] - for key in code: - if not isinstance(code[key], dict): - return None, "Invalid 'code' parameter given" - next_dict: dict = code[key] - for next_key in next_dict: - if not isinstance(next_dict[next_key], str): - return None, "Invalid 'code' parameter given" - return MessageDataProgram.from_dict(message_data), None - - -def validate_placeholder_query_message_data( - message_data: dict[str, Any] | str, -) -> tuple[MessageQueryInformation | None, str | None]: - """ - Validate the message data of a placeholder query message. - - Parameters - ---------- - message_data : dict[str, Any] | str - Message data dictionary or string. - - Returns - ------- - tuple[MessageQueryInformation | None, str | None] - A tuple containing either the validated message data or an error message. - """ - if not isinstance(message_data, dict): - return None, "Message data is not a JSON object" - elif "name" not in message_data: - return None, "No 'name' parameter given" - elif ( - "window" in message_data - and "begin" in message_data["window"] - and not isinstance(message_data["window"]["begin"], int) - ): - return None, "Invalid 'window'.'begin' parameter given" - elif ( - "window" in message_data - and "size" in message_data["window"] - and not isinstance(message_data["window"]["size"], int) - ): - return None, "Invalid 'window'.'size' parameter given" - else: - return MessageQueryInformation.from_dict(message_data), None + return Message(**message_dict), None, None diff --git a/src/safeds_runner/server/_pipeline_manager.py b/src/safeds_runner/server/_pipeline_manager.py index d40584d..0a8755a 100644 --- a/src/safeds_runner/server/_pipeline_manager.py +++ b/src/safeds_runner/server/_pipeline_manager.py @@ -28,7 +28,7 @@ ) from ._messages import ( Message, - MessageDataProgram, + ProgramMessageData, create_placeholder_description, create_runtime_error_description, create_runtime_progress_done, @@ -136,7 +136,7 @@ def disconnect(self, websocket_connection_queue: asyncio.Queue) -> None: def execute_pipeline( self, - pipeline: MessageDataProgram, + pipeline: ProgramMessageData, execution_id: str, ) -> None: """ @@ -144,7 +144,7 @@ def execute_pipeline( Parameters ---------- - pipeline : MessageDataProgram + pipeline : ProgramMessageData Message object that contains the information to run a pipeline. execution_id : str Unique ID to identify this execution. @@ -201,7 +201,7 @@ class PipelineProcess: def __init__( self, - pipeline: MessageDataProgram, + pipeline: ProgramMessageData, execution_id: str, messages_queue: queue.Queue[Message], placeholder_map: dict[str, Any], @@ -212,7 +212,7 @@ def __init__( Parameters ---------- - pipeline : MessageDataProgram + pipeline : ProgramMessageData Message object that contains the information to run a pipeline. execution_id : str Unique ID to identify this process. diff --git a/src/safeds_runner/server/_server.py b/src/safeds_runner/server/_server.py index 12095e4..9d75fbe 100644 --- a/src/safeds_runner/server/_server.py +++ b/src/safeds_runner/server/_server.py @@ -7,16 +7,17 @@ import hypercorn.asyncio import quart.app +from pydantic import ValidationError from ._json_encoder import SafeDsEncoder from ._messages import ( Message, + ProgramMessageData, + QueryMessageData, create_placeholder_value, message_type_placeholder_value, message_types, parse_validate_message, - validate_placeholder_query_message_data, - validate_program_message_data, ) from ._pipeline_manager import PipelineManager @@ -110,26 +111,29 @@ async def _ws_main_foreground( pipeline_manager.shutdown() sys.exit(0) case "program": - program_data, invalid_message = validate_program_message_data(received_object.data) - if program_data is None: - logging.error("Invalid message data specified in: %s (%s)", received_message, invalid_message) + try: + program_data = ProgramMessageData(**received_object.data) + except ValidationError as validation_error: + logging.exception("Invalid message data specified in: %s", received_message) await output_queue.put(None) pipeline_manager.disconnect(output_queue) - await ws.close(code=1000, reason=invalid_message) + await ws.close(code=1000, reason=str(validation_error)) return + # This should only be called from the extension as it is a security risk pipeline_manager.execute_pipeline(program_data, received_object.id) case "placeholder_query": # For this query, a response can be directly sent to the requesting connection - placeholder_query_data, invalid_message = validate_placeholder_query_message_data( - received_object.data, - ) - if placeholder_query_data is None: - logging.error("Invalid message data specified in: %s (%s)", received_message, invalid_message) + + try: + placeholder_query_data = QueryMessageData(**received_object.data) + except ValidationError as validation_error: + logging.exception("Invalid message data specified in: %s", received_message) await output_queue.put(None) pipeline_manager.disconnect(output_queue) - await ws.close(code=1000, reason=invalid_message) + await ws.close(code=1000, reason=str(validation_error)) return + placeholder_type, placeholder_value = pipeline_manager.get_placeholder( received_object.id, placeholder_query_data.name, diff --git a/tests/safeds_runner/server/test_memoization.py b/tests/safeds_runner/server/test_memoization.py index 71fdfef..ed70656 100644 --- a/tests/safeds_runner/server/test_memoization.py +++ b/tests/safeds_runner/server/test_memoization.py @@ -22,7 +22,7 @@ StatOrderExtractor, ) from safeds_runner.server._memoization_utils import _make_hashable -from safeds_runner.server._messages import MessageDataProgram, ProgramMainInformation +from safeds_runner.server._messages import ProgramMessageData, ProgramMessageMainInformation from safeds_runner.server._pipeline_manager import ( PipelineProcess, absolute_path, @@ -52,7 +52,7 @@ def test_memoization_static_already_present_values( expected_result: Any, ) -> None: _pipeline_manager.current_pipeline = PipelineProcess( - MessageDataProgram({}, ProgramMainInformation("", "", "")), + ProgramMessageData(code={}, main=ProgramMessageMainInformation(modulepath="", module="", pipeline="")), "", Queue(), {}, @@ -93,7 +93,7 @@ def test_memoization_static_not_present_values( expected_result: Any, ) -> None: _pipeline_manager.current_pipeline = PipelineProcess( - MessageDataProgram({}, ProgramMainInformation("", "", "")), + ProgramMessageData(code={}, main=ProgramMessageMainInformation(modulepath="", module="", pipeline="")), "", Queue(), {}, @@ -147,7 +147,7 @@ def test_memoization_dynamic( expected_result: Any, ) -> None: _pipeline_manager.current_pipeline = PipelineProcess( - MessageDataProgram({}, ProgramMainInformation("", "", "")), + ProgramMessageData(code={}, main=ProgramMessageMainInformation(modulepath="", module="", pipeline="")), "", Queue(), {}, @@ -191,7 +191,7 @@ def test_memoization_dynamic_contains_correct_fully_qualified_name( fully_qualified_function_name: Any, ) -> None: _pipeline_manager.current_pipeline = PipelineProcess( - MessageDataProgram({}, ProgramMainInformation("", "", "")), + ProgramMessageData(code={}, main=ProgramMessageMainInformation(modulepath="", module="", pipeline="")), "", Queue(), {}, @@ -226,7 +226,7 @@ def test_memoization_dynamic_not_base_name( fully_qualified_function_name: Any, ) -> None: _pipeline_manager.current_pipeline = PipelineProcess( - MessageDataProgram({}, ProgramMainInformation("", "", "")), + ProgramMessageData(code={}, main=ProgramMessageMainInformation(modulepath="", module="", pipeline="")), "", Queue(), {}, @@ -256,7 +256,7 @@ def test_memoization_static_unhashable_values( expected_result: Any, ) -> None: _pipeline_manager.current_pipeline = PipelineProcess( - MessageDataProgram({}, ProgramMainInformation("", "", "")), + ProgramMessageData(code={}, main=ProgramMessageMainInformation(modulepath="", module="", pipeline="")), "", Queue(), {}, @@ -457,7 +457,7 @@ def test_memoization_limited_static_not_present_values( ) memo_map.max_size = 45 _pipeline_manager.current_pipeline = PipelineProcess( - MessageDataProgram({}, ProgramMainInformation("", "", "")), + ProgramMessageData(code={}, main=ProgramMessageMainInformation(modulepath="", module="", pipeline="")), "", Queue(), {}, diff --git a/tests/safeds_runner/server/test_websocket_mock.py b/tests/safeds_runner/server/test_websocket_mock.py index afd3adc..47dc59e 100644 --- a/tests/safeds_runner/server/test_websocket_mock.py +++ b/tests/safeds_runner/server/test_websocket_mock.py @@ -1,21 +1,26 @@ +from __future__ import annotations + import asyncio import json import logging import multiprocessing +import re import sys import time -import typing +from typing import TYPE_CHECKING, Any import pytest import safeds_runner.server.main import simple_websocket +from pydantic import ValidationError from quart.testing.connections import WebsocketDisconnectError from safeds.data.tabular.containers import Table from safeds_runner.server._json_encoder import SafeDsEncoder from safeds_runner.server._messages import ( Message, - MessageQueryInformation, - QueryWindow, + ProgramMessageData, + QueryMessageData, + QueryMessageWindow, create_placeholder_description, create_placeholder_value, create_runtime_progress_done, @@ -24,11 +29,12 @@ message_type_runtime_error, message_type_runtime_progress, parse_validate_message, - validate_placeholder_query_message_data, - validate_program_message_data, ) from safeds_runner.server._server import SafeDsServer +if TYPE_CHECKING: + from regex import Regex + @pytest.mark.parametrize( argnames="websocket_message", @@ -179,178 +185,105 @@ def test_should_fail_message_validation_reason_general(websocket_message: str, e @pytest.mark.parametrize( - argnames="websocket_message,exception_message", + argnames=["data", "exception_regex"], argvalues=[ - (json.dumps({"type": "program", "id": "1234", "data": "a"}), "Message data is not a JSON object"), - ( - json.dumps( - { - "type": "program", - "id": "1234", - "data": {"main": {"modulepath": "1", "module": "2", "pipeline": "3"}}, - }, - ), - "No 'code' parameter given", - ), ( - json.dumps({"type": "program", "id": "1234", "data": {"code": {"": {"entry": ""}}}}), - "No 'main' parameter given", + {"main": {"modulepath": "1", "module": "2", "pipeline": "3"}}, + re.compile(r"code[\s\S]*missing"), ), ( - json.dumps( - { - "type": "program", - "id": "1234", - "data": {"code": {"": {"entry": ""}}, "main": {"modulepath": "1", "module": "2"}}, - }, - ), - "Invalid 'main' parameter given", + {"code": {"": {"entry": ""}}}, + re.compile(r"main[\s\S]*missing"), ), ( - json.dumps( - { - "type": "program", - "id": "1234", - "data": {"code": {"": {"entry": ""}}, "main": {"modulepath": "1", "pipeline": "3"}}, - }, - ), - "Invalid 'main' parameter given", + {"code": {"": {"entry": ""}}, "main": {"modulepath": "1", "module": "2"}}, + re.compile(r"main.pipeline[\s\S]*missing"), ), ( - json.dumps( - { - "type": "program", - "id": "1234", - "data": {"code": {"": {"entry": ""}}, "main": {"module": "2", "pipeline": "3"}}, - }, - ), - "Invalid 'main' parameter given", + {"code": {"": {"entry": ""}}, "main": {"modulepath": "1", "pipeline": "3"}}, + re.compile(r"main.module[\s\S]*missing"), ), ( - json.dumps( - { - "type": "program", - "id": "1234", - "data": { - "code": {"": {"entry": ""}}, - "main": {"modulepath": "1", "module": "2", "pipeline": "3", "other": "4"}, - }, - }, - ), - "Invalid 'main' parameter given", + {"code": {"": {"entry": ""}}, "main": {"module": "2", "pipeline": "3"}}, + re.compile(r"main.modulepath[\s\S]*missing"), ), ( - json.dumps( - { - "type": "program", - "id": "1234", - "data": { - "code": {"": {"entry": ""}}, - "main": {"modulepath": "1", "module": "2", "pipeline": "3", "other": {"4": "a"}}, - }, - }, - ), - "Invalid 'main' parameter given", + { + "code": {"": {"entry": ""}}, + "main": {"modulepath": "1", "module": "2", "pipeline": "3", "other": "4"}, + }, + re.compile(r"main.other[\s\S]*extra_forbidden"), ), ( - json.dumps( - { - "type": "program", - "id": "1234", - "data": {"code": "a", "main": {"modulepath": "1", "module": "2", "pipeline": "3"}}, - }, - ), - "Invalid 'code' parameter given", + {"code": "a", "main": {"modulepath": "1", "module": "2", "pipeline": "3"}}, + re.compile(r"code[\s\S]*dict_type"), ), ( - json.dumps( - { - "type": "program", - "id": "1234", - "data": {"code": {"": "a"}, "main": {"modulepath": "1", "module": "2", "pipeline": "3"}}, - }, - ), - "Invalid 'code' parameter given", + {"code": {"a": "n"}, "main": {"modulepath": "1", "module": "2", "pipeline": "3"}}, + re.compile(r"code\.a[\s\S]*dict_type"), ), ( - json.dumps( - { - "type": "program", - "id": "1234", - "data": { - "code": {"": {"a": {"b": "c"}}}, - "main": {"modulepath": "1", "module": "2", "pipeline": "3"}, - }, - }, - ), - "Invalid 'code' parameter given", + { + "code": {"a": {"b": {"c": "d"}}}, + "main": {"modulepath": "1", "module": "2", "pipeline": "3"}, + }, + re.compile(r"code\.a\.b[\s\S]*string_type"), ), ( - json.dumps( - { - "type": "program", - "id": "1234", - "data": { - "code": {}, - "main": {"modulepath": "1", "module": "2", "pipeline": "3"}, - "cwd": 1, - }, - }, - ), - "Invalid 'cwd' parameter given", + { + "code": {}, + "main": {"modulepath": "1", "module": "2", "pipeline": "3"}, + "cwd": 1, + }, + re.compile(r"cwd[\s\S]*string_type"), ), ], ids=[ - "program_invalid_data", "program_no_code", "program_no_main", "program_invalid_main1", "program_invalid_main2", "program_invalid_main3", "program_invalid_main4", - "program_invalid_main5", "program_invalid_code1", "program_invalid_code2", "program_invalid_code3", "program_invalid_cwd", ], ) -def test_should_fail_message_validation_reason_program(websocket_message: str, exception_message: str) -> None: - received_object, error_detail, error_short = parse_validate_message(websocket_message) - assert received_object is not None - program_data, invalid_message = validate_program_message_data(received_object.data) - assert invalid_message == exception_message +def test_should_fail_message_validation_reason_program(data: dict[str, Any], exception_regex: str) -> None: + with pytest.raises(ValidationError, match=exception_regex): + ProgramMessageData(**data) @pytest.mark.parametrize( - argnames="websocket_message,exception_message", + argnames=["data", "exception_regex"], argvalues=[ - (json.dumps({"type": "placeholder_query", "id": "123", "data": "abc"}), "Message data is not a JSON object"), - (json.dumps({"type": "placeholder_query", "id": "123", "data": {"a": "v"}}), "No 'name' parameter given"), ( - json.dumps({"type": "placeholder_query", "id": "123", "data": {"name": "v", "window": {"begin": "a"}}}), - "Invalid 'window'.'begin' parameter given", + {"a": "v"}, + re.compile(r"name[\s\S]*missing"), + ), + ( + {"name": "v", "window": {"begin": "a"}}, + re.compile(r"window.begin[\s\S]*int_parsing"), ), ( - json.dumps({"type": "placeholder_query", "id": "123", "data": {"name": "v", "window": {"size": "a"}}}), - "Invalid 'window'.'size' parameter given", + {"name": "v", "window": {"size": "a"}}, + re.compile(r"window.size[\s\S]*int_parsing"), ), ], ids=[ - "placeholder_query_invalid_data1", - "placeholder_query_invalid_data2", - "placeholder_query_invalid_data3", - "placeholder_query_invalid_data4", + "missing_name", + "wrong_type_begin", + "wrong_type_size", ], ) def test_should_fail_message_validation_reason_placeholder_query( - websocket_message: str, - exception_message: str, + data: dict[str, Any], + exception_regex: Regex, ) -> None: - received_object, error_detail, error_short = parse_validate_message(websocket_message) - assert received_object is not None - program_data, invalid_message = validate_placeholder_query_message_data(received_object.data) - assert invalid_message == exception_message + with pytest.raises(ValidationError, match=exception_regex): + QueryMessageData(**data) @pytest.mark.parametrize( @@ -463,25 +396,25 @@ async def test_should_execute_pipeline_return_exception( Message( message_type_placeholder_value, "abcdefg", - create_placeholder_value(MessageQueryInformation("value1"), "Int", 1), + create_placeholder_value(QueryMessageData(name="value1"), "Int", 1), ), # Query Result Valid (memoized) Message( message_type_placeholder_value, "abcdefg", - create_placeholder_value(MessageQueryInformation("table"), "Table", {"a": [1, 2], "b": [3, 4]}), + create_placeholder_value(QueryMessageData(name="table"), "Table", {"a": [1, 2], "b": [3, 4]}), ), # Query Result not displayable Message( message_type_placeholder_value, "abcdefg", - create_placeholder_value(MessageQueryInformation("obj"), "object", ""), + create_placeholder_value(QueryMessageData(name="obj"), "object", ""), ), # Query Result Invalid Message( message_type_placeholder_value, "abcdefg", - create_placeholder_value(MessageQueryInformation("value2"), "", ""), + create_placeholder_value(QueryMessageData(name="value2"), "", ""), ), ], ), @@ -570,7 +503,7 @@ async def test_should_execute_pipeline_return_valid_placeholder( Message( message_type_placeholder_value, "unknown-code-id-never-generated", - create_placeholder_value(MessageQueryInformation("v"), "", ""), + create_placeholder_value(QueryMessageData(name="v"), "", ""), ), ), ], @@ -667,13 +600,13 @@ def helper_should_accept_at_least_2_parallel_connections_in_subprocess_server( argnames="query,type_,value,result", argvalues=[ ( - MessageQueryInformation("name"), + QueryMessageData(name="name"), "Table", Table.from_dict({"a": [1, 2, 1, 2, 3, 2, 1], "b": [3, 4, 6, 2, 1, 2, 3]}), '{"name": "name", "type": "Table", "value": {"a": [1, 2, 1, 2, 3, 2, 1], "b": [3, 4, 6, 2, 1, 2, 3]}}', ), ( - MessageQueryInformation("name", QueryWindow(0, 1)), + QueryMessageData(name="name", window=QueryMessageWindow(begin=0, size=1)), "Table", Table.from_dict({"a": [1, 2, 1, 2, 3, 2, 1], "b": [3, 4, 6, 2, 1, 2, 3]}), ( @@ -682,7 +615,7 @@ def helper_should_accept_at_least_2_parallel_connections_in_subprocess_server( ), ), ( - MessageQueryInformation("name", QueryWindow(4, 3)), + QueryMessageData(name="name", window=QueryMessageWindow(begin=4, size=3)), "Table", Table.from_dict({"a": [1, 2, 1, 2, 3, 2, 1], "b": [3, 4, 6, 2, 1, 2, 3]}), ( @@ -691,7 +624,7 @@ def helper_should_accept_at_least_2_parallel_connections_in_subprocess_server( ), ), ( - MessageQueryInformation("name", QueryWindow(0, 0)), + QueryMessageData(name="name", window=QueryMessageWindow(begin=0, size=0)), "Table", Table.from_dict({"a": [1, 2, 1, 2, 3, 2, 1], "b": [3, 4, 6, 2, 1, 2, 3]}), ( @@ -700,7 +633,7 @@ def helper_should_accept_at_least_2_parallel_connections_in_subprocess_server( ), ), ( - MessageQueryInformation("name", QueryWindow(4, 30)), + QueryMessageData(name="name", window=QueryMessageWindow(begin=4, size=30)), "Table", Table.from_dict({"a": [1, 2, 1, 2, 3, 2, 1], "b": [3, 4, 6, 2, 1, 2, 3]}), ( @@ -709,7 +642,7 @@ def helper_should_accept_at_least_2_parallel_connections_in_subprocess_server( ), ), ( - MessageQueryInformation("name", QueryWindow(4, None)), + QueryMessageData(name="name", window=QueryMessageWindow(begin=4, size=None)), "Table", Table.from_dict({"a": [1, 2, 1, 2, 3, 2, 1], "b": [3, 4, 6, 2, 1, 2, 3]}), ( @@ -718,7 +651,7 @@ def helper_should_accept_at_least_2_parallel_connections_in_subprocess_server( ), ), ( - MessageQueryInformation("name", QueryWindow(0, -5)), + QueryMessageData(name="name", window=QueryMessageWindow(begin=0, size=-5)), "Table", Table.from_dict({"a": [1, 2, 1, 2, 3, 2, 1], "b": [3, 4, 6, 2, 1, 2, 3]}), ( @@ -727,7 +660,7 @@ def helper_should_accept_at_least_2_parallel_connections_in_subprocess_server( ), ), ( - MessageQueryInformation("name", QueryWindow(-5, None)), + QueryMessageData(name="name", window=QueryMessageWindow(begin=-5, size=None)), "Table", Table.from_dict({"a": [1, 2, 1, 2, 3, 2, 1], "b": [3, 4, 6, 2, 1, 2, 3]}), ( @@ -747,7 +680,7 @@ def helper_should_accept_at_least_2_parallel_connections_in_subprocess_server( "query_windowed_negative_offset", ], ) -def test_windowed_placeholder(query: MessageQueryInformation, type_: str, value: typing.Any, result: str) -> None: +def test_windowed_placeholder(query: QueryMessageData, type_: str, value: Any, result: str) -> None: message = create_placeholder_value(query, type_, value) assert json.dumps(message, cls=SafeDsEncoder) == result