From 83e7020908288312956e41654bfa46041588c9d2 Mon Sep 17 00:00:00 2001 From: Mikkel Pedersen Date: Fri, 6 Feb 2026 02:14:40 +0100 Subject: [PATCH] fix(deps): Make library compatible with Pydantic V2 --- poetry.lock | 248 +++++++++++++++++++++++--------- pyproject.toml | 4 +- queenbee/base/basemodel.py | 58 ++++---- queenbee/base/metadata.py | 32 ++--- queenbee/cli/plugin/new.py | 2 +- queenbee/cli/recipe/lint.py | 2 +- queenbee/cli/recipe/new.py | 2 +- queenbee/config/__init__.py | 11 +- queenbee/config/auth.py | 31 ++-- queenbee/config/repositories.py | 13 +- queenbee/io/artifact_source.py | 16 +-- queenbee/io/common.py | 45 +++--- queenbee/io/inputs/alias.py | 110 +++++++------- queenbee/io/inputs/dag.py | 117 +++++++-------- queenbee/io/inputs/function.py | 28 ++-- queenbee/io/inputs/job.py | 11 +- queenbee/io/inputs/step.py | 42 +++--- queenbee/io/inputs/task.py | 9 +- queenbee/io/outputs/alias.py | 38 ++--- queenbee/io/outputs/dag.py | 30 ++-- queenbee/io/outputs/function.py | 22 +-- queenbee/io/outputs/step.py | 42 +++--- queenbee/io/outputs/task.py | 8 +- queenbee/io/reference.py | 45 +++--- queenbee/job/job.py | 83 ++++++----- queenbee/job/run.py | 16 +-- queenbee/job/status.py | 12 +- queenbee/plugin/function.py | 45 +++--- queenbee/plugin/plugin.py | 28 ++-- queenbee/recipe/dag.py | 129 ++++++++--------- queenbee/recipe/dependency.py | 13 +- queenbee/recipe/recipe.py | 110 +++++++------- queenbee/recipe/task.py | 85 +++++------ queenbee/repository/index.py | 107 +++++++------- queenbee/repository/package.py | 28 ++-- tests/base/io_test.py | 12 +- tests/base/value_error.py | 35 ++++- tests/job/job_test.py | 74 +++++----- 38 files changed, 952 insertions(+), 791 deletions(-) diff --git a/poetry.lock b/poetry.lock index d39aef68..18907e45 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. [[package]] name = "alabaster" @@ -12,6 +12,18 @@ files = [ {file = "alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e"}, ] +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +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 = "attrs" version = "25.3.0" @@ -423,7 +435,7 @@ files = [ [package.dependencies] attrs = ">=22.2.0" -jsonschema-specifications = ">=2023.03.6" +jsonschema-specifications = ">=2023.3.6" referencing = ">=0.28.4" rpds-py = ">=0.7.1" @@ -547,85 +559,174 @@ testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] name = "pydantic" -version = "1.10.22" -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" groups = ["main", "dev"] files = [ - {file = "pydantic-1.10.22-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:57889565ccc1e5b7b73343329bbe6198ebc472e3ee874af2fa1865cfe7048228"}, - {file = "pydantic-1.10.22-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:90729e22426de79bc6a3526b4c45ec4400caf0d4f10d7181ba7f12c01bb3897d"}, - {file = "pydantic-1.10.22-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8684d347f351554ec94fdcb507983d3116dc4577fb8799fed63c65869a2d10"}, - {file = "pydantic-1.10.22-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c8dad498ceff2d9ef1d2e2bc6608f5b59b8e1ba2031759b22dfb8c16608e1802"}, - {file = "pydantic-1.10.22-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fac529cc654d4575cf8de191cce354b12ba705f528a0a5c654de6d01f76cd818"}, - {file = "pydantic-1.10.22-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4148232aded8dd1dd13cf910a01b32a763c34bd79a0ab4d1ee66164fcb0b7b9d"}, - {file = "pydantic-1.10.22-cp310-cp310-win_amd64.whl", hash = "sha256:ece68105d9e436db45d8650dc375c760cc85a6793ae019c08769052902dca7db"}, - {file = "pydantic-1.10.22-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8e530a8da353f791ad89e701c35787418605d35085f4bdda51b416946070e938"}, - {file = "pydantic-1.10.22-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:654322b85642e9439d7de4c83cb4084ddd513df7ff8706005dada43b34544946"}, - {file = "pydantic-1.10.22-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8bece75bd1b9fc1c32b57a32831517943b1159ba18b4ba32c0d431d76a120ae"}, - {file = "pydantic-1.10.22-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eccb58767f13c6963dcf96d02cb8723ebb98b16692030803ac075d2439c07b0f"}, - {file = "pydantic-1.10.22-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7778e6200ff8ed5f7052c1516617423d22517ad36cc7a3aedd51428168e3e5e8"}, - {file = "pydantic-1.10.22-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bffe02767d27c39af9ca7dc7cd479c00dda6346bb62ffc89e306f665108317a2"}, - {file = "pydantic-1.10.22-cp311-cp311-win_amd64.whl", hash = "sha256:23bc19c55427091b8e589bc08f635ab90005f2dc99518f1233386f46462c550a"}, - {file = "pydantic-1.10.22-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:92d0f97828a075a71d9efc65cf75db5f149b4d79a38c89648a63d2932894d8c9"}, - {file = "pydantic-1.10.22-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6af5a2811b6b95b58b829aeac5996d465a5f0c7ed84bd871d603cf8646edf6ff"}, - {file = "pydantic-1.10.22-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6cf06d8d40993e79af0ab2102ef5da77b9ddba51248e4cb27f9f3f591fbb096e"}, - {file = "pydantic-1.10.22-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:184b7865b171a6057ad97f4a17fbac81cec29bd103e996e7add3d16b0d95f609"}, - {file = "pydantic-1.10.22-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:923ad861677ab09d89be35d36111156063a7ebb44322cdb7b49266e1adaba4bb"}, - {file = "pydantic-1.10.22-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:82d9a3da1686443fb854c8d2ab9a473251f8f4cdd11b125522efb4d7c646e7bc"}, - {file = "pydantic-1.10.22-cp312-cp312-win_amd64.whl", hash = "sha256:1612604929af4c602694a7f3338b18039d402eb5ddfbf0db44f1ebfaf07f93e7"}, - {file = "pydantic-1.10.22-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b259dc89c9abcd24bf42f31951fb46c62e904ccf4316393f317abeeecda39978"}, - {file = "pydantic-1.10.22-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9238aa0964d80c0908d2f385e981add58faead4412ca80ef0fa352094c24e46d"}, - {file = "pydantic-1.10.22-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f8029f05b04080e3f1a550575a1bca747c0ea4be48e2d551473d47fd768fc1b"}, - {file = "pydantic-1.10.22-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5c06918894f119e0431a36c9393bc7cceeb34d1feeb66670ef9b9ca48c073937"}, - {file = "pydantic-1.10.22-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e205311649622ee8fc1ec9089bd2076823797f5cd2c1e3182dc0e12aab835b35"}, - {file = "pydantic-1.10.22-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:815f0a73d5688d6dd0796a7edb9eca7071bfef961a7b33f91e618822ae7345b7"}, - {file = "pydantic-1.10.22-cp313-cp313-win_amd64.whl", hash = "sha256:9dfce71d42a5cde10e78a469e3d986f656afc245ab1b97c7106036f088dd91f8"}, - {file = "pydantic-1.10.22-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3ecaf8177b06aac5d1f442db1288e3b46d9f05f34fd17fdca3ad34105328b61a"}, - {file = "pydantic-1.10.22-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb36c2de9ea74bd7f66b5481dea8032d399affd1cbfbb9bb7ce539437f1fce62"}, - {file = "pydantic-1.10.22-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6b8d14a256be3b8fff9286d76c532f1a7573fbba5f189305b22471c6679854d"}, - {file = "pydantic-1.10.22-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:1c33269e815db4324e71577174c29c7aa30d1bba51340ce6be976f6f3053a4c6"}, - {file = "pydantic-1.10.22-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:8661b3ab2735b2a9ccca2634738534a795f4a10bae3ab28ec0a10c96baa20182"}, - {file = "pydantic-1.10.22-cp37-cp37m-win_amd64.whl", hash = "sha256:22bdd5fe70d4549995981c55b970f59de5c502d5656b2abdfcd0a25be6f3763e"}, - {file = "pydantic-1.10.22-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e3f33d1358aa4bc2795208cc29ff3118aeaad0ea36f0946788cf7cadeccc166b"}, - {file = "pydantic-1.10.22-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:813f079f9cd136cac621f3f9128a4406eb8abd2ad9fdf916a0731d91c6590017"}, - {file = "pydantic-1.10.22-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab618ab8dca6eac7f0755db25f6aba3c22c40e3463f85a1c08dc93092d917704"}, - {file = "pydantic-1.10.22-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d128e1aaa38db88caca920d5822c98fc06516a09a58b6d3d60fa5ea9099b32cc"}, - {file = "pydantic-1.10.22-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:cc97bbc25def7025e55fc9016080773167cda2aad7294e06a37dda04c7d69ece"}, - {file = "pydantic-1.10.22-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dda5d7157d543b1fa565038cae6e952549d0f90071c839b3740fb77c820fab8"}, - {file = "pydantic-1.10.22-cp38-cp38-win_amd64.whl", hash = "sha256:a093fe44fe518cb445d23119511a71f756f8503139d02fcdd1173f7b76c95ffe"}, - {file = "pydantic-1.10.22-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ec54c89b2568b258bb30d7348ac4d82bec1b58b377fb56a00441e2ac66b24587"}, - {file = "pydantic-1.10.22-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d8f1d1a1532e4f3bcab4e34e8d2197a7def4b67072acd26cfa60e92d75803a48"}, - {file = "pydantic-1.10.22-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ad83ca35508c27eae1005b6b61f369f78aae6d27ead2135ec156a2599910121"}, - {file = "pydantic-1.10.22-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53cdb44b78c420f570ff16b071ea8cd5a477635c6b0efc343c8a91e3029bbf1a"}, - {file = "pydantic-1.10.22-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:16d0a5ae9d98264186ce31acdd7686ec05fd331fab9d68ed777d5cb2d1514e5e"}, - {file = "pydantic-1.10.22-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8aee040e25843f036192b1a1af62117504a209a043aa8db12e190bb86ad7e611"}, - {file = "pydantic-1.10.22-cp39-cp39-win_amd64.whl", hash = "sha256:7f691eec68dbbfca497d3c11b92a3e5987393174cbedf03ec7a4184c35c2def6"}, - {file = "pydantic-1.10.22-py3-none-any.whl", hash = "sha256:343037d608bcbd34df937ac259708bfc83664dadf88afe8516c4f282d7d471a9"}, - {file = "pydantic-1.10.22.tar.gz", hash = "sha256:ee1006cebd43a8e7158fb7190bb8f4e2da9649719bff65d0c287282ec38dec6d"}, + {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 ; python_version >= \"3.9\" and platform_system == \"Windows\""] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +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 = "pydantic-openapi-helper" -version = "0.2.11" +version = "1.0.2" description = "A small module to add additional post-processing to the OpenAPI schemas that are generated by Pydantic." optional = false python-versions = "*" groups = ["dev"] files = [ - {file = "pydantic-openapi-helper-0.2.11.tar.gz", hash = "sha256:d1dd47304ed3e9ceeff7067f85cc8462d6a6b598839a4b30161b1ed064b20e90"}, - {file = "pydantic_openapi_helper-0.2.11-py2.py3-none-any.whl", hash = "sha256:28752a7a079987c64800dc35acf09fda9e3b4dbdf487c5b19298b3a63a842904"}, + {file = "pydantic_openapi_helper-1.0.2-py2.py3-none-any.whl", hash = "sha256:a26f671cb251c88543a175bcda89519cecc2a0871c7e63e9ef5d690e2321ca3d"}, + {file = "pydantic_openapi_helper-1.0.2.tar.gz", hash = "sha256:fe4ad72b80a6bd35f69a220d41e8ee3933293069573f33b09fa73fc5cbae69a8"}, ] [package.dependencies] -pydantic = "<2.0" +pydantic = ">=2.0" [[package]] name = "pygments" @@ -1207,16 +1308,31 @@ markers = {dev = "python_full_version <= \"3.11.0a6\"", docs = "python_version = [[package]] name = "typing-extensions" -version = "4.14.0" +version = "4.15.0" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af"}, - {file = "typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4"}, + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {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" +groups = ["main", "dev"] +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.4.0" @@ -1241,4 +1357,4 @@ cli = ["click", "click-plugins"] [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "2f17da72874634589bc460a7075783e71be5c727b5944bb4bc79f7b950a9f6d3" +content-hash = "46469a0e854c629eaf8ccf0f5a6a6a3db7f59d9c5781a24cf55b5c15aed79084" diff --git a/pyproject.toml b/pyproject.toml index 4d473850..41fed65f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ queenbee = 'queenbee.cli:main' [tool.poetry.dependencies] python = "^3.10" -pydantic = "<2.0" +pydantic = "^2.0" pyyaml = ">=6.0" jsonschema = ">=4.17.3" click = "^8.1.7" @@ -33,7 +33,7 @@ click-plugins = "^1.1.1" cli = ["click", "click_plugins"] [tool.poetry.group.dev.dependencies] -pydantic-openapi-helper = "^0.2.11" +pydantic-openapi-helper = "^1.0.2" coverage = ">=6" coveralls = ">=3" pytest = ">=6.2.4" diff --git a/queenbee/base/basemodel.py b/queenbee/base/basemodel.py index 2e03be69..0794e1cc 100644 --- a/queenbee/base/basemodel.py +++ b/queenbee/base/basemodel.py @@ -1,11 +1,11 @@ """Queenbee utility functions.""" import hashlib import json -from typing import List, Dict, Any +from typing import List, Union, Dict, Any import yaml from pydantic import BaseModel as PydanticBaseModel -from pydantic import validator, Field, constr +from pydantic import Field, field_validator, ValidationError from .parser import parse_file from .variable import get_ref_variable @@ -30,27 +30,30 @@ class BaseModelNoType(PydanticBaseModel): extensions. """ - def yaml(self, exclude_unset=False, **kwargs): + def yaml(self, exclude_defaults: bool = False, exclude_none: bool = False, **kwargs): """Get a YAML string from the model Keyword Arguments: - exclude_unset {bool} -- Boolean toggle to add or remove any unset/None values - (default: {False}) + exclude_defaults {bool} -- Whether to exclude fields that are set to their + default values. (default: {False}) Returns: str -- A yaml string representing the model """ + data = self.model_dump( + mode='json', by_alias=True, exclude_defaults=exclude_defaults, exclude_none=exclude_none, **kwargs + ) return yaml.dump( - json.loads(self.json(by_alias=True, - exclude_unset=exclude_unset, **kwargs)), + data, default_flow_style=False ) - def to_dict(self, exclude_unset=False, by_alias=True, **kwargs): + def to_dict(self, exclude_defaults: bool = False, by_alias: bool = True, exclude_none: bool = False, **kwargs): """Get a dictionary from the model Keyword Arguments: - exclude_unset {bool} -- Boolean toggle to add or remove any unset/None values + exclude_defaults {bool} -- Whether to exclude fields that are set to their + default values. (default: {False}) by_alias {bool} -- Boolean toggle to use attribute alias or attribute names as key (default: {True}) @@ -58,9 +61,11 @@ def to_dict(self, exclude_unset=False, by_alias=True, **kwargs): Returns: dict -- A python dictionary representing the model """ - return json.loads(self.json(by_alias=by_alias, exclude_unset=exclude_unset, **kwargs)) + return self.model_dump( + mode='json', by_alias=by_alias, exclude_defaults=exclude_defaults, exclude_none=exclude_none, **kwargs + ) - def to_json(self, filepath, indent=None, **kwargs): + def to_json(self, filepath: str, indent: int = None, **kwargs): """Write a JSON file of the model Arguments: @@ -70,20 +75,19 @@ def to_json(self, filepath, indent=None, **kwargs): indent {int} -- indent amount (default: {None}) """ with open(filepath, 'w') as file: - file.write(self.json(by_alias=True, exclude_unset=False, - indent=indent, **kwargs)) + file.write(self.model_dump_json(by_alias=True, indent=indent, exclude_none=True, **kwargs)) - def to_yaml(self, filepath, exclude_unset=False, **kwargs): + def to_yaml(self, filepath: str, exclude_defaults: bool = False, exclude_none: bool = True, **kwargs): """Write a YAML file of the model Arguments: filepath {str} -- Path to the file to be written Keyword Arguments: - exclude_unset {bool} -- Boolean toggle to add or remove any unset/None values - (default: {False}) + exclude_defaults {bool} -- Whether to exclude fields that are set to their + default values. (default: {False}) """ - content = self.yaml(exclude_unset=exclude_unset, **kwargs) + content = self.yaml(exclude_defaults=exclude_defaults, exclude_none=exclude_none, **kwargs) with open(filepath, 'w') as out_file: out_file.write(content) @@ -99,7 +103,10 @@ def from_file(cls, filepath): cls -- An instance of the pydantic class """ data = parse_file(filepath) - return cls.parse_obj(data) + try: + return cls.model_validate(data) + except ValidationError as e: + raise ValueError(e) from e def __repr__(self): return self.yaml() @@ -112,7 +119,7 @@ def __hash__(self): str -- A hash/digest of the model """ return hashlib.sha256( - self.json(by_alias=True, exclude_unset=False).encode('utf-8') + self.model_dump_json(by_alias=True, exclude_none=True).encode('utf-8') ).hexdigest() def _referenced_values(self, var_names: List[str]) -> Dict[str, List[str]]: @@ -143,16 +150,17 @@ def _referenced_values(self, var_names: List[str]) -> Dict[str, List[str]]: class BaseModel(BaseModelNoType): """BaseModel with functionality to return the object as a yaml string.""" - type: constr(regex='^BaseModel$') = 'BaseModel' + type: str = Field('BaseModel', pattern='^BaseModel$') annotations: Dict[str, Any] = Field( - None, + default_factory=dict, description='An optional dictionary to add annotations to inputs. These ' 'annotations will be used by the client side libraries.' ) - @validator('type') - def ensure_type_match(cls, v): + @field_validator('type') + @classmethod + def ensure_type_match(cls, v: str) -> str: name = cls.__name__ if name != v: raise ValueError( @@ -161,7 +169,3 @@ def ensure_type_match(cls, v): f'Pydantic object. In this case: {name}' ) return v - - @validator('annotations', always=True) - def replace_none_value(cls, v): - return {} if not v else v diff --git a/queenbee/base/metadata.py b/queenbee/base/metadata.py index 439f6bed..498d2d8f 100644 --- a/queenbee/base/metadata.py +++ b/queenbee/base/metadata.py @@ -6,22 +6,22 @@ https://swagger.io/specification/#infoObject """ -from typing import List -from pydantic import Field, constr, AnyUrl +from typing import List, Literal, Union +from pydantic import Field, AnyUrl from .basemodel import BaseModel class Maintainer(BaseModel): """Maintainer information""" - type: constr(regex='^Maintainer$') = 'Maintainer' + type: Literal['Maintainer'] = 'Maintainer' name: str = Field( ..., description='The name of the author/maintainer person or organization.' ) - email: str = Field( + email: Union[str, None] = Field( None, description='The email address of the author/maintainer person or organization.' ) @@ -29,14 +29,14 @@ class Maintainer(BaseModel): class License(BaseModel): """License information for the Package""" - type: constr(regex='^License$') = 'License' + type: Literal['License'] = 'License' name: str = Field( ..., description='The license name used for the package.' ) - url: AnyUrl = Field( + url: Union[AnyUrl, None] = Field( None, description='A URL to the license used for the package.' ) @@ -44,7 +44,7 @@ class License(BaseModel): class MetaData(BaseModel): """Package metadata information.""" - type: constr(regex='^MetaData$') = 'MetaData' + type: Literal['MetaData'] = 'MetaData' name: str = Field( ..., @@ -56,47 +56,47 @@ class MetaData(BaseModel): description='The tag of the package' ) - app_version: str = Field( + app_version: Union[str, None] = Field( None, description='The version of the application code underlying the manifest' ) - keywords: List[str] = Field( + keywords: Union[List[str], None] = Field( None, description='A list of keywords to search the package by' ) - maintainers: List[Maintainer] = Field( + maintainers: Union[List[Maintainer], None] = Field( None, description='A list of maintainers for the package' ) - home: str = Field( + home: Union[str, None] = Field( None, description='The URL of this package\'s home page' ) - sources: List[str] = Field( + sources: Union[List[str], None] = Field( None, description='A list of URLs to source code for this project' ) - icon: str = Field( + icon: Union[str, None] = Field( None, description='A URL to an SVG or PNG image to be used as an icon' ) - deprecated: bool = Field( + deprecated: Union[bool, None] = Field( None, description='Whether this package is deprecated' ) - description: str = Field( + description: Union[str, None] = Field( None, description='A description of what this package does' ) - license: License = Field( + license: Union[License, None] = Field( None, description='The license information.' ) diff --git a/queenbee/cli/plugin/new.py b/queenbee/cli/plugin/new.py index 1099c2b3..9966c615 100644 --- a/queenbee/cli/plugin/new.py +++ b/queenbee/cli/plugin/new.py @@ -74,7 +74,7 @@ def new(name, path): input_dict = parse_file(path) input_dict['metadata']['name'] = name - plugin = Plugin.parse_obj(input_dict) + plugin = Plugin.model_validate(input_dict) # Readme readme_string = f""" diff --git a/queenbee/cli/recipe/lint.py b/queenbee/cli/recipe/lint.py index dfc6bf39..502c93d4 100644 --- a/queenbee/cli/recipe/lint.py +++ b/queenbee/cli/recipe/lint.py @@ -42,7 +42,7 @@ def lint(path, dependency_update): config=ctx.obj.config ) obj = br.to_dict() - BakedRecipe.parse_obj(obj) + BakedRecipe.model_validate(obj) except ValidationError as error: raise click.ClickException(error) except FileNotFoundError as error: diff --git a/queenbee/cli/recipe/new.py b/queenbee/cli/recipe/new.py index 385aa8af..9b4f5019 100644 --- a/queenbee/cli/recipe/new.py +++ b/queenbee/cli/recipe/new.py @@ -82,7 +82,7 @@ def new(name, path): input_dict = parse_file(path) input_dict['metadata']['name'] = name - recipe = Recipe.parse_obj(input_dict) + recipe = Recipe.model_validate(input_dict) # Readme readme_string = f""" diff --git a/queenbee/config/__init__.py b/queenbee/config/__init__.py index 03247e9f..ae89b6fe 100644 --- a/queenbee/config/__init__.py +++ b/queenbee/config/__init__.py @@ -1,6 +1,6 @@ -from typing import List, Union, Dict +from typing import List, Union, Dict, Literal from urllib.parse import urlparse -from pydantic import Field, SecretStr, constr +from pydantic import Field, SecretStr from ..base.basemodel import BaseModel from .auth import JWTAuth, HeaderAuth @@ -8,7 +8,7 @@ class Config(BaseModel): - type: constr(regex='^Config$') = 'Config' + type: Literal['Config'] = 'Config' auth: List[Union[JWTAuth, HeaderAuth]] = Field( [], @@ -111,8 +111,3 @@ def remove_repository(self, name: str): existing_index = i del self.repositories[existing_index] - - class Config: - json_encoders = { - SecretStr: lambda v: v.get_secret_value() if v else None, - } diff --git a/queenbee/config/auth.py b/queenbee/config/auth.py index fe1bc337..3c13d221 100644 --- a/queenbee/config/auth.py +++ b/queenbee/config/auth.py @@ -1,28 +1,21 @@ -import json -from enum import Enum -from urllib import request -from urllib.error import HTTPError -from typing import Union, Dict -from pydantic import Field, SecretStr, constr - +from typing import Dict, Literal, Union +from pydantic import Field, SecretStr from ..base.basemodel import BaseModel -from ..base.request import make_request, USER_AGENT_STRING class BaseAuth(BaseModel): - - type: constr(regex='^BaseAuth$') = 'BaseAuth' + type: Literal['BaseAuth'] = 'BaseAuth' domain: str = Field( ..., description='The host domain to authenticate to', - example='api.pollination.solutions' + json_schema_extra={'example': 'api.pollination.solutions'} ) - access_token: SecretStr = Field( + access_token: Union[SecretStr, None] = Field( None, - description='A JWT token retrieved from a previous login' + description='An access token for the domain.' ) @property @@ -41,12 +34,11 @@ def refresh_token(self): class HeaderAuth(BaseAuth): - - type: Enum('JWTAuth', {'type': 'jwt'}) = 'jwt' + type: Literal['HeaderAuth'] = 'HeaderAuth' header_name: str = Field( ..., - description='The HTTP header to user' + description='The HTTP header to use' ) @property @@ -56,11 +48,10 @@ def auth_header(self) -> Dict[str, str]: Returns: Dict[str, str]: a header with an API token """ + if self.access_token is None: + return {} return {self.header_name: self.access_token.get_secret_value()} class JWTAuth(BaseAuth): - - type: Enum('JWTAuth', {'type': 'jwt'}) = 'jwt' - - + type: Literal['JWTAuth'] = 'JWTAuth' diff --git a/queenbee/config/repositories.py b/queenbee/config/repositories.py index 324aee98..29ddd7f1 100644 --- a/queenbee/config/repositories.py +++ b/queenbee/config/repositories.py @@ -1,13 +1,13 @@ import os -from typing import Dict -from pydantic import Field, validator, constr +from typing import Dict, Literal +from pydantic import Field, field_validator from ..base.basemodel import BaseModel from ..base.request import make_request, urljoin, get_uri class RepositoryReference(BaseModel): - type: constr(regex='^RepositoryReference$') = 'RepositoryReference' + type: Literal['RepositoryReference'] = 'RepositoryReference' name: str = Field( ..., @@ -19,8 +19,9 @@ class RepositoryReference(BaseModel): description='The path to the repository' ) - @validator('path') - def remote_or_local(cls, v): + @field_validator('path') + @classmethod + def remote_or_local(cls, v: str) -> str: """Format local uri as needed (ie: file:///)""" return get_uri(v) @@ -41,7 +42,7 @@ def fetch(self, auth_header: Dict[str, str] = {}) -> 'RepositoryIndex': raw_bytes = res.read() - repo = RepositoryIndex.parse_raw(raw_bytes) + repo = RepositoryIndex.model_validate_json(raw_bytes) repo.metadata.name = self.name repo.metadata.source = self.path diff --git a/queenbee/io/artifact_source.py b/queenbee/io/artifact_source.py index ff0e085d..db656b96 100644 --- a/queenbee/io/artifact_source.py +++ b/queenbee/io/artifact_source.py @@ -3,8 +3,8 @@ ArtifactSource is a configuration to a source system to acquire artifacts from. """ -from typing import Dict, List -from pydantic import Field, constr +from typing import Dict, List, Literal, Any +from pydantic import Field from ..base.basemodel import BaseModel from ..io.reference import references_from_string @@ -15,14 +15,14 @@ class _ArtifactSource(BaseModel): An Artifact Source System. """ - type: constr(regex='^_ArtifactSource$') = '_ArtifactSource' + type: Literal['_ArtifactSource'] = '_ArtifactSource' @staticmethod - def _referenced_values(values: list = []) -> Dict[str, List[str]]: + def _referenced_values(values: List[Any] = None) -> Dict[str, List[str]]: """Get referenced variables if any""" ref_values = {} - if values == []: + if values is None: return ref_values for value in values: @@ -47,7 +47,7 @@ class ProjectFolder(_ArtifactSource): context of a workflow run on Pollination this folder will correspond to a Project scoped folder. """ - type: constr(regex='^ProjectFolder$') = 'ProjectFolder' + type: Literal['ProjectFolder'] = 'ProjectFolder' path: str = Field( None, @@ -72,7 +72,7 @@ class HTTP(_ArtifactSource): A web HTTP to an FTP server or an API for example. """ - type: constr(regex='^HTTP$') = 'HTTP' + type: Literal['HTTP'] = 'HTTP' url: str = Field( ..., @@ -92,7 +92,7 @@ class S3(_ArtifactSource): An S3 bucket artifact Source. """ - type: constr(regex='^S3$') = 'S3' + type: Literal['S3'] = 'S3' key: str = Field( ..., diff --git a/queenbee/io/common.py b/queenbee/io/common.py index 0108a1fa..7b0623e1 100644 --- a/queenbee/io/common.py +++ b/queenbee/io/common.py @@ -2,9 +2,9 @@ import collections from enum import Enum -from typing import Dict, List, Any +from typing import Dict, List, Any, Literal, Union -from pydantic import constr, Field, validator +from pydantic import Field, field_validator from .reference import FolderReference, FileReference, references_from_string from ..base.basemodel import BaseModel @@ -28,14 +28,14 @@ class ItemType(str, Enum): class GenericInput(BaseModel): """Base class for all input types.""" - type: constr(regex='^GenericInput$') = 'GenericInput' + type: Literal['GenericInput'] = 'GenericInput' name: str = Field( ..., description='Input name.' ) - description: str = Field( + description: Union[str, None] = Field( None, description='Optional description for input.' ) @@ -65,14 +65,14 @@ class GenericOutput(BaseModel): The baseclass uses a name to source the output. """ - type: constr(regex='^GenericOutput$') = 'GenericOutput' + type: Literal['GenericOutput'] = 'GenericOutput' name: str = Field( ..., description='Output name.' ) - description: str = Field( + description: Union[str, None] = Field( None, description='Optional description for output.' ) @@ -92,7 +92,7 @@ class PathOutput(GenericOutput): An example of using PathOutput is TaskFile and TaskFolder outputs. """ - type: constr(regex='^PathOutput$') = 'PathOutput' + type: Literal['PathOutput'] = 'PathOutput' path: str = Field( ..., @@ -126,7 +126,7 @@ class FromOutput(GenericOutput): See DAG output classes for more examples. """ - type: constr(regex='^FromOutput$') = 'FromOutput' + type: Literal['FromOutput'] = 'FromOutput' # This will be overwritten in all the subclasses. # We need this here to make sure the validator doesn't fail. @@ -136,8 +136,9 @@ class FromOutput(GenericOutput): alias='from' ) - @validator('from_') - def check_folder_artifact_has_no_refs(cls, v): + @field_validator('from_') + @classmethod + def check_folder_artifact_has_no_refs(cls, v: Any) -> Any: if isinstance(v, (FolderReference, FileReference)): refs = references_from_string(v.path) if refs != []: @@ -193,7 +194,7 @@ class IOBase(BaseModel): IOBase is the baseclass for Function, DAG and Workflow. """ - type: constr(regex='^IOBase$') = 'IOBase' + type: Literal['IOBase'] = 'IOBase' inputs: List[Any] = Field( None, @@ -205,8 +206,9 @@ class IOBase(BaseModel): description='Place-holder. Overwrite this!' ) - @validator('inputs', 'outputs', always=True) - def parameter_unique_names(cls, v): + @field_validator('inputs', 'outputs', mode='after') + @classmethod + def parameter_unique_names(cls, v: List[Any]) -> List[Any]: """Pydantic validator to check that IO item names are unique within their list Arguments: @@ -229,10 +231,10 @@ def parameter_unique_names(cls, v): platforms = [] # find all the platforms first for io in v: - if not hasattr(io, 'alias'): + if not hasattr(io, 'alias') or not io.alias: # function inputs/outputs return v - for alias in io.alias: + for alias in io['alias']: for platform in alias.platform: platforms.append(platform) @@ -240,11 +242,11 @@ def parameter_unique_names(cls, v): for platform in set(platforms): names = [] for io in v: - if not io.alias: + if not hasattr(io, 'alias') or not io.alias: # no alias use the original name names.append(io.name) continue - for alias in io.alias: + for alias in io['alias']: if platform in alias.platform: names.append(alias.name) break @@ -259,8 +261,9 @@ def parameter_unique_names(cls, v): return v - @validator('inputs', 'outputs') - def sort_list(cls, v): + @field_validator('inputs', 'outputs') + @classmethod + def sort_list(cls, v: List[Any]) -> List[Any]: """Pydantic validator to sort IO items by name Arguments: @@ -390,7 +393,7 @@ def parameter_output_by_name(self, name: str): class IOAliasHandler(BaseModel): """Input and output alias handler object.""" - type: constr(regex='^IOAliasHandler$') = 'IOAliasHandler' + type: Literal['IOAliasHandler'] = 'IOAliasHandler' language: str = Field( ..., @@ -402,7 +405,7 @@ class IOAliasHandler(BaseModel): module: str = Field( ..., description='Target module or namespace to load the alias function.', - example='honeybee_rhino.handlers' + examples=['honeybee_rhino.handlers'] ) function: str = Field( diff --git a/queenbee/io/inputs/alias.py b/queenbee/io/inputs/alias.py index bf209b7c..ac7f37ad 100644 --- a/queenbee/io/inputs/alias.py +++ b/queenbee/io/inputs/alias.py @@ -7,9 +7,9 @@ """ import os -from typing import Dict, Union, List +from typing import Dict, Union, List, Literal -from pydantic import constr, Field, validator +from pydantic import Field, field_validator, ValidationInfo from jsonschema import validate as json_schema_validator from ..common import ItemType, GenericInput, find_dup_items, IOAliasHandler @@ -23,7 +23,7 @@ class DAGGenericInputAlias(GenericInput): This class adds a handler to input to handle the process of loading the input from different graphical interfaces. """ - type: constr(regex='^DAGGenericInputAlias$') = 'DAGGenericInputAlias' + type: Literal['DAGGenericInputAlias'] = 'DAGGenericInputAlias' platform: List[str] = Field( ..., @@ -37,7 +37,7 @@ class DAGGenericInputAlias(GenericInput): description='List of process actions to process the input or output value.' ) - default: str = Field( + default: Union[str, None] = Field( None, description='Default value for generic input.' ) @@ -49,7 +49,7 @@ class DAGGenericInputAlias(GenericInput): 'be set explicitly even when a default value is provided.' ) - spec: Dict = Field( + spec: Union[Dict, None] = Field( None, description='An optional JSON Schema specification to validate the input value. ' 'You can use validate_spec method to validate a value against the spec.' @@ -65,14 +65,15 @@ def validate_spec(self, value): json_schema_validator(value, spec) return value - @validator('default') - def validate_default_refs(cls, v, values): + @field_validator('default') + @classmethod + def validate_default_refs(cls, v: str, info: ValidationInfo) -> str: """Validate referenced variables in the command""" - try: - type_ = values['type'] - except KeyError: + if 'type' not in info.data: raise ValueError(f'Input with missing type: {cls.__name__}') + type_ = info.data.get('type') + if type_ != cls.__name__ or not isinstance(v, (str, bytes)): # this is a check to ensure the default value only gets validated againt the # correct class type. See spec validation for more information @@ -89,14 +90,15 @@ def validate_default_refs(cls, v, values): return v - @validator('spec') - def validate_default_value(cls, v, values): + @field_validator('spec') + @classmethod + def validate_default_value(cls, v: Dict, info: ValidationInfo) -> Dict: """Validate default value against spec if provided.""" - try: - type_ = values['type'] - except KeyError: + if 'type' not in info.data: raise ValueError(f'Input with missing type: {cls.__name__}') + type_ = info.data.get('type') + if type_ != cls.__name__: # this is a check to ensure the default value only gets validated againt the # correct class type. The reason we need to do this is that Pydantic doesn't @@ -106,29 +108,31 @@ def validate_default_value(cls, v, values): # on a string before it gets to the integer class for an integer input. return v - try: - default = values['default'] - except KeyError: + if 'default' not in info.data: # spec is not set return v + default = info.data.get('default') if v is not None and default is not None: json_schema_validator(default, v) return v - @validator('platform', always=True) - def create_empty_platform_list(cls, v): + @field_validator('platform', mode='before') + @classmethod + def create_empty_platform_list(cls, v: List[str]) -> List[str]: return [] if v is None else v - @validator('handler', always=True) - def check_duplicate_platform_name(cls, v, values): + @field_validator('handler', mode='before') + @classmethod + def check_duplicate_platform_name(cls, v: List[IOAliasHandler], info: ValidationInfo) -> List[IOAliasHandler]: v = [] if v is None else v - languages = [h.language for h in v] + languages = [h['language'] for h in v] dup_lang = find_dup_items(languages) if dup_lang: + platform = info.data.get('platform', 'unknown') raise ValueError( f'Duplicate use of language(s) found in alias handlers for ' - f'{values["platform"]}: {dup_lang}. Each language can only be used once ' + f'{platform}: {dup_lang}. Each language can only be used once ' 'in each platform.' ) return v @@ -140,7 +144,7 @@ class DAGLinkedInputAlias(DAGGenericInputAlias): A linked input alias will be hidden in the UI and will be linked to an object in the UI using the input handler. """ - type: constr(regex='^DAGLinkedInputAlias$') = 'DAGLinkedInputAlias' + type: Literal['DAGLinkedInputAlias'] = 'DAGLinkedInputAlias' class DAGStringInputAlias(DAGGenericInputAlias): @@ -161,9 +165,9 @@ class DAGStringInputAlias(DAGGenericInputAlias): """ - type: constr(regex='^DAGStringInputAlias$') = 'DAGStringInputAlias' + type: Literal['DAGStringInputAlias'] = 'DAGStringInputAlias' - default: str = Field( + default: Union[str, None] = Field( None, description='Default value to use for an input if a value was not supplied.' ) @@ -189,9 +193,9 @@ class DAGIntegerInputAlias(DAGGenericInputAlias): for more information. """ - type: constr(regex='^DAGIntegerInputAlias$') = 'DAGIntegerInputAlias' + type: Literal['DAGIntegerInputAlias'] = 'DAGIntegerInputAlias' - default: int = Field( + default: Union[int, None] = Field( None, description='Default value to use for an input if a value was not supplied.' ) @@ -216,9 +220,9 @@ class DAGNumberInputAlias(DAGGenericInputAlias): See http://json-schema.org/understanding-json-schema/reference/numeric.html#numeric for more information. """ - type: constr(regex='^DAGNumberInputAlias$') = 'DAGNumberInputAlias' + type: Literal['DAGNumberInputAlias'] = 'DAGNumberInputAlias' - default: float = Field( + default: Union[float, None] = Field( None, description='Default value to use for an input if a value was not supplied.' ) @@ -245,9 +249,9 @@ class DAGBooleanInputAlias(DAGGenericInputAlias): See http://json-schema.org/understanding-json-schema/reference/boolean.html for more information. """ - type: constr(regex='^DAGBooleanInputAlias$') = 'DAGBooleanInputAlias' + type: Literal['DAGBooleanInputAlias'] = 'DAGBooleanInputAlias' - default: bool = Field( + default: Union[bool, None] = Field( None, description='Default value to use for an input if a value was not supplied.' ) @@ -282,15 +286,16 @@ class DAGFolderInputAlias(DAGGenericInputAlias): "maxLength": 50, } """ - type: constr(regex='^DAGFolderInputAlias$') = 'DAGFolderInputAlias' + type: Literal['DAGFolderInputAlias'] = 'DAGFolderInputAlias' - default: Union[HTTP, S3, ProjectFolder] = Field( + default: Union[HTTP, S3, ProjectFolder, None] = Field( None, description='The default source for file if the value is not provided.' ) - @validator('required', always=True) - def check_required(cls, v): + @field_validator('required', mode='before') + @classmethod + def check_required(cls, v: bool) -> bool: """Overwrite check_required fro artifacts to allow optional artifacts.""" return v @@ -339,9 +344,9 @@ class DAGFileInputAlias(DAGFolderInputAlias): } """ - type: constr(regex='^DAGFileInputAlias$') = 'DAGFileInputAlias' + type: Literal['DAGFileInputAlias'] = 'DAGFileInputAlias' - extensions: List[str] = Field( + extensions: Union[List[str], None] = Field( None, description='Optional list of extensions for file. The check for extension is ' 'case-insensitive.' @@ -389,9 +394,9 @@ class DAGPathInputAlias(DAGFolderInputAlias): } """ - type: constr(regex='^DAGPathInputAlias$') = 'DAGPathInputAlias' + type: Literal['DAGPathInputAlias'] = 'DAGPathInputAlias' - extensions: List[str] = Field( + extensions: Union[List[str], None] = Field( None, description='Optional list of extensions for path. The check for extension is ' 'case-insensitive. The extension will only be validated for file inputs.' @@ -423,9 +428,9 @@ class DAGArrayInputAlias(DAGGenericInputAlias): See http://json-schema.org/understanding-json-schema/reference/array.html for more information. """ - type: constr(regex='^DAGArrayInputAlias$') = 'DAGArrayInputAlias' + type: Literal['DAGArrayInputAlias'] = 'DAGArrayInputAlias' - default: List = Field( + default: Union[List, None] = Field( None, description='Default value to use for an input if a value was not supplied.' ) @@ -436,8 +441,9 @@ class DAGArrayInputAlias(DAGGenericInputAlias): 'the same type.' ) - @validator('default', always=True) - def replace_none_value(cls, v): + @field_validator('default', mode='before') + @classmethod + def replace_none_value(cls, v: List) -> List: return [] if not v else v def validate_spec(self, value): @@ -462,15 +468,16 @@ class DAGJSONObjectInputAlias(DAGGenericInputAlias): See http://json-schema.org/understanding-json-schema/reference/object.html for more information. """ - type: constr(regex='^DAGJSONObjectInputAlias$') = 'DAGJSONObjectInputAlias' + type: Literal['DAGJSONObjectInputAlias'] = 'DAGJSONObjectInputAlias' - default: Dict = Field( + default: Union[Dict, None] = Field( None, description='Default value to use for an input if a value was not supplied.' ) - @validator('default', always=True) - def replace_none_value(cls, v): + @field_validator('default', mode='before') + @classmethod + def replace_none_value(cls, v: Dict) -> Dict: return {} if not v else v def validate_spec(self, value): @@ -485,7 +492,8 @@ def validate_spec(self, value): DAGAliasInputs = Union[ - DAGGenericInputAlias, DAGStringInputAlias, DAGIntegerInputAlias, DAGNumberInputAlias, + DAGStringInputAlias, DAGIntegerInputAlias, DAGNumberInputAlias, DAGBooleanInputAlias, DAGFolderInputAlias, DAGFileInputAlias, DAGPathInputAlias, - DAGArrayInputAlias, DAGJSONObjectInputAlias, DAGLinkedInputAlias + DAGArrayInputAlias, DAGJSONObjectInputAlias, DAGLinkedInputAlias, + DAGGenericInputAlias ] diff --git a/queenbee/io/inputs/dag.py b/queenbee/io/inputs/dag.py index 836b6038..107237d8 100644 --- a/queenbee/io/inputs/dag.py +++ b/queenbee/io/inputs/dag.py @@ -2,8 +2,8 @@ import os from typing import Dict, Union, List - -from pydantic import constr, Field, validator +from typing import Literal +from pydantic import Field, field_validator, ValidationInfo from jsonschema import validate as json_schema_validator from ..common import ItemType, GenericInput @@ -18,15 +18,15 @@ class DAGGenericInput(GenericInput): This class adds a handler to input to handle the process of loading the input from different graphical interfaces. """ - type: constr(regex='^DAGGenericInput$') = 'DAGGenericInput' + type: Literal['DAGGenericInput'] = 'DAGGenericInput' - default: str = Field( + default: Union[str, None] = Field( None, description='Default value for generic input.' ) alias: List[DAGAliasInputs] = Field( - None, + default_factory=list, description='A list of aliases for this input in different platforms.' ) @@ -37,7 +37,7 @@ class DAGGenericInput(GenericInput): 'be set explicitly even when a default value is provided.' ) - spec: Dict = Field( + spec: Union[Dict, None] = Field( None, description='An optional JSON Schema specification to validate the input value. ' 'You can use validate_spec method to validate a value against the spec.' @@ -53,11 +53,12 @@ def validate_spec(self, value): json_schema_validator(value, spec) return value - @validator('required', always=True) - def check_required(cls, v, values): + @field_validator('required', mode='before') + @classmethod + def check_required(cls, v: bool, info: ValidationInfo) -> bool: """Ensure required is set to True when default value is not provided.""" - default = values.get('default', None) - name = values.get('name', None) + default = info.data.get('default', None) + name = info.data.get('name', None) if default is None and v is False: raise ValueError( f'{cls.__name__}.{name} -> required should be true if no default' @@ -65,14 +66,15 @@ def check_required(cls, v, values): ) return v - @validator('spec') - def validate_default_value(cls, v, values): + @field_validator('spec') + @classmethod + def validate_default_value(cls, v: Dict, info: ValidationInfo) -> Dict: """Validate default value against spec if provided.""" - try: - type_ = values['type'] - except KeyError: + if 'type' not in info.data: raise ValueError(f'Input with missing type: {cls.__name__}') + type_ = info.data.get('type') + if type_ != cls.__name__: # this is a check to ensure the default value only gets validated againt the # correct class type. The reason we need to do this is that Pydantic doesn't @@ -82,25 +84,23 @@ def validate_default_value(cls, v, values): # on a string before it gets to the integer class for an integer input. return v - try: - default = values['default'] - except KeyError: + if 'default' not in info.data: # spec is not set return v + default = info.data.get('default') if v is not None and default is not None: json_schema_validator(default, v) return v - @validator('default') - def validate_default_refs(cls, v, values): + @field_validator('default') + @classmethod + def validate_default_refs(cls, v: str, info: ValidationInfo) -> str: """Validate referenced variables in the command""" - try: - type_ = values['type'] - except KeyError: + if 'type' not in info.data: raise ValueError(f'Input with missing type: {cls.__name__}') - if type_ != cls.__name__ or not isinstance(v, (str, bytes)): + if info.data.get('type') != cls.__name__ or not isinstance(v, (str, bytes)): # this is a check to ensure the default value only gets validated againt the # correct class type. See spec validation for more information return v @@ -115,9 +115,9 @@ def validate_default_refs(cls, v, values): return v - @validator('alias', always=True) - def check_alias_required(cls, v): - v = [] if v is None else v + @field_validator('alias') + @classmethod + def check_alias_required(cls, v: List[DAGAliasInputs]) -> List[DAGAliasInputs]: for alias in v: default = alias.default name = alias.name @@ -148,9 +148,9 @@ class DAGStringInput(DAGGenericInput): """ - type: constr(regex='^DAGStringInput$') = 'DAGStringInput' + type: Literal['DAGStringInput'] = 'DAGStringInput' - default: str = Field( + default: Union[str, None] = Field( None, description='Default value to use for an input if a value was not supplied.' ) @@ -176,9 +176,9 @@ class DAGIntegerInput(DAGGenericInput): for more information. """ - type: constr(regex='^DAGIntegerInput$') = 'DAGIntegerInput' + type: Literal['DAGIntegerInput'] = 'DAGIntegerInput' - default: int = Field( + default: Union[int, None] = Field( None, description='Default value to use for an input if a value was not supplied.' ) @@ -203,9 +203,9 @@ class DAGNumberInput(DAGGenericInput): See http://json-schema.org/understanding-json-schema/reference/numeric.html#numeric for more information. """ - type: constr(regex='^DAGNumberInput$') = 'DAGNumberInput' + type: Literal['DAGNumberInput'] = 'DAGNumberInput' - default: float = Field( + default: Union[float, None] = Field( None, description='Default value to use for an input if a value was not supplied.' ) @@ -232,9 +232,9 @@ class DAGBooleanInput(DAGGenericInput): See http://json-schema.org/understanding-json-schema/reference/boolean.html for more information. """ - type: constr(regex='^DAGBooleanInput$') = 'DAGBooleanInput' + type: Literal['DAGBooleanInput'] = 'DAGBooleanInput' - default: bool = Field( + default: Union[bool, None] = Field( None, description='Default value to use for an input if a value was not supplied.' ) @@ -269,20 +269,22 @@ class DAGFolderInput(DAGGenericInput): "maxLength": 50, } """ - type: constr(regex='^DAGFolderInput$') = 'DAGFolderInput' + type: Literal['DAGFolderInput'] = 'DAGFolderInput' - default: Union[HTTP, S3, ProjectFolder] = Field( + default: Union[HTTP, S3, ProjectFolder, None] = Field( None, description='The default source for file if the value is not provided.' ) - @validator('required', always=True) - def check_required(cls, v): + @field_validator('required', mode='before') + @classmethod + def check_required(cls, v: bool) -> bool: """Overwrite check_required for artifacts to allow optional artifacts.""" - return [] if v is None else v + return v if v is not None else False - @validator('alias', always=True) - def check_alias_required(cls, v): + @field_validator('alias', mode='before') + @classmethod + def check_alias_required(cls, v: List[DAGAliasInputs]) -> List[DAGAliasInputs]: """Overwrite check_alias_required for artifacts. This will allow None input for aliases for optional artifacts. @@ -334,9 +336,9 @@ class DAGFileInput(DAGFolderInput): } """ - type: constr(regex='^DAGFileInput$') = 'DAGFileInput' + type: Literal['DAGFileInput'] = 'DAGFileInput' - extensions: List[str] = Field( + extensions: Union[List[str], None] = Field( None, description='Optional list of extensions for file. The check for extension is ' 'case-insensitive.' @@ -384,9 +386,9 @@ class DAGPathInput(DAGFolderInput): } """ - type: constr(regex='^DAGPathInput$') = 'DAGPathInput' + type: Literal['DAGPathInput'] = 'DAGPathInput' - extensions: List[str] = Field( + extensions: Union[List[str], None] = Field( None, description='Optional list of extensions for path. The check for extension is ' 'case-insensitive. The extension will only be validated for file inputs.' @@ -418,9 +420,9 @@ class DAGArrayInput(DAGGenericInput): See http://json-schema.org/understanding-json-schema/reference/array.html for more information. """ - type: constr(regex='^DAGArrayInput$') = 'DAGArrayInput' + type: Literal['DAGArrayInput'] = 'DAGArrayInput' - default: List = Field( + default: Union[List, None] = Field( None, description='Default value to use for an input if a value was not supplied.' ) @@ -431,8 +433,9 @@ class DAGArrayInput(DAGGenericInput): 'the same type.' ) - @validator('default', always=True) - def replace_none_value(cls, v): + @field_validator('default', mode='before') + @classmethod + def replace_none_value(cls, v: List) -> List: return [] if not v else v def validate_spec(self, value): @@ -457,15 +460,16 @@ class DAGJSONObjectInput(DAGGenericInput): See http://json-schema.org/understanding-json-schema/reference/object.html for more information. """ - type: constr(regex='^DAGJSONObjectInput$') = 'DAGJSONObjectInput' + type: Literal['DAGJSONObjectInput'] = 'DAGJSONObjectInput' - default: Dict = Field( + default: Union[Dict, None] = Field( None, description='Default value to use for an input if a value was not supplied.' ) - @validator('default', always=True) - def replace_none_value(cls, v): + @field_validator('default', mode='before') + @classmethod + def replace_none_value(cls, v: Dict) -> Dict: return {} if not v else v def validate_spec(self, value): @@ -480,6 +484,7 @@ def validate_spec(self, value): DAGInputs = Union[ - DAGGenericInput, DAGStringInput, DAGIntegerInput, DAGNumberInput, DAGBooleanInput, - DAGFolderInput, DAGFileInput, DAGPathInput, DAGArrayInput, DAGJSONObjectInput + DAGStringInput, DAGIntegerInput, DAGNumberInput, DAGBooleanInput, + DAGFolderInput, DAGFileInput, DAGPathInput, DAGArrayInput, DAGJSONObjectInput, + DAGGenericInput ] diff --git a/queenbee/io/inputs/function.py b/queenbee/io/inputs/function.py index b62022e3..2f05f125 100644 --- a/queenbee/io/inputs/function.py +++ b/queenbee/io/inputs/function.py @@ -5,7 +5,8 @@ import os from typing import Union, List, Dict -from pydantic import constr, Field, validator +from typing import Literal +from pydantic import Field, field_validator from jsonschema import validate as json_schema_validator from .dag import DAGStringInput, DAGIntegerInput, DAGNumberInput, DAGBooleanInput, \ @@ -30,7 +31,7 @@ class FunctionStringInput(DAGStringInput): """ - type: constr(regex='^FunctionStringInput$') = 'FunctionStringInput' + type: Literal['FunctionStringInput'] = 'FunctionStringInput' class FunctionIntegerInput(DAGIntegerInput): @@ -42,7 +43,7 @@ class FunctionIntegerInput(DAGIntegerInput): for more information. """ - type: constr(regex='^FunctionIntegerInput$') = 'FunctionIntegerInput' + type: Literal['FunctionIntegerInput'] = 'FunctionIntegerInput' class FunctionNumberInput(DAGNumberInput): @@ -53,7 +54,7 @@ class FunctionNumberInput(DAGNumberInput): See http://json-schema.org/understanding-json-schema/reference/numeric.html#numeric for more information. """ - type: constr(regex='^FunctionNumberInput$') = 'FunctionNumberInput' + type: Literal['FunctionNumberInput'] = 'FunctionNumberInput' class FunctionBooleanInput(DAGBooleanInput): @@ -66,7 +67,7 @@ class FunctionBooleanInput(DAGBooleanInput): See http://json-schema.org/understanding-json-schema/reference/boolean.html for more information. """ - type: constr(regex='^FunctionBooleanInput$') = 'FunctionBooleanInput' + type: Literal['FunctionBooleanInput'] = 'FunctionBooleanInput' class FunctionFolderInput(DAGFolderInput): @@ -87,7 +88,7 @@ class FunctionFolderInput(DAGFolderInput): "maxLength": 50, } """ - type: constr(regex='^FunctionFolderInput$') = 'FunctionFolderInput' + type: Literal['FunctionFolderInput'] = 'FunctionFolderInput' # TODO: Change this path to target_path and the one for output to source_path # for this iteration I'm keeping the changes as minimum as possible to break as @@ -98,8 +99,9 @@ class FunctionFolderInput(DAGFolderInput): ' This path is relative to the working directory where the command is executed.' ) - @validator('path') - def not_workspace_path(cls, v): + @field_validator('path') + @classmethod + def not_workspace_path(cls, v: str) -> str: if v == '.': raise ValueError('Input path for a function file or folder cannot be "."') return v @@ -137,9 +139,9 @@ class FunctionFileInput(FunctionFolderInput): } """ - type: constr(regex='^FunctionFileInput$') = 'FunctionFileInput' + type: Literal['FunctionFileInput'] = 'FunctionFileInput' - extensions: List[str] = Field( + extensions: Union[List[str], None] = Field( None, description='Optional list of extensions for file. The check for extension is ' 'case-insensitive.' @@ -185,7 +187,7 @@ class FunctionPathInput(FunctionFileInput): } """ - type: constr(regex='^FunctionPathInput$') = 'FunctionPathInput' + type: Literal['FunctionPathInput'] = 'FunctionPathInput' def validate_spec(self, value): """Validate an input value against specification. @@ -213,7 +215,7 @@ class FunctionArrayInput(DAGArrayInput): See http://json-schema.org/understanding-json-schema/reference/array.html for more information. """ - type: constr(regex='^FunctionArrayInput$') = 'FunctionArrayInput' + type: Literal['FunctionArrayInput'] = 'FunctionArrayInput' class FunctionJSONObjectInput(DAGJSONObjectInput): @@ -226,7 +228,7 @@ class FunctionJSONObjectInput(DAGJSONObjectInput): See http://json-schema.org/understanding-json-schema/reference/object.html for more information. """ - type: constr(regex='^FunctionJSONObjectInput$') = 'FunctionJSONObjectInput' + type: Literal['FunctionJSONObjectInput'] = 'FunctionJSONObjectInput' FunctionInputs = Union[ diff --git a/queenbee/io/inputs/job.py b/queenbee/io/inputs/job.py index 8ae4a714..be92735b 100644 --- a/queenbee/io/inputs/job.py +++ b/queenbee/io/inputs/job.py @@ -1,7 +1,8 @@ """Input objects for Queenbee jobs.""" from typing import Dict, List, Union, Any +from typing import Literal -from pydantic import Field, constr +from pydantic import Field from ..artifact_source import HTTP, S3, ProjectFolder from ...base.basemodel import BaseModel @@ -11,7 +12,7 @@ class JobArgument(BaseModel): """Job argument is an argument input for arguments which are not files or folders.""" - type: constr(regex='^JobArgument$') = 'JobArgument' + type: Literal['JobArgument'] = 'JobArgument' name: str = Field( ..., @@ -34,7 +35,7 @@ def is_parameter(self): class JobPathArgument(BaseModel): - type: constr(regex='^JobPathArgument$') = 'JobPathArgument' + type: Literal['JobPathArgument'] = 'JobPathArgument' name: str = Field( ..., @@ -91,9 +92,9 @@ def load_job_arguments_from_dict(data: List[Dict]) -> List[JobArguments]: f'JobArgument and JobPathArgument:\n{d}' ) if arg_type == 'JobArgument': - arg = JobArgument.parse_obj(d) + arg = JobArgument.model_validate(d) elif arg_type == 'JobPathArgument': - arg = JobPathArgument.parse_obj(d) + arg = JobPathArgument.model_validate(d) else: raise ValueError( f'Invalid type for Job argument: {arg_type}.' diff --git a/queenbee/io/inputs/step.py b/queenbee/io/inputs/step.py index 5604ec6b..3cc2eedc 100644 --- a/queenbee/io/inputs/step.py +++ b/queenbee/io/inputs/step.py @@ -5,8 +5,8 @@ import json from typing import Union, List, Dict, Any -from pydantic import constr, Field, root_validator - +from typing import Literal +from pydantic import Field from .function import FunctionStringInput, FunctionIntegerInput, \ FunctionNumberInput, FunctionBooleanInput, FunctionFolderInput, \ FunctionFileInput, FunctionPathInput, FunctionArrayInput, \ @@ -22,7 +22,7 @@ class StepStringInput(FunctionStringInput): """A String input.""" - type: constr(regex='^StepStringInput$') = 'StepStringInput' + type: Literal['StepStringInput'] = 'StepStringInput' value: str @@ -30,7 +30,7 @@ class StepStringInput(FunctionStringInput): class StepIntegerInput(FunctionIntegerInput): """An integer input.""" - type: constr(regex='^StepIntegerInput$') = 'StepIntegerInput' + type: Literal['StepIntegerInput'] = 'StepIntegerInput' value: int @@ -38,7 +38,7 @@ class StepIntegerInput(FunctionIntegerInput): class StepNumberInput(FunctionNumberInput): """A number input.""" - type: constr(regex='^StepNumberInput$') = 'StepNumberInput' + type: Literal['StepNumberInput'] = 'StepNumberInput' value: float @@ -46,14 +46,14 @@ class StepNumberInput(FunctionNumberInput): class StepBooleanInput(FunctionBooleanInput): """The boolean type matches only two special values: True and False.""" - type: constr(regex='^StepBooleanInput$') = 'StepBooleanInput' + type: Literal['StepBooleanInput'] = 'StepBooleanInput' value: bool class StepFolderInput(FunctionFolderInput): """A folder input.""" - type: constr(regex='^StepFolderInput$') = 'StepFolderInput' + type: Literal['StepFolderInput'] = 'StepFolderInput' source: Union[HTTP, S3, ProjectFolder] = Field( ..., @@ -73,7 +73,7 @@ class StepFolderInput(FunctionFolderInput): class StepFileInput(FunctionFileInput): """A file input.""" - type: constr(regex='^StepFileInput$') = 'StepFileInput' + type: Literal['StepFileInput'] = 'StepFileInput' source: Union[HTTP, S3, ProjectFolder] = Field( ..., @@ -93,7 +93,7 @@ class StepFileInput(FunctionFileInput): class StepPathInput(FunctionPathInput): """A file or a folder input.""" - type: constr(regex='^StepPathInput$') = 'StepPathInput' + type: Literal['StepPathInput'] = 'StepPathInput' source: Union[HTTP, S3, ProjectFolder] = Field( ..., @@ -113,7 +113,7 @@ class StepPathInput(FunctionPathInput): class StepArrayInput(FunctionArrayInput): """A JSON array input.""" - type: constr(regex='^StepArrayInput$') = 'StepArrayInput' + type: Literal['StepArrayInput'] = 'StepArrayInput' value: List @@ -121,7 +121,7 @@ class StepArrayInput(FunctionArrayInput): class StepJSONObjectInput(FunctionJSONObjectInput): """A JSON object input.""" - type: constr(regex='^StepJSONObjectInput$') = 'StepJSONObjectInput' + type: Literal['StepJSONObjectInput'] = 'StepJSONObjectInput' value: Dict @@ -155,38 +155,38 @@ def from_template(template: Union[DAGInputs, FunctionInputs], value: Any) -> Ste template_dict['value'] = value if template.__class__ in [DAGStringInput, FunctionStringInput]: - return StepStringInput.parse_obj(template_dict) + return StepStringInput.model_validate(template_dict) if template.__class__ in [DAGIntegerInput, FunctionIntegerInput]: template_dict['value'] = int(float(template_dict['value'])) - return StepIntegerInput.parse_obj(template_dict) + return StepIntegerInput.model_validate(template_dict) if template.__class__ in [DAGNumberInput, FunctionNumberInput]: - return StepNumberInput.parse_obj(template_dict) + return StepNumberInput.model_validate(template_dict) if template.__class__ in [DAGBooleanInput, FunctionBooleanInput]: - return StepBooleanInput.parse_obj(template_dict) + return StepBooleanInput.model_validate(template_dict) if template.__class__ in [DAGFolderInput, FunctionFolderInput]: - return StepFolderInput.parse_obj(template_dict) + return StepFolderInput.model_validate(template_dict) if template.__class__ in [DAGFileInput, FunctionFileInput]: - return StepFileInput.parse_obj(template_dict) + return StepFileInput.model_validate(template_dict) if template.__class__ in [DAGPathInput, FunctionPathInput]: - return StepPathInput.parse_obj(template_dict) + return StepPathInput.model_validate(template_dict) if template.__class__ in [DAGArrayInput, FunctionArrayInput]: if isinstance(template_dict['value'], str): template_dict['value'] = json.loads(template_dict['value']) - return StepArrayInput.parse_obj(template_dict) + return StepArrayInput.model_validate(template_dict) if template.__class__ in [DAGJSONObjectInput, FunctionJSONObjectInput]: if isinstance(template_dict['value'], str): template_dict['value'] = json.loads(template_dict['value']) try: # Try to parse JSON as a dict - return StepJSONObjectInput.parse_obj(template_dict) + return StepJSONObjectInput.model_validate(template_dict) except: # Try to parse JSON as an array - return StepArrayInput.parse_obj(template_dict) + return StepArrayInput.model_validate(template_dict) diff --git a/queenbee/io/inputs/task.py b/queenbee/io/inputs/task.py index dc516e79..1e988aa3 100644 --- a/queenbee/io/inputs/task.py +++ b/queenbee/io/inputs/task.py @@ -11,7 +11,8 @@ """ from typing import Union -from pydantic import Field, constr +from typing import Literal +from pydantic import Field from ...base.basemodel import BaseModel from ..reference import InputReference, ItemReference, TaskReference, ValueReference, \ InputFileReference, InputFolderReference, InputPathReference, \ @@ -22,7 +23,7 @@ class TaskArgument(BaseModel): """Task argument for receiving inputs that are not files or folders.""" - type: constr(regex='^TaskArgument$') = 'TaskArgument' + type: Literal['TaskArgument'] = 'TaskArgument' name: str = Field( ..., @@ -47,7 +48,7 @@ def is_parameter(self): class TaskPathArgument(BaseModel): - type: constr(regex='^TaskPathArgument$') = 'TaskPathArgument' + type: Literal['TaskPathArgument'] = 'TaskPathArgument' name: str = Field( ..., @@ -66,7 +67,7 @@ class TaskPathArgument(BaseModel): alias='from' ) - sub_path: str = Field( + sub_path: Union[str, None] = Field( None, description='A sub_path inside the path that is provided in the ``from`` field.' ' Use sub_path to only access part of the Path that is needed instead of' diff --git a/queenbee/io/outputs/alias.py b/queenbee/io/outputs/alias.py index fc6054e5..a457fdeb 100644 --- a/queenbee/io/outputs/alias.py +++ b/queenbee/io/outputs/alias.py @@ -3,9 +3,9 @@ Use these alias outputs to create a different IO object for client side UIs. """ -from typing import Union, List +from typing import Union, List, Literal -from pydantic import constr, Field, validator +from pydantic import Field, field_validator, ValidationInfo from ..common import ItemType, GenericOutput, find_dup_items, IOAliasHandler from ..reference import FileReference, FolderReference, TaskReference @@ -18,7 +18,7 @@ class DAGGenericOutputAlias(GenericOutput): output that changes its type in different platforms because of returning different objects in handler. """ - type: constr(regex='^DAGGenericOutputAlias$') = 'DAGGenericOutputAlias' + type: Literal['DAGGenericOutputAlias'] = 'DAGGenericOutputAlias' platform: List[str] = Field( ..., @@ -32,19 +32,21 @@ class DAGGenericOutputAlias(GenericOutput): description='List of process actions to process the input or output value.' ) - @validator('platform', always=True) - def create_empty_platform_list(cls, v): + @field_validator('platform', mode='before') + @classmethod + def create_empty_platform_list(cls, v: List[str]) -> List[str]: return [] if v is None else v - @validator('handler', always=True) - def check_duplicate_platform_name(cls, v, values): + @field_validator('handler', mode='before') + @classmethod + def check_duplicate_platform_name(cls, v: List[IOAliasHandler], info: ValidationInfo) -> List[IOAliasHandler]: v = [] if v is None else v languages = [h.language for h in v] dup_lang = find_dup_items(languages) if dup_lang: raise ValueError( f'Duplicate use of language(s) found in alias handlers for ' - f'{values["platform"]}: {dup_lang}. Each language can only be used once ' + f'{info.data.get("platform", "unknown")}: {dup_lang}. Each language can only be used once ' 'in each platform.' ) return v @@ -56,7 +58,7 @@ class DAGLinkedOutputAlias(DAGGenericOutputAlias): A linked output alias will be translated to an object in the UI and stay linked to it. """ - type: constr(regex='^DAGLinkedOutputAlias$') = 'DAGLinkedOutputAlias' + type: Literal['DAGLinkedOutputAlias'] = 'DAGLinkedOutputAlias' class _DAGArtifactOutputAlias(DAGGenericOutputAlias): @@ -77,7 +79,7 @@ def is_optional(self): class DAGFileOutputAlias(_DAGArtifactOutputAlias): """DAG alias file output.""" - type: constr(regex='^DAGFileOutputAlias$') = 'DAGFileOutputAlias' + type: Literal['DAGFileOutputAlias'] = 'DAGFileOutputAlias' from_: Union[TaskReference, FileReference] = Field( ..., @@ -92,7 +94,7 @@ def is_artifact(self): class DAGFolderOutputAlias(_DAGArtifactOutputAlias): """DAG alias folder output.""" - type: constr(regex='^DAGFolderOutputAlias$') = 'DAGFolderOutputAlias' + type: Literal['DAGFolderOutputAlias'] = 'DAGFolderOutputAlias' from_: Union[TaskReference, FolderReference] = Field( ..., @@ -107,7 +109,7 @@ def is_artifact(self): class DAGPathOutputAlias(_DAGArtifactOutputAlias): """DAG alias path output.""" - type: constr(regex='^DAGPathOutputAlias$') = 'DAGPathOutputAlias' + type: Literal['DAGPathOutputAlias'] = 'DAGPathOutputAlias' from_: Union[TaskReference, FileReference, FolderReference] = Field( ..., @@ -126,7 +128,7 @@ class DAGStringOutputAlias(DAGFileOutputAlias): This output loads the content from a file as a string. """ - type: constr(regex='^DAGStringOutputAlias$') = 'DAGStringOutputAlias' + type: Literal['DAGStringOutputAlias'] = 'DAGStringOutputAlias' @property def is_artifact(self): @@ -138,7 +140,7 @@ class DAGIntegerOutputAlias(DAGStringOutputAlias): This output loads the content from a file as an integer. """ - type: constr(regex='^DAGIntegerOutputAlias$') = 'DAGIntegerOutputAlias' + type: Literal['DAGIntegerOutputAlias'] = 'DAGIntegerOutputAlias' class DAGNumberOutputAlias(DAGStringOutputAlias): @@ -146,7 +148,7 @@ class DAGNumberOutputAlias(DAGStringOutputAlias): This output loads the content from a file as a floating number. """ - type: constr(regex='^DAGNumberOutputAlias$') = 'DAGNumberOutputAlias' + type: Literal['DAGNumberOutputAlias'] = 'DAGNumberOutputAlias' class DAGBooleanOutputAlias(DAGStringOutputAlias): @@ -154,7 +156,7 @@ class DAGBooleanOutputAlias(DAGStringOutputAlias): This output loads the content from a file as a boolean. """ - type: constr(regex='^DAGBooleanOutputAlias$') = 'DAGBooleanOutputAlias' + type: Literal['DAGBooleanOutputAlias'] = 'DAGBooleanOutputAlias' class DAGArrayOutputAlias(DAGStringOutputAlias): @@ -162,7 +164,7 @@ class DAGArrayOutputAlias(DAGStringOutputAlias): This output loads the content from a JSON file which must be a JSON Array. """ - type: constr(regex='^DAGArrayOutputAlias$') = 'DAGArrayOutputAlias' + type: Literal['DAGArrayOutputAlias'] = 'DAGArrayOutputAlias' items_type: ItemType = Field( ItemType.String, @@ -176,7 +178,7 @@ class DAGJSONObjectOutputAlias(DAGStringOutputAlias): This output loads the content from a file as a JSON object. """ - type: constr(regex='^DAGJSONObjectOutputAlias$') = 'DAGJSONObjectOutputAlias' + type: Literal['DAGJSONObjectOutputAlias'] = 'DAGJSONObjectOutputAlias' DAGAliasOutputs = Union[ diff --git a/queenbee/io/outputs/dag.py b/queenbee/io/outputs/dag.py index 56050179..d4f04854 100644 --- a/queenbee/io/outputs/dag.py +++ b/queenbee/io/outputs/dag.py @@ -2,11 +2,12 @@ from typing import Union, List -from pydantic import constr, Field, validator +from pydantic import Field, field_validator from ..common import ItemType, FromOutput from .alias import DAGAliasOutputs from ..reference import FileReference, FolderReference, TaskReference +from typing import Literal class DAGGenericOutput(FromOutput): @@ -16,19 +17,14 @@ class DAGGenericOutput(FromOutput): output that changes its type in different platforms because of returning different objects in handler. """ - type: constr(regex='^DAGGenericOutput$') = 'DAGGenericOutput' + type: Literal['DAGGenericOutput'] = 'DAGGenericOutput' alias: List[DAGAliasOutputs] = Field( - None, + default_factory=list, description='A list of additional processes for loading this output on ' 'different platforms.' ) - @validator('alias', always=True) - def create_empty_handler_list(cls, v): - return [] if v is None else v - - class _DAGArtifactOutput(DAGGenericOutput): """Base class for DAG artifact outputs. @@ -47,7 +43,7 @@ def is_optional(self): class DAGFileOutput(_DAGArtifactOutput): """DAG file output.""" - type: constr(regex='^DAGFileOutput$') = 'DAGFileOutput' + type: Literal['DAGFileOutput'] = 'DAGFileOutput' from_: Union[TaskReference, FileReference] = Field( ..., @@ -62,7 +58,7 @@ def is_artifact(self): class DAGFolderOutput(_DAGArtifactOutput): """DAG folder output.""" - type: constr(regex='^DAGFolderOutput$') = 'DAGFolderOutput' + type: Literal['DAGFolderOutput'] = 'DAGFolderOutput' from_: Union[TaskReference, FolderReference] = Field( ..., @@ -77,7 +73,7 @@ def is_artifact(self): class DAGPathOutput(_DAGArtifactOutput): """DAG path output.""" - type: constr(regex='^DAGPathOutput$') = 'DAGPathOutput' + type: Literal['DAGPathOutput'] = 'DAGPathOutput' from_: Union[TaskReference, FileReference, FolderReference] = Field( ..., @@ -96,7 +92,7 @@ class DAGStringOutput(DAGFileOutput): This output loads the content from a file as a string. """ - type: constr(regex='^DAGStringOutput$') = 'DAGStringOutput' + type: Literal['DAGStringOutput'] = 'DAGStringOutput' @property def is_artifact(self): @@ -108,7 +104,7 @@ class DAGIntegerOutput(DAGStringOutput): This output loads the content from a file as an integer. """ - type: constr(regex='^DAGIntegerOutput$') = 'DAGIntegerOutput' + type: Literal['DAGIntegerOutput'] = 'DAGIntegerOutput' class DAGNumberOutput(DAGStringOutput): @@ -116,7 +112,7 @@ class DAGNumberOutput(DAGStringOutput): This output loads the content from a file as a floating number. """ - type: constr(regex='^DAGNumberOutput$') = 'DAGNumberOutput' + type: Literal['DAGNumberOutput'] = 'DAGNumberOutput' class DAGBooleanOutput(DAGStringOutput): @@ -124,7 +120,7 @@ class DAGBooleanOutput(DAGStringOutput): This output loads the content from a file as a boolean. """ - type: constr(regex='^DAGBooleanOutput$') = 'DAGBooleanOutput' + type: Literal['DAGBooleanOutput'] = 'DAGBooleanOutput' class DAGArrayOutput(DAGStringOutput): @@ -132,7 +128,7 @@ class DAGArrayOutput(DAGStringOutput): This output loads the content from a JSON file which must be a JSON Array. """ - type: constr(regex='^DAGArrayOutput$') = 'DAGArrayOutput' + type: Literal['DAGArrayOutput'] = 'DAGArrayOutput' items_type: ItemType = Field( ItemType.String, @@ -146,7 +142,7 @@ class DAGJSONObjectOutput(DAGStringOutput): This output loads the content from a file as a JSON object. """ - type: constr(regex='^DAGJSONObjectOutput$') = 'DAGJSONObjectOutput' + type: Literal['DAGJSONObjectOutput'] = 'DAGJSONObjectOutput' DAGOutputs = Union[ diff --git a/queenbee/io/outputs/function.py b/queenbee/io/outputs/function.py index 816ebd3c..c06e51e8 100644 --- a/queenbee/io/outputs/function.py +++ b/queenbee/io/outputs/function.py @@ -3,15 +3,15 @@ For more information on plugins see plugin module. """ -from typing import Union -from pydantic import constr, Field +from typing import Union, Literal +from pydantic import Field from ..common import PathOutput, ItemType class FunctionFileOutput(PathOutput): """Function File output.""" - type: constr(regex='^FunctionFileOutput$') = 'FunctionFileOutput' + type: Literal['FunctionFileOutput'] = 'FunctionFileOutput' path: str = Field( ..., @@ -26,7 +26,7 @@ def is_artifact(self): class FunctionFolderOutput(PathOutput): """Function Folder output.""" - type: constr(regex='^FunctionFolderOutput$') = 'FunctionFolderOutput' + type: Literal['FunctionFolderOutput'] = 'FunctionFolderOutput' path: str = Field( ..., @@ -41,7 +41,7 @@ def is_artifact(self): class FunctionPathOutput(PathOutput): """Function Path output.""" - type: constr(regex='^FunctionPathOutput$') = 'FunctionPathOutput' + type: Literal['FunctionPathOutput'] = 'FunctionPathOutput' path: str = Field( ..., @@ -59,7 +59,7 @@ class FunctionStringOutput(FunctionFileOutput): This output loads the content from a file as a string. """ - type: constr(regex='^FunctionStringOutput$') = 'FunctionStringOutput' + type: Literal['FunctionStringOutput'] = 'FunctionStringOutput' @property def is_artifact(self): @@ -71,7 +71,7 @@ class FunctionIntegerOutput(FunctionStringOutput): This output loads the content from a file as an integer. """ - type: constr(regex='^FunctionIntegerOutput$') = 'FunctionIntegerOutput' + type: Literal['FunctionIntegerOutput'] = 'FunctionIntegerOutput' class FunctionNumberOutput(FunctionStringOutput): @@ -79,7 +79,7 @@ class FunctionNumberOutput(FunctionStringOutput): This output loads the content from a file as a floating number. """ - type: constr(regex='^FunctionNumberOutput$') = 'FunctionNumberOutput' + type: Literal['FunctionNumberOutput'] = 'FunctionNumberOutput' class FunctionBooleanOutput(FunctionStringOutput): @@ -87,7 +87,7 @@ class FunctionBooleanOutput(FunctionStringOutput): This output loads the content from a file as a boolean. """ - type: constr(regex='^FunctionBooleanOutput$') = 'FunctionBooleanOutput' + type: Literal['FunctionBooleanOutput'] = 'FunctionBooleanOutput' class FunctionArrayOutput(FunctionStringOutput): @@ -95,7 +95,7 @@ class FunctionArrayOutput(FunctionStringOutput): This output loads the content from a JSON file which must be a JSON Array. """ - type: constr(regex='^FunctionArrayOutput$') = 'FunctionArrayOutput' + type: Literal['FunctionArrayOutput'] = 'FunctionArrayOutput' items_type: ItemType = Field( ItemType.String, @@ -109,7 +109,7 @@ class FunctionJSONObjectOutput(FunctionStringOutput): This output loads the content from a file as a JSON object. """ - type: constr(regex='^FunctionJSONObjectOutput$') = 'FunctionJSONObjectOutput' + type: Literal['FunctionJSONObjectOutput'] = 'FunctionJSONObjectOutput' FunctionOutputs = Union[ diff --git a/queenbee/io/outputs/step.py b/queenbee/io/outputs/step.py index bb6c8417..e67a5390 100644 --- a/queenbee/io/outputs/step.py +++ b/queenbee/io/outputs/step.py @@ -4,8 +4,8 @@ """ import json -from typing import Union, List, Dict, Any -from pydantic import constr, Field +from typing import Union, List, Dict, Any, Literal +from pydantic import Field from .function import FunctionStringOutput, FunctionIntegerOutput, \ FunctionNumberOutput, FunctionBooleanOutput, FunctionFolderOutput, \ @@ -22,7 +22,7 @@ class StepStringOutput(FunctionStringOutput): """A String output.""" - type: constr(regex='^StepStringOutput$') = 'StepStringOutput' + type: Literal['StepStringOutput'] = 'StepStringOutput' value: str @@ -30,7 +30,7 @@ class StepStringOutput(FunctionStringOutput): class StepIntegerOutput(FunctionIntegerOutput): """An integer output.""" - type: constr(regex='^StepIntegerOutput$') = 'StepIntegerOutput' + type: Literal['StepIntegerOutput'] = 'StepIntegerOutput' value: int @@ -38,7 +38,7 @@ class StepIntegerOutput(FunctionIntegerOutput): class StepNumberOutput(FunctionNumberOutput): """A number output.""" - type: constr(regex='^StepNumberOutput$') = 'StepNumberOutput' + type: Literal['StepNumberOutput'] = 'StepNumberOutput' value: float @@ -46,14 +46,14 @@ class StepNumberOutput(FunctionNumberOutput): class StepBooleanOutput(FunctionBooleanOutput): """The boolean type matches only two special values: True and False.""" - type: constr(regex='^StepBooleanOutput$') = 'StepBooleanOutput' + type: Literal['StepBooleanOutput'] = 'StepBooleanOutput' value: bool class StepFolderOutput(FunctionFolderOutput): """A folder output.""" - type: constr(regex='^StepFolderOutput$') = 'StepFolderOutput' + type: Literal['StepFolderOutput'] = 'StepFolderOutput' source: Union[HTTP, S3, ProjectFolder] = Field( ..., @@ -64,7 +64,7 @@ class StepFolderOutput(FunctionFolderOutput): class StepFileOutput(FunctionFileOutput): """A file output.""" - type: constr(regex='^StepFileOutput$') = 'StepFileOutput' + type: Literal['StepFileOutput'] = 'StepFileOutput' source: Union[HTTP, S3, ProjectFolder] = Field( ..., @@ -75,7 +75,7 @@ class StepFileOutput(FunctionFileOutput): class StepPathOutput(FunctionPathOutput): """A file or a folder output.""" - type: constr(regex='^StepPathOutput$') = 'StepPathOutput' + type: Literal['StepPathOutput'] = 'StepPathOutput' source: Union[HTTP, S3, ProjectFolder] = Field( ..., @@ -86,7 +86,7 @@ class StepPathOutput(FunctionPathOutput): class StepArrayOutput(FunctionArrayOutput): """A JSON array output.""" - type: constr(regex='^StepArrayOutput$') = 'StepArrayOutput' + type: Literal['StepArrayOutput'] = 'StepArrayOutput' value: List @@ -94,7 +94,7 @@ class StepArrayOutput(FunctionArrayOutput): class StepJSONObjectOutput(FunctionJSONObjectOutput): """A JSON object output.""" - type: constr(regex='^StepJSONObjectOutput$') = 'StepJSONObjectOutput' + type: Literal['StepJSONObjectOutput'] = 'StepJSONObjectOutput' value: Dict @@ -131,38 +131,38 @@ def from_template(template: Union[DAGOutputs, FunctionOutputs], value: Any) -> S template_dict['value'] = value if template.__class__ in [DAGStringOutput, FunctionStringOutput]: - return StepStringOutput.parse_obj(template_dict) + return StepStringOutput.model_validate(template_dict) if template.__class__ in [DAGIntegerOutput, FunctionIntegerOutput]: template_dict['value'] = int(float(template_dict['value'])) - return StepIntegerOutput.parse_obj(template_dict) + return StepIntegerOutput.model_validate(template_dict) if template.__class__ in [DAGNumberOutput, FunctionNumberOutput]: - return StepNumberOutput.parse_obj(template_dict) + return StepNumberOutput.model_validate(template_dict) if template.__class__ in [DAGBooleanOutput, FunctionBooleanOutput]: - return StepBooleanOutput.parse_obj(template_dict) + return StepBooleanOutput.model_validate(template_dict) if template.__class__ in [DAGFolderOutput, FunctionFolderOutput]: - return StepFolderOutput.parse_obj(template_dict) + return StepFolderOutput.model_validate(template_dict) if template.__class__ in [DAGFileOutput, FunctionFileOutput]: - return StepFileOutput.parse_obj(template_dict) + return StepFileOutput.model_validate(template_dict) if template.__class__ in [DAGPathOutput, FunctionPathOutput]: - return StepPathOutput.parse_obj(template_dict) + return StepPathOutput.model_validate(template_dict) if template.__class__ in [DAGArrayOutput, FunctionArrayOutput]: if isinstance(template_dict['value'], str): template_dict['value'] = json.loads(template_dict['value']) - return StepArrayOutput.parse_obj(template_dict) + return StepArrayOutput.model_validate(template_dict) if template.__class__ in [DAGJSONObjectOutput, FunctionJSONObjectOutput]: if isinstance(template_dict['value'], str): template_dict['value'] = json.loads(template_dict['value']) try: # Try to parse JSON as a dict - return StepJSONObjectOutput.parse_obj(template_dict) + return StepJSONObjectOutput.model_validate(template_dict) except: # Try to parse JSON as an array - return StepArrayOutput.parse_obj(template_dict) + return StepArrayOutput.model_validate(template_dict) diff --git a/queenbee/io/outputs/task.py b/queenbee/io/outputs/task.py index 605f4738..3cd7238b 100644 --- a/queenbee/io/outputs/task.py +++ b/queenbee/io/outputs/task.py @@ -10,8 +10,8 @@ """ -from typing import Union -from pydantic import constr +from typing import Union, Literal + from ..common import GenericOutput, PathOutput @@ -19,7 +19,7 @@ class TaskReturn(GenericOutput): """A Task return output that exposes the values from a function or a DAG.""" - type: constr(regex='^TaskReturn$') = 'TaskReturn' + type: Literal['TaskReturn'] = 'TaskReturn' @property def is_artifact(self): @@ -29,7 +29,7 @@ def is_artifact(self): class TaskPathReturn(PathOutput): """A Task output that returns a file or a folder output from a function or a DAG.""" - type: constr(regex='^TaskPathReturn$') = 'TaskPathReturn' + type: Literal['TaskPathReturn'] = 'TaskPathReturn' @property def is_artifact(self): diff --git a/queenbee/io/reference.py b/queenbee/io/reference.py index 14ffa092..31584fba 100644 --- a/queenbee/io/reference.py +++ b/queenbee/io/reference.py @@ -1,8 +1,8 @@ """Objects to reference parameters, files and folders from inputs, tasks and items.""" import re -from typing import List, Union, Dict, Any -from pydantic import Field, constr, validator +from typing import List, Union, Dict, Any, Literal +from pydantic import Field, field_validator from ..base.basemodel import BaseModel @@ -22,7 +22,7 @@ def template_string(keys: List[str]) -> str: class _BaseReference(BaseModel): """A Base reference model.""" - type: constr(regex='^_BaseReference$') = '_BaseReference' + type: Literal['_BaseReference'] = '_BaseReference' @property def source(self): @@ -34,7 +34,7 @@ def fetch_from_flow(self, flow): class FileReference(_BaseReference): """Reference to a file.""" - type: constr(regex='^FileReference$') = 'FileReference' + type: Literal['FileReference'] = 'FileReference' path: str = Field( ..., @@ -44,7 +44,7 @@ class FileReference(_BaseReference): class FolderReference(_BaseReference): """Reference to a folder.""" - type: constr(regex='^FolderReference$') = 'FolderReference' + type: Literal['FolderReference'] = 'FolderReference' path: str = Field( ..., @@ -65,7 +65,7 @@ def referenced_values(self) -> Dict[str, List[str]]: class _TaskReferenceBase(_BaseReference): """A Task Reference""" - type: constr(regex='^_TaskReferenceBase$') = '_TaskReferenceBase' + type: Literal['_TaskReferenceBase'] = '_TaskReferenceBase' name: str = Field( ..., @@ -92,7 +92,7 @@ def to_ref_string(self) -> str: class TaskFileReference(_TaskReferenceBase): """A reference to a file that is generated in a task.""" - type: constr(regex='^TaskFileReference$') = 'TaskFileReference' + type: Literal['TaskFileReference'] = 'TaskFileReference' @property def source(self): @@ -102,7 +102,7 @@ def source(self): class TaskFolderReference(_TaskReferenceBase): """A reference to a folder that is generated in a task.""" - type: constr(regex='^TaskFolderReference$') = 'TaskFolderReference' + type: Literal['TaskFolderReference'] = 'TaskFolderReference' @property def source(self): @@ -112,7 +112,7 @@ def source(self): class TaskPathReference(_TaskReferenceBase): """A reference to a file or folder that is generated in a task.""" - type: constr(regex='^TaskPathReference$') = 'TaskPathReference' + type: Literal['TaskPathReference'] = 'TaskPathReference' @property def source(self): @@ -122,7 +122,7 @@ def source(self): class TaskReference(_TaskReferenceBase): """A Task reference for parameters other than files or folders.""" - type: constr(regex='^TaskReference$') = 'TaskReference' + type: Literal['TaskReference'] = 'TaskReference' @property def source(self): @@ -132,7 +132,7 @@ def source(self): class _InputReferenceBase(_BaseReference): """An input reference.""" - type: constr(regex='^_InputReferenceBase$') = '_InputReferenceBase' + type: Literal['_InputReferenceBase'] = '_InputReferenceBase' variable: str = Field( ..., @@ -156,7 +156,7 @@ class InputReference(_InputReferenceBase): InputPathReference instead. """ - type: constr(regex='^InputReference$') = 'InputReference' + type: Literal['InputReference'] = 'InputReference' @property def source(self): @@ -166,7 +166,7 @@ def source(self): class InputFileReference(_InputReferenceBase): """An input file reference""" - type: constr(regex='^InputFileReference$') = 'InputFileReference' + type: Literal['InputFileReference'] = 'InputFileReference' @property def source(self): @@ -176,7 +176,7 @@ def source(self): class InputFolderReference(_InputReferenceBase): """An input folder reference""" - type: constr(regex='^InputFolderReference$') = 'InputFolderReference' + type: Literal['InputFolderReference'] = 'InputFolderReference' @property def source(self): @@ -186,7 +186,7 @@ def source(self): class InputPathReference(_InputReferenceBase): """An input file or folder reference""" - type: constr(regex='^InputPathReference$') = 'InputPathReference' + type: Literal['InputPathReference'] = 'InputPathReference' @property def source(self): @@ -196,7 +196,7 @@ def source(self): class ItemReference(_BaseReference): """An Item Reference.""" - type: constr(regex='^ItemReference$') = 'ItemReference' + type: Literal['ItemReference'] = 'ItemReference' variable: str = Field( None, @@ -221,7 +221,7 @@ def source(self): class ValueReference(_BaseReference): """A reference to a fixed value.""" - type: constr(regex='^ValueReference$') = 'ValueReference' + type: Literal['ValueReference'] = 'ValueReference' value: Any = Field( ..., @@ -232,7 +232,7 @@ class ValueReference(_BaseReference): class ValueListReference(_BaseReference): """A reference to a fixed value.""" - type: constr(regex='^ValueListReference$') = 'ValueListReference' + type: Literal['ValueListReference'] = 'ValueListReference' # TODO: Add validation for fixed value reference types. value: List[Any] = Field( @@ -240,8 +240,9 @@ class ValueListReference(_BaseReference): description='A fixed value for this reference.' ) - @validator('value') - def check_value(cls, v): + @field_validator('value') + @classmethod + def check_value(cls, v: List[Any]) -> List[Any]: if v == []: raise ValueError( 'ValueListReference must be a non-empty list or null') @@ -251,7 +252,7 @@ def check_value(cls, v): class ValueFileReference(_BaseReference): """A reference to a fixed file.""" - type: constr(regex='^ValueFileReference$') = 'ValueFileReference' + type: Literal['ValueFileReference'] = 'ValueFileReference' path: str = Field( ..., @@ -262,7 +263,7 @@ class ValueFileReference(_BaseReference): class ValueFolderReference(ValueFileReference): """A reference to a fixed folder.""" - type: constr(regex='^ValueFolderReference$') = 'ValueFolderReference' + type: Literal['ValueFolderReference'] = 'ValueFolderReference' def references_from_string(string: str) -> List[ diff --git a/queenbee/job/job.py b/queenbee/job/job.py index 0c1248c9..8beae185 100644 --- a/queenbee/job/job.py +++ b/queenbee/job/job.py @@ -1,10 +1,11 @@ from datetime import datetime from enum import Enum -from typing import Dict, List - -from pydantic import Field, constr, validator -from pydantic.error_wrappers import ErrorWrapper, ValidationError +from typing import Dict, List, Union +from typing import Literal +from pydantic import Field, field_validator +from pydantic import ValidationError +from pydantic_core import InitErrorDetails from ..base.basemodel import BaseModel from ..io.inputs.dag import DAGInputs from ..io.inputs.job import JobArgument, JobArguments, JobPathArgument @@ -15,45 +16,48 @@ class Job(BaseModel): A Job is an object to submit a list of arguments to execute a Queenbee recipe. """ - api_version: constr(regex='^v1beta1$') = Field('v1beta1', readOnly=True) + api_version: Literal['v1beta1'] = Field('v1beta1', json_schema_extra={'readOnly': True}) - type: constr(regex='^Job$') = 'Job' + type: Literal['Job'] = 'Job' source: str = Field( ..., description='The source url for downloading the recipe.' ) - arguments: List[List[JobArguments]] = Field( + arguments: Union[List[List[JobArguments]], None] = Field( None, description='Input arguments for this job.' ) - name: str = Field( + name: Union[str, None] = Field( None, description='An optional name for this job. This name will be used a the ' 'display name for the run.' ) - description: str = Field( + description: Union[str, None] = Field( None, description='Run description.' ) - labels: Dict[str, str] = Field( + labels: Union[Dict[str, str], None] = Field( None, description='Optional user data as a dictionary. User data is for user reference' ' only and will not be used in the execution of the job.' ) - @validator('arguments', each_item=True) - def check_duplicate_names(cls, v): - argument_names = [] - for arg in v: - if arg.name in argument_names: - raise ValueError(f'duplicate argument name {arg.name}') - argument_names.append(arg.name) - + @field_validator('arguments') + @classmethod + def check_duplicate_names(cls, v: List[List[JobArguments]]): + if v is None: + return v + for arg_list in v: + argument_names = [] + for arg in arg_list: + if arg.name in argument_names: + raise ValueError(f'duplicate argument name {arg.name}') + argument_names.append(arg.name) return v def populate_default_arguments(self, inputs: List[DAGInputs]): @@ -81,20 +85,31 @@ def populate_default_arguments(self, inputs: List[DAGInputs]): combination.append(argument) def validate_arguments(self, inputs: List[DAGInputs]): - errors = [] + errors: List[InitErrorDetails] = [] + for i, arg in enumerate(self.arguments): + # Assuming this returns a list of Exceptions or error strings error_list = self._validate_argument_combination(arg, inputs) - errors.extend([ - ErrorWrapper(error, ('arguments', i)) for error in error_list - ]) - if len(errors) != 0: - raise ValidationError(errors, self.__class__) + + for error in error_list: + # In V2, we define errors as dictionaries (InitErrorDetails) + errors.append( + InitErrorDetails( + type='value_error', # Standard error type + loc=('arguments', i), + msg=str(error), # Convert the exception/error to a string message + input=arg, # The value that caused the error + ctx={'error': error} + ) + ) + + if errors: + # V2 method to raise validation errors manually + raise ValidationError.from_exception_data(self.__class__.__name__, errors) def _validate_argument_combination( - self, - arguments: List[JobArguments], - inputs: List[DAGInputs], - ) -> List[ErrorWrapper]: + self, arguments: List[JobArguments], inputs: List[DAGInputs] + ) -> List[ValueError]: errors = [] for recipe_input in inputs: @@ -104,7 +119,7 @@ def _validate_argument_combination( found_argument = True try: self._check_argument_type(argument, recipe_input) - except Exception as err: + except ValueError as err: errors.append(err) if recipe_input.required and not found_argument: @@ -149,9 +164,9 @@ class JobStatusEnum(str, Enum): class JobStatus(BaseModel): """Parametric Job Status.""" - api_version: constr(regex='^v1beta1$') = Field('v1beta1', readOnly=True) + api_version: Literal['v1beta1'] = Field('v1beta1', json_schema_extra={'readOnly': True}) - type: constr(regex='^JobStatus$') = 'JobStatus' + type: Literal['JobStatus'] = 'JobStatus' id: str = Field( ..., @@ -163,7 +178,7 @@ class JobStatus(BaseModel): description='The status of this job.' ) - message: str = Field( + message: Union[str, None] = Field( None, description='Any message produced by the job. Usually error/debugging hints.' ) @@ -173,12 +188,12 @@ class JobStatus(BaseModel): description='The time at which the job was started' ) - finished_at: datetime = Field( + finished_at: Union[datetime, None] = Field( None, description='The time at which the task was completed' ) - source: str = Field( + source: Union[str, None] = Field( None, description='Source url for the status object. It can be a recipe or a function.' ) diff --git a/queenbee/job/run.py b/queenbee/job/run.py index 6ddfe182..247d5882 100644 --- a/queenbee/job/run.py +++ b/queenbee/job/run.py @@ -3,8 +3,8 @@ A Run contains the status of an individual recipe being executed """ from enum import Enum -from pydantic import Field, constr -from typing import List, Dict +from pydantic import Field +from typing import List, Dict, Literal, Union from ..io.inputs.step import StepInputs from ..io.outputs.step import StepOutputs @@ -43,7 +43,7 @@ class StatusType(str, Enum): class StepStatus(BaseStatus): """The Status of a Job Step""" - type: constr(regex='^StepStatus$') = 'StepStatus' + type: Literal['StepStatus'] = 'StepStatus' id: str = Field( ..., @@ -73,7 +73,7 @@ class StepStatus(BaseStatus): description='The name of the template that spawned this step' ) - command: str = Field( + command: Union[str, None] = Field( None, description='The command used to run this step. Only applies to Function steps.' ) @@ -88,7 +88,7 @@ class StepStatus(BaseStatus): description='The outputs produced by this step.' ) - boundary_id: str = Field( + boundary_id: Union[str, None] = Field( None, description='This indicates the step ID of the associated template root \ step in which this step belongs to. A DAG step will have the id of the \ @@ -131,9 +131,9 @@ class RunStatusEnum(str, Enum): class RunStatus(BaseStatus): """Job Status.""" - api_version: constr(regex='^v1beta1$') = Field('v1beta1', readOnly=True) + api_version: Literal['v1beta1'] = Field('v1beta1', json_schema_extra={'readOnly': True}) - type: constr(regex='^RunStatus$') = 'RunStatus' + type: Literal['RunStatus'] = 'RunStatus' id: str = Field( ..., @@ -145,7 +145,7 @@ class RunStatus(BaseStatus): description='The ID of the job that generated this run.' ) - entrypoint: str = Field( + entrypoint: Union[str, None] = Field( None, description='The ID of the first step in the run.' ) diff --git a/queenbee/job/status.py b/queenbee/job/status.py index e3778b81..a7451c69 100644 --- a/queenbee/job/status.py +++ b/queenbee/job/status.py @@ -4,14 +4,15 @@ express the status of a job, run or step """ from datetime import datetime -from pydantic import Field, constr +from pydantic import Field from ..io.common import IOBase +from typing import Literal, Union class BaseStatus(IOBase): """Base Status model""" - type: constr(regex='^BaseStatus$') = 'BaseStatus' + type: Literal['BaseStatus'] = 'BaseStatus' - message: str = Field( + message: Union[str, None] = Field( None, description='Any message produced by the task. Usually error/debugging hints.' ) @@ -21,13 +22,12 @@ class BaseStatus(IOBase): description='The time at which the task was started' ) - finished_at: datetime = Field( + finished_at: Union[datetime, None] = Field( None, description='The time at which the task was completed' ) - source: str = Field( + source: Union[str, None] = Field( None, description='Source url for the status object. It can be a recipe or a function.' ) - diff --git a/queenbee/plugin/function.py b/queenbee/plugin/function.py index 57af4997..a7617c96 100644 --- a/queenbee/plugin/function.py +++ b/queenbee/plugin/function.py @@ -1,7 +1,7 @@ """Queenbee Function class.""" from enum import Enum -from typing import List -from pydantic import Field, validator, constr, root_validator, NoneStr +from typing import List, Union, Literal, Annotated +from pydantic import Field, field_validator, model_validator, ValidationInfo from ..io.common import IOBase from ..io.inputs.function import FunctionInputs @@ -17,30 +17,30 @@ class ScriptingLanguages(str, Enum): class Function(IOBase): """A Function with a single or a script.""" - type: constr(regex='^Function$') = 'Function' + type: Literal['Function'] = 'Function' name: str = Field( ..., description='Function name. Must be unique within a plugin.' ) - description: str = Field( + description: Union[str, None] = Field( None, description='Function description. A short human readable description for' ' this function.' ) - inputs: List[FunctionInputs] = Field( - None, + inputs: List[Annotated[FunctionInputs, Field(discriminator='type')]] = Field( + default_factory=list, description=u'Input arguments for this function.' ) - outputs: List[FunctionOutputs] = Field( - None, + outputs: List[Annotated[FunctionOutputs, Field(discriminator='type')]] = Field( + default_factory=list, description='List of output arguments.' ) - command: NoneStr = Field( + command: Union[str, None] = Field( None, description=u'Full shell command for this function. Each function accepts only ' 'one command. The command will be executed as a shell command in plugin. ' @@ -54,7 +54,7 @@ class Function(IOBase): 'supported.' ) - source: NoneStr = Field( + source: Union[str, None] = Field( None, description='Source contains the source code of the script to execute.' ) @@ -99,22 +99,23 @@ def validate_referenced_values(input_names: List[str], variables: List): msg = f'Invalid referenced value(s) in function:\n{info}' raise ValueError(msg) - @validator('command', 'source') - def validate_command_refs(cls, v, values): + @field_validator('command', 'source') + @classmethod + def validate_command_refs(cls, v: Union[str, None], info: ValidationInfo) -> Union[str, None]: """Validate referenced variables in the command""" if not v: - return + return v ref_var = get_ref_variable(v) # If inputs is not in values it has failed validation # and we cannot check/validate output refs - if 'inputs' not in values: + if 'inputs' not in info.data: return v - inputs = values.get('inputs') + inputs = info.data.get('inputs') - input_names = [param.name for param in inputs] + input_names = [param.name for param in inputs] if inputs else [] cls.validate_referenced_values( input_names=input_names, @@ -123,16 +124,16 @@ def validate_command_refs(cls, v, values): return v - @root_validator(skip_on_failure=True) - def check_either_source_or_command(cls, values): + @model_validator(mode='after') + def check_either_source_or_command(self) -> 'Function': """Validate either source or command is provided""" - command = values.get('command') - source = values.get('source') + command = self.command + source = self.source if command or source: - return values + return self # validation failed - name = values.get('name') + name = self.name raise ValueError( f'Invalid Function: {name}. Either command or source must be provided.' ) diff --git a/queenbee/plugin/plugin.py b/queenbee/plugin/plugin.py index 41460a2e..4bca3ee2 100644 --- a/queenbee/plugin/plugin.py +++ b/queenbee/plugin/plugin.py @@ -2,8 +2,8 @@ import json import os import yaml -from typing import List -from pydantic import Field, validator, constr +from typing import List, Literal, Union +from pydantic import Field, field_validator from ..base.basemodel import BaseModel from ..base.metadata import MetaData @@ -13,14 +13,14 @@ class DockerConfig(BaseModel): """Plugin Configuration to run in a Docker container""" - type: constr(regex='^DockerConfig') = 'DockerConfig' + type: Literal['DockerConfig'] = 'DockerConfig' image: str = Field( ..., description='Docker image name. Must include tag.' ) - registry: str = Field( + registry: Union[str, None] = Field( None, description='The container registry URLs that this container should be pulled' ' from. Will default to Dockerhub if none is specified.' @@ -37,7 +37,7 @@ class DockerConfig(BaseModel): class LocalConfig(BaseModel): """Plugin Configuration to run on a desktop.""" - type: constr(regex='^LocalConfig') = 'LocalConfig' + type: Literal['LocalConfig'] = 'LocalConfig' class PluginConfig(BaseModel): @@ -46,14 +46,14 @@ class PluginConfig(BaseModel): The config is used to schedule functions on a desktop or in other contexts (ie: Docker). """ - type: constr(regex='^PluginConfig') = 'PluginConfig' + type: Literal['PluginConfig'] = 'PluginConfig' docker: DockerConfig = Field( ..., description='The configuration to use this plugin in a docker container' ) - local: LocalConfig = Field( + local: Union[LocalConfig, None] = Field( None, description='The configuration to use this plugin locally' ) @@ -65,9 +65,9 @@ class Plugin(BaseModel): A plugin contains runtime configuration for a Command Line Interface (CLI) and a list of functions that can be executed using this CLI tool. """ - api_version: constr(regex='^v1beta1$') = Field('v1beta1', readOnly=True) + api_version: Literal['v1beta1'] = Field('v1beta1', json_schema_extra={'readOnly': True}) - type: constr(regex='^Plugin') = 'Plugin' + type: Literal['Plugin'] = 'Plugin' metadata: MetaData = Field( ..., @@ -84,7 +84,8 @@ class Plugin(BaseModel): description='List of Plugin functions' ) - @validator('functions') + @field_validator('functions') + @classmethod def sort_list(cls, v: list) -> list: """Sort functions list by name @@ -134,15 +135,16 @@ def from_folder(cls, folder_path: str): with open(config_path, 'r') as f: config = yaml.load(f, yaml.FullLoader) - for function in os.listdir(functions_path): - with open(os.path.join(functions_path, function), 'r') as f: + function_files = sorted(os.listdir(functions_path)) + for f_name in function_files: + with open(os.path.join(functions_path, f_name), 'r') as f: functions.append(yaml.load(f, yaml.FullLoader)) plugin['metadata'] = metadata plugin['config'] = config plugin['functions'] = functions - return cls.parse_obj(plugin) + return cls.model_validate(plugin) def to_folder(self, folder_path: str, *, readme_string: str = None): """Write a plugin to a folder diff --git a/queenbee/recipe/dag.py b/queenbee/recipe/dag.py index 85a97c87..26a97e3d 100644 --- a/queenbee/recipe/dag.py +++ b/queenbee/recipe/dag.py @@ -4,9 +4,9 @@ step indicates what function template should be used and maps inputs and outputs for the specific task. """ -from queenbee.io.outputs.task import TaskPathReturn, TaskReturn -from typing import List, Set, Union -from pydantic import Field, validator, root_validator, constr +from typing import List, Set, Union, Literal + +from pydantic import Field, field_validator, model_validator from .task import DAGTask from ..io.common import IOBase, find_dup_items @@ -14,11 +14,12 @@ from ..io.outputs.dag import DAGOutputs from ..io.reference import FileReference, FolderReference, TaskReference, \ TaskFileReference, TaskFolderReference, TaskPathReference +from ..io.outputs.task import TaskPathReturn, TaskReturn class DAG(IOBase): """A Directed Acyclic Graph containing a list of tasks.""" - type: constr(regex='^DAG$') = 'DAG' + type: Literal['DAG'] = 'DAG' name: str = Field( ..., @@ -26,7 +27,7 @@ class DAG(IOBase): ) inputs: List[DAGInputs] = Field( - None, + default_factory=list, description='Inputs for the DAG.' ) @@ -42,7 +43,7 @@ class DAG(IOBase): ) outputs: List[DAGOutputs] = Field( - None, + default_factory=list, description='Outputs of the DAG that can be used by other DAGs.' ) @@ -90,17 +91,19 @@ def find_task_return( return out[0] - @validator('tasks') - def check_unique_names(cls, v): + @field_validator('tasks') + @classmethod + def check_unique_names(cls, v: List[DAGTask]) -> List[DAGTask]: """Check that all tasks have unique names.""" names = [task.name for task in v] duplicates = find_dup_items(names) - if len(duplicates) != 0: + if duplicates: raise ValueError(f'Duplicate names: {duplicates}') return v - @validator('tasks') - def check_dependencies(cls, v): + @field_validator('tasks') + @classmethod + def check_dependencies(cls, v: List[DAGTask]) -> List[DAGTask]: """Check that all task dependencies exist.""" task_names = [task.name for task in v] @@ -120,86 +123,70 @@ def check_dependencies(cls, v): return v - @validator('tasks') - def check_references(cls, v, values): - """Check that input and output references exist.""" - dag_inputs = values.get('inputs', []) + @field_validator('tasks', 'inputs', 'outputs') + @classmethod + def sort_list(cls, v: list) -> list: + """Sort the list of tasks, inputs and outputs by name""" + v.sort(key=lambda x: x.name) + return v - dag_input_names = set(d.name for d in dag_inputs) + @model_validator(mode='after') + def check_dag_references(self) -> 'DAG': + """Check references within the DAG.""" + if self.tasks is None: + # another validation has failed + return self + # check_references + dag_inputs = self.inputs or [] + dag_input_names = set(d.name for d in dag_inputs) exceptions = [] - for task in v: - if task.arguments is None: - continue - - # Check DAG input references - for arg in task.argument_by_ref_source('dag'): - if arg.from_.variable not in dag_input_names: - exceptions.append( - f'Invalid input reference variable: "{arg.from_.variable}" ' - f'in task "{task.name}"' - ) - - # Check DAG task inputs - for arg in task.argument_by_ref_source('task'): - try: - cls.find_task_return(tasks=v, reference=arg.from_) - except ValueError as error: - exceptions.append(f'tasks.{task.name}.{arg.name}: %s' % error) + for task in self.tasks: + if task.arguments is not None: + # Check DAG input references + for arg in task.argument_by_ref_source('dag'): + if arg.from_.variable not in dag_input_names: + exceptions.append( + f'Invalid input reference variable: "{arg.from_.variable}" ' + f'in task "{task.name}"' + ) + + # Check DAG task inputs + for arg in task.argument_by_ref_source('task'): + try: + self.find_task_return(tasks=self.tasks, reference=arg.from_) + except ValueError as error: + exceptions.append(f'tasks.{task.name}.{arg.name}: {error}') + + # check_template_name + plugin = task.template.split('/')[0] + if plugin == self.name: + raise ValueError('Task cannot refer to its own DAG as a template.') if exceptions: raise ValueError('\n '.join(exceptions)) - return v - - @validator('tasks', each_item=True) - def check_template_name(cls, v, values): - """Check that a task name does not refer to itself in a template.""" - name = values.get('name') - - plugin = v.template.split('/')[0] + # check_dag_outputs + if self.outputs is None: + return self - assert plugin != name, \ - ValueError('Task cannot refer to its own DAG as a template.') - - return v - - @validator('tasks') - def sort_list(cls, v): - """Sort the list of tasks by name""" - v.sort(key=lambda x: x.name) - return v - - @root_validator - def check_dag_outputs(cls, values): - """Check DAG outputs refer to existing Task outputs. - - It can't be a normal validator because of the order in which the inputs get - assigned in Pydantic when a class is a subclass from another. - """ - if 'tasks' not in values or 'outputs' not in values: - # another validation has failed - return values - - tasks = values['tasks'] - outputs = values['outputs'] exceptions = [] - for out in outputs: + for out in self.outputs: if isinstance(out.from_, TaskReference): try: - cls.find_task_return(tasks=tasks, reference=out.from_) + self.find_task_return(tasks=self.tasks, reference=out.from_) except ValueError as error: - exceptions.append(f'outputs.{out.name}: %s' % error) + exceptions.append(f'outputs.{out.name}: {error}') elif isinstance(out.from_, (FolderReference, FileReference)): # the validation will be checked when the Pydantic model is being loaded pass if exceptions: - raise ValueError(' \n'.join(exceptions)) + raise ValueError('\n '.join(exceptions)) - return values + return self def get_task(self, name): """Get task by name. diff --git a/queenbee/recipe/dependency.py b/queenbee/recipe/dependency.py index 8fa6aec5..b87a7c09 100644 --- a/queenbee/recipe/dependency.py +++ b/queenbee/recipe/dependency.py @@ -1,8 +1,7 @@ """Queenbee dependency class.""" -from typing import Dict +from typing import Dict, Literal, Union from enum import Enum -from pydantic import Field, constr - +from pydantic import Field from ..base.basemodel import BaseModel from ..base.request import make_request, urljoin, resolve_local_source @@ -17,7 +16,7 @@ class DependencyKind(str, Enum): class Dependency(BaseModel): """Configuration to fetch a Recipe or Plugin that another Recipe depends on.""" - type: constr(regex='^Dependency$') = 'Dependency' + type: Literal['Dependency'] = 'Dependency' kind: DependencyKind = Field( ..., @@ -31,14 +30,14 @@ class Dependency(BaseModel): ' in your resource. Use an alias if this is not the case.' ) - digest: str = Field( + digest: Union[str, None] = Field( None, alias='hash', description='The digest hash of the dependency when retrieved from its source.' ' This is locked when the resource dependencies are downloaded.' ) - alias: str = Field( + alias: Union[str, None] = Field( None, description='An optional alias to refer to this dependency. Useful if the name' ' is already used somewhere else.' @@ -102,7 +101,7 @@ def _fetch_index(self, auth_header: Dict[str, str] = {}): res = make_request(url=url, auth_header=auth_header) raw_bytes = res.read() - return RepositoryIndex.parse_raw(raw_bytes) + return RepositoryIndex.model_validate_json(raw_bytes) def fetch(self, verify_digest: bool = True, auth_header: Dict[str, str] = {}) -> 'PackageVersion': """Fetch the dependency from its source diff --git a/queenbee/recipe/recipe.py b/queenbee/recipe/recipe.py index 0492ef70..b1ce2f62 100644 --- a/queenbee/recipe/recipe.py +++ b/queenbee/recipe/recipe.py @@ -5,11 +5,10 @@ """ import os import shutil -import json -from typing import List, Union, Dict +from typing import List, Union, Dict, Literal import yaml -from pydantic import Field, validator, root_validator, constr +from pydantic import Field, field_validator, model_validator, ValidationInfo from ..base.basemodel import BaseModel from ..base.metadata import MetaData @@ -25,7 +24,7 @@ class TemplateFunction(Function): """Function template.""" - type: constr(regex='^TemplateFunction$') = 'TemplateFunction' + type: Literal['TemplateFunction'] = 'TemplateFunction' config: PluginConfig = Field( ..., @@ -49,23 +48,23 @@ def from_plugin(cls, plugin: Plugin) -> list: input_dict['type'] = 'TemplateFunction' input_dict['name'] = f'{plugin.__hash__}/{function.name}' input_dict['config'] = plugin.config.to_dict() - functions.append(cls.parse_obj(input_dict)) + functions.append(cls.model_validate(input_dict)) return functions class Recipe(BaseModel): """A Queenbee Recipe""" - api_version: constr(regex='^v1beta1$') = Field('v1beta1', readOnly=True) + api_version: Literal['v1beta1'] = Field('v1beta1', json_schema_extra={'readOnly': True}) - type: constr(regex='^Recipe$') = 'Recipe' + type: Literal['Recipe'] = 'Recipe' - metadata: MetaData = Field( + metadata: Union[MetaData, None] = Field( None, description='Recipe metadata information.' ) - dependencies: List[Dependency] = Field( + dependencies: Union[List[Dependency], None] = Field( None, description='A list of plugins and other recipes this recipe depends on.' ) @@ -118,18 +117,21 @@ def from_folder(cls, folder_path: str): flow = [] - for dag_path in os.listdir(flow_path): - - with open(os.path.join(flow_path, dag_path), 'r') as f: + dag_files = sorted(os.listdir(flow_path)) + for f_name in dag_files: + with open(os.path.join(flow_path, f_name), 'r') as f: flow.append(yaml.load(f, yaml.FullLoader)) recipe['flow'] = flow - return cls.parse_obj(recipe) + return cls.model_validate(recipe) - @validator('flow', allow_reuse=True) - def check_entrypoint(cls, v): + @field_validator('flow') + @classmethod + def check_entrypoint(cls, v: List[DAG]) -> List[DAG]: """Check the `main` DAG exists in the flow""" + if v is None: + return v for dag in v: if dag.name == 'main': return v @@ -138,40 +140,41 @@ def check_entrypoint(cls, v): raise ValueError('No DAG with name "main" found in flow') - @validator('flow', allow_reuse=True) - def sort_list(cls, v): + @field_validator('flow') + @classmethod + def sort_list(cls, v: List[DAG]) -> List[DAG]: """Sort the list of DAGs by name""" + if v is None: + return v v.sort(key=lambda x: x.name) return v - @validator('flow', allow_reuse=True) - def check_dag_names(cls, v, values): + @field_validator('flow') + @classmethod + def check_dag_names(cls, v: List[DAG], info: ValidationInfo) -> List[DAG]: """Check DAG names do not overlap with dependency names""" - op_names = [dep.ref_name for dep in values.get('dependencies')] + op_names = [dep.ref_name for dep in info.data.get('dependencies', [])] for dag in v: assert dag.name not in op_names, \ - ValueError( - f'DAG name {dag.name} cannot be used because a recipe dependency' - f' already uses it' - ) + f'DAG name {dag.name} cannot be used because a recipe dependency' \ + f' already uses it' return v - @validator('flow', allow_reuse=True) - def check_template_dependency_names(cls, v, values): + @field_validator('flow') + @classmethod + def check_template_dependency_names(cls, v: List[DAG], info: ValidationInfo) -> List[DAG]: """Check all DAG template names exist in dependencies or other DAGs""" - op_names = [dep.ref_name for dep in values.get('dependencies')] - op_names.extend([dag.name for dag in v]) + op_names = [dep.ref_name for dep in info.data.get('dependencies', [])] + op_names.extend([dag.name for dag in v or []]) for dag in v: for template in dag.templates: plugin = template.split('/')[0] assert plugin in op_names, \ - ValueError( - f'Cannot use template {template} from DAG {dag.name} because no' - f' dependency or other DAG matching that name was found.' - ) + f'Cannot use template {template} from DAG {dag.name} because no' \ + f' dependency or other DAG matching that name was found.' return v @@ -380,7 +383,7 @@ class BakedRecipe(Recipe): A Baked Recipe contains all the templates referred to in the DAG within a templates list. """ - type: constr(regex='^BakedRecipe$') = 'BakedRecipe' + type: Literal['BakedRecipe'] = 'BakedRecipe' digest: str @@ -407,7 +410,7 @@ def from_recipe(cls, recipe: Recipe, config: Config = Config()): BakedRecipe -- A baked recipe """ - recipe = recipe.copy(deep=True) + recipe = recipe.model_copy(deep=True) digest = recipe.__hash__ @@ -451,7 +454,7 @@ def from_recipe(cls, recipe: Recipe, config: Config = Config()): input_dict['templates'] = [template.to_dict() for template in templates] - return cls.parse_obj(input_dict) + return cls.model_validate(input_dict) @classmethod def from_folder(cls, folder_path: str, refresh_deps: bool = True, config: Config = Config()): @@ -545,7 +548,7 @@ def from_folder(cls, folder_path: str, refresh_deps: bool = True, config: Config input_dict['templates'] = [template.to_dict() for template in templates] - return cls.parse_obj(input_dict) + return cls.model_validate(input_dict) @classmethod def replace_template_refs( @@ -622,16 +625,20 @@ def replace_template_refs( return dags - @validator('flow', allow_reuse=True) - def check_template_dependency_names(cls, v, values): + @field_validator('flow') + @classmethod + def check_template_dependency_names(cls, v: List[DAG], info: ValidationInfo) -> List[DAG]: """Overwrite test function from inherited class""" return v - @validator('templates') - def remove_duplicates(cls, v): + @field_validator('templates') + @classmethod + def remove_duplicates(cls, v: List[Union[TemplateFunction, DAG]]) -> List[Union[TemplateFunction, DAG]]: """Remove duplicated templates by name""" temp_names = [] templates = [] + if v is None: + return templates for template in v: if template.name not in temp_names: temp_names.append(template.name) @@ -639,20 +646,20 @@ def remove_duplicates(cls, v): return templates - @root_validator - def check_inputs(cls, values): + @model_validator(mode='after') + def check_inputs(self): """Check DAG Task inputs match template inputs""" - flow = values.get('flow') - templates = values.get('templates') + flow = self.flow + templates = self.templates all_templates = templates + flow for dag in flow: for task in dag.tasks: - template = cls.template_by_name(all_templates, task.template) + template = self.template_by_name(all_templates, task.template) task.check_template(template) - return values + return self @property def root_dag(self) -> DAG: @@ -687,9 +694,9 @@ class RecipeInterface(BaseModel): Recipe information only includes metadata, source, inputs and outputs of a Recipe. This object is useful for creating user interface for Recipes. """ - api_version: constr(regex='^v1beta1$') = Field('v1beta1', readOnly=True) + api_version: Literal['v1beta1'] = Field('v1beta1', json_schema_extra={'readOnly': True}) - type: constr(regex='^RecipeInterface$') = 'RecipeInterface' + type: Literal['RecipeInterface'] = 'RecipeInterface' metadata: MetaData = Field( ..., @@ -711,9 +718,10 @@ class RecipeInterface(BaseModel): description='A list of recipe outputs.' ) - @validator('inputs', 'outputs') - def create_empty_list(cls, v): - return [] if not v else v + @field_validator('inputs', 'outputs', mode='before') + @classmethod + def create_empty_list(cls, v: list) -> list: + return [] if v is None else v @classmethod def from_recipe(cls, recipe: Union[Recipe, BakedRecipe], source: str = None): diff --git a/queenbee/recipe/task.py b/queenbee/recipe/task.py index 85d9fbac..3f8b5d67 100644 --- a/queenbee/recipe/task.py +++ b/queenbee/recipe/task.py @@ -1,6 +1,6 @@ """DAGTask module.""" -from typing import Union, List -from pydantic import Field, validator, constr +from typing import Union, List, Literal +from pydantic import Field, field_validator, ValidationInfo from ..base.basemodel import BaseModel from ..io.common import find_io_by_name, find_dup_items @@ -19,7 +19,7 @@ class DAGTaskLoop(BaseModel): This will run the template provided multiple times and in parallel relative to an input or task parameter which should be a list. """ - type: constr(regex='^DAGTaskLoop$') = 'DAGTaskLoop' + type: Literal['DAGTaskLoop'] = 'DAGTaskLoop' from_: Union[InputReference, TaskReference, ValueListReference] = Field( None, @@ -31,7 +31,7 @@ class DAGTaskLoop(BaseModel): class DAGTask(BaseModel): """A single task in a DAG flow.""" - type: constr(regex='^DAGTask$') = 'DAGTask' + type: Literal['DAGTask'] = 'DAGTask' name: str = Field( ..., @@ -44,39 +44,40 @@ class DAGTask(BaseModel): 'must be available in the dependencies.' ) - needs: List[str] = Field( + needs: Union[List[str], None] = Field( None, description='List of DAG tasks that this task depends on and needs to be' ' executed before this task.' ) arguments: List[TaskArguments] = Field( - None, + default_factory=list, description='The input arguments for this task.' ) - loop: DAGTaskLoop = Field( + loop: Union[DAGTaskLoop, None] = Field( None, description='Loop configuration for this task.' ) - sub_folder: str = Field( + sub_folder: Union[str, None] = Field( None, description='A path relative to the current folder context where artifacts ' 'should be saved. This is useful when performing a loop or invoking another ' 'workflow and wanting to save results in a specific sub_folder.' ) - + returns: List[TaskReturns] = Field( - None, + default_factory=list, description='List of task returns.' ) - @validator('loop', always=True) - def check_item_references(cls, v, values): + @field_validator('loop', mode='before') + @classmethod + def check_item_references(cls, v: 'DAGTaskLoop', info: ValidationInfo) -> 'DAGTaskLoop': if v is None: - arguments = values.get('arguments') - if arguments is None: + arguments = info.data.get('arguments') + if not arguments: return v artifacts = [arg for arg in arguments if arg.is_artifact] assert len(cls.parameters_by_ref_source(artifacts, 'item')) == 0, \ @@ -86,11 +87,12 @@ def check_item_references(cls, v, values): ) return v - @validator('sub_folder', always=True) - def check_references(cls, v, values): - loop = values.get('loop') - arguments = values.get('arguments') - + @field_validator('sub_folder', mode='before') + @classmethod + def check_references(cls, v: str, info: ValidationInfo) -> str: + loop = info.data.get('loop') + arguments = info.data.get('arguments') + if v is None: return v @@ -110,69 +112,72 @@ def check_references(cls, v, values): find_io_by_name(arguments, ref_list[1]) return v - @validator('arguments') - def check_duplicate_argument_name(cls, v, values): - v = [] if v is None else v + @field_validator('arguments') + @classmethod + def check_duplicate_argument_name(cls, v: List[TaskArguments], info: ValidationInfo) -> List[TaskArguments]: names = [arg.name for arg in v] dup_arg = find_dup_items(names) if dup_arg: raise ValueError( - f'Duplicate argument name in task "{values["name"]}": {dup_arg}. ' + f'Duplicate argument name in task "{info.data.get("name")}": {dup_arg}. ' f'Each task argument name should be unique.' ) return v - @validator('returns', always=True) - def check_duplicate_return_name(cls, v, values): - v = [] if v is None else v + @field_validator('returns') + @classmethod + def check_duplicate_return_name(cls, v: List[TaskReturns], info: ValidationInfo) -> List[TaskReturns]: names = [arg.name for arg in v] dup_arg = find_dup_items(names) if dup_arg: raise ValueError( - f'Duplicate return name in task "{values["name"]}": {dup_arg}. ' + f'Duplicate return name in task "{info.data.get("name")}": {dup_arg}. ' f'Each task return name should be unique.' ) return v - @validator('arguments', always=True) - def check_referenced_values(cls, v, values): - if v is None: - return [] + @field_validator('arguments') + @classmethod + def check_referenced_values(cls, v: List[TaskArguments], info: ValidationInfo) -> List[TaskArguments]: deps = [] for arg in v: try: - dep = arg.from_.name + # only check for task references + if isinstance(arg.from_, TaskReference): + dep = arg.from_.name + deps.append(dep) except AttributeError: # non-referenced arguments pass - else: - deps.append(dep) - needs = values.get('needs', []) + needs = info.data.get('needs', []) missing = [d for d in deps if d not in needs] if missing: raise ValueError( - f'Missing task name(s) from `needs` field for task {values["name"]}:' + f'Missing task name(s) from `needs` field for task {info.data.get("name")}:' f'\n\t-> {missing}' ) return v - @validator('loop') - def check_loop_referenced_task(cls, v, values): + @field_validator('loop') + @classmethod + def check_loop_referenced_task(cls, v: 'DAGTaskLoop', info: ValidationInfo) -> 'DAGTaskLoop': + if v is None: + return v try: dep = v.from_.name except AttributeError: # non-referenced arguments return v - needs = values.get('needs', []) + needs = info.data.get('needs', []) if dep not in needs: raise ValueError( - f'Missing loop reference from `needs` field for task {values["name"]}:' + f'Missing loop reference from `needs` field for task {info.data.get("name")}:' f'\n\t-> {dep}' ) diff --git a/queenbee/repository/index.py b/queenbee/repository/index.py index 0db699d9..4c9c966c 100644 --- a/queenbee/repository/index.py +++ b/queenbee/repository/index.py @@ -1,8 +1,8 @@ import os -from typing import List, Union, Dict +from typing import List, Union, Dict, Literal, Optional from datetime import datetime -from pydantic import Field, root_validator, validator, constr - +from pydantic import Field, model_validator, field_validator + from ..base.basemodel import BaseModel from ..plugin import Plugin @@ -13,9 +13,9 @@ class RepositoryMetadata(BaseModel): - type: constr(regex='^RepositoryMetadata$') = 'RepositoryMetadata' + type: Literal['RepositoryMetadata'] = 'RepositoryMetadata' - name: str = Field( + name: Union[str, None] = Field( None, description='The name of the repository' ) @@ -25,7 +25,7 @@ class RepositoryMetadata(BaseModel): description='A short description of the repository' ) - source: str = Field( + source: Union[str, None] = Field( None, description='The source path (url or local) to the repository' ) @@ -43,11 +43,11 @@ class RepositoryMetadata(BaseModel): class RepositoryIndex(BaseModel): """A searchable index for a Queenbee Plugin and Recipe repository""" - api_version: constr(regex='^v1beta1$') = Field('v1beta1', readOnly=True) - - type: constr(regex='^RepositoryIndex$') = 'RepositoryIndex' + api_version: Literal['v1beta1'] = Field('v1beta1', json_schema_extra={'readOnly': True}) - generated: datetime = Field( + type: Literal['RepositoryIndex'] = 'RepositoryIndex' + + generated: Union[datetime, None] = Field( None, description='The timestamp at which the index was generated' ) @@ -69,47 +69,45 @@ class RepositoryIndex(BaseModel): ' list of recipesversions' ) - @validator('plugin') - def set_plugin_type(cls, v): + @field_validator('plugin') + @classmethod + def set_plugin_type(cls, v: Dict[str, List[PackageVersion]]) -> Dict[str, List[PackageVersion]]: for _, package in v.items(): for pv in package: pv.kind = 'plugin' return v - @validator('recipe') - def set_recipe_type(cls, v): + @field_validator('recipe') + @classmethod + def set_recipe_type(cls, v: Dict[str, List[PackageVersion]]) -> Dict[str, List[PackageVersion]]: for _, package in v.items(): for pv in package: pv.kind = 'recipe' return v - @root_validator - def metadata_counts(cls, values): - values['metadata'].plugin_count = len(values.get('plugin')) - values['metadata'].recipe_count = len(values.get('recipe')) - - return values + @model_validator(mode='after') + def after_validation(self) -> 'RepositoryIndex': + self.metadata.plugin_count = len(self.plugin) + self.metadata.recipe_count = len(self.recipe) - @root_validator - def check_slugs(cls, values): - name = values.get('metadata').name + name = self.metadata.name if name is None: - return values + return self - cls.add_slugs( + self.add_slugs( root=name, - packages=values.get('plugin') + packages=self.plugin ) - cls.add_slugs( + self.add_slugs( root=name, - packages=values.get('recipe') + packages=self.recipe ) - return values + return self @classmethod def from_folder(cls, folder_path): @@ -124,7 +122,7 @@ def from_folder(cls, folder_path): Returns: RepositoryIndex -- An index generated from packages in the folder """ - index = cls.parse_obj({}) + index = cls.model_validate({}) head, tail = os.path.split(folder_path) @@ -134,7 +132,7 @@ def from_folder(cls, folder_path): recipes_folder = os.path.join(folder_path, 'recipes') if os.path.exists(plugins_folder): - for package in os.listdir(plugins_folder): + for package in sorted(os.listdir(plugins_folder)): package_path = os.path.join(folder_path, 'plugins', package) resource_version = PackageVersion.from_package(package_path) resource_version.url = \ @@ -142,7 +140,7 @@ def from_folder(cls, folder_path): index.index_plugin_version(resource_version) if os.path.exists(recipes_folder): - for package in os.listdir(recipes_folder): + for package in sorted(os.listdir(recipes_folder)): package_path = os.path.join(folder_path, 'recipes', package) resource_version = PackageVersion.from_package(package_path) resource_version.url = \ @@ -355,7 +353,7 @@ def merge_folder(self, folder_path, overwrite: bool = False, skip: bool = False) Raises: ValueError: Resource version already exists or is invalid """ - for package in os.listdir(os.path.join(folder_path, 'plugins')): + for package in sorted(os.listdir(os.path.join(folder_path, 'plugins'))): package_path = os.path.join(folder_path, 'plugins', package) resource_version = PackageVersion.from_package(package_path) resource_version.url = \ @@ -368,7 +366,7 @@ def merge_folder(self, folder_path, overwrite: bool = False, skip: bool = False) continue raise error - for package in os.listdir(os.path.join(folder_path, 'recipes')): + for package in sorted(os.listdir(os.path.join(folder_path, 'recipes'))): package_path = os.path.join(folder_path, 'recipes', package) resource_version = PackageVersion.from_package(package_path) resource_version.url = \ @@ -500,33 +498,24 @@ def search( return packages - def json(self, *args, **kwargs): + def model_dump(self, *args, **kwargs): """Overwrite the BaseModel json method to exclude certain keys The objective is to remove the readme, license and manifest keys which are not needed in a serialized index object. """ - exclude_keys = { - 'plugin': {}, - 'recipe': {}, - } - - for key in self.plugin: - exclude_keys['plugin'][key] = { - '__all__': { - 'readme', - 'license', - 'manifest' - } - } - - for key in self.recipe: - exclude_keys['recipe'][key] = { - '__all__': { - 'readme', - 'license', - 'manifest' - } - } - - return super(RepositoryIndex, self).json(exclude=exclude_keys, *args, **kwargs) + # remove keys that are not needed for serialization from kwargs if they exist + # to avoid unexpected keyword argument error in super().model_dump + kwargs.pop('exclude', None) + + dump = super().model_dump(*args, **kwargs) + + for key in ('plugin', 'recipe'): + if dump.get(key): + for _, package_list in dump[key].items(): + for package in package_list: + package.pop('readme', None) + package.pop('license', None) + package.pop('manifest', None) + + return dump diff --git a/queenbee/repository/package.py b/queenbee/repository/package.py index 5d74329d..d5157672 100644 --- a/queenbee/repository/package.py +++ b/queenbee/repository/package.py @@ -4,9 +4,9 @@ from io import BytesIO from datetime import datetime from tarfile import TarInfo, TarFile -from typing import Union, Tuple, Dict +from typing import Union, Tuple, Dict, Literal -from pydantic import Field, constr +from pydantic import Field from ..plugin import Plugin from ..recipe import Recipe, BakedRecipe @@ -39,7 +39,7 @@ class PackageVersion(MetaData): A MetaData object to distinguish a specific package version within a repository index. """ - type: constr(regex='^PackageVersion$') = 'PackageVersion' + type: Literal['PackageVersion'] = 'PackageVersion' url: str @@ -47,7 +47,7 @@ class PackageVersion(MetaData): digest: str - slug: str = Field( + slug: Union[str, None] = Field( None, description='A slug of the repository name and the package name.' ) @@ -57,12 +57,12 @@ class PackageVersion(MetaData): description='The type of Queenbee package (ie: recipe or plugin)' ) - readme: str = Field( + readme: Union[str, None] = Field( None, description='The README file string for this package' ) - manifest: Union[Recipe, Plugin] = Field( + manifest: Union[Recipe, Plugin, None] = Field( None, description="The package Recipe or Plugin manifest" ) @@ -108,7 +108,7 @@ def from_resource( if include_manifest: input_dict['manifest'] = resource.to_dict() - return cls.parse_obj(input_dict) + return cls.model_validate(input_dict) @classmethod def pack_tar(cls, @@ -144,10 +144,10 @@ def pack_tar(cls, fileobj=file_object, ) - resource_bytes = bytes(resource.json( - by_alias=True, exclude_unset=False), 'utf-8') - resource_version_bytes = bytes(resource_version.json( - by_alias=True, exclude_unset=False), 'utf-8') + resource_bytes = bytes(resource.model_dump_json( + by_alias=True), 'utf-8') + resource_version_bytes = bytes(resource_version.model_dump_json( + by_alias=True), 'utf-8') add_to_tar( tar=tar, @@ -205,7 +205,7 @@ def unpack_tar( f' {read_digest}' ) elif member.name == 'version.json': - version = cls.parse_raw(tar.extractfile(member).read()) + version = cls.model_validate_json(tar.extractfile(member).read()) elif member.name == 'README.md': readme_string = tar.extractfile(member).read().decode('utf-8') @@ -216,11 +216,11 @@ def unpack_tar( ) try: - manifest = Plugin.parse_raw(manifest_bytes) + manifest = Plugin.model_validate_json(manifest_bytes) version.kind = 'plugin' except Exception as error: try: - manifest = Recipe.parse_raw(manifest_bytes) + manifest = Recipe.model_validate_json(manifest_bytes) version.kind = 'recipe' except Exception as error: raise ValueError( diff --git a/tests/base/io_test.py b/tests/base/io_test.py index 5067f517..2cc6d9e6 100644 --- a/tests/base/io_test.py +++ b/tests/base/io_test.py @@ -36,7 +36,7 @@ def test_from_json(self, valid_dict): instance = self.klass.from_file(test_file_path) - assert instance == self.klass.parse_obj(valid_dict) + assert instance == self.klass.model_validate(valid_dict) def test_from_yaml(self, valid_dict): test_file_path = self.generate_test_file('valid.yaml') @@ -46,10 +46,10 @@ def test_from_yaml(self, valid_dict): instance = self.klass.from_file(test_file_path) - assert instance == self.klass.parse_obj(valid_dict) + assert instance == self.klass.model_validate(valid_dict) def test_to_yaml(self, valid_dict): - valid_instance = self.klass.parse_obj(valid_dict) + valid_instance = self.klass.model_validate(valid_dict) loc_file = self.generate_test_file('valid.yaml') @@ -58,12 +58,12 @@ def test_to_yaml(self, valid_dict): with open(loc_file) as inf: obj = yaml.safe_load(inf.read()) - assert obj == valid_instance.to_dict() + assert obj == valid_instance.to_dict(exclude_none=True) assert self.klass.from_file( loc_file).__hash__ == valid_instance.__hash__ def test_to_json(self, valid_dict): - valid_instance = self.klass.parse_obj(valid_dict) + valid_instance = self.klass.model_validate(valid_dict) loc_file = self.generate_test_file('valid.json') @@ -72,6 +72,6 @@ def test_to_json(self, valid_dict): with open(loc_file) as inf: obj = json.load(inf) - assert obj == valid_instance.to_dict() + assert obj == valid_instance.to_dict(exclude_none=True) assert self.klass.from_file( loc_file).__hash__ == valid_instance.__hash__ diff --git a/tests/base/value_error.py b/tests/base/value_error.py index 64a81ad3..f4ef6667 100644 --- a/tests/base/value_error.py +++ b/tests/base/value_error.py @@ -1,7 +1,9 @@ +from pydantic_core import ValidationError import pytest import os import yaml import json +import re from ._base import BaseTestClass @@ -62,5 +64,36 @@ def test_raise_value_error(self, invalid_file): self.klass.from_file(invalid_file) def test_value_error_message(self, invalid_file, error_message): - with pytest.raises(ValueError, match=error_message): + # 1. Prepare the Regex + + # Strip the legacy (type=...) suffix + pattern = re.sub(r'\\\(type=.*\)$', '', error_message.strip()) + + # FIX: Remove the greedy "match everything" prefix `((.|\n)*)`. + # Since we use re.search(), we don't need to explicitly match the start of the string. + # We also strip leading whitespace to ignore the old indentation. + pattern = pattern.replace(r"((.|\n)*)", "").strip() + + # 2. Catch ValueError + with pytest.raises(ValueError) as excinfo: self.klass.from_file(invalid_file) + + # 3. Unwrap Pydantic error + wrapped_error = excinfo.value.args[0] + + if hasattr(wrapped_error, 'errors'): + # Extract plain messages (e.g., "Assertion failed, Cannot use template...") + actual_messages = [e['msg'] for e in wrapped_error.errors()] + + # Check if the cleaned pattern exists inside any of the error messages + match_found = any(re.search(pattern, msg) for msg in actual_messages) + + assert match_found, ( + f"Regex pattern '{pattern}' did not match any error messages.\n" + f"Found messages: {actual_messages}" + ) + else: + # Fallback for generic ValueErrors + actual_error = str(excinfo.value) + assert re.search(pattern, actual_error), \ + f"Regex '{pattern}' not found in error '{actual_error}'" diff --git a/tests/job/job_test.py b/tests/job/job_test.py index bc6e7da5..8f69160c 100644 --- a/tests/job/job_test.py +++ b/tests/job/job_test.py @@ -1,7 +1,7 @@ from typing import List import pytest -from pydantic.main import ValidationError +from pydantic import ValidationError from queenbee.io.artifact_source import HTTP from queenbee.io.inputs.dag import DAGInputs, DAGPathInput, DAGStringInput from queenbee.job import Job @@ -71,7 +71,7 @@ def all_inputs(required_inputs, optional_inputs): def test_duplicated_job_arguments(): with pytest.raises(ValidationError) as validation_err: - Job.parse_obj({ + Job.model_validate({ 'source': 'https://example.com/registries/recipe/daylight-factor/latest', 'arguments': [[ { @@ -98,15 +98,15 @@ def test_duplicated_job_arguments(): ]] }) - assert validation_err.value.errors() == [{ - 'loc': ('arguments', 0), - 'msg': 'duplicate argument name sensor-grid-count', - 'type': 'value_error' - }] + errors = validation_err.value.errors() + assert len(errors) == 1 + error = errors[0] + assert error['type'] == 'value_error' + assert 'duplicate argument name sensor-grid-count' in error['msg'] def test_valid_job_arguments(all_inputs: List[DAGInputs]): - job: Job = Job.parse_obj({ + job: Job = Job.model_validate({ 'source': 'https://example.com/registries/recipe/daylight-factor/latest', 'arguments': [[ { @@ -142,7 +142,7 @@ def test_valid_job_arguments(all_inputs: List[DAGInputs]): def test_required_job_arguments_missing(all_inputs: List[DAGInputs]): - job: Job = Job.parse_obj({ + job: Job = Job.model_validate({ 'source': 'https://example.com/registries/recipe/daylight-factor/latest', 'arguments': [[ { @@ -176,25 +176,23 @@ def test_required_job_arguments_missing(all_inputs: List[DAGInputs]): with pytest.raises(ValidationError) as validation_err: job.validate_arguments(all_inputs) - assert validation_err.value.errors() == [ - { - 'loc': ('arguments', 0), - 'msg': 'missing required argument required-parameter-input', - 'type': 'value_error' - }, { - 'loc': ('arguments', 0), - 'msg': 'missing required argument required-artifact-input', - 'type': 'value_error' - }, { - 'loc': ('arguments', 1), - 'msg': 'missing required argument required-parameter-input', - 'type': 'value_error' - } - ] + errors = validation_err.value.errors() + assert len(errors) == 3 + + error_msgs = [err['msg'] for err in errors] + + assert 'missing required argument required-parameter-input' in error_msgs[0] + assert errors[0]['loc'] == ('arguments', 0) + + assert 'missing required argument required-artifact-input' in error_msgs[1] + assert errors[1]['loc'] == ('arguments', 0) + + assert 'missing required argument required-parameter-input' in error_msgs[2] + assert errors[2]['loc'] == ('arguments', 1) def test_invalid_job_arguments_type(all_inputs: List[DAGInputs]): - job: Job = Job.parse_obj({ + job: Job = Job.model_validate({ 'source': 'https://example.com/registries/recipe/daylight-factor/latest', 'arguments': [[ { @@ -235,22 +233,20 @@ def test_invalid_job_arguments_type(all_inputs: List[DAGInputs]): with pytest.raises(ValidationError) as validation_err: job.validate_arguments(all_inputs) - assert validation_err.value.errors() == [ - { - 'loc': ('arguments', 0), - 'msg': 'invalid argument type for optional-parameter-input, should be "JobArgument"', - 'type': 'value_error' - }, - { - 'loc': ('arguments', 1), - 'msg': 'invalid argument type for required-artifact-input, should be "JobPathArgument"', - 'type': 'value_error' - }, - ] + errors = validation_err.value.errors() + assert len(errors) == 2 + + error_msgs = [err['msg'] for err in errors] + + assert 'invalid argument type for optional-parameter-input, should be "JobArgument"' in error_msgs[0] + assert errors[0]['loc'] == ('arguments', 0) + + assert 'invalid argument type for required-artifact-input, should be "JobPathArgument"' in error_msgs[1] + assert errors[1]['loc'] == ('arguments', 1) def test_populate_optional_arguments(optional_inputs: List[DAGInputs]): - job: Job = Job.parse_obj({ + job: Job = Job.model_validate({ 'source': 'https://example.com/registries/recipe/daylight-factor/latest', 'arguments': [[ { @@ -272,7 +268,7 @@ def test_populate_optional_arguments(optional_inputs: List[DAGInputs]): job.populate_default_arguments(optional_inputs) - expected_job = Job.parse_obj({ + expected_job = Job.model_validate({ 'source': 'https://example.com/registries/recipe/daylight-factor/latest', 'arguments': [[ {