From 619c9d43b85b9e85d8fe1cd2e0d2f21e2af69c5d Mon Sep 17 00:00:00 2001 From: Joshua Kitenge Date: Wed, 7 Jan 2026 13:44:09 +0000 Subject: [PATCH 01/12] chore: update version of pydantic to V2 #520 --- poetry.lock | 220 ++++++++++++++++++++++++++++++++++++++----------- pyproject.toml | 2 +- 2 files changed, 174 insertions(+), 48 deletions(-) diff --git a/poetry.lock b/poetry.lock index dc9b1614..0fd32eb0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -14,6 +14,17 @@ files = [ [package.extras] dev = ["black", "coverage", "isort", "pre-commit", "pyenchant", "pylint"] +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + [[package]] name = "apispec" version = "3.3.0" @@ -1428,13 +1439,13 @@ files = [ [[package]] name = "pathspec" -version = "1.0.0" +version = "1.0.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.9" files = [ - {file = "pathspec-1.0.0-py3-none-any.whl", hash = "sha256:1373719036e64a2b9de3b8ddd9e30afb082a915619f07265ed76d9ae507800ae"}, - {file = "pathspec-1.0.0.tar.gz", hash = "sha256:9ada63a23541746b0cf7d5672a39ea77eac31dd23a80470be90df83537512131"}, + {file = "pathspec-1.0.1-py3-none-any.whl", hash = "sha256:8870061f22c58e6d83463cfce9a7dd6eca0512c772c1001fb09ac64091816721"}, + {file = "pathspec-1.0.1.tar.gz", hash = "sha256:e2769b508d0dd47b09af6ee2c75b2744a2cb1f474ae4b1494fd6a1b7a841613c"}, ] [package.extras] @@ -1579,56 +1590,157 @@ files = [ [[package]] name = "pydantic" -version = "1.10.26" -description = "Data validation and settings management using python type hints" +version = "2.12.5" +description = "Data validation using Python type hints" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "pydantic-1.10.26-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f7ae36fa0ecef8d39884120f212e16c06bb096a38f523421278e2f39c1784546"}, - {file = "pydantic-1.10.26-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d95a76cf503f0f72ed7812a91de948440b2bf564269975738a4751e4fadeb572"}, - {file = "pydantic-1.10.26-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a943ce8e00ad708ed06a1d9df5b4fd28f5635a003b82a4908ece6f24c0b18464"}, - {file = "pydantic-1.10.26-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:465ad8edb29b15c10b779b16431fe8e77c380098badf6db367b7a1d3e572cf53"}, - {file = "pydantic-1.10.26-cp310-cp310-win_amd64.whl", hash = "sha256:80e6be6272839c8a7641d26ad569ab77772809dd78f91d0068dc0fc97f071945"}, - {file = "pydantic-1.10.26-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:116233e53889bcc536f617e38c1b8337d7fa9c280f0fd7a4045947515a785637"}, - {file = "pydantic-1.10.26-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c3cfdd361addb6eb64ccd26ac356ad6514cee06a61ab26b27e16b5ed53108f77"}, - {file = "pydantic-1.10.26-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e4451951a9a93bf9a90576f3e25240b47ee49ab5236adccb8eff6ac943adf0f"}, - {file = "pydantic-1.10.26-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9858ed44c6bea5f29ffe95308db9e62060791c877766c67dd5f55d072c8612b5"}, - {file = "pydantic-1.10.26-cp311-cp311-win_amd64.whl", hash = "sha256:ac1089f723e2106ebde434377d31239e00870a7563245072968e5af5cc4d33df"}, - {file = "pydantic-1.10.26-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:468d5b9cacfcaadc76ed0a4645354ab6f263ec01a63fb6d05630ea1df6ae453f"}, - {file = "pydantic-1.10.26-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2c1b0b914be31671000ca25cf7ea17fcaaa68cfeadf6924529c5c5aa24b7ab1f"}, - {file = "pydantic-1.10.26-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15b13b9f8ba8867095769e1156e0d7fbafa1f65b898dd40fd1c02e34430973cb"}, - {file = "pydantic-1.10.26-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad7025ca324ae263d4313998e25078dcaec5f9ed0392c06dedb57e053cc8086b"}, - {file = "pydantic-1.10.26-cp312-cp312-win_amd64.whl", hash = "sha256:4482b299874dabb88a6c3759e3d85c6557c407c3b586891f7d808d8a38b66b9c"}, - {file = "pydantic-1.10.26-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1ae7913bb40a96c87e3d3f6fe4e918ef53bf181583de4e71824360a9b11aef1c"}, - {file = "pydantic-1.10.26-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8154c13f58d4de5d3a856bb6c909c7370f41fb876a5952a503af6b975265f4ba"}, - {file = "pydantic-1.10.26-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f8af0507bf6118b054a9765fb2e402f18a8b70c964f420d95b525eb711122d62"}, - {file = "pydantic-1.10.26-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dcb5a7318fb43189fde6af6f21ac7149c4bcbcfffc54bc87b5becddc46084847"}, - {file = "pydantic-1.10.26-cp313-cp313-win_amd64.whl", hash = "sha256:71cde228bc0600cf8619f0ee62db050d1880dcc477eba0e90b23011b4ee0f314"}, - {file = "pydantic-1.10.26-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6b40730cc81d53d515dc0b8bb5c9b43fadb9bed46de4a3c03bd95e8571616dba"}, - {file = "pydantic-1.10.26-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c3bbb9c0eecdf599e4db9b372fa9cc55be12e80a0d9c6d307950a39050cb0e37"}, - {file = "pydantic-1.10.26-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc2e3fe7bc4993626ef6b6fa855defafa1d6f8996aa1caef2deb83c5ac4d043a"}, - {file = "pydantic-1.10.26-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:36d9e46b588aaeb1dcd2409fa4c467fe0b331f3cc9f227b03a7a00643704e962"}, - {file = "pydantic-1.10.26-cp314-cp314-win_amd64.whl", hash = "sha256:81ce3c8616d12a7be31b4aadfd3434f78f6b44b75adbfaec2fe1ad4f7f999b8c"}, - {file = "pydantic-1.10.26-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bc5c91a3b3106caf07ac6735ec6efad8ba37b860b9eb569923386debe65039ad"}, - {file = "pydantic-1.10.26-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:dde599e0388e04778480d57f49355c9cc7916de818bf674de5d5429f2feebfb6"}, - {file = "pydantic-1.10.26-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8be08b5cfe88e58198722861c7aab737c978423c3a27300911767931e5311d0d"}, - {file = "pydantic-1.10.26-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0141f4bafe5eda539d98c9755128a9ea933654c6ca4306b5059fc87a01a38573"}, - {file = "pydantic-1.10.26-cp38-cp38-win_amd64.whl", hash = "sha256:eb664305ffca8a9766a8629303bb596607d77eae35bb5f32ff9245984881b638"}, - {file = "pydantic-1.10.26-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:502b9d30d18a2dfaf81b7302f6ba0e5853474b1c96212449eb4db912cb604b7d"}, - {file = "pydantic-1.10.26-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0d8f6087bf697dec3bf7ffcd7fe8362674f16519f3151789f33cbe8f1d19fc15"}, - {file = "pydantic-1.10.26-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dd40a99c358419910c85e6f5d22f9c56684c25b5e7abc40879b3b4a52f34ae90"}, - {file = "pydantic-1.10.26-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ce3293b86ca9f4125df02ff0a70be91bc7946522467cbd98e7f1493f340616ba"}, - {file = "pydantic-1.10.26-cp39-cp39-win_amd64.whl", hash = "sha256:1a4e3062b71ab1d5df339ba12c48f9ed5817c5de6cb92a961dd5c64bb32e7b96"}, - {file = "pydantic-1.10.26-py3-none-any.whl", hash = "sha256:c43ad70dc3ce7787543d563792426a16fd7895e14be4b194b5665e36459dd917"}, - {file = "pydantic-1.10.26.tar.gz", hash = "sha256:8c6aa39b494c5af092e690127c283d84f363ac36017106a9e66cb33a22ac412e"}, + {file = "pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"}, + {file = "pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49"}, ] [package.dependencies] -typing-extensions = ">=4.2.0" +annotated-types = ">=0.6.0" +pydantic-core = "2.41.5" +typing-extensions = ">=4.14.1" +typing-inspection = ">=0.4.2" [package.extras] -dotenv = ["python-dotenv (>=0.10.4)"] -email = ["email-validator (>=1.0.3)"] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146"}, + {file = "pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49"}, + {file = "pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba"}, + {file = "pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9"}, + {file = "pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6"}, + {file = "pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f"}, + {file = "pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7"}, + {file = "pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3"}, + {file = "pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9"}, + {file = "pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd"}, + {file = "pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a"}, + {file = "pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008"}, + {file = "pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf"}, + {file = "pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3"}, + {file = "pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460"}, + {file = "pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51"}, + {file = "pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e"}, +] + +[package.dependencies] +typing-extensions = ">=4.14.1" [[package]] name = "pyflakes" @@ -2326,6 +2438,20 @@ files = [ {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, ] +[[package]] +name = "typing-inspection" +version = "0.4.2" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +files = [ + {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, + {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" + [[package]] name = "urllib3" version = "2.6.2" @@ -2396,4 +2522,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.10.0,<4.0" -content-hash = "e7f670602a4002cc775bd0231962e622c850cbb0e2284de30663306763013375" +content-hash = "8dc4de56df61cfd81f4b04524f4551b62efadd4f7147aae9db95f92daa6e01ff" diff --git a/pyproject.toml b/pyproject.toml index f00afc5e..fa5831f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ py-object-pool = "^1.1" cachetools = "^4.2.1" requests = "^2.25.1" python-dateutil = "^2.8.1" -pydantic = "^1.8.2" +pydantic = "^v2.12.5" [tool.poetry.dev-dependencies] pip-tools = "5.3.1" From d77a00b9cff120796a3bf5884489597555ba6485 Mon Sep 17 00:00:00 2001 From: Joshua Kitenge Date: Wed, 7 Jan 2026 14:41:58 +0000 Subject: [PATCH 02/12] update the config to use pydantic v2 #519 --- datagateway_api/src/common/config.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/datagateway_api/src/common/config.py b/datagateway_api/src/common/config.py index 65d155b9..9255b4de 100644 --- a/datagateway_api/src/common/config.py +++ b/datagateway_api/src/common/config.py @@ -5,11 +5,11 @@ from pydantic import ( BaseModel, + field_validator, StrictBool, StrictInt, StrictStr, ValidationError, - validator, ) import yaml @@ -59,12 +59,14 @@ class DataGatewayAPI(BaseModel): icat_url: Optional[StrictStr] use_reader_for_performance: Optional[UseReaderForPerformance] - _validate_extension = validator("extension", allow_reuse=True)(validate_extension) + _validate_extension = field_validator("extension", allow_reuse=True)( + validate_extension, + ) def __getitem__(self, item): return getattr(self, item) - @validator( + @field_validator( "client_cache_size", "client_pool_init_size", "client_pool_max_size", @@ -78,7 +80,7 @@ def require_icat_config_value(cls, value): # noqa: B902, N805 are present and not None. If any of these config values are missing, an error is raised, causing the application to exit. - :param cls: :class:`DataGatewayAPI` pointer + :param self: :class:`DataGatewayAPI` pointer :param value: The value of the given config field """ if value is None: @@ -116,7 +118,9 @@ class SearchAPI(BaseModel): password: StrictStr search_scoring: SearchScoring - _validate_extension = validator("extension", allow_reuse=True)(validate_extension) + _validate_extension = field_validator("extension", allow_reuse=True)( + validate_extension, + ) def __getitem__(self, item): return getattr(self, item) @@ -161,7 +165,9 @@ class APIConfig(BaseModel): test_user_credentials: Optional[TestUserCredentials] url_prefix: StrictStr - _validate_extension = validator("url_prefix", allow_reuse=True)(validate_extension) + _validate_extension = field_validator("url_prefix", allow_reuse=True)( + validate_extension, + ) def __getitem__(self, item): return getattr(self, item) @@ -194,14 +200,14 @@ def load(cls, path=None): except (IOError, ValidationError) as error: sys.exit(f"An error occurred while trying to load the config data: {error}") - @validator("search_api") + @field_validator("search_api") def validate_api_extensions(cls, value, values): # noqa: B902, N805 """ Checks that the DataGateway API and Search API extensions are not the same. An error is raised, at which point the application exits, if the extensions are the same. - :param cls: :class:`APIConfig` pointer + :param self: :class:`APIConfig` pointer :param value: The value of the given config field :param values: The config field values loaded before the given config field """ From 5af109346f987549e7911b766e91a31d3f62748f Mon Sep 17 00:00:00 2001 From: Joshua Kitenge Date: Wed, 7 Jan 2026 16:09:52 +0000 Subject: [PATCH 03/12] update search api to pydantic v2 #519 --- datagateway_api/src/search_api/models.py | 117 ++++++++++++----------- 1 file changed, 61 insertions(+), 56 deletions(-) diff --git a/datagateway_api/src/search_api/models.py b/datagateway_api/src/search_api/models.py index 19c8e054..64c0ac20 100644 --- a/datagateway_api/src/search_api/models.py +++ b/datagateway_api/src/search_api/models.py @@ -1,15 +1,30 @@ from abc import ABC, abstractmethod from datetime import datetime import sys -from typing import ClassVar, List, Optional, Union +from typing import Annotated, ClassVar, List, Optional, Union -from pydantic import BaseModel, Field, root_validator, ValidationError, validator -from pydantic.datetime_parse import parse_datetime -from pydantic.error_wrappers import ErrorWrapper +from pydantic import ( + BaseModel, + Field, + field_validator, + model_validator, + PlainSerializer, + ValidationError, +) from datagateway_api.src.search_api.panosc_mappings import mappings +SearchAPIDatetime = Annotated[ + datetime, + PlainSerializer( + lambda dt: dt.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z", + return_type=str, + when_used="always", + ), +] + + def _is_panosc_entity_field_of_type_list(entity_field): entity_field_outer_type = entity_field.outer_type_ if ( @@ -45,37 +60,20 @@ def _get_icat_field_value(icat_field_name, icat_data): return icat_data -class SearchAPIDatetime(datetime): - """ - An alternative datetime class so datetimes can be outputted in a different format to - DataGateway API - """ - - @classmethod - def __get_validators__(cls): - # Use default pydantic behaviour as well as behaviour defined in this class - yield parse_datetime - yield cls.use_search_api_format - - @classmethod - def use_search_api_format(cls, v): - return v.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" - - class PaNOSCAttribute(ABC, BaseModel): _datetime_field_names = ["creationDate", "startDate", "endDate", "releaseDate"] @classmethod @abstractmethod def from_icat(cls, icat_data, required_related_fields): # noqa: B902, N805 - entity_fields = cls.__fields__ + entity_fields = cls.model_fields entity_data = {} for entity_field in entity_fields: # Some fields have aliases so we must use them when creating a model # instance. If a field does not have an alias then the `alias` property # holds the name of the field - entity_field_alias = cls.__fields__[entity_field].alias + entity_field_alias = cls.model_fields[entity_field].alias entity_name, icat_field_name = mappings.get_icat_mapping( cls.__name__, @@ -148,7 +146,7 @@ def from_icat(cls, icat_data, required_related_fields): # noqa: B902, N805 ] if not _is_panosc_entity_field_of_type_list( - cls.__fields__[entity_field], + cls.model_fields[entity_field], ) and isinstance(field_value, list): # If the field does not hold list of values but `field_value` # is a list, then just get its first element @@ -170,11 +168,18 @@ def from_icat(cls, icat_data, required_related_fields): # noqa: B902, N805 # entity but the relevant ICAT data needed for its creation cannot be # found in the provided ICAT response. Because of this, a # `ValidationError` is raised. - error_wrapper = ErrorWrapper( - TypeError("field required"), - loc=required_related_field, + exc = TypeError("field required") + raise ValidationError( + errors=[ + { + "type": "type_error", + "loc": (required_related_field,), + "msg": str(exc), # -> "documents must be a non-empty list" + "input": None, + }, + ], + model=cls, ) - raise ValidationError(errors=[error_wrapper], model=cls) return cls(**entity_data) @@ -223,16 +228,16 @@ class Dataset(PaNOSCAttribute): parameters: Optional[List["Parameter"]] = [] samples: Optional[List["Sample"]] = [] - @validator("pid", pre=True, always=True) - def set_pid(cls, value): # noqa: B902, N805 - return f"pid:{value}" if isinstance(value, int) else value + @field_validator("pid", mode="before") + def set_pid(cls, info): # noqa: B902, N805 + return f"pid:{info.data}" if isinstance(info.data, int) else info.data - @root_validator(pre=True) - def set_is_public(cls, values): # noqa: B902, N805 + @model_validator(mode="before") + def set_is_public(cls, info): # noqa: B902, N805 # Hardcoding this to True because anon user is used for querying so all data # returned by it is public - values["isPublic"] = True - return values + info.data["isPublic"] = True + return info.data @classmethod def from_icat(cls, icat_data, required_related_fields): @@ -263,16 +268,16 @@ class Document(PaNOSCAttribute): members: Optional[List["Member"]] = [] parameters: Optional[List["Parameter"]] = [] - @validator("pid", pre=True, always=True) - def set_pid(cls, value): # noqa: B902, N805 - return f"pid:{value}" if isinstance(value, int) else value + @field_validator("pid", mode="before") + def set_pid(cls, info): # noqa: B902, N805 + return f"pid:{info.data}" if isinstance(info.data, int) else info.data - @root_validator(pre=True) - def set_is_public(cls, values): # noqa: B902, N805 + @model_validator(mode="before") + def set_is_public(cls, info): # noqa: B902, N805 # Hardcoding this to True because anon user is used for querying so all data # returned by it is public - values["isPublic"] = True - return values + info.data["isPublic"] = True + return info.data @classmethod def from_icat(cls, icat_data, required_related_fields): @@ -309,9 +314,9 @@ class Instrument(PaNOSCAttribute): datasets: Optional[List[Dataset]] = [] - @validator("pid", pre=True, always=True) - def set_pid(cls, value): # noqa: B902, N805 - return f"pid:{value}" if isinstance(value, int) else value + @field_validator("pid", mode="before") + def set_pid(cls, info): # noqa: B902, N805 + return f"pid:{info.data}" if isinstance(info.data, int) else info.data @classmethod def from_icat(cls, icat_data, required_related_fields): @@ -412,9 +417,9 @@ class Sample(PaNOSCAttribute): datasets: Optional[List[Dataset]] = [] - @validator("pid", pre=True, always=True) - def set_pid(cls, value): # noqa: B902, N805 - return f"pid:{value}" if isinstance(value, int) else value + @field_validator("pid", mode="before") + def set_pid(cls, info): # noqa: B902, N805 + return f"pid:{info.data}" if isinstance(info.data, int) else info.data @classmethod def from_icat(cls, icat_data, required_related_fields): @@ -432,9 +437,9 @@ class Technique(PaNOSCAttribute): datasets: Optional[List[Dataset]] = [] - @validator("pid", pre=True, always=True) - def set_pid(cls, value): # noqa: B902, N805 - return f"pid:{value}" if isinstance(value, int) else value + @field_validator("pid", mode="before") + def set_pid(cls, info): # noqa: B902, N805 + return f"pid:{info.data}" if isinstance(info.data, int) else info.data @classmethod def from_icat(cls, icat_data, required_related_fields): @@ -445,8 +450,8 @@ def from_icat(cls, icat_data, required_related_fields): # creation so their references have to manually be updated to lead to the actual # models or else an exception will be raised. This can be done with the help of # the postponed annotations via the future import together with the -# `update_forward_refs` method, only after all related models are declared. -Affiliation.update_forward_refs() -Dataset.update_forward_refs() -Document.update_forward_refs() -Member.update_forward_refs() +# `model_rebuild` method, only after all related models are declared. +Affiliation.model_rebuild() +Dataset.model_rebuild() +Document.model_rebuild() +Member.model_rebuild() From b9fbab08cdcfa8745ec160ce82ba13e663d6395c Mon Sep 17 00:00:00 2001 From: Joshua Kitenge Date: Thu, 8 Jan 2026 14:55:36 +0000 Subject: [PATCH 04/12] Fix model for unit_tests #520 --- datagateway_api/src/common/config.py | 72 ++++++------------- datagateway_api/src/search_api/models.py | 91 +++++++++++++++--------- poetry.lock | 2 +- pyproject.toml | 2 +- test/unit/search_api/test_models.py | 76 ++++++++++---------- test/unit/search_api/utlis.py | 7 ++ 6 files changed, 126 insertions(+), 124 deletions(-) create mode 100644 test/unit/search_api/utlis.py diff --git a/datagateway_api/src/common/config.py b/datagateway_api/src/common/config.py index 9255b4de..d6d0fdb6 100644 --- a/datagateway_api/src/common/config.py +++ b/datagateway_api/src/common/config.py @@ -5,6 +5,7 @@ from pydantic import ( BaseModel, + ConfigDict, field_validator, StrictBool, StrictInt, @@ -51,49 +52,22 @@ class DataGatewayAPI(BaseModel): validation of the DataGatewayAPI config data using Python type annotations. """ - client_cache_size: Optional[StrictInt] - client_pool_init_size: Optional[StrictInt] - client_pool_max_size: Optional[StrictInt] + client_cache_size: StrictInt + client_pool_init_size: StrictInt + client_pool_max_size: StrictInt extension: StrictStr - icat_check_cert: Optional[StrictBool] - icat_url: Optional[StrictStr] - use_reader_for_performance: Optional[UseReaderForPerformance] + icat_check_cert: StrictBool + icat_url: StrictStr + use_reader_for_performance: Optional[UseReaderForPerformance] = None - _validate_extension = field_validator("extension", allow_reuse=True)( + _validate_extension = field_validator("extension", mode="after")( validate_extension, ) def __getitem__(self, item): return getattr(self, item) - @field_validator( - "client_cache_size", - "client_pool_init_size", - "client_pool_max_size", - "icat_check_cert", - "icat_url", - always=True, - ) - def require_icat_config_value(cls, value): # noqa: B902, N805 - """ - Validates that the required config fields for the `python_icat` - are present and not None. If any of these config values are missing, - an error is raised, causing the application to exit. - - :param self: :class:`DataGatewayAPI` pointer - :param value: The value of the given config field - """ - if value is None: - raise TypeError("Field required for `python_icat`.") - return value - - class Config: - """ - The behaviour of the BaseModel class can be controlled via this class. - """ - - # Enables assignment validation on the BaseModel fields. Useful for when the - validate_assignment = True + model_config = ConfigDict(validate_assignment=True) class SearchScoring(BaseModel): @@ -118,7 +92,7 @@ class SearchAPI(BaseModel): password: StrictStr search_scoring: SearchScoring - _validate_extension = field_validator("extension", allow_reuse=True)( + _validate_extension = field_validator("extension", mode="after")( validate_extension, ) @@ -152,20 +126,20 @@ class APIConfig(BaseModel): API startup so any missing options will be caught quickly. """ - datagateway_api: Optional[DataGatewayAPI] - debug_mode: Optional[StrictBool] - flask_reloader: Optional[StrictBool] + datagateway_api: Optional[DataGatewayAPI] = None + debug_mode: Optional[StrictBool] = None + flask_reloader: Optional[StrictBool] = None generate_swagger: StrictBool - host: Optional[StrictStr] + host: Optional[StrictStr] = None log_level: StrictStr log_location: StrictStr - port: Optional[StrictStr] - search_api: Optional[SearchAPI] - test_mechanism: Optional[StrictStr] - test_user_credentials: Optional[TestUserCredentials] + port: Optional[StrictStr] = None + search_api: Optional[SearchAPI] = None + test_mechanism: Optional[StrictStr] = None url_prefix: StrictStr + test_user_credentials: Optional[TestUserCredentials] = None - _validate_extension = field_validator("url_prefix", allow_reuse=True)( + _validate_extension = field_validator("url_prefix", mode="after")( validate_extension, ) @@ -201,7 +175,7 @@ def load(cls, path=None): sys.exit(f"An error occurred while trying to load the config data: {error}") @field_validator("search_api") - def validate_api_extensions(cls, value, values): # noqa: B902, N805 + def validate_api_extensions(cls, value, info): # noqa: B902, N805 """ Checks that the DataGateway API and Search API extensions are not the same. An error is raised, at which point the application exits, if the extensions are the @@ -212,10 +186,10 @@ def validate_api_extensions(cls, value, values): # noqa: B902, N805 :param values: The config field values loaded before the given config field """ if ( - "datagateway_api" in values - and values["datagateway_api"] is not None + "datagateway_api" in info.data + and info.data["datagateway_api"] is not None and value is not None - and values["datagateway_api"].extension == value.extension + and info.data["datagateway_api"].extension == value.extension ): raise ValueError( "extension cannot be the same as datagateway_api extension", diff --git a/datagateway_api/src/search_api/models.py b/datagateway_api/src/search_api/models.py index 64c0ac20..8d68a89f 100644 --- a/datagateway_api/src/search_api/models.py +++ b/datagateway_api/src/search_api/models.py @@ -5,12 +5,14 @@ from pydantic import ( BaseModel, + BeforeValidator, Field, field_validator, model_validator, PlainSerializer, ValidationError, ) +from pydantic_core import ErrorDetails from datagateway_api.src.search_api.panosc_mappings import mappings @@ -24,12 +26,30 @@ ), ] +# The PaNOSC fields that map to id fields in ICAT were set to be of type StrictStr but +# because they are integers in ICAT, the creation of the PaNOSC models was failing +# because they were only accepting strings. To deal with this issue, the type of such +# PaNOSC fields were changed to str instead as this casts integers to strings but still +# throws a ValidationError if None is passed to it. To keep things consistent and less +# confusing, the types of all the other fields were made non-strict. +SearchAPIId = Annotated[ + str, + BeforeValidator(lambda v: str(v) if isinstance(v, (int, float, bool)) else v), + Field(alias="id"), +] + +SearchAPIIdOptional = Annotated[ + Optional[str], + BeforeValidator(lambda v: str(v) if isinstance(v, (int, float, bool)) else v), + Field(None, alias="id"), +] + def _is_panosc_entity_field_of_type_list(entity_field): - entity_field_outer_type = entity_field.outer_type_ + entity_field_annotation = entity_field.annotation if ( - hasattr(entity_field_outer_type, "_name") - and entity_field_outer_type._name == "List" + hasattr(entity_field_annotation, "_name") + and entity_field_annotation._name == "List" ): is_list = True # pragma: py-37-code # The `_name` `outer_type_` attribute was introduced in Python 3.7 so to check @@ -73,7 +93,8 @@ def from_icat(cls, icat_data, required_related_fields): # noqa: B902, N805 # Some fields have aliases so we must use them when creating a model # instance. If a field does not have an alias then the `alias` property # holds the name of the field - entity_field_alias = cls.model_fields[entity_field].alias + field_info = cls.model_fields[entity_field] + entity_field_alias = field_info.alias or entity_field entity_name, icat_field_name = mappings.get_icat_mapping( cls.__name__, @@ -168,17 +189,17 @@ def from_icat(cls, icat_data, required_related_fields): # noqa: B902, N805 # entity but the relevant ICAT data needed for its creation cannot be # found in the provided ICAT response. Because of this, a # `ValidationError` is raised. - exc = TypeError("field required") - raise ValidationError( - errors=[ - { - "type": "type_error", - "loc": (required_related_field,), - "msg": str(exc), # -> "documents must be a non-empty list" - "input": None, - }, + + raise ValidationError.from_exception_data( + cls.__name__, + [ + ErrorDetails( + type="missing", + loc=(required_related_field,), + msg="Field required", + input=None, + ), ], - model=cls, ) return cls(**entity_data) @@ -191,7 +212,7 @@ class Affiliation(PaNOSCAttribute): _text_operator_fields: ClassVar[List[str]] = [] name: Optional[str] = None - id_: Optional[str] = Field(None, alias="id") + id_: SearchAPIIdOptional address: Optional[str] = None city: Optional[str] = None country: Optional[str] = None @@ -229,15 +250,15 @@ class Dataset(PaNOSCAttribute): samples: Optional[List["Sample"]] = [] @field_validator("pid", mode="before") - def set_pid(cls, info): # noqa: B902, N805 - return f"pid:{info.data}" if isinstance(info.data, int) else info.data + def set_pid(cls, value): # noqa: B902, N805 + return f"pid:{value}" if isinstance(value, int) else value @model_validator(mode="before") - def set_is_public(cls, info): # noqa: B902, N805 + def set_is_public(cls, value): # noqa: B902, N805 # Hardcoding this to True because anon user is used for querying so all data # returned by it is public - info.data["isPublic"] = True - return info.data + value["isPublic"] = True + return value @classmethod def from_icat(cls, icat_data, required_related_fields): @@ -269,15 +290,15 @@ class Document(PaNOSCAttribute): parameters: Optional[List["Parameter"]] = [] @field_validator("pid", mode="before") - def set_pid(cls, info): # noqa: B902, N805 - return f"pid:{info.data}" if isinstance(info.data, int) else info.data + def set_pid(cls, value): # noqa: B902, N805 + return f"pid:{value}" if isinstance(value, int) else value @model_validator(mode="before") - def set_is_public(cls, info): # noqa: B902, N805 + def set_is_public(cls, value): # noqa: B902, N805 # Hardcoding this to True because anon user is used for querying so all data # returned by it is public - info.data["isPublic"] = True - return info.data + value["isPublic"] = True + return value @classmethod def from_icat(cls, icat_data, required_related_fields): @@ -290,7 +311,7 @@ class File(PaNOSCAttribute): _related_fields_with_min_cardinality_one: ClassVar[List[str]] = ["dataset"] _text_operator_fields: ClassVar[List[str]] = ["name"] - id_: str = Field(alias="id") + id_: SearchAPIId name: str path: Optional[str] = None size: Optional[int] = None @@ -315,8 +336,8 @@ class Instrument(PaNOSCAttribute): datasets: Optional[List[Dataset]] = [] @field_validator("pid", mode="before") - def set_pid(cls, info): # noqa: B902, N805 - return f"pid:{info.data}" if isinstance(info.data, int) else info.data + def set_pid(cls, value): # noqa: B902, N805 + return f"pid:{value}" if isinstance(value, int) else value @classmethod def from_icat(cls, icat_data, required_related_fields): @@ -329,7 +350,7 @@ class Member(PaNOSCAttribute): _related_fields_with_min_cardinality_one: ClassVar[List[str]] = ["document"] _text_operator_fields: ClassVar[List[str]] = [] - id_: str = Field(alias="id") + id_: SearchAPIId role: Optional[str] = Field(None, alias="role") document: Document = None @@ -350,7 +371,7 @@ class Parameter(PaNOSCAttribute): _related_fields_with_min_cardinality_one: ClassVar[List[str]] = [] _text_operator_fields: ClassVar[List[str]] = [] - id_: str = Field(alias="id") + id_: SearchAPIId name: str value: Union[float, int, str] unit: Optional[str] = None @@ -391,7 +412,7 @@ class Person(PaNOSCAttribute): _related_fields_with_min_cardinality_one: ClassVar[List[str]] = [] _text_operator_fields: ClassVar[List[str]] = [] - id_: str = Field(alias="id") + id_: SearchAPIId full_name: str = Field(alias="fullName") orcid: Optional[str] = None researcher_id: Optional[str] = Field(None, alias="researcherId") @@ -418,8 +439,8 @@ class Sample(PaNOSCAttribute): datasets: Optional[List[Dataset]] = [] @field_validator("pid", mode="before") - def set_pid(cls, info): # noqa: B902, N805 - return f"pid:{info.data}" if isinstance(info.data, int) else info.data + def set_pid(cls, value): # noqa: B902, N805 + return f"pid:{value}" if isinstance(value, int) else value @classmethod def from_icat(cls, icat_data, required_related_fields): @@ -438,8 +459,8 @@ class Technique(PaNOSCAttribute): datasets: Optional[List[Dataset]] = [] @field_validator("pid", mode="before") - def set_pid(cls, info): # noqa: B902, N805 - return f"pid:{info.data}" if isinstance(info.data, int) else info.data + def set_pid(cls, value): # noqa: B902, N805 + return f"pid:{value}" if isinstance(value, int) else value @classmethod def from_icat(cls, icat_data, required_related_fields): diff --git a/poetry.lock b/poetry.lock index 0fd32eb0..7871c5e9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2522,4 +2522,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.10.0,<4.0" -content-hash = "8dc4de56df61cfd81f4b04524f4551b62efadd4f7147aae9db95f92daa6e01ff" +content-hash = "d5b514f312139b1d8c64ae227ab395c679627e5dc0676c0d6cd2d56bfc0d4d85" diff --git a/pyproject.toml b/pyproject.toml index fa5831f4..7c7adfe3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ py-object-pool = "^1.1" cachetools = "^4.2.1" requests = "^2.25.1" python-dateutil = "^2.8.1" -pydantic = "^v2.12.5" +pydantic = "^2.12.5" [tool.poetry.dev-dependencies] pip-tools = "5.3.1" diff --git a/test/unit/search_api/test_models.py b/test/unit/search_api/test_models.py index b880025e..ffc3abb2 100644 --- a/test/unit/search_api/test_models.py +++ b/test/unit/search_api/test_models.py @@ -3,7 +3,7 @@ from datagateway_api.src.common.date_handler import DateHandler import datagateway_api.src.search_api.models as models -from datagateway_api.src.search_api.models import SearchAPIDatetime +from test.unit.search_api.utlis import DateModel AFFILIATION_ICAT_DATA = { @@ -200,9 +200,9 @@ DATASET_PANOSC_DATA = { "pid": DATASET_ICAT_DATA["doi"], "title": DATASET_ICAT_DATA["name"], - "creationDate": SearchAPIDatetime.use_search_api_format( - DateHandler.str_to_datetime_object(DATASET_ICAT_DATA["createTime"]), - ), + "creationDate": DateModel( + date=DateHandler.str_to_datetime_object(DATASET_ICAT_DATA["createTime"]), + ).model_dump(mode="json")["date"], "isPublic": True, "size": None, "documents": [], @@ -220,15 +220,15 @@ "title": INVESTIGATION_ICAT_DATA["name"], "summary": INVESTIGATION_ICAT_DATA["summary"], "doi": INVESTIGATION_ICAT_DATA["doi"], - "startDate": SearchAPIDatetime.use_search_api_format( - DateHandler.str_to_datetime_object(INVESTIGATION_ICAT_DATA["startDate"]), - ), - "endDate": SearchAPIDatetime.use_search_api_format( - DateHandler.str_to_datetime_object(INVESTIGATION_ICAT_DATA["endDate"]), - ), - "releaseDate": SearchAPIDatetime.use_search_api_format( - DateHandler.str_to_datetime_object(INVESTIGATION_ICAT_DATA["releaseDate"]), - ), + "startDate": DateModel( + date=DateHandler.str_to_datetime_object(INVESTIGATION_ICAT_DATA["startDate"]), + ).model_dump(mode="json")["date"], + "endDate": DateModel( + date=DateHandler.str_to_datetime_object(INVESTIGATION_ICAT_DATA["endDate"]), + ).model_dump(mode="json")["date"], + "releaseDate": DateModel( + date=DateHandler.str_to_datetime_object(INVESTIGATION_ICAT_DATA["releaseDate"]), + ).model_dump(mode="json")["date"], "license": None, "keywords": [KEYWORD_ICAT_DATA["name"]], "datasets": [], @@ -296,7 +296,7 @@ class TestModels: def test_from_icat_affiliation_entity_without_data_for_related_entities(self): affiliation_entity = models.Affiliation.from_icat(AFFILIATION_ICAT_DATA, []) - assert affiliation_entity.dict(by_alias=True) == AFFILIATION_PANOSC_DATA + assert affiliation_entity.model_dump(by_alias=True) == AFFILIATION_PANOSC_DATA def test_from_icat_affiliation_entity_with_data_for_all_related_entities(self): expected_entity_data = AFFILIATION_PANOSC_DATA.copy() @@ -309,12 +309,12 @@ def test_from_icat_affiliation_entity_with_data_for_all_related_entities(self): affiliation_entity = models.Affiliation.from_icat(icat_data, ["members"]) - assert affiliation_entity.dict(by_alias=True) == expected_entity_data + assert affiliation_entity.model_dump(by_alias=True) == expected_entity_data def test_from_icat_dataset_entity_without_data_for_related_entities(self): dataset_entity = models.Dataset.from_icat(DATASET_ICAT_DATA, []) - assert dataset_entity.dict(by_alias=True) == DATASET_PANOSC_DATA + assert dataset_entity.model_dump(by_alias=True) == DATASET_PANOSC_DATA def test_from_icat_dataset_entity_with_data_for_mandatory_related_entities(self): expected_entity_data = DATASET_PANOSC_DATA.copy() @@ -338,7 +338,7 @@ def test_from_icat_dataset_entity_with_data_for_mandatory_related_entities(self) ["documents", "techniques"], ) - assert dataset_entity.dict(by_alias=True) == expected_entity_data + assert dataset_entity.model_dump(by_alias=True) == expected_entity_data def test_from_icat_dataset_entity_with_data_for_all_related_entities(self): expected_entity_data = DATASET_PANOSC_DATA.copy() @@ -383,7 +383,7 @@ def test_from_icat_dataset_entity_with_data_for_all_related_entities(self): ["documents", "techniques", "instrument", "files", "parameters", "samples"], ) - assert dataset_entity.dict(by_alias=True) == expected_entity_data + assert dataset_entity.model_dump(by_alias=True) == expected_entity_data def test_from_icat_document_entity_without_data_for_related_entities(self): icat_data = INVESTIGATION_ICAT_DATA.copy() @@ -392,7 +392,7 @@ def test_from_icat_document_entity_without_data_for_related_entities(self): document_entity = models.Document.from_icat(icat_data, []) - assert document_entity.dict(by_alias=True) == DOCUMENT_PANOSC_DATA + assert document_entity.model_dump(by_alias=True) == DOCUMENT_PANOSC_DATA def test_from_icat_document_entity_with_data_for_mandatory_related_entities(self): expected_entity_data = DOCUMENT_PANOSC_DATA.copy() @@ -405,7 +405,7 @@ def test_from_icat_document_entity_with_data_for_mandatory_related_entities(self document_entity = models.Document.from_icat(icat_data, ["datasets"]) - assert document_entity.dict(by_alias=True) == expected_entity_data + assert document_entity.model_dump(by_alias=True) == expected_entity_data def test_from_icat_document_entity_with_data_for_all_related_entities(self): expected_entity_data = DOCUMENT_PANOSC_DATA.copy() @@ -424,12 +424,12 @@ def test_from_icat_document_entity_with_data_for_all_related_entities(self): document_entity = models.Document.from_icat(icat_data, ["datasets"]) - assert document_entity.dict(by_alias=True) == expected_entity_data + assert document_entity.model_dump(by_alias=True) == expected_entity_data def test_from_icat_file_entity_without_data_for_related_entities(self): file_entity = models.File.from_icat(DATAFILE_ICAT_DATA, []) - assert file_entity.dict(by_alias=True) == FILE_PANOSC_DATA + assert file_entity.model_dump(by_alias=True) == FILE_PANOSC_DATA def test_from_icat_file_entity_with_data_for_all_related_entities(self): expected_entity_data = FILE_PANOSC_DATA.copy() @@ -439,7 +439,7 @@ def test_from_icat_file_entity_with_data_for_all_related_entities(self): file_entity = models.File.from_icat(icat_data, []) - assert file_entity.dict(by_alias=True) == expected_entity_data + assert file_entity.model_dump(by_alias=True) == expected_entity_data def test_from_icat_instrument_entity_without_data_for_related_entities(self): icat_data = INSTRUMENT_ICAT_DATA.copy() @@ -447,7 +447,7 @@ def test_from_icat_instrument_entity_without_data_for_related_entities(self): instrument_entity = models.Instrument.from_icat(icat_data, []) - assert instrument_entity.dict(by_alias=True) == INSTRUMENT_PANOSC_DATA + assert instrument_entity.model_dump(by_alias=True) == INSTRUMENT_PANOSC_DATA def test_from_icat_instrument_entity_with_data_for_all_related_entities(self): expected_entity_data = INSTRUMENT_PANOSC_DATA.copy() @@ -461,12 +461,12 @@ def test_from_icat_instrument_entity_with_data_for_all_related_entities(self): instrument_entity = models.Instrument.from_icat(icat_data, ["datasets"]) - assert instrument_entity.dict(by_alias=True) == expected_entity_data + assert instrument_entity.model_dump(by_alias=True) == expected_entity_data def test_from_icat_member_entity_without_data_for_related_entities(self): member_entity = models.Member.from_icat(INVESTIGATION_USER_ICAT_DATA, []) - assert member_entity.dict(by_alias=True) == MEMBER_PANOSC_DATA + assert member_entity.model_dump(by_alias=True) == MEMBER_PANOSC_DATA def test_from_icat_member_entity_with_data_for_mandatory_related_entities(self): expected_entity_data = MEMBER_PANOSC_DATA.copy() @@ -479,7 +479,7 @@ def test_from_icat_member_entity_with_data_for_mandatory_related_entities(self): member_entity = models.Member.from_icat(icat_data, ["document"]) - assert member_entity.dict(by_alias=True) == expected_entity_data + assert member_entity.model_dump(by_alias=True) == expected_entity_data def test_from_icat_member_entity_with_data_for_all_related_entities(self): expected_entity_data = MEMBER_PANOSC_DATA.copy() @@ -501,7 +501,7 @@ def test_from_icat_member_entity_with_data_for_all_related_entities(self): ["document", "person", "affiliation"], ) - assert member_entity.dict(by_alias=True) == expected_entity_data + assert member_entity.model_dump(by_alias=True) == expected_entity_data def test_from_icat_parameter_entity_without_data_for_related_entities(self): icat_data = INVESTIGATION_PARAMETER_ICAT_DATA.copy() @@ -509,7 +509,7 @@ def test_from_icat_parameter_entity_without_data_for_related_entities(self): parameter_entity = models.Parameter.from_icat(icat_data, []) - assert parameter_entity.dict(by_alias=True) == PARAMETER_PANOSC_DATA + assert parameter_entity.model_dump(by_alias=True) == PARAMETER_PANOSC_DATA def test_from_icat_parameter_entity_with_investigation_parameter_data(self): expected_entity_data = PARAMETER_PANOSC_DATA.copy() @@ -523,7 +523,7 @@ def test_from_icat_parameter_entity_with_investigation_parameter_data(self): parameter_entity = models.Parameter.from_icat(icat_data, ["document"]) - assert parameter_entity.dict(by_alias=True) == expected_entity_data + assert parameter_entity.model_dump(by_alias=True) == expected_entity_data def test_from_icat_parameter_entity_with_dataset_parameter_data(self): expected_entity_data = PARAMETER_PANOSC_DATA.copy() @@ -536,12 +536,12 @@ def test_from_icat_parameter_entity_with_dataset_parameter_data(self): parameter_entity = models.Parameter.from_icat(icat_data, ["dataset"]) - assert parameter_entity.dict(by_alias=True) == expected_entity_data + assert parameter_entity.model_dump(by_alias=True) == expected_entity_data def test_from_icat_person_entity_without_data_for_related_entities(self): person_entity = models.Person.from_icat(USER_ICAT_DATA, []) - assert person_entity.dict(by_alias=True) == PERSON_PANOSC_DATA + assert person_entity.model_dump(by_alias=True) == PERSON_PANOSC_DATA def test_from_icat_person_entity_with_data_for_all_related_entities(self): expected_entity_data = PERSON_PANOSC_DATA.copy() @@ -555,7 +555,7 @@ def test_from_icat_person_entity_with_data_for_all_related_entities(self): person_entity = models.Person.from_icat(icat_data, ["members"]) - assert person_entity.dict(by_alias=True) == expected_entity_data + assert person_entity.model_dump(by_alias=True) == expected_entity_data def test_from_icat_sample_entity_without_data_for_related_entities(self): icat_data = SAMPLE_ICAT_DATA.copy() @@ -564,7 +564,7 @@ def test_from_icat_sample_entity_without_data_for_related_entities(self): ] sample_entity = models.Sample.from_icat(icat_data, []) - assert sample_entity.dict(by_alias=True) == SAMPLE_PANOSC_DATA + assert sample_entity.model_dump(by_alias=True) == SAMPLE_PANOSC_DATA def test_from_icat_sample_entity_with_data_for_all_related_entities(self): expected_entity_data = SAMPLE_PANOSC_DATA.copy() @@ -580,12 +580,12 @@ def test_from_icat_sample_entity_with_data_for_all_related_entities(self): sample_entity = models.Sample.from_icat(icat_data, ["datasets"]) - assert sample_entity.dict(by_alias=True) == expected_entity_data + assert sample_entity.model_dump(by_alias=True) == expected_entity_data def test_from_icat_technique_entity_without_data_for_related_entities(self): technique_entity = models.Technique.from_icat(TECHNIQUE_ICAT_DATA, []) - assert technique_entity.dict(by_alias=True) == TECHNIQUE_PANOSC_DATA + assert technique_entity.model_dump(by_alias=True) == TECHNIQUE_PANOSC_DATA def test_from_icat_technique_entity_with_data_for_all_related_entities(self): expected_entity_data = TECHNIQUE_PANOSC_DATA.copy() @@ -598,7 +598,7 @@ def test_from_icat_technique_entity_with_data_for_all_related_entities(self): technique_entity = models.Technique.from_icat(icat_data, ["datasets"]) - assert technique_entity.dict(by_alias=True) == expected_entity_data + assert technique_entity.model_dump(by_alias=True) == expected_entity_data def test_from_icat_multiple_and_nested_relations(self): expected_entity_data = DOCUMENT_PANOSC_DATA.copy() @@ -661,7 +661,7 @@ def test_from_icat_multiple_and_nested_relations(self): document_entity = models.Document.from_icat(icat_data, relations) - assert document_entity.dict(by_alias=True) == expected_entity_data + assert document_entity.model_dump(by_alias=True) == expected_entity_data @pytest.mark.parametrize( "panosc_entity_name, icat_data, required_related_fields", diff --git a/test/unit/search_api/utlis.py b/test/unit/search_api/utlis.py new file mode 100644 index 00000000..12b2d02f --- /dev/null +++ b/test/unit/search_api/utlis.py @@ -0,0 +1,7 @@ +from pydantic import BaseModel + +from datagateway_api.src.search_api import models + + +class DateModel(BaseModel): + date: models.SearchAPIDatetime From da2eb4ddeba87b21bcd25e7aea6107682e82fbae Mon Sep 17 00:00:00 2001 From: Joshua Kitenge Date: Thu, 8 Jan 2026 17:09:17 +0000 Subject: [PATCH 05/12] fix pip install test #520 --- datagateway_api/src/swagger/initialise_spec.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datagateway_api/src/swagger/initialise_spec.py b/datagateway_api/src/swagger/initialise_spec.py index 7f6d53db..348e821d 100644 --- a/datagateway_api/src/swagger/initialise_spec.py +++ b/datagateway_api/src/swagger/initialise_spec.py @@ -310,7 +310,7 @@ def initialise_search_api_spec(spec): ] for panosc_model in panosc_models: schema = panosc_model.schema(ref_template="#/components/schemas/{model}")[ - "definitions" + "$defs" ][panosc_model.__name__] schema_name = panosc_model.__name__ From f5a8fbfabebdcb58254f15b7036ba33dde054f85 Mon Sep 17 00:00:00 2001 From: Joshua Kitenge Date: Thu, 8 Jan 2026 17:27:41 +0000 Subject: [PATCH 06/12] fix integration_tests #520 --- datagateway_api/src/search_api/models.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/datagateway_api/src/search_api/models.py b/datagateway_api/src/search_api/models.py index 8d68a89f..8a7383fe 100644 --- a/datagateway_api/src/search_api/models.py +++ b/datagateway_api/src/search_api/models.py @@ -81,7 +81,12 @@ def _get_icat_field_value(icat_field_name, icat_data): class PaNOSCAttribute(ABC, BaseModel): - _datetime_field_names = ["creationDate", "startDate", "endDate", "releaseDate"] + _datetime_field_names: ClassVar[List[str]] = [ + "creationDate", + "startDate", + "endDate", + "releaseDate", + ] @classmethod @abstractmethod From 946caea8a5c98bae9944c3efe5b8ed52ef041ed5 Mon Sep 17 00:00:00 2001 From: Joshua Kitenge Date: Thu, 8 Jan 2026 18:01:37 +0000 Subject: [PATCH 07/12] create normalize_date utils func #520 --- test/unit/search_api/test_models.py | 19 +++++-------------- test/unit/search_api/utlis.py | 22 ++++++++++++++++++++-- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/test/unit/search_api/test_models.py b/test/unit/search_api/test_models.py index ffc3abb2..685e47ea 100644 --- a/test/unit/search_api/test_models.py +++ b/test/unit/search_api/test_models.py @@ -1,9 +1,8 @@ from pydantic import ValidationError import pytest -from datagateway_api.src.common.date_handler import DateHandler import datagateway_api.src.search_api.models as models -from test.unit.search_api.utlis import DateModel +from test.unit.search_api.utlis import normalise_date AFFILIATION_ICAT_DATA = { @@ -200,9 +199,7 @@ DATASET_PANOSC_DATA = { "pid": DATASET_ICAT_DATA["doi"], "title": DATASET_ICAT_DATA["name"], - "creationDate": DateModel( - date=DateHandler.str_to_datetime_object(DATASET_ICAT_DATA["createTime"]), - ).model_dump(mode="json")["date"], + "creationDate": normalise_date(DATASET_ICAT_DATA["createTime"]), "isPublic": True, "size": None, "documents": [], @@ -220,15 +217,9 @@ "title": INVESTIGATION_ICAT_DATA["name"], "summary": INVESTIGATION_ICAT_DATA["summary"], "doi": INVESTIGATION_ICAT_DATA["doi"], - "startDate": DateModel( - date=DateHandler.str_to_datetime_object(INVESTIGATION_ICAT_DATA["startDate"]), - ).model_dump(mode="json")["date"], - "endDate": DateModel( - date=DateHandler.str_to_datetime_object(INVESTIGATION_ICAT_DATA["endDate"]), - ).model_dump(mode="json")["date"], - "releaseDate": DateModel( - date=DateHandler.str_to_datetime_object(INVESTIGATION_ICAT_DATA["releaseDate"]), - ).model_dump(mode="json")["date"], + "startDate": normalise_date(INVESTIGATION_ICAT_DATA["startDate"]), + "endDate": normalise_date(INVESTIGATION_ICAT_DATA["endDate"]), + "releaseDate": normalise_date(INVESTIGATION_ICAT_DATA["releaseDate"]), "license": None, "keywords": [KEYWORD_ICAT_DATA["name"]], "datasets": [], diff --git a/test/unit/search_api/utlis.py b/test/unit/search_api/utlis.py index 12b2d02f..3ee5302d 100644 --- a/test/unit/search_api/utlis.py +++ b/test/unit/search_api/utlis.py @@ -1,7 +1,25 @@ from pydantic import BaseModel -from datagateway_api.src.search_api import models +from datagateway_api.src.common.date_handler import DateHandler +from datagateway_api.src.search_api.models import SearchAPIDatetime class DateModel(BaseModel): - date: models.SearchAPIDatetime + """ + A Pydantic model that wraps a date value for serialization and validation. + + Attributes + ---------- + date : SearchAPIDatetime + A custom datetime type used by the Search API for consistent date handling. + """ + + date: SearchAPIDatetime + + +def normalise_date(date_str: str) -> str: + """ + Convert a date string to the JSON-serializable 'date' field using DateModel. + """ + dt_obj = DateHandler.str_to_datetime_object(date_str) + return DateModel(date=dt_obj).model_dump(mode="json")["date"] From 06c6be25c142c2175b45a3ac50487c1cd842dffe Mon Sep 17 00:00:00 2001 From: Joshua Kitenge Date: Fri, 9 Jan 2026 10:28:53 +0000 Subject: [PATCH 08/12] Improve pydantic models #520 --- datagateway_api/src/common/config.py | 26 +++----- datagateway_api/src/search_api/models.py | 60 +++++++++---------- .../src/swagger/initialise_spec.py | 6 +- test/unit/search_api/utlis.py | 2 +- 4 files changed, 40 insertions(+), 54 deletions(-) diff --git a/datagateway_api/src/common/config.py b/datagateway_api/src/common/config.py index d6d0fdb6..876b35a8 100644 --- a/datagateway_api/src/common/config.py +++ b/datagateway_api/src/common/config.py @@ -1,9 +1,10 @@ import logging from pathlib import Path import sys -from typing import Optional +from typing import Annotated, Optional from pydantic import ( + AfterValidator, BaseModel, ConfigDict, field_validator, @@ -39,6 +40,9 @@ def validate_extension(extension): return extension +DataGatewayAPIExtension = Annotated[StrictStr, AfterValidator(validate_extension)] + + class UseReaderForPerformance(BaseModel): enabled: StrictBool reader_mechanism: StrictStr @@ -55,15 +59,11 @@ class DataGatewayAPI(BaseModel): client_cache_size: StrictInt client_pool_init_size: StrictInt client_pool_max_size: StrictInt - extension: StrictStr + extension: DataGatewayAPIExtension icat_check_cert: StrictBool icat_url: StrictStr use_reader_for_performance: Optional[UseReaderForPerformance] = None - _validate_extension = field_validator("extension", mode="after")( - validate_extension, - ) - def __getitem__(self, item): return getattr(self, item) @@ -84,7 +84,7 @@ class SearchAPI(BaseModel): validation of the SearchAPI config data using Python type annotations. """ - extension: StrictStr + extension: DataGatewayAPIExtension icat_check_cert: StrictBool icat_url: StrictStr mechanism: StrictStr @@ -92,10 +92,6 @@ class SearchAPI(BaseModel): password: StrictStr search_scoring: SearchScoring - _validate_extension = field_validator("extension", mode="after")( - validate_extension, - ) - def __getitem__(self, item): return getattr(self, item) @@ -136,13 +132,9 @@ class APIConfig(BaseModel): port: Optional[StrictStr] = None search_api: Optional[SearchAPI] = None test_mechanism: Optional[StrictStr] = None - url_prefix: StrictStr + url_prefix: DataGatewayAPIExtension test_user_credentials: Optional[TestUserCredentials] = None - _validate_extension = field_validator("url_prefix", mode="after")( - validate_extension, - ) - def __getitem__(self, item): return getattr(self, item) @@ -183,7 +175,7 @@ def validate_api_extensions(cls, value, info): # noqa: B902, N805 :param self: :class:`APIConfig` pointer :param value: The value of the given config field - :param values: The config field values loaded before the given config field + :param info: The config field values loaded before the given config field """ if ( "datagateway_api" in info.data diff --git a/datagateway_api/src/search_api/models.py b/datagateway_api/src/search_api/models.py index 8a7383fe..2486bf2a 100644 --- a/datagateway_api/src/search_api/models.py +++ b/datagateway_api/src/search_api/models.py @@ -7,7 +7,6 @@ BaseModel, BeforeValidator, Field, - field_validator, model_validator, PlainSerializer, ValidationError, @@ -17,30 +16,45 @@ from datagateway_api.src.search_api.panosc_mappings import mappings +def format_datetime(dt): + return dt.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" + + SearchAPIDatetime = Annotated[ datetime, - PlainSerializer( - lambda dt: dt.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z", - return_type=str, - when_used="always", - ), + PlainSerializer(format_datetime, return_type=str, when_used="always"), ] + +def pid_prefix(value): + return f"pid:{value}" if isinstance(value, int) else value + + +SearchAPIPid = Annotated[str, BeforeValidator(pid_prefix)] + # The PaNOSC fields that map to id fields in ICAT were set to be of type StrictStr but # because they are integers in ICAT, the creation of the PaNOSC models was failing # because they were only accepting strings. To deal with this issue, the type of such # PaNOSC fields were changed to str instead as this casts integers to strings but still # throws a ValidationError if None is passed to it. To keep things consistent and less # confusing, the types of all the other fields were made non-strict. + + +def normalize_id(value): + if isinstance(value, (int, float, bool)): + return str(value) + return value + + SearchAPIId = Annotated[ str, - BeforeValidator(lambda v: str(v) if isinstance(v, (int, float, bool)) else v), + BeforeValidator(normalize_id), Field(alias="id"), ] SearchAPIIdOptional = Annotated[ Optional[str], - BeforeValidator(lambda v: str(v) if isinstance(v, (int, float, bool)) else v), + BeforeValidator(normalize_id), Field(None, alias="id"), ] @@ -241,7 +255,7 @@ class Dataset(PaNOSCAttribute): ] _text_operator_fields: ClassVar[List[str]] = ["title"] - pid: str + pid: SearchAPIPid title: str is_public: bool = Field(alias="isPublic") creation_date: SearchAPIDatetime = Field(alias="creationDate") @@ -254,10 +268,6 @@ class Dataset(PaNOSCAttribute): parameters: Optional[List["Parameter"]] = [] samples: Optional[List["Sample"]] = [] - @field_validator("pid", mode="before") - def set_pid(cls, value): # noqa: B902, N805 - return f"pid:{value}" if isinstance(value, int) else value - @model_validator(mode="before") def set_is_public(cls, value): # noqa: B902, N805 # Hardcoding this to True because anon user is used for querying so all data @@ -278,7 +288,7 @@ class Document(PaNOSCAttribute): _related_fields_with_min_cardinality_one: ClassVar[List[str]] = ["datasets"] _text_operator_fields: ClassVar[List[str]] = ["title", "summary"] - pid: str + pid: SearchAPIPid is_public: bool = Field(alias="isPublic") type_: str = Field(alias="type") title: str @@ -294,10 +304,6 @@ class Document(PaNOSCAttribute): members: Optional[List["Member"]] = [] parameters: Optional[List["Parameter"]] = [] - @field_validator("pid", mode="before") - def set_pid(cls, value): # noqa: B902, N805 - return f"pid:{value}" if isinstance(value, int) else value - @model_validator(mode="before") def set_is_public(cls, value): # noqa: B902, N805 # Hardcoding this to True because anon user is used for querying so all data @@ -334,16 +340,12 @@ class Instrument(PaNOSCAttribute): _related_fields_with_min_cardinality_one: ClassVar[List[str]] = [] _text_operator_fields: ClassVar[List[str]] = ["name", "facility"] - pid: str + pid: SearchAPIPid name: str facility: str datasets: Optional[List[Dataset]] = [] - @field_validator("pid", mode="before") - def set_pid(cls, value): # noqa: B902, N805 - return f"pid:{value}" if isinstance(value, int) else value - @classmethod def from_icat(cls, icat_data, required_related_fields): return super(Instrument, cls).from_icat(icat_data, required_related_fields) @@ -438,15 +440,11 @@ class Sample(PaNOSCAttribute): _text_operator_fields: ClassVar[List[str]] = ["name", "description"] name: str - pid: str + pid: SearchAPIPid description: Optional[str] = None datasets: Optional[List[Dataset]] = [] - @field_validator("pid", mode="before") - def set_pid(cls, value): # noqa: B902, N805 - return f"pid:{value}" if isinstance(value, int) else value - @classmethod def from_icat(cls, icat_data, required_related_fields): return super(Sample, cls).from_icat(icat_data, required_related_fields) @@ -458,15 +456,11 @@ class Technique(PaNOSCAttribute): _related_fields_with_min_cardinality_one: ClassVar[List[str]] = [] _text_operator_fields: ClassVar[List[str]] = ["name"] - pid: str + pid: SearchAPIPid name: str datasets: Optional[List[Dataset]] = [] - @field_validator("pid", mode="before") - def set_pid(cls, value): # noqa: B902, N805 - return f"pid:{value}" if isinstance(value, int) else value - @classmethod def from_icat(cls, icat_data, required_related_fields): return super(Technique, cls).from_icat(icat_data, required_related_fields) diff --git a/datagateway_api/src/swagger/initialise_spec.py b/datagateway_api/src/swagger/initialise_spec.py index 348e821d..96609b55 100644 --- a/datagateway_api/src/swagger/initialise_spec.py +++ b/datagateway_api/src/swagger/initialise_spec.py @@ -309,9 +309,9 @@ def initialise_search_api_spec(spec): Technique, ] for panosc_model in panosc_models: - schema = panosc_model.schema(ref_template="#/components/schemas/{model}")[ - "$defs" - ][panosc_model.__name__] + schema = panosc_model.model_json_schema( + ref_template="#/components/schemas/{model}", + )["$defs"][panosc_model.__name__] schema_name = panosc_model.__name__ spec.components.schema(schema_name, schema) diff --git a/test/unit/search_api/utlis.py b/test/unit/search_api/utlis.py index 3ee5302d..2853204f 100644 --- a/test/unit/search_api/utlis.py +++ b/test/unit/search_api/utlis.py @@ -17,7 +17,7 @@ class DateModel(BaseModel): date: SearchAPIDatetime -def normalise_date(date_str: str) -> str: +def normalise_date(date_str: str): """ Convert a date string to the JSON-serializable 'date' field using DateModel. """ From d4459b0c8c5e1cad6d4d1404e71b16fac46b4ac4 Mon Sep 17 00:00:00 2001 From: Joshua Kitenge Date: Thu, 12 Feb 2026 10:56:50 +0000 Subject: [PATCH 09/12] fix type of optional fields in search api #520 --- datagateway_api/src/search_api/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/datagateway_api/src/search_api/models.py b/datagateway_api/src/search_api/models.py index 2486bf2a..fefa7244 100644 --- a/datagateway_api/src/search_api/models.py +++ b/datagateway_api/src/search_api/models.py @@ -327,7 +327,7 @@ class File(PaNOSCAttribute): path: Optional[str] = None size: Optional[int] = None - dataset: Dataset = None + dataset: Optional[Dataset] = None @classmethod def from_icat(cls, icat_data, required_related_fields): @@ -360,7 +360,7 @@ class Member(PaNOSCAttribute): id_: SearchAPIId role: Optional[str] = Field(None, alias="role") - document: Document = None + document: Optional[Document] = None person: Optional["Person"] = None affiliation: Optional[Affiliation] = None From ce999ef338daed12ad9b5b31d584eef7e172bf26 Mon Sep 17 00:00:00 2001 From: Joshua Kitenge Date: Wed, 25 Feb 2026 12:30:57 +0000 Subject: [PATCH 10/12] use @classmethod for validators #520 --- datagateway_api/src/common/config.py | 3 ++- datagateway_api/src/search_api/models.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/datagateway_api/src/common/config.py b/datagateway_api/src/common/config.py index 876b35a8..e937d6c0 100644 --- a/datagateway_api/src/common/config.py +++ b/datagateway_api/src/common/config.py @@ -167,13 +167,14 @@ def load(cls, path=None): sys.exit(f"An error occurred while trying to load the config data: {error}") @field_validator("search_api") + @classmethod def validate_api_extensions(cls, value, info): # noqa: B902, N805 """ Checks that the DataGateway API and Search API extensions are not the same. An error is raised, at which point the application exits, if the extensions are the same. - :param self: :class:`APIConfig` pointer + :param cls: :class:`APIConfig` pointer :param value: The value of the given config field :param info: The config field values loaded before the given config field """ diff --git a/datagateway_api/src/search_api/models.py b/datagateway_api/src/search_api/models.py index fefa7244..46dfbd89 100644 --- a/datagateway_api/src/search_api/models.py +++ b/datagateway_api/src/search_api/models.py @@ -269,6 +269,7 @@ class Dataset(PaNOSCAttribute): samples: Optional[List["Sample"]] = [] @model_validator(mode="before") + @classmethod def set_is_public(cls, value): # noqa: B902, N805 # Hardcoding this to True because anon user is used for querying so all data # returned by it is public @@ -305,6 +306,7 @@ class Document(PaNOSCAttribute): parameters: Optional[List["Parameter"]] = [] @model_validator(mode="before") + @classmethod def set_is_public(cls, value): # noqa: B902, N805 # Hardcoding this to True because anon user is used for querying so all data # returned by it is public From eed604ff376175bb1e71663ef60f6afeda331066 Mon Sep 17 00:00:00 2001 From: Joshua Kitenge Date: Wed, 25 Feb 2026 14:08:53 +0000 Subject: [PATCH 11/12] refactor isPublic and Id implementation in search models #520 --- datagateway_api/src/search_api/models.py | 45 ++++++------------------ 1 file changed, 11 insertions(+), 34 deletions(-) diff --git a/datagateway_api/src/search_api/models.py b/datagateway_api/src/search_api/models.py index 46dfbd89..531913de 100644 --- a/datagateway_api/src/search_api/models.py +++ b/datagateway_api/src/search_api/models.py @@ -1,13 +1,13 @@ from abc import ABC, abstractmethod from datetime import datetime import sys -from typing import Annotated, ClassVar, List, Optional, Union +from typing import Annotated, ClassVar, List, Literal, Optional, Union from pydantic import ( BaseModel, BeforeValidator, + ConfigDict, Field, - model_validator, PlainSerializer, ValidationError, ) @@ -40,24 +40,11 @@ def pid_prefix(value): # confusing, the types of all the other fields were made non-strict. -def normalize_id(value): - if isinstance(value, (int, float, bool)): - return str(value) - return value - - SearchAPIId = Annotated[ str, - BeforeValidator(normalize_id), Field(alias="id"), ] -SearchAPIIdOptional = Annotated[ - Optional[str], - BeforeValidator(normalize_id), - Field(None, alias="id"), -] - def _is_panosc_entity_field_of_type_list(entity_field): entity_field_annotation = entity_field.annotation @@ -102,6 +89,8 @@ class PaNOSCAttribute(ABC, BaseModel): "releaseDate", ] + model_config = ConfigDict(coerce_numbers_to_str=True) + @classmethod @abstractmethod def from_icat(cls, icat_data, required_related_fields): # noqa: B902, N805 @@ -231,7 +220,7 @@ class Affiliation(PaNOSCAttribute): _text_operator_fields: ClassVar[List[str]] = [] name: Optional[str] = None - id_: SearchAPIIdOptional + id_: Annotated[Optional[str], Field(alias="id")] address: Optional[str] = None city: Optional[str] = None country: Optional[str] = None @@ -257,7 +246,9 @@ class Dataset(PaNOSCAttribute): pid: SearchAPIPid title: str - is_public: bool = Field(alias="isPublic") + # Hardcoding this to True because anon user is used for querying so all data + # returned by it is public + is_public: Literal[True] = Field(True, alias="isPublic") creation_date: SearchAPIDatetime = Field(alias="creationDate") size: Optional[int] = None @@ -268,14 +259,6 @@ class Dataset(PaNOSCAttribute): parameters: Optional[List["Parameter"]] = [] samples: Optional[List["Sample"]] = [] - @model_validator(mode="before") - @classmethod - def set_is_public(cls, value): # noqa: B902, N805 - # Hardcoding this to True because anon user is used for querying so all data - # returned by it is public - value["isPublic"] = True - return value - @classmethod def from_icat(cls, icat_data, required_related_fields): return super(Dataset, cls).from_icat(icat_data, required_related_fields) @@ -290,7 +273,9 @@ class Document(PaNOSCAttribute): _text_operator_fields: ClassVar[List[str]] = ["title", "summary"] pid: SearchAPIPid - is_public: bool = Field(alias="isPublic") + # Hardcoding this to True because anon user is used for querying so all data + # returned by it is public + is_public: Literal[True] = Field(True, alias="isPublic") type_: str = Field(alias="type") title: str summary: Optional[str] = None @@ -305,14 +290,6 @@ class Document(PaNOSCAttribute): members: Optional[List["Member"]] = [] parameters: Optional[List["Parameter"]] = [] - @model_validator(mode="before") - @classmethod - def set_is_public(cls, value): # noqa: B902, N805 - # Hardcoding this to True because anon user is used for querying so all data - # returned by it is public - value["isPublic"] = True - return value - @classmethod def from_icat(cls, icat_data, required_related_fields): return super(Document, cls).from_icat(icat_data, required_related_fields) From 11686cd1a78f754eeae2722a4eb1d4cbfdad32e1 Mon Sep 17 00:00:00 2001 From: Joshua Kitenge Date: Mon, 2 Mar 2026 10:00:17 +0000 Subject: [PATCH 12/12] fix linting #520 --- datagateway_api/src/common/config.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/datagateway_api/src/common/config.py b/datagateway_api/src/common/config.py index 45428678..5fd4df72 100644 --- a/datagateway_api/src/common/config.py +++ b/datagateway_api/src/common/config.py @@ -6,7 +6,6 @@ from pydantic import ( AfterValidator, BaseModel, - ConfigDict, field_validator, StrictBool, StrictInt, @@ -68,7 +67,6 @@ def __getitem__(self, item): return getattr(self, item) - class SearchScoring(BaseModel): enabled: StrictBool api_url: StrictStr