From 46a62b9c4bef94033e10f7ccf2c1652474043760 Mon Sep 17 00:00:00 2001 From: Lukas Holecek Date: Mon, 4 Mar 2024 12:04:15 +0100 Subject: [PATCH] Upgrade to Pydantic 2 --- poetry.lock | 170 +++++++++++++++++++++++++---------- pyproject.toml | 6 +- tests/test_access_control.py | 2 +- tests/test_api_v10.py | 111 +++++++++++------------ tests/test_models.py | 2 +- waiverdb/api_v1.py | 113 +++++++++++------------ waiverdb/app.py | 4 +- waiverdb/config.py | 2 + waiverdb/models/requests.py | 115 ++++++++++++------------ waiverdb/utils.py | 23 +++++ 10 files changed, 315 insertions(+), 233 deletions(-) diff --git a/poetry.lock b/poetry.lock index 1921f1e..24cca0f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "alabaster" @@ -46,6 +46,20 @@ files = [ [package.extras] dev = ["black", "coverage", "isort", "pre-commit", "pyenchant", "pylint"] +[[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.dependencies] +typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.9\""} + [[package]] name = "attrs" version = "23.2.0" @@ -694,18 +708,18 @@ six = "*" [[package]] name = "flask-pydantic" -version = "0.11.0" +version = "0.12.0" description = "Flask extension for integration with Pydantic library" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "Flask-Pydantic-0.11.0.tar.gz", hash = "sha256:eef679b24e34ffb02e24e1d998ac023007aa2b531927fdc2f9b8114a12c19bb4"}, - {file = "Flask_Pydantic-0.11.0-py3-none-any.whl", hash = "sha256:c5a6283d1227631683ceb72a41f652036829476fbb6e7b6ccef8cae8ed51ebab"}, + {file = "Flask-Pydantic-0.12.0.tar.gz", hash = "sha256:b80f18cc7efe332b47eafbae141541e9875777132ba30d7c95f9fef9bf2a6977"}, + {file = "Flask_Pydantic-0.12.0-py3-none-any.whl", hash = "sha256:c80825d74eefa137d8d7f2396f6b376ee128329196d79eff1442bcfd816a6228"}, ] [package.dependencies] Flask = "*" -pydantic = ">=1.7" +pydantic = ">=2.0" [[package]] name = "flask-restx" @@ -1736,55 +1750,113 @@ files = [ [[package]] name = "pydantic" -version = "1.10.14" -description = "Data validation and settings management using python type hints" +version = "2.6.3" +description = "Data validation using Python type hints" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pydantic-1.10.14-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7f4fcec873f90537c382840f330b90f4715eebc2bc9925f04cb92de593eae054"}, - {file = "pydantic-1.10.14-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e3a76f571970fcd3c43ad982daf936ae39b3e90b8a2e96c04113a369869dc87"}, - {file = "pydantic-1.10.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d886bd3c3fbeaa963692ef6b643159ccb4b4cefaf7ff1617720cbead04fd1d"}, - {file = "pydantic-1.10.14-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:798a3d05ee3b71967844a1164fd5bdb8c22c6d674f26274e78b9f29d81770c4e"}, - {file = "pydantic-1.10.14-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:23d47a4b57a38e8652bcab15a658fdb13c785b9ce217cc3a729504ab4e1d6bc9"}, - {file = "pydantic-1.10.14-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f9f674b5c3bebc2eba401de64f29948ae1e646ba2735f884d1594c5f675d6f2a"}, - {file = "pydantic-1.10.14-cp310-cp310-win_amd64.whl", hash = "sha256:24a7679fab2e0eeedb5a8924fc4a694b3bcaac7d305aeeac72dd7d4e05ecbebf"}, - {file = "pydantic-1.10.14-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d578ac4bf7fdf10ce14caba6f734c178379bd35c486c6deb6f49006e1ba78a7"}, - {file = "pydantic-1.10.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa7790e94c60f809c95602a26d906eba01a0abee9cc24150e4ce2189352deb1b"}, - {file = "pydantic-1.10.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad4e10efa5474ed1a611b6d7f0d130f4aafadceb73c11d9e72823e8f508e663"}, - {file = "pydantic-1.10.14-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1245f4f61f467cb3dfeced2b119afef3db386aec3d24a22a1de08c65038b255f"}, - {file = "pydantic-1.10.14-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:21efacc678a11114c765eb52ec0db62edffa89e9a562a94cbf8fa10b5db5c046"}, - {file = "pydantic-1.10.14-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:412ab4a3f6dbd2bf18aefa9f79c7cca23744846b31f1d6555c2ee2b05a2e14ca"}, - {file = "pydantic-1.10.14-cp311-cp311-win_amd64.whl", hash = "sha256:e897c9f35281f7889873a3e6d6b69aa1447ceb024e8495a5f0d02ecd17742a7f"}, - {file = "pydantic-1.10.14-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d604be0f0b44d473e54fdcb12302495fe0467c56509a2f80483476f3ba92b33c"}, - {file = "pydantic-1.10.14-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a42c7d17706911199798d4c464b352e640cab4351efe69c2267823d619a937e5"}, - {file = "pydantic-1.10.14-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:596f12a1085e38dbda5cbb874d0973303e34227b400b6414782bf205cc14940c"}, - {file = "pydantic-1.10.14-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bfb113860e9288d0886e3b9e49d9cf4a9d48b441f52ded7d96db7819028514cc"}, - {file = "pydantic-1.10.14-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bc3ed06ab13660b565eed80887fcfbc0070f0aa0691fbb351657041d3e874efe"}, - {file = "pydantic-1.10.14-cp37-cp37m-win_amd64.whl", hash = "sha256:ad8c2bc677ae5f6dbd3cf92f2c7dc613507eafe8f71719727cbc0a7dec9a8c01"}, - {file = "pydantic-1.10.14-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c37c28449752bb1f47975d22ef2882d70513c546f8f37201e0fec3a97b816eee"}, - {file = "pydantic-1.10.14-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:49a46a0994dd551ec051986806122767cf144b9702e31d47f6d493c336462597"}, - {file = "pydantic-1.10.14-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53e3819bd20a42470d6dd0fe7fc1c121c92247bca104ce608e609b59bc7a77ee"}, - {file = "pydantic-1.10.14-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fbb503bbbbab0c588ed3cd21975a1d0d4163b87e360fec17a792f7d8c4ff29f"}, - {file = "pydantic-1.10.14-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:336709883c15c050b9c55a63d6c7ff09be883dbc17805d2b063395dd9d9d0022"}, - {file = "pydantic-1.10.14-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4ae57b4d8e3312d486e2498d42aed3ece7b51848336964e43abbf9671584e67f"}, - {file = "pydantic-1.10.14-cp38-cp38-win_amd64.whl", hash = "sha256:dba49d52500c35cfec0b28aa8b3ea5c37c9df183ffc7210b10ff2a415c125c4a"}, - {file = "pydantic-1.10.14-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c66609e138c31cba607d8e2a7b6a5dc38979a06c900815495b2d90ce6ded35b4"}, - {file = "pydantic-1.10.14-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d986e115e0b39604b9eee3507987368ff8148222da213cd38c359f6f57b3b347"}, - {file = "pydantic-1.10.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:646b2b12df4295b4c3148850c85bff29ef6d0d9621a8d091e98094871a62e5c7"}, - {file = "pydantic-1.10.14-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282613a5969c47c83a8710cc8bfd1e70c9223feb76566f74683af889faadc0ea"}, - {file = "pydantic-1.10.14-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:466669501d08ad8eb3c4fecd991c5e793c4e0bbd62299d05111d4f827cded64f"}, - {file = "pydantic-1.10.14-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:13e86a19dca96373dcf3190fcb8797d40a6f12f154a244a8d1e8e03b8f280593"}, - {file = "pydantic-1.10.14-cp39-cp39-win_amd64.whl", hash = "sha256:08b6ec0917c30861e3fe71a93be1648a2aa4f62f866142ba21670b24444d7fd8"}, - {file = "pydantic-1.10.14-py3-none-any.whl", hash = "sha256:8ee853cd12ac2ddbf0ecbac1c289f95882b2d4482258048079d13be700aa114c"}, - {file = "pydantic-1.10.14.tar.gz", hash = "sha256:46f17b832fe27de7850896f3afee50ea682220dd218f7e9c88d436788419dca6"}, + {file = "pydantic-2.6.3-py3-none-any.whl", hash = "sha256:72c6034df47f46ccdf81869fddb81aade68056003900a8724a4f160700016a2a"}, + {file = "pydantic-2.6.3.tar.gz", hash = "sha256:e07805c4c7f5c6826e33a1d4c9d47950d7eaf34868e2690f8594d2e30241f11f"}, ] [package.dependencies] -typing-extensions = ">=4.2.0" +annotated-types = ">=0.4.0" +pydantic-core = "2.16.3" +typing-extensions = ">=4.6.1" [package.extras] -dotenv = ["python-dotenv (>=0.10.4)"] -email = ["email-validator (>=1.0.3)"] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.16.3" +description = "" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.16.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4"}, + {file = "pydantic_core-2.16.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99"}, + {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979"}, + {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db"}, + {file = "pydantic_core-2.16.3-cp310-none-win32.whl", hash = "sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132"}, + {file = "pydantic_core-2.16.3-cp310-none-win_amd64.whl", hash = "sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb"}, + {file = "pydantic_core-2.16.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4"}, + {file = "pydantic_core-2.16.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f"}, + {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e"}, + {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba"}, + {file = "pydantic_core-2.16.3-cp311-none-win32.whl", hash = "sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721"}, + {file = "pydantic_core-2.16.3-cp311-none-win_amd64.whl", hash = "sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df"}, + {file = "pydantic_core-2.16.3-cp311-none-win_arm64.whl", hash = "sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9"}, + {file = "pydantic_core-2.16.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff"}, + {file = "pydantic_core-2.16.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e"}, + {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca"}, + {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf"}, + {file = "pydantic_core-2.16.3-cp312-none-win32.whl", hash = "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe"}, + {file = "pydantic_core-2.16.3-cp312-none-win_amd64.whl", hash = "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed"}, + {file = "pydantic_core-2.16.3-cp312-none-win_arm64.whl", hash = "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6"}, + {file = "pydantic_core-2.16.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01"}, + {file = "pydantic_core-2.16.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c"}, + {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8"}, + {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5"}, + {file = "pydantic_core-2.16.3-cp38-none-win32.whl", hash = "sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a"}, + {file = "pydantic_core-2.16.3-cp38-none-win_amd64.whl", hash = "sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed"}, + {file = "pydantic_core-2.16.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820"}, + {file = "pydantic_core-2.16.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8"}, + {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b"}, + {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972"}, + {file = "pydantic_core-2.16.3-cp39-none-win32.whl", hash = "sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2"}, + {file = "pydantic_core-2.16.3-cp39-none-win_amd64.whl", hash = "sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da"}, + {file = "pydantic_core-2.16.3.tar.gz", hash = "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pyflakes" @@ -2758,4 +2830,4 @@ test = ["flake8", "mock", "pytest", "pytest-cov"] [metadata] lock-version = "2.0" python-versions = ">=3.8,<3.13" -content-hash = "ecd301981bce99b83ce3a9cf926e045c621e0b75aee148ec17dab57775a303f8" +content-hash = "18920276966c390fa52f2ef5dd36b4daa2c9b7f98134264181ecffcc2dcded1a" diff --git a/pyproject.toml b/pyproject.toml index 87d405e..eb58b83 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,9 +87,9 @@ opentelemetry-instrumentation-sqlalchemy = "^0.44b0" sphinx = {version = "^7.1.1", optional = true} sphinxcontrib-httpdomain = {version = "^1.8.1", optional = true} markupsafe = {version = "==2.1.5", optional = true} -pydantic = "^1.10.14" -Flask-Pydantic = "^0.11.0" -flask-restx = "^1.1.0" +pydantic = "^2.6.3" +Flask-Pydantic = "^0.12.0" +flask-restx = "^1.3.0" [tool.poetry.extras] test = [ diff --git a/tests/test_access_control.py b/tests/test_access_control.py index a13c3fb..9022fd5 100644 --- a/tests/test_access_control.py +++ b/tests/test_access_control.py @@ -60,7 +60,7 @@ def test_ldap_host_base_not_defined(self, client, session): r = client.post('/api/v1.0/waivers/', data=json.dumps(self.data), content_type='application/json', headers=self.headers) res_data = json.loads(r.get_data(as_text=True)) - assert r.status_code == 500 + assert r.status_code == 500, r.text assert res_data['message'] == ('LDAP_HOST and LDAP_SEARCHES also need to be defined ' 'if PERMISSIONS is defined.') diff --git a/tests/test_api_v10.py b/tests/test_api_v10.py index 99c0008..063d14e 100644 --- a/tests/test_api_v10.py +++ b/tests/test_api_v10.py @@ -5,7 +5,7 @@ import pytest from requests import ConnectionError, HTTPError -from mock import patch, Mock +from mock import patch, ANY, Mock from stomp.exception import StompException from .utils import create_waiver @@ -96,7 +96,7 @@ def test_create_waiver_with_subject(mocked_user, client, session): r = client.post('/api/v1.0/waivers/', data=json.dumps(data), content_type='application/json') res_data = json.loads(r.get_data(as_text=True)) - assert r.status_code == 201 + assert r.status_code == 201, r.text assert res_data['username'] == 'foo' assert res_data['subject'] == {'type': 'koji_build', 'item': 'glibc-2.26-27.fc27'} assert res_data['subject_type'] == 'koji_build' @@ -179,21 +179,20 @@ def test_create_waiver_without_comment(mocked_user, client, session): content_type='application/json') res_data = json.loads(r.get_data(as_text=True)) assert r.status_code == 400 - bp = res_data['validation_error']['body_params'] - assert { - 'loc': ['__root__'], - 'msg': 'value is not a valid list', - 'type': 'type_error.list' - } in bp + bp = res_data['validation_error'] assert { - 'loc': ['__root__', 'comment'], - 'msg': 'field required', - 'type': 'value_error.missing' + 'msg': 'Input should be a valid list', + 'type': 'list_type', + 'loc': [ANY], + 'input': ANY, + 'url': ANY, } in bp assert { - 'loc': ['__root__', '__root__'], - 'msg': 'Argument testcase is missing', - 'type': 'value_error' + 'msg': 'Field required', + 'type': 'missing', + 'loc': [ANY, 'comment'], + 'input': ANY, + 'url': ANY, } in bp @@ -249,16 +248,13 @@ def test_create_waiver_with_no_testcase(mocked_user, client): content_type='application/json') res_data = json.loads(r.get_data(as_text=True)) assert r.status_code == 400 - bp = res_data['validation_error']['body_params'] - assert { - 'loc': ['__root__'], - 'msg': 'value is not a valid list', - 'type': 'type_error.list' - } in bp + bp = res_data['validation_error'] assert { - 'loc': ['__root__', '__root__'], - 'msg': 'Argument testcase is missing', - 'type': 'value_error' + 'msg': 'Value error, Argument testcase is missing', + 'type': 'value_error', + 'loc': ANY, + 'input': ANY, + 'url': ANY, } in bp @@ -271,33 +267,13 @@ def test_create_waiver_with_malformed_subject(mocked_user, client): content_type='application/json') res_data = json.loads(r.get_data(as_text=True)) assert r.status_code == 400 - bp = res_data['validation_error']['body_params'] - assert { - 'loc': ['__root__'], - 'msg': 'value is not a valid list', - 'type': 'type_error.list' - } in bp - assert { - 'loc': ['__root__', 'subject'], - 'msg': 'value is not a valid dict', - 'type': 'type_error.dict' - } in bp + bp = res_data['validation_error'] assert { - 'loc': ['__root__', 'product_version'], - 'msg': 'field required', 'type': 'value_error.missing' - } in bp - assert { - 'loc': ['__root__', 'comment'], - 'msg': 'field required', - 'type': 'value_error.missing' - } in bp - assert { - 'loc': ['__root__', '__root__'], - 'msg': ( - 'subject must be defined using result_id or subject ' - 'or both subject_identifier, subject_type' - ), - 'type': 'value_error' + 'msg': 'Input should be a valid dictionary or instance of TestSubject', + 'type': 'model_type', + 'loc': [ANY, 'subject'], + 'input': ANY, + 'url': ANY, } in bp @@ -738,8 +714,14 @@ def test_filtering_with_missing_filter(client, session): content_type='application/json') res_data = json.loads(r.get_data(as_text=True)) assert r.status_code == 400 - bp = res_data['validation_error']['body_params'] - assert {'loc': ['filters'], 'msg': 'field required', 'type': 'value_error.missing'} in bp + bp = res_data['validation_error'] + assert { + 'loc': ['filters'], + 'msg': 'Field required', + 'type': 'missing', + 'input': ANY, + 'url': ANY, + } in bp def test_waivers_by_subjects_and_testcases(client, session): @@ -774,9 +756,9 @@ def test_waivers_by_subjects_and_testcases(client, session): @pytest.mark.parametrize("results,expected_error_message,excepted_error_type", [ - ([{'item': {'subject.test1': 'subject1'}}], 'field required', 'value_error.missing'), - ([{'subject': 'subject1'}], 'value is not a valid dict', 'type_error.dict'), - ([{}], 'field required', 'value_error.missing') + ([{'item': {'subject.test1': 'subject1'}}], 'Field required', 'missing'), + ([{'subject': 'subject1'}], 'value is not a valid dict', 'dict_type'), + ([{}], 'Field required', 'missing') ]) def test_waivers_by_subjects_and_testcases_with_bad_results_parameter( client, session, results, expected_error_message, excepted_error_type @@ -786,16 +768,20 @@ def test_waivers_by_subjects_and_testcases_with_bad_results_parameter( content_type='application/json') res_data = json.loads(r.get_data(as_text=True)) assert r.status_code == 400 - bp = res_data['validation_error']['body_params'] + bp = res_data['validation_error'] assert { 'loc': ['results', 0, 'testcase'], - 'type': 'value_error.missing', - 'msg': 'field required' + 'type': 'missing', + 'msg': 'Field required', + 'input': ANY, + 'url': ANY, } in bp assert { 'loc': ['results', 0, 'subject'], 'type': excepted_error_type, - 'msg': expected_error_message + 'msg': expected_error_message, + 'input': ANY, + 'url': ANY, } @@ -836,7 +822,14 @@ def test_waivers_by_subjects_and_testcases_with_malformed_since(client, session) content_type='application/json') res_data = json.loads(r.get_data(as_text=True)) assert r.status_code == 400 - assert res_data['message']['since'] == "Invalid isoformat string: '123'" + bp = res_data['validation_error'] + assert { + 'msg': 'Input should be a valid string', + 'type': 'string_type', + 'loc': ['since'], + 'input': ANY, + 'url': ANY, + } in bp data = {'since': 'asdf'} r = client.post('/api/v1.0/waivers/+by-subjects-and-testcases', data=json.dumps(data), diff --git a/tests/test_models.py b/tests/test_models.py index eff26b5..f9189a8 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -21,7 +21,7 @@ 'compose', 'Fedora-Rawhide-20170508.n.0'), ]) def test_subject_dict_to_type_identifier(subject, expected_type, expected_identifier): - ts = TestSubject.parse_obj(subject) + ts = TestSubject.model_validate(subject) subject_type, subject_identifier = subject_dict_to_type_identifier(ts) assert subject_type == expected_type assert subject_identifier == expected_identifier diff --git a/waiverdb/api_v1.py b/waiverdb/api_v1.py index f1b2ef5..d951f69 100644 --- a/waiverdb/api_v1.py +++ b/waiverdb/api_v1.py @@ -133,8 +133,8 @@ def _authorization_warning(request): class WaiversResource(Resource): @jsonp - @validate(query=GetWaivers) - def get(self): + @validate() + def get(self, query: GetWaivers): """ Get waiver records. @@ -188,40 +188,38 @@ def get(self): :statuscode 400: The request was malformed and could not be processed. """ - args = GetWaivers.parse_obj(request.args) - - query = Waiver.query.order_by(Waiver.timestamp.desc()) - - if args.subject_type: - query = query.filter(Waiver.subject_type == args.subject_type) - if args.subject_identifier: - query = query.filter(Waiver.subject_identifier == args.subject_identifier) - if args.testcase: - query = query.filter(Waiver.testcase == args.testcase) - if args.scenario: - query = query.filter(Waiver.scenario == args.scenario) - if args.product_version: - query = query.filter(Waiver.product_version == args.product_version) - if args.username: - query = query.filter(Waiver.username == args.username) - if args.proxied_by: - query = query.filter(Waiver.proxied_by == args.proxied_by) - if args.since: - since_start, since_end = parse_since(args.since) + q = Waiver.query.order_by(Waiver.timestamp.desc()) + + if query.subject_type: + q = q.filter(Waiver.subject_type == query.subject_type) + if query.subject_identifier: + q = q.filter(Waiver.subject_identifier == query.subject_identifier) + if query.testcase: + q = q.filter(Waiver.testcase == query.testcase) + if query.scenario: + q = q.filter(Waiver.scenario == query.scenario) + if query.product_version: + q = q.filter(Waiver.product_version == query.product_version) + if query.username: + q = q.filter(Waiver.username == query.username) + if query.proxied_by: + q = q.filter(Waiver.proxied_by == query.proxied_by) + if query.since: + since_start, since_end = parse_since(query.since) if since_start: - query = query.filter(Waiver.timestamp >= since_start) + q = q.filter(Waiver.timestamp >= since_start) if since_end: - query = query.filter(Waiver.timestamp <= since_end) - if not args.include_obsolete: - query = _filter_out_obsolete_waivers(query) + q = q.filter(Waiver.timestamp <= since_end) + if not query.include_obsolete: + q = _filter_out_obsolete_waivers(q) - query = query.order_by(Waiver.timestamp.desc()) - return json_collection(query, args.page, args.limit) + q = q.order_by(Waiver.timestamp.desc()) + return json_collection(q, query.page, query.limit) @jsonp - @validate(body=CreateWaiverList) + @validate() @marshal_with(waiver_fields) - def post(self): + def post(self, body: CreateWaiverList): """ Create a new waiver or multiple waivers. @@ -290,14 +288,11 @@ def post(self): """ user, headers = waiverdb.auth.get_user(request) - data = request.get_json(force=True) - - data = CreateWaiverList.parse_obj(data).__root__ - if isinstance(data, list): - result = [self._create_waiver(sub_data, user) for sub_data in data] + if isinstance(body.root, list): + result = [self._create_waiver(sub_data, user) for sub_data in body.root] db.session.add_all(result) else: - result = self._create_waiver(data, user) + result = self._create_waiver(body.root, user) db.session.add(result) db.session.commit() @@ -428,9 +423,9 @@ def get(self, waiver_id: int) -> Waiver: class FilteredWaiversResource(Resource): - @validate(body=FilterWaivers) + @validate() @marshal_with(waiver_fields, envelope='data') - def post(self): + def post(self, body: FilterWaivers): """ Get waiver records, filtered by some criteria. @@ -498,11 +493,10 @@ def post(self): :statuscode 200: Returns matching waivers, if any. :statuscode 400: The request was malformed (invalid filter critera). """ - args = FilterWaivers.parse_obj(request.get_json(force=True)) query = Waiver.query.order_by(Waiver.timestamp.desc()) clauses = [] filter_: WaiverFilter - for filter_ in args.filters: + for filter_ in body.filters: inner_clauses = [] if filter_.subject_type: inner_clauses.append(Waiver.subject_type == filter_.subject_type) @@ -526,7 +520,7 @@ def post(self): inner_clauses.append(Waiver.timestamp <= since_end) clauses.append(and_(*inner_clauses)) query = query.filter(or_(*clauses)) - if not args.include_obsolete: + if not body.include_obsolete: subquery = db.session.query(func.max(Waiver.id)).group_by( Waiver.subject_type, Waiver.subject_identifier, @@ -539,8 +533,8 @@ def post(self): class GetWaiversBySubjectsAndTestcases(Resource): @jsonp - @validate(body=GetWaiversBySubjectAndTestcase) - def post(self): + @validate() + def post(self, body: GetWaiversBySubjectAndTestcase): """ **Deprecated.** Use :http:post:`/api/v1.0/waivers/+filtered` instead. @@ -586,24 +580,22 @@ def post(self): ] } """ - data = request.get_json(force=True) - args = GetWaiversBySubjectAndTestcase.parse_obj(data) query = Waiver.query.order_by(Waiver.timestamp.desc()) - if args.results: - query = Waiver.by_results(query, args.results) - if args.product_version: - query = query.filter(Waiver.product_version == args.product_version) - if args.username: - query = query.filter(Waiver.username == args.username) - if args.proxied_by: - query = query.filter(Waiver.proxied_by == args.proxied_by) - if args.since: - since_start, since_end = parse_since(args.since) + if body.results: + query = Waiver.by_results(query, body.results) + if body.product_version: + query = query.filter(Waiver.product_version == body.product_version) + if body.username: + query = query.filter(Waiver.username == body.username) + if body.proxied_by: + query = query.filter(Waiver.proxied_by == body.proxied_by) + if body.since: + since_start, since_end = parse_since(body.since) if since_start: query = query.filter(Waiver.timestamp >= since_start) if since_end: query = query.filter(Waiver.timestamp <= since_end) - if not args.include_obsolete: + if not body.include_obsolete: query = _filter_out_obsolete_waivers(query) query = query.order_by(Waiver.timestamp.desc()) @@ -676,8 +668,8 @@ def get(self): class PermissionsResource(Resource): @jsonp - @validate(query=GetPermissions) - def get(self): + @validate() + def get(self, query: GetPermissions): """ Returns the waiver permissions. @@ -724,12 +716,11 @@ def get(self): :json string testcase: If specified, only permissions for given test case is returned. :statuscode 200: Permissions are returned. """ - args = GetPermissions.parse_obj(request.args) - testcase = args.testcase + testcase = query.testcase permissions_to_ret = permissions() if testcase: permissions_to_ret = list(match_testcase_permissions(testcase, permissions_to_ret)) - if not args.html: + if not query.html: return permissions_to_ret return Response( render_template('permissions.html', permissions=permissions_to_ret), diff --git a/waiverdb/app.py b/waiverdb/app.py index a5ae2f7..d2277a5 100644 --- a/waiverdb/app.py +++ b/waiverdb/app.py @@ -11,6 +11,7 @@ from flask import Flask, current_app, send_from_directory from flask_cors import CORS from flask_migrate import Migrate +from flask_pydantic.exceptions import ValidationError from sqlalchemy import event, text from sqlalchemy.exc import ProgrammingError import requests @@ -19,7 +20,7 @@ from waiverdb.tracing import init_tracing from waiverdb.api_v1 import api_v1, oidc from waiverdb.models import db -from waiverdb.utils import auth_methods, json_error +from waiverdb.utils import auth_methods, handle_validation_error, json_error from werkzeug.exceptions import default_exceptions from waiverdb.monitor import db_hook_event_listeners @@ -103,6 +104,7 @@ def create_app(config_obj=None): app.register_error_handler(code, json_error) app.register_error_handler(requests.ConnectionError, json_error) app.register_error_handler(requests.Timeout, json_error) + app.register_error_handler(ValidationError, handle_validation_error) populate_db_config(app) if 'OIDC' in auth_methods(app): diff --git a/waiverdb/config.py b/waiverdb/config.py index 1c30ca9..4283ef9 100644 --- a/waiverdb/config.py +++ b/waiverdb/config.py @@ -21,6 +21,8 @@ class Config(object): # closely match the requested endpoint. RESTX_ERROR_404_HELP = False + FLASK_PYDANTIC_VALIDATION_ERROR_RAISE = True + AUTH_METHOD = 'OIDC' # Specify OIDC, Kerberos or SSL for authentication OIDC_USERNAME_FIELD = 'preferred_username' # Set this to True or False to enable publishing to a message bus diff --git a/waiverdb/models/requests.py b/waiverdb/models/requests.py index 36813ee..5fc578a 100644 --- a/waiverdb/models/requests.py +++ b/waiverdb/models/requests.py @@ -1,8 +1,8 @@ # SPDX-License-Identifier: LGPL-2.0-or-later -from typing import List, Optional, Tuple, Union +from typing import Annotated, List, Optional, Tuple, Union from datetime import datetime -from pydantic import BaseModel, Field, root_validator, conlist, constr +from pydantic import BaseModel, Field, StringConstraints, RootModel, conlist, model_validator from werkzeug.exceptions import BadRequest @@ -12,9 +12,9 @@ # WaiverDB < 0.11 compatibility class TestSubject(BaseModel): - type: Optional[str] - item: Optional[str] - original_spec_nvr: Optional[str] + type: Optional[str] = None + item: Optional[str] = None + original_spec_nvr: Optional[str] = None productmd_compose_id: Optional[str] = Field(alias='productmd.compose.id', default=None) __test__ = False # to tell the PyTest that this is not a test class @@ -26,101 +26,100 @@ class TestResult(BaseModel): class CreateWaiver(BaseModel): - subject_type: Optional[str] - subject_identifier: Optional[str] - testcase: Optional[str] - subject: Optional[TestSubject] - result_id: Optional[int] + subject_type: Optional[str] = None + subject_identifier: Optional[str] = None + testcase: Optional[str] = None + subject: Optional[TestSubject] = None + result_id: Optional[int] = None waived: bool = True - product_version: constr(min_length=1) - comment: constr(min_length=1) + product_version: Annotated[str, StringConstraints(min_length=1)] + comment: Annotated[str, StringConstraints(min_length=1)] username: Optional[str] = None scenario: Optional[str] = None - @root_validator - def result_id_must_not_conflict(cls, values): - if values.get("result_id") is None: - if values.get("testcase") is None: + @model_validator(mode='after') + def result_id_must_not_conflict(self): + if self.result_id is None: + if self.testcase is None: raise ValueError("Argument testcase is missing") - return values - if all(values.get(x) is None for x in RESULT_ID_CONFLICTS_WITH): - return values + return self + if all(getattr(self, x) is None for x in RESULT_ID_CONFLICTS_WITH): + return self raise ValueError( "result_id argument should not be used together with arguments: " f"{', '.join(RESULT_ID_CONFLICTS_WITH)}" ) - @root_validator - def subject_must_not_conflict(cls, values): - if values.get("subject") is None: - return values - if all(values.get(x) is None for x in SUBJECT_CONFLICTS_WITH): - return values + @model_validator(mode='after') + def subject_must_not_conflict(self): + if self.subject is None: + return self + if all(getattr(self, x) is None for x in SUBJECT_CONFLICTS_WITH): + return self raise ValueError( "subject argument should not be used together with arguments: " f"{', '.join(SUBJECT_CONFLICTS_WITH)}" ) - @root_validator - def subject_must_be_defined(cls, values): - if values.get("result_id") is not None: - return values - if values.get("subject") is not None: - return values - if all(values.get(x) is not None for x in SUBJECT_CONFLICTS_WITH): - return values + @model_validator(mode='after') + def subject_must_be_defined(self): + if self.result_id is not None: + return self + if self.subject is not None: + return self + if all(getattr(self, x) is not None for x in SUBJECT_CONFLICTS_WITH): + return self raise ValueError( "subject must be defined using result_id or subject or both " f"{', '.join(SUBJECT_CONFLICTS_WITH)}" ) -class CreateWaiverList(BaseModel): - __root__: Union[conlist(CreateWaiver, min_items=1), CreateWaiver] +CreateWaiverList = RootModel[Union[CreateWaiver, conlist(CreateWaiver, min_length=1)]] class GetWaivers(BaseModel): - subject_type: Optional[str] - subject_identifier: Optional[str] - testcase: Optional[str] - product_version: Optional[str] - username: Optional[str] + subject_type: Optional[str] = None + subject_identifier: Optional[str] = None + testcase: Optional[str] = None + product_version: Optional[str] = None + username: Optional[str] = None include_obsolete: bool = False scenario: Optional[str] = None - since: Optional[str] + since: Optional[str] = None page: int = 1 limit: int = 10 - proxied_by: Optional[str] + proxied_by: Optional[str] = None class GetPermissions(BaseModel): - testcase: Optional[str] + testcase: Optional[str] = None html: Optional[bool] = False class WaiverFilter(BaseModel): - subject_type: Optional[str] - subject_identifier: Optional[str] - testcase: Optional[str] - scenario: Optional[str] - product_version: Optional[str] - username: Optional[str] - proxied_by: Optional[str] - since: Optional[str] + subject_type: Optional[str] = None + subject_identifier: Optional[str] = None + testcase: Optional[str] = None + scenario: Optional[str] = None + product_version: Optional[str] = None + username: Optional[str] = None + proxied_by: Optional[str] = None + since: Optional[str] = None class FilterWaivers(BaseModel): - filters: conlist(WaiverFilter, min_items=1) + filters: conlist(WaiverFilter, min_length=1) include_obsolete: bool = False class GetWaiversBySubjectAndTestcase(BaseModel): - results: Optional[List[TestResult]] - testcase: Optional[str] - product_version: Optional[str] - username: Optional[str] - proxied_by: Optional[str] - since: Optional[str] + results: Optional[List[TestResult]] = None + testcase: Optional[str] = None + product_version: Optional[str] = None + username: Optional[str] = None + proxied_by: Optional[str] = None + since: Optional[str] = None include_obsolete: bool = False diff --git a/waiverdb/utils.py b/waiverdb/utils.py index e5dc1a6..3541ced 100644 --- a/waiverdb/utils.py +++ b/waiverdb/utils.py @@ -4,10 +4,15 @@ import stomp from flask import request, url_for, jsonify, current_app from flask_restx import marshal +from flask_pydantic.exceptions import ValidationError from waiverdb.fields import waiver_fields from werkzeug.exceptions import NotFound, HTTPException from contextlib import contextmanager +VALIDATION_KEYS = frozenset({ + "input", "loc", "msg", "type", "url" +}) + def json_collection(query, page=1, limit=10): """ @@ -59,6 +64,24 @@ def json_error(error): return response +def handle_validation_error(error: ValidationError): + errors = ( + error.body_params + or error.form_params + or error.path_params + or error.query_params + ) + # Keep only interesting stuff and remove objects potentially + # unserializable in JSON. + err = [ + {k: v for k, v in e.items() if k in VALIDATION_KEYS} + for e in errors + ] + response = jsonify({"validation_error": err}) + response.status_code = 400 + return response + + def jsonp(func): """Wraps Jsonified output for JSONP requests.""" @functools.wraps(func)