diff --git a/packages/models-library/src/models_library/api_schemas_payments/errors.py b/packages/models-library/src/models_library/api_schemas_payments/errors.py index eaeba92aab1..362482772f7 100644 --- a/packages/models-library/src/models_library/api_schemas_payments/errors.py +++ b/packages/models-library/src/models_library/api_schemas_payments/errors.py @@ -1,7 +1,7 @@ -from pydantic.errors import PydanticErrorMixin +from common_library.errors_classes import OsparcErrorMixin -class _BaseRpcApiError(PydanticErrorMixin, ValueError): +class _BaseRpcApiError(OsparcErrorMixin, ValueError): @classmethod def get_full_class_name(cls) -> str: # Can be used as unique code identifier diff --git a/packages/postgres-database/src/simcore_postgres_database/models/products.py b/packages/postgres-database/src/simcore_postgres_database/models/products.py index 03e137528ec..831db2610d9 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/products.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/products.py @@ -6,8 +6,14 @@ """ import json +import sys from typing import Literal, TypedDict +if sys.version_info >= (3, 12): + from typing import TypedDict +else: + from typing_extensions import TypedDict + import sqlalchemy as sa from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.sql import func diff --git a/packages/settings-library/src/settings_library/base.py b/packages/settings-library/src/settings_library/base.py index c3f0e103e7a..54f2ac37c79 100644 --- a/packages/settings-library/src/settings_library/base.py +++ b/packages/settings-library/src/settings_library/base.py @@ -102,9 +102,6 @@ def __pydantic_init_subclass__(cls, **kwargs: Any): and issubclass(field_type, BaseCustomSettings) ): if auto_default_from_env: - assert field.default is PydanticUndefined - assert field.default_factory is None - # Transform it into something like `Field(default_factory=create_settings_from_env(field))` field.default_factory = _create_settings_from_env(name, field) field.default = None diff --git a/services/web/server/requirements/_base.in b/services/web/server/requirements/_base.in index 8d5ba7d34d8..308a1604cb3 100644 --- a/services/web/server/requirements/_base.in +++ b/services/web/server/requirements/_base.in @@ -9,6 +9,7 @@ # - Added as constraints instead of requirements in order to avoid polluting base.txt # - Will be installed when prod.txt or dev.txt # +--requirement ../../../../packages/common-library/requirements/_base.in --requirement ../../../../packages/models-library/requirements/_base.in --requirement ../../../../packages/postgres-database/requirements/_base.in --requirement ../../../../packages/settings-library/requirements/_base.in diff --git a/services/web/server/requirements/_base.txt b/services/web/server/requirements/_base.txt index b7d14fef70a..2b23fdba098 100644 --- a/services/web/server/requirements/_base.txt +++ b/services/web/server/requirements/_base.txt @@ -26,17 +26,30 @@ aiofiles==0.8.0 # -r requirements/_base.in aiohttp==3.8.5 # via + # -c requirements/../../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../requirements/constraints.txt # -c requirements/../../../../requirements/constraints.txt @@ -71,6 +84,8 @@ alembic==1.8.1 # via # -r requirements/../../../../packages/postgres-database/requirements/_base.in # -r requirements/../../../../packages/simcore-sdk/requirements/../../../packages/postgres-database/requirements/_base.in +annotated-types==0.7.0 + # via pydantic anyio==4.3.0 # via # fast-depends @@ -103,17 +118,30 @@ captcha==0.5.0 # via -r requirements/_base.in certifi==2023.7.22 # via + # -c requirements/../../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../requirements/constraints.txt # -c requirements/../../../../requirements/constraints.txt @@ -128,17 +156,30 @@ click==8.1.3 # via typer cryptography==41.0.7 # via + # -c requirements/../../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../requirements/constraints.txt # -c requirements/../../../../requirements/constraints.txt @@ -152,7 +193,7 @@ deprecated==1.2.14 # opentelemetry-semantic-conventions dnspython==2.2.1 # via email-validator -email-validator==1.2.1 +email-validator==2.2.0 # via pydantic et-xmlfile==1.1.0 # via openpyxl @@ -193,17 +234,30 @@ jinja-app-loader==1.0.2 # via -r requirements/_base.in jinja2==3.1.2 # via + # -c requirements/../../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../requirements/constraints.txt # -c requirements/../../../../requirements/constraints.txt @@ -226,17 +280,30 @@ lazy-object-proxy==1.7.1 # via openapi-core mako==1.2.2 # via + # -c requirements/../../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../requirements/constraints.txt # -c requirements/../../../../requirements/constraints.txt @@ -335,17 +402,30 @@ opentelemetry-util-http==0.47b0 # opentelemetry-instrumentation-requests orjson==3.10.0 # via + # -c requirements/../../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../requirements/constraints.txt # -c requirements/../../../../requirements/constraints.txt @@ -387,39 +467,84 @@ pycountry==23.12.11 # via -r requirements/_base.in pycparser==2.21 # via cffi -pydantic==1.10.17 +pydantic==2.9.2 # via + # -c requirements/../../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt - # -c requirements/../../../../packages/service-library/requirements/../../../packages/settings-library/requirements/_base.in # -c requirements/../../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../requirements/constraints.txt # -c requirements/../../../../requirements/constraints.txt # -c requirements/./constraints.txt + # -r requirements/../../../../packages/models-library/requirements/../../../packages/common-library/requirements/_base.in # -r requirements/../../../../packages/models-library/requirements/_base.in + # -r requirements/../../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/_base.in # -r requirements/../../../../packages/postgres-database/requirements/_base.in + # -r requirements/../../../../packages/service-library/requirements/../../../packages/common-library/requirements/_base.in + # -r requirements/../../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/_base.in # -r requirements/../../../../packages/service-library/requirements/../../../packages/models-library/requirements/_base.in + # -r requirements/../../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/_base.in # -r requirements/../../../../packages/service-library/requirements/../../../packages/settings-library/requirements/_base.in # -r requirements/../../../../packages/service-library/requirements/_base.in + # -r requirements/../../../../packages/settings-library/requirements/../../../packages/common-library/requirements/_base.in # -r requirements/../../../../packages/settings-library/requirements/_base.in + # -r requirements/../../../../packages/simcore-sdk/requirements/../../../packages/common-library/requirements/_base.in + # -r requirements/../../../../packages/simcore-sdk/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/_base.in # -r requirements/../../../../packages/simcore-sdk/requirements/../../../packages/models-library/requirements/_base.in + # -r requirements/../../../../packages/simcore-sdk/requirements/../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/_base.in # -r requirements/../../../../packages/simcore-sdk/requirements/../../../packages/postgres-database/requirements/_base.in + # -r requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/common-library/requirements/_base.in + # -r requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/_base.in # -r requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/_base.in + # -r requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/_base.in # -r requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/_base.in # -r requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/_base.in + # -r requirements/../../../../packages/simcore-sdk/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/_base.in # -r requirements/../../../../packages/simcore-sdk/requirements/../../../packages/settings-library/requirements/_base.in # -r requirements/../../../../packages/simcore-sdk/requirements/_base.in # -r requirements/_base.in # fast-depends + # pydantic-extra-types + # pydantic-settings +pydantic-core==2.23.4 + # via pydantic +pydantic-extra-types==2.9.0 + # via + # -r requirements/../../../../packages/models-library/requirements/_base.in + # -r requirements/../../../../packages/service-library/requirements/../../../packages/models-library/requirements/_base.in + # -r requirements/../../../../packages/simcore-sdk/requirements/../../../packages/models-library/requirements/_base.in + # -r requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/_base.in +pydantic-settings==2.5.2 + # via + # -r requirements/../../../../packages/models-library/requirements/_base.in + # -r requirements/../../../../packages/service-library/requirements/../../../packages/models-library/requirements/_base.in + # -r requirements/../../../../packages/service-library/requirements/../../../packages/settings-library/requirements/_base.in + # -r requirements/../../../../packages/settings-library/requirements/_base.in + # -r requirements/../../../../packages/simcore-sdk/requirements/../../../packages/models-library/requirements/_base.in + # -r requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/_base.in + # -r requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/_base.in + # -r requirements/../../../../packages/simcore-sdk/requirements/../../../packages/settings-library/requirements/_base.in pygments==2.15.1 # via rich pyinstrument==4.6.1 @@ -434,6 +559,8 @@ python-dateutil==2.8.2 # via # arrow # faker +python-dotenv==1.0.1 + # via pydantic-settings python-engineio==4.3.4 # via python-socketio python-magic==0.4.25 @@ -444,17 +571,30 @@ pytz==2022.1 # via twilio pyyaml==6.0.1 # via + # -c requirements/../../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../requirements/constraints.txt # -c requirements/../../../../requirements/constraints.txt @@ -464,17 +604,30 @@ pyyaml==6.0.1 # openapi-spec-validator redis==5.0.4 # via + # -c requirements/../../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../requirements/constraints.txt # -c requirements/../../../../requirements/constraints.txt @@ -515,17 +668,30 @@ sniffio==1.3.1 # via anyio sqlalchemy==1.4.47 # via + # -c requirements/../../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../requirements/constraints.txt # -c requirements/../../../../requirements/constraints.txt @@ -566,37 +732,64 @@ typing-extensions==4.12.0 # faststream # opentelemetry-sdk # pydantic + # pydantic-core # typer ujson==5.5.0 # via + # -c requirements/../../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../requirements/constraints.txt # -c requirements/../../../../requirements/constraints.txt # aiohttp-swagger urllib3==1.26.11 # via + # -c requirements/../../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../../packages/simcore-sdk/requirements/../../../requirements/constraints.txt # -c requirements/../../../../requirements/constraints.txt diff --git a/services/web/server/requirements/_test.txt b/services/web/server/requirements/_test.txt index 1b8cff63723..20ab61468d7 100644 --- a/services/web/server/requirements/_test.txt +++ b/services/web/server/requirements/_test.txt @@ -173,7 +173,9 @@ python-dateutil==2.8.2 # -c requirements/_base.txt # faker python-dotenv==1.0.1 - # via -r requirements/_test.in + # via + # -c requirements/_base.txt + # -r requirements/_test.in pyyaml==6.0.1 # via # -c requirements/../../../../requirements/constraints.txt diff --git a/services/web/server/requirements/ci.txt b/services/web/server/requirements/ci.txt index 9a171226abf..2c6d577c042 100644 --- a/services/web/server/requirements/ci.txt +++ b/services/web/server/requirements/ci.txt @@ -11,6 +11,7 @@ --requirement _test.txt # installs this repo's packages +simcore-common-library @ ../../../packages/common-library simcore-models-library @ ../../../packages/models-library simcore-postgres-database @ ../../../packages/postgres-database simcore-settings-library @ ../../../packages/settings-library diff --git a/services/web/server/requirements/dev.txt b/services/web/server/requirements/dev.txt index b62c7127482..fdc9cb27429 100644 --- a/services/web/server/requirements/dev.txt +++ b/services/web/server/requirements/dev.txt @@ -12,6 +12,7 @@ --requirement _tools.txt # installs this repo's packages +--editable ../../../packages/common-library/ --editable ../../../packages/models-library/ --editable ../../../packages/postgres-database/ --editable ../../../packages/settings-library/ diff --git a/services/web/server/requirements/prod.txt b/services/web/server/requirements/prod.txt index 9494dd12c30..2ccad765e49 100644 --- a/services/web/server/requirements/prod.txt +++ b/services/web/server/requirements/prod.txt @@ -10,6 +10,7 @@ --requirement _base.txt # installs this repo's packages +simcore-common-library @ ../../../packages/common-library simcore-models-library @ ../../../packages/models-library simcore-postgres-database @ ../../../packages/postgres-database simcore-settings-library @ ../../../packages/settings-library diff --git a/services/web/server/src/simcore_service_webserver/activity/_handlers.py b/services/web/server/src/simcore_service_webserver/activity/_handlers.py index ba7ec32557a..4e87c8f3bc0 100644 --- a/services/web/server/src/simcore_service_webserver/activity/_handlers.py +++ b/services/web/server/src/simcore_service_webserver/activity/_handlers.py @@ -4,7 +4,7 @@ import aiohttp import aiohttp.web from models_library.api_schemas_webserver.activity import ActivityStatusDict -from pydantic import parse_obj_as +from pydantic import TypeAdapter from servicelib.aiohttp.client_session import get_client_session from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON from servicelib.request_keys import RQT_USERID_KEY @@ -73,5 +73,5 @@ async def get_activity_status(request: aiohttp.web.Request): if not res: raise aiohttp.web.HTTPNoContent(content_type=MIMETYPE_APPLICATION_JSON) - assert parse_obj_as(ActivityStatusDict, res) is not None # nosec + assert TypeAdapter(ActivityStatusDict).validate_python(res) is not None # nosec return dict(res) diff --git a/services/web/server/src/simcore_service_webserver/announcements/_models.py b/services/web/server/src/simcore_service_webserver/announcements/_models.py index 4edb7c8d20a..164f6f9327a 100644 --- a/services/web/server/src/simcore_service_webserver/announcements/_models.py +++ b/services/web/server/src/simcore_service_webserver/announcements/_models.py @@ -1,8 +1,8 @@ from datetime import datetime -from typing import Any, ClassVar, Literal +from typing import Literal import arrow -from pydantic import BaseModel, validator +from pydantic import BaseModel, ConfigDict, field_validator # NOTE: this model is used for BOTH @@ -18,7 +18,7 @@ class Announcement(BaseModel): link: str widgets: list[Literal["login", "ribbon", "user-menu"]] - @validator("end") + @field_validator("end") @classmethod def check_start_before_end(cls, v, values): if start := values.get("start"): @@ -31,8 +31,8 @@ def check_start_before_end(cls, v, values): def expired(self) -> bool: return self.end <= arrow.utcnow().datetime - class Config: - schema_extra: ClassVar[dict[str, Any]] = { + model_config = ConfigDict( + json_schema_extra={ "examples": [ { "id": "Student_Competition_2023", @@ -56,3 +56,4 @@ class Config: }, ] } + ) diff --git a/services/web/server/src/simcore_service_webserver/api_keys/_api.py b/services/web/server/src/simcore_service_webserver/api_keys/_api.py index 9a46ad9f512..9bbe56f7c6f 100644 --- a/services/web/server/src/simcore_service_webserver/api_keys/_api.py +++ b/services/web/server/src/simcore_service_webserver/api_keys/_api.py @@ -70,7 +70,7 @@ async def get_api_key( ) -> ApiKeyGet | None: repo = ApiKeyRepo.create_from_app(app) row = await repo.get(display_name=name, user_id=user_id, product_name=product_name) - return ApiKeyGet.parse_obj(row) if row else None + return ApiKeyGet.model_validate(row) if row else None async def get_or_create_api_key( diff --git a/services/web/server/src/simcore_service_webserver/api_keys/_handlers.py b/services/web/server/src/simcore_service_webserver/api_keys/_handlers.py index ce7a7be0943..aac66007940 100644 --- a/services/web/server/src/simcore_service_webserver/api_keys/_handlers.py +++ b/services/web/server/src/simcore_service_webserver/api_keys/_handlers.py @@ -31,7 +31,7 @@ class _RequestContext(RequestParams): @login_required @permission_required("user.apikey.*") async def list_api_keys(request: web.Request): - req_ctx = _RequestContext.parse_obj(request) + req_ctx = _RequestContext.model_validate(request) api_keys_names = await _api.list_api_keys( request.app, user_id=req_ctx.user_id, @@ -44,7 +44,7 @@ async def list_api_keys(request: web.Request): @login_required @permission_required("user.apikey.*") async def create_api_key(request: web.Request): - req_ctx = _RequestContext.parse_obj(request) + req_ctx = _RequestContext.model_validate(request) new = await parse_request_body_as(ApiKeyCreate, request) try: data = await _api.create_api_key( @@ -66,7 +66,7 @@ async def create_api_key(request: web.Request): @login_required @permission_required("user.apikey.*") async def delete_api_key(request: web.Request): - req_ctx = _RequestContext.parse_obj(request) + req_ctx = _RequestContext.model_validate(request) # NOTE: SEE https://github.com/ITISFoundation/osparc-simcore/issues/4920 body = await request.json() diff --git a/services/web/server/src/simcore_service_webserver/application_settings.py b/services/web/server/src/simcore_service_webserver/application_settings.py index fcdec0f9eb3..c01e5bd96e5 100644 --- a/services/web/server/src/simcore_service_webserver/application_settings.py +++ b/services/web/server/src/simcore_service_webserver/application_settings.py @@ -3,6 +3,7 @@ from typing import Any, Final from aiohttp import web +from common_library.pydantic_fields_extension import is_nullable from models_library.basic_types import ( BootModeEnum, BuildTargetEnum, @@ -11,8 +12,15 @@ VersionTag, ) from models_library.utils.change_case import snake_to_camel -from pydantic import AnyHttpUrl, parse_obj_as, root_validator, validator -from pydantic.fields import Field, ModelField +from pydantic import ( + AliasChoices, + AnyHttpUrl, + TypeAdapter, + ValidationInfo, + field_validator, + model_validator, +) +from pydantic.fields import Field from pydantic.types import PositiveInt from settings_library.base import BaseCustomSettings from settings_library.email import SMTPSettings @@ -53,7 +61,7 @@ class ApplicationSettings(BaseCustomSettings, MixinLoggingSettings): # CODE STATICS --------------------------------------------------------- API_VERSION: str = API_VERSION APP_NAME: str = APP_NAME - API_VTAG: VersionTag = parse_obj_as(VersionTag, API_VTAG) + API_VTAG: VersionTag = TypeAdapter(VersionTag).validate_python(API_VTAG) # IMAGE BUILDTIME ------------------------------------------------------ # @Makefile @@ -83,13 +91,13 @@ class ApplicationSettings(BaseCustomSettings, MixinLoggingSettings): SIMCORE_VCS_RELEASE_TAG: str | None = Field( default=None, description="Name of the tag that marks this release, or None if undefined", - example="ResistanceIsFutile10", + examples=["ResistanceIsFutile10"], ) SIMCORE_VCS_RELEASE_URL: AnyHttpUrl | None = Field( default=None, description="URL to release notes", - example="https://github.com/ITISFoundation/osparc-simcore/releases/tag/staging_ResistanceIsFutile10", + examples=["https://github.com/ITISFoundation/osparc-simcore/releases/tag/staging_ResistanceIsFutile10"], ) SWARM_STACK_NAME: str | None = Field( @@ -105,12 +113,14 @@ class ApplicationSettings(BaseCustomSettings, MixinLoggingSettings): ) WEBSERVER_LOGLEVEL: LogLevel = Field( default=LogLevel.WARNING.value, - env=["WEBSERVER_LOGLEVEL", "LOG_LEVEL", "LOGLEVEL"], + validation_alias=AliasChoices("WEBSERVER_LOGLEVEL", "LOG_LEVEL", "LOGLEVEL"), # NOTE: suffix '_LOGLEVEL' is used overall ) WEBSERVER_LOG_FORMAT_LOCAL_DEV_ENABLED: bool = Field( default=False, - env=["WEBSERVER_LOG_FORMAT_LOCAL_DEV_ENABLED", "LOG_FORMAT_LOCAL_DEV_ENABLED"], + validation_alias=AliasChoices( + "WEBSERVER_LOG_FORMAT_LOCAL_DEV_ENABLED", "LOG_FORMAT_LOCAL_DEV_ENABLED" + ), description="Enables local development log format. WARNING: make sure it is disabled if you want to have structured logs!", ) # TODO: find a better name!? @@ -119,100 +129,117 @@ class ApplicationSettings(BaseCustomSettings, MixinLoggingSettings): description="host name to serve within the container." "NOTE that this different from WEBSERVER_HOST env which is the host seen outside the container", ) - WEBSERVER_HOST: str | None = Field(None, env=["WEBSERVER_HOST", "HOST", "HOSTNAME"]) - WEBSERVER_PORT: PortInt = parse_obj_as(PortInt, DEFAULT_AIOHTTP_PORT) + WEBSERVER_HOST: str | None = Field( + None, validation_alias=AliasChoices("WEBSERVER_HOST", "HOST", "HOSTNAME") + ) + WEBSERVER_PORT: PortInt = TypeAdapter(PortInt).validate_python(DEFAULT_AIOHTTP_PORT) WEBSERVER_FRONTEND: FrontEndAppSettings | None = Field( - auto_default_from_env=True, description="front-end static settings" + json_schema_extra={"auto_default_from_env": True}, + description="front-end static settings", ) # PLUGINS ---------------- WEBSERVER_ACTIVITY: PrometheusSettings | None = Field( - auto_default_from_env=True, + json_schema_extra={"auto_default_from_env": True}, description="activity plugin", ) WEBSERVER_CATALOG: CatalogSettings | None = Field( - auto_default_from_env=True, description="catalog service client's plugin" + json_schema_extra={"auto_default_from_env": True}, + description="catalog service client's plugin", ) # TODO: Shall be required WEBSERVER_DB: PostgresSettings | None = Field( - auto_default_from_env=True, description="database plugin" + json_schema_extra={"auto_default_from_env": True}, description="database plugin" ) WEBSERVER_DIAGNOSTICS: DiagnosticsSettings | None = Field( - auto_default_from_env=True, description="diagnostics plugin" + json_schema_extra={"auto_default_from_env": True}, + description="diagnostics plugin", ) WEBSERVER_DIRECTOR_V2: DirectorV2Settings | None = Field( - auto_default_from_env=True, description="director-v2 service client's plugin" + json_schema_extra={"auto_default_from_env": True}, + description="director-v2 service client's plugin", ) WEBSERVER_EMAIL: SMTPSettings | None = Field( - auto_default_from_env=True, description="email plugin" + json_schema_extra={"auto_default_from_env": True}, description="email plugin" ) WEBSERVER_EXPORTER: ExporterSettings | None = Field( - auto_default_from_env=True, description="exporter plugin" + json_schema_extra={"auto_default_from_env": True}, description="exporter plugin" ) WEBSERVER_GARBAGE_COLLECTOR: GarbageCollectorSettings | None = Field( - auto_default_from_env=True, description="garbage collector plugin" + json_schema_extra={"auto_default_from_env": True}, + description="garbage collector plugin", ) WEBSERVER_INVITATIONS: InvitationsSettings | None = Field( - auto_default_from_env=True, description="invitations plugin" + json_schema_extra={"auto_default_from_env": True}, + description="invitations plugin", ) WEBSERVER_LOGIN: LoginSettings | None = Field( - auto_default_from_env=True, description="login plugin" + json_schema_extra={"auto_default_from_env": True}, description="login plugin" ) WEBSERVER_PAYMENTS: PaymentsSettings | None = Field( - auto_default_from_env=True, description="payments plugin settings" + json_schema_extra={"auto_default_from_env": True}, + description="payments plugin settings", ) WEBSERVER_DYNAMIC_SCHEDULER: DynamicSchedulerSettings | None = Field( - auto_default_from_env=True, description="dynamic-scheduler plugin settings" + description="dynamic-scheduler plugin settings", + json_schema_extra={"auto_default_from_env": True}, ) - WEBSERVER_REDIS: RedisSettings | None = Field(auto_default_from_env=True) + WEBSERVER_REDIS: RedisSettings | None = Field( + json_schema_extra={"auto_default_from_env": True} + ) WEBSERVER_REST: RestSettings | None = Field( - auto_default_from_env=True, description="rest api plugin" + description="rest api plugin", json_schema_extra={"auto_default_from_env": True} ) WEBSERVER_RESOURCE_MANAGER: ResourceManagerSettings = Field( - auto_default_from_env=True, description="resource_manager plugin" + description="resource_manager plugin", + json_schema_extra={"auto_default_from_env": True}, ) WEBSERVER_RESOURCE_USAGE_TRACKER: ResourceUsageTrackerSettings | None = Field( - auto_default_from_env=True, description="resource usage tracker service client's plugin", + json_schema_extra={"auto_default_from_env": True}, ) WEBSERVER_SCICRUNCH: SciCrunchSettings | None = Field( - auto_default_from_env=True, description="scicrunch plugin" + description="scicrunch plugin", + json_schema_extra={"auto_default_from_env": True}, ) WEBSERVER_SESSION: SessionSettings = Field( - auto_default_from_env=True, description="session plugin" + description="session plugin", json_schema_extra={"auto_default_from_env": True} ) WEBSERVER_STATICWEB: StaticWebserverModuleSettings | None = Field( - auto_default_from_env=True, description="static-webserver service plugin" + description="static-webserver service plugin", + json_schema_extra={"auto_default_from_env": True}, ) WEBSERVER_STORAGE: StorageSettings | None = Field( - auto_default_from_env=True, description="storage service client's plugin" + description="storage service client's plugin", + json_schema_extra={"auto_default_from_env": True}, ) WEBSERVER_STUDIES_DISPATCHER: StudiesDispatcherSettings | None = Field( - auto_default_from_env=True, description="studies dispatcher plugin" + description="studies dispatcher plugin", + json_schema_extra={"auto_default_from_env": True}, ) WEBSERVER_TRACING: TracingSettings | None = Field( - auto_default_from_env=True, description="tracing plugin" + description="tracing plugin", json_schema_extra={"auto_default_from_env": True} ) WEBSERVER_PROJECTS: ProjectsSettings | None = Field( - auto_default_from_env=True, description="projects plugin" + description="projects plugin", json_schema_extra={"auto_default_from_env": True} ) WEBSERVER_RABBITMQ: RabbitSettings | None = Field( - auto_default_from_env=True, description="rabbitmq plugin" + description="rabbitmq plugin", json_schema_extra={"auto_default_from_env": True} ) WEBSERVER_USERS: UsersSettings | None = Field( - auto_default_from_env=True, description="users plugin" + description="users plugin", json_schema_extra={"auto_default_from_env": True} ) # These plugins only require (for the moment) an entry to toggle between enabled/disabled @@ -241,7 +268,7 @@ class ApplicationSettings(BaseCustomSettings, MixinLoggingSettings): "Currently this is a system plugin and cannot be disabled", ) - @root_validator() + @model_validator(mode="after") @classmethod def build_vcs_release_url_if_unset(cls, values): release_url = values.get("SIMCORE_VCS_RELEASE_URL") @@ -259,39 +286,43 @@ def build_vcs_release_url_if_unset(cls, values): return values - @validator( + @field_validator( # List of plugins under-development (keep up-to-date) # TODO: consider mark as dev-feature in field extras of Config attr. # Then they can be automtically advertised "WEBSERVER_META_MODELING", "WEBSERVER_VERSION_CONTROL", - pre=True, - always=True, + mode="before", ) @classmethod - def enable_only_if_dev_features_allowed(cls, v, values, field: ModelField): + def enable_only_if_dev_features_allowed(cls, v, info: ValidationInfo): """Ensures that plugins 'under development' get programatically disabled if WEBSERVER_DEV_FEATURES_ENABLED=False """ - if values["WEBSERVER_DEV_FEATURES_ENABLED"]: + if info.data["WEBSERVER_DEV_FEATURES_ENABLED"]: return v if v: _logger.warning( - "%s still under development and will be disabled.", field.name + "%s still under development and will be disabled.", info.field_name ) - return None if field.allow_none else False + + return ( + None + if info.field_name and is_nullable(cls.model_fields[info.field_name]) + else False + ) @cached_property def log_level(self) -> int: level: int = getattr(logging, self.WEBSERVER_LOGLEVEL.upper()) return level - @validator("WEBSERVER_LOGLEVEL") + @field_validator("WEBSERVER_LOGLEVEL") @classmethod def valid_log_level(cls, value): return cls.validate_log_level(value) - @validator("SC_HEALTHCHECK_TIMEOUT", pre=True) + @field_validator("SC_HEALTHCHECK_TIMEOUT", mode="before") @classmethod def get_healthcheck_timeout_in_seconds(cls, v): # Ex. HEALTHCHECK --interval=5m --timeout=3s diff --git a/services/web/server/src/simcore_service_webserver/catalog/_api.py b/services/web/server/src/simcore_service_webserver/catalog/_api.py index 9bbbae4e43c..3b02e74f679 100644 --- a/services/web/server/src/simcore_service_webserver/catalog/_api.py +++ b/services/web/server/src/simcore_service_webserver/catalog/_api.py @@ -23,7 +23,7 @@ from models_library.users import UserID from models_library.utils.fastapi_encoders import jsonable_encoder from pint import UnitRegistry -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict from servicelib.aiohttp.requests_validation import handle_validation_as_http_error from servicelib.rabbitmq.rpc_interfaces.catalog import services as catalog_rpc from servicelib.rest_constants import RESPONSE_MODEL_POLICY @@ -42,9 +42,7 @@ class CatalogRequestContext(BaseModel): user_id: UserID product_name: str unit_registry: UnitRegistry - - class Config: - arbitrary_types_allowed = True + model_config = ConfigDict(arbitrary_types_allowed=True) @classmethod def create(cls, request: Request) -> "CatalogRequestContext": @@ -157,7 +155,7 @@ async def update_service_v2( user_id=user_id, service_key=service_key, service_version=service_version, - update=ServiceUpdateV2.parse_obj(update_data), + update=ServiceUpdateV2.model_validate(update_data), ) data = jsonable_encoder(service, exclude_unset=True) @@ -286,8 +284,8 @@ async def get_compatible_inputs_given_source_output( from_service_key, from_service_version, from_output_key, ctx ) - from_output: ServiceOutput = ServiceOutput.construct( - **service_output.dict(include=ServiceOutput.__fields__.keys()) + from_output: ServiceOutput = ServiceOutput.model_construct( + **service_output.model_dump(include=ServiceOutput.model_fields.keys()) ) # N inputs @@ -295,8 +293,8 @@ async def get_compatible_inputs_given_source_output( def iter_service_inputs() -> Iterator[tuple[ServiceInputKey, ServiceInput]]: for service_input in service_inputs: - yield service_input.key_id, ServiceInput.construct( - **service_input.dict(include=ServiceInput.__fields__.keys()) + yield service_input.key_id, ServiceInput.model_construct( + **service_input.model_dump(include=ServiceInput.model_fields.keys()) ) # check @@ -354,16 +352,16 @@ async def get_compatible_outputs_given_target_input( def iter_service_outputs() -> Iterator[tuple[ServiceOutputKey, ServiceOutput]]: for service_output in service_outputs: - yield service_output.key_id, ServiceOutput.construct( - **service_output.dict(include=ServiceOutput.__fields__.keys()) + yield service_output.key_id, ServiceOutput.model_construct( + **service_output.model_dump(include=ServiceOutput.model_fields.keys()) ) # 1 input service_input = await get_service_input( to_service_key, to_service_version, to_input_key, ctx ) - to_input: ServiceInput = ServiceInput.construct( - **service_input.dict(include=ServiceInput.__fields__.keys()) + to_input: ServiceInput = ServiceInput.model_construct( + **service_input.model_dump(include=ServiceInput.model_fields.keys()) ) # check diff --git a/services/web/server/src/simcore_service_webserver/catalog/_handlers.py b/services/web/server/src/simcore_service_webserver/catalog/_handlers.py index 02e21f37e29..1c92cfb3c43 100644 --- a/services/web/server/src/simcore_service_webserver/catalog/_handlers.py +++ b/services/web/server/src/simcore_service_webserver/catalog/_handlers.py @@ -26,7 +26,7 @@ ServiceResourcesDict, ServiceResourcesDictHelpers, ) -from pydantic import BaseModel, Extra, Field, parse_obj_as, validator +from pydantic import BaseModel, ConfigDict, Field, TypeAdapter, field_validator from servicelib.aiohttp.requests_validation import ( parse_request_body_as, parse_request_path_parameters_as, @@ -54,12 +54,9 @@ class ServicePathParams(BaseModel): service_key: ServiceKey service_version: ServiceVersion + model_config = ConfigDict(populate_by_name=True, extra="forbid") - class Config: - allow_population_by_field_name = True - extra = Extra.forbid - - @validator("service_key", pre=True) + @field_validator("service_key", mode="before") @classmethod def ensure_unquoted(cls, v): # NOTE: this is needed as in pytest mode, the aiohttp server does not seem to unquote automatically @@ -90,7 +87,7 @@ async def list_services_latest(request: Request): user_id=request_ctx.user_id, product_name=request_ctx.product_name, unit_registry=request_ctx.unit_registry, - page_params=PageQueryParameters.construct( + page_params=PageQueryParameters.model_construct( offset=query_params.offset, limit=query_params.limit ), ) @@ -98,7 +95,7 @@ async def list_services_latest(request: Request): assert page_meta.limit == query_params.limit # nosec assert page_meta.offset == query_params.offset # nosec - page = Page[CatalogServiceGet].parse_obj( + page = Page[CatalogServiceGet].model_validate( paginate_data( chunk=page_items, request_url=request.url, @@ -133,7 +130,7 @@ async def get_service(request: Request): service_version=path_params.service_version, ) - return envelope_json_response(CatalogServiceGet.parse_obj(service)) + return envelope_json_response(CatalogServiceGet.model_validate(service)) @routes.patch( @@ -160,11 +157,11 @@ async def update_service(request: Request): product_name=request_ctx.product_name, service_key=path_params.service_key, service_version=path_params.service_version, - update_data=update.dict(exclude_unset=True), + update_data=update.model_dump(exclude_unset=True), unit_registry=request_ctx.unit_registry, ) - return envelope_json_response(CatalogServiceGet.parse_obj(updated)) + return envelope_json_response(CatalogServiceGet.model_validate(updated)) @routes.get( @@ -182,7 +179,7 @@ async def list_service_inputs(request: Request): path_params.service_key, path_params.service_version, ctx ) - data = [m.dict(**RESPONSE_MODEL_POLICY) for m in response_model] + data = [m.model_dump(**RESPONSE_MODEL_POLICY) for m in response_model] return await asyncio.get_event_loop().run_in_executor( None, envelope_json_response, data ) @@ -210,7 +207,7 @@ async def get_service_input(request: Request): ctx, ) - data = response_model.dict(**RESPONSE_MODEL_POLICY) + data = response_model.model_dump(**RESPONSE_MODEL_POLICY) return await asyncio.get_event_loop().run_in_executor( None, envelope_json_response, data ) @@ -265,7 +262,7 @@ async def list_service_outputs(request: Request): path_params.service_key, path_params.service_version, ctx ) - data = [m.dict(**RESPONSE_MODEL_POLICY) for m in response_model] + data = [m.model_dump(**RESPONSE_MODEL_POLICY) for m in response_model] return await asyncio.get_event_loop().run_in_executor( None, envelope_json_response, data ) @@ -293,7 +290,7 @@ async def get_service_output(request: Request): ctx, ) - data = response_model.dict(**RESPONSE_MODEL_POLICY) + data = response_model.model_dump(**RESPONSE_MODEL_POLICY) return await asyncio.get_event_loop().run_in_executor( None, envelope_json_response, data ) @@ -387,4 +384,6 @@ async def get_service_pricing_plan(request: Request): service_version=f"{path_params.service_version}", ) - return envelope_json_response(parse_obj_as(PricingPlanGet, pricing_plan)) + return envelope_json_response( + TypeAdapter(PricingPlanGet).validate_python(pricing_plan) + ) diff --git a/services/web/server/src/simcore_service_webserver/catalog/client.py b/services/web/server/src/simcore_service_webserver/catalog/client.py index 8a8f6083252..386ae811da0 100644 --- a/services/web/server/src/simcore_service_webserver/catalog/client.py +++ b/services/web/server/src/simcore_service_webserver/catalog/client.py @@ -19,7 +19,7 @@ ) from models_library.services_resources import ServiceResourcesDict from models_library.users import UserID -from pydantic import parse_obj_as +from pydantic import TypeAdapter from servicelib.aiohttp import status from servicelib.aiohttp.client_session import get_client_session from servicelib.rest_constants import X_PRODUCT_NAME_HEADER @@ -146,7 +146,7 @@ async def get_service_resources( async with session.get(url) as resp: resp.raise_for_status() dict_response = await resp.json() - return parse_obj_as(ServiceResourcesDict, dict_response) + return TypeAdapter(ServiceResourcesDict).validate_python(dict_response) async def get_service_access_rights( @@ -168,7 +168,7 @@ async def get_service_access_rights( ) as resp: resp.raise_for_status() body = await resp.json() - return ServiceAccessRightsGet.parse_obj(body) + return ServiceAccessRightsGet.model_validate(body) async def update_service( diff --git a/services/web/server/src/simcore_service_webserver/clusters/_handlers.py b/services/web/server/src/simcore_service_webserver/clusters/_handlers.py index 70752da883b..da80bb56428 100644 --- a/services/web/server/src/simcore_service_webserver/clusters/_handlers.py +++ b/services/web/server/src/simcore_service_webserver/clusters/_handlers.py @@ -11,7 +11,7 @@ ClusterPing, ) from models_library.users import UserID -from pydantic import BaseModel, Field, parse_obj_as +from pydantic import BaseModel, Field, TypeAdapter from servicelib.aiohttp import status from servicelib.aiohttp.requests_validation import ( parse_request_body_as, @@ -78,7 +78,7 @@ class _RequestContext(BaseModel): @permission_required("clusters.create") @_handle_cluster_exceptions async def create_cluster(request: web.Request) -> web.Response: - req_ctx = _RequestContext.parse_obj(request) + req_ctx = _RequestContext.model_validate(request) new_cluster = await parse_request_body_as(ClusterCreate, request) created_cluster = await director_v2_api.create_cluster( @@ -94,13 +94,13 @@ async def create_cluster(request: web.Request) -> web.Response: @permission_required("clusters.read") @_handle_cluster_exceptions async def list_clusters(request: web.Request) -> web.Response: - req_ctx = _RequestContext.parse_obj(request) + req_ctx = _RequestContext.model_validate(request) clusters = await director_v2_api.list_clusters( app=request.app, user_id=req_ctx.user_id, ) - assert parse_obj_as(list[ClusterGet], clusters) is not None # nosec + assert TypeAdapter(list[ClusterGet]).validate_python(clusters) is not None # nosec return envelope_json_response(clusters) @@ -109,7 +109,7 @@ async def list_clusters(request: web.Request) -> web.Response: @permission_required("clusters.read") @_handle_cluster_exceptions async def get_cluster(request: web.Request) -> web.Response: - req_ctx = _RequestContext.parse_obj(request) + req_ctx = _RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ClusterPathParams, request) cluster = await director_v2_api.get_cluster( @@ -117,7 +117,7 @@ async def get_cluster(request: web.Request) -> web.Response: user_id=req_ctx.user_id, cluster_id=path_params.cluster_id, ) - assert parse_obj_as(ClusterGet, cluster) is not None # nosec + assert TypeAdapter(ClusterGet).validate_python(cluster) is not None # nosec return envelope_json_response(cluster) @@ -126,7 +126,7 @@ async def get_cluster(request: web.Request) -> web.Response: @permission_required("clusters.write") @_handle_cluster_exceptions async def update_cluster(request: web.Request) -> web.Response: - req_ctx = _RequestContext.parse_obj(request) + req_ctx = _RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ClusterPathParams, request) cluster_patch = await parse_request_body_as(ClusterPatch, request) @@ -137,7 +137,7 @@ async def update_cluster(request: web.Request) -> web.Response: cluster_patch=cluster_patch, ) - assert parse_obj_as(ClusterGet, updated_cluster) is not None # nosec + assert TypeAdapter(ClusterGet).validate_python(updated_cluster) is not None # nosec return envelope_json_response(updated_cluster) @@ -146,7 +146,7 @@ async def update_cluster(request: web.Request) -> web.Response: @permission_required("clusters.delete") @_handle_cluster_exceptions async def delete_cluster(request: web.Request) -> web.Response: - req_ctx = _RequestContext.parse_obj(request) + req_ctx = _RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ClusterPathParams, request) await director_v2_api.delete_cluster( @@ -165,7 +165,7 @@ async def delete_cluster(request: web.Request) -> web.Response: @permission_required("clusters.read") @_handle_cluster_exceptions async def get_cluster_details(request: web.Request) -> web.Response: - req_ctx = _RequestContext.parse_obj(request) + req_ctx = _RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ClusterPathParams, request) cluster_details = await director_v2_api.get_cluster_details( @@ -173,7 +173,9 @@ async def get_cluster_details(request: web.Request) -> web.Response: user_id=req_ctx.user_id, cluster_id=path_params.cluster_id, ) - assert parse_obj_as(ClusterDetails, cluster_details) is not None # nosec + assert ( + TypeAdapter(ClusterDetails).validate_python(cluster_details) is not None + ) # nosec return envelope_json_response(cluster_details) @@ -199,7 +201,7 @@ async def ping_cluster(request: web.Request) -> web.Response: @permission_required("clusters.read") @_handle_cluster_exceptions async def ping_cluster_cluster_id(request: web.Request) -> web.Response: - req_ctx = _RequestContext.parse_obj(request) + req_ctx = _RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ClusterPathParams, request) await director_v2_api.ping_specific_cluster( diff --git a/services/web/server/src/simcore_service_webserver/diagnostics/_handlers.py b/services/web/server/src/simcore_service_webserver/diagnostics/_handlers.py index 13154bf5723..7f0ceb8bfa6 100644 --- a/services/web/server/src/simcore_service_webserver/diagnostics/_handlers.py +++ b/services/web/server/src/simcore_service_webserver/diagnostics/_handlers.py @@ -10,7 +10,7 @@ from aiohttp import ClientError, ClientSession, web from models_library.app_diagnostics import AppStatusCheck from models_library.utils.pydantic_tools_extension import FieldNotRequired -from pydantic import BaseModel, parse_obj_as +from pydantic import BaseModel, TypeAdapter from servicelib.aiohttp.client_session import get_client_session from servicelib.aiohttp.requests_validation import parse_request_query_parameters_as from servicelib.utils import logged_gather @@ -62,7 +62,7 @@ async def get_app_diagnostics(request: web.Request): top_tracemalloc=get_tracemalloc_info(top=query_params.top_tracemalloc) ) - assert parse_obj_as(StatusDiagnosticsGet, data) is not None # nosec + assert TypeAdapter(StatusDiagnosticsGet).validate_python(data) is not None # nosec return envelope_json_response(data) @@ -99,7 +99,7 @@ def _get_client_session_info(): return info - check = AppStatusCheck.parse_obj( + check = AppStatusCheck.model_validate( { "app_name": APP_NAME, "version": API_VERSION, diff --git a/services/web/server/src/simcore_service_webserver/diagnostics/settings.py b/services/web/server/src/simcore_service_webserver/diagnostics/settings.py index 5c82496ad34..6c39a5d2824 100644 --- a/services/web/server/src/simcore_service_webserver/diagnostics/settings.py +++ b/services/web/server/src/simcore_service_webserver/diagnostics/settings.py @@ -1,5 +1,11 @@ from aiohttp.web import Application -from pydantic import Field, NonNegativeFloat, PositiveFloat, validator +from pydantic import ( + AliasChoices, + Field, + NonNegativeFloat, + PositiveFloat, + field_validator, +) from servicelib.aiohttp.application_keys import APP_SETTINGS_KEY from settings_library.base import BaseCustomSettings @@ -11,7 +17,9 @@ class DiagnosticsSettings(BaseCustomSettings): "Any task blocked more than slow_duration_secs is logged as WARNING" "Aims to identify possible blocking calls" ), - env=["DIAGNOSTICS_SLOW_DURATION_SECS", "AIODEBUG_SLOW_DURATION_SECS"], + validation_alias=AliasChoices( + "DIAGNOSTICS_SLOW_DURATION_SECS", "AIODEBUG_SLOW_DURATION_SECS" + ), ) DIAGNOSTICS_HEALTHCHECK_ENABLED: bool = Field( @@ -32,7 +40,7 @@ class DiagnosticsSettings(BaseCustomSettings): DIAGNOSTICS_START_SENSING_DELAY: NonNegativeFloat = 60.0 - @validator("DIAGNOSTICS_MAX_TASK_DELAY", pre=True) + @field_validator("DIAGNOSTICS_MAX_TASK_DELAY", mode="before") @classmethod def _validate_max_task_delay(cls, v, values): # Sets an upper threshold for blocking functions, i.e. diff --git a/services/web/server/src/simcore_service_webserver/director_v2/_api_utils.py b/services/web/server/src/simcore_service_webserver/director_v2/_api_utils.py index e9bbca91c50..74bc8e8ee14 100644 --- a/services/web/server/src/simcore_service_webserver/director_v2/_api_utils.py +++ b/services/web/server/src/simcore_service_webserver/director_v2/_api_utils.py @@ -2,7 +2,7 @@ from models_library.projects import ProjectID from models_library.users import UserID from models_library.wallets import WalletID, WalletInfo -from pydantic import parse_obj_as +from pydantic import TypeAdapter from ..application_settings import get_application_settings from ..products.api import Product @@ -35,7 +35,9 @@ async def get_wallet_info( ) if user_default_wallet_preference is None: raise UserDefaultWalletNotFoundError(uid=user_id) - project_wallet_id = parse_obj_as(WalletID, user_default_wallet_preference.value) + project_wallet_id = TypeAdapter(WalletID).validate_python( + user_default_wallet_preference.value + ) await projects_api.connect_wallet_to_project( app, product_name=product_name, diff --git a/services/web/server/src/simcore_service_webserver/director_v2/_core_computations.py b/services/web/server/src/simcore_service_webserver/director_v2/_core_computations.py index 950d92fa2eb..edfb843d45c 100644 --- a/services/web/server/src/simcore_service_webserver/director_v2/_core_computations.py +++ b/services/web/server/src/simcore_service_webserver/director_v2/_core_computations.py @@ -26,7 +26,7 @@ from models_library.projects_pipeline import ComputationTask from models_library.users import UserID from models_library.utils.fastapi_encoders import jsonable_encoder -from pydantic import parse_obj_as +from pydantic import TypeAdapter from pydantic.types import PositiveInt from servicelib.aiohttp import status from servicelib.logging_utils import log_decorator @@ -182,7 +182,7 @@ async def get_computation_task( computation_task_out_dict = await request_director_v2( app, "GET", backend_url, expected_status=web.HTTPOk ) - task_out = ComputationTask.parse_obj(computation_task_out_dict) + task_out = ComputationTask.model_validate(computation_task_out_dict) _logger.debug("found computation task: %s", f"{task_out=}") return task_out except DirectorServiceError as exc: @@ -234,7 +234,7 @@ async def create_cluster( ), ) assert isinstance(cluster, dict) # nosec - assert parse_obj_as(ClusterGet, cluster) is not None # nosec + assert TypeAdapter(ClusterGet).validate_python(cluster) is not None # nosec return cluster @@ -248,7 +248,7 @@ async def list_clusters(app: web.Application, user_id: UserID) -> list[DataType] ) assert isinstance(clusters, list) # nosec - assert parse_obj_as(list[ClusterGet], clusters) is not None # nosec + assert TypeAdapter(list[ClusterGet]).validate_python(clusters) is not None # nosec return clusters @@ -276,7 +276,7 @@ async def get_cluster( ) assert isinstance(cluster, dict) # nosec - assert parse_obj_as(ClusterGet, cluster) is not None # nosec + assert TypeAdapter(ClusterGet).validate_python(cluster) is not None # nosec return cluster @@ -304,7 +304,7 @@ async def get_cluster_details( }, ) assert isinstance(cluster, dict) # nosec - assert parse_obj_as(ClusterDetails, cluster) is not None # nosec + assert TypeAdapter(ClusterDetails).validate_python(cluster) is not None # nosec return cluster @@ -342,7 +342,7 @@ async def update_cluster( ) assert isinstance(cluster, dict) # nosec - assert parse_obj_as(ClusterGet, cluster) is not None # nosec + assert TypeAdapter(ClusterGet).validate_python(cluster) is not None # nosec return cluster diff --git a/services/web/server/src/simcore_service_webserver/director_v2/_core_dynamic_services.py b/services/web/server/src/simcore_service_webserver/director_v2/_core_dynamic_services.py index 20cf772075e..eec5bcf7b85 100644 --- a/services/web/server/src/simcore_service_webserver/director_v2/_core_dynamic_services.py +++ b/services/web/server/src/simcore_service_webserver/director_v2/_core_dynamic_services.py @@ -10,7 +10,7 @@ from models_library.api_schemas_directorv2.dynamic_services import DynamicServiceGet from models_library.projects import ProjectID from models_library.services import ServicePortKey -from pydantic import BaseModel, NonNegativeInt, parse_obj_as +from pydantic import BaseModel, NonNegativeInt, TypeAdapter from pydantic.types import PositiveInt from servicelib.logging_utils import log_decorator from yarl import URL @@ -49,7 +49,7 @@ async def list_dynamic_services( if services is None: services = [] assert isinstance(services, list) # nosec - return parse_obj_as(list[DynamicServiceGet], services) + return TypeAdapter(list[DynamicServiceGet]).validate_python(services) # NOTE: ANE https://github.com/ITISFoundation/osparc-simcore/issues/3191 diff --git a/services/web/server/src/simcore_service_webserver/director_v2/_handlers.py b/services/web/server/src/simcore_service_webserver/director_v2/_handlers.py index fb80f3aa3fa..36bae2d15e8 100644 --- a/services/web/server/src/simcore_service_webserver/director_v2/_handlers.py +++ b/services/web/server/src/simcore_service_webserver/director_v2/_handlers.py @@ -8,7 +8,7 @@ from models_library.projects import ProjectID from models_library.users import UserID from models_library.utils.json_serialization import json_dumps -from pydantic import BaseModel, Field, ValidationError, parse_obj_as +from pydantic import BaseModel, Field, TypeAdapter, ValidationError from pydantic.types import NonNegativeInt from servicelib.aiohttp.rest_responses import ( create_http_error, @@ -67,7 +67,7 @@ class _ComputationStarted(BaseModel): async def start_computation(request: web.Request) -> web.Response: # pylint: disable=too-many-statements try: - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) computations = ComputationsApi(request.app) run_policy = get_project_run_policy(request.app) @@ -81,7 +81,9 @@ async def start_computation(request: web.Request) -> web.Response: if request.can_read_body: body = await request.json() - assert parse_obj_as(ComputationStart, body) is not None # nosec + assert ( + TypeAdapter(ComputationStart).validate_python(body) is not None + ) # nosec subgraph = body.get("subgraph", []) force_restart = bool(body.get("force_restart", force_restart)) @@ -161,7 +163,9 @@ async def start_computation(request: web.Request) -> web.Response: if project_vc_commits: data["ref_ids"] = project_vc_commits - assert parse_obj_as(_ComputationStarted, data) is not None # nosec + assert ( + TypeAdapter(_ComputationStarted).validate_python(data) is not None + ) # nosec return envelope_json_response(data, status_cls=web.HTTPCreated) @@ -188,7 +192,7 @@ async def start_computation(request: web.Request) -> web.Response: @permission_required("services.pipeline.*") @permission_required("project.read") async def stop_computation(request: web.Request) -> web.Response: - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) computations = ComputationsApi(request.app) run_policy = get_project_run_policy(request.app) assert run_policy # nosec @@ -241,8 +245,7 @@ async def get_computation(request: web.Request) -> web.Response: request, project_id ) _logger.debug("Project %s will get %d variants", project_id, len(project_ids)) - list_computation_tasks = parse_obj_as( - list[ComputationTaskGet], + list_computation_tasks = TypeAdapter(list[ComputationTaskGet]).validate_python( await asyncio.gather( *[ computations.get(project_id=pid, user_id=user_id) @@ -258,7 +261,7 @@ async def get_computation(request: web.Request) -> web.Response: for c in list_computation_tasks ) return web.json_response( - data={"data": list_computation_tasks[0].dict(by_alias=True)}, + data={"data": list_computation_tasks[0].model_dump(by_alias=True)}, dumps=json_dumps, ) except DirectorServiceError as exc: diff --git a/services/web/server/src/simcore_service_webserver/director_v2/_models.py b/services/web/server/src/simcore_service_webserver/director_v2/_models.py index 70dd53ff5fd..e3c391e4ca5 100644 --- a/services/web/server/src/simcore_service_webserver/director_v2/_models.py +++ b/services/web/server/src/simcore_service_webserver/director_v2/_models.py @@ -1,5 +1,3 @@ -from typing import Any, ClassVar - from models_library.clusters import ( CLUSTER_ADMIN_RIGHTS, CLUSTER_MANAGER_RIGHTS, @@ -10,7 +8,7 @@ ExternalClusterAuthentication, ) from models_library.users import GroupID -from pydantic import AnyHttpUrl, BaseModel, Field, validator +from pydantic import AnyHttpUrl, BaseModel, ConfigDict, Field, field_validator from pydantic.networks import AnyUrl, HttpUrl from simcore_postgres_database.models.clusters import ClusterType @@ -33,7 +31,7 @@ class ClusterCreate(BaseCluster): alias="accessRights", default_factory=dict ) - @validator("thumbnail", always=True, pre=True) + @field_validator("thumbnail", mode="before") @classmethod def set_default_thumbnail_if_empty(cls, v, values): if v is None and ( @@ -42,12 +40,12 @@ def set_default_thumbnail_if_empty(cls, v, values): return _DEFAULT_THUMBNAILS[f"{cluster_type}"] return v - class Config(BaseCluster.Config): - schema_extra: ClassVar[dict[str, Any]] = { + model_config = ConfigDict( + json_schema_extra={ "examples": [ { "name": "My awesome cluster", - "type": ClusterType.ON_PREMISE, # can use also values from equivalent enum + "type": ClusterType.ON_PREMISE, # type: ignore[dict-item] # can use also values from equivalent enum "endpoint": "https://registry.osparc-development.fake.dev", "authentication": { "type": "simple", @@ -58,7 +56,7 @@ class Config(BaseCluster.Config): { "name": "My AWS cluster", "description": "a AWS cluster administered by me", - "type": ClusterType.AWS, + "type": ClusterType.AWS, # type: ignore[dict-item] "owner": 154, "endpoint": "https://registry.osparc-development.fake.dev", "authentication": { @@ -67,13 +65,14 @@ class Config(BaseCluster.Config): "password": "somepassword", }, "access_rights": { - 154: CLUSTER_ADMIN_RIGHTS, - 12: CLUSTER_MANAGER_RIGHTS, - 7899: CLUSTER_USER_RIGHTS, + 154: CLUSTER_ADMIN_RIGHTS, # type: ignore[dict-item] + 12: CLUSTER_MANAGER_RIGHTS, # type: ignore[dict-item] + 7899: CLUSTER_USER_RIGHTS, # type: ignore[dict-item] }, }, ] } + ) class ClusterPatch(BaseCluster): diff --git a/services/web/server/src/simcore_service_webserver/director_v2/settings.py b/services/web/server/src/simcore_service_webserver/director_v2/settings.py index d182ad6df28..21cb368ff50 100644 --- a/services/web/server/src/simcore_service_webserver/director_v2/settings.py +++ b/services/web/server/src/simcore_service_webserver/director_v2/settings.py @@ -6,7 +6,7 @@ from aiohttp import ClientSession, ClientTimeout, web from models_library.basic_types import VersionTag -from pydantic import Field, PositiveInt +from pydantic import AliasChoices, Field, PositiveInt from servicelib.aiohttp.application_keys import APP_CLIENT_SESSION_KEY from settings_library.base import BaseCustomSettings from settings_library.basic_types import PortInt @@ -36,9 +36,9 @@ def base_url(self) -> URL: DIRECTOR_V2_RESTART_DYNAMIC_SERVICE_TIMEOUT: PositiveInt = Field( 1 * _MINUTE, description="timeout of containers restart", - envs=[ + validation_alias=AliasChoices( "DIRECTOR_V2_RESTART_DYNAMIC_SERVICE_TIMEOUT", - ], + ), ) DIRECTOR_V2_STORAGE_SERVICE_UPLOAD_DOWNLOAD_TIMEOUT: PositiveInt = Field( @@ -49,9 +49,9 @@ def base_url(self) -> URL: "such payloads it is required to have long timeouts which " "allow the service to finish the operation." ), - envs=[ + validation_alias=AliasChoices( "DIRECTOR_V2_DYNAMIC_SERVICE_DATA_UPLOAD_DOWNLOAD_TIMEOUT", - ], + ), ) def get_service_retrieve_timeout(self) -> ClientTimeout: diff --git a/services/web/server/src/simcore_service_webserver/dynamic_scheduler/settings.py b/services/web/server/src/simcore_service_webserver/dynamic_scheduler/settings.py index b92a0e2d432..2ebe8bc141c 100644 --- a/services/web/server/src/simcore_service_webserver/dynamic_scheduler/settings.py +++ b/services/web/server/src/simcore_service_webserver/dynamic_scheduler/settings.py @@ -1,7 +1,7 @@ from typing import Final from aiohttp import web -from pydantic import Field, NonNegativeInt +from pydantic import AliasChoices, Field, NonNegativeInt from settings_library.base import BaseCustomSettings from settings_library.utils_service import MixinServiceSettings @@ -23,10 +23,10 @@ class DynamicSchedulerSettings(BaseCustomSettings, MixinServiceSettings): "- director-v* requests save_state and uses a 01:00:00 timeout" "The +10 seconds is used to make sure the director replies" ), - envs=[ + validation_alias=AliasChoices( "DIRECTOR_V2_STOP_SERVICE_TIMEOUT", "DYNAMIC_SCHEDULER_STOP_SERVICE_TIMEOUT", - ], + ), ) diff --git a/services/web/server/src/simcore_service_webserver/errors.py b/services/web/server/src/simcore_service_webserver/errors.py index 173699f5888..ac21f882297 100644 --- a/services/web/server/src/simcore_service_webserver/errors.py +++ b/services/web/server/src/simcore_service_webserver/errors.py @@ -1,8 +1,5 @@ -from typing import Any - -from models_library.errors_classes import OsparcErrorMixin +from common_library.errors_classes import OsparcErrorMixin class WebServerBaseError(OsparcErrorMixin, Exception): - def __init__(self, **ctx: Any) -> None: - super().__init__(**ctx) + """WebServer base error.""" diff --git a/services/web/server/src/simcore_service_webserver/exporter/_formatter/_sds.py b/services/web/server/src/simcore_service_webserver/exporter/_formatter/_sds.py index 994d06690de..3bb91820619 100644 --- a/services/web/server/src/simcore_service_webserver/exporter/_formatter/_sds.py +++ b/services/web/server/src/simcore_service_webserver/exporter/_formatter/_sds.py @@ -5,7 +5,7 @@ from typing import Any, Final from aiohttp import web -from pydantic import parse_obj_as +from pydantic import TypeAdapter from servicelib.pools import non_blocking_process_pool_executor from ...catalog.client import get_service @@ -79,8 +79,7 @@ async def _add_rrid_entries( continue rrid_entires.append( - parse_obj_as( - RRIDEntry, + TypeAdapter(RRIDEntry).validate_python( { "rrid_term": scicrunch_resource.name, "rrid_identifier": scicrunch_resource.rrid, @@ -158,8 +157,7 @@ async def create_sds_directory( _logger.debug("Project data: %s", project_data) # assemble params here - dataset_description_params = parse_obj_as( - DatasetDescriptionParams, + dataset_description_params = TypeAdapter(DatasetDescriptionParams).validate_python( {"name": project_data["name"], "description": project_data["description"]}, ) diff --git a/services/web/server/src/simcore_service_webserver/folders/_folders_handlers.py b/services/web/server/src/simcore_service_webserver/folders/_folders_handlers.py index a7ab54530c4..8d02216b19a 100644 --- a/services/web/server/src/simcore_service_webserver/folders/_folders_handlers.py +++ b/services/web/server/src/simcore_service_webserver/folders/_folders_handlers.py @@ -16,7 +16,7 @@ from models_library.users import UserID from models_library.utils.common_validators import null_or_none_str_to_none_validator from models_library.workspaces import WorkspaceID -from pydantic import Extra, Field, Json, parse_obj_as, validator +from pydantic import ConfigDict, Field, Json, TypeAdapter, field_validator from servicelib.aiohttp.requests_validation import ( RequestParams, StrictRequestParams, @@ -93,7 +93,7 @@ class FolderListWithJsonStrQueryParams(PageQueryParameters): order_by: Json[OrderBy] = Field( default=OrderBy(field=IDStr("modified"), direction=OrderDirection.DESC), description="Order by field (modified_at|name|description) and direction (asc|desc). The default sorting order is ascending.", - example='{"field": "name", "direction": "desc"}', + examples=['{"field": "name", "direction": "desc"}'], alias="order_by", ) folder_id: FolderID | None = Field( @@ -105,7 +105,7 @@ class FolderListWithJsonStrQueryParams(PageQueryParameters): description="List folders in specific workspace. By default, list in the user private workspace", ) - @validator("order_by", check_fields=False) + @field_validator("order_by", check_fields=False) @classmethod def validate_order_by_field(cls, v): if v.field not in { @@ -119,16 +119,15 @@ def validate_order_by_field(cls, v): v.field = "modified" return v - class Config: - extra = Extra.forbid + model_config = ConfigDict(extra="forbid") # validators - _null_or_none_str_to_none_validator = validator( - "folder_id", allow_reuse=True, pre=True - )(null_or_none_str_to_none_validator) + _null_or_none_str_to_none_validator = field_validator("folder_id", mode="before")( + null_or_none_str_to_none_validator + ) - _null_or_none_str_to_none_validator2 = validator( - "workspace_id", allow_reuse=True, pre=True + _null_or_none_str_to_none_validator2 = field_validator( + "workspace_id", mode="before" )(null_or_none_str_to_none_validator) @@ -137,7 +136,7 @@ class Config: @permission_required("folder.create") @handle_folders_exceptions async def create_folder(request: web.Request): - req_ctx = FoldersRequestContext.parse_obj(request) + req_ctx = FoldersRequestContext.model_validate(request) body_params = await parse_request_body_as(CreateFolderBodyParams, request) folder = await _folders_api.create_folder( @@ -157,7 +156,7 @@ async def create_folder(request: web.Request): @permission_required("folder.read") @handle_folders_exceptions async def list_folders(request: web.Request): - req_ctx = FoldersRequestContext.parse_obj(request) + req_ctx = FoldersRequestContext.model_validate(request) query_params: FolderListWithJsonStrQueryParams = parse_request_query_parameters_as( FolderListWithJsonStrQueryParams, request ) @@ -170,10 +169,10 @@ async def list_folders(request: web.Request): workspace_id=query_params.workspace_id, offset=query_params.offset, limit=query_params.limit, - order_by=parse_obj_as(OrderBy, query_params.order_by), + order_by=TypeAdapter(OrderBy).validate_python(query_params.order_by), ) - page = Page[FolderGet].parse_obj( + page = Page[FolderGet].model_validate( paginate_data( chunk=folders.items, request_url=request.url, @@ -193,7 +192,7 @@ async def list_folders(request: web.Request): @permission_required("folder.read") @handle_folders_exceptions async def get_folder(request: web.Request): - req_ctx = FoldersRequestContext.parse_obj(request) + req_ctx = FoldersRequestContext.model_validate(request) path_params = parse_request_path_parameters_as(FoldersPathParams, request) folder: FolderGet = await _folders_api.get_folder( @@ -214,7 +213,7 @@ async def get_folder(request: web.Request): @permission_required("folder.update") @handle_folders_exceptions async def replace_folder(request: web.Request): - req_ctx = FoldersRequestContext.parse_obj(request) + req_ctx = FoldersRequestContext.model_validate(request) path_params = parse_request_path_parameters_as(FoldersPathParams, request) body_params = await parse_request_body_as(PutFolderBodyParams, request) @@ -237,7 +236,7 @@ async def replace_folder(request: web.Request): @permission_required("folder.delete") @handle_folders_exceptions async def delete_folder_group(request: web.Request): - req_ctx = FoldersRequestContext.parse_obj(request) + req_ctx = FoldersRequestContext.model_validate(request) path_params = parse_request_path_parameters_as(FoldersPathParams, request) await _folders_api.delete_folder( diff --git a/services/web/server/src/simcore_service_webserver/groups/_classifiers.py b/services/web/server/src/simcore_service_webserver/groups/_classifiers.py index 9c79ab5376c..a7a419bd694 100644 --- a/services/web/server/src/simcore_service_webserver/groups/_classifiers.py +++ b/services/web/server/src/simcore_service_webserver/groups/_classifiers.py @@ -9,14 +9,20 @@ """ import logging -import re -from typing import Any, Final, Literal +from typing import Annotated, Any, Final, Literal, TypeAlias import sqlalchemy as sa from aiohttp import web from aiopg.sa.result import RowProxy -from common_library.pydantic_basic_types import ConstrainedStr -from pydantic import BaseModel, Field, HttpUrl, ValidationError, parse_obj_as, validator +from pydantic import ( + BaseModel, + Field, + HttpUrl, + StringConstraints, + TypeAdapter, + ValidationError, + field_validator, +) from simcore_postgres_database.models.classifiers import group_classifiers from ..db.plugin import get_database_engine @@ -30,8 +36,9 @@ # DOMAIN MODELS --- -class TreePath(ConstrainedStr): - regex = re.compile(r"[\w:]+") # Examples 'a::b::c +TreePath: TypeAlias = Annotated[ + str, StringConstraints(pattern=r"[\w:]+") +] # Examples 'a::b::c class ClassifierItem(BaseModel): @@ -43,10 +50,10 @@ class ClassifierItem(BaseModel): url: HttpUrl | None = Field( None, description="Link to more information", - example="https://scicrunch.org/resources/Any/search?q=osparc&l=osparc", + examples=["https://scicrunch.org/resources/Any/search?q=osparc&l=osparc"], ) - @validator("short_description", pre=True) + @field_validator("short_description", mode="before") @classmethod def truncate_to_short(cls, v): if v and len(v) >= MAX_SIZE_SHORT_MSG: @@ -84,7 +91,9 @@ async def get_classifiers_from_bundle(self, gid: int) -> dict[str, Any]: if bundle: try: # truncate bundle to what is needed and drop the rest - return Classifiers(**bundle).dict(exclude_unset=True, exclude_none=True) + return Classifiers(**bundle).model_dump( + exclude_unset=True, exclude_none=True + ) except ValidationError as err: _logger.error( "DB corrupt data in 'groups_classifiers' table. " @@ -129,7 +138,9 @@ async def build_rrids_tree_view( url=scicrunch.get_resolver_web_url(resource.rrid), ) - node = parse_obj_as(TreePath, validated_item.display_name.replace(":", " ")) + node = TypeAdapter(TreePath).validate_python( + validated_item.display_name.replace(":", " ") + ) flat_tree_view[node] = validated_item except ValidationError as err: @@ -137,4 +148,6 @@ async def build_rrids_tree_view( "Cannot convert RRID into a classifier item. Skipping. Details: %s", err ) - return Classifiers.construct(classifiers=flat_tree_view).dict(exclude_unset=True) + return Classifiers.model_construct(classifiers=flat_tree_view).model_dump( + exclude_unset=True + ) diff --git a/services/web/server/src/simcore_service_webserver/groups/_db.py b/services/web/server/src/simcore_service_webserver/groups/_db.py index 38bbb4e7d7c..509251de5c0 100644 --- a/services/web/server/src/simcore_service_webserver/groups/_db.py +++ b/services/web/server/src/simcore_service_webserver/groups/_db.py @@ -6,7 +6,7 @@ from aiopg.sa.result import ResultProxy, RowProxy from models_library.groups import GroupAtDB from models_library.users import GroupID, UserID -from pydantic import parse_obj_as +from pydantic import TypeAdapter from simcore_postgres_database.utils_products import get_or_create_product_group from sqlalchemy import and_, literal_column from sqlalchemy.dialects.postgresql import insert @@ -117,7 +117,7 @@ async def get_all_user_groups(conn: SAConnection, user_id: UserID) -> list[Group .where(user_to_groups.c.uid == user_id) ) rows = await result.fetchall() or [] - return [parse_obj_as(GroupAtDB, row) for row in rows] + return [TypeAdapter(GroupAtDB).validate_python(row) for row in rows] async def get_user_group( diff --git a/services/web/server/src/simcore_service_webserver/groups/_handlers.py b/services/web/server/src/simcore_service_webserver/groups/_handlers.py index 2f0b0411601..46c5e48f890 100644 --- a/services/web/server/src/simcore_service_webserver/groups/_handlers.py +++ b/services/web/server/src/simcore_service_webserver/groups/_handlers.py @@ -12,7 +12,7 @@ from models_library.emails import LowerCaseEmailStr from models_library.users import GroupID, UserID from models_library.utils.json_serialization import json_dumps -from pydantic import BaseModel, Extra, Field, parse_obj_as +from pydantic import BaseModel, ConfigDict, Field, TypeAdapter from servicelib.aiohttp.requests_validation import ( parse_request_path_parameters_as, parse_request_query_parameters_as, @@ -82,7 +82,7 @@ async def list_groups(request: web.Request): """ product: Product = get_current_product(request) - req_ctx = _GroupsRequestContext.parse_obj(request) + req_ctx = _GroupsRequestContext.model_validate(request) primary_group, user_groups, all_group = await api.list_user_groups_with_read_access( request.app, req_ctx.user_id @@ -103,15 +103,13 @@ async def list_groups(request: web.Request): product_gid=product.group_id, ) - assert parse_obj_as(AllUsersGroups, result) is not None # nosec + assert TypeAdapter(AllUsersGroups).validate_python(result) is not None # nosec return result class _GroupPathParams(BaseModel): gid: GroupID - - class Config: - extra = Extra.forbid + model_config = ConfigDict(extra="forbid") @routes.get(f"/{API_VTAG}/groups/{{gid}}", name="get_group") @@ -120,11 +118,11 @@ class Config: @_handle_groups_exceptions async def get_group(request: web.Request): """Get one group details""" - req_ctx = _GroupsRequestContext.parse_obj(request) + req_ctx = _GroupsRequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_GroupPathParams, request) group = await api.get_user_group(request.app, req_ctx.user_id, path_params.gid) - assert parse_obj_as(UsersGroup, group) is not None # nosec + assert TypeAdapter(UsersGroup).validate_python(group) is not None # nosec return group @@ -134,11 +132,11 @@ async def get_group(request: web.Request): @_handle_groups_exceptions async def create_group(request: web.Request): """Creates organization groups""" - req_ctx = _GroupsRequestContext.parse_obj(request) + req_ctx = _GroupsRequestContext.model_validate(request) new_group = await request.json() created_group = await api.create_user_group(request.app, req_ctx.user_id, new_group) - assert parse_obj_as(UsersGroup, created_group) is not None # nosec + assert TypeAdapter(UsersGroup).validate_python(created_group) is not None # nosec raise web.HTTPCreated( text=json_dumps({"data": created_group}), content_type=MIMETYPE_APPLICATION_JSON ) @@ -149,14 +147,14 @@ async def create_group(request: web.Request): @permission_required("groups.*") @_handle_groups_exceptions async def update_group(request: web.Request): - req_ctx = _GroupsRequestContext.parse_obj(request) + req_ctx = _GroupsRequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_GroupPathParams, request) new_group_values = await request.json() updated_group = await api.update_user_group( request.app, req_ctx.user_id, path_params.gid, new_group_values ) - assert parse_obj_as(UsersGroup, updated_group) is not None # nosec + assert TypeAdapter(UsersGroup).validate_python(updated_group) is not None # nosec return envelope_json_response(updated_group) @@ -165,7 +163,7 @@ async def update_group(request: web.Request): @permission_required("groups.*") @_handle_groups_exceptions async def delete_group(request: web.Request): - req_ctx = _GroupsRequestContext.parse_obj(request) + req_ctx = _GroupsRequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_GroupPathParams, request) await api.delete_user_group(request.app, req_ctx.user_id, path_params.gid) @@ -177,13 +175,15 @@ async def delete_group(request: web.Request): @permission_required("groups.*") @_handle_groups_exceptions async def get_group_users(request: web.Request): - req_ctx = _GroupsRequestContext.parse_obj(request) + req_ctx = _GroupsRequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_GroupPathParams, request) group_user = await api.list_users_in_group( request.app, req_ctx.user_id, path_params.gid ) - assert parse_obj_as(list[GroupUserGet], group_user) is not None # nosec + assert ( + TypeAdapter(list[GroupUserGet]).validate_python(group_user) is not None + ) # nosec return envelope_json_response(group_user) @@ -195,7 +195,7 @@ async def add_group_user(request: web.Request): """ Adds a user in an organization group """ - req_ctx = _GroupsRequestContext.parse_obj(request) + req_ctx = _GroupsRequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_GroupPathParams, request) new_user_in_group = await request.json() @@ -203,7 +203,7 @@ async def add_group_user(request: web.Request): new_user_id = new_user_in_group["uid"] if "uid" in new_user_in_group else None new_user_email = ( - parse_obj_as(LowerCaseEmailStr, new_user_in_group["email"]) + TypeAdapter(LowerCaseEmailStr).validate_python(new_user_in_group["email"]) if "email" in new_user_in_group else None ) @@ -221,9 +221,7 @@ async def add_group_user(request: web.Request): class _GroupUserPathParams(BaseModel): gid: GroupID uid: UserID - - class Config: - extra = Extra.forbid + model_config = ConfigDict(extra="forbid") @routes.get(f"/{API_VTAG}/groups/{{gid}}/users/{{uid}}", name="get_group_user") @@ -234,12 +232,12 @@ async def get_group_user(request: web.Request): """ Gets specific user in group """ - req_ctx = _GroupsRequestContext.parse_obj(request) + req_ctx = _GroupsRequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_GroupUserPathParams, request) user = await api.get_user_in_group( request.app, req_ctx.user_id, path_params.gid, path_params.uid ) - assert parse_obj_as(GroupUserGet, user) is not None # nosec + assert TypeAdapter(GroupUserGet).validate_python(user) is not None # nosec return envelope_json_response(user) @@ -251,7 +249,7 @@ async def update_group_user(request: web.Request): """ Modify specific user in group """ - req_ctx = _GroupsRequestContext.parse_obj(request) + req_ctx = _GroupsRequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_GroupUserPathParams, request) new_values_for_user_in_group = await request.json() user = await api.update_user_in_group( @@ -261,7 +259,7 @@ async def update_group_user(request: web.Request): path_params.uid, new_values_for_user_in_group, ) - assert parse_obj_as(GroupUserGet, user) is not None # nosec + assert TypeAdapter(GroupUserGet).validate_python(user) is not None # nosec return envelope_json_response(user) @@ -270,7 +268,7 @@ async def update_group_user(request: web.Request): @permission_required("groups.*") @_handle_groups_exceptions async def delete_group_user(request: web.Request): - req_ctx = _GroupsRequestContext.parse_obj(request) + req_ctx = _GroupsRequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_GroupUserPathParams, request) await api.delete_user_in_group( request.app, req_ctx.user_id, path_params.gid, path_params.uid @@ -351,7 +349,7 @@ async def get_scicrunch_resource(request: web.Request): scicrunch = SciCrunch.get_instance(request.app) resource = await scicrunch.get_resource_fields(rrid) - return envelope_json_response(resource.dict()) + return envelope_json_response(resource.model_dump()) @routes.post( @@ -375,7 +373,7 @@ async def add_scicrunch_resource(request: web.Request): # insert new or if exists, then update await repo.upsert(resource) - return envelope_json_response(resource.dict()) + return envelope_json_response(resource.model_dump()) @routes.get( @@ -391,4 +389,4 @@ async def search_scicrunch_resources(request: web.Request): scicrunch = SciCrunch.get_instance(request.app) hits: list[ResourceHit] = await scicrunch.search_resource(guess_name) - return envelope_json_response([hit.dict() for hit in hits]) + return envelope_json_response([hit.model_dump() for hit in hits]) diff --git a/services/web/server/src/simcore_service_webserver/invitations/_client.py b/services/web/server/src/simcore_service_webserver/invitations/_client.py index cc427cf28ce..9bf2a28da32 100644 --- a/services/web/server/src/simcore_service_webserver/invitations/_client.py +++ b/services/web/server/src/simcore_service_webserver/invitations/_client.py @@ -10,7 +10,7 @@ ApiInvitationInputs, ) from models_library.utils.fastapi_encoders import jsonable_encoder -from pydantic import AnyHttpUrl, parse_obj_as +from pydantic import AnyHttpUrl, TypeAdapter from yarl import URL from .._constants import APP_SETTINGS_KEY @@ -86,7 +86,7 @@ async def extract_invitation( url=self._url_vtag("/invitations:extract"), json={"invitation_url": invitation_url}, ) - return parse_obj_as(ApiInvitationContent, await response.json()) + return TypeAdapter(ApiInvitationContent).validate_python(await response.json()) async def generate_invitation( self, params: ApiInvitationInputs @@ -95,7 +95,9 @@ async def generate_invitation( url=self._url_vtag("/invitations"), json=jsonable_encoder(params), ) - return parse_obj_as(ApiInvitationContentAndLink, await response.json()) + return TypeAdapter(ApiInvitationContentAndLink).validate_python( + await response.json() + ) # diff --git a/services/web/server/src/simcore_service_webserver/invitations/_core.py b/services/web/server/src/simcore_service_webserver/invitations/_core.py index b834ac55b26..64a5fa976ed 100644 --- a/services/web/server/src/simcore_service_webserver/invitations/_core.py +++ b/services/web/server/src/simcore_service_webserver/invitations/_core.py @@ -9,7 +9,7 @@ ApiInvitationInputs, ) from models_library.emails import LowerCaseEmailStr -from pydantic import AnyHttpUrl, ValidationError, parse_obj_as +from pydantic import AnyHttpUrl, TypeAdapter, ValidationError from servicelib.aiohttp import status from servicelib.error_codes import create_error_code @@ -94,7 +94,7 @@ async def validate_invitation_url( with _handle_exceptions_as_invitations_errors(): try: - valid_url = parse_obj_as(AnyHttpUrl, invitation_url) + valid_url = TypeAdapter(AnyHttpUrl).validate_python(invitation_url) except ValidationError as err: raise InvalidInvitationError(reason=MSG_INVALID_INVITATION_URL) from err @@ -145,7 +145,7 @@ async def extract_invitation( with _handle_exceptions_as_invitations_errors(): try: - valid_url = parse_obj_as(AnyHttpUrl, invitation_url) + valid_url = TypeAdapter(AnyHttpUrl).validate_python(invitation_url) except ValidationError as err: raise InvalidInvitationError(reason=MSG_INVALID_INVITATION_URL) from err diff --git a/services/web/server/src/simcore_service_webserver/invitations/settings.py b/services/web/server/src/simcore_service_webserver/invitations/settings.py index 025f89955ff..02755291910 100644 --- a/services/web/server/src/simcore_service_webserver/invitations/settings.py +++ b/services/web/server/src/simcore_service_webserver/invitations/settings.py @@ -8,7 +8,7 @@ from typing import Final from aiohttp import web -from pydantic import Field, SecretStr, parse_obj_as +from pydantic import Field, SecretStr, TypeAdapter from settings_library.base import BaseCustomSettings from settings_library.basic_types import PortInt, VersionTag from settings_library.utils_service import ( @@ -19,7 +19,7 @@ from .._constants import APP_SETTINGS_KEY -_INVITATION_VTAG_V1: Final[VersionTag] = parse_obj_as(VersionTag, "v1") +_INVITATION_VTAG_V1: Final[VersionTag] = TypeAdapter(VersionTag).validate_python("v1") class InvitationsSettings(BaseCustomSettings, MixinServiceSettings): diff --git a/services/web/server/src/simcore_service_webserver/login/_auth_handlers.py b/services/web/server/src/simcore_service_webserver/login/_auth_handlers.py index ca1e1a3a18d..db8ee3421e3 100644 --- a/services/web/server/src/simcore_service_webserver/login/_auth_handlers.py +++ b/services/web/server/src/simcore_service_webserver/login/_auth_handlers.py @@ -4,7 +4,7 @@ from aiohttp.web import RouteTableDef from models_library.authentification import TwoFactorAuthentificationMethod from models_library.emails import LowerCaseEmailStr -from pydantic import BaseModel, Field, PositiveInt, SecretStr, parse_obj_as +from pydantic import BaseModel, Field, PositiveInt, SecretStr, TypeAdapter from servicelib.aiohttp import status from servicelib.aiohttp.requests_validation import parse_request_body_as from servicelib.logging_utils import get_log_record_extra, log_context @@ -137,9 +137,9 @@ async def login(request: web.Request): value=user_2fa_authentification_method, ) else: - user_2fa_authentification_method = parse_obj_as( - TwoFactorAuthentificationMethod, user_2fa_preference.value - ) + user_2fa_authentification_method = TypeAdapter( + TwoFactorAuthentificationMethod + ).validate_python(user_2fa_preference.value) if user_2fa_authentification_method == TwoFactorAuthentificationMethod.DISABLED: return await login_granted_response(request, user=user) @@ -275,7 +275,7 @@ async def login_2fa(request: web.Request): class LogoutBody(InputSchema): client_session_id: str | None = Field( - None, example="5ac57685-c40f-448f-8711-70be1936fd63" + None, examples=["5ac57685-c40f-448f-8711-70be1936fd63"] ) diff --git a/services/web/server/src/simcore_service_webserver/login/_models.py b/services/web/server/src/simcore_service_webserver/login/_models.py index 2ac7b94f11a..2684c8a98d1 100644 --- a/services/web/server/src/simcore_service_webserver/login/_models.py +++ b/services/web/server/src/simcore_service_webserver/login/_models.py @@ -1,15 +1,12 @@ from typing import Any, Callable -from pydantic import BaseModel, Extra, SecretStr +from pydantic import BaseModel, ConfigDict, SecretStr from ._constants import MSG_PASSWORD_MISMATCH class InputSchema(BaseModel): - class Config: - allow_population_by_field_name = False - extra = Extra.forbid - allow_mutations = False + model_config = ConfigDict(populate_by_name=False, extra="forbid", frozen=True) def create_password_match_validator( diff --git a/services/web/server/src/simcore_service_webserver/login/_registration.py b/services/web/server/src/simcore_service_webserver/login/_registration.py index 322dbb026c4..8729968a141 100644 --- a/services/web/server/src/simcore_service_webserver/login/_registration.py +++ b/services/web/server/src/simcore_service_webserver/login/_registration.py @@ -18,9 +18,9 @@ Field, Json, PositiveInt, + TypeAdapter, ValidationError, - parse_obj_as, - validator, + field_validator, ) from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON from simcore_postgres_database.models.confirmations import ConfirmationAction @@ -76,7 +76,7 @@ class _InvitationValidator(BaseModel): action: ConfirmationAction data: Json[InvitationData] # pylint: disable=unsubscriptable-object - @validator("action", pre=True) + @field_validator("action", mode="before") @classmethod def ensure_enum(cls, v): if isinstance(v, ConfirmationAction): @@ -190,7 +190,7 @@ async def create_invitation_token( return await db.create_confirmation( user_id=user_id, action=ConfirmationAction.INVITATION.name, - data=data_model.json(), + data=data_model.model_dump_json(), ) @@ -233,7 +233,7 @@ async def extract_email_from_invitation( """Returns associated email""" with _invitations_request_context(invitation_code=invitation_code) as url: content = await extract_invitation(app, invitation_url=f"{url}") - return parse_obj_as(LowerCaseEmailStr, content.guest) + return TypeAdapter(LowerCaseEmailStr).validate_python(content.guest) async def check_and_consume_invitation( @@ -276,7 +276,7 @@ async def check_and_consume_invitation( # database-type invitations if confirmation_token := await validate_confirmation_code(invitation_code, db, cfg): try: - invitation_data: InvitationData = _InvitationValidator.parse_obj( + invitation_data: InvitationData = _InvitationValidator.model_validate( confirmation_token ).data return invitation_data diff --git a/services/web/server/src/simcore_service_webserver/login/_registration_api.py b/services/web/server/src/simcore_service_webserver/login/_registration_api.py index 1dfd1a5a500..eaf6836ae7d 100644 --- a/services/web/server/src/simcore_service_webserver/login/_registration_api.py +++ b/services/web/server/src/simcore_service_webserver/login/_registration_api.py @@ -9,7 +9,7 @@ from models_library.utils.fastapi_encoders import jsonable_encoder from models_library.utils.json_serialization import json_dumps from PIL.Image import Image -from pydantic import EmailStr, PositiveInt, ValidationError, parse_obj_as +from pydantic import EmailStr, PositiveInt, TypeAdapter, ValidationError from servicelib.utils_secrets import generate_passcode from ..email.utils import send_email_from_template @@ -66,7 +66,9 @@ async def send_account_request_email_to_support( support_email = product.support_email email_template_path = await get_product_template_path(request, template_name) try: - user_email = parse_obj_as(LowerCaseEmailStr, request_form.get("email", None)) + user_email = TypeAdapter(LowerCaseEmailStr).validate_python( + request_form.get("email", None) + ) except ValidationError: user_email = None diff --git a/services/web/server/src/simcore_service_webserver/login/_registration_handlers.py b/services/web/server/src/simcore_service_webserver/login/_registration_handlers.py index 2ad608e3c12..26518312d7b 100644 --- a/services/web/server/src/simcore_service_webserver/login/_registration_handlers.py +++ b/services/web/server/src/simcore_service_webserver/login/_registration_handlers.py @@ -97,7 +97,7 @@ class _AuthenticatedContext(BaseModel): @login_required @permission_required("user.profile.delete") async def unregister_account(request: web.Request): - req_ctx = _AuthenticatedContext.parse_obj(request) + req_ctx = _AuthenticatedContext.model_validate(request) body = await parse_request_body_as(UnregisterCheck, request) product: Product = get_current_product(request) diff --git a/services/web/server/src/simcore_service_webserver/login/handlers_confirmation.py b/services/web/server/src/simcore_service_webserver/login/handlers_confirmation.py index ecb99ce84e7..9d2d5a4dc46 100644 --- a/services/web/server/src/simcore_service_webserver/login/handlers_confirmation.py +++ b/services/web/server/src/simcore_service_webserver/login/handlers_confirmation.py @@ -11,8 +11,8 @@ Field, PositiveInt, SecretStr, + TypeAdapter, ValidationError, - parse_obj_as, validator, ) from servicelib.aiohttp import status @@ -109,7 +109,11 @@ async def _handle_confirm_change_email( # update and consume confirmation token await db.delete_confirmation_and_update_user( user_id=user_id, - updates={"email": parse_obj_as(LowerCaseEmailStr, confirmation["data"])}, + updates={ + "email": TypeAdapter(LowerCaseEmailStr).validate_python( + confirmation["data"] + ) + }, confirmation=confirmation, ) diff --git a/services/web/server/src/simcore_service_webserver/login/handlers_registration.py b/services/web/server/src/simcore_service_webserver/login/handlers_registration.py index d3f553db71c..16099132630 100644 --- a/services/web/server/src/simcore_service_webserver/login/handlers_registration.py +++ b/services/web/server/src/simcore_service_webserver/login/handlers_registration.py @@ -1,11 +1,18 @@ import logging -from datetime import datetime, timedelta -from typing import Any, ClassVar, Literal +from datetime import datetime, timedelta, timezone +from typing import Literal from aiohttp import web from aiohttp.web import RouteTableDef from models_library.emails import LowerCaseEmailStr -from pydantic import BaseModel, Field, PositiveInt, SecretStr, validator +from pydantic import ( + BaseModel, + ConfigDict, + Field, + PositiveInt, + SecretStr, + field_validator, +) from servicelib.aiohttp import status from servicelib.aiohttp.requests_validation import parse_request_body_as from servicelib.error_codes import create_error_code @@ -114,12 +121,9 @@ class RegisterBody(InputSchema): confirm: SecretStr | None = Field(None, description="Password confirmation") invitation: str | None = Field(None, description="Invitation code") - _password_confirm_match = validator("confirm", allow_reuse=True)( - check_confirm_password_match - ) - - class Config: - schema_extra: ClassVar[dict[str, Any]] = { + _password_confirm_match = field_validator("confirm")(check_confirm_password_match) + model_config = ConfigDict( + json_schema_extra={ "examples": [ { "email": "foo@mymail.com", @@ -129,6 +133,7 @@ class Config: } ] } + ) @routes.post(f"/{API_VTAG}/auth/register", name="auth_register") @@ -203,7 +208,9 @@ async def register(request: web.Request): app=request.app, ) if invitation.trial_account_days: - expires_at = datetime.utcnow() + timedelta(invitation.trial_account_days) + expires_at = datetime.now(timezone.utc) + timedelta( + invitation.trial_account_days + ) # get authorized user or create new user = await _auth_api.get_user_by_email(request.app, email=registration.email) diff --git a/services/web/server/src/simcore_service_webserver/login/settings.py b/services/web/server/src/simcore_service_webserver/login/settings.py index c32ce319c7f..90adaa976be 100644 --- a/services/web/server/src/simcore_service_webserver/login/settings.py +++ b/services/web/server/src/simcore_service_webserver/login/settings.py @@ -2,7 +2,7 @@ from typing import Final, Literal from aiohttp import web -from pydantic import BaseModel, validator +from pydantic import BaseModel, field_validator from pydantic.fields import Field from pydantic.types import PositiveFloat, PositiveInt, SecretStr from settings_library.base import BaseCustomSettings @@ -36,8 +36,8 @@ class LoginSettings(BaseCustomSettings): ) LOGIN_TWILIO: TwilioSettings | None = Field( - auto_default_from_env=True, description="Twilio service settings. Used to send SMS for 2FA", + json_schema_extra={"auto_default_from_env": True}, ) LOGIN_2FA_CODE_EXPIRATION_SEC: PositiveInt = Field( @@ -54,7 +54,7 @@ class LoginSettings(BaseCustomSettings): description="Minimum length of password", ) - @validator("LOGIN_2FA_REQUIRED") + @field_validator("LOGIN_2FA_REQUIRED") @classmethod def login_2fa_needs_email_registration(cls, v, values): # NOTE: this constraint ensures that a phone is registered in current workflow @@ -63,7 +63,7 @@ def login_2fa_needs_email_registration(cls, v, values): raise ValueError(msg) return v - @validator("LOGIN_2FA_REQUIRED") + @field_validator("LOGIN_2FA_REQUIRED") @classmethod def login_2fa_needs_sms_service(cls, v, values): if v and values.get("LOGIN_TWILIO") is None: @@ -94,7 +94,10 @@ def create_from_composition( """ For the LoginSettings, product-specific settings override app-specifics settings """ - composed_settings = {**app_login_settings.dict(), **product_login_settings} + composed_settings = { + **app_login_settings.model_dump(), + **product_login_settings, + } if "two_factor_enabled" in composed_settings: # legacy safe diff --git a/services/web/server/src/simcore_service_webserver/long_running_tasks.py b/services/web/server/src/simcore_service_webserver/long_running_tasks.py index a7e4e8c725b..2f42f18927e 100644 --- a/services/web/server/src/simcore_service_webserver/long_running_tasks.py +++ b/services/web/server/src/simcore_service_webserver/long_running_tasks.py @@ -28,7 +28,7 @@ async def _test_task_context_decorator( request: web.Request, ) -> web.StreamResponse: """this task context callback tries to get the user_id from the query if available""" - req_ctx = _RequestContext.parse_obj(request) + req_ctx = _RequestContext.model_validate(request) request[RQT_LONG_RUNNING_TASKS_CONTEXT_KEY] = jsonable_encoder(req_ctx) return await handler(request) diff --git a/services/web/server/src/simcore_service_webserver/meta_modeling/_function_nodes.py b/services/web/server/src/simcore_service_webserver/meta_modeling/_function_nodes.py index c51f3374510..3e0e3a630f7 100644 --- a/services/web/server/src/simcore_service_webserver/meta_modeling/_function_nodes.py +++ b/services/web/server/src/simcore_service_webserver/meta_modeling/_function_nodes.py @@ -37,7 +37,7 @@ def create_param_node_from_iterator_with_outputs(iterator_node: Node) -> Node: label=iterator_node.label, inputs={}, inputNodes=[], - thumbnail="", # type: ignore[arg-type] # NOTE: hack due to issue in projects json-schema + thumbnail="", # NOTE: hack due to issue in projects json-schema outputs=deepcopy(iterator_node.outputs), ) diff --git a/services/web/server/src/simcore_service_webserver/meta_modeling/_handlers.py b/services/web/server/src/simcore_service_webserver/meta_modeling/_handlers.py index 35244dc5363..4a3ef629cac 100644 --- a/services/web/server/src/simcore_service_webserver/meta_modeling/_handlers.py +++ b/services/web/server/src/simcore_service_webserver/meta_modeling/_handlers.py @@ -9,7 +9,7 @@ from models_library.projects import ProjectID from models_library.rest_pagination import Page, PageQueryParameters from models_library.rest_pagination_utils import paginate_data -from pydantic import BaseModel, ValidationError, validator +from pydantic import BaseModel, ValidationError, field_validator from pydantic.fields import Field from pydantic.networks import HttpUrl from servicelib.rest_constants import RESPONSE_MODEL_POLICY @@ -33,7 +33,7 @@ class ParametersModel(PageQueryParameters): project_uuid: ProjectID ref_id: CommitID - @validator("ref_id", pre=True) + @field_validator("ref_id", mode="before") @classmethod def tags_as_refid_not_implemented(cls, v): try: @@ -292,7 +292,7 @@ async def list_project_iterations(request: web.Request) -> web.Response: for item in iterations_range.items ] - page = Page[ProjectIterationItem].parse_obj( + page = Page[ProjectIterationItem].model_validate( paginate_data( chunk=page_items, request_url=request.url, @@ -395,7 +395,7 @@ def _get_project_results(project_id) -> ExtractedResults: for item in iterations_range.items ] - page = Page[ProjectIterationResultItem].parse_obj( + page = Page[ProjectIterationResultItem].model_validate( paginate_data( chunk=page_items, request_url=request.url, diff --git a/services/web/server/src/simcore_service_webserver/meta_modeling/_iterations.py b/services/web/server/src/simcore_service_webserver/meta_modeling/_iterations.py index c4b16f12caf..1663d7ac96d 100644 --- a/services/web/server/src/simcore_service_webserver/meta_modeling/_iterations.py +++ b/services/web/server/src/simcore_service_webserver/meta_modeling/_iterations.py @@ -156,7 +156,7 @@ def from_tag_name( ) -> Optional["ProjectIteration"]: """Parses iteration info from tag name""" try: - return cls.parse_obj(parse_iteration_tag_name(tag_name)) + return cls.model_validate(parse_iteration_tag_name(tag_name)) except ValidationError as err: if return_none_if_fails: _logger.debug("%s", f"{err=}") @@ -218,7 +218,7 @@ async def get_or_create_runnable_projects( raise web.HTTPForbidden(reason="Unauthenticated request") from err project_nodes: dict[NodeID, Node] = { - nid: Node.parse_obj(n) for nid, n in project["workbench"].items() + nid: Node.model_validate(n) for nid, n in project["workbench"].items() } # init returns @@ -326,7 +326,7 @@ async def get_runnable_projects_ids( project: ProjectDict = await vc_repo.get_project(str(project_uuid)) assert project["uuid"] == str(project_uuid) # nosec project_nodes: dict[NodeID, Node] = { - nid: Node.parse_obj(n) for nid, n in project["workbench"].items() + nid: Node.model_validate(n) for nid, n in project["workbench"].items() } # init returns diff --git a/services/web/server/src/simcore_service_webserver/meta_modeling/_results.py b/services/web/server/src/simcore_service_webserver/meta_modeling/_results.py index 68829e3489a..51bcfadb663 100644 --- a/services/web/server/src/simcore_service_webserver/meta_modeling/_results.py +++ b/services/web/server/src/simcore_service_webserver/meta_modeling/_results.py @@ -7,18 +7,16 @@ import logging -from typing import Any +from typing import Annotated, Any, TypeAlias from models_library.projects_nodes import OutputsDict from models_library.projects_nodes_io import NodeIDStr -from pydantic import BaseModel, ConstrainedInt, Field +from pydantic import BaseModel, ConfigDict, Field _logger = logging.getLogger(__name__) -class ProgressInt(ConstrainedInt): - ge = 0 - le = 100 +ProgressInt: TypeAlias = Annotated[int, Field(ge=0, le=100)] class ExtractedResults(BaseModel): @@ -31,9 +29,8 @@ class ExtractedResults(BaseModel): values: dict[NodeIDStr, OutputsDict] = Field( ..., description="Captured outputs per node" ) - - class Config: - schema_extra = { + model_config = ConfigDict( + json_schema_extra={ "example": { # sample with 2 computational services, 2 data sources (iterator+parameter) and 2 observers (probes) "progress": { @@ -57,6 +54,7 @@ class Config: }, } } + ) def extract_project_results(workbench: dict[str, Any]) -> ExtractedResults: @@ -112,5 +110,5 @@ def extract_project_results(workbench: dict[str, Any]) -> ExtractedResults: values = node["outputs"] results[noid], labels[noid] = values, label - res = ExtractedResults(progress=progress, labels=labels, values=results) # type: ignore[arg-type] + res = ExtractedResults(progress=progress, labels=labels, values=results) return res diff --git a/services/web/server/src/simcore_service_webserver/notifications/_rabbitmq_exclusive_queue_consumers.py b/services/web/server/src/simcore_service_webserver/notifications/_rabbitmq_exclusive_queue_consumers.py index d9a6b1f0861..29cb62fb2d7 100644 --- a/services/web/server/src/simcore_service_webserver/notifications/_rabbitmq_exclusive_queue_consumers.py +++ b/services/web/server/src/simcore_service_webserver/notifications/_rabbitmq_exclusive_queue_consumers.py @@ -13,7 +13,7 @@ ) from models_library.socketio import SocketMessageDict from models_library.users import GroupID -from pydantic import parse_raw_as +from pydantic import TypeAdapter, parse_raw_as from servicelib.logging_utils import log_catch, log_context from servicelib.rabbitmq import RabbitMQClient from servicelib.utils import logged_gather @@ -67,8 +67,10 @@ async def _convert_to_node_update_event( async def _progress_message_parser(app: web.Application, data: bytes) -> bool: rabbit_message: ( ProgressRabbitMessageNode | ProgressRabbitMessageProject - ) = parse_raw_as( - ProgressRabbitMessageNode | ProgressRabbitMessageProject, data # type: ignore[arg-type] # from pydantic v2 --> https://github.com/pydantic/pydantic/discussions/4950 + ) = TypeAdapter( + ProgressRabbitMessageNode | ProgressRabbitMessageProject + ).validate_json( + data ) message: SocketMessageDict | None = None if isinstance(rabbit_message, ProgressRabbitMessageProject): diff --git a/services/web/server/src/simcore_service_webserver/payments/_autorecharge_db.py b/services/web/server/src/simcore_service_webserver/payments/_autorecharge_db.py index 8aec3e45359..8123343e44c 100644 --- a/services/web/server/src/simcore_service_webserver/payments/_autorecharge_db.py +++ b/services/web/server/src/simcore_service_webserver/payments/_autorecharge_db.py @@ -6,7 +6,7 @@ from models_library.basic_types import NonNegativeDecimal from models_library.users import UserID from models_library.wallets import WalletID -from pydantic import BaseModel, PositiveInt +from pydantic import BaseModel, ConfigDict, PositiveInt from simcore_postgres_database.utils_payments_autorecharge import AutoRechargeStmts from ..db.plugin import get_database_engine @@ -23,10 +23,8 @@ class PaymentsAutorechargeDB(BaseModel): enabled: bool primary_payment_method_id: PaymentMethodID top_up_amount_in_usd: NonNegativeDecimal - monthly_limit_in_usd: NonNegativeDecimal | None - - class Config: - orm_mode = True + monthly_limit_in_usd: NonNegativeDecimal | None = None + model_config = ConfigDict(from_attributes=True) async def get_wallet_autorecharge( diff --git a/services/web/server/src/simcore_service_webserver/payments/_methods_api.py b/services/web/server/src/simcore_service_webserver/payments/_methods_api.py index a1eac2b440d..e303a382f35 100644 --- a/services/web/server/src/simcore_service_webserver/payments/_methods_api.py +++ b/services/web/server/src/simcore_service_webserver/payments/_methods_api.py @@ -15,7 +15,7 @@ from models_library.products import ProductName from models_library.users import UserID from models_library.wallets import WalletID -from pydantic import HttpUrl, parse_obj_as +from pydantic import HttpUrl, TypeAdapter from servicelib.logging_utils import log_decorator from simcore_postgres_database.models.payments_methods import InitPromptAckFlowState from yarl import URL @@ -56,7 +56,7 @@ def _to_api_model( ) -> PaymentMethodGet: assert entry.completed_at # nosec - return PaymentMethodGet.parse_obj( + return PaymentMethodGet.model_validate( { **payment_method_details_from_gateway, "idr": entry.payment_method_id, @@ -97,7 +97,7 @@ async def _fake_init_creation_of_wallet_payment_method( return PaymentMethodInitiated( wallet_id=wallet_id, payment_method_id=payment_method_id, - payment_method_form_url=parse_obj_as(HttpUrl, f"{form_link}"), + payment_method_form_url=TypeAdapter(HttpUrl).validate_python(f"{form_link}"), ) diff --git a/services/web/server/src/simcore_service_webserver/payments/_methods_db.py b/services/web/server/src/simcore_service_webserver/payments/_methods_db.py index b5838eb171c..e453c55001a 100644 --- a/services/web/server/src/simcore_service_webserver/payments/_methods_db.py +++ b/services/web/server/src/simcore_service_webserver/payments/_methods_db.py @@ -8,7 +8,7 @@ from models_library.api_schemas_webserver.wallets import PaymentMethodID from models_library.users import UserID from models_library.wallets import WalletID -from pydantic import BaseModel, parse_obj_as +from pydantic import BaseModel, ConfigDict, TypeAdapter from simcore_postgres_database.models.payments_methods import ( InitPromptAckFlowState, payments_methods, @@ -36,8 +36,9 @@ class PaymentsMethodsDB(BaseModel): state: InitPromptAckFlowState state_message: str | None - class Config: - orm_mode = True + model_config = ConfigDict( + from_attributes=True, + ) async def insert_init_payment_method( @@ -81,7 +82,7 @@ async def list_successful_payment_methods( .order_by(payments_methods.c.created.desc()) ) # newest first rows = await result.fetchall() or [] - return parse_obj_as(list[PaymentsMethodsDB], rows) + return TypeAdapter(list[PaymentsMethodsDB]).validate_python(rows) async def get_successful_payment_method( @@ -104,7 +105,7 @@ async def get_successful_payment_method( if row is None: raise PaymentMethodNotFoundError(payment_method_id=payment_method_id) - return PaymentsMethodsDB.from_orm(row) + return PaymentsMethodsDB.model_validate(row) async def get_pending_payment_methods_ids( @@ -117,7 +118,10 @@ async def get_pending_payment_methods_ids( .order_by(payments_methods.c.initiated_at.asc()) # oldest first ) rows = await result.fetchall() or [] - return [parse_obj_as(PaymentMethodID, row.payment_method_id) for row in rows] + return [ + TypeAdapter(PaymentMethodID).validate_python(row.payment_method_id) + for row in rows + ] async def udpate_payment_method( @@ -168,7 +172,7 @@ async def udpate_payment_method( row = await result.first() assert row, "execute above should have caught this" # nosec - return PaymentsMethodsDB.from_orm(row) + return PaymentsMethodsDB.model_validate(row) async def delete_payment_method( diff --git a/services/web/server/src/simcore_service_webserver/payments/_onetime_api.py b/services/web/server/src/simcore_service_webserver/payments/_onetime_api.py index 4a515216191..a3d30508677 100644 --- a/services/web/server/src/simcore_service_webserver/payments/_onetime_api.py +++ b/services/web/server/src/simcore_service_webserver/payments/_onetime_api.py @@ -1,6 +1,6 @@ import logging from decimal import Decimal -from typing import Any, cast +from typing import Any from uuid import uuid4 import arrow @@ -15,7 +15,7 @@ from models_library.products import ProductName from models_library.users import UserID from models_library.wallets import WalletID -from pydantic import HttpUrl, parse_obj_as +from pydantic import HttpUrl, TypeAdapter from servicelib.logging_utils import log_decorator from simcore_postgres_database.models.payments_transactions import ( PaymentTransactionState, @@ -62,7 +62,7 @@ def _to_api_model( if transaction.invoice_url: data["invoice_url"] = transaction.invoice_url - return PaymentTransaction.parse_obj(data) + return PaymentTransaction.model_validate(data) @log_decorator(_logger, level=logging.INFO) @@ -81,7 +81,7 @@ async def _fake_init_payment( # get_form_payment_url settings: PaymentsSettings = get_plugin_settings(app) external_form_link = ( - URL(settings.PAYMENTS_FAKE_GATEWAY_URL) + URL(f"{settings.PAYMENTS_FAKE_GATEWAY_URL}") .with_path("/pay") .with_query(id=payment_id) ) @@ -235,8 +235,8 @@ async def _fake_get_payment_invoice_url( assert user_id # nosec assert wallet_id # nosec - return cast( - HttpUrl, parse_obj_as(HttpUrl, f"https://fake-invoice.com/?id={payment_id}") + return TypeAdapter(HttpUrl).validate_python( + f"https://fake-invoice.com/?id={payment_id}", ) diff --git a/services/web/server/src/simcore_service_webserver/payments/_onetime_db.py b/services/web/server/src/simcore_service_webserver/payments/_onetime_db.py index 9f94d46b707..fcd1d4be9d9 100644 --- a/services/web/server/src/simcore_service_webserver/payments/_onetime_db.py +++ b/services/web/server/src/simcore_service_webserver/payments/_onetime_db.py @@ -9,7 +9,7 @@ from models_library.products import ProductName from models_library.users import UserID from models_library.wallets import WalletID -from pydantic import BaseModel, HttpUrl, PositiveInt, parse_obj_as +from pydantic import BaseModel, ConfigDict, HttpUrl, PositiveInt, TypeAdapter from simcore_postgres_database.models.payments_transactions import ( PaymentTransactionState, payments_transactions, @@ -44,9 +44,7 @@ class PaymentsTransactionsDB(BaseModel): completed_at: datetime.datetime | None state: PaymentTransactionState state_message: str | None - - class Config: - orm_mode = True + model_config = ConfigDict(from_attributes=True) async def list_user_payment_transactions( @@ -64,7 +62,7 @@ async def list_user_payment_transactions( total_number_of_items, rows = await get_user_payments_transactions( conn, user_id=user_id, offset=offset, limit=limit ) - page = parse_obj_as(list[PaymentsTransactionsDB], rows) + page = TypeAdapter(list[PaymentsTransactionsDB]).validate_python(rows) return total_number_of_items, page @@ -76,7 +74,7 @@ async def get_pending_payment_transactions_ids(app: web.Application) -> list[Pay .order_by(payments_transactions.c.initiated_at.asc()) # oldest first ) rows = await result.fetchall() or [] - return [parse_obj_as(PaymentID, row.payment_id) for row in rows] + return [TypeAdapter(PaymentID).validate_python(row.payment_id) for row in rows] async def complete_payment_transaction( diff --git a/services/web/server/src/simcore_service_webserver/payments/_rpc.py b/services/web/server/src/simcore_service_webserver/payments/_rpc.py index 76feccce794..6e59d402baf 100644 --- a/services/web/server/src/simcore_service_webserver/payments/_rpc.py +++ b/services/web/server/src/simcore_service_webserver/payments/_rpc.py @@ -21,7 +21,7 @@ from models_library.rabbitmq_basic_types import RPCMethodName from models_library.users import UserID from models_library.wallets import WalletID -from pydantic import EmailStr, HttpUrl, parse_obj_as +from pydantic import EmailStr, HttpUrl, TypeAdapter from servicelib.logging_utils import log_decorator from servicelib.rabbitmq import RPC_REQUEST_DEFAULT_TIMEOUT_S @@ -52,7 +52,7 @@ async def init_payment( # pylint: disable=too-many-arguments # NOTE: remote errors are aio_pika.MessageProcessError result = await rpc_client.request( PAYMENTS_RPC_NAMESPACE, - parse_obj_as(RPCMethodName, "init_payment"), + TypeAdapter(RPCMethodName).validate_python("init_payment"), amount_dollars=amount_dollars, target_credits=target_credits, product_name=product_name, @@ -83,7 +83,7 @@ async def cancel_payment( await rpc_client.request( PAYMENTS_RPC_NAMESPACE, - parse_obj_as(RPCMethodName, "cancel_payment"), + TypeAdapter(RPCMethodName).validate_python("cancel_payment"), payment_id=payment_id, user_id=user_id, wallet_id=wallet_id, @@ -104,7 +104,7 @@ async def get_payments_page( result: tuple[int, list[PaymentTransaction]] = await rpc_client.request( PAYMENTS_RPC_NAMESPACE, - parse_obj_as(RPCMethodName, "get_payments_page"), + TypeAdapter(RPCMethodName).validate_python("get_payments_page"), user_id=user_id, product_name=product_name, limit=limit, @@ -112,7 +112,8 @@ async def get_payments_page( timeout_s=2 * RPC_REQUEST_DEFAULT_TIMEOUT_S, ) assert ( # nosec - parse_obj_as(tuple[int, list[PaymentTransaction]], result) is not None + TypeAdapter(tuple[int, list[PaymentTransaction]]).validate_python(result) + is not None ) return result @@ -129,7 +130,7 @@ async def get_payment_invoice_url( result: HttpUrl = await rpc_client.request( PAYMENTS_RPC_NAMESPACE, - parse_obj_as(RPCMethodName, "get_payment_invoice_url"), + TypeAdapter(RPCMethodName).validate_python("get_payment_invoice_url"), user_id=user_id, wallet_id=wallet_id, payment_id=payment_id, @@ -152,7 +153,7 @@ async def init_creation_of_payment_method( result = await rpc_client.request( PAYMENTS_RPC_NAMESPACE, - parse_obj_as(RPCMethodName, "init_creation_of_payment_method"), + TypeAdapter(RPCMethodName).validate_python("init_creation_of_payment_method"), wallet_id=wallet_id, wallet_name=wallet_name, user_id=user_id, @@ -176,7 +177,7 @@ async def cancel_creation_of_payment_method( result = await rpc_client.request( PAYMENTS_RPC_NAMESPACE, - parse_obj_as(RPCMethodName, "cancel_creation_of_payment_method"), + TypeAdapter(RPCMethodName).validate_python("cancel_creation_of_payment_method"), payment_method_id=payment_method_id, user_id=user_id, wallet_id=wallet_id, @@ -196,7 +197,7 @@ async def list_payment_methods( result = await rpc_client.request( PAYMENTS_RPC_NAMESPACE, - parse_obj_as(RPCMethodName, "list_payment_methods"), + TypeAdapter(RPCMethodName).validate_python("list_payment_methods"), user_id=user_id, wallet_id=wallet_id, timeout_s=2 * RPC_REQUEST_DEFAULT_TIMEOUT_S, @@ -217,7 +218,7 @@ async def get_payment_method( result = await rpc_client.request( PAYMENTS_RPC_NAMESPACE, - parse_obj_as(RPCMethodName, "get_payment_method"), + TypeAdapter(RPCMethodName).validate_python("get_payment_method"), payment_method_id=payment_method_id, user_id=user_id, wallet_id=wallet_id, @@ -239,7 +240,7 @@ async def delete_payment_method( result = await rpc_client.request( PAYMENTS_RPC_NAMESPACE, - parse_obj_as(RPCMethodName, "delete_payment_method"), + TypeAdapter(RPCMethodName).validate_python("delete_payment_method"), payment_method_id=payment_method_id, user_id=user_id, wallet_id=wallet_id, @@ -270,7 +271,7 @@ async def pay_with_payment_method( # noqa: PLR0913 # pylint: disable=too-many-a result = await rpc_client.request( PAYMENTS_RPC_NAMESPACE, - parse_obj_as(RPCMethodName, "pay_with_payment_method"), + TypeAdapter(RPCMethodName).validate_python("pay_with_payment_method"), payment_method_id=payment_method_id, amount_dollars=amount_dollars, target_credits=target_credits, diff --git a/services/web/server/src/simcore_service_webserver/payments/_tasks.py b/services/web/server/src/simcore_service_webserver/payments/_tasks.py index d6c8a5719fb..b87465f5f3e 100644 --- a/services/web/server/src/simcore_service_webserver/payments/_tasks.py +++ b/services/web/server/src/simcore_service_webserver/payments/_tasks.py @@ -6,7 +6,7 @@ from aiohttp import web from models_library.api_schemas_webserver.wallets import PaymentID, PaymentMethodID -from pydantic import HttpUrl, parse_obj_as +from pydantic import HttpUrl, TypeAdapter from servicelib.aiohttp.typing_extension import CleanupContextFunc from servicelib.logging_utils import log_decorator from simcore_postgres_database.models.payments_methods import InitPromptAckFlowState @@ -51,8 +51,7 @@ def _create_possible_outcomes(accepted, rejected): accepted={ "completion_state": PaymentTransactionState.SUCCESS, "message": "Succesful payment (fake)", - "invoice_url": parse_obj_as( - HttpUrl, + "invoice_url": TypeAdapter(HttpUrl).validate_python( "https://assets.website-files.com/63206faf68ab2dc3ee3e623b/634ea60a9381021f775e7a28_Placeholder%20PDF.pdf", ), }, diff --git a/services/web/server/src/simcore_service_webserver/payments/settings.py b/services/web/server/src/simcore_service_webserver/payments/settings.py index 846e2b1e9f9..db40f5007a9 100644 --- a/services/web/server/src/simcore_service_webserver/payments/settings.py +++ b/services/web/server/src/simcore_service_webserver/payments/settings.py @@ -3,7 +3,14 @@ from aiohttp import web from models_library.basic_types import NonNegativeDecimal -from pydantic import Field, HttpUrl, PositiveInt, SecretStr, parse_obj_as, validator +from pydantic import ( + Field, + HttpUrl, + PositiveInt, + SecretStr, + TypeAdapter, + field_validator, +) from settings_library.base import BaseCustomSettings from settings_library.basic_types import PortInt, VersionTag from settings_library.utils_service import ( @@ -18,7 +25,7 @@ class PaymentsSettings(BaseCustomSettings, MixinServiceSettings): PAYMENTS_HOST: str = "payments" PAYMENTS_PORT: PortInt = DEFAULT_FASTAPI_PORT - PAYMENTS_VTAG: VersionTag = parse_obj_as(VersionTag, "v1") + PAYMENTS_VTAG: VersionTag = TypeAdapter(VersionTag).validate_python("v1") PAYMENTS_USERNAME: str = Field( ..., @@ -42,7 +49,9 @@ class PaymentsSettings(BaseCustomSettings, MixinServiceSettings): ) PAYMENTS_FAKE_GATEWAY_URL: HttpUrl = Field( - default=parse_obj_as(HttpUrl, "https://fake-payment-gateway.com"), + default=TypeAdapter(HttpUrl).validate_python( + "https://fake-payment-gateway.com" + ), description="FAKE Base url to the payment gateway", ) @@ -82,7 +91,7 @@ def base_url(self) -> str: ) return base_url_without_vtag - @validator("PAYMENTS_FAKE_COMPLETION") + @field_validator("PAYMENTS_FAKE_COMPLETION") @classmethod def _payments_cannot_be_faken_in_production(cls, v): if v is True and "production" in os.environ.get("SWARM_STACK_NAME", ""): @@ -90,7 +99,7 @@ def _payments_cannot_be_faken_in_production(cls, v): raise ValueError(msg) return v - @validator("PAYMENTS_AUTORECHARGE_DEFAULT_MONTHLY_LIMIT") + @field_validator("PAYMENTS_AUTORECHARGE_DEFAULT_MONTHLY_LIMIT") @classmethod def _monthly_limit_greater_than_top_up(cls, v, values): top_up = values["PAYMENTS_AUTORECHARGE_DEFAULT_TOP_UP_AMOUNT"] diff --git a/services/web/server/src/simcore_service_webserver/products/_db.py b/services/web/server/src/simcore_service_webserver/products/_db.py index 37a960bf9a4..d0317fdc61b 100644 --- a/services/web/server/src/simcore_service_webserver/products/_db.py +++ b/services/web/server/src/simcore_service_webserver/products/_db.py @@ -100,7 +100,7 @@ async def get_product(self, product_name: str) -> Product | None: return Product( **dict(row.items()), is_payment_enabled=payments.enabled, - credits_per_usd=payments.credits_per_usd, # type: ignore[arg-type] + credits_per_usd=payments.credits_per_usd, ) return None diff --git a/services/web/server/src/simcore_service_webserver/products/_events.py b/services/web/server/src/simcore_service_webserver/products/_events.py index f1e4601d7c7..836e43a902f 100644 --- a/services/web/server/src/simcore_service_webserver/products/_events.py +++ b/services/web/server/src/simcore_service_webserver/products/_events.py @@ -90,7 +90,7 @@ async def load_products_on_startup(app: web.Application): app_products[name] = Product( **dict(row.items()), is_payment_enabled=payments.enabled, - credits_per_usd=payments.credits_per_usd, # type: ignore[arg-type] + credits_per_usd=payments.credits_per_usd, ) assert name in FRONTEND_APPS_AVAILABLE # nosec diff --git a/services/web/server/src/simcore_service_webserver/products/_handlers.py b/services/web/server/src/simcore_service_webserver/products/_handlers.py index eb2068acafd..c1e5361146d 100644 --- a/services/web/server/src/simcore_service_webserver/products/_handlers.py +++ b/services/web/server/src/simcore_service_webserver/products/_handlers.py @@ -5,7 +5,7 @@ from common_library.pydantic_basic_types import IDStr from models_library.api_schemas_webserver.product import GetCreditPrice, GetProduct from models_library.users import UserID -from pydantic import Extra, Field +from pydantic import Field from servicelib.aiohttp.requests_validation import ( RequestParams, StrictRequestParams, @@ -36,12 +36,12 @@ class _ProductsRequestContext(RequestParams): @login_required @permission_required("product.price.read") async def _get_current_product_price(request: web.Request): - req_ctx = _ProductsRequestContext.parse_obj(request) + req_ctx = _ProductsRequestContext.model_validate(request) price_info = await _api.get_current_product_credit_price_info(request) credit_price = GetCreditPrice( product_name=req_ctx.product_name, - usd_per_credit=price_info.usd_per_credit if price_info else None, # type: ignore[arg-type] + usd_per_credit=price_info.usd_per_credit if price_info else None, min_payment_amount_usd=price_info.min_payment_amount_usd # type: ignore[arg-type] if price_info else None, @@ -57,7 +57,7 @@ class _ProductsRequestParams(StrictRequestParams): @login_required @permission_required("product.details.*") async def _get_product(request: web.Request): - req_ctx = _ProductsRequestContext.parse_obj(request) + req_ctx = _ProductsRequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_ProductsRequestParams, request) if path_params.product_name == "current": @@ -70,8 +70,8 @@ async def _get_product(request: web.Request): except KeyError as err: raise web.HTTPNotFound(reason=f"{product_name=} not found") from err - assert GetProduct.Config.extra == Extra.ignore # nosec - data = GetProduct(**product.dict(), templates=[]) + assert GetProduct.model_config["extra"] == "ignore" # nosec + data = GetProduct(**product.model_dump(), templates=[]) return envelope_json_response(data) @@ -86,7 +86,7 @@ class _ProductTemplateParams(_ProductsRequestParams): @login_required @permission_required("product.details.*") async def update_product_template(request: web.Request): - req_ctx = _ProductsRequestContext.parse_obj(request) + req_ctx = _ProductsRequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_ProductTemplateParams, request) assert req_ctx # nosec diff --git a/services/web/server/src/simcore_service_webserver/products/_invitations_handlers.py b/services/web/server/src/simcore_service_webserver/products/_invitations_handlers.py index a300a4c43e9..0eb1209178e 100644 --- a/services/web/server/src/simcore_service_webserver/products/_invitations_handlers.py +++ b/services/web/server/src/simcore_service_webserver/products/_invitations_handlers.py @@ -35,7 +35,7 @@ class _ProductsRequestContext(RequestParams): @login_required @permission_required("product.invitations.create") async def generate_invitation(request: web.Request): - req_ctx = _ProductsRequestContext.parse_obj(request) + req_ctx = _ProductsRequestContext.model_validate(request) body = await parse_request_body_as(GenerateInvitation, request) _, user_email = await get_user_name_and_email(request.app, user_id=req_ctx.user_id) @@ -55,13 +55,13 @@ async def generate_invitation(request: web.Request): assert generated.product == req_ctx.product_name # nosec assert generated.guest == body.guest # nosec - url = URL(generated.invitation_url) + url = URL(f"{generated.invitation_url}") invitation_link = request.url.with_path(url.path).with_fragment(url.raw_fragment) invitation = InvitationGenerated( product_name=generated.product, issuer=generated.issuer, - guest=generated.guest, # type: ignore[arg-type] + guest=generated.guest, trial_account_days=generated.trial_account_days, extra_credits_in_usd=generated.extra_credits_in_usd, created=generated.created, diff --git a/services/web/server/src/simcore_service_webserver/products/_model.py b/services/web/server/src/simcore_service_webserver/products/_model.py index 82c4a3b64aa..29551f9d9b3 100644 --- a/services/web/server/src/simcore_service_webserver/products/_model.py +++ b/services/web/server/src/simcore_service_webserver/products/_model.py @@ -1,10 +1,7 @@ import logging +import re import string -from typing import ( # noqa: UP035 # pydantic does not validate with re.Pattern - Any, - ClassVar, - Pattern, -) +from typing import Annotated, Any from models_library.basic_regex import ( PUBLIC_VARIABLE_NAME_RE, @@ -14,7 +11,14 @@ from models_library.emails import LowerCaseEmailStr from models_library.products import ProductName from models_library.utils.change_case import snake_to_camel -from pydantic import BaseModel, Extra, Field, PositiveInt, validator +from pydantic import ( + BaseModel, + BeforeValidator, + ConfigDict, + Field, + PositiveInt, + field_validator, +) from simcore_postgres_database.models.products import ( EmailFeedback, Forum, @@ -40,19 +44,20 @@ class Product(BaseModel): SEE descriptions in packages/postgres-database/src/simcore_postgres_database/models/products.py """ - name: ProductName = Field(regex=PUBLIC_VARIABLE_NAME_RE) + name: ProductName = Field(pattern=PUBLIC_VARIABLE_NAME_RE, validate_default=True) display_name: str = Field(..., description="Long display name") short_name: str | None = Field( None, - regex=TWILIO_ALPHANUMERIC_SENDER_ID_RE, + pattern=re.compile(TWILIO_ALPHANUMERIC_SENDER_ID_RE), min_length=2, max_length=11, description="Short display name for SMS", ) - host_regex: Pattern = Field(..., description="Host regex") - # NOTE: typing.Pattern is supported but not re.Pattern (SEE https://github.com/pydantic/pydantic/pull/4366) + host_regex: Annotated[re.Pattern, BeforeValidator(str.strip)] = Field( + ..., description="Host regex" + ) support_email: LowerCaseEmailStr = Field( ..., @@ -82,7 +87,7 @@ class Product(BaseModel): ) registration_email_template: str | None = Field( - None, x_template_name="registration_email" + None, json_schema_extra={"x_template_name": "registration_email"} ) max_open_studies_per_user: PositiveInt | None = Field( @@ -109,7 +114,7 @@ class Product(BaseModel): description="Price of the credits in this product given in credit/USD. None for free product.", ) - @validator("*", pre=True) + @field_validator("*", mode="before") @classmethod def _parse_empty_string_as_null(cls, v): """Safe measure: database entries are sometimes left blank instead of null""" @@ -117,7 +122,7 @@ def _parse_empty_string_as_null(cls, v): return None return v - @validator("name", pre=True, always=True) + @field_validator("name", mode="before") @classmethod def _validate_name(cls, v): if v not in FRONTEND_APPS_AVAILABLE: @@ -125,27 +130,18 @@ def _validate_name(cls, v): raise ValueError(msg) return v - @validator("host_regex", pre=True) - @classmethod - def _strip_whitespaces(cls, v): - if v and isinstance(v, str): - # Prevents unintended leading & trailing spaces when added - # manually in the database - return v.strip() - return v - @property def twilio_alpha_numeric_sender_id(self) -> str: return self.short_name or self.display_name.replace(string.punctuation, "")[:11] - class Config: - alias_generator = snake_to_camel # to export - allow_population_by_field_name = True - anystr_strip_whitespace = True - extra = Extra.ignore - frozen = True # read-only - orm_mode = True - schema_extra: ClassVar[dict[str, Any]] = { + model_config = ConfigDict( + alias_generator=snake_to_camel, + populate_by_name=True, + str_strip_whitespace=True, + frozen=True, + from_attributes=True, + extra="ignore", + json_schema_extra={ "examples": [ { # fake mandatory @@ -234,7 +230,8 @@ class Config: "is_payment_enabled": False, }, ] - } + }, + ) # helpers ---- @@ -247,7 +244,7 @@ def to_statics(self) -> dict[str, Any]: # SECURITY WARNING: do not expose sensitive information here # keys will be named as e.g. displayName, supportEmail, ... - return self.dict( + return self.model_dump( include={ "display_name": True, "support_email": True, @@ -266,8 +263,8 @@ def to_statics(self) -> dict[str, Any]: def get_template_name_for(self, filename: str) -> str | None: """Checks for field marked with 'x_template_name' that fits the argument""" template_name = filename.removesuffix(".jinja2") - for field in self.__fields__.values(): - if field.field_info.extra.get("x_template_name") == template_name: - template_name_attribute: str = getattr(self, field.name) + for name, field in self.model_fields.items(): + if field.json_schema_extra.get("x_template_name") == template_name: + template_name_attribute: str = getattr(self, name) return template_name_attribute return None diff --git a/services/web/server/src/simcore_service_webserver/projects/_comments_db.py b/services/web/server/src/simcore_service_webserver/projects/_comments_db.py index 102e43971da..03f43fd52ea 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_comments_db.py +++ b/services/web/server/src/simcore_service_webserver/projects/_comments_db.py @@ -9,7 +9,7 @@ from models_library.projects import ProjectID from models_library.projects_comments import CommentID, ProjectsCommentsDB from models_library.users import UserID -from pydantic import parse_obj_as +from pydantic import TypeAdapter from pydantic.types import PositiveInt from simcore_postgres_database.models.projects_comments import projects_comments from sqlalchemy import func, literal_column @@ -32,7 +32,7 @@ async def create_project_comment( .returning(projects_comments.c.comment_id) ) result: tuple[PositiveInt] = await project_comment_id.first() - return parse_obj_as(CommentID, result[0]) + return TypeAdapter(CommentID).validate_python(result[0]) async def list_project_comments( @@ -50,7 +50,7 @@ async def list_project_comments( .limit(limit) ) result = [ - parse_obj_as(ProjectsCommentsDB, row) + TypeAdapter(ProjectsCommentsDB).validate_python(row) for row in await project_comment_result.fetchall() ] return result @@ -86,7 +86,7 @@ async def update_project_comment( .returning(literal_column("*")) ) result = await project_comment_result.first() - return parse_obj_as(ProjectsCommentsDB, result) + return TypeAdapter(ProjectsCommentsDB).validate_python(result) async def delete_project_comment(conn, comment_id: CommentID) -> None: @@ -100,4 +100,4 @@ async def get_project_comment(conn, comment_id: CommentID) -> ProjectsCommentsDB projects_comments.select().where(projects_comments.c.comment_id == comment_id) ) result = await project_comment_result.first() - return parse_obj_as(ProjectsCommentsDB, result) + return TypeAdapter(ProjectsCommentsDB).validate_python(result) diff --git a/services/web/server/src/simcore_service_webserver/projects/_comments_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_comments_handlers.py index fff41cd016c..f1d854bc176 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_comments_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_comments_handlers.py @@ -15,7 +15,7 @@ Page, ) from models_library.rest_pagination_utils import paginate_data -from pydantic import BaseModel, Extra, Field, NonNegativeInt +from pydantic import BaseModel, ConfigDict, Field, NonNegativeInt from servicelib.aiohttp.requests_validation import ( parse_request_body_as, parse_request_path_parameters_as, @@ -59,24 +59,18 @@ async def wrapper(request: web.Request) -> web.StreamResponse: class _ProjectCommentsPathParams(BaseModel): project_uuid: ProjectID - - class Config: - extra = Extra.forbid + model_config = ConfigDict(extra="forbid") class _ProjectCommentsWithCommentPathParams(BaseModel): project_uuid: ProjectID comment_id: CommentID - - class Config: - extra = Extra.forbid + model_config = ConfigDict(extra="forbid") class _ProjectCommentsBodyParams(BaseModel): contents: str - - class Config: - extra = Extra.forbid + model_config = ConfigDict(extra="forbid") @routes.post( @@ -86,7 +80,7 @@ class Config: @permission_required("project.read") @_handle_project_comments_exceptions async def create_project_comment(request: web.Request): - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_ProjectCommentsPathParams, request) body_params = await parse_request_body_as(_ProjectCommentsBodyParams, request) @@ -118,9 +112,7 @@ class _ListProjectCommentsQueryParams(BaseModel): offset: NonNegativeInt = Field( default=0, description="index to the first item to return (pagination)" ) - - class Config: - extra = Extra.forbid + model_config = ConfigDict(extra="forbid") @routes.get(f"/{VTAG}/projects/{{project_uuid}}/comments", name="list_project_comments") @@ -128,7 +120,7 @@ class Config: @permission_required("project.read") @_handle_project_comments_exceptions async def list_project_comments(request: web.Request): - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_ProjectCommentsPathParams, request) query_params: _ListProjectCommentsQueryParams = parse_request_query_parameters_as( _ListProjectCommentsQueryParams, request @@ -154,7 +146,7 @@ async def list_project_comments(request: web.Request): limit=query_params.limit, ) - page = Page[dict[str, Any]].parse_obj( + page = Page[dict[str, Any]].model_validate( paginate_data( chunk=project_comments, request_url=request.url, @@ -176,7 +168,7 @@ async def list_project_comments(request: web.Request): @login_required @permission_required("project.read") async def update_project_comment(request: web.Request): - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as( _ProjectCommentsWithCommentPathParams, request ) @@ -206,7 +198,7 @@ async def update_project_comment(request: web.Request): @permission_required("project.read") @_handle_project_comments_exceptions async def delete_project_comment(request: web.Request): - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as( _ProjectCommentsWithCommentPathParams, request ) @@ -234,7 +226,7 @@ async def delete_project_comment(request: web.Request): @permission_required("project.read") @_handle_project_comments_exceptions async def get_project_comment(request: web.Request): - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as( _ProjectCommentsWithCommentPathParams, request ) diff --git a/services/web/server/src/simcore_service_webserver/projects/_common_models.py b/services/web/server/src/simcore_service_webserver/projects/_common_models.py index d25a0f6c24b..38af25e4b73 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_common_models.py +++ b/services/web/server/src/simcore_service_webserver/projects/_common_models.py @@ -6,7 +6,7 @@ from models_library.projects import ProjectID from models_library.users import UserID -from pydantic import BaseModel, Extra, Field +from pydantic import BaseModel, ConfigDict, Field from servicelib.request_keys import RQT_USERID_KEY from .._constants import RQ_PRODUCT_KEY @@ -19,7 +19,4 @@ class RequestContext(BaseModel): class ProjectPathParams(BaseModel): project_id: ProjectID - - class Config: - allow_population_by_field_name = True - extra = Extra.forbid + model_config = ConfigDict(populate_by_name=True, extra="forbid") diff --git a/services/web/server/src/simcore_service_webserver/projects/_crud_api_create.py b/services/web/server/src/simcore_service_webserver/projects/_crud_api_create.py index 37416912e15..f5f892069d8 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_crud_api_create.py +++ b/services/web/server/src/simcore_service_webserver/projects/_crud_api_create.py @@ -15,7 +15,7 @@ from models_library.utils.fastapi_encoders import jsonable_encoder from models_library.utils.json_serialization import json_dumps from models_library.workspaces import UserWorkspaceAccessRightsDB -from pydantic import parse_obj_as +from pydantic import TypeAdapter from servicelib.aiohttp.long_running_tasks.server import TaskProgress from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON from simcore_postgres_database.utils_projects_nodes import ( @@ -132,14 +132,16 @@ async def _copy_project_nodes_from_source_project( db: ProjectDBAPI = ProjectDBAPI.get_from_app_context(app) def _mapped_node_id(node: ProjectNode) -> NodeID: - return NodeID(nodes_map[NodeIDStr(f"{node.node_id}")]) + return NodeID( + nodes_map[TypeAdapter(NodeIDStr).validate_python(f"{node.node_id}")] + ) return { _mapped_node_id(node): ProjectNodeCreate( node_id=_mapped_node_id(node), **{ k: v - for k, v in node.dict().items() + for k, v in node.model_dump().items() if k in ProjectNodeCreate.get_field_names(exclude={"node_id"}) }, ) @@ -157,7 +159,9 @@ async def _copy_files_from_source_project( ): db: ProjectDBAPI = ProjectDBAPI.get_from_app_context(app) needs_lock_source_project: bool = ( - await db.get_project_type(parse_obj_as(ProjectID, source_project["uuid"])) + await db.get_project_type( + TypeAdapter(ProjectID).validate_python(source_project["uuid"]) + ) != ProjectTypeDB.TEMPLATE ) @@ -178,8 +182,7 @@ async def _copy_files_from_source_project( ): task_progress.update( message=long_running_task.progress.message, - percent=parse_obj_as( - ProgressPercent, + percent=TypeAdapter(ProgressPercent).validate_python( ( starting_value + long_running_task.progress.percent * (1.0 - starting_value) @@ -416,11 +419,12 @@ async def create_project( # pylint: disable=too-many-arguments,too-many-branche ) ) new_project["accessRights"] = { - gid: access.dict() for gid, access in workspace_db.access_rights.items() + gid: access.model_dump() + for gid, access in workspace_db.access_rights.items() } # Ensures is like ProjectGet - data = ProjectGet.parse_obj(new_project).data(exclude_unset=True) + data = ProjectGet.model_validate(new_project).data(exclude_unset=True) raise web.HTTPCreated( text=json_dumps({"data": data}), diff --git a/services/web/server/src/simcore_service_webserver/projects/_crud_api_read.py b/services/web/server/src/simcore_service_webserver/projects/_crud_api_read.py index 21802e9841d..a589a79bba8 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_crud_api_read.py +++ b/services/web/server/src/simcore_service_webserver/projects/_crud_api_read.py @@ -57,7 +57,7 @@ async def _append_fields( } # validate - return model_schema_cls.parse_obj(project).data(exclude_unset=True) + return model_schema_cls.model_validate(project).data(exclude_unset=True) async def list_projects( # pylint: disable=too-many-arguments diff --git a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py index d2cce731d21..54f67992908 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py @@ -25,7 +25,7 @@ from models_library.rest_pagination_utils import paginate_data from models_library.utils.fastapi_encoders import jsonable_encoder from models_library.utils.json_serialization import json_dumps -from pydantic import parse_obj_as +from pydantic import TypeAdapter from servicelib.aiohttp.long_running_tasks.server import start_long_running_task from servicelib.aiohttp.requests_validation import ( parse_request_body_as, @@ -130,7 +130,7 @@ async def _wrapper(request: web.Request) -> web.StreamResponse: @permission_required("project.create") @permission_required("services.pipeline.*") # due to update_pipeline_db async def create_project(request: web.Request): - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) query_params: ProjectCreateParams = parse_request_query_parameters_as( ProjectCreateParams, request ) @@ -197,7 +197,7 @@ async def list_projects(request: web.Request): web.HTTPUnprocessableEntity: (422) if validation of request parameters fail """ - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) query_params: ProjectListWithJsonStrParams = parse_request_query_parameters_as( ProjectListWithJsonStrParams, request ) @@ -211,12 +211,12 @@ async def list_projects(request: web.Request): limit=query_params.limit, offset=query_params.offset, search=query_params.search, - order_by=parse_obj_as(OrderBy, query_params.order_by), + order_by=TypeAdapter(OrderBy).validate_python(query_params.order_by), folder_id=query_params.folder_id, workspace_id=query_params.workspace_id, ) - page = Page[ProjectDict].parse_obj( + page = Page[ProjectDict].model_validate( paginate_data( chunk=projects, request_url=request.url, @@ -286,7 +286,7 @@ async def get_active_project(request: web.Request) -> web.Response: web.HTTPUnprocessableEntity: (422) if validation of request parameters fail web.HTTPNotFound: If active project is not found """ - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) query_params: ProjectActiveParams = parse_request_query_parameters_as( ProjectActiveParams, request ) @@ -311,7 +311,7 @@ async def get_active_project(request: web.Request) -> web.Response: # updates project's permalink field await update_or_pop_permalink_in_project(request, project) - data = ProjectGet.parse_obj(project).data(exclude_unset=True) + data = ProjectGet.model_validate(project).data(exclude_unset=True) return web.json_response({"data": data}, dumps=json_dumps) @@ -332,7 +332,7 @@ async def get_project(request: web.Request): web.HTTPNotFound: This project was not found """ - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ProjectPathParams, request) user_available_services: list[dict] = await get_services_for_user_in_product( @@ -367,7 +367,7 @@ async def get_project(request: web.Request): # Adds permalink await update_or_pop_permalink_in_project(request, project) - data = ProjectGet.parse_obj(project).data(exclude_unset=True) + data = ProjectGet.model_validate(project).data(exclude_unset=True) return web.json_response({"data": data}, dumps=json_dumps) except ProjectInvalidRightsError as exc: @@ -425,7 +425,7 @@ async def replace_project(request: web.Request): """ db: ProjectDBAPI = ProjectDBAPI.get_from_app_context(request.app) - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ProjectPathParams, request) try: @@ -451,7 +451,7 @@ async def replace_project(request: web.Request): ) try: - Project.parse_obj(new_project) # validate + Project.model_validate(new_project) # validate current_project = await projects_api.get_project_for_user( request.app, @@ -564,7 +564,7 @@ async def replace_project(request: web.Request): @permission_required("services.pipeline.*") @_handle_projects_exceptions async def patch_project(request: web.Request): - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ProjectPathParams, request) project_patch = await parse_request_body_as(ProjectPatch, request) @@ -601,7 +601,7 @@ async def delete_project(request: web.Request): web.HTTPNoContent: Sucess """ - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ProjectPathParams, request) try: @@ -676,7 +676,7 @@ async def delete_project(request: web.Request): @permission_required("project.create") @permission_required("services.pipeline.*") # due to update_pipeline_db async def clone_project(request: web.Request): - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ProjectPathParams, request) return await start_long_running_task( diff --git a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers_models.py b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers_models.py index 88be3e8ee58..1d03ade2711 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers_models.py +++ b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers_models.py @@ -19,12 +19,11 @@ from models_library.workspaces import WorkspaceID from pydantic import ( BaseModel, - Extra, + ConfigDict, Field, Json, - parse_obj_as, - root_validator, - validator, + field_validator, + model_validator, ) from servicelib.common_headers import ( UNDEFINED_DEFAULT_SIMCORE_USER_AGENT_VALUE, @@ -56,7 +55,7 @@ class ProjectCreateHeaders(BaseModel): alias=X_SIMCORE_PARENT_NODE_ID, ) - @root_validator + @model_validator(mode="before") @classmethod def check_parent_valid(cls, values: dict[str, Any]) -> dict[str, Any]: if ( @@ -70,8 +69,7 @@ def check_parent_valid(cls, values: dict[str, Any]) -> dict[str, Any]: raise ValueError(msg) return values - class Config: - allow_population_by_field_name = False + model_config = ConfigDict(populate_by_name=False) class ProjectCreateParams(BaseModel): @@ -91,9 +89,7 @@ class ProjectCreateParams(BaseModel): default=False, description="Enables/disables hidden flag. Hidden projects are by default unlisted", ) - - class Config: - extra = Extra.forbid + model_config = ConfigDict(extra="forbid") class ProjectListParams(PageQueryParameters): @@ -105,30 +101,32 @@ class ProjectListParams(PageQueryParameters): default=None, description="Multi column full text search", max_length=100, - example="My Project", + examples=["My Project"], ) folder_id: FolderID | None = Field( default=None, description="Filter projects in specific folder. Default filtering is a root directory.", + validate_default=True, ) workspace_id: WorkspaceID | None = Field( default=None, description="Filter projects in specific workspace. Default filtering is a private workspace.", + validate_default=True, ) - @validator("search", pre=True) + @field_validator("search", mode="before") @classmethod def search_check_empty_string(cls, v): if not v: return None return v - _null_or_none_str_to_none_validator = validator( - "folder_id", allow_reuse=True, pre=True - )(null_or_none_str_to_none_validator) + _null_or_none_str_to_none_validator = field_validator("folder_id", mode="before")( + null_or_none_str_to_none_validator + ) - _null_or_none_str_to_none_validator2 = validator( - "workspace_id", allow_reuse=True, pre=True + _null_or_none_str_to_none_validator2 = field_validator( + "workspace_id", mode="before" )(null_or_none_str_to_none_validator) @@ -136,11 +134,12 @@ class ProjectListWithOrderByParams(BaseModel): order_by: Json[OrderBy] = Field( # pylint: disable=unsubscriptable-object default=OrderBy(field=IDStr("last_change_date"), direction=OrderDirection.DESC), description="Order by field (type|uuid|name|description|prj_owner|creation_date|last_change_date) and direction (asc|desc). The default sorting order is ascending.", - example='{"field": "prj_owner", "direction": "desc"}', + examples=['{"field": "prj_owner", "direction": "desc"}'], alias="order_by", ) - @validator("order_by", check_fields=False) + @field_validator("order_by", check_fields=False) + @classmethod @classmethod def validate_order_by_field(cls, v): if v.field not in { @@ -156,8 +155,7 @@ def validate_order_by_field(cls, v): raise ValueError(msg) return v - class Config: - extra = Extra.forbid + model_config = ConfigDict(extra="forbid") class ProjectListWithJsonStrParams(ProjectListParams, ProjectListWithOrderByParams): diff --git a/services/web/server/src/simcore_service_webserver/projects/_db_utils.py b/services/web/server/src/simcore_service_webserver/projects/_db_utils.py index 8bda162ab6f..9173c2b131d 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_db_utils.py +++ b/services/web/server/src/simcore_service_webserver/projects/_db_utils.py @@ -380,7 +380,7 @@ def patch_workbench( raise ProjectInvalidUsageError # if it's a new node, let's check that it validates try: - Node.parse_obj(new_node_data) + Node.model_validate(new_node_data) patched_project["workbench"][node_key] = new_node_data changed_entries.update({node_key: new_node_data}) except ValidationError as err: diff --git a/services/web/server/src/simcore_service_webserver/projects/_folders_db.py b/services/web/server/src/simcore_service_webserver/projects/_folders_db.py index 1ac57057c53..d76bbc20a7b 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_folders_db.py +++ b/services/web/server/src/simcore_service_webserver/projects/_folders_db.py @@ -11,7 +11,7 @@ from models_library.folders import FolderID from models_library.projects import ProjectID from models_library.users import UserID -from pydantic import BaseModel, parse_obj_as +from pydantic import BaseModel, TypeAdapter from simcore_postgres_database.models.projects_to_folders import projects_to_folders from sqlalchemy import func, literal_column from sqlalchemy.sql import select @@ -56,7 +56,7 @@ async def insert_project_to_folder( .returning(literal_column("*")) ) row = await result.first() - return parse_obj_as(ProjectToFolderDB, row) + return TypeAdapter(ProjectToFolderDB).validate_python(row) async def get_project_to_folder( @@ -81,7 +81,7 @@ async def get_project_to_folder( row = await result.first() if row is None: return None - return parse_obj_as(ProjectToFolderDB, row) + return TypeAdapter(ProjectToFolderDB).validate_python(row) async def delete_project_to_folder( diff --git a/services/web/server/src/simcore_service_webserver/projects/_folders_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_folders_handlers.py index 591fecf8a94..3de47b7bc8d 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_folders_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_folders_handlers.py @@ -5,7 +5,7 @@ from models_library.folders import FolderID from models_library.projects import ProjectID from models_library.utils.common_validators import null_or_none_str_to_none_validator -from pydantic import BaseModel, Extra, validator +from pydantic import BaseModel, ConfigDict, field_validator from servicelib.aiohttp.requests_validation import parse_request_path_parameters_as from servicelib.aiohttp.typing_extension import Handler from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON @@ -41,14 +41,12 @@ async def wrapper(request: web.Request) -> web.StreamResponse: class _ProjectsFoldersPathParams(BaseModel): project_id: ProjectID folder_id: FolderID | None - - class Config: - extra = Extra.forbid + model_config = ConfigDict(extra="forbid") # validators - _null_or_none_str_to_none_validator = validator( - "folder_id", allow_reuse=True, pre=True - )(null_or_none_str_to_none_validator) + _null_or_none_str_to_none_validator = field_validator("folder_id", mode="before")( + null_or_none_str_to_none_validator + ) @routes.put( @@ -59,7 +57,7 @@ class Config: @permission_required("project.folders.*") @_handle_projects_folders_exceptions async def replace_project_folder(request: web.Request): - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_ProjectsFoldersPathParams, request) await _folders_api.move_project_into_folder( diff --git a/services/web/server/src/simcore_service_webserver/projects/_groups_api.py b/services/web/server/src/simcore_service_webserver/projects/_groups_api.py index 2477c36ecfc..a9abe9c3c21 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_groups_api.py +++ b/services/web/server/src/simcore_service_webserver/projects/_groups_api.py @@ -5,7 +5,7 @@ from models_library.products import ProductName from models_library.projects import ProjectID from models_library.users import GroupID, UserID -from pydantic import BaseModel, parse_obj_as +from pydantic import BaseModel, TypeAdapter from ..users import api as users_api from . import _groups_db as projects_groups_db @@ -78,7 +78,8 @@ async def list_project_groups_by_user_and_project( ] = await projects_groups_db.list_project_groups(app=app, project_id=project_id) project_groups_api: list[ProjectGroupGet] = [ - parse_obj_as(ProjectGroupGet, group) for group in project_groups_db + TypeAdapter(ProjectGroupGet).validate_python(group) + for group in project_groups_db ] return project_groups_api diff --git a/services/web/server/src/simcore_service_webserver/projects/_groups_db.py b/services/web/server/src/simcore_service_webserver/projects/_groups_db.py index 8420d71ef7a..ad0245e4d7c 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_groups_db.py +++ b/services/web/server/src/simcore_service_webserver/projects/_groups_db.py @@ -9,7 +9,7 @@ from aiohttp import web from models_library.projects import ProjectID from models_library.users import GroupID -from pydantic import BaseModel, parse_obj_as +from pydantic import BaseModel, TypeAdapter from simcore_postgres_database.models.project_to_groups import project_to_groups from sqlalchemy import func, literal_column from sqlalchemy.dialects.postgresql import insert as pg_insert @@ -59,7 +59,7 @@ async def create_project_group( .returning(literal_column("*")) ) row = await result.first() - return parse_obj_as(ProjectGroupGetDB, row) + return TypeAdapter(ProjectGroupGetDB).validate_python(row) async def list_project_groups( @@ -82,7 +82,7 @@ async def list_project_groups( async with get_database_engine(app).acquire() as conn: result = await conn.execute(stmt) rows = await result.fetchall() or [] - return parse_obj_as(list[ProjectGroupGetDB], rows) + return TypeAdapter(list[ProjectGroupGetDB]).validate_python(rows) async def get_project_group( @@ -113,7 +113,7 @@ async def get_project_group( raise ProjectGroupNotFoundError( reason=f"Project {project_id} group {group_id} not found" ) - return parse_obj_as(ProjectGroupGetDB, row) + return TypeAdapter(ProjectGroupGetDB).validate_python(row) async def replace_project_group( @@ -144,7 +144,7 @@ async def replace_project_group( raise ProjectGroupNotFoundError( reason=f"Project {project_id} group {group_id} not found" ) - return parse_obj_as(ProjectGroupGetDB, row) + return TypeAdapter(ProjectGroupGetDB).validate_python(row) async def update_or_insert_project_group( diff --git a/services/web/server/src/simcore_service_webserver/projects/_groups_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_groups_handlers.py index 607dd499df2..9a5d2f66bf4 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_groups_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_groups_handlers.py @@ -8,7 +8,7 @@ from aiohttp import web from models_library.projects import ProjectID from models_library.users import GroupID -from pydantic import BaseModel, Extra +from pydantic import BaseModel, ConfigDict from servicelib.aiohttp.requests_validation import ( parse_request_body_as, parse_request_path_parameters_as, @@ -53,18 +53,14 @@ async def wrapper(request: web.Request) -> web.StreamResponse: class _ProjectsGroupsPathParams(BaseModel): project_id: ProjectID group_id: GroupID - - class Config: - extra = Extra.forbid + model_config = ConfigDict(extra="forbid") class _ProjectsGroupsBodyParams(BaseModel): read: bool write: bool delete: bool - - class Config: - extra = Extra.forbid + model_config = ConfigDict(extra="forbid") @routes.post( @@ -74,7 +70,7 @@ class Config: @permission_required("project.access_rights.update") @_handle_projects_groups_exceptions async def create_project_group(request: web.Request): - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_ProjectsGroupsPathParams, request) body_params = await parse_request_body_as(_ProjectsGroupsBodyParams, request) @@ -97,7 +93,7 @@ async def create_project_group(request: web.Request): @permission_required("project.read") @_handle_projects_groups_exceptions async def list_project_groups(request: web.Request): - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ProjectPathParams, request) project_groups: list[ @@ -120,7 +116,7 @@ async def list_project_groups(request: web.Request): @permission_required("project.access_rights.update") @_handle_projects_groups_exceptions async def replace_project_group(request: web.Request): - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_ProjectsGroupsPathParams, request) body_params = await parse_request_body_as(_ProjectsGroupsBodyParams, request) @@ -144,7 +140,7 @@ async def replace_project_group(request: web.Request): @permission_required("project.access_rights.update") @_handle_projects_groups_exceptions async def delete_project_group(request: web.Request): - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_ProjectsGroupsPathParams, request) await _groups_api.delete_project_group( diff --git a/services/web/server/src/simcore_service_webserver/projects/_metadata_api.py b/services/web/server/src/simcore_service_webserver/projects/_metadata_api.py index db27c3359bd..f17c7941a1d 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_metadata_api.py +++ b/services/web/server/src/simcore_service_webserver/projects/_metadata_api.py @@ -6,7 +6,7 @@ from models_library.projects import ProjectID from models_library.projects_nodes_io import NodeID from models_library.users import UserID -from pydantic import parse_obj_as +from pydantic import TypeAdapter from ..db.plugin import get_database_engine from . import _metadata_db @@ -67,7 +67,7 @@ async def set_project_ancestors_from_custom_metadata( if parent_node_idstr := custom_metadata.get("node_id"): # NOTE: backward compatibility with S4l old client - parent_node_id = parse_obj_as(NodeID, parent_node_idstr) + parent_node_id = TypeAdapter(NodeID).validate_python(parent_node_idstr) if parent_node_id == _NIL_NODE_UUID: return diff --git a/services/web/server/src/simcore_service_webserver/projects/_metadata_db.py b/services/web/server/src/simcore_service_webserver/projects/_metadata_db.py index 6a511a8ba4c..2c72a395a5a 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_metadata_db.py +++ b/services/web/server/src/simcore_service_webserver/projects/_metadata_db.py @@ -6,7 +6,7 @@ from models_library.api_schemas_webserver.projects_metadata import MetadataDict from models_library.projects import ProjectID from models_library.projects_nodes_io import NodeID -from pydantic import parse_obj_as +from pydantic import TypeAdapter from simcore_postgres_database import utils_projects_metadata from simcore_postgres_database.utils_projects_metadata import ( DBProjectInvalidAncestorsError, @@ -84,7 +84,7 @@ async def get_project_custom_metadata( connection, project_uuid=project_uuid ) # NOTE: if no metadata in table, it returns None -- which converts here to --> {} - return parse_obj_as(MetadataDict, metadata.custom or {}) + return TypeAdapter(MetadataDict).validate_python(metadata.custom or {}) @_handle_projects_metadata_exceptions @@ -104,7 +104,7 @@ async def set_project_custom_metadata( custom_metadata=custom_metadata, ) - return parse_obj_as(MetadataDict, metadata.custom) + return TypeAdapter(MetadataDict).validate_python(metadata.custom) @_handle_projects_metadata_exceptions diff --git a/services/web/server/src/simcore_service_webserver/projects/_metadata_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_metadata_handlers.py index 614d0ba03b9..802c13f7937 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_metadata_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_metadata_handlers.py @@ -79,7 +79,7 @@ async def wrapper(request: web.Request) -> web.StreamResponse: @permission_required("project.read") @_handle_project_exceptions async def get_project_metadata(request: web.Request) -> web.Response: - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ProjectPathParams, request) custom_metadata = await _metadata_api.get_project_custom_metadata( @@ -99,7 +99,7 @@ async def get_project_metadata(request: web.Request) -> web.Response: @permission_required("project.update") @_handle_project_exceptions async def update_project_metadata(request: web.Request) -> web.Response: - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ProjectPathParams, request) update = await parse_request_body_as(ProjectMetadataUpdate, request) diff --git a/services/web/server/src/simcore_service_webserver/projects/_nodes_api.py b/services/web/server/src/simcore_service_webserver/projects/_nodes_api.py index ab6ba4b7d93..30b08b82012 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_nodes_api.py +++ b/services/web/server/src/simcore_service_webserver/projects/_nodes_api.py @@ -19,9 +19,9 @@ HttpUrl, NonNegativeFloat, NonNegativeInt, + TypeAdapter, ValidationError, - parse_obj_as, - root_validator, + model_validator, ) from servicelib.utils import logged_gather @@ -96,10 +96,10 @@ class NodeScreenshot(BaseModel): mimetype: str | None = Field( default=None, description="File's media type or None if unknown. SEE https://www.iana.org/assignments/media-types/media-types.xhtml", - example="image/jpeg", + examples=["image/jpeg"], ) - @root_validator(pre=True) + @model_validator(mode="before") @classmethod def guess_mimetype_if_undefined(cls, values): mimetype = values.get("mimetype") @@ -173,7 +173,9 @@ async def __get_link( return __get_search_key(file_meta_data), await get_download_link( app, user_id, - parse_obj_as(SimCoreFileLink, {"store": "0", "path": file_meta_data.file_id}), + TypeAdapter(SimCoreFileLink).validate_python( + {"store": "0", "path": file_meta_data.file_id} + ), ) @@ -228,7 +230,9 @@ async def get_node_screenshots( assert node.outputs is not None # nosec - filelink = parse_obj_as(SimCoreFileLink, node.outputs[KeyIDStr("outFile")]) + filelink = TypeAdapter(SimCoreFileLink).validate_python( + node.outputs[TypeAdapter(KeyIDStr).validate_python("outFile")] + ) file_url = await get_download_link(app, user_id, filelink) screenshots.append( diff --git a/services/web/server/src/simcore_service_webserver/projects/_nodes_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_nodes_handlers.py index 255260c60ad..4b100b75fa7 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_nodes_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_nodes_handlers.py @@ -32,7 +32,7 @@ from models_library.users import GroupID from models_library.utils.fastapi_encoders import jsonable_encoder from models_library.utils.json_serialization import json_dumps -from pydantic import BaseModel, Field, parse_obj_as +from pydantic import BaseModel, Field, TypeAdapter from servicelib.aiohttp.long_running_tasks.server import ( TaskProgress, start_long_running_task, @@ -144,7 +144,7 @@ class NodePathParams(ProjectPathParams): @permission_required("project.node.create") @_handle_project_nodes_exceptions async def create_node(request: web.Request) -> web.Response: - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ProjectPathParams, request) body = await parse_request_body_as(NodeCreate, request) @@ -176,7 +176,7 @@ async def create_node(request: web.Request) -> web.Response: body.service_id, ) } - assert parse_obj_as(NodeCreated, data) is not None # nosec + assert TypeAdapter(NodeCreated).validate_python(data) is not None # nosec return envelope_json_response(data, status_cls=web.HTTPCreated) @@ -187,7 +187,7 @@ async def create_node(request: web.Request) -> web.Response: @_handle_project_nodes_exceptions # NOTE: Careful, this endpoint is actually "get_node_state," and it doesn't return a Node resource. async def get_node(request: web.Request) -> web.Response: - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(NodePathParams, request) # ensure the project exists @@ -225,7 +225,7 @@ async def get_node(request: web.Request) -> web.Response: @permission_required("project.node.update") @_handle_project_nodes_exceptions async def patch_project_node(request: web.Request) -> web.Response: - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(NodePathParams, request) node_patch = await parse_request_body_as(NodePatch, request) @@ -246,7 +246,7 @@ async def patch_project_node(request: web.Request) -> web.Response: @permission_required("project.node.delete") @_handle_project_nodes_exceptions async def delete_node(request: web.Request) -> web.Response: - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(NodePathParams, request) # ensure the project exists @@ -294,7 +294,7 @@ async def retrieve_node(request: web.Request) -> web.Response: @permission_required("project.node.update") @_handle_project_nodes_exceptions async def update_node_outputs(request: web.Request) -> web.Response: - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(NodePathParams, request) node_outputs = await parse_request_body_as(NodeOutputs, request) @@ -322,7 +322,7 @@ async def update_node_outputs(request: web.Request) -> web.Response: @_handle_project_nodes_exceptions async def start_node(request: web.Request) -> web.Response: """Has only effect on nodes associated to dynamic services""" - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(NodePathParams, request) await projects_api.start_project_node( @@ -366,7 +366,7 @@ async def _stop_dynamic_service_task( @_handle_project_nodes_exceptions async def stop_node(request: web.Request) -> web.Response: """Has only effect on nodes associated to dynamic services""" - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(NodePathParams, request) save_state = await has_user_project_access_rights( @@ -429,7 +429,7 @@ async def restart_node(request: web.Request) -> web.Response: @permission_required("project.node.read") @_handle_project_nodes_exceptions async def get_node_resources(request: web.Request) -> web.Response: - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(NodePathParams, request) # ensure the project exists @@ -462,7 +462,7 @@ async def get_node_resources(request: web.Request) -> web.Response: @permission_required("project.node.update") @_handle_project_nodes_exceptions async def replace_node_resources(request: web.Request) -> web.Response: - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(NodePathParams, request) body = await parse_request_body_as(ServiceResourcesDict, request) @@ -523,7 +523,7 @@ class _ProjectGroupAccess(BaseModel): async def get_project_services_access_for_gid( request: web.Request, ) -> web.Response: - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ProjectPathParams, request) query_params: _ServicesAccessQuery = parse_request_query_parameters_as( _ServicesAccessQuery, request @@ -639,7 +639,7 @@ class _ProjectNodePreview(BaseModel): @permission_required("project.read") @_handle_project_nodes_exceptions async def list_project_nodes_previews(request: web.Request) -> web.Response: - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ProjectPathParams, request) assert req_ctx # nosec @@ -649,7 +649,7 @@ async def list_project_nodes_previews(request: web.Request) -> web.Response: project_uuid=f"{path_params.project_id}", user_id=req_ctx.user_id, ) - project = Project.parse_obj(project_data) + project = Project.model_validate(project_data) for node_id, node in project.workbench.items(): screenshots = await get_node_screenshots( @@ -679,7 +679,7 @@ async def list_project_nodes_previews(request: web.Request) -> web.Response: @permission_required("project.read") @_handle_project_nodes_exceptions async def get_project_node_preview(request: web.Request) -> web.Response: - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(NodePathParams, request) assert req_ctx # nosec @@ -689,7 +689,7 @@ async def get_project_node_preview(request: web.Request) -> web.Response: user_id=req_ctx.user_id, ) - project = Project.parse_obj(project_data) + project = Project.model_validate(project_data) node = project.workbench.get(NodeIDStr(path_params.node_id)) if node is None: diff --git a/services/web/server/src/simcore_service_webserver/projects/_ports_api.py b/services/web/server/src/simcore_service_webserver/projects/_ports_api.py index e05229e43a1..03386b40e1c 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_ports_api.py +++ b/services/web/server/src/simcore_service_webserver/projects/_ports_api.py @@ -25,7 +25,7 @@ jsonschema_validate_data, ) from models_library.utils.services_io import JsonSchemaDict, get_service_io_json_schema -from pydantic import ValidationError +from pydantic import ConfigDict, ValidationError from ..director_v2.api import get_batch_tasks_outputs from .exceptions import InvalidInputValue @@ -164,8 +164,9 @@ def set_inputs_in_project( class _NonStrictPortLink(PortLink): - class Config(PortLink.Config): - allow_population_by_field_name = True + model_config = ConfigDict( + populate_by_name=True, + ) class _OutputPortInfo(NamedTuple): @@ -182,7 +183,7 @@ def _get_outputs_in_workbench(workbench: dict[NodeID, Node]) -> dict[NodeID, Any if port.node.inputs: try: # Every port is associated to the output of a task - port_link = _NonStrictPortLink.parse_obj( + port_link = _NonStrictPortLink.model_validate( port.node.inputs[KeyIDStr("in_1")] ) # Here we resolve which task and which tasks' output is associated to this port? diff --git a/services/web/server/src/simcore_service_webserver/projects/_ports_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_ports_handlers.py index 0d2fb6f3eca..3a50b5a40c6 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_ports_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_ports_handlers.py @@ -22,7 +22,7 @@ from models_library.utils.fastapi_encoders import jsonable_encoder from models_library.utils.json_serialization import json_dumps from models_library.utils.services_io import JsonSchemaDict -from pydantic import BaseModel, Field, parse_obj_as +from pydantic import BaseModel, Field, TypeAdapter from servicelib.aiohttp.requests_validation import ( parse_request_body_as, parse_request_path_parameters_as, @@ -88,7 +88,7 @@ async def _get_validated_workbench_model( include_state=False, ) - return parse_obj_as(dict[NodeID, Node], project["workbench"]) + return TypeAdapter(dict[NodeID, Node]).validate_python(project["workbench"]) routes = web.RouteTableDef() @@ -103,7 +103,7 @@ async def _get_validated_workbench_model( @permission_required("project.read") @_handle_project_exceptions async def get_project_inputs(request: web.Request) -> web.Response: - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ProjectPathParams, request) assert request.app # nosec @@ -129,7 +129,7 @@ async def get_project_inputs(request: web.Request) -> web.Response: @_handle_project_exceptions async def update_project_inputs(request: web.Request) -> web.Response: db: ProjectDBAPI = ProjectDBAPI.get_from_app_context(request.app) - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ProjectPathParams, request) inputs_updates = await parse_request_body_as(list[ProjectInputUpdate], request) @@ -169,7 +169,9 @@ async def update_project_inputs(request: web.Request) -> web.Response: partial_workbench_data=jsonable_encoder(partial_workbench_data), ) - workbench = parse_obj_as(dict[NodeID, Node], updated_project["workbench"]) + workbench = TypeAdapter(dict[NodeID, Node]).validate_python( + updated_project["workbench"] + ) inputs: dict[NodeID, Any] = _ports_api.get_project_inputs(workbench) return _web_json_response_enveloped( @@ -192,7 +194,7 @@ async def update_project_inputs(request: web.Request) -> web.Response: @permission_required("project.read") @_handle_project_exceptions async def get_project_outputs(request: web.Request) -> web.Response: - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ProjectPathParams, request) assert request.app # nosec @@ -239,7 +241,7 @@ class ProjectMetadataPortGet(BaseModel): @permission_required("project.read") @_handle_project_exceptions async def list_project_metadata_ports(request: web.Request) -> web.Response: - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ProjectPathParams, request) assert request.app # nosec diff --git a/services/web/server/src/simcore_service_webserver/projects/_projects_nodes_pricing_unit_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_projects_nodes_pricing_unit_handlers.py index 552869a0404..6b87d0a433a 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_projects_nodes_pricing_unit_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_projects_nodes_pricing_unit_handlers.py @@ -6,12 +6,12 @@ import logging from aiohttp import web +from common_library.errors_classes import OsparcErrorMixin from models_library.api_schemas_webserver.resource_usage import PricingUnitGet from models_library.projects import ProjectID from models_library.projects_nodes_io import NodeID, NodeIDStr from models_library.resource_tracker import PricingPlanId, PricingUnitId -from pydantic import BaseModel, Extra -from pydantic.errors import PydanticErrorMixin +from pydantic import BaseModel, ConfigDict from servicelib.aiohttp.requests_validation import parse_request_path_parameters_as from servicelib.aiohttp.typing_extension import Handler @@ -29,7 +29,7 @@ _logger = logging.getLogger(__name__) -class PricingUnitError(PydanticErrorMixin, ValueError): +class PricingUnitError(OsparcErrorMixin, ValueError): ... @@ -64,7 +64,7 @@ async def wrapper(request: web.Request) -> web.StreamResponse: @_handle_projects_nodes_pricing_unit_exceptions async def get_project_node_pricing_unit(request: web.Request): db: ProjectDBAPI = ProjectDBAPI.get_from_app_context(request.app) - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(NodePathParams, request) # ensure the project exists @@ -99,9 +99,7 @@ class _ProjectNodePricingUnitPathParams(BaseModel): node_id: NodeID pricing_plan_id: PricingPlanId pricing_unit_id: PricingUnitId - - class Config: - extra = Extra.forbid + model_config = ConfigDict(extra="forbid") @routes.put( @@ -113,7 +111,7 @@ class Config: @_handle_projects_nodes_pricing_unit_exceptions async def connect_pricing_unit_to_project_node(request: web.Request): db: ProjectDBAPI = ProjectDBAPI.get_from_app_context(request.app) - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as( _ProjectNodePricingUnitPathParams, request ) @@ -134,7 +132,7 @@ async def connect_pricing_unit_to_project_node(request: web.Request): path_params.pricing_unit_id, ) if rut_pricing_unit.pricing_unit_id != path_params.pricing_unit_id: - raise PricingUnitNotFoundError + raise PricingUnitNotFoundError() await db.connect_pricing_unit_to_project_node( path_params.project_id, diff --git a/services/web/server/src/simcore_service_webserver/projects/_states_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_states_handlers.py index fe7c62960f0..d8d13341a61 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_states_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_states_handlers.py @@ -93,7 +93,7 @@ class _OpenProjectQuery(BaseModel): @permission_required("project.open") @_handle_project_exceptions async def open_project(request: web.Request) -> web.Response: - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ProjectPathParams, request) query_params: _OpenProjectQuery = parse_request_query_parameters_as( _OpenProjectQuery, request @@ -196,7 +196,7 @@ async def open_project(request: web.Request) -> web.Response: @permission_required("project.close") @_handle_project_exceptions async def close_project(request: web.Request) -> web.Response: - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ProjectPathParams, request) try: @@ -234,7 +234,7 @@ async def close_project(request: web.Request) -> web.Response: @login_required @permission_required("project.read") async def get_project_state(request: web.Request) -> web.Response: - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ProjectPathParams, request) # check that project exists and queries state diff --git a/services/web/server/src/simcore_service_webserver/projects/_wallets_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_wallets_handlers.py index 04c6fd3f218..56e7136d299 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_wallets_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_wallets_handlers.py @@ -9,7 +9,7 @@ from models_library.api_schemas_webserver.wallets import WalletGet from models_library.projects import ProjectID from models_library.wallets import WalletID -from pydantic import BaseModel, Extra +from pydantic import BaseModel, ConfigDict from servicelib.aiohttp.requests_validation import parse_request_path_parameters_as from servicelib.aiohttp.typing_extension import Handler from simcore_service_webserver.utils_aiohttp import envelope_json_response @@ -49,7 +49,7 @@ async def wrapper(request: web.Request) -> web.StreamResponse: @permission_required("project.wallet.*") @_handle_project_wallet_exceptions async def get_project_wallet(request: web.Request): - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ProjectPathParams, request) # ensure the project exists @@ -69,9 +69,7 @@ async def get_project_wallet(request: web.Request): class _ProjectWalletPathParams(BaseModel): project_id: ProjectID wallet_id: WalletID - - class Config: - extra = Extra.forbid + model_config = ConfigDict(extra="forbid") @routes.put( @@ -82,7 +80,7 @@ class Config: @permission_required("project.wallet.*") @_handle_project_wallet_exceptions async def connect_wallet_to_project(request: web.Request): - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_ProjectWalletPathParams, request) # ensure the project exists diff --git a/services/web/server/src/simcore_service_webserver/projects/_workspaces_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_workspaces_handlers.py index e262ce8dc29..c3cc69a8180 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_workspaces_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_workspaces_handlers.py @@ -5,7 +5,7 @@ from models_library.projects import ProjectID from models_library.utils.common_validators import null_or_none_str_to_none_validator from models_library.workspaces import WorkspaceID -from pydantic import BaseModel, Extra, validator +from pydantic import BaseModel, ConfigDict, field_validator from servicelib.aiohttp.requests_validation import parse_request_path_parameters_as from servicelib.aiohttp.typing_extension import Handler from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON @@ -51,13 +51,11 @@ async def wrapper(request: web.Request) -> web.StreamResponse: class _ProjectWorkspacesPathParams(BaseModel): project_id: ProjectID workspace_id: WorkspaceID | None - - class Config: - extra = Extra.forbid + model_config = ConfigDict(extra="forbid") # validators - _null_or_none_str_to_none_validator = validator( - "workspace_id", allow_reuse=True, pre=True + _null_or_none_str_to_none_validator = field_validator( + "workspace_id", mode="before" )(null_or_none_str_to_none_validator) @@ -69,7 +67,7 @@ class Config: @permission_required("project.workspaces.*") @_handle_projects_workspaces_exceptions async def replace_project_workspace(request: web.Request): - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as( _ProjectWorkspacesPathParams, request ) diff --git a/services/web/server/src/simcore_service_webserver/projects/db.py b/services/web/server/src/simcore_service_webserver/projects/db.py index 8c88648ae51..188e9861bc2 100644 --- a/services/web/server/src/simcore_service_webserver/projects/db.py +++ b/services/web/server/src/simcore_service_webserver/projects/db.py @@ -32,7 +32,7 @@ from models_library.utils.fastapi_encoders import jsonable_encoder from models_library.wallets import WalletDB, WalletID from models_library.workspaces import WorkspaceID -from pydantic import parse_obj_as +from pydantic import TypeAdapter from pydantic.types import PositiveInt from servicelib.aiohttp.application_keys import APP_AIOPG_ENGINE_KEY from servicelib.logging_utils import get_log_record_extra, log_context @@ -254,7 +254,9 @@ async def insert_project( """ # NOTE: tags are removed in convert_to_db_names so we keep it - project_tag_ids = parse_obj_as(list[int], project.get("tags", []).copy()) + project_tag_ids = TypeAdapter(list[int]).validate_python( + project.get("tags", []).copy() + ) insert_values = convert_to_db_names(project) insert_values.update( { @@ -1353,7 +1355,7 @@ async def get_project_wallet( .where(projects_to_wallet.c.project_uuid == f"{project_uuid}") ) row = await result.fetchone() - return parse_obj_as(WalletDB, row) if row else None + return TypeAdapter(WalletDB).validate_python(row) if row else None async def connect_wallet_to_project( self, diff --git a/services/web/server/src/simcore_service_webserver/projects/lock.py b/services/web/server/src/simcore_service_webserver/projects/lock.py index eccab79c45a..8a7ff51576d 100644 --- a/services/web/server/src/simcore_service_webserver/projects/lock.py +++ b/services/web/server/src/simcore_service_webserver/projects/lock.py @@ -53,7 +53,7 @@ async def lock_project( blocking=False, token=ProjectLocked( value=True, - owner=Owner(user_id=user_id, **user_fullname), # type: ignore[arg-type] + owner=Owner(user_id=user_id, **user_fullname), status=status, ).json(), ): diff --git a/services/web/server/src/simcore_service_webserver/projects/models.py b/services/web/server/src/simcore_service_webserver/projects/models.py index 8f4a13c172b..87ad529859f 100644 --- a/services/web/server/src/simcore_service_webserver/projects/models.py +++ b/services/web/server/src/simcore_service_webserver/projects/models.py @@ -13,7 +13,7 @@ none_to_empty_str_pre_validator, ) from models_library.workspaces import WorkspaceID -from pydantic import BaseModel, validator +from pydantic import BaseModel, ConfigDict, field_validator from simcore_postgres_database.models.projects import ProjectType, projects ProjectDict: TypeAlias = dict[str, Any] @@ -51,27 +51,26 @@ class ProjectDB(BaseModel): published: bool hidden: bool workspace_id: WorkspaceID | None - - class Config: - orm_mode = True + model_config = ConfigDict( + from_attributes=True, + arbitrary_types_allowed=True, + ) # validators - _empty_thumbnail_is_none = validator("thumbnail", allow_reuse=True, pre=True)( + _empty_thumbnail_is_none = field_validator("thumbnail", mode="before")( empty_str_to_none_pre_validator ) - _none_description_is_empty = validator("description", allow_reuse=True, pre=True)( + _none_description_is_empty = field_validator("description", mode="before")( none_to_empty_str_pre_validator ) - class UserSpecificProjectDataDB(ProjectDB): folder_id: FolderID | None class Config: orm_mode = True - -assert set(ProjectDB.__fields__.keys()).issubset( # nosec +assert set(ProjectDB.model_fields.keys()).issubset( # nosec {c.name for c in projects.columns if c.name not in ["access_rights"]} ) @@ -92,9 +91,9 @@ class UserProjectAccessRightsWithWorkspace(BaseModel): read: bool write: bool delete: bool - - class Config: - orm_mode = True + model_config = ConfigDict( + from_attributes=True, + ) __all__: tuple[str, ...] = ( diff --git a/services/web/server/src/simcore_service_webserver/projects/projects_api.py b/services/web/server/src/simcore_service_webserver/projects/projects_api.py index 8a5e69c39ce..a1b869e8bf5 100644 --- a/services/web/server/src/simcore_service_webserver/projects/projects_api.py +++ b/services/web/server/src/simcore_service_webserver/projects/projects_api.py @@ -63,7 +63,7 @@ from models_library.utils.json_serialization import json_dumps from models_library.wallets import ZERO_CREDITS, WalletID, WalletInfo from models_library.workspaces import UserWorkspaceAccessRightsDB -from pydantic import ByteSize, parse_obj_as +from pydantic import ByteSize, TypeAdapter from servicelib.aiohttp.application_keys import APP_FIRE_AND_FORGET_TASKS_KEY from servicelib.common_headers import ( UNDEFINED_DEFAULT_SIMCORE_USER_AGENT_VALUE, @@ -219,7 +219,7 @@ async def get_project_for_user( gid: access.dict() for gid, access in workspace_db.access_rights.items() } - Project.parse_obj(project) # NOTE: only validates + Project.model_validate(project) # NOTE: only validates return project @@ -384,7 +384,9 @@ async def _get_default_pricing_and_hardware_info( _MACHINE_TOTAL_RAM_SAFE_MARGIN_RATIO: Final[ float ] = 0.1 # NOTE: machines always have less available RAM than advertised -_SIDECARS_OPS_SAFE_RAM_MARGIN: Final[ByteSize] = parse_obj_as(ByteSize, "1GiB") +_SIDECARS_OPS_SAFE_RAM_MARGIN: Final[ByteSize] = TypeAdapter(ByteSize).validate_python( + "1GiB" +) _CPUS_SAFE_MARGIN: Final[float] = 1.4 _MIN_NUM_CPUS: Final[float] = 0.5 @@ -641,8 +643,8 @@ async def _start_dynamic_service( ) if user_default_wallet_preference is None: raise UserDefaultWalletNotFoundError(uid=user_id) - project_wallet_id = parse_obj_as( - WalletID, user_default_wallet_preference.value + project_wallet_id = TypeAdapter(WalletID).validate_python( + user_default_wallet_preference.value ) await connect_wallet_to_project( request.app, @@ -798,7 +800,7 @@ async def add_project_node( ProjectNodeCreate( node_id=node_uuid, required_resources=jsonable_encoder(default_resources) ), - Node.parse_obj( + Node.model_validate( { "key": service_key, "version": service_version, @@ -1424,7 +1426,7 @@ async def _get_project_lock_state( ) return ProjectLocked( value=False, - owner=Owner(user_id=list(set_user_ids)[0], **usernames[0]), # type: ignore[arg-type] + owner=Owner(user_id=list(set_user_ids)[0], **usernames[0]), status=ProjectStatus.OPENED, ) # the project is opened in another tab or browser, or by another user, both case resolves to the project being locked, and opened @@ -1435,7 +1437,7 @@ async def _get_project_lock_state( ) return ProjectLocked( value=True, - owner=Owner(user_id=list(set_user_ids)[0], **usernames[0]), # type: ignore[arg-type] + owner=Owner(user_id=list(set_user_ids)[0], **usernames[0]), status=ProjectStatus.OPENED, ) @@ -1515,8 +1517,12 @@ async def is_service_deprecated( app, user_id, service_key, service_version, product_name ) if deprecation_date := service.get("deprecated"): - deprecation_date = parse_obj_as(datetime.datetime, deprecation_date) - deprecation_date_bool: bool = datetime.datetime.utcnow() > deprecation_date + deprecation_date = TypeAdapter(datetime.datetime).validate_python( + deprecation_date + ) + deprecation_date_bool: bool = ( + datetime.datetime.now(datetime.timezone.utc) > deprecation_date + ) return deprecation_date_bool return False @@ -1551,8 +1557,8 @@ async def get_project_node_resources( db = ProjectDBAPI.get_from_app_context(app) try: project_node = await db.get_project_node(project_id, node_id) - node_resources = parse_obj_as( - ServiceResourcesDict, project_node.required_resources + node_resources = TypeAdapter(ServiceResourcesDict).validate_python( + project_node.required_resources ) if not node_resources: # get default resources @@ -1581,8 +1587,8 @@ async def update_project_node_resources( try: # validate the resource are applied to the same container names current_project_node = await db.get_project_node(project_id, node_id) - current_resources = parse_obj_as( - ServiceResourcesDict, current_project_node.required_resources + current_resources = TypeAdapter(ServiceResourcesDict).validate_python( + current_project_node.required_resources ) if not current_resources: # NOTE: this can happen after the migration @@ -1602,7 +1608,9 @@ async def update_project_node_resources( required_resources=jsonable_encoder(resources), check_update_allowed=True, ) - return parse_obj_as(ServiceResourcesDict, project_node.required_resources) + return TypeAdapter(ServiceResourcesDict).validate_python( + project_node.required_resources + ) except ProjectNodesNodeNotFoundError as exc: raise NodeNotFoundError( project_uuid=f"{project_id}", node_uuid=f"{node_id}" @@ -1871,4 +1879,4 @@ async def get_project_inactivity( project_settings.PROJECTS_INACTIVITY_INTERVAL.total_seconds() ), ) - return parse_obj_as(GetProjectInactivityResponse, project_inactivity) + return TypeAdapter(GetProjectInactivityResponse).validate_python(project_inactivity) diff --git a/services/web/server/src/simcore_service_webserver/projects/settings.py b/services/web/server/src/simcore_service_webserver/projects/settings.py index 727dcb51b35..30a10a86d2a 100644 --- a/services/web/server/src/simcore_service_webserver/projects/settings.py +++ b/services/web/server/src/simcore_service_webserver/projects/settings.py @@ -1,7 +1,7 @@ from datetime import timedelta from aiohttp import web -from pydantic import ByteSize, Field, NonNegativeInt, parse_obj_as +from pydantic import ByteSize, Field, NonNegativeInt, TypeAdapter from settings_library.base import BaseCustomSettings from .._constants import APP_SETTINGS_KEY @@ -9,7 +9,7 @@ class ProjectsSettings(BaseCustomSettings): PROJECTS_MAX_COPY_SIZE_BYTES: ByteSize = Field( - parse_obj_as(ByteSize, "30Gib"), + TypeAdapter(ByteSize).validate_python("30Gib"), description="defines the maximum authorized project data size" " when copying a project (disable with 0)", ) diff --git a/services/web/server/src/simcore_service_webserver/projects/utils.py b/services/web/server/src/simcore_service_webserver/projects/utils.py index d54bc2b433d..18a02a5fb3c 100644 --- a/services/web/server/src/simcore_service_webserver/projects/utils.py +++ b/services/web/server/src/simcore_service_webserver/projects/utils.py @@ -7,7 +7,7 @@ from models_library.projects_nodes_io import NodeIDStr from models_library.services import ServiceKey -from pydantic import parse_obj_as +from pydantic import TypeAdapter from servicelib.decorators import safe_return from yarl import URL @@ -378,7 +378,9 @@ def default_copy_project_name(name: str) -> str: new_copy_index = 1 if current_copy_index := match.group(2): # we receive something of type "(23)" - new_copy_index = parse_obj_as(int, current_copy_index.strip("()")) + 1 + new_copy_index = ( + TypeAdapter(int).validate_python(current_copy_index.strip("()")) + 1 + ) return f"{match.group(1)}({new_copy_index})" return f"{name} (Copy)" diff --git a/services/web/server/src/simcore_service_webserver/resource_usage/_client.py b/services/web/server/src/simcore_service_webserver/resource_usage/_client.py index eb616b5d209..b33a39573c2 100644 --- a/services/web/server/src/simcore_service_webserver/resource_usage/_client.py +++ b/services/web/server/src/simcore_service_webserver/resource_usage/_client.py @@ -22,7 +22,7 @@ from models_library.resource_tracker import PricingPlanId, PricingUnitId from models_library.users import UserID from models_library.wallets import WalletID -from pydantic import NonNegativeInt, parse_obj_as +from pydantic import NonNegativeInt, TypeAdapter from servicelib.aiohttp import status from servicelib.aiohttp.client_session import get_client_session from settings_library.resource_usage_tracker import ResourceUsageTrackerSettings @@ -101,13 +101,12 @@ async def get_default_service_pricing_plan( async with session.get(url) as response: response.raise_for_status() body: dict = await response.json() - return parse_obj_as(PricingPlanGet, body) + return TypeAdapter(PricingPlanGet).validate_python(body) except ClientResponseError as e: if e.status == status.HTTP_404_NOT_FOUND: raise DefaultPricingPlanNotFoundError from e raise - async def get_pricing_plan_unit( app: web.Application, product_name: str, @@ -130,7 +129,7 @@ async def get_pricing_plan_unit( async with session.get(url) as response: response.raise_for_status() body: dict = await response.json() - return parse_obj_as(PricingUnitGet, body) + return TypeAdapter(PricingUnitGet).validate_python(body) async def sum_total_available_credits_in_the_wallet( diff --git a/services/web/server/src/simcore_service_webserver/resource_usage/_pricing_plans_admin_handlers.py b/services/web/server/src/simcore_service_webserver/resource_usage/_pricing_plans_admin_handlers.py index b71317b1aab..60d810c5b42 100644 --- a/services/web/server/src/simcore_service_webserver/resource_usage/_pricing_plans_admin_handlers.py +++ b/services/web/server/src/simcore_service_webserver/resource_usage/_pricing_plans_admin_handlers.py @@ -20,7 +20,7 @@ PricingUnitWithCostUpdate, ) from models_library.users import UserID -from pydantic import BaseModel, Extra, Field +from pydantic import BaseModel, ConfigDict, Field from servicelib.aiohttp.requests_validation import ( parse_request_body_as, parse_request_path_parameters_as, @@ -72,9 +72,9 @@ class _RequestContext(BaseModel): class _GetPricingPlanPathParams(BaseModel): pricing_plan_id: PricingPlanId - - class Config: - extra = Extra.forbid + model_config = ConfigDict( + extra="forbid", + ) @routes.get( @@ -85,7 +85,7 @@ class Config: @permission_required("resource-usage.write") @_handle_pricing_plan_admin_exceptions async def list_pricing_plans(request: web.Request): - req_ctx = _RequestContext.parse_obj(request) + req_ctx = _RequestContext.model_validate(request) pricing_plans_list = await admin_api.list_pricing_plans( app=request.app, @@ -116,7 +116,7 @@ async def list_pricing_plans(request: web.Request): @permission_required("resource-usage.write") @_handle_pricing_plan_admin_exceptions async def get_pricing_plan(request: web.Request): - req_ctx = _RequestContext.parse_obj(request) + req_ctx = _RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_GetPricingPlanPathParams, request) pricing_plan_get = await admin_api.get_pricing_plan( @@ -159,7 +159,7 @@ async def get_pricing_plan(request: web.Request): @permission_required("resource-usage.write") @_handle_pricing_plan_admin_exceptions async def create_pricing_plan(request: web.Request): - req_ctx = _RequestContext.parse_obj(request) + req_ctx = _RequestContext.model_validate(request) body_params = await parse_request_body_as(CreatePricingPlanBodyParams, request) _data = PricingPlanCreate( @@ -208,7 +208,7 @@ async def create_pricing_plan(request: web.Request): @permission_required("resource-usage.write") @_handle_pricing_plan_admin_exceptions async def update_pricing_plan(request: web.Request): - req_ctx = _RequestContext.parse_obj(request) + req_ctx = _RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_GetPricingPlanPathParams, request) body_params = await parse_request_body_as(UpdatePricingPlanBodyParams, request) @@ -256,9 +256,9 @@ async def update_pricing_plan(request: web.Request): class _GetPricingUnitPathParams(BaseModel): pricing_plan_id: PricingPlanId pricing_unit_id: PricingUnitId - - class Config: - extra = Extra.forbid + model_config = ConfigDict( + extra="forbid", + ) @routes.get( @@ -269,7 +269,7 @@ class Config: @permission_required("resource-usage.write") @_handle_pricing_plan_admin_exceptions async def get_pricing_unit(request: web.Request): - req_ctx = _RequestContext.parse_obj(request) + req_ctx = _RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_GetPricingUnitPathParams, request) pricing_unit_get = await admin_api.get_pricing_unit( @@ -299,7 +299,7 @@ async def get_pricing_unit(request: web.Request): @permission_required("resource-usage.write") @_handle_pricing_plan_admin_exceptions async def create_pricing_unit(request: web.Request): - req_ctx = _RequestContext.parse_obj(request) + req_ctx = _RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_GetPricingPlanPathParams, request) body_params = await parse_request_body_as(CreatePricingUnitBodyParams, request) @@ -338,7 +338,7 @@ async def create_pricing_unit(request: web.Request): @permission_required("resource-usage.write") @_handle_pricing_plan_admin_exceptions async def update_pricing_unit(request: web.Request): - req_ctx = _RequestContext.parse_obj(request) + req_ctx = _RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_GetPricingUnitPathParams, request) body_params = await parse_request_body_as(UpdatePricingUnitBodyParams, request) @@ -380,7 +380,7 @@ async def update_pricing_unit(request: web.Request): @permission_required("resource-usage.write") @_handle_pricing_plan_admin_exceptions async def list_connected_services_to_pricing_plan(request: web.Request): - req_ctx = _RequestContext.parse_obj(request) + req_ctx = _RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_GetPricingPlanPathParams, request) connected_services_list = await admin_api.list_connected_services_to_pricing_plan( @@ -409,7 +409,7 @@ async def list_connected_services_to_pricing_plan(request: web.Request): @permission_required("resource-usage.write") @_handle_pricing_plan_admin_exceptions async def connect_service_to_pricing_plan(request: web.Request): - req_ctx = _RequestContext.parse_obj(request) + req_ctx = _RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_GetPricingPlanPathParams, request) body_params = await parse_request_body_as( ConnectServiceToPricingPlanBodyParams, request diff --git a/services/web/server/src/simcore_service_webserver/resource_usage/_pricing_plans_handlers.py b/services/web/server/src/simcore_service_webserver/resource_usage/_pricing_plans_handlers.py index 86072f00e5e..9582cf421cb 100644 --- a/services/web/server/src/simcore_service_webserver/resource_usage/_pricing_plans_handlers.py +++ b/services/web/server/src/simcore_service_webserver/resource_usage/_pricing_plans_handlers.py @@ -4,7 +4,7 @@ from models_library.api_schemas_webserver.resource_usage import PricingUnitGet from models_library.resource_tracker import PricingPlanId, PricingUnitId from models_library.users import UserID -from pydantic import BaseModel, Extra, Field +from pydantic import BaseModel, ConfigDict, Field from servicelib.aiohttp.requests_validation import parse_request_path_parameters_as from servicelib.aiohttp.typing_extension import Handler from servicelib.request_keys import RQT_USERID_KEY @@ -49,9 +49,9 @@ class _RequestContext(BaseModel): class _GetPricingPlanUnitPathParams(BaseModel): pricing_plan_id: PricingPlanId pricing_unit_id: PricingUnitId - - class Config: - extra = Extra.forbid + model_config = ConfigDict( + extra="forbid", + ) @routes.get( @@ -62,7 +62,7 @@ class Config: @permission_required("resource-usage.read") @_handle_resource_usage_exceptions async def get_pricing_plan_unit(request: web.Request): - req_ctx = _RequestContext.parse_obj(request) + req_ctx = _RequestContext.model_validate(request) path_params = parse_request_path_parameters_as( _GetPricingPlanUnitPathParams, request ) diff --git a/services/web/server/src/simcore_service_webserver/resource_usage/_service_runs_handlers.py b/services/web/server/src/simcore_service_webserver/resource_usage/_service_runs_handlers.py index 21180c25203..9060fc3a219 100644 --- a/services/web/server/src/simcore_service_webserver/resource_usage/_service_runs_handlers.py +++ b/services/web/server/src/simcore_service_webserver/resource_usage/_service_runs_handlers.py @@ -24,12 +24,12 @@ from models_library.wallets import WalletID from pydantic import ( BaseModel, - Extra, + ConfigDict, Field, Json, NonNegativeInt, - parse_obj_as, - validator, + TypeAdapter, + field_validator, ) from servicelib.aiohttp.requests_validation import parse_request_query_parameters_as from servicelib.aiohttp.typing_extension import Handler @@ -74,7 +74,7 @@ class _ListServicesResourceUsagesQueryParams(BaseModel): order_by: Json[OrderBy] = Field( # pylint: disable=unsubscriptable-object default=OrderBy(field=IDStr("started_at"), direction=OrderDirection.DESC), description=ORDER_BY_DESCRIPTION, - example='{"field": "started_at", "direction": "desc"}', + examples=['{"field": "started_at", "direction": "desc"}'], ) filters: ( Json[ServiceResourceUsagesFilters] # pylint: disable=unsubscriptable-object @@ -82,10 +82,10 @@ class _ListServicesResourceUsagesQueryParams(BaseModel): ) = Field( default=None, description="Filters to process on the resource usages list, encoded as JSON. Currently supports the filtering of 'started_at' field with 'from' and 'until' parameters in ISO 8601 format. The date range specified is inclusive.", - example='{"started_at": {"from": "yyyy-mm-dd", "until": "yyyy-mm-dd"}}', + examples=['{"started_at": {"from": "yyyy-mm-dd", "until": "yyyy-mm-dd"}}'], ) - @validator("order_by", allow_reuse=True) + @field_validator("order_by") @classmethod def validate_order_by_field(cls, v): if v.field not in { @@ -113,8 +113,7 @@ def validate_order_by_field(cls, v): v.field = "osparc_credits" return v - class Config: - extra = Extra.forbid + model_config = ConfigDict(extra="forbid") class _ListServicesResourceUsagesQueryParamsWithPagination( @@ -129,18 +128,14 @@ class _ListServicesResourceUsagesQueryParamsWithPagination( offset: NonNegativeInt = Field( default=0, description="index to the first item to return (pagination)" ) - - class Config: - extra = Extra.forbid + model_config = ConfigDict(extra="forbid") class _ListServicesAggregatedUsagesQueryParams(PageQueryParameters): aggregated_by: ServicesAggregatedUsagesType time_period: ServicesAggregatedUsagesTimePeriod wallet_id: WalletID - - class Config: - extra = Extra.forbid + model_config = ConfigDict(extra="forbid") # @@ -155,7 +150,7 @@ class Config: @permission_required("resource-usage.read") @_handle_resource_usage_exceptions async def list_resource_usage_services(request: web.Request): - req_ctx = _RequestContext.parse_obj(request) + req_ctx = _RequestContext.model_validate(request) query_params: _ListServicesResourceUsagesQueryParamsWithPagination = ( parse_request_query_parameters_as( _ListServicesResourceUsagesQueryParamsWithPagination, request @@ -169,11 +164,13 @@ async def list_resource_usage_services(request: web.Request): wallet_id=query_params.wallet_id, offset=query_params.offset, limit=query_params.limit, - order_by=parse_obj_as(OrderBy, query_params.order_by), - filters=parse_obj_as(ServiceResourceUsagesFilters | None, query_params.filters), # type: ignore[arg-type] # from pydantic v2 --> https://github.com/pydantic/pydantic/discussions/4950 + order_by=TypeAdapter(OrderBy).validate_python(query_params.order_by), + filters=TypeAdapter(ServiceResourceUsagesFilters | None).validate_python( + query_params.filters + ), ) - page = Page[dict[str, Any]].parse_obj( + page = Page[dict[str, Any]].model_validate( paginate_data( chunk=services.items, request_url=request.url, @@ -183,7 +180,7 @@ async def list_resource_usage_services(request: web.Request): ) ) return web.Response( - text=page.json(**RESPONSE_MODEL_POLICY), + text=page.model_dump_json(**RESPONSE_MODEL_POLICY), content_type=MIMETYPE_APPLICATION_JSON, ) @@ -196,7 +193,7 @@ async def list_resource_usage_services(request: web.Request): @permission_required("resource-usage.read") @_handle_resource_usage_exceptions async def list_osparc_credits_aggregated_usages(request: web.Request): - req_ctx = _RequestContext.parse_obj(request) + req_ctx = _RequestContext.model_validate(request) query_params: _ListServicesAggregatedUsagesQueryParams = ( parse_request_query_parameters_as( _ListServicesAggregatedUsagesQueryParams, request @@ -216,7 +213,7 @@ async def list_osparc_credits_aggregated_usages(request: web.Request): ) ) - page = Page[dict[str, Any]].parse_obj( + page = Page[dict[str, Any]].model_validate( paginate_data( chunk=aggregated_services.items, request_url=request.url, @@ -236,7 +233,7 @@ async def list_osparc_credits_aggregated_usages(request: web.Request): @permission_required("resource-usage.read") @_handle_resource_usage_exceptions async def export_resource_usage_services(request: web.Request): - req_ctx = _RequestContext.parse_obj(request) + req_ctx = _RequestContext.model_validate(request) query_params: _ListServicesResourceUsagesQueryParams = ( parse_request_query_parameters_as( _ListServicesResourceUsagesQueryParams, request @@ -247,7 +244,9 @@ async def export_resource_usage_services(request: web.Request): user_id=req_ctx.user_id, product_name=req_ctx.product_name, wallet_id=query_params.wallet_id, - order_by=parse_obj_as(OrderBy | None, query_params.order_by), # type: ignore[arg-type] # from pydantic v2 --> https://github.com/pydantic/pydantic/discussions/4950 - filters=parse_obj_as(ServiceResourceUsagesFilters | None, query_params.filters), # type: ignore[arg-type] # from pydantic v2 --> https://github.com/pydantic/pydantic/discussions/4950 + order_by=TypeAdapter(OrderBy | None).validate_python(query_params.order_by), + filters=TypeAdapter(ServiceResourceUsagesFilters | None).validate_python( + query_params.filters + ), ) raise web.HTTPFound(location=f"{download_url}") diff --git a/services/web/server/src/simcore_service_webserver/rest/_handlers.py b/services/web/server/src/simcore_service_webserver/rest/_handlers.py index 0cf11ad4810..e90f1e82b56 100644 --- a/services/web/server/src/simcore_service_webserver/rest/_handlers.py +++ b/services/web/server/src/simcore_service_webserver/rest/_handlers.py @@ -8,7 +8,7 @@ from aiohttp import web from models_library.utils.pydantic_tools_extension import FieldNotRequired -from pydantic import BaseModel, parse_obj_as +from pydantic import BaseModel, TypeAdapter from servicelib.aiohttp import status from .._constants import APP_PUBLIC_CONFIG_PER_PRODUCT, APP_SETTINGS_KEY @@ -104,7 +104,8 @@ async def get_scheduled_maintenance(request: web.Request): if maintenance_data := await redis_client.get(hash_key): assert ( # nosec - parse_obj_as(_ScheduledMaintenanceGet, maintenance_data) is not None + TypeAdapter(_ScheduledMaintenanceGet).validate_python(maintenance_data) + is not None ) return envelope_json_response(maintenance_data) diff --git a/services/web/server/src/simcore_service_webserver/scicrunch/_resolver.py b/services/web/server/src/simcore_service_webserver/scicrunch/_resolver.py index efef7f77668..07d62498230 100644 --- a/services/web/server/src/simcore_service_webserver/scicrunch/_resolver.py +++ b/services/web/server/src/simcore_service_webserver/scicrunch/_resolver.py @@ -93,7 +93,7 @@ async def resolve_rrid( body = await resp.json() # process and simplify response - resolved = ResolverResponseBody.parse_obj(body) + resolved = ResolverResponseBody.model_validate(body) if resolved.hits.total == 0: return [] @@ -113,7 +113,7 @@ async def resolve_rrid( items = [] for hit in resolved.hits.hits: try: - items.append(ResolvedItem.parse_obj(hit.source.flatten_dict())) + items.append(ResolvedItem.model_validate(hit.source.flatten_dict())) except ValidationError as err: logger.warning("Skipping unexpected response %s: %s", url, err) diff --git a/services/web/server/src/simcore_service_webserver/scicrunch/_rest.py b/services/web/server/src/simcore_service_webserver/scicrunch/_rest.py index 70e4963fc68..3f6285766b8 100644 --- a/services/web/server/src/simcore_service_webserver/scicrunch/_rest.py +++ b/services/web/server/src/simcore_service_webserver/scicrunch/_rest.py @@ -120,4 +120,4 @@ async def autocomplete_by_name( ) as resp: body = await resp.json() assert body.get("success") # nosec - return ListOfResourceHits.parse_obj(body.get("data", [])) + return ListOfResourceHits.model_validate(body.get("data", [])) diff --git a/services/web/server/src/simcore_service_webserver/scicrunch/models.py b/services/web/server/src/simcore_service_webserver/scicrunch/models.py index 743f4bd8211..b7144b455c1 100644 --- a/services/web/server/src/simcore_service_webserver/scicrunch/models.py +++ b/services/web/server/src/simcore_service_webserver/scicrunch/models.py @@ -6,7 +6,7 @@ import re from datetime import datetime -from pydantic import BaseModel, Field, validator +from pydantic import BaseModel, ConfigDict, Field, field_validator logger = logging.getLogger(__name__) @@ -58,19 +58,20 @@ class ResearchResource(BaseModel): rrid: str = Field( ..., description="Unique identifier used as classifier, i.e. to tag studies and services", - regex=STRICT_RRID_PATTERN, + pattern=STRICT_RRID_PATTERN, ) name: str description: str - @validator("rrid", pre=True) + @field_validator("rrid", mode="before") @classmethod def format_rrid(cls, v): return normalize_rrid_tags(v, with_prefix=True) - class Config: - orm_mode = True - anystr_strip_whitespace = True + model_config = ConfigDict( + from_attributes=True, + str_strip_whitespace=True, + ) # postgres_database.scicrunch_resources ORM -------------------- diff --git a/services/web/server/src/simcore_service_webserver/scicrunch/service_client.py b/services/web/server/src/simcore_service_webserver/scicrunch/service_client.py index bcaf413b4db..c863d768fd6 100644 --- a/services/web/server/src/simcore_service_webserver/scicrunch/service_client.py +++ b/services/web/server/src/simcore_service_webserver/scicrunch/service_client.py @@ -8,7 +8,7 @@ import logging from aiohttp import ClientSession, client_exceptions, web -from pydantic import HttpUrl, ValidationError, parse_obj_as +from pydantic import HttpUrl, TypeAdapter, ValidationError from servicelib.aiohttp.client_session import get_client_session from yarl import URL @@ -90,8 +90,8 @@ def get_search_web_url(self, rrid: str) -> str: def get_resolver_web_url(self, rrid: str) -> HttpUrl: # example https://scicrunch.org/resolver/RRID:AB_90755 - output: HttpUrl = parse_obj_as( - HttpUrl, f"{self.settings.SCICRUNCH_RESOLVER_BASE_URL}/{rrid}" + output: HttpUrl = TypeAdapter(HttpUrl).validate_python( + f"{self.settings.SCICRUNCH_RESOLVER_BASE_URL}/{rrid}" ) return output diff --git a/services/web/server/src/simcore_service_webserver/scicrunch/settings.py b/services/web/server/src/simcore_service_webserver/scicrunch/settings.py index ecc027374c0..0bf88e69b05 100644 --- a/services/web/server/src/simcore_service_webserver/scicrunch/settings.py +++ b/services/web/server/src/simcore_service_webserver/scicrunch/settings.py @@ -1,5 +1,5 @@ from aiohttp import web -from pydantic import Field, HttpUrl, SecretStr, parse_obj_as +from pydantic import Field, HttpUrl, SecretStr, TypeAdapter from settings_library.base import BaseCustomSettings from .._constants import APP_SETTINGS_KEY @@ -11,7 +11,7 @@ class SciCrunchSettings(BaseCustomSettings): SCICRUNCH_API_BASE_URL: HttpUrl = Field( - default=parse_obj_as(HttpUrl, f"{SCICRUNCH_DEFAULT_URL}/api/1"), + default=TypeAdapter(HttpUrl).validate_python(f"{SCICRUNCH_DEFAULT_URL}/api/1"), description="Base url to scicrunch API's entrypoint", ) @@ -20,7 +20,9 @@ class SciCrunchSettings(BaseCustomSettings): SCICRUNCH_API_KEY: SecretStr SCICRUNCH_RESOLVER_BASE_URL: HttpUrl = Field( - default=parse_obj_as(HttpUrl, f"{SCICRUNCH_DEFAULT_URL}/resolver"), + default=TypeAdapter(HttpUrl).validate_python( + f"{SCICRUNCH_DEFAULT_URL}/resolver" + ), description="Base url to scicrunch resolver entrypoint", ) diff --git a/services/web/server/src/simcore_service_webserver/security/_authz_db.py b/services/web/server/src/simcore_service_webserver/security/_authz_db.py index dbb04f7943c..b62baf946f4 100644 --- a/services/web/server/src/simcore_service_webserver/security/_authz_db.py +++ b/services/web/server/src/simcore_service_webserver/security/_authz_db.py @@ -7,7 +7,7 @@ from models_library.basic_types import IdInt from models_library.products import ProductName from models_library.users import UserID -from pydantic import parse_obj_as +from pydantic import TypeAdapter from simcore_postgres_database.models.groups import user_to_groups from simcore_postgres_database.models.products import products from simcore_postgres_database.models.users import UserRole @@ -35,8 +35,12 @@ async def get_active_user_or_none(engine: Engine, email: str) -> AuthInfoDict | ) ) row = await result.fetchone() - assert row is None or parse_obj_as(IdInt, row.id) is not None # nosec - assert row is None or parse_obj_as(UserRole, row.role) is not None # nosec + assert ( + row is None or TypeAdapter(IdInt).validate_python(row.id) is not None + ) # nosec + assert ( + row is None or TypeAdapter(UserRole).validate_python(row.role) is not None + ) # nosec return AuthInfoDict(id=row.id, role=row.role) if row else None diff --git a/services/web/server/src/simcore_service_webserver/session/settings.py b/services/web/server/src/simcore_service_webserver/session/settings.py index b5f3c333fa8..f71620e7c95 100644 --- a/services/web/server/src/simcore_service_webserver/session/settings.py +++ b/services/web/server/src/simcore_service_webserver/session/settings.py @@ -1,8 +1,7 @@ from typing import Final from aiohttp import web -from pydantic import PositiveInt -from pydantic.class_validators import validator +from pydantic import AliasChoices, PositiveInt, field_validator from pydantic.fields import Field from pydantic.types import SecretStr from settings_library.base import BaseCustomSettings @@ -22,7 +21,9 @@ class SessionSettings(BaseCustomSettings, MixinSessionSettings): description="Secret key to encrypt cookies. " 'TIP: python3 -c "from cryptography.fernet import *; print(Fernet.generate_key())"', min_length=44, - env=["SESSION_SECRET_KEY", "WEBSERVER_SESSION_SECRET_KEY"], + validation_alias=AliasChoices( + "SESSION_SECRET_KEY", "WEBSERVER_SESSION_SECRET_KEY" + ), ) SESSION_ACCESS_TOKENS_EXPIRATION_INTERVAL_SECS: int = Field( @@ -53,12 +54,12 @@ class SessionSettings(BaseCustomSettings, MixinSessionSettings): description="This prevents JavaScript from accessing the session cookie", ) - @validator("SESSION_SECRET_KEY") + @field_validator("SESSION_SECRET_KEY") @classmethod def check_valid_fernet_key(cls, v): return cls.do_check_valid_fernet_key(v) - @validator("SESSION_COOKIE_SAMESITE") + @field_validator("SESSION_COOKIE_SAMESITE") @classmethod def check_valid_samesite_attribute(cls, v): # NOTE: Replacement to `Literal["Strict", "Lax"] | None` due to bug in settings_library/base.py:93: in prepare_field diff --git a/services/web/server/src/simcore_service_webserver/socketio/models.py b/services/web/server/src/simcore_service_webserver/socketio/models.py index 06e5b9014cb..367354d39d0 100644 --- a/services/web/server/src/simcore_service_webserver/socketio/models.py +++ b/services/web/server/src/simcore_service_webserver/socketio/models.py @@ -12,23 +12,24 @@ from models_library.socketio import SocketMessageDict from models_library.users import UserID from models_library.utils.fastapi_encoders import jsonable_encoder -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict class WebSocketMessageBase(BaseModel): - event_type: str = Field(..., const=True) + event_type: Literal[...] = ... @classmethod def get_event_type(cls) -> str: - _event_type: str = cls.__fields__["event_type"].default + _event_type: str = cls.model_fields["event_type"].default return _event_type @abstractmethod def to_socket_dict(self) -> SocketMessageDict: ... - class Config: - frozen = True + model_config = ConfigDict( + frozen=True, + ) class _WebSocketProjectMixin(BaseModel): diff --git a/services/web/server/src/simcore_service_webserver/statics/_events.py b/services/web/server/src/simcore_service_webserver/statics/_events.py index 49cef6deb64..d5731754f02 100644 --- a/services/web/server/src/simcore_service_webserver/statics/_events.py +++ b/services/web/server/src/simcore_service_webserver/statics/_events.py @@ -64,7 +64,7 @@ async def create_cached_indexes(app: web.Application) -> None: session: ClientSession = get_client_session(app) for frontend_name in FRONTEND_APPS_AVAILABLE: - url = URL(settings.STATIC_WEBSERVER_URL) / frontend_name + url = URL(f"{settings.STATIC_WEBSERVER_URL}") / frontend_name _logger.info("Fetching index from %s", url) try: body = "" diff --git a/services/web/server/src/simcore_service_webserver/statics/settings.py b/services/web/server/src/simcore_service_webserver/statics/settings.py index 275def8154b..46471b2f235 100644 --- a/services/web/server/src/simcore_service_webserver/statics/settings.py +++ b/services/web/server/src/simcore_service_webserver/statics/settings.py @@ -7,7 +7,7 @@ import pycountry from aiohttp import web from models_library.utils.change_case import snake_to_camel -from pydantic import AnyHttpUrl, Field, parse_obj_as +from pydantic import AliasChoices, AnyHttpUrl, Field, TypeAdapter from settings_library.base import BaseCustomSettings from .._constants import APP_SETTINGS_KEY @@ -121,12 +121,12 @@ def to_statics(self) -> dict[str, Any]: class StaticWebserverModuleSettings(BaseCustomSettings): STATIC_WEBSERVER_URL: AnyHttpUrl = Field( - default=parse_obj_as(AnyHttpUrl, "http://static-webserver:8000"), + default=TypeAdapter(AnyHttpUrl).validate_python("http://static-webserver:8000"), description="url fort static content", - env=[ + validation_alias=AliasChoices( "STATIC_WEBSERVER_URL", "WEBSERVER_STATIC_MODULE_STATIC_WEB_SERVER_URL", # legacy - ], + ), ) diff --git a/services/web/server/src/simcore_service_webserver/storage/_handlers.py b/services/web/server/src/simcore_service_webserver/storage/_handlers.py index f5acb0171b1..a9388f6d57e 100644 --- a/services/web/server/src/simcore_service_webserver/storage/_handlers.py +++ b/services/web/server/src/simcore_service_webserver/storage/_handlers.py @@ -14,7 +14,7 @@ ) from models_library.projects_nodes_io import LocationID from models_library.utils.fastapi_encoders import jsonable_encoder -from pydantic import AnyUrl, BaseModel, ByteSize, parse_obj_as +from pydantic import AnyUrl, BaseModel, ByteSize, TypeAdapter from servicelib.aiohttp.client_session import get_client_session from servicelib.aiohttp.requests_validation import ( parse_request_body_as, @@ -74,7 +74,7 @@ def _from_storage_url(request: web.Request, storage_url: AnyUrl) -> AnyUrl: f"/v0/storage{storage_url.path.removeprefix(prefix)}", encoded=True ).with_scheme(request.headers.get(X_FORWARDED_PROTO, request.url.scheme)) - webserver_url: AnyUrl = parse_obj_as(AnyUrl, f"{converted_url}") + webserver_url: AnyUrl = TypeAdapter(AnyUrl).validate_python(f"{converted_url}") return webserver_url @@ -237,7 +237,7 @@ class _QueryParams(BaseModel): payload, status = await _forward_request_to_storage(request, "PUT", body=None) data, _ = unwrap_envelope(payload) - file_upload_schema = FileUploadSchema.parse_obj(data) + file_upload_schema = FileUploadSchema.model_validate(data) file_upload_schema.links.complete_upload = _from_storage_url( request, file_upload_schema.links.complete_upload ) @@ -265,7 +265,7 @@ class _PathParams(BaseModel): request, "POST", body=body_item.dict() ) data, _ = unwrap_envelope(payload) - file_upload_complete = FileUploadCompleteResponse.parse_obj(data) + file_upload_complete = FileUploadCompleteResponse.model_validate(data) file_upload_complete.links.state = _from_storage_url( request, file_upload_complete.links.state ) diff --git a/services/web/server/src/simcore_service_webserver/storage/api.py b/services/web/server/src/simcore_service_webserver/storage/api.py index 2ddf66d8907..8e1ad334beb 100644 --- a/services/web/server/src/simcore_service_webserver/storage/api.py +++ b/services/web/server/src/simcore_service_webserver/storage/api.py @@ -20,7 +20,7 @@ from models_library.projects_nodes_io import LocationID, NodeID, SimCoreFileLink from models_library.users import UserID from models_library.utils.fastapi_encoders import jsonable_encoder -from pydantic import ByteSize, HttpUrl, parse_obj_as +from pydantic import ByteSize, HttpUrl, TypeAdapter from servicelib.aiohttp.client_session import get_client_session from servicelib.aiohttp.long_running_tasks.client import ( LRTask, @@ -56,7 +56,7 @@ async def get_storage_locations( locations_url = (api_endpoint / "locations").with_query(user_id=user_id) async with session.get(f"{locations_url}") as response: response.raise_for_status() - locations_enveloped = Envelope[FileLocationArray].parse_obj( + locations_enveloped = Envelope[FileLocationArray].model_validate( await response.json() ) assert locations_enveloped.data # nosec @@ -89,15 +89,15 @@ async def get_project_total_size_simcore_s3( ).with_query(user_id=user_id, project_id=f"{project_uuid}") async with session.get(f"{files_metadata_url}") as response: response.raise_for_status() - list_of_files_enveloped = Envelope[list[FileMetaDataGet]].parse_obj( - await response.json() - ) + list_of_files_enveloped = Envelope[ + list[FileMetaDataGet] + ].model_validate(await response.json()) assert list_of_files_enveloped.data is not None # nosec project_size_bytes += sum( file_metadata.file_size for file_metadata in list_of_files_enveloped.data ) - return parse_obj_as(ByteSize, project_size_bytes) + return TypeAdapter(ByteSize).validate_python(project_size_bytes) async def copy_data_folders_from_project( @@ -204,10 +204,10 @@ async def get_download_link( async with session.get(f"{url}") as response: response.raise_for_status() download: PresignedLink | None = ( - Envelope[PresignedLink].parse_obj(await response.json()).data + Envelope[PresignedLink].model_validate(await response.json()).data ) assert download is not None # nosec - link: HttpUrl = parse_obj_as(HttpUrl, download.link) + link: HttpUrl = TypeAdapter(HttpUrl).validate_python(download.link) return link @@ -227,7 +227,7 @@ async def get_files_in_node_folder( async with session.get(f"{files_metadata_url}") as response: response.raise_for_status() - list_of_files_enveloped = Envelope[list[FileMetaDataGet]].parse_obj( + list_of_files_enveloped = Envelope[list[FileMetaDataGet]].model_validate( await response.json() ) assert list_of_files_enveloped.data is not None # nosec diff --git a/services/web/server/src/simcore_service_webserver/storage/schemas.py b/services/web/server/src/simcore_service_webserver/storage/schemas.py index 4c47c99a8ff..ee99c6d6a94 100644 --- a/services/web/server/src/simcore_service_webserver/storage/schemas.py +++ b/services/web/server/src/simcore_service_webserver/storage/schemas.py @@ -1,8 +1,8 @@ from enum import Enum -from typing import Any, ClassVar, TypeAlias +from typing import Any, TypeAlias from models_library.api_schemas_storage import TableSynchronisation -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field, RootModel # NOTE: storage generates URLs that contain double encoded # slashes, and when applying validation via `StorageFileID` @@ -14,14 +14,14 @@ class FileLocation(BaseModel): name: str | None = None id: float | None = None - - class Config: - schema_extra: ClassVar[dict[str, Any]] = { + model_config = ConfigDict( + json_schema_extra={ "example": { "name": "simcore.s3", "id": 0, }, } + ) class FileLocationArray(BaseModel): @@ -60,18 +60,18 @@ class FileUploadCompleteFuture(BaseModel): class DatasetMetaData(BaseModel): dataset_id: str | None = None display_name: str | None = None - - class Config: - schema_extra: ClassVar[dict[str, Any]] = { + model_config = ConfigDict( + json_schema_extra={ "example": { "dataset_id": "N:id-aaaa", "display_name": "simcore-testing", }, } + ) -class DatasetMetaDataArray(BaseModel): - __root__: list[DatasetMetaData] +class DatasetMetaDataArray(RootModel[list[DatasetMetaData]]): + ... class FileLocationEnveloped(BaseModel): @@ -121,9 +121,8 @@ class FileMetaData(BaseModel): file_size: int | None = None entity_tag: str | None = None is_directory: bool | None = None - - class Config: - schema_extra: ClassVar[dict[str, Any]] = { + model_config = ConfigDict( + json_schema_extra={ "example": { "file_uuid": "simcore-testing/105/1000/3", "location_id": "0", @@ -138,6 +137,7 @@ class Config: "is_directory": False, } } + ) class FileMetaDataArray(BaseModel): @@ -151,9 +151,9 @@ class FileMetaEnvelope(BaseModel): class PresignedLink(BaseModel): link: str | None = None - - class Config: - schema_extra: ClassVar[dict[str, Any]] = {"example": {"link": "example_link"}} + model_config = ConfigDict( + json_schema_extra={"example": {"link": "example_link"}}, + ) class PresignedLinkEnveloped(BaseModel): diff --git a/services/web/server/src/simcore_service_webserver/storage/settings.py b/services/web/server/src/simcore_service_webserver/storage/settings.py index e49e652699d..c69dc43253b 100644 --- a/services/web/server/src/simcore_service_webserver/storage/settings.py +++ b/services/web/server/src/simcore_service_webserver/storage/settings.py @@ -2,7 +2,7 @@ from aiohttp import web from models_library.basic_types import PortInt, VersionTag -from pydantic import parse_obj_as +from pydantic import TypeAdapter from settings_library.base import BaseCustomSettings from settings_library.utils_service import DEFAULT_AIOHTTP_PORT, MixinServiceSettings from yarl import URL @@ -12,8 +12,8 @@ class StorageSettings(BaseCustomSettings, MixinServiceSettings): STORAGE_HOST: str = "storage" - STORAGE_PORT: PortInt = parse_obj_as(PortInt, DEFAULT_AIOHTTP_PORT) - STORAGE_VTAG: VersionTag = parse_obj_as(VersionTag, "v0") + STORAGE_PORT: PortInt = TypeAdapter(PortInt).validate_python(DEFAULT_AIOHTTP_PORT) + STORAGE_VTAG: VersionTag = TypeAdapter(VersionTag).validate_python("v0") @cached_property def base_url(self) -> URL: diff --git a/services/web/server/src/simcore_service_webserver/studies_dispatcher/_catalog.py b/services/web/server/src/simcore_service_webserver/studies_dispatcher/_catalog.py index 3df62ebd379..3cd377ee163 100644 --- a/services/web/server/src/simcore_service_webserver/studies_dispatcher/_catalog.py +++ b/services/web/server/src/simcore_service_webserver/studies_dispatcher/_catalog.py @@ -9,7 +9,7 @@ from aiopg.sa.engine import Engine from models_library.groups import EVERYONE_GROUP_ID from models_library.services import ServiceKey, ServiceVersion -from pydantic import HttpUrl, PositiveInt, ValidationError, parse_obj_as +from pydantic import HttpUrl, PositiveInt, TypeAdapter, ValidationError from servicelib.logging_utils import log_decorator from simcore_postgres_database.models.services import ( services_access_rights, @@ -114,7 +114,7 @@ async def iter_latest_product_services( version=row.version, title=row.name, description=row.description, - thumbnail=row.thumbnail or settings.STUDIES_DEFAULT_SERVICE_THUMBNAIL, + thumbnail=row.thumbnail or f"{settings.STUDIES_DEFAULT_SERVICE_THUMBNAIL}", file_extensions=service_filetypes.get(row.key, []), ) @@ -171,7 +171,7 @@ async def validate_requested_service( thumbnail_or_none = None if row.thumbnail is not None: with suppress(ValidationError): - thumbnail_or_none = parse_obj_as(HttpUrl, row.thumbnail) + thumbnail_or_none = TypeAdapter(HttpUrl).validate_python(row.thumbnail) return ValidService( key=service_key, diff --git a/services/web/server/src/simcore_service_webserver/studies_dispatcher/_core.py b/services/web/server/src/simcore_service_webserver/studies_dispatcher/_core.py index dcafdf528de..fe76e1a2855 100644 --- a/services/web/server/src/simcore_service_webserver/studies_dispatcher/_core.py +++ b/services/web/server/src/simcore_service_webserver/studies_dispatcher/_core.py @@ -7,7 +7,7 @@ from aiohttp import web from models_library.services import ServiceVersion from models_library.utils.pydantic_tools_extension import parse_obj_or_none -from pydantic import ByteSize, ValidationError, parse_obj_as +from pydantic import ByteSize, TypeAdapter, ValidationError from servicelib.logging_utils import log_decorator from simcore_postgres_database.models.services_consume_filetypes import ( services_consume_filetypes, @@ -138,7 +138,9 @@ def _version(column_or_value): row = await result.first() if row: view = ViewerInfo.create_from_db(row) - view.version = parse_obj_as(ServiceVersion, service_version) + view.version = TypeAdapter(ServiceVersion).validate_python( + service_version + ) return view raise IncompatibleService(file_type=file_type) diff --git a/services/web/server/src/simcore_service_webserver/studies_dispatcher/_errors.py b/services/web/server/src/simcore_service_webserver/studies_dispatcher/_errors.py index d68cc284190..3c994daa1ed 100644 --- a/services/web/server/src/simcore_service_webserver/studies_dispatcher/_errors.py +++ b/services/web/server/src/simcore_service_webserver/studies_dispatcher/_errors.py @@ -6,22 +6,22 @@ class StudyDispatcherError(WebServerBaseError, ValueError): class IncompatibleService(StudyDispatcherError): - code = "studies_dispatcher.incompatible_service" + code = "studies_dispatcher.incompatible_service" # type: ignore[assignment] msg_template = "None of the registered services can handle '{file_type}'" class FileToLarge(StudyDispatcherError): - code = "studies_dispatcher.file_to_large" + code = "studies_dispatcher.file_to_large" # type: ignore[assignment] msg_template = "File size {file_size_in_mb} MB is over allowed limit" class ServiceNotFound(StudyDispatcherError): - code = "studies_dispatcher.service_not_found" + code = "studies_dispatcher.service_not_found" # type: ignore[assignment] msg_template = "Service {service_key}:{service_version} not found" class InvalidRedirectionParams(StudyDispatcherError): - code = "studies_dispatcher.invalid_redirection_params" + code = "studies_dispatcher.invalid_redirection_params" # type: ignore[assignment] msg_template = ( "The link you provided is invalid because it doesn't contain any information related to data or a service." " Please check the link and make sure it is correct." diff --git a/services/web/server/src/simcore_service_webserver/studies_dispatcher/_models.py b/services/web/server/src/simcore_service_webserver/studies_dispatcher/_models.py index 30aa1387269..c697b409c0f 100644 --- a/services/web/server/src/simcore_service_webserver/studies_dispatcher/_models.py +++ b/services/web/server/src/simcore_service_webserver/studies_dispatcher/_models.py @@ -1,6 +1,6 @@ from aiopg.sa.result import RowProxy from models_library.services import ServiceKey, ServiceVersion -from pydantic import BaseModel, Field, HttpUrl, PositiveInt, parse_obj_as +from pydantic import BaseModel, Field, HttpUrl, PositiveInt, TypeAdapter class ServiceInfo(BaseModel): @@ -10,7 +10,9 @@ class ServiceInfo(BaseModel): label: str = Field(..., description="Display name") thumbnail: HttpUrl = Field( - default=parse_obj_as(HttpUrl, "https://via.placeholder.com/170x120.png") + default=TypeAdapter(HttpUrl).validate_python( + "https://via.placeholder.com/170x120.png" + ) ) is_guest_allowed: bool = True diff --git a/services/web/server/src/simcore_service_webserver/studies_dispatcher/_projects.py b/services/web/server/src/simcore_service_webserver/studies_dispatcher/_projects.py index e4b71213ee6..b9dd5ee7eb5 100644 --- a/services/web/server/src/simcore_service_webserver/studies_dispatcher/_projects.py +++ b/services/web/server/src/simcore_service_webserver/studies_dispatcher/_projects.py @@ -18,7 +18,7 @@ from models_library.projects_nodes_io import DownloadLink, NodeID, PortLink from models_library.projects_ui import StudyUI from models_library.services import ServiceKey, ServiceVersion -from pydantic import AnyUrl, HttpUrl, parse_obj_as +from pydantic import AnyUrl, HttpUrl, TypeAdapter from servicelib.logging_utils import log_decorator from ..projects.db import ProjectDBAPI @@ -32,10 +32,12 @@ _logger = logging.getLogger(__name__) -_FILE_PICKER_KEY: ServiceKey = parse_obj_as( - ServiceKey, "simcore/services/frontend/file-picker" +_FILE_PICKER_KEY: ServiceKey = TypeAdapter(ServiceKey).validate_python( + "simcore/services/frontend/file-picker" +) +_FILE_PICKER_VERSION: ServiceVersion = TypeAdapter(ServiceVersion).validate_python( + "1.0.0" ) -_FILE_PICKER_VERSION: ServiceVersion = parse_obj_as(ServiceVersion, "1.0.0") def _generate_nodeids(project_id: ProjectID) -> tuple[NodeID, NodeID]: @@ -55,12 +57,12 @@ def _create_file_picker(download_link: str, output_label: str | None): # also to name the file in case it is downloaded data = {} - data["downloadLink"] = url = parse_obj_as(AnyUrl, download_link) + data["downloadLink"] = url = TypeAdapter(AnyUrl).validate_python(download_link) if output_label: data["label"] = Path(output_label).name elif url.path: data["label"] = Path(url.path).name - output = DownloadLink.parse_obj(data) + output = DownloadLink.model_validate(data) output_id = "outFile" node = Node( @@ -69,7 +71,7 @@ def _create_file_picker(download_link: str, output_label: str | None): label="File Picker", inputs={}, inputNodes=[], - outputs={output_id: output}, # type: ignore[dict-item] + outputs={output_id: output}, progress=0, ) return node, output_id @@ -94,12 +96,12 @@ def _create_project( uuid=project_id, name=name, description=description, - thumbnail=thumbnail, # type: ignore[arg-type] + thumbnail=thumbnail, prjOwner=owner.email, accessRights={owner.primary_gid: access_rights}, # type: ignore[dict-item] creationDate=DateTimeStr(now_str()), lastChangeDate=DateTimeStr(now_str()), - workbench=workbench, # type: ignore[arg-type] + workbench=workbench, ui=StudyUI(workbench=workbench_ui), # type: ignore[arg-type] ) @@ -145,7 +147,7 @@ def _create_project_with_filepicker_and_service( viewer_info: ViewerInfo, ) -> Project: file_picker, file_picker_output_id = _create_file_picker( - download_link, output_label=None + f"{download_link}", output_label=None ) viewer_service = Node( @@ -153,7 +155,7 @@ def _create_project_with_filepicker_and_service( version=viewer_info.version, label=viewer_info.label, inputs={ - viewer_info.input_port_key: PortLink( # type: ignore[dict-item] + viewer_info.input_port_key: PortLink( nodeUuid=file_picker_id, output=file_picker_output_id, ) @@ -344,7 +346,7 @@ async def get_or_create_project_with_file( ): # nodes file_picker, _ = _create_file_picker( - file_params.download_link, output_label=file_params.file_name + f"{file_params.download_link}", output_label=file_params.file_name ) # project diff --git a/services/web/server/src/simcore_service_webserver/studies_dispatcher/_projects_permalinks.py b/services/web/server/src/simcore_service_webserver/studies_dispatcher/_projects_permalinks.py index 055f0f78fcf..9308f4e3c81 100644 --- a/services/web/server/src/simcore_service_webserver/studies_dispatcher/_projects_permalinks.py +++ b/services/web/server/src/simcore_service_webserver/studies_dispatcher/_projects_permalinks.py @@ -4,7 +4,7 @@ import sqlalchemy as sa from aiohttp import web from models_library.projects import ProjectID, ProjectIDStr -from pydantic import HttpUrl, parse_obj_as +from pydantic import HttpUrl, TypeAdapter from simcore_postgres_database.models.project_to_groups import project_to_groups from simcore_postgres_database.models.projects import ProjectType, projects @@ -58,8 +58,7 @@ def create_permalink_for_study( # create url_for = create_url_for_function(request) - permalink = parse_obj_as( - HttpUrl, + permalink = TypeAdapter(HttpUrl).validate_python( url_for(route_name="get_redirection_to_study_page", id=f"{project_uuid}"), ) diff --git a/services/web/server/src/simcore_service_webserver/studies_dispatcher/_redirects_handlers.py b/services/web/server/src/simcore_service_webserver/studies_dispatcher/_redirects_handlers.py index 1b60fd5f7e0..27252d6abfd 100644 --- a/services/web/server/src/simcore_service_webserver/studies_dispatcher/_redirects_handlers.py +++ b/services/web/server/src/simcore_service_webserver/studies_dispatcher/_redirects_handlers.py @@ -11,7 +11,7 @@ from models_library.projects import ProjectID from models_library.projects_nodes_io import NodeID from models_library.services import ServiceKey, ServiceVersion -from pydantic import BaseModel, Extra, ValidationError, validator +from pydantic import BaseModel, ConfigDict, ValidationError, field_validator from servicelib.aiohttp import status from servicelib.aiohttp.requests_validation import parse_request_query_parameters_as from servicelib.aiohttp.typing_extension import Handler @@ -146,15 +146,17 @@ async def wrapper(request: web.Request) -> web.StreamResponse: class ServiceQueryParams(ServiceParams): - class Config: - extra = Extra.forbid + model_config = ConfigDict( + extra="forbid", + ) class FileQueryParams(FileParams): - class Config: - extra = Extra.forbid + model_config = ConfigDict( + extra="forbid", + ) - @validator("file_type") + @field_validator("file_type") @classmethod def ensure_extension_upper_and_dotless(cls, v): # NOTE: see filetype constraint-check @@ -165,14 +167,15 @@ def ensure_extension_upper_and_dotless(cls, v): class ServiceAndFileParams(FileQueryParams, ServiceParams): - class Config: + model_config = ConfigDict( # Optional configuration to exclude duplicates from schema - schema_extra = { + json_schema_extra={ "allOf": [ {"$ref": "#/definitions/FileParams"}, {"$ref": "#/definitions/ServiceParams"}, ] } + ) class ViewerQueryParams(BaseModel): @@ -189,7 +192,7 @@ def from_viewer(viewer: ViewerInfo) -> "ViewerQueryParams": viewer_version=viewer.version, ) - @validator("file_type") + @field_validator("file_type") @classmethod def ensure_extension_upper_and_dotless(cls, v): # NOTE: see filetype constraint-check diff --git a/services/web/server/src/simcore_service_webserver/studies_dispatcher/_rest_handlers.py b/services/web/server/src/simcore_service_webserver/studies_dispatcher/_rest_handlers.py index 9f66cd460b0..da5f1d8809f 100644 --- a/services/web/server/src/simcore_service_webserver/studies_dispatcher/_rest_handlers.py +++ b/services/web/server/src/simcore_service_webserver/studies_dispatcher/_rest_handlers.py @@ -3,13 +3,20 @@ """ import logging from dataclasses import asdict -from typing import Any, ClassVar from aiohttp import web from aiohttp.web import Request +from common_library.pydantic_networks_extension import HttpUrlLegacy from models_library.services import ServiceKey from models_library.services_types import ServiceVersion -from pydantic import BaseModel, Field, ValidationError, parse_obj_as, validator +from pydantic import ( + BaseModel, + ConfigDict, + Field, + TypeAdapter, + ValidationError, + field_validator, +) from pydantic.networks import HttpUrl from .._meta import API_VTAG @@ -32,11 +39,13 @@ def _compose_file_and_service_dispatcher_prefix_url( request: web.Request, viewer: ViewerInfo ) -> HttpUrl: """This is denoted PREFIX URL because it needs to append extra query parameters""" - params = ViewerQueryParams.from_viewer(viewer).dict() + params = ViewerQueryParams.from_viewer(viewer).model_dump() absolute_url = request.url.join( request.app.router["get_redirection_to_viewer"].url_for().with_query(**params) ) - absolute_url_: HttpUrl = parse_obj_as(HttpUrl, f"{absolute_url}") + absolute_url_: HttpUrl = TypeAdapter(HttpUrlLegacy).validate_python( + f"{absolute_url}" + ) return absolute_url_ @@ -44,13 +53,15 @@ def _compose_service_only_dispatcher_prefix_url( request: web.Request, service_key: str, service_version: str ) -> HttpUrl: params = ViewerQueryParams( - viewer_key=ServiceKey(service_key), - viewer_version=ServiceVersion(service_version), - ).dict(exclude_none=True, exclude_unset=True) + viewer_key=TypeAdapter(ServiceKey).validate_python(service_key), + viewer_version=TypeAdapter(ServiceVersion).validate_python(service_version), + ).model_dump(exclude_none=True, exclude_unset=True) absolute_url = request.url.join( request.app.router["get_redirection_to_viewer"].url_for().with_query(**params) ) - absolute_url_: HttpUrl = parse_obj_as(HttpUrl, f"{absolute_url}") + absolute_url_: HttpUrl = TypeAdapter(HttpUrlLegacy).validate_python( + f"{absolute_url}" + ) return absolute_url_ @@ -125,15 +136,15 @@ def create(cls, meta: ServiceMetaData, request: web.Request): **asdict(meta), ) - @validator("file_extensions") + @field_validator("file_extensions") @classmethod def remove_dot_prefix_from_extension(cls, v): if v: return [ext.removeprefix(".") for ext in v] return v - class Config: - schema_extra: ClassVar[dict[str, Any]] = { + model_config = ConfigDict( + json_schema_extra={ "example": { "key": "simcore/services/dynamic/sim4life", "title": "Sim4Life Mattermost", @@ -143,6 +154,7 @@ class Config: "view_url": "https://host.com/view?viewer_key=simcore/services/dynamic/raw-graphs&viewer_version=1.2.3", } } + ) # @@ -177,7 +189,7 @@ async def list_viewers(request: Request): file_type: str | None = request.query.get("file_type", None) viewers = [ - Viewer.create(request, viewer).dict() + Viewer.create(request, viewer).model_dump() for viewer in await list_viewers_info(request.app, file_type=file_type) ] return envelope_json_response(viewers) @@ -189,7 +201,7 @@ async def list_default_viewers(request: Request): file_type: str | None = request.query.get("file_type", None) viewers = [ - Viewer.create(request, viewer).dict() + Viewer.create(request, viewer).model_dump() for viewer in await list_viewers_info( request.app, file_type=file_type, only_default=True ) diff --git a/services/web/server/src/simcore_service_webserver/studies_dispatcher/_users.py b/services/web/server/src/simcore_service_webserver/studies_dispatcher/_users.py index c9ff40adbd9..0f7a382fd65 100644 --- a/services/web/server/src/simcore_service_webserver/studies_dispatcher/_users.py +++ b/services/web/server/src/simcore_service_webserver/studies_dispatcher/_users.py @@ -12,12 +12,12 @@ import secrets import string from contextlib import suppress -from datetime import datetime +from datetime import datetime, timezone import redis.asyncio as aioredis from aiohttp import web from models_library.emails import LowerCaseEmailStr -from pydantic import BaseModel, parse_obj_as +from pydantic import BaseModel, TypeAdapter from redis.exceptions import LockNotOwnedError from servicelib.aiohttp.application_keys import APP_FIRE_AND_FORGET_TASKS_KEY from servicelib.logging_utils import log_decorator @@ -80,9 +80,11 @@ async def create_temporary_guest_user(request: web.Request): random_user_name = "".join( secrets.choice(string.ascii_lowercase) for _ in range(10) ) - email = parse_obj_as(LowerCaseEmailStr, f"{random_user_name}@guest-at-osparc.io") + email = TypeAdapter(LowerCaseEmailStr).validate_python( + f"{random_user_name}@guest-at-osparc.io" + ) password = generate_password(length=12) - expires_at = datetime.utcnow() + settings.STUDIES_GUEST_ACCOUNT_LIFETIME + expires_at = datetime.now(timezone.utc) + settings.STUDIES_GUEST_ACCOUNT_LIFETIME # GUEST_USER_RC_LOCK: # diff --git a/services/web/server/src/simcore_service_webserver/studies_dispatcher/settings.py b/services/web/server/src/simcore_service_webserver/studies_dispatcher/settings.py index 3ef317631ed..2d7fbb5dfb7 100644 --- a/services/web/server/src/simcore_service_webserver/studies_dispatcher/settings.py +++ b/services/web/server/src/simcore_service_webserver/studies_dispatcher/settings.py @@ -1,9 +1,10 @@ from datetime import timedelta -from typing import Any, ClassVar from aiohttp import web -from pydantic import ByteSize, HttpUrl, parse_obj_as, validator +from common_library.pydantic_networks_extension import HttpUrlLegacy +from pydantic import ByteSize, TypeAdapter, field_validator from pydantic.fields import Field +from pydantic_settings import SettingsConfigDict from servicelib.aiohttp.application_keys import APP_SETTINGS_KEY from settings_library.base import BaseCustomSettings @@ -20,23 +21,27 @@ class StudiesDispatcherSettings(BaseCustomSettings): " and removed by the GC", ) - STUDIES_DEFAULT_SERVICE_THUMBNAIL: HttpUrl = Field( - default=parse_obj_as(HttpUrl, "https://via.placeholder.com/170x120.png"), + STUDIES_DEFAULT_SERVICE_THUMBNAIL: HttpUrlLegacy = Field( + default=TypeAdapter(HttpUrlLegacy).validate_python( + "https://via.placeholder.com/170x120.png" + ), description="Default thumbnail for services or dispatch project with a service", ) - STUDIES_DEFAULT_FILE_THUMBNAIL: HttpUrl = Field( - default=parse_obj_as(HttpUrl, "https://via.placeholder.com/170x120.png"), + STUDIES_DEFAULT_FILE_THUMBNAIL: HttpUrlLegacy = Field( + default=TypeAdapter(HttpUrlLegacy).validate_python( + "https://via.placeholder.com/170x120.png" + ), description="Default thumbnail for dispatch projects with only data (i.e. file-picker)", ) STUDIES_MAX_FILE_SIZE_ALLOWED: ByteSize = Field( - default=parse_obj_as(ByteSize, "50Mib"), + default=TypeAdapter(ByteSize).validate_python("50Mib"), description="Limits the size of the files that can be dispatched" "Note that the accuracy of the file size is not guaranteed and this limit might be surpassed", ) - @validator("STUDIES_GUEST_ACCOUNT_LIFETIME") + @field_validator("STUDIES_GUEST_ACCOUNT_LIFETIME") @classmethod def _is_positive_lifetime(cls, v): if v and isinstance(v, timedelta) and v.total_seconds() <= 0: @@ -50,13 +55,14 @@ def is_login_required(self): """ return not self.STUDIES_ACCESS_ANONYMOUS_ALLOWED - class Config: - schema_extra: ClassVar[dict[str, Any]] = { + model_config = SettingsConfigDict( + json_schema_extra={ "example": { "STUDIES_GUEST_ACCOUNT_LIFETIME": "2 1:10:00", # 2 days 1h and 10 mins "STUDIES_ACCESS_ANONYMOUS_ALLOWED": "1", }, } + ) def get_plugin_settings(app: web.Application) -> StudiesDispatcherSettings: diff --git a/services/web/server/src/simcore_service_webserver/tags/_handlers.py b/services/web/server/src/simcore_service_webserver/tags/_handlers.py index 5492d5e468e..5ce39ea0b08 100644 --- a/services/web/server/src/simcore_service_webserver/tags/_handlers.py +++ b/services/web/server/src/simcore_service_webserver/tags/_handlers.py @@ -1,7 +1,9 @@ import functools from aiohttp import web -from pydantic import parse_obj_as +from aiopg.sa.engine import Engine +from pydantic import TypeAdapter +from servicelib.aiohttp.application_keys import APP_DB_ENGINE_KEY from servicelib.aiohttp.requests_validation import ( parse_request_body_as, parse_request_path_parameters_as, @@ -57,7 +59,7 @@ async def wrapper(request: web.Request) -> web.StreamResponse: @_handle_tags_exceptions async def create_tag(request: web.Request): assert request.app # nosec - req_ctx = TagRequestContext.parse_obj(request) + req_ctx = TagRequestContext.model_validate(request) new_tag = await parse_request_body_as(TagCreate, request) created = await _api.create_tag( @@ -71,8 +73,7 @@ async def create_tag(request: web.Request): @permission_required("tag.crud.*") @_handle_tags_exceptions async def list_tags(request: web.Request): - - req_ctx = TagRequestContext.parse_obj(request) + req_ctx = TagRequestContext.model_validate(request) got = await _api.list_tags(request.app, user_id=req_ctx.user_id) return envelope_json_response(got) @@ -82,7 +83,7 @@ async def list_tags(request: web.Request): @permission_required("tag.crud.*") @_handle_tags_exceptions async def update_tag(request: web.Request): - req_ctx = TagRequestContext.parse_obj(request) + req_ctx = TagRequestContext.model_validate(request) path_params = parse_request_path_parameters_as(TagPathParams, request) tag_updates = await parse_request_body_as(TagUpdate, request) @@ -100,7 +101,7 @@ async def update_tag(request: web.Request): @permission_required("tag.crud.*") @_handle_tags_exceptions async def delete_tag(request: web.Request): - req_ctx = TagRequestContext.parse_obj(request) + req_ctx = TagRequestContext.model_validate(request) path_params = parse_request_path_parameters_as(TagPathParams, request) await _api.delete_tag( @@ -123,7 +124,7 @@ async def list_tag_groups(request: web.Request): path_params = parse_request_path_parameters_as(TagPathParams, request) assert path_params # nosec - assert envelope_json_response(parse_obj_as(list[TagGroupGet], [])) + assert envelope_json_response(TypeAdapter(list[TagGroupGet]).validate_python([])) raise NotImplementedError diff --git a/services/web/server/src/simcore_service_webserver/tags/schemas.py b/services/web/server/src/simcore_service_webserver/tags/schemas.py index 84ce7bc8c03..6e89749828e 100644 --- a/services/web/server/src/simcore_service_webserver/tags/schemas.py +++ b/services/web/server/src/simcore_service_webserver/tags/schemas.py @@ -1,10 +1,11 @@ import re from datetime import datetime +from typing import Annotated, TypeAlias from common_library.pydantic_basic_types import ConstrainedStr from models_library.api_schemas_webserver._base import InputSchema, OutputSchema from models_library.users import GroupID, UserID -from pydantic import Field, PositiveInt +from pydantic import Field, PositiveInt, StringConstraints from servicelib.aiohttp.requests_validation import RequestParams, StrictRequestParams from servicelib.request_keys import RQT_USERID_KEY from simcore_postgres_database.utils_tags import TagDict @@ -18,8 +19,9 @@ class TagPathParams(StrictRequestParams): tag_id: PositiveInt -class ColorStr(ConstrainedStr): - regex = re.compile(r"^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$") +ColorStr: TypeAlias = Annotated[ + str, StringConstraints(pattern=re.compile(r"^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$")) +] class TagUpdate(InputSchema): diff --git a/services/web/server/src/simcore_service_webserver/users/_api.py b/services/web/server/src/simcore_service_webserver/users/_api.py index a054bfe5927..46ffb99e84a 100644 --- a/services/web/server/src/simcore_service_webserver/users/_api.py +++ b/services/web/server/src/simcore_service_webserver/users/_api.py @@ -6,7 +6,7 @@ from models_library.emails import LowerCaseEmailStr from models_library.payments import UserInvoiceAddress from models_library.users import UserBillingDetails, UserID -from pydantic import parse_obj_as +from pydantic import TypeAdapter from simcore_postgres_database.models.users import UserStatus from ..db.plugin import get_database_engine @@ -50,7 +50,7 @@ async def get_user_credentials( ) return UserCredentialsTuple( - email=parse_obj_as(LowerCaseEmailStr, row.email), + email=TypeAdapter(LowerCaseEmailStr).validate_python(row.email), password_hash=row.password_hash, display_name=row.first_name or row.name.capitalize(), ) diff --git a/services/web/server/src/simcore_service_webserver/users/_handlers.py b/services/web/server/src/simcore_service_webserver/users/_handlers.py index a8516095e57..fe737d8b82d 100644 --- a/services/web/server/src/simcore_service_webserver/users/_handlers.py +++ b/services/web/server/src/simcore_service_webserver/users/_handlers.py @@ -79,7 +79,7 @@ async def wrapper(request: web.Request) -> web.StreamResponse: @login_required @_handle_users_exceptions async def get_my_profile(request: web.Request) -> web.Response: - req_ctx = UsersRequestContext.parse_obj(request) + req_ctx = UsersRequestContext.model_validate(request) profile: ProfileGet = await api.get_user_profile( request.app, req_ctx.user_id, req_ctx.product_name ) @@ -91,7 +91,7 @@ async def get_my_profile(request: web.Request) -> web.Response: @permission_required("user.profile.update") @_handle_users_exceptions async def update_my_profile(request: web.Request) -> web.Response: - req_ctx = UsersRequestContext.parse_obj(request) + req_ctx = UsersRequestContext.model_validate(request) profile_update = await parse_request_body_as(ProfileUpdate, request) await api.update_user_profile( request.app, req_ctx.user_id, profile_update, as_patch=False @@ -116,7 +116,7 @@ class _SearchQueryParams(BaseModel): @permission_required("user.users.*") @_handle_users_exceptions async def search_users(request: web.Request) -> web.Response: - req_ctx = UsersRequestContext.parse_obj(request) + req_ctx = UsersRequestContext.model_validate(request) assert req_ctx.product_name # nosec query_params: _SearchQueryParams = parse_request_query_parameters_as( @@ -137,7 +137,7 @@ async def search_users(request: web.Request) -> web.Response: @permission_required("user.users.*") @_handle_users_exceptions async def pre_register_user(request: web.Request) -> web.Response: - req_ctx = UsersRequestContext.parse_obj(request) + req_ctx = UsersRequestContext.model_validate(request) pre_user_profile = await parse_request_body_as(PreUserProfile, request) try: diff --git a/services/web/server/src/simcore_service_webserver/users/_notifications.py b/services/web/server/src/simcore_service_webserver/users/_notifications.py index 256e521f89c..b02e5a44540 100644 --- a/services/web/server/src/simcore_service_webserver/users/_notifications.py +++ b/services/web/server/src/simcore_service_webserver/users/_notifications.py @@ -1,12 +1,12 @@ from datetime import datetime from enum import auto -from typing import Any, ClassVar, Final, Literal +from typing import Final, Literal from uuid import uuid4 from models_library.products import ProductName from models_library.users import UserID from models_library.utils.enums import StrAutoEnum -from pydantic import BaseModel, NonNegativeInt, validator +from pydantic import BaseModel, ConfigDict, NonNegativeInt, field_validator MAX_NOTIFICATIONS_FOR_USER_TO_SHOW: Final[NonNegativeInt] = 10 MAX_NOTIFICATIONS_FOR_USER_TO_KEEP: Final[NonNegativeInt] = 100 @@ -33,7 +33,7 @@ class BaseUserNotification(BaseModel): date: datetime product: Literal["UNDEFINED"] | ProductName = "UNDEFINED" - @validator("category", pre=True) + @field_validator("category", mode="before") @classmethod def category_to_upper(cls, value: str) -> str: return value.upper() @@ -58,10 +58,12 @@ class UserNotification(BaseUserNotification): def create_from_request_data( cls, request_data: UserNotificationCreate ) -> "UserNotification": - return cls.construct(id=f"{uuid4()}", read=False, **request_data.dict()) + return cls.model_construct( + id=f"{uuid4()}", read=False, **request_data.model_dump() + ) - class Config: - schema_extra: ClassVar[dict[str, Any]] = { + model_config = ConfigDict( + json_schema_extra={ "examples": [ { "id": "3fb96d89-ff5d-4d27-b5aa-d20d46e20eb8", @@ -120,3 +122,4 @@ class Config: }, ] } + ) diff --git a/services/web/server/src/simcore_service_webserver/users/_notifications_handlers.py b/services/web/server/src/simcore_service_webserver/users/_notifications_handlers.py index b30a435210b..41f3330af52 100644 --- a/services/web/server/src/simcore_service_webserver/users/_notifications_handlers.py +++ b/services/web/server/src/simcore_service_webserver/users/_notifications_handlers.py @@ -52,7 +52,7 @@ async def _get_user_notifications( # Filter by product included = [product_name, "UNDEFINED"] filtered_notifications = [n for n in notifications if n["product"] in included] - return [UserNotification.parse_obj(x) for x in filtered_notifications] + return [UserNotification.model_validate(x) for x in filtered_notifications] @routes.get(f"/{API_VTAG}/me/notifications", name="list_user_notifications") @@ -60,7 +60,7 @@ async def _get_user_notifications( @permission_required("user.notifications.read") async def list_user_notifications(request: web.Request) -> web.Response: redis_client = get_redis_user_notifications_client(request.app) - req_ctx = UsersRequestContext.parse_obj(request) + req_ctx = UsersRequestContext.model_validate(request) product_name = get_product_name(request) notifications = await _get_user_notifications( redis_client, req_ctx.user_id, product_name @@ -99,7 +99,7 @@ class _NotificationPathParams(BaseModel): @permission_required("user.notifications.update") async def mark_notification_as_read(request: web.Request) -> web.Response: redis_client = get_redis_user_notifications_client(request.app) - req_ctx = UsersRequestContext.parse_obj(request) + req_ctx = UsersRequestContext.model_validate(request) req_path_params = parse_request_path_parameters_as(_NotificationPathParams, request) body = await parse_request_body_as(UserNotificationPatch, request) @@ -124,7 +124,7 @@ async def mark_notification_as_read(request: web.Request) -> web.Response: @login_required @permission_required("user.permissions.read") async def list_user_permissions(request: web.Request) -> web.Response: - req_ctx = UsersRequestContext.parse_obj(request) + req_ctx = UsersRequestContext.model_validate(request) list_permissions: list[Permission] = await _api.list_user_permissions( request.app, req_ctx.user_id, req_ctx.product_name ) diff --git a/services/web/server/src/simcore_service_webserver/users/_preferences_api.py b/services/web/server/src/simcore_service_webserver/users/_preferences_api.py index 8e17a4a25d4..fb55ac58d2f 100644 --- a/services/web/server/src/simcore_service_webserver/users/_preferences_api.py +++ b/services/web/server/src/simcore_service_webserver/users/_preferences_api.py @@ -13,7 +13,7 @@ PreferenceName, ) from models_library.users import UserID -from pydantic import NonNegativeInt, parse_obj_as +from pydantic import NonNegativeInt, TypeAdapter from servicelib.utils import logged_gather from simcore_postgres_database.utils_groups_extra_properties import ( GroupExtraPropertiesRepo, @@ -96,7 +96,7 @@ def include_preference(identifier: PreferenceIdentifier) -> bool: return True aggregated_preferences: AggregatedPreferences = { - p.preference_identifier: Preference.parse_obj( + p.preference_identifier: Preference.model_validate( {"value": p.value, "default_value": p.get_default_value()} ) for p in await _get_frontend_user_preferences(app, user_id, product_name) @@ -130,6 +130,6 @@ async def set_frontend_user_preference( await _preferences_db.set_user_preference( app, user_id=user_id, - preference=parse_obj_as(preference_class, {"value": value}), # type: ignore[arg-type] # GitHK this is suspicious + preference=TypeAdapter(preference_class).validate_python({"value": value}), # type: ignore[arg-type] # GitHK this is suspicious product_name=product_name, ) diff --git a/services/web/server/src/simcore_service_webserver/users/_preferences_db.py b/services/web/server/src/simcore_service_webserver/users/_preferences_db.py index 45903403af9..e64ce5e579b 100644 --- a/services/web/server/src/simcore_service_webserver/users/_preferences_db.py +++ b/services/web/server/src/simcore_service_webserver/users/_preferences_db.py @@ -31,7 +31,7 @@ async def get_user_preference( return ( None if preference_payload is None - else preference_class.parse_obj(preference_payload) + else preference_class.model_validate(preference_payload) ) diff --git a/services/web/server/src/simcore_service_webserver/users/_preferences_handlers.py b/services/web/server/src/simcore_service_webserver/users/_preferences_handlers.py index 9f5513b904f..35f7b7118a9 100644 --- a/services/web/server/src/simcore_service_webserver/users/_preferences_handlers.py +++ b/services/web/server/src/simcore_service_webserver/users/_preferences_handlers.py @@ -55,7 +55,7 @@ async def wrapper(request: web.Request) -> web.StreamResponse: @login_required @_handle_users_exceptions async def set_frontend_preference(request: web.Request) -> web.Response: - req_ctx = _RequestContext.parse_obj(request) + req_ctx = _RequestContext.model_validate(request) req_body = await parse_request_body_as(PatchRequestBody, request) req_path_params = parse_request_path_parameters_as(PatchPathParams, request) diff --git a/services/web/server/src/simcore_service_webserver/users/_schemas.py b/services/web/server/src/simcore_service_webserver/users/_schemas.py index 1dd4f59992f..cb986ff1135 100644 --- a/services/web/server/src/simcore_service_webserver/users/_schemas.py +++ b/services/web/server/src/simcore_service_webserver/users/_schemas.py @@ -12,7 +12,7 @@ from models_library.api_schemas_webserver._base import InputSchema, OutputSchema from models_library.emails import LowerCaseEmailStr from models_library.products import ProductName -from pydantic import Field, root_validator, validator +from pydantic import ConfigDict, Field, field_validator, model_validator from simcore_postgres_database.models.users import UserStatus @@ -43,7 +43,7 @@ class UserProfile(OutputSchema): description="List of products this users is included or None if fields is unset", ) - @validator("status") + @field_validator("status") @classmethod def _consistency_check(cls, v, values): registered = values["registered"] @@ -74,11 +74,10 @@ class PreUserProfile(InputSchema): description="Keeps extra information provided in the request form. At most MAX_NUM_EXTRAS fields", ) - class Config(InputSchema.Config): - anystr_strip_whitespace = True - max_anystr_length = 200 + model_config = ConfigDict(str_strip_whitespace=True, str_max_length=200) - @root_validator(pre=True) + @model_validator(mode="before") + @classmethod @classmethod def _preprocess_aliases_and_extras(cls, values): # multiple aliases for "institution" @@ -92,8 +91,8 @@ def _preprocess_aliases_and_extras(cls, values): # collect extras extra_fields = {} field_names_and_aliases = ( - set(cls.__fields__.keys()) - | {f.alias for f in cls.__fields__.values() if f.alias} + set(cls.model_fields.keys()) + | {f.alias for f in cls.model_fields.values() if f.alias} | set(alias_by_priority) ) for key, value in values.items(): @@ -111,7 +110,7 @@ def _preprocess_aliases_and_extras(cls, values): return values - @validator("first_name", "last_name", "institution", pre=True) + @field_validator("first_name", "last_name", "institution", mode="before") @classmethod def _pre_normalize_given_names(cls, v): if v: @@ -120,7 +119,7 @@ def _pre_normalize_given_names(cls, v): return re.sub(r"\b\w+\b", lambda m: m.group(0).capitalize(), name) return v - @validator("country", pre=True) + @field_validator("country", mode="before") @classmethod def _pre_check_and_normalize_country(cls, v): if v: @@ -131,4 +130,4 @@ def _pre_check_and_normalize_country(cls, v): return v -assert set(PreUserProfile.__fields__).issubset(UserProfile.__fields__) # nosec +assert set(PreUserProfile.model_fields).issubset(UserProfile.model_fields) # nosec diff --git a/services/web/server/src/simcore_service_webserver/users/_tokens_handlers.py b/services/web/server/src/simcore_service_webserver/users/_tokens_handlers.py index ddca5a94b2f..77f0551333d 100644 --- a/services/web/server/src/simcore_service_webserver/users/_tokens_handlers.py +++ b/services/web/server/src/simcore_service_webserver/users/_tokens_handlers.py @@ -44,7 +44,7 @@ async def _wrapper(request: web.Request) -> web.StreamResponse: @_handle_tokens_errors @permission_required("user.tokens.*") async def list_tokens(request: web.Request) -> web.Response: - req_ctx = UsersRequestContext.parse_obj(request) + req_ctx = UsersRequestContext.model_validate(request) all_tokens = await _tokens.list_tokens(request.app, req_ctx.user_id) return envelope_json_response(all_tokens) @@ -54,7 +54,7 @@ async def list_tokens(request: web.Request) -> web.Response: @_handle_tokens_errors @permission_required("user.tokens.*") async def create_token(request: web.Request) -> web.Response: - req_ctx = UsersRequestContext.parse_obj(request) + req_ctx = UsersRequestContext.model_validate(request) token_create = await parse_request_body_as(TokenCreate, request) await _tokens.create_token(request.app, req_ctx.user_id, token_create) return envelope_json_response(token_create, web.HTTPCreated) @@ -69,7 +69,7 @@ class _TokenPathParams(BaseModel): @_handle_tokens_errors @permission_required("user.tokens.*") async def get_token(request: web.Request) -> web.Response: - req_ctx = UsersRequestContext.parse_obj(request) + req_ctx = UsersRequestContext.model_validate(request) req_path_params = parse_request_path_parameters_as(_TokenPathParams, request) token = await _tokens.get_token( request.app, req_ctx.user_id, req_path_params.service @@ -82,7 +82,7 @@ async def get_token(request: web.Request) -> web.Response: @_handle_tokens_errors @permission_required("user.tokens.*") async def delete_token(request: web.Request) -> web.Response: - req_ctx = UsersRequestContext.parse_obj(request) + req_ctx = UsersRequestContext.model_validate(request) req_path_params = parse_request_path_parameters_as(_TokenPathParams, request) await _tokens.delete_token(request.app, req_ctx.user_id, req_path_params.service) raise web.HTTPNoContent(content_type=MIMETYPE_APPLICATION_JSON) diff --git a/services/web/server/src/simcore_service_webserver/users/api.py b/services/web/server/src/simcore_service_webserver/users/api.py index 38a357bcc93..6cf847c925f 100644 --- a/services/web/server/src/simcore_service_webserver/users/api.py +++ b/services/web/server/src/simcore_service_webserver/users/api.py @@ -16,7 +16,7 @@ from common_library.pydantic_basic_types import IDStr from models_library.products import ProductName from models_library.users import GroupID, UserID -from pydantic import EmailStr, ValidationError, parse_obj_as +from pydantic import EmailStr, TypeAdapter, ValidationError from simcore_postgres_database.models.users import UserRole from simcore_postgres_database.utils_groups_extra_properties import ( GroupExtraPropertiesNotFoundError, @@ -38,7 +38,7 @@ def _parse_as_user(user_id: Any) -> UserID: try: - return parse_obj_as(UserID, user_id) + return TypeAdapter(UserID).validate_python(user_id) except ValidationError as err: raise UserNotFoundError(uid=user_id) from err diff --git a/services/web/server/src/simcore_service_webserver/users/schemas.py b/services/web/server/src/simcore_service_webserver/users/schemas.py index 53c9ee9b756..3be894ad035 100644 --- a/services/web/server/src/simcore_service_webserver/users/schemas.py +++ b/services/web/server/src/simcore_service_webserver/users/schemas.py @@ -1,5 +1,5 @@ from datetime import date -from typing import Any, ClassVar, Literal +from typing import Literal from uuid import UUID from models_library.api_schemas_webserver._base import OutputSchema @@ -7,8 +7,7 @@ from models_library.api_schemas_webserver.users_preferences import AggregatedPreferences from models_library.emails import LowerCaseEmailStr from models_library.users import FirstNameStr, LastNameStr, UserID -from models_library.utils.json_serialization import json_dumps -from pydantic import BaseModel, Field, root_validator, validator +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from simcore_postgres_database.models.users import UserRole from ..utils import gravatar_hash @@ -27,14 +26,14 @@ class ThirdPartyToken(BaseModel): ) token_key: UUID = Field(..., description="basic token key") token_secret: UUID | None = None - - class Config: - schema_extra: ClassVar[dict[str, Any]] = { + model_config = ConfigDict( + json_schema_extra={ "example": { "service": "github-api-v1", "token_key": "5f21abf5-c596-47b7-bfd1-c0e436ef1107", } } + ) class TokenCreate(ThirdPartyToken): @@ -49,14 +48,14 @@ class TokenCreate(ThirdPartyToken): class ProfileUpdate(BaseModel): first_name: FirstNameStr | None = None last_name: LastNameStr | None = None - - class Config: - schema_extra: ClassVar[dict[str, Any]] = { + model_config = ConfigDict( + json_schema_extra={ "example": { "first_name": "Pedro", "last_name": "Crespo", } } + ) class ProfileGet(BaseModel): @@ -74,13 +73,9 @@ class ProfileGet(BaseModel): ) preferences: AggregatedPreferences - class Config: - # NOTE: old models have an hybrid between snake and camel cases! - # Should be unified at some point - allow_population_by_field_name = True - json_dumps = json_dumps - - schema_extra: ClassVar[dict[str, Any]] = { + model_config = ConfigDict( + populate_by_name=True, + json_schema_extra={ "examples": [ { "id": 1, @@ -97,9 +92,10 @@ class Config: "preferences": {}, }, ] - } + }, + ) - @root_validator(pre=True) + @model_validator(mode="before") @classmethod def _auto_generate_gravatar(cls, values): gravatar_id = values.get("gravatar_id") @@ -108,7 +104,7 @@ def _auto_generate_gravatar(cls, values): values["gravatar_id"] = gravatar_hash(email) return values - @validator("role", pre=True) + @field_validator("role", mode="before") @classmethod def _to_upper_string(cls, v): if isinstance(v, str): diff --git a/services/web/server/src/simcore_service_webserver/utils.py b/services/web/server/src/simcore_service_webserver/utils.py index d9ff8b9fcda..49c682600d1 100644 --- a/services/web/server/src/simcore_service_webserver/utils.py +++ b/services/web/server/src/simcore_service_webserver/utils.py @@ -8,12 +8,13 @@ import sys import traceback import tracemalloc -from datetime import datetime +from datetime import datetime, timezone from pathlib import Path -from typing import Any, TypedDict, cast +from typing import Any, TypedDict import orjson from models_library.basic_types import SHA1Str +from pydantic import TypeAdapter from servicelib.error_codes import ErrorCodeStr _CURRENT_DIR = ( @@ -64,7 +65,7 @@ def gravatar_hash(email: str) -> str: def now() -> datetime: - return datetime.utcnow() + return datetime.now(timezone.utc) def format_datetime(snapshot: datetime) -> str: @@ -190,4 +191,6 @@ def compute_sha1_on_small_dataset(d: Any) -> SHA1Str: """ # SEE options in https://github.com/ijl/orjson#option data_bytes = orjson.dumps(d, option=orjson.OPT_NON_STR_KEYS | orjson.OPT_SORT_KEYS) - return cast(SHA1Str, hashlib.sha1(data_bytes).hexdigest()) # nosec + return TypeAdapter(SHA1Str).validate_python( + hashlib.sha1(data_bytes).hexdigest() + ) # nosec diff --git a/services/web/server/src/simcore_service_webserver/utils_aiohttp.py b/services/web/server/src/simcore_service_webserver/utils_aiohttp.py index ae35a58ee6f..816122926f1 100644 --- a/services/web/server/src/simcore_service_webserver/utils_aiohttp.py +++ b/services/web/server/src/simcore_service_webserver/utils_aiohttp.py @@ -9,7 +9,6 @@ from models_library.generics import Envelope from models_library.utils.json_serialization import json_dumps from pydantic import BaseModel, Field -from pydantic.generics import GenericModel from servicelib.common_headers import X_FORWARDED_PROTO from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON from servicelib.rest_constants import RESPONSE_MODEL_POLICY @@ -116,7 +115,7 @@ def create_redirect_to_page_response( PageParameters = TypeVar("PageParameters", bound=BaseModel) -class NextPage(GenericModel, Generic[PageParameters]): +class NextPage(BaseModel, Generic[PageParameters]): """ This is the body of a 2XX response to pass the front-end what kind of page shall be display next and some information about it diff --git a/services/web/server/src/simcore_service_webserver/version_control/_core.py b/services/web/server/src/simcore_service_webserver/version_control/_core.py index 53f10829b48..c8d444339ea 100644 --- a/services/web/server/src/simcore_service_webserver/version_control/_core.py +++ b/services/web/server/src/simcore_service_webserver/version_control/_core.py @@ -136,7 +136,7 @@ async def get_workbench( # prefer actual project to snapshot content = await vc_repo.get_workbench_view(repo_id, commit_id) - return WorkbenchView.parse_obj(content) + return WorkbenchView.model_validate(content) # diff --git a/services/web/server/src/simcore_service_webserver/version_control/_handlers.py b/services/web/server/src/simcore_service_webserver/version_control/_handlers.py index 0cf849effb0..315968009f3 100644 --- a/services/web/server/src/simcore_service_webserver/version_control/_handlers.py +++ b/services/web/server/src/simcore_service_webserver/version_control/_handlers.py @@ -4,7 +4,7 @@ from models_library.projects import ProjectID from models_library.rest_pagination import Page, PageQueryParameters from models_library.rest_pagination_utils import paginate_data -from pydantic import BaseModel, validator +from pydantic import BaseModel, field_validator from servicelib.aiohttp.requests_validation import ( parse_request_body_as, parse_request_path_parameters_as, @@ -46,7 +46,7 @@ class _CheckpointsPathParam(BaseModel): project_uuid: ProjectID ref_id: RefID - @validator("ref_id", pre=True) + @field_validator("ref_id", mode="before") @classmethod def _normalize_refid(cls, v): if v and v == "HEAD": @@ -81,7 +81,7 @@ async def _list_repos_handler(request: web.Request): # parse and validate repos_list = [ - RepoApiModel.parse_obj( + RepoApiModel.model_validate( { "url": url_for("list_repos"), **dict(row.items()), @@ -90,7 +90,7 @@ async def _list_repos_handler(request: web.Request): for row in repos_rows ] - page = Page[RepoApiModel].parse_obj( + page = Page[RepoApiModel].model_validate( paginate_data( chunk=repos_list, request_url=request.url, @@ -116,7 +116,7 @@ async def _create_checkpoint_handler(request: web.Request): vc_repo = VersionControlRepository.create_from_request(request) path_params = parse_request_path_parameters_as(_ProjectPathParam, request) - _body = CheckpointNew.parse_obj(await request.json()) + _body = CheckpointNew.model_validate(await request.json()) checkpoint: Checkpoint = await create_checkpoint( vc_repo, @@ -124,7 +124,7 @@ async def _create_checkpoint_handler(request: web.Request): **_body.dict(include={"tag", "message"}), ) - data = CheckpointApiModel.parse_obj( + data = CheckpointApiModel.model_validate( { "url": url_for( "get_checkpoint", @@ -163,7 +163,7 @@ async def _list_checkpoints_handler(request: web.Request): # parse and validate checkpoints_list = [ - CheckpointApiModel.parse_obj( + CheckpointApiModel.model_validate( { "url": url_for( "get_checkpoint", @@ -176,7 +176,7 @@ async def _list_checkpoints_handler(request: web.Request): for checkpoint in checkpoints ] - page = Page[CheckpointApiModel].parse_obj( + page = Page[CheckpointApiModel].model_validate( paginate_data( chunk=checkpoints_list, request_url=request.url, @@ -211,7 +211,7 @@ async def _get_checkpoint_handler(request: web.Request): ref_id=path_params.ref_id, ) - data = CheckpointApiModel.parse_obj( + data = CheckpointApiModel.model_validate( { "url": url_for( "get_checkpoint", @@ -245,7 +245,7 @@ async def _update_checkpoint_annotations_handler(request: web.Request): **update.dict(include={"tag", "message"}, exclude_none=True), ) - data = CheckpointApiModel.parse_obj( + data = CheckpointApiModel.model_validate( { "url": url_for( "get_checkpoint", @@ -277,7 +277,7 @@ async def _checkout_handler(request: web.Request): ref_id=path_params.ref_id, ) - data = CheckpointApiModel.parse_obj( + data = CheckpointApiModel.model_validate( { "url": url_for( "get_checkpoint", @@ -315,7 +315,7 @@ async def _view_project_workbench_handler(request: web.Request): ref_id=checkpoint.id, ) - data = WorkbenchViewApiModel.parse_obj( + data = WorkbenchViewApiModel.model_validate( { # = request.url?? "url": url_for( diff --git a/services/web/server/src/simcore_service_webserver/version_control/models.py b/services/web/server/src/simcore_service_webserver/version_control/models.py index a562459547e..8d57de6d561 100644 --- a/services/web/server/src/simcore_service_webserver/version_control/models.py +++ b/services/web/server/src/simcore_service_webserver/version_control/models.py @@ -5,7 +5,15 @@ from models_library.basic_types import SHA1Str from models_library.projects import ProjectID from models_library.projects_nodes import Node -from pydantic import BaseModel, Field, PositiveInt, StrictBool, StrictFloat, StrictInt +from pydantic import ( + BaseModel, + ConfigDict, + Field, + PositiveInt, + StrictBool, + StrictFloat, + StrictInt, +) from pydantic.networks import HttpUrl BuiltinTypes: TypeAlias = Union[StrictBool, StrictInt, StrictFloat, str] @@ -52,8 +60,9 @@ def from_commit_log(cls, commit: RowProxy, tags: list[RowProxy]) -> "Checkpoint" class WorkbenchView(BaseModel): """A view (i.e. read-only and visual) of the project's workbench""" - class Config: - orm_mode = True + model_config = ConfigDict( + from_attributes=True, + ) # NOTE: Tmp replacing UUIDS by str due to a problem serializing to json UUID keys # in the response https://github.com/samuelcolvin/pydantic/issues/2096#issuecomment-814860206 diff --git a/services/web/server/src/simcore_service_webserver/wallets/_api.py b/services/web/server/src/simcore_service_webserver/wallets/_api.py index 37fab1e6f22..398c5b78a3d 100644 --- a/services/web/server/src/simcore_service_webserver/wallets/_api.py +++ b/services/web/server/src/simcore_service_webserver/wallets/_api.py @@ -13,7 +13,7 @@ from models_library.products import ProductName from models_library.users import UserID from models_library.wallets import UserWalletDB, WalletDB, WalletID, WalletStatus -from pydantic import parse_obj_as +from pydantic import TypeAdapter from ..resource_usage.api import get_wallet_total_available_credits from ..users import api as users_api @@ -42,7 +42,7 @@ async def create_wallet( thumbnail=thumbnail, product_name=product_name, ) - wallet_api: WalletGet = parse_obj_as(WalletGet, wallet_db) + wallet_api: WalletGet = TypeAdapter(WalletGet).validate_python(wallet_db) return wallet_api @@ -122,7 +122,9 @@ async def get_user_default_wallet_with_available_credits( ) if user_default_wallet_preference is None: raise UserDefaultWalletNotFoundError(uid=user_id) - default_wallet_id = parse_obj_as(WalletID, user_default_wallet_preference.value) + default_wallet_id = TypeAdapter(WalletID).validate_python( + user_default_wallet_preference.value + ) return await get_wallet_with_available_credits_by_user_and_wallet( app, user_id=user_id, wallet_id=default_wallet_id, product_name=product_name ) @@ -136,7 +138,7 @@ async def list_wallets_for_user( user_wallets: list[UserWalletDB] = await db.list_wallets_for_user( app=app, user_id=user_id, product_name=product_name ) - return parse_obj_as(list[WalletGet], user_wallets) + return TypeAdapter(list[WalletGet]).validate_python(user_wallets) async def any_wallet_owned_by_user( @@ -193,7 +195,7 @@ async def update_wallet( product_name=product_name, ) - wallet_api: WalletGet = parse_obj_as(WalletGet, wallet_db) + wallet_api: WalletGet = TypeAdapter(WalletGet).validate_python(wallet_db) return wallet_api @@ -263,5 +265,7 @@ async def get_wallet_with_permissions_by_user( app=app, user_id=user_id, wallet_id=wallet_id, product_name=product_name ) - permissions: WalletGetPermissions = parse_obj_as(WalletGetPermissions, wallet) + permissions: WalletGetPermissions = TypeAdapter( + WalletGetPermissions + ).validate_python(wallet) return permissions diff --git a/services/web/server/src/simcore_service_webserver/wallets/_db.py b/services/web/server/src/simcore_service_webserver/wallets/_db.py index 467bc69e437..f42324b1618 100644 --- a/services/web/server/src/simcore_service_webserver/wallets/_db.py +++ b/services/web/server/src/simcore_service_webserver/wallets/_db.py @@ -9,7 +9,7 @@ from models_library.products import ProductName from models_library.users import GroupID, UserID from models_library.wallets import UserWalletDB, WalletDB, WalletID, WalletStatus -from pydantic import parse_obj_as +from pydantic import TypeAdapter from simcore_postgres_database.models.groups import user_to_groups from simcore_postgres_database.models.wallet_to_groups import wallet_to_groups from simcore_postgres_database.models.wallets import wallets @@ -47,7 +47,7 @@ async def create_wallet( .returning(literal_column("*")) ) row = await result.first() - return parse_obj_as(WalletDB, row) + return TypeAdapter(WalletDB).validate_python(row) _SELECTION_ARGS = ( @@ -98,7 +98,9 @@ async def list_wallets_for_user( async with get_database_engine(app).acquire() as conn: result = await conn.execute(stmt) rows = await result.fetchall() or [] - output: list[UserWalletDB] = [parse_obj_as(UserWalletDB, row) for row in rows] + output: list[UserWalletDB] = [ + TypeAdapter(UserWalletDB).validate_python(row) for row in rows + ] return output @@ -160,7 +162,7 @@ async def get_wallet_for_user( wallet_id=wallet_id, product_name=product_name, ) - return parse_obj_as(UserWalletDB, row) + return TypeAdapter(UserWalletDB).validate_python(row) async def get_wallet( @@ -188,7 +190,7 @@ async def get_wallet( row = await result.first() if row is None: raise WalletNotFoundError(reason=f"Wallet {wallet_id} not found.") - return parse_obj_as(WalletDB, row) + return TypeAdapter(WalletDB).validate_python(row) async def update_wallet( @@ -219,7 +221,7 @@ async def update_wallet( row = await result.first() if row is None: raise WalletNotFoundError(reason=f"Wallet {wallet_id} not found.") - return parse_obj_as(WalletDB, row) + return TypeAdapter(WalletDB).validate_python(row) async def delete_wallet( diff --git a/services/web/server/src/simcore_service_webserver/wallets/_groups_api.py b/services/web/server/src/simcore_service_webserver/wallets/_groups_api.py index 98dcd40058b..aa563edf12e 100644 --- a/services/web/server/src/simcore_service_webserver/wallets/_groups_api.py +++ b/services/web/server/src/simcore_service_webserver/wallets/_groups_api.py @@ -5,7 +5,7 @@ from models_library.products import ProductName from models_library.users import GroupID, UserID from models_library.wallets import UserWalletDB, WalletID -from pydantic import BaseModel, parse_obj_as +from pydantic import BaseModel, TypeAdapter from ..users import api as users_api from . import _db as wallets_db @@ -89,7 +89,7 @@ async def list_wallet_groups_by_user_and_wallet( ] = await wallets_groups_db.list_wallet_groups(app=app, wallet_id=wallet_id) wallet_groups_api: list[WalletGroupGet] = [ - parse_obj_as(WalletGroupGet, group) for group in wallet_groups_db + TypeAdapter(WalletGroupGet).validate_python(group) for group in wallet_groups_db ] return wallet_groups_api @@ -105,7 +105,7 @@ async def list_wallet_groups_with_read_access_by_wallet( ] = await wallets_groups_db.list_wallet_groups(app=app, wallet_id=wallet_id) wallet_groups_api: list[WalletGroupGet] = [ - parse_obj_as(WalletGroupGet, group) + TypeAdapter(WalletGroupGet).validate_python(group) for group in wallet_groups_db if group.read is True ] diff --git a/services/web/server/src/simcore_service_webserver/wallets/_groups_db.py b/services/web/server/src/simcore_service_webserver/wallets/_groups_db.py index f9d42cc6ddd..8e416325b3b 100644 --- a/services/web/server/src/simcore_service_webserver/wallets/_groups_db.py +++ b/services/web/server/src/simcore_service_webserver/wallets/_groups_db.py @@ -9,7 +9,7 @@ from aiohttp import web from models_library.users import GroupID from models_library.wallets import WalletID -from pydantic import BaseModel, parse_obj_as +from pydantic import BaseModel, TypeAdapter from simcore_postgres_database.models.wallet_to_groups import wallet_to_groups from sqlalchemy import func, literal_column from sqlalchemy.sql import select @@ -58,7 +58,7 @@ async def create_wallet_group( .returning(literal_column("*")) ) row = await result.first() - return parse_obj_as(WalletGroupGetDB, row) + return TypeAdapter(WalletGroupGetDB).validate_python(row) async def list_wallet_groups( @@ -81,7 +81,7 @@ async def list_wallet_groups( async with get_database_engine(app).acquire() as conn: result = await conn.execute(stmt) rows = await result.fetchall() or [] - return parse_obj_as(list[WalletGroupGetDB], rows) + return TypeAdapter(list[WalletGroupGetDB]).validate_python(rows) async def get_wallet_group( @@ -112,7 +112,7 @@ async def get_wallet_group( raise WalletGroupNotFoundError( reason=f"Wallet {wallet_id} group {group_id} not found" ) - return parse_obj_as(WalletGroupGetDB, row) + return TypeAdapter(WalletGroupGetDB).validate_python(row) async def update_wallet_group( @@ -143,7 +143,7 @@ async def update_wallet_group( raise WalletGroupNotFoundError( reason=f"Wallet {wallet_id} group {group_id} not found" ) - return parse_obj_as(WalletGroupGetDB, row) + return TypeAdapter(WalletGroupGetDB).validate_python(row) async def delete_wallet_group( diff --git a/services/web/server/src/simcore_service_webserver/wallets/_groups_handlers.py b/services/web/server/src/simcore_service_webserver/wallets/_groups_handlers.py index 6690d6d41e4..6b911a234e2 100644 --- a/services/web/server/src/simcore_service_webserver/wallets/_groups_handlers.py +++ b/services/web/server/src/simcore_service_webserver/wallets/_groups_handlers.py @@ -8,7 +8,7 @@ from aiohttp import web from models_library.users import GroupID, UserID from models_library.wallets import WalletID -from pydantic import BaseModel, Extra, Field +from pydantic import BaseModel, ConfigDict, Field from servicelib.aiohttp.requests_validation import ( parse_request_body_as, parse_request_path_parameters_as, @@ -60,18 +60,18 @@ async def wrapper(request: web.Request) -> web.StreamResponse: class _WalletsGroupsPathParams(BaseModel): wallet_id: WalletID group_id: GroupID - - class Config: - extra = Extra.forbid + model_config = ConfigDict( + extra="forbid", + ) class _WalletsGroupsBodyParams(BaseModel): read: bool write: bool delete: bool - - class Config: - extra = Extra.forbid + model_config = ConfigDict( + extra="forbid", + ) @routes.post( @@ -81,7 +81,7 @@ class Config: @permission_required("wallets.*") @_handle_wallets_groups_exceptions async def create_wallet_group(request: web.Request): - req_ctx = _RequestContext.parse_obj(request) + req_ctx = _RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_WalletsGroupsPathParams, request) body_params = await parse_request_body_as(_WalletsGroupsBodyParams, request) @@ -104,7 +104,7 @@ async def create_wallet_group(request: web.Request): @permission_required("wallets.*") @_handle_wallets_groups_exceptions async def list_wallet_groups(request: web.Request): - req_ctx = _RequestContext.parse_obj(request) + req_ctx = _RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(WalletsPathParams, request) wallets: list[ @@ -127,7 +127,7 @@ async def list_wallet_groups(request: web.Request): @permission_required("wallets.*") @_handle_wallets_groups_exceptions async def update_wallet_group(request: web.Request): - req_ctx = _RequestContext.parse_obj(request) + req_ctx = _RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_WalletsGroupsPathParams, request) body_params = await parse_request_body_as(_WalletsGroupsBodyParams, request) @@ -151,7 +151,7 @@ async def update_wallet_group(request: web.Request): @permission_required("wallets.*") @_handle_wallets_groups_exceptions async def delete_wallet_group(request: web.Request): - req_ctx = _RequestContext.parse_obj(request) + req_ctx = _RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_WalletsGroupsPathParams, request) await _groups_api.delete_wallet_group( diff --git a/services/web/server/src/simcore_service_webserver/wallets/_handlers.py b/services/web/server/src/simcore_service_webserver/wallets/_handlers.py index e7c67919f10..3da7821fc90 100644 --- a/services/web/server/src/simcore_service_webserver/wallets/_handlers.py +++ b/services/web/server/src/simcore_service_webserver/wallets/_handlers.py @@ -127,7 +127,7 @@ class WalletsPathParams(StrictRequestParams): @permission_required("wallets.*") @handle_wallets_exceptions async def create_wallet(request: web.Request): - req_ctx = WalletsRequestContext.parse_obj(request) + req_ctx = WalletsRequestContext.model_validate(request) body_params = await parse_request_body_as(CreateWalletBodyParams, request) wallet: WalletGet = await _api.create_wallet( @@ -147,7 +147,7 @@ async def create_wallet(request: web.Request): @permission_required("wallets.*") @handle_wallets_exceptions async def list_wallets(request: web.Request): - req_ctx = WalletsRequestContext.parse_obj(request) + req_ctx = WalletsRequestContext.model_validate(request) wallets: list[ WalletGetWithAvailableCredits @@ -163,7 +163,7 @@ async def list_wallets(request: web.Request): @permission_required("wallets.*") @handle_wallets_exceptions async def get_default_wallet(request: web.Request): - req_ctx = WalletsRequestContext.parse_obj(request) + req_ctx = WalletsRequestContext.model_validate(request) wallet: WalletGetWithAvailableCredits = ( await _api.get_user_default_wallet_with_available_credits( @@ -178,7 +178,7 @@ async def get_default_wallet(request: web.Request): @permission_required("wallets.*") @handle_wallets_exceptions async def get_wallet(request: web.Request): - req_ctx = WalletsRequestContext.parse_obj(request) + req_ctx = WalletsRequestContext.model_validate(request) path_params = parse_request_path_parameters_as(WalletsPathParams, request) wallet: WalletGetWithAvailableCredits = ( @@ -201,7 +201,7 @@ async def get_wallet(request: web.Request): @permission_required("wallets.*") @handle_wallets_exceptions async def update_wallet(request: web.Request): - req_ctx = WalletsRequestContext.parse_obj(request) + req_ctx = WalletsRequestContext.model_validate(request) path_params = parse_request_path_parameters_as(WalletsPathParams, request) body_params = await parse_request_body_as(PutWalletBodyParams, request) diff --git a/services/web/server/src/simcore_service_webserver/wallets/_payments_handlers.py b/services/web/server/src/simcore_service_webserver/wallets/_payments_handlers.py index 27060372abd..736a3411cce 100644 --- a/services/web/server/src/simcore_service_webserver/wallets/_payments_handlers.py +++ b/services/web/server/src/simcore_service_webserver/wallets/_payments_handlers.py @@ -65,7 +65,7 @@ @permission_required("wallets.*") @handle_wallets_exceptions async def _create_payment(request: web.Request): - req_ctx = WalletsRequestContext.parse_obj(request) + req_ctx = WalletsRequestContext.model_validate(request) path_params = parse_request_path_parameters_as(WalletsPathParams, request) body_params = await parse_request_body_as(CreateWalletPayment, request) @@ -113,7 +113,7 @@ async def _list_all_payments(request: web.Request): be listed here. """ - req_ctx = WalletsRequestContext.parse_obj(request) + req_ctx = WalletsRequestContext.model_validate(request) query_params: PageQueryParameters = parse_request_query_parameters_as( PageQueryParameters, request ) @@ -126,7 +126,7 @@ async def _list_all_payments(request: web.Request): offset=query_params.offset, ) - page = Page[PaymentTransaction].parse_obj( + page = Page[PaymentTransaction].model_validate( paginate_data( chunk=payments, request_url=request.url, @@ -148,7 +148,7 @@ async def _list_all_payments(request: web.Request): @handle_wallets_exceptions async def _get_payment_invoice_link(request: web.Request): """Get invoice for concrete payment""" - req_ctx = WalletsRequestContext.parse_obj(request) + req_ctx = WalletsRequestContext.model_validate(request) path_params = parse_request_path_parameters_as(PaymentsPathParams, request) payment_invoice = await get_payment_invoice_url( @@ -174,7 +174,7 @@ class PaymentsPathParams(WalletsPathParams): @permission_required("wallets.*") @handle_wallets_exceptions async def _cancel_payment(request: web.Request): - req_ctx = WalletsRequestContext.parse_obj(request) + req_ctx = WalletsRequestContext.model_validate(request) path_params = parse_request_path_parameters_as(PaymentsPathParams, request) await api.cancel_payment_to_wallet( @@ -208,7 +208,7 @@ async def _init_creation_of_payment_method(request: web.Request): """Triggers the creation of a new payment method. Note that creating a payment-method follows the init-prompt-ack flow """ - req_ctx = WalletsRequestContext.parse_obj(request) + req_ctx = WalletsRequestContext.model_validate(request) path_params = parse_request_path_parameters_as(WalletsPathParams, request) with log_context( @@ -241,7 +241,7 @@ async def _init_creation_of_payment_method(request: web.Request): @permission_required("wallets.*") @handle_wallets_exceptions async def _cancel_creation_of_payment_method(request: web.Request): - req_ctx = WalletsRequestContext.parse_obj(request) + req_ctx = WalletsRequestContext.model_validate(request) path_params = parse_request_path_parameters_as(PaymentMethodsPathParams, request) with log_context( @@ -272,7 +272,7 @@ async def _cancel_creation_of_payment_method(request: web.Request): @permission_required("wallets.*") @handle_wallets_exceptions async def _list_payments_methods(request: web.Request): - req_ctx = WalletsRequestContext.parse_obj(request) + req_ctx = WalletsRequestContext.model_validate(request) path_params = parse_request_path_parameters_as(WalletsPathParams, request) payments_methods: list[PaymentMethodGet] = await list_wallet_payment_methods( @@ -292,7 +292,7 @@ async def _list_payments_methods(request: web.Request): @permission_required("wallets.*") @handle_wallets_exceptions async def _get_payment_method(request: web.Request): - req_ctx = WalletsRequestContext.parse_obj(request) + req_ctx = WalletsRequestContext.model_validate(request) path_params = parse_request_path_parameters_as(PaymentMethodsPathParams, request) payment_method: PaymentMethodGet = await get_wallet_payment_method( @@ -313,7 +313,7 @@ async def _get_payment_method(request: web.Request): @permission_required("wallets.*") @handle_wallets_exceptions async def _delete_payment_method(request: web.Request): - req_ctx = WalletsRequestContext.parse_obj(request) + req_ctx = WalletsRequestContext.model_validate(request) path_params = parse_request_path_parameters_as(PaymentMethodsPathParams, request) await delete_wallet_payment_method( @@ -337,7 +337,7 @@ async def _delete_payment_method(request: web.Request): @permission_required("wallets.*") @handle_wallets_exceptions async def _pay_with_payment_method(request: web.Request): - req_ctx = WalletsRequestContext.parse_obj(request) + req_ctx = WalletsRequestContext.model_validate(request) path_params = parse_request_path_parameters_as(PaymentMethodsPathParams, request) body_params = await parse_request_body_as(CreateWalletPayment, request) @@ -409,7 +409,7 @@ async def _notify_payment_completed_after_response(app, user_id, payment): @permission_required("wallets.*") @handle_wallets_exceptions async def _get_wallet_autorecharge(request: web.Request): - req_ctx = WalletsRequestContext.parse_obj(request) + req_ctx = WalletsRequestContext.model_validate(request) path_params = parse_request_path_parameters_as(WalletsPathParams, request) auto_recharge = await get_wallet_payment_autorecharge( @@ -426,7 +426,7 @@ async def _get_wallet_autorecharge(request: web.Request): product_name=req_ctx.product_name, ) - return envelope_json_response(GetWalletAutoRecharge.parse_obj(auto_recharge)) + return envelope_json_response(GetWalletAutoRecharge.model_validate(auto_recharge)) @routes.put( @@ -437,7 +437,7 @@ async def _get_wallet_autorecharge(request: web.Request): @permission_required("wallets.*") @handle_wallets_exceptions async def _replace_wallet_autorecharge(request: web.Request): - req_ctx = WalletsRequestContext.parse_obj(request) + req_ctx = WalletsRequestContext.model_validate(request) path_params = parse_request_path_parameters_as(WalletsPathParams, request) body_params = await parse_request_body_as(ReplaceWalletAutoRecharge, request) @@ -454,4 +454,4 @@ async def _replace_wallet_autorecharge(request: web.Request): wallet_id=path_params.wallet_id, new=body_params, ) - return envelope_json_response(GetWalletAutoRecharge.parse_obj(udpated)) + return envelope_json_response(GetWalletAutoRecharge.model_validate(udpated)) diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_groups_api.py b/services/web/server/src/simcore_service_webserver/workspaces/_groups_api.py index 0ec1e44618e..225c459b0c3 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_groups_api.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_groups_api.py @@ -5,7 +5,7 @@ from models_library.products import ProductName from models_library.users import GroupID, UserID from models_library.workspaces import UserWorkspaceAccessRightsDB, WorkspaceID -from pydantic import BaseModel, parse_obj_as +from pydantic import BaseModel, TypeAdapter from ..users import api as users_api from . import _groups_db as workspaces_groups_db @@ -84,7 +84,8 @@ async def list_workspace_groups_by_user_and_workspace( ) workspace_groups_api: list[WorkspaceGroupGet] = [ - parse_obj_as(WorkspaceGroupGet, group) for group in workspace_groups_db + TypeAdapter(WorkspaceGroupGet).validate_python(group) + for group in workspace_groups_db ] return workspace_groups_api @@ -102,7 +103,7 @@ async def list_workspace_groups_with_read_access_by_workspace( ) workspace_groups_api: list[WorkspaceGroupGet] = [ - parse_obj_as(WorkspaceGroupGet, group) + TypeAdapter(WorkspaceGroupGet).validate_python(group) for group in workspace_groups_db if group.read is True ] diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_groups_db.py b/services/web/server/src/simcore_service_webserver/workspaces/_groups_db.py index daeba51ae80..4347ac0677f 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_groups_db.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_groups_db.py @@ -9,7 +9,7 @@ from aiohttp import web from models_library.users import GroupID from models_library.workspaces import WorkspaceID -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict from simcore_postgres_database.models.workspaces_access_rights import ( workspaces_access_rights, ) @@ -31,9 +31,9 @@ class WorkspaceGroupGetDB(BaseModel): delete: bool created: datetime modified: datetime - - class Config: - orm_mode = True + model_config = ConfigDict( + from_attributes=True, + ) ## DB API diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_groups_handlers.py b/services/web/server/src/simcore_service_webserver/workspaces/_groups_handlers.py index 0bf7d09eb68..b146735b71f 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_groups_handlers.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_groups_handlers.py @@ -8,7 +8,7 @@ from aiohttp import web from models_library.users import GroupID, UserID from models_library.workspaces import WorkspaceID -from pydantic import BaseModel, Extra, Field +from pydantic import BaseModel, ConfigDict, Field from servicelib.aiohttp.requests_validation import ( parse_request_body_as, parse_request_path_parameters_as, @@ -60,18 +60,18 @@ async def wrapper(request: web.Request) -> web.StreamResponse: class _WorkspacesGroupsPathParams(BaseModel): workspace_id: WorkspaceID group_id: GroupID - - class Config: - extra = Extra.forbid + model_config = ConfigDict( + extra="forbid", + ) class _WorkspacesGroupsBodyParams(BaseModel): read: bool write: bool delete: bool - - class Config: - extra = Extra.forbid + model_config = ConfigDict( + extra="forbid", + ) @routes.post( @@ -82,7 +82,7 @@ class Config: @permission_required("workspaces.*") @_handle_workspaces_groups_exceptions async def create_workspace_group(request: web.Request): - req_ctx = _RequestContext.parse_obj(request) + req_ctx = _RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_WorkspacesGroupsPathParams, request) body_params = await parse_request_body_as(_WorkspacesGroupsBodyParams, request) @@ -105,7 +105,7 @@ async def create_workspace_group(request: web.Request): @permission_required("workspaces.*") @_handle_workspaces_groups_exceptions async def list_workspace_groups(request: web.Request): - req_ctx = _RequestContext.parse_obj(request) + req_ctx = _RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(WorkspacesPathParams, request) workspaces: list[ @@ -128,7 +128,7 @@ async def list_workspace_groups(request: web.Request): @permission_required("workspaces.*") @_handle_workspaces_groups_exceptions async def replace_workspace_group(request: web.Request): - req_ctx = _RequestContext.parse_obj(request) + req_ctx = _RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_WorkspacesGroupsPathParams, request) body_params = await parse_request_body_as(_WorkspacesGroupsBodyParams, request) @@ -152,7 +152,7 @@ async def replace_workspace_group(request: web.Request): @permission_required("workspaces.*") @_handle_workspaces_groups_exceptions async def delete_workspace_group(request: web.Request): - req_ctx = _RequestContext.parse_obj(request) + req_ctx = _RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_WorkspacesGroupsPathParams, request) await _groups_api.delete_workspace_group( diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py index 73d49117211..e8d4022d4ed 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py @@ -14,7 +14,7 @@ from models_library.rest_pagination_utils import paginate_data from models_library.users import UserID from models_library.workspaces import WorkspaceID -from pydantic import Extra, Field, Json, parse_obj_as, validator +from pydantic import ConfigDict, Field, Json, TypeAdapter, field_validator from servicelib.aiohttp.requests_validation import ( RequestParams, StrictRequestParams, @@ -74,11 +74,11 @@ class WorkspacesListWithJsonStrQueryParams(PageQueryParameters): order_by: Json[OrderBy] = Field( default=OrderBy(field=IDStr("modified"), direction=OrderDirection.DESC), description="Order by field (modified_at|name|description) and direction (asc|desc). The default sorting order is ascending.", - example='{"field": "name", "direction": "desc"}', + examples=['{"field": "name", "direction": "desc"}'], alias="order_by", ) - @validator("order_by", check_fields=False) + @field_validator("order_by", check_fields=False) @classmethod def validate_order_by_field(cls, v): if v.field not in { @@ -92,8 +92,9 @@ def validate_order_by_field(cls, v): v.field = "modified" return v - class Config: - extra = Extra.forbid + model_config = ConfigDict( + extra="forbid", + ) @routes.post(f"/{VTAG}/workspaces", name="create_workspace") @@ -101,7 +102,7 @@ class Config: @permission_required("workspaces.*") @handle_workspaces_exceptions async def create_workspace(request: web.Request): - req_ctx = WorkspacesRequestContext.parse_obj(request) + req_ctx = WorkspacesRequestContext.model_validate(request) body_params = await parse_request_body_as(CreateWorkspaceBodyParams, request) workspace: WorkspaceGet = await _workspaces_api.create_workspace( @@ -121,7 +122,7 @@ async def create_workspace(request: web.Request): @permission_required("workspaces.*") @handle_workspaces_exceptions async def list_workspaces(request: web.Request): - req_ctx = WorkspacesRequestContext.parse_obj(request) + req_ctx = WorkspacesRequestContext.model_validate(request) query_params: WorkspacesListWithJsonStrQueryParams = ( parse_request_query_parameters_as(WorkspacesListWithJsonStrQueryParams, request) ) @@ -132,10 +133,10 @@ async def list_workspaces(request: web.Request): product_name=req_ctx.product_name, offset=query_params.offset, limit=query_params.limit, - order_by=parse_obj_as(OrderBy, query_params.order_by), + order_by=TypeAdapter(OrderBy).validate_python(query_params.order_by), ) - page = Page[WorkspaceGet].parse_obj( + page = Page[WorkspaceGet].model_validate( paginate_data( chunk=workspaces.items, request_url=request.url, @@ -145,7 +146,7 @@ async def list_workspaces(request: web.Request): ) ) return web.Response( - text=page.json(**RESPONSE_MODEL_POLICY), + text=page.model_dump_json(**RESPONSE_MODEL_POLICY), content_type=MIMETYPE_APPLICATION_JSON, ) @@ -155,7 +156,7 @@ async def list_workspaces(request: web.Request): @permission_required("workspaces.*") @handle_workspaces_exceptions async def get_workspace(request: web.Request): - req_ctx = WorkspacesRequestContext.parse_obj(request) + req_ctx = WorkspacesRequestContext.model_validate(request) path_params = parse_request_path_parameters_as(WorkspacesPathParams, request) workspace: WorkspaceGet = await _workspaces_api.get_workspace( @@ -176,7 +177,7 @@ async def get_workspace(request: web.Request): @permission_required("workspaces.*") @handle_workspaces_exceptions async def replace_workspace(request: web.Request): - req_ctx = WorkspacesRequestContext.parse_obj(request) + req_ctx = WorkspacesRequestContext.model_validate(request) path_params = parse_request_path_parameters_as(WorkspacesPathParams, request) body_params = await parse_request_body_as(PutWorkspaceBodyParams, request) @@ -200,7 +201,7 @@ async def replace_workspace(request: web.Request): @permission_required("workspaces.*") @handle_workspaces_exceptions async def delete_workspace(request: web.Request): - req_ctx = WorkspacesRequestContext.parse_obj(request) + req_ctx = WorkspacesRequestContext.model_validate(request) path_params = parse_request_path_parameters_as(WorkspacesPathParams, request) await _workspaces_api.delete_workspace( diff --git a/services/web/server/tests/conftest.py b/services/web/server/tests/conftest.py index a57dfd3c852..04b9f1a54f6 100644 --- a/services/web/server/tests/conftest.py +++ b/services/web/server/tests/conftest.py @@ -326,7 +326,7 @@ async def _creator( data, error = await assert_status(result, status.HTTP_200_OK) assert data assert not error - task_status = TaskStatus.parse_obj(data) + task_status = TaskStatus.model_validate(data) assert task_status print(f"<-- status: {task_status.json(indent=2)}") assert task_status.done, "task incomplete" diff --git a/services/web/server/tests/integration/02/scicrunch/test_scicrunch__rest.py b/services/web/server/tests/integration/02/scicrunch/test_scicrunch__rest.py index 014418a25fb..39e507b2fda 100644 --- a/services/web/server/tests/integration/02/scicrunch/test_scicrunch__rest.py +++ b/services/web/server/tests/integration/02/scicrunch/test_scicrunch__rest.py @@ -145,7 +145,7 @@ async def test_scicrunch_get_fields_from_invalid_rrid( async def test_scicrunch_service_autocomplete_by_name(settings: SciCrunchSettings): - expected: list[dict[str, Any]] = ListOfResourceHits.parse_obj( + expected: list[dict[str, Any]] = ListOfResourceHits.model_validate( [ { "rid": "SCR_000860", diff --git a/services/web/server/tests/unit/isolated/test_application_settings.py b/services/web/server/tests/unit/isolated/test_application_settings.py index 9b03e109202..1b636560201 100644 --- a/services/web/server/tests/unit/isolated/test_application_settings.py +++ b/services/web/server/tests/unit/isolated/test_application_settings.py @@ -8,7 +8,7 @@ import pytest from aiohttp import web from models_library.utils.json_serialization import json_dumps -from pydantic import HttpUrl, parse_obj_as +from pydantic import HttpUrl, TypeAdapter from pytest_simcore.helpers.monkeypatch_envs import ( setenvs_from_dict, setenvs_from_envfile, @@ -240,7 +240,7 @@ def test_settings_to_client_statics_plugins( ) assert statics["vcsReleaseTag"] - assert parse_obj_as(HttpUrl, statics["vcsReleaseUrl"]) + assert TypeAdapter(HttpUrl).validate_python(statics["vcsReleaseUrl"]) assert set(statics["pluginsDisabled"]) == (disable_plugins | {"WEBSERVER_CLUSTERS"}) diff --git a/services/web/server/tests/unit/isolated/test_catalog_api_units.py b/services/web/server/tests/unit/isolated/test_catalog_api_units.py index 479165189d2..39d1824a775 100644 --- a/services/web/server/tests/unit/isolated/test_catalog_api_units.py +++ b/services/web/server/tests/unit/isolated/test_catalog_api_units.py @@ -45,8 +45,8 @@ def test_can_connect_enums(unit_registry: UnitRegistry): } assert can_connect( - from_output=ServiceOutput.parse_obj(enum_port), - to_input=ServiceInput.parse_obj(enum_port), + from_output=ServiceOutput.model_validate(enum_port), + to_input=ServiceInput.model_validate(enum_port), units_registry=unit_registry, ) @@ -71,15 +71,15 @@ def test_can_connect_generic_data_types(unit_registry: UnitRegistry): # data:*/* -> data:text/plain assert can_connect( - from_output=ServiceOutput.parse_obj(file_picker_outfile), - to_input=ServiceInput.parse_obj(input_sleeper_input_1), + from_output=ServiceOutput.model_validate(file_picker_outfile), + to_input=ServiceInput.model_validate(input_sleeper_input_1), units_registry=unit_registry, ) # data:text/plain -> data:*/* assert can_connect( - from_output=ServiceOutput.parse_obj(input_sleeper_input_1), - to_input=ServiceInput.parse_obj(file_picker_outfile), + from_output=ServiceOutput.model_validate(input_sleeper_input_1), + to_input=ServiceInput.model_validate(file_picker_outfile), units_registry=unit_registry, ) @@ -127,15 +127,15 @@ def test_can_connect_no_units_with_units( ): # w/o -> w assert can_connect( - from_output=ServiceOutput.parse_obj(port_without_unit), - to_input=ServiceInput.parse_obj(port_with_unit), + from_output=ServiceOutput.model_validate(port_without_unit), + to_input=ServiceInput.model_validate(port_with_unit), units_registry=unit_registry, ) # w -> w/o assert can_connect( - from_output=ServiceOutput.parse_obj(port_with_unit), - to_input=ServiceInput.parse_obj(port_without_unit), + from_output=ServiceOutput.model_validate(port_with_unit), + to_input=ServiceInput.model_validate(port_without_unit), units_registry=unit_registry, ) @@ -178,8 +178,8 @@ def test_units_compatible( assert ( can_connect( - from_output=ServiceOutput.parse_obj(from_port), - to_input=ServiceInput.parse_obj(to_port), + from_output=ServiceOutput.model_validate(from_port), + to_input=ServiceInput.model_validate(to_port), units_registry=unit_registry, ) == are_compatible diff --git a/services/web/server/tests/unit/isolated/test_dynamic_scheduler.py b/services/web/server/tests/unit/isolated/test_dynamic_scheduler.py index 6308141d254..e64cee2e9db 100644 --- a/services/web/server/tests/unit/isolated/test_dynamic_scheduler.py +++ b/services/web/server/tests/unit/isolated/test_dynamic_scheduler.py @@ -47,18 +47,20 @@ def mock_rpc_client( @pytest.fixture def dynamic_service_start() -> DynamicServiceStart: - return DynamicServiceStart.parse_obj( - DynamicServiceStart.Config.schema_extra["example"] + return DynamicServiceStart.model_validate( + DynamicServiceStart.model_config["json_schema_extra"]["example"] ) @pytest.mark.parametrize( "expected_response", [ - *[NodeGet.parse_obj(x) for x in NodeGet.Config.schema_extra["examples"]], - NodeGetIdle.parse_obj(NodeGetIdle.Config.schema_extra["example"]), - DynamicServiceGet.parse_obj( - DynamicServiceGet.Config.schema_extra["examples"][0] + *[NodeGet.model_validate(NodeGet.model_config["json_schema_extra"]["example"])], + NodeGetIdle.model_validate( + NodeGetIdle.model_config["json_schema_extra"]["example"] + ), + DynamicServiceGet.model_validate( + DynamicServiceGet.model_config["json_schema_extra"]["examples"][0] ), ], ) @@ -98,9 +100,9 @@ async def test_get_service_status_raises_rpc_server_error( @pytest.mark.parametrize( "expected_response", [ - *[NodeGet.parse_obj(x) for x in NodeGet.Config.schema_extra["examples"]], - DynamicServiceGet.parse_obj( - DynamicServiceGet.Config.schema_extra["examples"][0] + *[NodeGet.model_validate(NodeGet.model_config["json_schema_extra"]["example"])], + DynamicServiceGet.model_validate( + DynamicServiceGet.model_config["json_schema_extra"]["examples"][0] ), ], ) diff --git a/services/web/server/tests/unit/isolated/test_garbage_collector_core.py b/services/web/server/tests/unit/isolated/test_garbage_collector_core.py index 0d84fbb534c..920dfb2b035 100644 --- a/services/web/server/tests/unit/isolated/test_garbage_collector_core.py +++ b/services/web/server/tests/unit/isolated/test_garbage_collector_core.py @@ -123,8 +123,8 @@ async def test_remove_orphaned_services_with_no_running_services_does_nothing( @pytest.fixture def faker_dynamic_service_get() -> Callable[[], DynamicServiceGet]: def _() -> DynamicServiceGet: - return DynamicServiceGet.parse_obj( - DynamicServiceGet.Config.schema_extra["examples"][1] + return DynamicServiceGet.model_validate( + DynamicServiceGet.model_config["json_schema_extra"]["examples"][1] ) return _ diff --git a/services/web/server/tests/unit/isolated/test_groups_models.py b/services/web/server/tests/unit/isolated/test_groups_models.py index 10b49979b1c..5ad6f8863cc 100644 --- a/services/web/server/tests/unit/isolated/test_groups_models.py +++ b/services/web/server/tests/unit/isolated/test_groups_models.py @@ -14,7 +14,7 @@ def test_models_library_and_postgress_database_enums_are_equivalent(): def test_sanitize_legacy_data(): - users_group_1 = UsersGroup.parse_obj( + users_group_1 = UsersGroup.model_validate( { "gid": "27", "label": "A user", @@ -26,7 +26,7 @@ def test_sanitize_legacy_data(): assert users_group_1.thumbnail is None - users_group_2 = UsersGroup.parse_obj( + users_group_2 = UsersGroup.model_validate( { "gid": "27", "label": "A user", diff --git a/services/web/server/tests/unit/isolated/test_products_model.py b/services/web/server/tests/unit/isolated/test_products_model.py index 45ecbd0f4c3..45b6405faf3 100644 --- a/services/web/server/tests/unit/isolated/test_products_model.py +++ b/services/web/server/tests/unit/isolated/test_products_model.py @@ -34,13 +34,17 @@ def test_product_examples( def test_product_to_static(): - product = Product.parse_obj(Product.Config.schema_extra["examples"][0]) + product = Product.model_validate( + Product.model_config["json_schema_extra"]["examples"][0] + ) assert product.to_statics() == { "displayName": "o²S²PARC", "supportEmail": "support@osparc.io", } - product = Product.parse_obj(Product.Config.schema_extra["examples"][2]) + product = Product.model_validate( + Product.model_config["json_schema_extra"]["examples"][2] + ) assert product.to_statics() == { "displayName": "o²S²PARC FOO", diff --git a/services/web/server/tests/unit/isolated/test_projects__nodes_api.py b/services/web/server/tests/unit/isolated/test_projects__nodes_api.py index ef58b4b2451..02db4969ad7 100644 --- a/services/web/server/tests/unit/isolated/test_projects__nodes_api.py +++ b/services/web/server/tests/unit/isolated/test_projects__nodes_api.py @@ -3,7 +3,7 @@ import pytest from models_library.api_schemas_storage import FileMetaDataGet -from pydantic import parse_obj_as +from pydantic import TypeAdapter from simcore_service_webserver.projects._nodes_api import ( _SUPPORTED_PREVIEW_FILE_EXTENSIONS, _FileWithThumbnail, @@ -17,8 +17,7 @@ def _c(file_name: str) -> FileMetaDataGet: """simple converter utility""" - return parse_obj_as( - FileMetaDataGet, + return TypeAdapter(FileMetaDataGet).validate_python( { "file_uuid": f"{_PROJECT_ID}/{_NODE_ID}/{file_name}", "location_id": 0, diff --git a/services/web/server/tests/unit/isolated/test_projects__nodes_resources.py b/services/web/server/tests/unit/isolated/test_projects__nodes_resources.py index 12f6bfc23b4..b1d63fef967 100644 --- a/services/web/server/tests/unit/isolated/test_projects__nodes_resources.py +++ b/services/web/server/tests/unit/isolated/test_projects__nodes_resources.py @@ -5,7 +5,7 @@ ServiceResourcesDict, ServiceResourcesDictHelpers, ) -from pydantic import parse_obj_as +from pydantic import TypeAdapter from simcore_service_webserver.projects._nodes_utils import ( validate_new_service_resources, ) @@ -17,8 +17,10 @@ @pytest.mark.parametrize( "resources", [ - parse_obj_as(ServiceResourcesDict, example) - for example in ServiceResourcesDictHelpers.Config.schema_extra["examples"] + TypeAdapter(ServiceResourcesDict).validate_python(example) + for example in ServiceResourcesDictHelpers.model_config["json_schema_extra"][ + "examples" + ] ], ) def test_check_can_update_service_resources_with_same_does_not_raise( @@ -31,8 +33,10 @@ def test_check_can_update_service_resources_with_same_does_not_raise( @pytest.mark.parametrize( "resources", [ - parse_obj_as(ServiceResourcesDict, example) - for example in ServiceResourcesDictHelpers.Config.schema_extra["examples"] + TypeAdapter(ServiceResourcesDict).validate_python(example) + for example in ServiceResourcesDictHelpers.model_config["json_schema_extra"][ + "examples" + ] ], ) def test_check_can_update_service_resources_with_invalid_container_name_raises( @@ -50,8 +54,10 @@ def test_check_can_update_service_resources_with_invalid_container_name_raises( @pytest.mark.parametrize( "resources", [ - parse_obj_as(ServiceResourcesDict, example) - for example in ServiceResourcesDictHelpers.Config.schema_extra["examples"] + TypeAdapter(ServiceResourcesDict).validate_python(example) + for example in ServiceResourcesDictHelpers.model_config["json_schema_extra"][ + "examples" + ] ], ) def test_check_can_update_service_resources_with_invalid_image_name_raises( diff --git a/services/web/server/tests/unit/isolated/test_projects_utils.py b/services/web/server/tests/unit/isolated/test_projects_utils.py index e83e02e295f..0178882d760 100644 --- a/services/web/server/tests/unit/isolated/test_projects_utils.py +++ b/services/web/server/tests/unit/isolated/test_projects_utils.py @@ -58,7 +58,7 @@ def test_clone_project_document( # # SEE https://swagger.io/docs/specification/data-models/data-types/#Null - assert Project.parse_obj(clone) is not None + assert Project.model_validate(clone) is not None @pytest.mark.parametrize( @@ -145,4 +145,4 @@ def test_validate_project_json_schema(): with open(CURRENT_DIR / "data/project-data.json") as f: project: ProjectDict = json.load(f) - Project.parse_obj(project) + Project.model_validate(project) diff --git a/services/web/server/tests/unit/isolated/test_security_api.py b/services/web/server/tests/unit/isolated/test_security_api.py index f7e73620435..1f4e04194bf 100644 --- a/services/web/server/tests/unit/isolated/test_security_api.py +++ b/services/web/server/tests/unit/isolated/test_security_api.py @@ -17,7 +17,7 @@ from aiohttp_session import get_session from models_library.emails import LowerCaseEmailStr from models_library.products import ProductName -from pydantic import parse_obj_as +from pydantic import TypeAdapter from pytest_mock import MockerFixture from pytest_simcore.helpers.typing_env import EnvVarsDict from servicelib.aiohttp import status @@ -167,7 +167,7 @@ async def _set_other_product(request: web.Request): async def _login(request: web.Request): product_name = await _get_product_name(request) body = await request.json() - email = parse_obj_as(LowerCaseEmailStr, body["email"]) + email = TypeAdapter(LowerCaseEmailStr).validate_python(body["email"]) # Permission in this product: Has user access to product? if product_name not in registered_users[email]["registered_products"]: diff --git a/services/web/server/tests/unit/isolated/test_statics_settings.py b/services/web/server/tests/unit/isolated/test_statics_settings.py index 376a8330eb9..30d9035e05d 100644 --- a/services/web/server/tests/unit/isolated/test_statics_settings.py +++ b/services/web/server/tests/unit/isolated/test_statics_settings.py @@ -4,7 +4,7 @@ import json -from pydantic import AnyHttpUrl, BaseModel, parse_obj_as +from pydantic import AnyHttpUrl, BaseModel, TypeAdapter from simcore_service_webserver.statics.settings import ( _THIRD_PARTY_REFERENCES, FrontEndAppSettings, @@ -23,7 +23,7 @@ class OsparcDependency(BaseModel): def test_valid_osparc_dependencies(): - deps = parse_obj_as(list[OsparcDependency], _THIRD_PARTY_REFERENCES) + deps = TypeAdapter(list[OsparcDependency]).validate_python(_THIRD_PARTY_REFERENCES) assert deps @@ -36,7 +36,7 @@ def test_frontend_app_settings(mock_env_devel_environment: dict[str, str]): statics = settings.to_statics() assert json.dumps(statics) - parse_obj_as(list[OsparcDependency], statics["thirdPartyReferences"]) + TypeAdapter(list[OsparcDependency]).validate_python(statics["thirdPartyReferences"]) def test_static_webserver_module_settings(mock_env_devel_environment: dict[str, str]): diff --git a/services/web/server/tests/unit/isolated/test_storage_schemas.py b/services/web/server/tests/unit/isolated/test_storage_schemas.py index c11ce1f1345..31ea4260bb4 100644 --- a/services/web/server/tests/unit/isolated/test_storage_schemas.py +++ b/services/web/server/tests/unit/isolated/test_storage_schemas.py @@ -20,4 +20,4 @@ def test_model_examples( model_cls: type[BaseModel], example_name: int, example_data: Any ): print(example_name, ":", json.dumps(example_data)) - assert model_cls.parse_obj(example_data) + assert model_cls.model_validate(example_data) diff --git a/services/web/server/tests/unit/isolated/test_studies_dispatcher_core.py b/services/web/server/tests/unit/isolated/test_studies_dispatcher_core.py index 8faada91005..6ad69d6242e 100644 --- a/services/web/server/tests/unit/isolated/test_studies_dispatcher_core.py +++ b/services/web/server/tests/unit/isolated/test_studies_dispatcher_core.py @@ -12,7 +12,7 @@ import pytest from models_library.projects import Project, ProjectID from models_library.projects_nodes_io import NodeID -from pydantic import validator +from pydantic import field_validator from pydantic.main import BaseModel from pydantic.networks import HttpUrl from pytest_simcore.helpers.webserver_fake_services_data import list_fake_file_consumers @@ -50,7 +50,7 @@ async def test_create_project_with_viewer(view: dict[str, Any]): print(json.dumps(project_in, indent=2)) # This operation is done exactly before adding to the database in projects_handlers.create_projects - Project.parse_obj(project_in) + Project.model_validate(project_in) def test_url_quoting_and_validation(): @@ -63,7 +63,7 @@ def test_url_quoting_and_validation(): class M(BaseModel): url: HttpUrl - @validator("url", pre=True) + @field_validator("url", mode="before") @classmethod def unquote_url(cls, v): w = urllib.parse.unquote(v) @@ -71,14 +71,14 @@ def unquote_url(cls, v): w = w.replace(SPACE, "%20") return w - M.parse_obj( + M.model_validate( { # encoding %20 as %2520 "url": "https://raw.githubusercontent.com/pcrespov/osparc-sample-studies/master/files%2520samples/sample.ipynb" } ) - obj2 = M.parse_obj( + obj2 = M.model_validate( { # encoding space as %20 "url": "https://raw.githubusercontent.com/pcrespov/osparc-sample-studies/master/files%20samples/sample.ipynb" @@ -86,7 +86,7 @@ def unquote_url(cls, v): ) url_with_url_in_query = "http://127.0.0.1:9081/view?file_type=IPYNB&viewer_key=simcore/services/dynamic/jupyter-octave-python-math&viewer_version=1.6.9&file_size=1&download_link=https://raw.githubusercontent.com/pcrespov/osparc-sample-studies/master/files%2520samples/sample.ipynb" - obj4 = M.parse_obj({"url": URL(url_with_url_in_query).query["download_link"]}) + obj4 = M.model_validate({"url": URL(url_with_url_in_query).query["download_link"]}) assert obj2.url.path == obj4.url.path @@ -94,7 +94,7 @@ def unquote_url(cls, v): "https://raw.githubusercontent.com/pcrespov/osparc-sample-studies/master/files%20samples/sample.ipynb" ) M(url=quoted_url) - M.parse_obj({"url": url_with_url_in_query}) + M.model_validate({"url": url_with_url_in_query}) assert ( URL(url_with_url_in_query).query["download_link"] diff --git a/services/web/server/tests/unit/isolated/test_studies_dispatcher_models.py b/services/web/server/tests/unit/isolated/test_studies_dispatcher_models.py index 0ab58dfd77e..3dbcd062d47 100644 --- a/services/web/server/tests/unit/isolated/test_studies_dispatcher_models.py +++ b/services/web/server/tests/unit/isolated/test_studies_dispatcher_models.py @@ -11,7 +11,7 @@ import pytest from aiohttp.test_utils import make_mocked_request from models_library.utils.pydantic_tools_extension import parse_obj_or_none -from pydantic import ByteSize, parse_obj_as +from pydantic import ByteSize, TypeAdapter from servicelib.aiohttp.requests_validation import parse_request_query_parameters_as from simcore_service_webserver.studies_dispatcher._models import ( FileParams, @@ -23,7 +23,7 @@ ) from yarl import URL -_SIZEBYTES = parse_obj_as(ByteSize, "3MiB") +_SIZEBYTES = TypeAdapter(ByteSize).validate_python("3MiB") # SEE https://github.com/ITISFoundation/osparc-simcore/issues/3951#issuecomment-1489992645 # AWS download links have query arg @@ -74,7 +74,7 @@ def file_and_service_params() -> dict[str, Any]: def test_download_link_validators_2(file_and_service_params: dict[str, Any]): - params = ServiceAndFileParams.parse_obj(file_and_service_params) + params = ServiceAndFileParams.model_validate(file_and_service_params) assert params.download_link diff --git a/services/web/server/tests/unit/isolated/test_studies_dispatcher_settings.py b/services/web/server/tests/unit/isolated/test_studies_dispatcher_settings.py index 91364e64beb..e2255bbcf26 100644 --- a/services/web/server/tests/unit/isolated/test_studies_dispatcher_settings.py +++ b/services/web/server/tests/unit/isolated/test_studies_dispatcher_settings.py @@ -21,7 +21,7 @@ def environment(monkeypatch: pytest.MonkeyPatch) -> EnvVarsDict: envs = setenvs_from_dict( monkeypatch, - envs=StudiesDispatcherSettings.Config.schema_extra["example"], + envs=StudiesDispatcherSettings.model_config["json_schema_extra"]["example"], ) return envs diff --git a/services/web/server/tests/unit/isolated/test_user_notifications.py b/services/web/server/tests/unit/isolated/test_user_notifications.py index d606a84297f..3caca391881 100644 --- a/services/web/server/tests/unit/isolated/test_user_notifications.py +++ b/services/web/server/tests/unit/isolated/test_user_notifications.py @@ -12,9 +12,11 @@ ) -@pytest.mark.parametrize("raw_data", UserNotification.Config.schema_extra["examples"]) +@pytest.mark.parametrize( + "raw_data", UserNotification.model_config["json_schema_extra"]["examples"] +) def test_user_notification(raw_data: dict[str, Any]): - assert UserNotification.parse_obj(raw_data) + assert UserNotification.model_validate(raw_data) @pytest.mark.parametrize("user_id", [10]) @@ -26,7 +28,7 @@ def test_get_notification_key(user_id: UserID): "request_data", [ pytest.param( - UserNotificationCreate.parse_obj( + UserNotificationCreate.model_validate( { "user_id": "1", "category": NotificationCategory.NEW_ORGANIZATION, @@ -40,7 +42,7 @@ def test_get_notification_key(user_id: UserID): id="normal_usage", ), pytest.param( - UserNotificationCreate.parse_obj( + UserNotificationCreate.model_validate( { "user_id": "1", "category": NotificationCategory.NEW_ORGANIZATION, @@ -55,7 +57,7 @@ def test_get_notification_key(user_id: UserID): id="read_is_always_set_false", ), pytest.param( - UserNotificationCreate.parse_obj( + UserNotificationCreate.model_validate( { "id": "some_id", "user_id": "1", @@ -70,7 +72,7 @@ def test_get_notification_key(user_id: UserID): id="a_new_id_is_alway_recreated", ), pytest.param( - UserNotificationCreate.parse_obj( + UserNotificationCreate.model_validate( { "id": "some_id", "user_id": "1", @@ -85,7 +87,7 @@ def test_get_notification_key(user_id: UserID): id="category_from_string", ), pytest.param( - UserNotificationCreate.parse_obj( + UserNotificationCreate.model_validate( { "id": "some_id", "user_id": "1", diff --git a/services/web/server/tests/unit/isolated/test_users_models.py b/services/web/server/tests/unit/isolated/test_users_models.py index ef5ee03c7b0..8f3b45cd3df 100644 --- a/services/web/server/tests/unit/isolated/test_users_models.py +++ b/services/web/server/tests/unit/isolated/test_users_models.py @@ -81,7 +81,7 @@ def test_auto_compute_gravatar(faker: Faker): @pytest.mark.parametrize("user_role", [u.name for u in UserRole]) def test_profile_get_role(user_role: str): - for example in ProfileGet.Config.schema_extra["examples"]: + for example in ProfileGet.model_config["json_schema_extra"]["examples"]: data = deepcopy(example) data["role"] = user_role m1 = ProfileGet(**data) @@ -134,5 +134,5 @@ def test_parsing_output_of_get_user_profile(): }, } - profile = ProfileGet.parse_obj(result_from_db_query_and_composition) + profile = ProfileGet.model_validate(result_from_db_query_and_composition) assert "password" not in profile.dict(exclude_unset=True) diff --git a/services/web/server/tests/unit/isolated/test_utils_rate_limiting.py b/services/web/server/tests/unit/isolated/test_utils_rate_limiting.py index 5e2b5a891b0..6568b1b7db4 100644 --- a/services/web/server/tests/unit/isolated/test_utils_rate_limiting.py +++ b/services/web/server/tests/unit/isolated/test_utils_rate_limiting.py @@ -10,8 +10,9 @@ from aiohttp import web from aiohttp.test_utils import TestClient from aiohttp.web_exceptions import HTTPOk, HTTPTooManyRequests -from pydantic import ValidationError, conint, parse_obj_as +from pydantic import Field, TypeAdapter, ValidationError from simcore_service_webserver.utils_rate_limiting import global_rate_limit_route +from typing_extensions import Annotated TOTAL_TEST_TIME = 1 # secs MAX_NUM_REQUESTS = 3 @@ -110,7 +111,7 @@ async def test_global_rate_limit_route(requests_per_second: float, client: TestC for t in tasks: if retry_after := t.result().headers.get("Retry-After"): try: - parse_obj_as(conint(ge=1), retry_after) + TypeAdapter(Annotated[int, Field(ge=1)]).validate_python(retry_after) except ValidationError as err: failed.append((retry_after, f"{err}")) assert not failed diff --git a/services/web/server/tests/unit/with_dbs/01/clusters/test_clusters_handlers.py b/services/web/server/tests/unit/with_dbs/01/clusters/test_clusters_handlers.py index c3f6b1d8570..510b3118dca 100644 --- a/services/web/server/tests/unit/with_dbs/01/clusters/test_clusters_handlers.py +++ b/services/web/server/tests/unit/with_dbs/01/clusters/test_clusters_handlers.py @@ -45,19 +45,19 @@ def mocked_director_v2_api(mocker: MockerFixture): "simcore_service_webserver.clusters._handlers.director_v2_api", autospec=True ) - mocked_director_v2_api.create_cluster.return_value = Cluster.parse_obj( - random.choice(Cluster.Config.schema_extra["examples"]) + mocked_director_v2_api.create_cluster.return_value = Cluster.model_validate( + random.choice(Cluster.model_config["json_schema_extra"]["examples"]) ) mocked_director_v2_api.list_clusters.return_value = [] - mocked_director_v2_api.get_cluster.return_value = Cluster.parse_obj( - random.choice(Cluster.Config.schema_extra["examples"]) + mocked_director_v2_api.get_cluster.return_value = Cluster.model_validate( + random.choice(Cluster.model_config["json_schema_extra"]["examples"]) ) mocked_director_v2_api.get_cluster_details.return_value = { "scheduler": {"status": "running"}, "dashboardLink": "https://link.to.dashboard", } - mocked_director_v2_api.update_cluster.return_value = Cluster.parse_obj( - random.choice(Cluster.Config.schema_extra["examples"]) + mocked_director_v2_api.update_cluster.return_value = Cluster.model_validate( + random.choice(Cluster.model_config["json_schema_extra"]["examples"]) ) mocked_director_v2_api.delete_cluster.return_value = None mocked_director_v2_api.ping_cluster.return_value = None @@ -132,7 +132,7 @@ async def test_create_cluster( # we are done here return - created_cluster = Cluster.parse_obj(data) + created_cluster = Cluster.model_validate(data) assert created_cluster diff --git a/services/web/server/tests/unit/with_dbs/01/studies_dispatcher/test_studies_dispatcher_handlers.py b/services/web/server/tests/unit/with_dbs/01/studies_dispatcher/test_studies_dispatcher_handlers.py index 7933270984a..9710882aed9 100644 --- a/services/web/server/tests/unit/with_dbs/01/studies_dispatcher/test_studies_dispatcher_handlers.py +++ b/services/web/server/tests/unit/with_dbs/01/studies_dispatcher/test_studies_dispatcher_handlers.py @@ -18,7 +18,7 @@ from aioresponses import aioresponses from models_library.projects_state import ProjectLocked, ProjectStatus from models_library.utils.json_serialization import json_dumps -from pydantic import BaseModel, ByteSize, parse_obj_as +from pydantic import BaseModel, ByteSize, TypeAdapter from pytest_mock import MockerFixture from pytest_simcore.helpers.assert_checks import assert_status from pytest_simcore.helpers.webserver_login import UserInfoDict, UserRole @@ -241,7 +241,7 @@ def test_model_examples( model_cls: type[BaseModel], example_name: int, example_data: Any ): print(example_name, ":", json_dumps(example_data)) - model = model_cls.parse_obj(example_data) + model = model_cls.model_validate(example_data) assert model @@ -253,7 +253,7 @@ async def test_api_list_services(client: TestClient): data, error = await assert_status(response, status.HTTP_200_OK) - services = parse_obj_as(list[ServiceGet], data) + services = TypeAdapter(list[ServiceGet]).validate_python(data) assert services # latest versions of services with everyone + ospar-product (see stmt_create_services_access_rights) @@ -350,7 +350,7 @@ def redirect_url(redirect_type: str, client: TestClient) -> URL: if redirect_type == "service_and_file": query = { "file_name": "users.csv", - "file_size": parse_obj_as(ByteSize, "100KB"), + "file_size": TypeAdapter(ByteSize).validate_python("100KB"), "file_type": "CSV", "viewer_key": "simcore/services/dynamic/raw-graphs", "viewer_version": "2.11.1", @@ -366,7 +366,7 @@ def redirect_url(redirect_type: str, client: TestClient) -> URL: elif redirect_type == "file_only": query = { "file_name": "users.csv", - "file_size": parse_obj_as(ByteSize, "1MiB"), + "file_size": TypeAdapter(ByteSize).validate_python("1MiB"), "file_type": "CSV", "download_link": URL( "https://raw.githubusercontent.com/ITISFoundation/osparc-simcore/8987c95d0ca0090e14f3a5b52db724fa24114cf5/services/storage/tests/data/users.csv" diff --git a/services/web/server/tests/unit/with_dbs/01/studies_dispatcher/test_studies_dispatcher_projects.py b/services/web/server/tests/unit/with_dbs/01/studies_dispatcher/test_studies_dispatcher_projects.py index 48aacf56c6c..cd9bc502089 100644 --- a/services/web/server/tests/unit/with_dbs/01/studies_dispatcher/test_studies_dispatcher_projects.py +++ b/services/web/server/tests/unit/with_dbs/01/studies_dispatcher/test_studies_dispatcher_projects.py @@ -106,7 +106,7 @@ async def test_add_new_project_from_model_instance( project_id=project_id, service_id=viewer_id, owner=user, - service_info=ServiceInfo.parse_obj(viewer_info), + service_info=ServiceInfo.model_validate(viewer_info), ) else: project = _create_project_with_filepicker_and_service( diff --git a/services/web/server/tests/unit/with_dbs/01/test_api_keys_rpc.py b/services/web/server/tests/unit/with_dbs/01/test_api_keys_rpc.py index 7ad51c739d7..51467f3c822 100644 --- a/services/web/server/tests/unit/with_dbs/01/test_api_keys_rpc.py +++ b/services/web/server/tests/unit/with_dbs/01/test_api_keys_rpc.py @@ -12,7 +12,7 @@ from models_library.api_schemas_webserver.auth import ApiKeyCreate from models_library.products import ProductName from models_library.rabbitmq_basic_types import RPCMethodName -from pydantic import parse_obj_as +from pydantic import TypeAdapter from pytest_mock import MockerFixture from pytest_simcore.helpers.monkeypatch_envs import setenvs_from_dict from pytest_simcore.helpers.typing_env import EnvVarsDict @@ -104,7 +104,7 @@ async def test_api_key_get( for api_key_name in fake_user_api_keys: result = await rpc_client.request( WEBSERVER_RPC_NAMESPACE, - parse_obj_as(RPCMethodName, "api_key_get"), + TypeAdapter(RPCMethodName).validate_python("api_key_get"), product_name=osparc_product_name, user_id=logged_user["id"], name=api_key_name, @@ -124,7 +124,7 @@ async def test_api_keys_workflow( # creating a key created_api_key = await rpc_client.request( WEBSERVER_RPC_NAMESPACE, - parse_obj_as(RPCMethodName, "create_api_keys"), + TypeAdapter(RPCMethodName).validate_python("create_api_keys"), product_name=osparc_product_name, user_id=logged_user["id"], new=ApiKeyCreate(display_name=key_name, expiration=None), @@ -134,7 +134,7 @@ async def test_api_keys_workflow( # query the key is still present queried_api_key = await rpc_client.request( WEBSERVER_RPC_NAMESPACE, - parse_obj_as(RPCMethodName, "api_key_get"), + TypeAdapter(RPCMethodName).validate_python("api_key_get"), product_name=osparc_product_name, user_id=logged_user["id"], name=key_name, @@ -146,7 +146,7 @@ async def test_api_keys_workflow( # remove the key delete_key_result = await rpc_client.request( WEBSERVER_RPC_NAMESPACE, - parse_obj_as(RPCMethodName, "delete_api_keys"), + TypeAdapter(RPCMethodName).validate_python("delete_api_keys"), product_name=osparc_product_name, user_id=logged_user["id"], name=key_name, @@ -156,7 +156,7 @@ async def test_api_keys_workflow( # key no longer present query_missing_query = await rpc_client.request( WEBSERVER_RPC_NAMESPACE, - parse_obj_as(RPCMethodName, "api_key_get"), + TypeAdapter(RPCMethodName).validate_python("api_key_get"), product_name=osparc_product_name, user_id=logged_user["id"], name=key_name, diff --git a/services/web/server/tests/unit/with_dbs/01/test_catalog_handlers__pricing_plan.py b/services/web/server/tests/unit/with_dbs/01/test_catalog_handlers__pricing_plan.py index b328ddc4c7d..4aa63762dea 100644 --- a/services/web/server/tests/unit/with_dbs/01/test_catalog_handlers__pricing_plan.py +++ b/services/web/server/tests/unit/with_dbs/01/test_catalog_handlers__pricing_plan.py @@ -13,7 +13,7 @@ PricingPlanGet, ) from models_library.utils.fastapi_encoders import jsonable_encoder -from pydantic import parse_obj_as +from pydantic import TypeAdapter from pytest_simcore.aioresponses_mocker import AioResponsesMock from pytest_simcore.helpers.assert_checks import assert_status from pytest_simcore.helpers.webserver_login import UserInfoDict @@ -30,9 +30,8 @@ def mock_rut_api_responses( assert client.app settings: ResourceUsageTrackerSettings = get_plugin_settings(client.app) - service_pricing_plan_get = parse_obj_as( - PricingPlanGet, - PricingPlanGet.Config.schema_extra["examples"][0], + service_pricing_plan_get = TypeAdapter(PricingPlanGet).validate_python( + PricingPlanGet.model_config["json_schema_extra"]["examples"][0], ) aioresponses_mocker.get( re.compile(f"^{settings.api_base_url}/services/+.+$"), diff --git a/services/web/server/tests/unit/with_dbs/01/test_catalog_handlers__services.py b/services/web/server/tests/unit/with_dbs/01/test_catalog_handlers__services.py index 396e3a1f8a0..f346ce0bb9f 100644 --- a/services/web/server/tests/unit/with_dbs/01/test_catalog_handlers__services.py +++ b/services/web/server/tests/unit/with_dbs/01/test_catalog_handlers__services.py @@ -19,7 +19,7 @@ from models_library.services_types import ServiceKey, ServiceVersion from models_library.users import UserID from models_library.utils.fastapi_encoders import jsonable_encoder -from pydantic import NonNegativeInt, parse_obj_as +from pydantic import NonNegativeInt, TypeAdapter from pytest_mock import MockerFixture from pytest_simcore.helpers.assert_checks import assert_status from pytest_simcore.helpers.monkeypatch_envs import setenvs_from_dict @@ -55,8 +55,8 @@ async def _list( assert product_name assert user_id - items = parse_obj_as( - list[ServiceGetV2], ServiceGetV2.Config.schema_extra["examples"] + items = TypeAdapter(list[ServiceGetV2]).validate_python( + ServiceGetV2.model_config["json_schema_extra"]["examples"], ) total_count = len(items) @@ -79,8 +79,8 @@ async def _get( assert product_name assert user_id - got = parse_obj_as( - ServiceGetV2, ServiceGetV2.Config.schema_extra["examples"][0] + got = TypeAdapter(ServiceGetV2).validate_python( + ServiceGetV2.model_config["json_schema_extra"]["examples"][0] ) got.version = service_version got.key = service_key @@ -100,8 +100,8 @@ async def _update( assert product_name assert user_id - got = parse_obj_as( - ServiceGetV2, ServiceGetV2.Config.schema_extra["examples"][0] + got = TypeAdapter(ServiceGetV2).validate_python( + ServiceGetV2.model_config["json_schema_extra"]["examples"][0] ) got.version = service_version got.key = service_key @@ -146,7 +146,7 @@ async def test_list_services_latest( assert data assert error is None - model = parse_obj_as(Page[CatalogServiceGet], data) + model = TypeAdapter(Page[CatalogServiceGet]).validate_python(data) assert model assert model.data assert len(model.data) == model.meta.count @@ -180,7 +180,7 @@ async def test_get_and_patch_service( assert data assert error is None - model = parse_obj_as(CatalogServiceGet, data) + model = TypeAdapter(CatalogServiceGet).validate_python(data) assert model.key == service_key assert model.version == service_version @@ -205,7 +205,7 @@ async def test_get_and_patch_service( assert data assert error is None - model = parse_obj_as(CatalogServiceGet, data) + model = TypeAdapter(CatalogServiceGet).validate_python(data) assert model.key == service_key assert model.version == service_version assert model.name == update.name diff --git a/services/web/server/tests/unit/with_dbs/01/test_catalog_handlers__services_resources.py b/services/web/server/tests/unit/with_dbs/01/test_catalog_handlers__services_resources.py index afffca3652a..5c65109ef0a 100644 --- a/services/web/server/tests/unit/with_dbs/01/test_catalog_handlers__services_resources.py +++ b/services/web/server/tests/unit/with_dbs/01/test_catalog_handlers__services_resources.py @@ -13,7 +13,7 @@ ServiceResourcesDict, ServiceResourcesDictHelpers, ) -from pydantic import parse_obj_as +from pydantic import TypeAdapter from pytest_simcore.aioresponses_mocker import AioResponsesMock from pytest_simcore.helpers.assert_checks import assert_status from pytest_simcore.helpers.webserver_login import UserInfoDict @@ -32,9 +32,8 @@ def mock_catalog_service_api_responses( url_pattern = re.compile(f"^{settings.base_url}+/.+$") - service_resources = parse_obj_as( - ServiceResourcesDict, - ServiceResourcesDictHelpers.Config.schema_extra["examples"][0], + service_resources = TypeAdapter(ServiceResourcesDict).validate_python( + ServiceResourcesDictHelpers.model_config["json_schema_extra"]["examples"][0], ) jsonable_service_resources = ServiceResourcesDictHelpers.create_jsonable( service_resources diff --git a/services/web/server/tests/unit/with_dbs/02/conftest.py b/services/web/server/tests/unit/with_dbs/02/conftest.py index 425756375b1..9eda857afd0 100644 --- a/services/web/server/tests/unit/with_dbs/02/conftest.py +++ b/services/web/server/tests/unit/with_dbs/02/conftest.py @@ -24,7 +24,7 @@ ServiceResourcesDict, ServiceResourcesDictHelpers, ) -from pydantic import parse_obj_as +from pydantic import TypeAdapter from pytest_mock import MockerFixture from pytest_simcore.helpers.assert_checks import assert_status from pytest_simcore.helpers.monkeypatch_envs import setenvs_from_dict @@ -39,9 +39,8 @@ @pytest.fixture def mock_service_resources() -> ServiceResourcesDict: - return parse_obj_as( - ServiceResourcesDict, - ServiceResourcesDictHelpers.Config.schema_extra["examples"][0], + return TypeAdapter(ServiceResourcesDict).validate_python( + ServiceResourcesDictHelpers.model_config["json_schema_extra"]["examples"][0], ) @@ -505,4 +504,4 @@ def workbench_db_column() -> dict[str, Any]: @pytest.fixture def workbench(workbench_db_column: dict[str, Any]) -> dict[NodeID, Node]: # convert to model - return parse_obj_as(dict[NodeID, Node], workbench_db_column) + return TypeAdapter(dict[NodeID, Node]).validate_python(workbench_db_column) diff --git a/services/web/server/tests/unit/with_dbs/02/test_announcements.py b/services/web/server/tests/unit/with_dbs/02/test_announcements.py index cd87e2526c6..19ca7413827 100644 --- a/services/web/server/tests/unit/with_dbs/02/test_announcements.py +++ b/services/web/server/tests/unit/with_dbs/02/test_announcements.py @@ -185,7 +185,7 @@ async def test_list_announcements_filtered( def test_model_examples( model_cls: type[BaseModel], example_name: int, example_data: Any ): - assert model_cls.parse_obj( + assert model_cls.model_validate( example_data ), f"Failed {example_name} : {json.dumps(example_data)}" @@ -193,7 +193,7 @@ def test_model_examples( def test_invalid_announcement(faker: Faker): now = arrow.utcnow() with pytest.raises(ValidationError): - Announcement.parse_obj( + Announcement.model_validate( { "id": "Student_Competition_2023", "products": ["s4llite", "osparc"], @@ -209,7 +209,7 @@ def test_invalid_announcement(faker: Faker): def test_announcement_expired(faker: Faker): now = arrow.utcnow() - model = Announcement.parse_obj( + model = Announcement.model_validate( { "id": "Student_Competition_2023", "products": ["s4llite", "osparc"], diff --git a/services/web/server/tests/unit/with_dbs/02/test_projects_cancellations.py b/services/web/server/tests/unit/with_dbs/02/test_projects_cancellations.py index 6c841fa8650..74c932ca600 100644 --- a/services/web/server/tests/unit/with_dbs/02/test_projects_cancellations.py +++ b/services/web/server/tests/unit/with_dbs/02/test_projects_cancellations.py @@ -9,7 +9,7 @@ import pytest from aiohttp.test_utils import TestClient -from pydantic import ByteSize, parse_obj_as +from pydantic import ByteSize, TypeAdapter from pytest_simcore.helpers.assert_checks import assert_status from pytest_simcore.helpers.monkeypatch_envs import setenvs_from_dict from pytest_simcore.helpers.typing_env import EnvVarsDict @@ -150,14 +150,14 @@ async def test_copying_large_project_and_retrieving_copy_task( create_url = create_url.with_query(from_study=user_project["uuid"]) resp = await client.post(f"{create_url}", json={}) data, error = await assert_status(resp, expected.accepted) - created_copy_task = TaskGet.parse_obj(data) + created_copy_task = TaskGet.model_validate(data) # list current tasks list_task_url = client.app.router["list_tasks"].url_for() resp = await client.get(f"{list_task_url}") data, error = await assert_status(resp, expected.ok) assert data assert not error - list_of_tasks = parse_obj_as(list[TaskGet], data) + list_of_tasks = TypeAdapter(list[TaskGet]).validate_python(data) assert len(list_of_tasks) == 1 task = list_of_tasks[0] assert task.task_name == f"POST {create_url}" @@ -290,9 +290,9 @@ async def test_copying_too_large_project_returns_422( large_project_total_size = ( app_settings.WEBSERVER_PROJECTS.PROJECTS_MAX_COPY_SIZE_BYTES + 1 ) - storage_subsystem_mock.get_project_total_size_simcore_s3.return_value = ( - parse_obj_as(ByteSize, large_project_total_size) - ) + storage_subsystem_mock.get_project_total_size_simcore_s3.return_value = TypeAdapter( + ByteSize + ).validate_python(large_project_total_size) # POST /v0/projects await request_create_project( diff --git a/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers.py b/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers.py index ec856a80f05..7f58895769b 100644 --- a/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers.py +++ b/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers.py @@ -23,7 +23,7 @@ from models_library.projects_state import ProjectState from models_library.services import ServiceKey from models_library.utils.fastapi_encoders import jsonable_encoder -from pydantic import parse_obj_as +from pydantic import TypeAdapter from pytest_simcore.helpers.assert_checks import assert_status from pytest_simcore.helpers.webserver_login import UserInfoDict from pytest_simcore.helpers.webserver_parametrizations import ( @@ -173,10 +173,10 @@ async def _assert_get_same_project( assert data == project if project_state: - assert parse_obj_as(ProjectState, project_state) + assert TypeAdapter(ProjectState).validate_python(project_state) if project_permalink: - assert parse_obj_as(ProjectPermalink, project_permalink) + assert TypeAdapter(ProjectPermalink).validate_python(project_permalink) assert folder_id is None @@ -231,7 +231,7 @@ async def test_list_projects( assert not ProjectState( **project_state ).locked.value, "Templates are not locked" - assert parse_obj_as(ProjectPermalink, project_permalink) + assert TypeAdapter(ProjectPermalink).validate_python(project_permalink) # standard project project_state = data[1].pop("state") @@ -274,7 +274,7 @@ async def test_list_projects( assert not ProjectState( **project_state ).locked.value, "Templates are not locked" - assert parse_obj_as(ProjectPermalink, project_permalink) + assert TypeAdapter(ProjectPermalink).validate_python(project_permalink) @pytest.fixture(scope="session") @@ -454,7 +454,7 @@ async def test_new_project_from_template( if new_project: # check uuid replacement for node_name in new_project["workbench"]: - parse_obj_as(uuidlib.UUID, node_name) + TypeAdapter(uuidlib.UUID).validate_python(node_name) @pytest.mark.parametrize(*standard_user_role_response()) @@ -483,7 +483,7 @@ async def test_new_project_from_other_study( # check uuid replacement assert new_project["name"].endswith("(Copy)") for node_name in new_project["workbench"]: - parse_obj_as(uuidlib.UUID, node_name) + TypeAdapter(uuidlib.UUID).validate_python(node_name) @pytest.mark.parametrize(*standard_user_role_response()) @@ -537,7 +537,7 @@ async def test_new_project_from_template_with_body( # check uuid replacement for node_name in project["workbench"]: - parse_obj_as(uuidlib.UUID, node_name) + TypeAdapter(uuidlib.UUID).validate_python(node_name) @pytest.mark.parametrize(*standard_user_role_response()) @@ -593,7 +593,7 @@ async def test_new_template_from_project( # check uuid replacement for node_name in template_project["workbench"]: - parse_obj_as(uuidlib.UUID, node_name) + TypeAdapter(uuidlib.UUID).validate_python(node_name) # do the same with a body predefined = { @@ -653,7 +653,7 @@ async def test_new_template_from_project( # check uuid replacement for node_name in template_project["workbench"]: - parse_obj_as(uuidlib.UUID, node_name) + TypeAdapter(uuidlib.UUID).validate_python(node_name) # PUT -------- diff --git a/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers__clone.py b/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers__clone.py index 6ca7392dd4b..657b19e20d6 100644 --- a/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers__clone.py +++ b/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers__clone.py @@ -14,7 +14,7 @@ from faker import Faker from models_library.api_schemas_webserver.projects import ProjectGet from models_library.projects import ProjectID -from pydantic import parse_obj_as +from pydantic import TypeAdapter from pytest_simcore.helpers.webserver_login import UserInfoDict from pytest_simcore.helpers.webserver_parametrizations import ( MockedStorageSubsystem, @@ -49,7 +49,7 @@ async def _request_clone_project(client: TestClient, url: URL) -> ProjectGet: data = await long_running_task.result() assert data is not None - return ProjectGet.parse_obj(data) + return ProjectGet.model_validate(data) @pytest.mark.parametrize(*standard_role_response(), ids=str) @@ -105,9 +105,9 @@ async def test_clone_project( # check whether it's a clone assert ProjectID(project["uuid"]) != cloned_project.uuid assert project["description"] == cloned_project.description - assert parse_obj_as(datetime, project["creationDate"]) < parse_obj_as( - datetime, cloned_project.creation_date - ) + assert TypeAdapter(datetime).validate_python(project["creationDate"]) < TypeAdapter( + datetime + ).validate_python(cloned_project.creation_date) assert len(project["workbench"]) == len(cloned_project.workbench) assert set(project["workbench"].keys()) != set( diff --git a/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers__clone_in_workspace_and_folder.py b/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers__clone_in_workspace_and_folder.py index 051f522fcd9..755a1081fcb 100644 --- a/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers__clone_in_workspace_and_folder.py +++ b/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers__clone_in_workspace_and_folder.py @@ -80,7 +80,7 @@ async def _request_clone_project(client: TestClient, url: URL) -> ProjectGet: data = await long_running_task.result() assert data is not None - return ProjectGet.parse_obj(data) + return ProjectGet.model_validate(data) @pytest.mark.parametrize( diff --git a/services/web/server/tests/unit/with_dbs/02/test_projects_metadata_handlers.py b/services/web/server/tests/unit/with_dbs/02/test_projects_metadata_handlers.py index 7a7056d7bbc..52aed0c6bf4 100644 --- a/services/web/server/tests/unit/with_dbs/02/test_projects_metadata_handlers.py +++ b/services/web/server/tests/unit/with_dbs/02/test_projects_metadata_handlers.py @@ -18,7 +18,7 @@ ) from models_library.projects import ProjectID from models_library.projects_nodes_io import NodeID -from pydantic import parse_obj_as +from pydantic import TypeAdapter from pytest_simcore.helpers.assert_checks import assert_status from pytest_simcore.helpers.webserver_login import UserInfoDict from pytest_simcore.helpers.webserver_parametrizations import ( @@ -88,7 +88,9 @@ async def test_custom_metadata_handlers( data, _ = await assert_status(response, expected_status_code=expected.ok) - assert parse_obj_as(ProjectMetadataGet, data).custom == custom_metadata + assert ( + TypeAdapter(ProjectMetadataGet).validate_python(data).custom == custom_metadata + ) # delete project url = client.app.router["delete_project"].url_for(project_id=user_project["uuid"]) @@ -138,9 +140,9 @@ async def test_new_project_with_parent_project_node( ) assert parent_project - parent_project_uuid = parse_obj_as(ProjectID, parent_project["uuid"]) - parent_node_id = parse_obj_as( - NodeID, random.choice(list(parent_project["workbench"])) # noqa: S311 + parent_project_uuid = TypeAdapter(ProjectID).validate_python(parent_project["uuid"]) + parent_node_id = TypeAdapter(NodeID).validate_python( + random.choice(list(parent_project["workbench"])) # noqa: S311 ) child_project = await request_create_project( client, @@ -178,7 +180,9 @@ async def test_new_project_with_parent_project_node( f"{url}", json=ProjectMetadataUpdate(custom=custom_metadata).dict() ) data, _ = await assert_status(response, expected_status_code=status.HTTP_200_OK) - assert parse_obj_as(ProjectMetadataGet, data).custom == custom_metadata + assert ( + TypeAdapter(ProjectMetadataGet).validate_python(data).custom == custom_metadata + ) # check child project has parent unchanged async with aiopg_engine.acquire() as connection: project_db_metadata = await get_db_project_metadata( @@ -216,13 +220,13 @@ async def test_new_project_with_invalid_parent_project_node( ) assert parent_project - parent_project_uuid = parse_obj_as(ProjectID, parent_project["uuid"]) - parent_node_id = parse_obj_as( - NodeID, random.choice(list(parent_project["workbench"])) # noqa: S311 + parent_project_uuid = TypeAdapter(ProjectID).validate_python(parent_project["uuid"]) + parent_node_id = TypeAdapter(NodeID).validate_python( + random.choice(list(parent_project["workbench"])) # noqa: S311 ) # creating with random project UUID should fail - random_project_uuid = parse_obj_as(ProjectID, faker.uuid4()) + random_project_uuid = TypeAdapter(ProjectID).validate_python(faker.uuid4()) child_project = await request_create_project( client, expected.accepted, @@ -235,7 +239,7 @@ async def test_new_project_with_invalid_parent_project_node( assert not child_project # creating with a random node ID should fail too - random_node_id = parse_obj_as(NodeID, faker.uuid4()) + random_node_id = TypeAdapter(NodeID).validate_python(faker.uuid4()) child_project = await request_create_project( client, expected.accepted, @@ -259,7 +263,7 @@ async def test_new_project_with_invalid_parent_project_node( assert not child_project # creating with only a parent node ID should fail too - random_node_id = parse_obj_as(NodeID, faker.uuid4()) + random_node_id = TypeAdapter(NodeID).validate_python(faker.uuid4()) child_project = await request_create_project( client, expected.unprocessable, @@ -320,10 +324,12 @@ async def test_set_project_parent_backward_compatibility( project_id=child_project["uuid"] ) response = await client.patch( - f"{url}", json=ProjectMetadataUpdate(custom=custom_metadata).dict() + f"{url}", json=ProjectMetadataUpdate(custom=custom_metadata).model_dump() ) data, _ = await assert_status(response, expected_status_code=status.HTTP_200_OK) - assert parse_obj_as(ProjectMetadataGet, data).custom == custom_metadata + assert ( + TypeAdapter(ProjectMetadataGet).validate_python(data).custom == custom_metadata + ) # check child project has parent set correctly async with aiopg_engine.acquire() as connection: project_db_metadata = await get_db_project_metadata( @@ -430,7 +436,9 @@ async def test_update_project_metadata_s4lacad_backward_compatibility_passing_ni f"{url}", json=ProjectMetadataUpdate(custom=custom_metadata).dict() ) data, _ = await assert_status(response, expected_status_code=status.HTTP_200_OK) - assert parse_obj_as(ProjectMetadataGet, data).custom == custom_metadata + assert ( + TypeAdapter(ProjectMetadataGet).validate_python(data).custom == custom_metadata + ) # check project has no parent async with aiopg_engine.acquire() as connection: diff --git a/services/web/server/tests/unit/with_dbs/02/test_projects_nodes_handler.py b/services/web/server/tests/unit/with_dbs/02/test_projects_nodes_handler.py index 8243228681b..b7285f19cda 100644 --- a/services/web/server/tests/unit/with_dbs/02/test_projects_nodes_handler.py +++ b/services/web/server/tests/unit/with_dbs/02/test_projects_nodes_handler.py @@ -33,7 +33,7 @@ ServiceResourcesDictHelpers, ) from models_library.utils.fastapi_encoders import jsonable_encoder -from pydantic import NonNegativeFloat, NonNegativeInt, parse_obj_as +from pydantic import NonNegativeFloat, NonNegativeInt, TypeAdapter from pytest_simcore.helpers.assert_checks import assert_status from pytest_simcore.helpers.monkeypatch_envs import setenvs_from_dict from pytest_simcore.helpers.webserver_parametrizations import ( @@ -72,12 +72,14 @@ async def test_get_node_resources( data, error = await assert_status(response, expected) if data: assert not error - node_resources = parse_obj_as(ServiceResourcesDict, data) + node_resources = TypeAdapter(ServiceResourcesDict).validate_python(data) assert node_resources assert DEFAULT_SINGLE_SERVICE_NAME in node_resources assert ( node_resources - == ServiceResourcesDictHelpers.Config.schema_extra["examples"][0] + == ServiceResourcesDictHelpers.model_config["json_schema_extra"][ + "examples" + ][0] ) else: assert not data @@ -145,18 +147,22 @@ async def test_replace_node_resources_is_forbidden_by_default( response = await client.put( f"{url}", json=ServiceResourcesDictHelpers.create_jsonable( - ServiceResourcesDictHelpers.Config.schema_extra["examples"][0] + ServiceResourcesDictHelpers.model_config["json_schema_extra"][ + "examples" + ][0] ), ) data, error = await assert_status(response, expected) if data: assert not error - node_resources = parse_obj_as(ServiceResourcesDict, data) + node_resources = TypeAdapter(ServiceResourcesDict).validate_python(data) assert node_resources assert DEFAULT_SINGLE_SERVICE_NAME in node_resources assert ( node_resources - == ServiceResourcesDictHelpers.Config.schema_extra["examples"][0] + == ServiceResourcesDictHelpers.model_config["json_schema_extra"][ + "examples" + ][0] ) @@ -183,18 +189,22 @@ async def test_replace_node_resources_is_ok_if_explicitly_authorized( response = await client.put( f"{url}", json=ServiceResourcesDictHelpers.create_jsonable( - ServiceResourcesDictHelpers.Config.schema_extra["examples"][0] + ServiceResourcesDictHelpers.model_config["json_schema_extra"][ + "examples" + ][0] ), ) data, error = await assert_status(response, expected) if data: assert not error - node_resources = parse_obj_as(ServiceResourcesDict, data) + node_resources = TypeAdapter(ServiceResourcesDict).validate_python(data) assert node_resources assert DEFAULT_SINGLE_SERVICE_NAME in node_resources assert ( node_resources - == ServiceResourcesDictHelpers.Config.schema_extra["examples"][0] + == ServiceResourcesDictHelpers.model_config["json_schema_extra"][ + "examples" + ][0] ) @@ -218,7 +228,9 @@ async def test_replace_node_resources_raises_422_if_resource_does_not_validate( f"{url}", json=ServiceResourcesDictHelpers.create_jsonable( # NOTE: we apply a different resource set - ServiceResourcesDictHelpers.Config.schema_extra["examples"][1] + ServiceResourcesDictHelpers.model_config["json_schema_extra"][ + "examples" + ][1] ), ) await assert_status(response, expected) @@ -383,8 +395,8 @@ def num_services( self, *args, **kwargs ) -> list[DynamicServiceGet]: # noqa: ARG002 return [ - DynamicServiceGet.parse_obj( - DynamicServiceGet.Config.schema_extra["examples"][1] + DynamicServiceGet.model_validate( + DynamicServiceGet.model_config["json_schema_extra"]["examples"][1] | {"service_uuid": service_uuid, "project_id": user_project["uuid"]} ) for service_uuid in self.running_services_uuids @@ -928,8 +940,7 @@ def mock_storage_calls(aioresponses_mocker: aioresponses, faker: Faker) -> None: payload=jsonable_encoder( Envelope[list[FileMetaDataGet]]( data=[ - parse_obj_as( - FileMetaDataGet, + TypeAdapter(FileMetaDataGet).validate_python( { "file_uuid": file_uuid, "location_id": 0, @@ -979,7 +990,7 @@ async def test_read_project_nodes_previews( assert not error assert len(data) == 3 - nodes_previews = parse_obj_as(list[_ProjectNodePreview], data) + nodes_previews = TypeAdapter(list[_ProjectNodePreview]).validate_python(data) # GET node's preview for node_preview in nodes_previews: @@ -995,4 +1006,4 @@ async def test_read_project_nodes_previews( status.HTTP_200_OK, ) - assert parse_obj_as(_ProjectNodePreview, data) == node_preview + assert TypeAdapter(_ProjectNodePreview).validate_python(data) == node_preview diff --git a/services/web/server/tests/unit/with_dbs/02/test_projects_nodes_pricing_unit_handlers.py b/services/web/server/tests/unit/with_dbs/02/test_projects_nodes_pricing_unit_handlers.py index 06957402de2..d69f6466b54 100644 --- a/services/web/server/tests/unit/with_dbs/02/test_projects_nodes_pricing_unit_handlers.py +++ b/services/web/server/tests/unit/with_dbs/02/test_projects_nodes_pricing_unit_handlers.py @@ -18,7 +18,7 @@ PricingUnitGet, ) from models_library.utils.fastapi_encoders import jsonable_encoder -from pydantic import parse_obj_as +from pydantic import TypeAdapter from pytest_mock.plugin import MockerFixture from pytest_simcore.aioresponses_mocker import AioResponsesMock from pytest_simcore.helpers.assert_checks import assert_status @@ -98,8 +98,8 @@ def mock_rut_api_responses( assert client.app settings: ResourceUsageTrackerSettings = get_plugin_settings(client.app) - pricing_unit_get_base = parse_obj_as( - PricingUnitGet, PricingUnitGet.Config.schema_extra["examples"][0] + pricing_unit_get_base = TypeAdapter(PricingUnitGet).validate_python( + PricingUnitGet.model_config["json_schema_extra"]["examples"][0] ) pricing_unit_get_1 = pricing_unit_get_base.copy() pricing_unit_get_1.pricing_unit_id = _PRICING_UNIT_ID_1 diff --git a/services/web/server/tests/unit/with_dbs/02/test_projects_ports_handlers.py b/services/web/server/tests/unit/with_dbs/02/test_projects_ports_handlers.py index ae1b62e0558..8a82df500b6 100644 --- a/services/web/server/tests/unit/with_dbs/02/test_projects_ports_handlers.py +++ b/services/web/server/tests/unit/with_dbs/02/test_projects_ports_handlers.py @@ -15,7 +15,7 @@ from models_library.api_schemas_directorv2.comp_tasks import TasksOutputs from models_library.api_schemas_webserver.projects import ProjectGet from models_library.utils.fastapi_encoders import jsonable_encoder -from pydantic import parse_obj_as +from pydantic import TypeAdapter from pytest_simcore.helpers.assert_checks import assert_status from pytest_simcore.helpers.webserver_fake_ports_data import ( PROJECTS_METADATA_PORTS_RESPONSE_BODY_DATA, @@ -276,13 +276,13 @@ async def test_clone_project_and_set_inputs( data = await long_running_task.result() assert data is not None - cloned_project = ProjectGet.parse_obj(data) + cloned_project = ProjectGet.model_validate(data) assert parent_project_id != cloned_project.uuid assert user_project["description"] == cloned_project.description - assert parse_obj_as(datetime, user_project["creationDate"]) < parse_obj_as( - datetime, cloned_project.creation_date - ) + assert TypeAdapter(datetime).validate_python( + user_project["creationDate"] + ) < TypeAdapter(datetime).validate_python(cloned_project.creation_date) # - set_inputs project_clone_id ---------------------------------------------- job_inputs_values = {"X": 42} # like JobInputs.values diff --git a/services/web/server/tests/unit/with_dbs/02/test_projects_states_handlers.py b/services/web/server/tests/unit/with_dbs/02/test_projects_states_handlers.py index 4c8b41e4861..a37c7e3bb1e 100644 --- a/services/web/server/tests/unit/with_dbs/02/test_projects_states_handlers.py +++ b/services/web/server/tests/unit/with_dbs/02/test_projects_states_handlers.py @@ -1039,10 +1039,11 @@ async def test_project_node_lifetime( # noqa: PLR0915 project_id=user_project["uuid"], node_id=dynamic_node_id ) - node_sample = deepcopy(NodeGet.Config.schema_extra["examples"][1]) + node_sample = deepcopy(NodeGet.model_config["json_schema_extra"]["example"]) + mocked_director_v2_api[ "dynamic_scheduler.api.get_dynamic_service" - ].return_value = NodeGet.parse_obj( + ].return_value = NodeGet.model_validate( { **node_sample, "service_state": "running", @@ -1061,7 +1062,7 @@ async def test_project_node_lifetime( # noqa: PLR0915 ) mocked_director_v2_api[ "dynamic_scheduler.api.get_dynamic_service" - ].return_value = NodeGetIdle.parse_obj( + ].return_value = NodeGetIdle.model_validate( { "service_uuid": node_sample["service_uuid"], "service_state": "idle", diff --git a/services/web/server/tests/unit/with_dbs/02/test_projects_wallet_handlers.py b/services/web/server/tests/unit/with_dbs/02/test_projects_wallet_handlers.py index 9443f773c03..b1e140a325f 100644 --- a/services/web/server/tests/unit/with_dbs/02/test_projects_wallet_handlers.py +++ b/services/web/server/tests/unit/with_dbs/02/test_projects_wallet_handlers.py @@ -12,7 +12,7 @@ import sqlalchemy as sa from aiohttp.test_utils import TestClient from models_library.api_schemas_webserver.wallets import WalletGet -from pydantic import parse_obj_as +from pydantic import TypeAdapter from pytest_simcore.helpers.assert_checks import assert_status from pytest_simcore.helpers.webserver_login import LoggedUser, UserInfoDict from servicelib.aiohttp import status @@ -93,7 +93,7 @@ def setup_wallets_db( ) .returning(sa.literal_column("*")) ) - output.append(parse_obj_as(WalletGet, result.fetchone())) + output.append(TypeAdapter(WalletGet).validate_python(result.fetchone())) yield output con.execute(wallets.delete()) diff --git a/services/web/server/tests/unit/with_dbs/03/folders/test_folders.py b/services/web/server/tests/unit/with_dbs/03/folders/test_folders.py index 7643afa7367..64aedb4c40b 100644 --- a/services/web/server/tests/unit/with_dbs/03/folders/test_folders.py +++ b/services/web/server/tests/unit/with_dbs/03/folders/test_folders.py @@ -58,7 +58,7 @@ async def test_folders_full_workflow( url = client.app.router["create_folder"].url_for() resp = await client.post(url.path, json={"name": "My first folder"}) added_folder, _ = await assert_status(resp, status.HTTP_201_CREATED) - assert FolderGet.parse_obj(added_folder) + assert FolderGet.model_validate(added_folder) # list user folders url = client.app.router["list_folders"].url_for() @@ -78,7 +78,7 @@ async def test_folders_full_workflow( ) resp = await client.get(url) data, _ = await assert_status(resp, status.HTTP_200_OK) - assert FolderGet.parse_obj(data) + assert FolderGet.model_validate(data) assert data["folderId"] == added_folder["folderId"] assert data["name"] == "My first folder" @@ -93,7 +93,7 @@ async def test_folders_full_workflow( }, ) data, _ = await assert_status(resp, status.HTTP_200_OK) - assert FolderGet.parse_obj(data) + assert FolderGet.model_validate(data) # list user folders url = client.app.router["list_folders"].url_for() @@ -206,7 +206,7 @@ async def test_sub_folders_full_workflow( }, ) data, _ = await assert_status(resp, status.HTTP_200_OK) - assert FolderGet.parse_obj(data) + assert FolderGet.model_validate(data) # list user root folders base_url = client.app.router["list_folders"].url_for() diff --git a/services/web/server/tests/unit/with_dbs/03/invitations/conftest.py b/services/web/server/tests/unit/with_dbs/03/invitations/conftest.py index ea792b8f726..d318e17a96f 100644 --- a/services/web/server/tests/unit/with_dbs/03/invitations/conftest.py +++ b/services/web/server/tests/unit/with_dbs/03/invitations/conftest.py @@ -66,7 +66,7 @@ def fake_osparc_invitation( Emulates an invitation for osparc product """ oas = deepcopy(invitations_service_openapi_specs) - content = ApiInvitationContent.parse_obj( + content = ApiInvitationContent.model_validate( oas["components"]["schemas"]["ApiInvitationContent"]["example"] ) content.product = "osparc" @@ -150,7 +150,7 @@ def _generate(url, **kwargs): return CallbackResult( status=status.HTTP_200_OK, payload=jsonable_encoder( - ApiInvitationContentAndLink.parse_obj( + ApiInvitationContentAndLink.model_validate( { **example, **body, diff --git a/services/web/server/tests/unit/with_dbs/03/invitations/test_login_handlers_registration_invitations.py b/services/web/server/tests/unit/with_dbs/03/invitations/test_login_handlers_registration_invitations.py index 7fa3ee144a7..be73ad487c9 100644 --- a/services/web/server/tests/unit/with_dbs/03/invitations/test_login_handlers_registration_invitations.py +++ b/services/web/server/tests/unit/with_dbs/03/invitations/test_login_handlers_registration_invitations.py @@ -52,7 +52,7 @@ async def test_check_registration_invitation_when_not_required( ) data, _ = await assert_status(response, status.HTTP_200_OK) - invitation = InvitationInfo.parse_obj(data) + invitation = InvitationInfo.model_validate(data) assert invitation.email is None @@ -74,7 +74,7 @@ async def test_check_registration_invitations_with_old_code( ) data, _ = await assert_status(response, status.HTTP_200_OK) - invitation = InvitationInfo.parse_obj(data) + invitation = InvitationInfo.model_validate(data) assert invitation.email is None @@ -100,7 +100,7 @@ async def test_check_registration_invitation_and_get_email( ) data, _ = await assert_status(response, status.HTTP_200_OK) - invitation = InvitationInfo.parse_obj(data) + invitation = InvitationInfo.model_validate(data) assert invitation.email == fake_osparc_invitation.guest diff --git a/services/web/server/tests/unit/with_dbs/03/invitations/test_products__invitations_handlers.py b/services/web/server/tests/unit/with_dbs/03/invitations/test_products__invitations_handlers.py index 0f8a85544f4..749fddb1548 100644 --- a/services/web/server/tests/unit/with_dbs/03/invitations/test_products__invitations_handlers.py +++ b/services/web/server/tests/unit/with_dbs/03/invitations/test_products__invitations_handlers.py @@ -56,7 +56,7 @@ async def test_role_access_to_generate_invitation( ) data, error = await assert_status(response, expected_status) if not error: - got = InvitationGenerated.parse_obj(data) + got = InvitationGenerated.model_validate(data) assert got.guest == guest_email else: assert error @@ -99,7 +99,7 @@ async def test_product_owner_generates_invitation( data, error = await assert_status(response, expected_status) assert not error - got = InvitationGenerated.parse_obj(data) + got = InvitationGenerated.model_validate(data) expected = { "issuer": logged_user["email"][:_MAX_LEN], **request_model.dict(exclude_none=True), @@ -186,7 +186,7 @@ async def test_pre_registration_and_invitation_workflow( response = await client.post("/v0/invitation:generate", json=invitation) data, _ = await assert_status(response, status.HTTP_200_OK) assert data["guest"] == guest_email - got_invitation = InvitationGenerated.parse_obj(data) + got_invitation = InvitationGenerated.model_validate(data) # register user assert got_invitation.invitation_link.fragment diff --git a/services/web/server/tests/unit/with_dbs/03/login/test_login_2fa_resend.py b/services/web/server/tests/unit/with_dbs/03/login/test_login_2fa_resend.py index 7139811a6b1..9ca5f6c6a29 100644 --- a/services/web/server/tests/unit/with_dbs/03/login/test_login_2fa_resend.py +++ b/services/web/server/tests/unit/with_dbs/03/login/test_login_2fa_resend.py @@ -6,7 +6,7 @@ import pytest import sqlalchemy as sa from aiohttp.test_utils import TestClient -from pydantic import parse_obj_as +from pydantic import TypeAdapter from pytest_mock import MockFixture from pytest_simcore.helpers.assert_checks import assert_status from pytest_simcore.helpers.monkeypatch_envs import EnvVarsDict, setenvs_from_dict @@ -106,7 +106,7 @@ async def test_resend_2fa_workflow( }, ) data, _ = await assert_status(response, status.HTTP_202_ACCEPTED) - next_page = parse_obj_as(NextPage[CodePageParams], data) + next_page = TypeAdapter(NextPage[CodePageParams]).validate_python(data) assert next_page.name == CODE_2FA_SMS_CODE_REQUIRED assert next_page.parameters.expiration_2fa > 0 diff --git a/services/web/server/tests/unit/with_dbs/03/login/test_login_registration.py b/services/web/server/tests/unit/with_dbs/03/login/test_login_registration.py index 61ade5ec24b..a3b51fe4d2c 100644 --- a/services/web/server/tests/unit/with_dbs/03/login/test_login_registration.py +++ b/services/web/server/tests/unit/with_dbs/03/login/test_login_registration.py @@ -494,7 +494,7 @@ async def test_registraton_with_invitation_for_trial_account( url = client.app.router["get_my_profile"].url_for() response = await client.get(url.path) data, _ = await assert_status(response, status.HTTP_200_OK) - profile = ProfileGet.parse_obj(data) + profile = ProfileGet.model_validate(data) expected = invitation.user["created_at"] + timedelta(days=TRIAL_DAYS) assert profile.expiration_date diff --git a/services/web/server/tests/unit/with_dbs/03/login/test_login_registration_handlers.py b/services/web/server/tests/unit/with_dbs/03/login/test_login_registration_handlers.py index 782d6bba93d..906350725e5 100644 --- a/services/web/server/tests/unit/with_dbs/03/login/test_login_registration_handlers.py +++ b/services/web/server/tests/unit/with_dbs/03/login/test_login_registration_handlers.py @@ -184,7 +184,7 @@ async def test_request_an_account( assert client.app # A form similar to the one in https://github.com/ITISFoundation/osparc-simcore/pull/5378 user_data = { - **AccountRequestInfo.Config.schema_extra["example"]["form"], + **AccountRequestInfo.model_config["json_schema_extra"]["example"]["form"], # fields required in the form "firstName": faker.first_name(), "lastName": faker.last_name(), diff --git a/services/web/server/tests/unit/with_dbs/03/meta_modeling/test_meta_modeling_iterations.py b/services/web/server/tests/unit/with_dbs/03/meta_modeling/test_meta_modeling_iterations.py index 20cb885bdfa..b01c9029f3b 100644 --- a/services/web/server/tests/unit/with_dbs/03/meta_modeling/test_meta_modeling_iterations.py +++ b/services/web/server/tests/unit/with_dbs/03/meta_modeling/test_meta_modeling_iterations.py @@ -131,7 +131,7 @@ async def test_iterators_workflow( project_id=project_data["uuid"] ) for node_id, node_data in modifications["workbench"].items(): - node = Node.parse_obj(node_data) + node = Node.model_validate(node_data) response = await client.post( f"{create_node_url}", json={ @@ -188,7 +188,7 @@ async def _mock_start(project_id, user_id, product_name, **options): f"/v0/projects/{project_uuid}/checkpoint/{head_ref_id}/iterations?offset=0" ) body = await response.json() - first_iterlist = Page[ProjectIterationItem].parse_obj(body).data + first_iterlist = Page[ProjectIterationItem].model_validate(body).data assert len(first_iterlist) == 3 @@ -231,7 +231,7 @@ async def _mock_catalog_get(*args, **kwarg): assert response.status == status.HTTP_200_OK, await response.text() body = await response.json() - assert Page[ProjectIterationResultItem].parse_obj(body).data is not None + assert Page[ProjectIterationResultItem].model_validate(body).data is not None # GET project and MODIFY iterator values---------------------------------------------- # - Change iterations from 0:4 -> HEAD+1 @@ -245,7 +245,7 @@ async def _mock_catalog_get(*args, **kwarg): # Dict keys are usually some sort of identifier, typically a UUID or # and index but nothing prevents a dict from using any other type of key types # - project = Project.parse_obj(body["data"]) + project = Project.model_validate(body["data"]) new_project = project.copy( update={ # TODO: HACK to overcome export from None -> string @@ -290,7 +290,7 @@ async def _mock_catalog_get(*args, **kwarg): ) body = await response.json() assert response.status == status.HTTP_200_OK, f"{body=}" # nosec - second_iterlist = Page[ProjectIterationItem].parse_obj(body).data + second_iterlist = Page[ProjectIterationItem].model_validate(body).data assert len(second_iterlist) == 4 assert len({it.workcopy_project_id for it in second_iterlist}) == len( diff --git a/services/web/server/tests/unit/with_dbs/03/products/test_products_rpc.py b/services/web/server/tests/unit/with_dbs/03/products/test_products_rpc.py index 3de1f7a95c8..334eaeae93d 100644 --- a/services/web/server/tests/unit/with_dbs/03/products/test_products_rpc.py +++ b/services/web/server/tests/unit/with_dbs/03/products/test_products_rpc.py @@ -10,7 +10,7 @@ from models_library.api_schemas_webserver import WEBSERVER_RPC_NAMESPACE from models_library.products import CreditResultGet, ProductName from models_library.rabbitmq_basic_types import RPCMethodName -from pydantic import parse_obj_as +from pydantic import TypeAdapter from pytest_mock import MockerFixture from pytest_simcore.helpers.monkeypatch_envs import setenvs_from_dict from pytest_simcore.helpers.typing_env import EnvVarsDict @@ -70,26 +70,26 @@ async def test_get_credit_amount( ): result = await rpc_client.request( WEBSERVER_RPC_NAMESPACE, - parse_obj_as(RPCMethodName, "get_credit_amount"), + TypeAdapter(RPCMethodName).validate_python("get_credit_amount"), dollar_amount=Decimal(900), product_name="s4l", ) - credit_result = parse_obj_as(CreditResultGet, result) + credit_result = TypeAdapter(CreditResultGet).validate_python(result) assert credit_result.credit_amount == 100 result = await rpc_client.request( WEBSERVER_RPC_NAMESPACE, - parse_obj_as(RPCMethodName, "get_credit_amount"), + TypeAdapter(RPCMethodName).validate_python("get_credit_amount"), dollar_amount=Decimal(900), product_name="tis", ) - credit_result = parse_obj_as(CreditResultGet, result) + credit_result = TypeAdapter(CreditResultGet).validate_python(result) assert credit_result.credit_amount == 180 with pytest.raises(RPCServerError) as exc_info: await rpc_client.request( WEBSERVER_RPC_NAMESPACE, - parse_obj_as(RPCMethodName, "get_credit_amount"), + TypeAdapter(RPCMethodName).validate_python("get_credit_amount"), dollar_amount=Decimal(900), product_name="osparc", ) diff --git a/services/web/server/tests/unit/with_dbs/03/resource_usage/test_admin_pricing_plans.py b/services/web/server/tests/unit/with_dbs/03/resource_usage/test_admin_pricing_plans.py index 6e67883e357..f6a7f20c687 100644 --- a/services/web/server/tests/unit/with_dbs/03/resource_usage/test_admin_pricing_plans.py +++ b/services/web/server/tests/unit/with_dbs/03/resource_usage/test_admin_pricing_plans.py @@ -17,7 +17,7 @@ PricingUnitGet, ) from models_library.resource_tracker import PricingPlanClassification -from pydantic import parse_obj_as +from pydantic import TypeAdapter from pytest_mock import MockerFixture from pytest_simcore.helpers.assert_checks import assert_status from pytest_simcore.helpers.webserver_login import UserInfoDict @@ -35,52 +35,52 @@ def mock_rpc_resource_usage_tracker_service_api( "simcore_service_webserver.resource_usage._pricing_plans_admin_api.pricing_plans.list_pricing_plans", autospec=True, return_value=[ - parse_obj_as( - PricingPlanGet, PricingPlanGet.Config.schema_extra["examples"][0] + TypeAdapter(PricingPlanGet).validate_python( + PricingPlanGet.model_config["json_schema_extra"]["examples"][0], ) ], ), "get_pricing_plan": mocker.patch( "simcore_service_webserver.resource_usage._pricing_plans_admin_api.pricing_plans.get_pricing_plan", autospec=True, - return_value=parse_obj_as( - PricingPlanGet, PricingPlanGet.Config.schema_extra["examples"][0] + return_value=TypeAdapter(PricingPlanGet).validate_python( + PricingPlanGet.model_config["json_schema_extra"]["examples"][0], ), ), "create_pricing_plan": mocker.patch( "simcore_service_webserver.resource_usage._pricing_plans_admin_api.pricing_plans.create_pricing_plan", autospec=True, - return_value=parse_obj_as( - PricingPlanGet, PricingPlanGet.Config.schema_extra["examples"][0] + return_value=TypeAdapter(PricingPlanGet).validate_python( + PricingPlanGet.model_config["json_schema_extra"]["examples"][0], ), ), "update_pricing_plan": mocker.patch( "simcore_service_webserver.resource_usage._pricing_plans_admin_api.pricing_plans.update_pricing_plan", autospec=True, - return_value=parse_obj_as( - PricingPlanGet, PricingPlanGet.Config.schema_extra["examples"][0] + return_value=TypeAdapter(PricingPlanGet).validate_python( + PricingPlanGet.model_config["json_schema_extra"]["examples"][0], ), ), ## Pricing units "get_pricing_unit": mocker.patch( "simcore_service_webserver.resource_usage._pricing_plans_admin_api.pricing_units.get_pricing_unit", autospec=True, - return_value=parse_obj_as( - PricingUnitGet, PricingUnitGet.Config.schema_extra["examples"][0] + return_value=TypeAdapter(PricingUnitGet).validate_python( + PricingUnitGet.model_config["json_schema_extra"]["examples"][0], ), ), "create_pricing_unit": mocker.patch( "simcore_service_webserver.resource_usage._pricing_plans_admin_api.pricing_units.create_pricing_unit", autospec=True, - return_value=parse_obj_as( - PricingUnitGet, PricingUnitGet.Config.schema_extra["examples"][0] + return_value=TypeAdapter(PricingUnitGet).validate_python( + PricingUnitGet.model_config["json_schema_extra"]["examples"][0], ), ), "update_pricing_unit": mocker.patch( "simcore_service_webserver.resource_usage._pricing_plans_admin_api.pricing_units.update_pricing_unit", autospec=True, - return_value=parse_obj_as( - PricingUnitGet, PricingUnitGet.Config.schema_extra["examples"][0] + return_value=TypeAdapter(PricingUnitGet).validate_python( + PricingUnitGet.model_config["json_schema_extra"]["examples"][0], ), ), ## Pricing plan to service @@ -88,18 +88,20 @@ def mock_rpc_resource_usage_tracker_service_api( "simcore_service_webserver.resource_usage._pricing_plans_admin_api.pricing_plans.list_connected_services_to_pricing_plan_by_pricing_plan", autospec=True, return_value=[ - parse_obj_as( - PricingPlanToServiceGet, - PricingPlanToServiceGet.Config.schema_extra["examples"][0], + TypeAdapter(PricingPlanToServiceGet).validate_python( + PricingPlanToServiceGet.model_config["json_schema_extra"][ + "examples" + ][0], ) ], ), "connect_service_to_pricing_plan": mocker.patch( "simcore_service_webserver.resource_usage._pricing_plans_admin_api.pricing_plans.connect_service_to_pricing_plan", autospec=True, - return_value=parse_obj_as( - PricingPlanToServiceGet, - PricingPlanToServiceGet.Config.schema_extra["examples"][0], + return_value=TypeAdapter(PricingPlanToServiceGet).validate_python( + PricingPlanToServiceGet.model_config["json_schema_extra"]["examples"][ + 0 + ], ), ), } diff --git a/services/web/server/tests/unit/with_dbs/03/resource_usage/test_pricing_plans.py b/services/web/server/tests/unit/with_dbs/03/resource_usage/test_pricing_plans.py index 7b25e33a799..fea4640bf57 100644 --- a/services/web/server/tests/unit/with_dbs/03/resource_usage/test_pricing_plans.py +++ b/services/web/server/tests/unit/with_dbs/03/resource_usage/test_pricing_plans.py @@ -15,7 +15,7 @@ PricingUnitGet, ) from models_library.utils.fastapi_encoders import jsonable_encoder -from pydantic import parse_obj_as +from pydantic import TypeAdapter from pytest_simcore.aioresponses_mocker import AioResponsesMock from pytest_simcore.helpers.assert_checks import assert_status from pytest_simcore.helpers.webserver_login import UserInfoDict @@ -32,13 +32,12 @@ def mock_rut_api_responses( assert client.app settings: ResourceUsageTrackerSettings = get_plugin_settings(client.app) - pricing_unit_get = parse_obj_as( - PricingUnitGet, PricingUnitGet.Config.schema_extra["examples"][0] + pricing_unit_get = TypeAdapter(PricingUnitGet).validate_python( + PricingUnitGet.model_config["json_schema_extra"]["examples"][0] ) - service_pricing_plan_get = parse_obj_as( - PricingPlanGet, - PricingPlanGet.Config.schema_extra["examples"][0], + service_pricing_plan_get = TypeAdapter(PricingPlanGet).validate_python( + PricingPlanGet.model_config["json_schema_extra"]["examples"][0], ) aioresponses_mocker.get( diff --git a/services/web/server/tests/unit/with_dbs/03/resource_usage/test_usage_services__export.py b/services/web/server/tests/unit/with_dbs/03/resource_usage/test_usage_services__export.py index 6a80bccca0d..03116b3abea 100644 --- a/services/web/server/tests/unit/with_dbs/03/resource_usage/test_usage_services__export.py +++ b/services/web/server/tests/unit/with_dbs/03/resource_usage/test_usage_services__export.py @@ -16,7 +16,7 @@ from aiohttp.test_utils import TestClient from models_library.resource_tracker import ServiceResourceUsagesFilters from models_library.rest_ordering import OrderBy -from pydantic import AnyUrl, parse_obj_as +from pydantic import AnyUrl, TypeAdapter from pytest_mock.plugin import MockerFixture from pytest_simcore.helpers.webserver_login import UserInfoDict from servicelib.aiohttp import status @@ -29,7 +29,7 @@ def mock_export_usage_services(mocker: MockerFixture) -> MagicMock: return mocker.patch( "simcore_service_webserver.resource_usage._service_runs_api.service_runs.export_service_runs", spec=True, - return_value=parse_obj_as(AnyUrl, "https://www.google.com/"), + return_value=TypeAdapter(AnyUrl).validate_python("https://www.google.com/"), ) @@ -115,5 +115,7 @@ async def test_list_service_usage( assert mock_export_usage_services.called args = mock_export_usage_services.call_args[1] - assert args["order_by"] == parse_obj_as(OrderBy, _order_by) - assert args["filters"] == parse_obj_as(ServiceResourceUsagesFilters, _filter) + assert args["order_by"] == TypeAdapter(OrderBy).validate_python(_order_by) + assert args["filters"] == TypeAdapter(ServiceResourceUsagesFilters).validate_python( + _filter + ) diff --git a/services/web/server/tests/unit/with_dbs/03/test_email.py b/services/web/server/tests/unit/with_dbs/03/test_email.py index e2164071c16..d6f7a050239 100644 --- a/services/web/server/tests/unit/with_dbs/03/test_email.py +++ b/services/web/server/tests/unit/with_dbs/03/test_email.py @@ -136,9 +136,9 @@ async def test_email_handlers( assert error is None with pytest.raises(ValidationError): - EmailTestFailed.parse_obj(data) + EmailTestFailed.model_validate(data) - passed = EmailTestPassed.parse_obj(data) + passed = EmailTestPassed.model_validate(data) print(passed.json(indent=1)) diff --git a/services/web/server/tests/unit/with_dbs/03/test_storage_handlers.py b/services/web/server/tests/unit/with_dbs/03/test_storage_handlers.py index 28e7d0590c9..1f2247d6947 100644 --- a/services/web/server/tests/unit/with_dbs/03/test_storage_handlers.py +++ b/services/web/server/tests/unit/with_dbs/03/test_storage_handlers.py @@ -17,7 +17,7 @@ FileUploadLinks, FileUploadSchema, ) -from pydantic import AnyUrl, ByteSize, parse_obj_as +from pydantic import AnyUrl, ByteSize, TypeAdapter from pytest_mock import MockerFixture from pytest_simcore.helpers.monkeypatch_envs import setenvs_from_dict from pytest_simcore.helpers.typing_env import EnvVarsDict @@ -59,7 +59,7 @@ async def _resp(*args, **kwargs) -> tuple[Any, int]: ) def _resolve(*args, **kwargs) -> AnyUrl: - return parse_obj_as(AnyUrl, "http://private-url") + return TypeAdapter(AnyUrl).validate_python("http://private-url") mocker.patch( "simcore_service_webserver.storage._handlers._from_storage_url", @@ -69,16 +69,19 @@ def _resolve(*args, **kwargs) -> AnyUrl: MOCK_FILE_UPLOAD_SCHEMA = FileUploadSchema( - chunk_size=parse_obj_as(ByteSize, "5GiB"), - urls=[parse_obj_as(AnyUrl, "s3://file_id")], + chunk_size=TypeAdapter(ByteSize).validate_python("5GiB"), + urls=[TypeAdapter(AnyUrl).validate_python("s3://file_id")], links=FileUploadLinks( - abort_upload=parse_obj_as(AnyUrl, "http://private-url/operation:abort"), - complete_upload=parse_obj_as(AnyUrl, "http://private-url/operation:complete"), + abort_upload=TypeAdapter(AnyUrl).validate_python( + "http://private-url/operation:abort" + ), + complete_upload=TypeAdapter(AnyUrl).validate_python( + "http://private-url/operation:complete" + ), ), ) -MOCK_FILE_UPLOAD_SCHEMA = parse_obj_as( - FileUploadSchema, +MOCK_FILE_UPLOAD_SCHEMA = TypeAdapter(FileUploadSchema).validate_python( { "chunk_size": "5", "urls": ["s3://file_id"], @@ -90,9 +93,9 @@ def _resolve(*args, **kwargs) -> AnyUrl: ) -MOCK_FILE_UPLOAD_COMPLETE_RESPONSE = parse_obj_as( - FileUploadCompleteResponse, {"links": {"state": "http://private-url"}} -) +MOCK_FILE_UPLOAD_COMPLETE_RESPONSE = TypeAdapter( + FileUploadCompleteResponse +).validate_python({"links": {"state": "http://private-url"}}) DOUBLE_ENCODE_SLASH_IN_FILE_ID = "ef944bbe-14c7-11ee-a195-02420a0f07ab%252F46ac4913-92dc-432c-98e3-2dea21d3f0ed%252Fa_text_file.txt" @@ -145,14 +148,14 @@ def _resolve(*args, **kwargs) -> AnyUrl: "POST", "/v0/storage/locations/0/files/{file_id}:complete", {"parts": []}, - json.loads(MOCK_FILE_UPLOAD_COMPLETE_RESPONSE.json()), + json.loads(MOCK_FILE_UPLOAD_COMPLETE_RESPONSE.model_dump_json()), id="complete_upload_file", ), pytest.param( "POST", "/v0/storage/locations/0/files/{file_id}:complete/futures/RANDOM_FUTURE_ID", None, - json.loads(MOCK_FILE_UPLOAD_SCHEMA.json()), + json.loads(MOCK_FILE_UPLOAD_SCHEMA.model_dump_json()), id="is_completed_upload_file", ), ], @@ -208,7 +211,7 @@ def test_url_storage_resolver_helpers(faker: Faker, app_environment: EnvVarsDict # storage -> web web_url: AnyUrl = _from_storage_url( - web_request, parse_obj_as(AnyUrl, str(storage_url)) + web_request, TypeAdapter(AnyUrl).validate_python(str(storage_url)) ) assert storage_url.host != web_url.host diff --git a/services/web/server/tests/unit/with_dbs/03/test_users.py b/services/web/server/tests/unit/with_dbs/03/test_users.py index 80c0c7912af..d37cbd8f0e2 100644 --- a/services/web/server/tests/unit/with_dbs/03/test_users.py +++ b/services/web/server/tests/unit/with_dbs/03/test_users.py @@ -82,12 +82,12 @@ async def test_get_profile( data, error = await assert_status(resp, expected) # check enveloped - e = Envelope[ProfileGet].parse_obj(await resp.json()) + e = Envelope[ProfileGet].model_validate(await resp.json()) assert e.error == error assert e.data.dict(**RESPONSE_MODEL_POLICY) == data if e.data else e.data == data if not error: - profile = ProfileGet.parse_obj(data) + profile = ProfileGet.model_validate(data) product_group = { "accessRights": {"delete": False, "read": False, "write": False}, @@ -250,7 +250,9 @@ def account_request_form(faker: Faker) -> dict[str, Any]: } # keeps in sync fields from example and this fixture - assert set(form) == set(AccountRequestInfo.Config.schema_extra["example"]["form"]) + assert set(form) == set( + AccountRequestInfo.model_config["json_schema_extra"]["example"]["form"] + ) return form diff --git a/services/web/server/tests/unit/with_dbs/03/test_users__notifications.py b/services/web/server/tests/unit/with_dbs/03/test_users__notifications.py index 77aaccade51..49b43ec2229 100644 --- a/services/web/server/tests/unit/with_dbs/03/test_users__notifications.py +++ b/services/web/server/tests/unit/with_dbs/03/test_users__notifications.py @@ -18,7 +18,7 @@ import redis.asyncio as aioredis from aiohttp.test_utils import TestClient from models_library.products import ProductName -from pydantic import parse_obj_as +from pydantic import TypeAdapter from pytest_simcore.helpers.assert_checks import assert_status from pytest_simcore.helpers.monkeypatch_envs import EnvVarsDict, setenvs_from_dict from pytest_simcore.helpers.webserver_login import UserInfoDict @@ -71,7 +71,7 @@ def _create_notification( notification_categories = tuple(NotificationCategory) notification: UserNotification = UserNotification.create_from_request_data( - UserNotificationCreate.parse_obj( + UserNotificationCreate.model_validate( { "user_id": user_id, "category": random.choice(notification_categories), @@ -154,7 +154,9 @@ async def test_list_user_notifications( response = await client.get(url.path) json_response = await response.json() - result = parse_obj_as(list[UserNotification], json_response["data"]) + result = TypeAdapter(list[UserNotification]).validate_python( + json_response["data"] + ) assert len(result) <= MAX_NOTIFICATIONS_FOR_USER_TO_SHOW assert result == list( reversed(created_notifications[:MAX_NOTIFICATIONS_FOR_USER_TO_SHOW]) @@ -444,7 +446,7 @@ async def test_list_permissions( data, error = await assert_status(resp, expected_response) if data: assert not error - list_of_permissions = parse_obj_as(list[PermissionGet], data) + list_of_permissions = TypeAdapter(list[PermissionGet]).validate_python(data) assert ( len(list_of_permissions) == 1 ), "for now there is only 1 permission, but when we sync frontend/backend permissions there will be more" @@ -475,7 +477,7 @@ async def test_list_permissions_with_overriden_extra_properties( data, error = await assert_status(resp, expected_response) assert data assert not error - list_of_permissions = parse_obj_as(list[PermissionGet], data) + list_of_permissions = TypeAdapter(list[PermissionGet]).validate_python(data) filtered_permissions = list( filter( lambda x: x.name == "override_services_specifications", list_of_permissions diff --git a/services/web/server/tests/unit/with_dbs/03/version_control/conftest.py b/services/web/server/tests/unit/with_dbs/03/version_control/conftest.py index 7343176760e..5843d32408e 100644 --- a/services/web/server/tests/unit/with_dbs/03/version_control/conftest.py +++ b/services/web/server/tests/unit/with_dbs/03/version_control/conftest.py @@ -207,7 +207,7 @@ async def _go(client: TestClient, project_uuid: UUID) -> None: # add a node node_id = faker.uuid4() - node = Node.parse_obj( + node = Node.model_validate( { "key": f"simcore/services/comp/test_{__name__}", "version": "1.0.0", diff --git a/services/web/server/tests/unit/with_dbs/03/version_control/test_version_control.py b/services/web/server/tests/unit/with_dbs/03/version_control/test_version_control.py index ed04b3728e2..eee53fc8b99 100644 --- a/services/web/server/tests/unit/with_dbs/03/version_control/test_version_control.py +++ b/services/web/server/tests/unit/with_dbs/03/version_control/test_version_control.py @@ -4,16 +4,16 @@ from models_library.projects import NodesDict -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict from simcore_service_webserver.projects.models import ProjectDict from simcore_service_webserver.version_control.db import compute_workbench_checksum class WorkbenchModel(BaseModel): __root__: NodesDict - - class Config: - allow_population_by_field_name = True + model_config = ConfigDict( + populate_by_name=True, + ) def test_compute_workbench_checksum(fake_project: ProjectDict): @@ -21,7 +21,7 @@ def test_compute_workbench_checksum(fake_project: ProjectDict): # as a dict sha1_w_dict = compute_workbench_checksum(fake_project["workbench"]) - workbench = WorkbenchModel.parse_obj(fake_project["workbench"]) + workbench = WorkbenchModel.model_validate(fake_project["workbench"]) # with pydantic models, i.e. Nodes # diff --git a/services/web/server/tests/unit/with_dbs/03/version_control/test_version_control_handlers.py b/services/web/server/tests/unit/with_dbs/03/version_control/test_version_control_handlers.py index 05ab31ccdf8..078ba287a5e 100644 --- a/services/web/server/tests/unit/with_dbs/03/version_control/test_version_control_handlers.py +++ b/services/web/server/tests/unit/with_dbs/03/version_control/test_version_control_handlers.py @@ -32,7 +32,7 @@ async def assert_resp_page( assert resp.status == status.HTTP_200_OK, f"Got {await resp.text()}" body = await resp.json() - page = expected_page_cls.parse_obj(body) + page = expected_page_cls.model_validate(body) assert page.meta.total == expected_total assert page.meta.count == expected_count return page @@ -42,7 +42,7 @@ async def assert_status_and_body( resp, expected_cls: HTTPStatus, expected_model: type[BaseModel] ) -> BaseModel: data, _ = await assert_status(resp, expected_cls) - return expected_model.parse_obj(data) + return expected_model.model_validate(data) @pytest.mark.acceptance_test() @@ -59,7 +59,7 @@ async def test_workflow( # get existing project resp = await client.get(f"/{VX}/projects/{project_uuid}") data, _ = await assert_status(resp, status.HTTP_200_OK) - project = Project.parse_obj(data) + project = Project.model_validate(data) assert project.uuid == UUID(project_uuid) # @@ -78,7 +78,7 @@ async def test_workflow( data, _ = await assert_status(resp, status.HTTP_201_CREATED) assert data - checkpoint1 = CheckpointApiModel.parse_obj(data) # NOTE: this is NOT API model + checkpoint1 = CheckpointApiModel.model_validate(data) # NOTE: this is NOT API model # # this project now has a repo @@ -87,20 +87,20 @@ async def test_workflow( resp, expected_page_cls=Page[ProjectDict], expected_total=1, expected_count=1 ) - repo = RepoApiModel.parse_obj(page.data[0]) + repo = RepoApiModel.model_validate(page.data[0]) assert repo.project_uuid == UUID(project_uuid) # GET checkpoint with HEAD resp = await client.get(f"/{VX}/repos/projects/{project_uuid}/checkpoints/HEAD") data, _ = await assert_status(resp, status.HTTP_200_OK) - assert CheckpointApiModel.parse_obj(data) == checkpoint1 + assert CheckpointApiModel.model_validate(data) == checkpoint1 # TODO: GET checkpoint with tag with pytest.raises(aiohttp.ClientResponseError) as excinfo: resp = await client.get(f"/{VX}/repos/projects/{project_uuid}/checkpoints/v1") resp.raise_for_status() - assert CheckpointApiModel.parse_obj(data) == checkpoint1 + assert CheckpointApiModel.model_validate(data) == checkpoint1 assert excinfo.value.status == status.HTTP_501_NOT_IMPLEMENTED @@ -109,7 +109,7 @@ async def test_workflow( f"/{VX}/repos/projects/{project_uuid}/checkpoints/{checkpoint1.id}" ) assert str(resp.url) == checkpoint1.url - assert CheckpointApiModel.parse_obj(data) == checkpoint1 + assert CheckpointApiModel.model_validate(data) == checkpoint1 # LIST checkpoints resp = await client.get(f"/{VX}/repos/projects/{project_uuid}/checkpoints") @@ -120,7 +120,7 @@ async def test_workflow( expected_count=1, ) - assert CheckpointApiModel.parse_obj(page.data[0]) == checkpoint1 + assert CheckpointApiModel.model_validate(page.data[0]) == checkpoint1 # UPDATE checkpoint annotations resp = await client.patch( @@ -128,7 +128,7 @@ async def test_workflow( json={"message": "updated message", "tag": "Version 1"}, ) data, _ = await assert_status(resp, status.HTTP_200_OK) - checkpoint1_updated = CheckpointApiModel.parse_obj(data) + checkpoint1_updated = CheckpointApiModel.model_validate(data) assert checkpoint1.id == checkpoint1_updated.id assert checkpoint1.checksum == checkpoint1_updated.checksum @@ -154,30 +154,30 @@ async def test_workflow( json={"tag": "v2", "message": "new commit"}, ) data, _ = await assert_status(resp, status.HTTP_201_CREATED) - checkpoint2 = CheckpointApiModel.parse_obj(data) + checkpoint2 = CheckpointApiModel.model_validate(data) assert checkpoint2.tags == ("v2",) # GET checkpoint with HEAD resp = await client.get(f"/{VX}/repos/projects/{project_uuid}/checkpoints/HEAD") data, _ = await assert_status(resp, status.HTTP_200_OK) - assert CheckpointApiModel.parse_obj(data) == checkpoint2 + assert CheckpointApiModel.model_validate(data) == checkpoint2 # CHECKOUT resp = await client.post( f"/{VX}/repos/projects/{project_uuid}/checkpoints/{checkpoint1.id}:checkout" ) data, _ = await assert_status(resp, status.HTTP_200_OK) - assert CheckpointApiModel.parse_obj(data) == checkpoint1_updated + assert CheckpointApiModel.model_validate(data) == checkpoint1_updated # GET checkpoint with HEAD resp = await client.get(f"/{VX}/repos/projects/{project_uuid}/checkpoints/HEAD") data, _ = await assert_status(resp, status.HTTP_200_OK) - assert CheckpointApiModel.parse_obj(data) == checkpoint1_updated + assert CheckpointApiModel.model_validate(data) == checkpoint1_updated # get working copy resp = await client.get(f"/{VX}/projects/{project_uuid}") data, _ = await assert_status(resp, status.HTTP_200_OK) - project_wc = Project.parse_obj(data) + project_wc = Project.model_validate(data) assert project_wc.uuid == UUID(project_uuid) assert project_wc != project @@ -193,7 +193,7 @@ async def test_create_checkpoint_without_changes( data, _ = await assert_status(resp, status.HTTP_201_CREATED) assert data - checkpoint1 = CheckpointApiModel.parse_obj(data) # NOTE: this is NOT API model + checkpoint1 = CheckpointApiModel.model_validate(data) # NOTE: this is NOT API model # CREATE checkpoint WITHOUT changes resp = await client.post( @@ -203,7 +203,7 @@ async def test_create_checkpoint_without_changes( data, _ = await assert_status(resp, status.HTTP_201_CREATED) assert data - checkpoint2 = CheckpointApiModel.parse_obj(data) # NOTE: this is NOT API model + checkpoint2 = CheckpointApiModel.model_validate(data) # NOTE: this is NOT API model assert ( checkpoint1 == checkpoint2 diff --git a/services/web/server/tests/unit/with_dbs/03/wallets/payments/conftest.py b/services/web/server/tests/unit/with_dbs/03/wallets/payments/conftest.py index 7e83d7fba7a..d63bbf31397 100644 --- a/services/web/server/tests/unit/with_dbs/03/wallets/payments/conftest.py +++ b/services/web/server/tests/unit/with_dbs/03/wallets/payments/conftest.py @@ -79,7 +79,7 @@ async def _create(): }, ) data, _ = await assert_status(resp, status.HTTP_201_CREATED) - return WalletGet.parse_obj(data) + return WalletGet.model_validate(data) return _create diff --git a/services/web/server/tests/unit/with_dbs/03/wallets/payments/test_payments.py b/services/web/server/tests/unit/with_dbs/03/wallets/payments/test_payments.py index f6519735ed1..244f9e4f3ca 100644 --- a/services/web/server/tests/unit/with_dbs/03/wallets/payments/test_payments.py +++ b/services/web/server/tests/unit/with_dbs/03/wallets/payments/test_payments.py @@ -10,6 +10,7 @@ import pytest from aiohttp.test_utils import TestClient +from common_library.pydantic_fields_extension import get_type from faker import Faker from models_library.api_schemas_webserver.wallets import ( PaymentTransaction, @@ -17,7 +18,7 @@ WalletPaymentInitiated, ) from models_library.rest_pagination import Page -from pydantic import parse_obj_as +from pydantic import TypeAdapter from pytest_mock import MockerFixture from pytest_simcore.helpers.assert_checks import assert_status from pytest_simcore.helpers.webserver_login import LoggedUser, NewUser, UserInfoDict @@ -105,7 +106,7 @@ async def test_one_time_payment_worfklow( data, error = await assert_status(response, expected_status) if not error: - payment = WalletPaymentInitiated.parse_obj(data) + payment = WalletPaymentInitiated.model_validate(data) assert payment.payment_id assert payment.payment_form_url @@ -134,7 +135,7 @@ async def test_one_time_payment_worfklow( response = await client.get("/v0/wallets/-/payments") data, error = await assert_status(response, status.HTTP_200_OK) - page = parse_obj_as(Page[PaymentTransaction], data) + page = TypeAdapter(Page[PaymentTransaction]).validate_python(data) assert page.data assert page.meta.total == 1 @@ -200,7 +201,7 @@ async def test_multiple_payments( data, error = await assert_status(response, status.HTTP_201_CREATED) assert data assert not error - payment = WalletPaymentInitiated.parse_obj(data) + payment = WalletPaymentInitiated.model_validate(data) if n % 2: transaction = await _ack_creation_of_wallet_payment( @@ -233,7 +234,7 @@ async def test_multiple_payments( response = await client.get("/v0/wallets/-/payments") data, error = await assert_status(response, status.HTTP_200_OK) - page = parse_obj_as(Page[PaymentTransaction], data) + page = TypeAdapter(Page[PaymentTransaction]).validate_python(data) assert page.meta.total == num_payments all_transactions = {t.payment_id: t for t in page.data} @@ -286,7 +287,7 @@ async def test_complete_payment_errors( assert mock_rpc_payments_service_api["init_payment"].called data, _ = await assert_status(response, status.HTTP_201_CREATED) - payment = WalletPaymentInitiated.parse_obj(data) + payment = WalletPaymentInitiated.model_validate(data) # Cannot complete as PENDING with pytest.raises(ValueError): @@ -362,9 +363,11 @@ async def test_payment_not_found( def test_payment_transaction_state_and_literals_are_in_sync(): - state_literals = PaymentTransaction.__fields__["state"].type_ + state_literals = get_type(PaymentTransaction.model_fields["state"]) assert ( - parse_obj_as(list[state_literals], [f"{s}" for s in PaymentTransactionState]) + TypeAdapter(list[state_literals]).validate_python( + [f"{s}" for s in PaymentTransactionState] + ) is not None ) diff --git a/services/web/server/tests/unit/with_dbs/03/wallets/payments/test_payments_methods.py b/services/web/server/tests/unit/with_dbs/03/wallets/payments/test_payments_methods.py index 0980e45caa2..e9153608265 100644 --- a/services/web/server/tests/unit/with_dbs/03/wallets/payments/test_payments_methods.py +++ b/services/web/server/tests/unit/with_dbs/03/wallets/payments/test_payments_methods.py @@ -22,7 +22,7 @@ ) from models_library.rest_pagination import Page from models_library.wallets import WalletID -from pydantic import parse_obj_as +from pydantic import TypeAdapter from pytest_mock import MockerFixture from pytest_simcore.helpers.assert_checks import assert_status from servicelib.aiohttp import status @@ -65,7 +65,7 @@ async def test_payment_method_worfklow( ) data, error = await assert_status(response, status.HTTP_202_ACCEPTED) assert error is None - inited = PaymentMethodInitiated.parse_obj(data) + inited = PaymentMethodInitiated.model_validate(data) assert inited.payment_method_id assert inited.payment_method_form_url.query @@ -103,7 +103,7 @@ async def test_payment_method_worfklow( data, _ = await assert_status(response, status.HTTP_200_OK) assert mock_rpc_payments_service_api["list_payment_methods"].called - wallet_payments_methods = parse_obj_as(list[PaymentMethodGet], data) + wallet_payments_methods = TypeAdapter(list[PaymentMethodGet]).validate_python(data) assert wallet_payments_methods == [payment_method] # Delete @@ -140,7 +140,7 @@ async def test_init_and_cancel_payment_method( ) data, error = await assert_status(response, status.HTTP_202_ACCEPTED) assert error is None - inited = PaymentMethodInitiated.parse_obj(data) + inited = PaymentMethodInitiated.model_validate(data) # cancel Create response = await client.post( @@ -165,7 +165,7 @@ async def _add_payment_method( ) data, error = await assert_status(response, status.HTTP_202_ACCEPTED) assert error is None - inited = PaymentMethodInitiated.parse_obj(data) + inited = PaymentMethodInitiated.model_validate(data) await _ack_creation_of_wallet_payment_method( client.app, payment_method_id=inited.payment_method_id, @@ -249,7 +249,7 @@ async def test_wallet_autorecharge( ) data, error = await assert_status(response, expected_status) if not error: - updated_auto_recharge = GetWalletAutoRecharge.parse_obj(data) + updated_auto_recharge = GetWalletAutoRecharge.model_validate(data) assert updated_auto_recharge == GetWalletAutoRecharge( payment_method_id=payment_method_id, min_balance_in_credits=settings.PAYMENTS_AUTORECHARGE_MIN_BALANCE_IN_CREDITS, @@ -263,12 +263,14 @@ async def test_wallet_autorecharge( f"/v0/wallets/{wallet.wallet_id}/auto-recharge", ) data, _ = await assert_status(response, status.HTTP_200_OK) - assert updated_auto_recharge == GetWalletAutoRecharge.parse_obj(data) + assert updated_auto_recharge == GetWalletAutoRecharge.model_validate(data) # payment-methods.auto_recharge response = await client.get(f"/v0/wallets/{wallet.wallet_id}/payments-methods") data, _ = await assert_status(response, status.HTTP_200_OK) - wallet_payment_methods = parse_obj_as(list[PaymentMethodGet], data) + wallet_payment_methods = TypeAdapter(list[PaymentMethodGet]).validate_python( + data + ) for payment_method in wallet_payment_methods: assert payment_method.auto_recharge == ( @@ -305,7 +307,7 @@ async def test_delete_primary_payment_method_in_autorecharge( }, ) data, _ = await assert_status(response, status.HTTP_200_OK) - auto_recharge = GetWalletAutoRecharge.parse_obj(data) + auto_recharge = GetWalletAutoRecharge.model_validate(data) assert auto_recharge.enabled is True assert auto_recharge.payment_method_id == payment_method_id assert auto_recharge.monthly_limit_in_usd == 123 @@ -321,7 +323,7 @@ async def test_delete_primary_payment_method_in_autorecharge( f"/v0/wallets/{wallet.wallet_id}/auto-recharge", ) data, _ = await assert_status(response, status.HTTP_200_OK) - auto_recharge_after_delete = GetWalletAutoRecharge.parse_obj(data) + auto_recharge_after_delete = GetWalletAutoRecharge.model_validate(data) assert auto_recharge_after_delete.payment_method_id is None assert auto_recharge_after_delete.enabled is False @@ -334,7 +336,7 @@ async def test_delete_primary_payment_method_in_autorecharge( f"/v0/wallets/{wallet.wallet_id}/auto-recharge", ) data, _ = await assert_status(response, status.HTTP_200_OK) - auto_recharge = GetWalletAutoRecharge.parse_obj(data) + auto_recharge = GetWalletAutoRecharge.model_validate(data) assert auto_recharge.payment_method_id == new_payment_method_id assert auto_recharge.enabled is False @@ -398,7 +400,7 @@ async def test_one_time_payment_with_payment_method( ) data, error = await assert_status(response, expected_status) if not error: - payment = WalletPaymentInitiated.parse_obj(data) + payment = WalletPaymentInitiated.model_validate(data) assert mock_rpc_payments_service_api["pay_with_payment_method"].called assert payment.payment_id @@ -417,7 +419,7 @@ async def test_one_time_payment_with_payment_method( response = await client.get("/v0/wallets/-/payments") data, error = await assert_status(response, status.HTTP_200_OK) - page = parse_obj_as(Page[PaymentTransaction], data) + page = TypeAdapter(Page[PaymentTransaction]).validate_python(data) assert page.data assert page.meta.total == 1 diff --git a/services/web/server/tests/unit/with_dbs/03/wallets/payments/test_payments_rpc.py b/services/web/server/tests/unit/with_dbs/03/wallets/payments/test_payments_rpc.py index 756c008adba..f3a75597a24 100644 --- a/services/web/server/tests/unit/with_dbs/03/wallets/payments/test_payments_rpc.py +++ b/services/web/server/tests/unit/with_dbs/03/wallets/payments/test_payments_rpc.py @@ -12,7 +12,7 @@ from models_library.api_schemas_webserver import WEBSERVER_RPC_NAMESPACE from models_library.payments import InvoiceDataGet from models_library.rabbitmq_basic_types import RPCMethodName -from pydantic import parse_obj_as +from pydantic import TypeAdapter from pytest_simcore.helpers.monkeypatch_envs import setenvs_from_dict from pytest_simcore.helpers.typing_env import EnvVarsDict from pytest_simcore.helpers.webserver_login import UserInfoDict @@ -77,11 +77,11 @@ async def test_one_time_payment_worfklow( result = await rpc_client.request( WEBSERVER_RPC_NAMESPACE, - parse_obj_as(RPCMethodName, "get_invoice_data"), + TypeAdapter(RPCMethodName).validate_python("get_invoice_data"), user_id=logged_user["id"], dollar_amount=Decimal(900), product_name="osparc", ) - invoice_data_get = parse_obj_as(InvoiceDataGet, result) + invoice_data_get = TypeAdapter(InvoiceDataGet).validate_python(result) assert invoice_data_get assert len(invoice_data_get.user_invoice_address.country) == 2 diff --git a/services/web/server/tests/unit/with_dbs/03/workspaces/test_workspaces.py b/services/web/server/tests/unit/with_dbs/03/workspaces/test_workspaces.py index e2ace9daa6a..9365094d679 100644 --- a/services/web/server/tests/unit/with_dbs/03/workspaces/test_workspaces.py +++ b/services/web/server/tests/unit/with_dbs/03/workspaces/test_workspaces.py @@ -59,7 +59,7 @@ async def test_workspaces_workflow( }, ) added_workspace, _ = await assert_status(resp, status.HTTP_201_CREATED) - assert WorkspaceGet.parse_obj(added_workspace) + assert WorkspaceGet.model_validate(added_workspace) # list user workspaces url = client.app.router["list_workspaces"].url_for() @@ -96,7 +96,7 @@ async def test_workspaces_workflow( }, ) data, _ = await assert_status(resp, status.HTTP_200_OK) - assert WorkspaceGet.parse_obj(data) + assert WorkspaceGet.model_validate(data) # list user workspaces url = client.app.router["list_workspaces"].url_for() diff --git a/services/web/server/tests/unit/with_dbs/conftest.py b/services/web/server/tests/unit/with_dbs/conftest.py index f4f527179b1..2e364019c3e 100644 --- a/services/web/server/tests/unit/with_dbs/conftest.py +++ b/services/web/server/tests/unit/with_dbs/conftest.py @@ -38,7 +38,7 @@ from models_library.api_schemas_directorv2.dynamic_services import DynamicServiceGet from models_library.products import ProductName from models_library.services_enums import ServiceState -from pydantic import ByteSize, parse_obj_as +from pydantic import ByteSize, TypeAdapter from pytest_mock import MockerFixture from pytest_simcore.helpers.dict_tools import ConfigDict from pytest_simcore.helpers.monkeypatch_envs import setenvs_from_dict @@ -369,7 +369,7 @@ async def _mock_result(): mock3 = mocker.patch( "simcore_service_webserver.projects._crud_api_create.get_project_total_size_simcore_s3", autospec=True, - return_value=parse_obj_as(ByteSize, "1Gib"), + return_value=TypeAdapter(ByteSize).validate_python("1Gib"), ) return MockedStorageSubsystem(mock, mock1, mock2, mock3)