diff --git a/packages/common-library/src/common_library/pydantic_validators.py b/packages/common-library/src/common_library/pydantic_validators.py index 60f6219fb13..471ba1a4bf3 100644 --- a/packages/common-library/src/common_library/pydantic_validators.py +++ b/packages/common-library/src/common_library/pydantic_validators.py @@ -1,10 +1,35 @@ import datetime +import re import warnings from datetime import timedelta from pydantic import TypeAdapter, field_validator +def _validate_legacy_timedelta_str(time_str: str | timedelta) -> str | timedelta: + if not isinstance(time_str, str): + return time_str + + # Match the format [-][DD ][HH:MM]SS[.ffffff] + match = re.match( + r"^(?P-)?(?:(?P\d+)\s)?(?:(?P\d+):)?(?:(?P\d+):)?(?P\d+)(?P\.\d+)?$", + time_str, + ) + if not match: + return time_str + + # Extract components with defaults if not present + sign = match.group("sign") or "" + days = match.group("days") or "0" + hours = match.group("hours") or "0" + minutes = match.group("minutes") or "0" + seconds = match.group("seconds") + fraction = match.group("fraction") or "" + + # Convert to the format [-][DD]D[,][HH:MM:]SS[.ffffff] + return f"{sign}{int(days)}D,{int(hours):02}:{int(minutes):02}:{seconds}{fraction}" + + def validate_numeric_string_as_timedelta(field: str): """Transforms a float/int number into a valid datetime as it used to work in the past""" @@ -29,7 +54,7 @@ def _numeric_string_as_timedelta( return converted_value except ValueError: # returns format like "1:00:00" - return v + return _validate_legacy_timedelta_str(v) return v return field_validator(field, mode="before")(_numeric_string_as_timedelta) diff --git a/packages/common-library/tests/test_pydantic_validators.py b/packages/common-library/tests/test_pydantic_validators.py index da1ccf95adb..89e57bc0999 100644 --- a/packages/common-library/tests/test_pydantic_validators.py +++ b/packages/common-library/tests/test_pydantic_validators.py @@ -1,13 +1,45 @@ from datetime import timedelta +from typing import Annotated import pytest -from common_library.pydantic_validators import validate_numeric_string_as_timedelta +from common_library.pydantic_validators import ( + validate_legacy_timedelta_str, + validate_numeric_string_as_timedelta, +) from faker import Faker -from pydantic import Field +from pydantic import BeforeValidator, Field from pydantic_settings import BaseSettings, SettingsConfigDict from pytest_simcore.helpers.monkeypatch_envs import setenvs_from_dict +def test_validate_legacy_timedelta(monkeypatch: pytest.MonkeyPatch, faker: Faker): + class Settings(BaseSettings): + APP_NAME: str + REQUEST_TIMEOUT: Annotated[ + timedelta, BeforeValidator(validate_legacy_timedelta_str) + ] = Field(default=timedelta(hours=1)) + + model_config = SettingsConfigDict() + + app_name = faker.pystr() + env_vars: dict[str, str | bool] = {"APP_NAME": app_name} + + # without timedelta + setenvs_from_dict(monkeypatch, env_vars) + settings = Settings() + print(settings.model_dump()) + assert app_name == settings.APP_NAME + assert timedelta(hours=1) == settings.REQUEST_TIMEOUT + + # with timedelta in seconds + env_vars["REQUEST_TIMEOUT"] = "2 1:10:00" + setenvs_from_dict(monkeypatch, env_vars) + settings = Settings() + print(settings.model_dump()) + assert app_name == settings.APP_NAME + assert timedelta(days=2, hours=1, minutes=10) == settings.REQUEST_TIMEOUT + + def test_validate_timedelta_in_legacy_mode( monkeypatch: pytest.MonkeyPatch, faker: Faker ): diff --git a/packages/models-library/src/models_library/api_schemas_directorv2/clusters.py b/packages/models-library/src/models_library/api_schemas_directorv2/clusters.py index 1c9892a7201..9c4698a5dc3 100644 --- a/packages/models-library/src/models_library/api_schemas_directorv2/clusters.py +++ b/packages/models-library/src/models_library/api_schemas_directorv2/clusters.py @@ -7,6 +7,7 @@ Field, HttpUrl, NonNegativeFloat, + ValidationInfo, field_validator, model_validator, ) @@ -96,7 +97,7 @@ class ClusterGet(Cluster): alias="accessRights", default_factory=dict ) - model_config = ConfigDict(populate_by_name=True) + model_config = ConfigDict(extra="allow", populate_by_name=True) @model_validator(mode="before") @classmethod @@ -112,7 +113,7 @@ class ClusterDetailsGet(ClusterDetails): class ClusterCreate(BaseCluster): - owner: GroupID | None # type: ignore[assignment] + owner: GroupID | None authentication: ExternalClusterAuthentication access_rights: dict[GroupID, ClusterAccessRights] = Field( alias="accessRights", default_factory=dict @@ -154,9 +155,9 @@ class ClusterCreate(BaseCluster): @field_validator("thumbnail", mode="before") @classmethod - def set_default_thumbnail_if_empty(cls, v, values): + def set_default_thumbnail_if_empty(cls, v, info: ValidationInfo): if v is None: - cluster_type = values["type"] + cluster_type = info.data["type"] default_thumbnails = { ClusterTypeInModel.AWS.value: "https://upload.wikimedia.org/wikipedia/commons/thumb/9/93/Amazon_Web_Services_Logo.svg/250px-Amazon_Web_Services_Logo.svg.png", ClusterTypeInModel.ON_PREMISE.value: "https://upload.wikimedia.org/wikipedia/commons/thumb/a/ac/Crystal_Clear_app_network_local.png/120px-Crystal_Clear_app_network_local.png", @@ -167,15 +168,15 @@ def set_default_thumbnail_if_empty(cls, v, values): class ClusterPatch(BaseCluster): - name: str | None # type: ignore[assignment] - description: str | None - type: ClusterTypeInModel | None # type: ignore[assignment] - owner: GroupID | None # type: ignore[assignment] - thumbnail: HttpUrl | None - endpoint: AnyUrl | None # type: ignore[assignment] - authentication: ExternalClusterAuthentication | None # type: ignore[assignment] + name: str | None = None # type: ignore[assignment] + description: str | None = None + type: ClusterTypeInModel | None = None # type: ignore[assignment] + owner: GroupID | None = None # type: ignore[assignment] + thumbnail: HttpUrl | None = None + endpoint: AnyUrl | None = None # type: ignore[assignment] + authentication: ExternalClusterAuthentication | None = None # type: ignore[assignment] access_rights: dict[GroupID, ClusterAccessRights] | None = Field( # type: ignore[assignment] - alias="accessRights" + default=None, alias="accessRights" ) model_config = ConfigDict( diff --git a/packages/models-library/src/models_library/api_schemas_webserver/auth.py b/packages/models-library/src/models_library/api_schemas_webserver/auth.py index b0b11661cb3..c841056d40c 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/auth.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/auth.py @@ -83,9 +83,10 @@ class ApiKeyGet(BaseModel): api_secret: str model_config = ConfigDict( + from_attributes=True, json_schema_extra={ "examples": [ {"display_name": "myapi", "api_key": "key", "api_secret": "secret"}, ] - } + }, ) diff --git a/packages/models-library/src/models_library/api_schemas_webserver/clusters.py b/packages/models-library/src/models_library/api_schemas_webserver/clusters.py index 109e0618b98..17232a8b482 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/clusters.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/clusters.py @@ -8,7 +8,7 @@ class ClusterPathParams(BaseModel): cluster_id: ClusterID model_config = ConfigDict( - populate_by_name=True, + populate_by_name=True, extra="forbid", ) diff --git a/packages/models-library/src/models_library/api_schemas_webserver/folders_v2.py b/packages/models-library/src/models_library/api_schemas_webserver/folders_v2.py index ee44a9b18d4..85f3604381a 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/folders_v2.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/folders_v2.py @@ -45,7 +45,7 @@ class CreateFolderBodyParams(InputSchema): class PutFolderBodyParams(InputSchema): name: IDStr - parent_folder_id: FolderID | None + parent_folder_id: FolderID | None = None model_config = ConfigDict(extra="forbid") _null_or_none_str_to_none_validator = field_validator( diff --git a/packages/models-library/src/models_library/api_schemas_webserver/groups.py b/packages/models-library/src/models_library/api_schemas_webserver/groups.py index 46e9da3dc52..d77c04288c9 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/groups.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/groups.py @@ -1,6 +1,7 @@ from contextlib import suppress from pydantic import ( + AnyHttpUrl, AnyUrl, BaseModel, ConfigDict, @@ -90,7 +91,7 @@ def _sanitize_legacy_data(cls, v): if v: # Enforces null if thumbnail is not valid URL or empty with suppress(ValidationError): - return TypeAdapter(AnyUrl).validate_python(v) + return TypeAdapter(AnyHttpUrl).validate_python(v) return None @@ -143,12 +144,14 @@ class AllUsersGroups(BaseModel): class GroupUserGet(BaseModel): - id: str | None = Field(None, description="the user id") + id: str | None = Field(None, description="the user id", coerce_numbers_to_str=True) login: LowerCaseEmailStr | None = Field(None, description="the user login email") first_name: str | None = Field(None, description="the user first name") last_name: str | None = Field(None, description="the user last name") gravatar_id: str | None = Field(None, description="the user gravatar id hash") - gid: str | None = Field(None, description="the user primary gid") + gid: str | None = Field( + None, description="the user primary gid", coerce_numbers_to_str=True + ) access_rights: GroupAccessRights = Field(..., alias="accessRights") model_config = ConfigDict( diff --git a/packages/models-library/src/models_library/api_schemas_webserver/projects.py b/packages/models-library/src/models_library/api_schemas_webserver/projects.py index 976ee74a712..0ae2bdee311 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/projects.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/projects.py @@ -6,9 +6,11 @@ """ from datetime import datetime -from typing import Any, Literal, TypeAlias +from typing import Annotated, Any, Literal, TypeAlias -from pydantic import ConfigDict, Field, HttpUrl, field_validator +from models_library.folders import FolderID +from models_library.workspaces import WorkspaceID +from pydantic import BeforeValidator, ConfigDict, Field, HttpUrl, field_validator from ..api_schemas_long_running_tasks.tasks import TaskGet from ..basic_types import LongTruncatedStr, ShortTruncatedStr @@ -105,7 +107,7 @@ class ProjectReplace(InputSchema): uuid: ProjectID name: ShortTruncatedStr description: LongTruncatedStr - thumbnail: HttpUrl | None + thumbnail: Annotated[HttpUrl | None, BeforeValidator(empty_str_to_none_pre_validator)] = Field(default=None) creation_date: DateTimeStr last_change_date: DateTimeStr workbench: NodesDict @@ -121,25 +123,17 @@ class ProjectReplace(InputSchema): default_factory=dict, json_schema_extra={"default": {}} ) - _empty_is_none = field_validator("thumbnail", mode="before")( - empty_str_to_none_pre_validator - ) - class ProjectPatch(InputSchema): name: ShortTruncatedStr | None = Field(default=None) description: LongTruncatedStr | None = Field(default=None) - thumbnail: HttpUrl | None = Field(default=None) + thumbnail: Annotated[HttpUrl | None, BeforeValidator(empty_str_to_none_pre_validator)] = Field(default=None) access_rights: dict[GroupIDStr, AccessRights] | None = Field(default=None) classifiers: list[ClassifierID] | None = Field(default=None) dev: dict | None = Field(default=None) ui: StudyUI | None = Field(default=None) quality: dict[str, Any] | None = Field(default=None) - - _empty_is_none = field_validator("thumbnail", mode="before")( - empty_str_to_none_pre_validator - ) - + __all__: tuple[str, ...] = ( "EmptyModel", diff --git a/packages/models-library/src/models_library/api_schemas_webserver/resource_usage.py b/packages/models-library/src/models_library/api_schemas_webserver/resource_usage.py index 8242105f55a..3eea55c6d67 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/resource_usage.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/resource_usage.py @@ -1,7 +1,7 @@ from datetime import datetime from decimal import Decimal -from pydantic import BaseModel, ConfigDict +from pydantic import BaseModel, ConfigDict, Field from ..projects import ProjectID from ..projects_nodes_io import NodeID @@ -48,7 +48,7 @@ class ServiceRunGet( class PricingUnitGet(OutputSchema): pricing_unit_id: PricingUnitId unit_name: str - unit_extra_info: dict + unit_extra_info: UnitExtraInfo current_cost_per_unit: Decimal default: bool @@ -131,7 +131,7 @@ class UpdatePricingUnitBodyParams(InputSchema): unit_extra_info: UnitExtraInfo default: bool specific_info: SpecificInfo - pricing_unit_cost_update: PricingUnitCostUpdate | None + pricing_unit_cost_update: PricingUnitCostUpdate | None = Field(default=None) model_config = ConfigDict( str_strip_whitespace=True, diff --git a/packages/models-library/src/models_library/api_schemas_webserver/wallets.py b/packages/models-library/src/models_library/api_schemas_webserver/wallets.py index 3ba88301f88..ae20bee1e62 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/wallets.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/wallets.py @@ -2,7 +2,7 @@ from decimal import Decimal from typing import Annotated, Literal, TypeAlias -from pydantic import ConfigDict, Field, HttpUrl, PlainSerializer, field_validator +from pydantic import ConfigDict, Field, HttpUrl, PlainSerializer, ValidationInfo, field_validator from ..basic_types import AmountDecimal, IDStr, NonNegativeDecimal from ..users import GroupID @@ -20,7 +20,10 @@ class WalletGet(OutputSchema): created: datetime modified: datetime - model_config = ConfigDict(frozen=False) + model_config = ConfigDict( + from_attributes=True, + frozen=False + ) class WalletGetWithAvailableCredits(WalletGet): @@ -139,6 +142,7 @@ class PaymentMethodGet(OutputSchema): ) model_config = ConfigDict( + frozen=False, json_schema_extra={ "examples": [ { @@ -200,8 +204,8 @@ class ReplaceWalletAutoRecharge(InputSchema): @field_validator("monthly_limit_in_usd") @classmethod - def _monthly_limit_greater_than_top_up(cls, v, values): - top_up = values["top_up_amount_in_usd"] + def _monthly_limit_greater_than_top_up(cls, v, info: ValidationInfo): + top_up = info.data["top_up_amount_in_usd"] if v is not None and v < top_up: msg = "Monthly limit ({v} USD) should be greater than top up amount ({top_up} USD)" raise ValueError(msg) diff --git a/packages/models-library/src/models_library/clusters.py b/packages/models-library/src/models_library/clusters.py index c98ea29757a..09540b8c16e 100644 --- a/packages/models-library/src/models_library/clusters.py +++ b/packages/models-library/src/models_library/clusters.py @@ -159,6 +159,7 @@ class Cluster(BaseCluster): id: ClusterID = Field(..., description="The cluster ID") model_config = ConfigDict( + extra="allow", json_schema_extra={ "examples": [ { @@ -217,7 +218,7 @@ class Cluster(BaseCluster): }, }, ] - } + }, ) @model_validator(mode="before") diff --git a/packages/models-library/src/models_library/projects_ui.py b/packages/models-library/src/models_library/projects_ui.py index 93aa68d628b..0e914de9dba 100644 --- a/packages/models-library/src/models_library/projects_ui.py +++ b/packages/models-library/src/models_library/projects_ui.py @@ -68,7 +68,7 @@ class StudyUI(BaseModel): current_node_id: NodeID | None = Field(default=None, alias="currentNodeId") annotations: dict[NodeIDStr, Annotation] | None = None - model_config = ConfigDict(extra="allow") + model_config = ConfigDict(extra="allow", populate_by_name=True) _empty_is_none = field_validator("*", mode="before")( empty_str_to_none_pre_validator diff --git a/packages/models-library/src/models_library/resource_tracker.py b/packages/models-library/src/models_library/resource_tracker.py index c3b42a08795..53e370913a9 100644 --- a/packages/models-library/src/models_library/resource_tracker.py +++ b/packages/models-library/src/models_library/resource_tracker.py @@ -259,7 +259,7 @@ class PricingUnitWithCostUpdate(BaseModel): unit_extra_info: UnitExtraInfo default: bool specific_info: SpecificInfo - pricing_unit_cost_update: None | PricingUnitCostUpdate + pricing_unit_cost_update: PricingUnitCostUpdate | None model_config = ConfigDict( json_schema_extra={ diff --git a/packages/models-library/src/models_library/rest_pagination_utils.py b/packages/models-library/src/models_library/rest_pagination_utils.py index 41899acd8cf..d4027583ae7 100644 --- a/packages/models-library/src/models_library/rest_pagination_utils.py +++ b/packages/models-library/src/models_library/rest_pagination_utils.py @@ -1,5 +1,6 @@ from math import ceil -from typing import Any, Protocol, TypedDict, Union, runtime_checkable +from typing import Any, Protocol, runtime_checkable +from typing_extensions import TypedDict from common_library.pydantic_networks_extension import AnyHttpUrlLegacy from pydantic import TypeAdapter @@ -29,7 +30,7 @@ def replace_query_params(self, **kwargs: Any) -> "_StarletteURL": ... -_URLType = Union[_YarlURL, _StarletteURL] +_URLType = _YarlURL | _StarletteURL def _replace_query(url: _URLType, query: dict[str, Any]) -> str: @@ -69,9 +70,11 @@ def paginate_data( """ last_page = ceil(total / limit) - 1 + data = [item.model_dump() if hasattr(item, "model_dump") else item for item in chunk] + return PageDict( _meta=PageMetaInfoLimitOffset( - total=total, count=len(chunk), limit=limit, offset=offset + total=total, count=len(data), limit=limit, offset=offset ), _links=PageLinks( self=_replace_query(request_url, {"offset": offset, "limit": limit}), @@ -91,5 +94,5 @@ def paginate_data( request_url, {"offset": last_page * limit, "limit": limit} ), ), - data=chunk, + data=data, ) diff --git a/packages/models-library/src/models_library/user_preferences.py b/packages/models-library/src/models_library/user_preferences.py index 2680c10223d..f16c934b2da 100644 --- a/packages/models-library/src/models_library/user_preferences.py +++ b/packages/models-library/src/models_library/user_preferences.py @@ -1,6 +1,7 @@ from enum import auto -from typing import Annotated, Any, ClassVar, Literal, TypeAlias, get_args +from typing import Annotated, Any, ClassVar, Literal, TypeAlias +from common_library.pydantic_fields_extension import get_type from pydantic import BaseModel, Field from pydantic._internal._model_construction import ModelMetaclass @@ -94,11 +95,7 @@ def to_db(self) -> dict: @classmethod def update_preference_default_value(cls, new_default: Any) -> None: - expected_type = ( - t[0] - if (t := get_args(cls.model_fields["value"].annotation)) - else cls.model_fields["value"].annotation - ) + expected_type = get_type(cls.model_fields["value"]) detected_type = type(new_default) if expected_type != detected_type: msg = ( @@ -110,6 +107,9 @@ def update_preference_default_value(cls, new_default: Any) -> None: cls.model_fields["value"].default_factory = lambda: new_default else: cls.model_fields["value"].default = new_default + cls.model_fields["value"].default_factory = None + + cls.model_rebuild(force=True) class UserServiceUserPreference(_BaseUserPreferenceModel): 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..a71906ace59 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/products.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/products.py @@ -6,11 +6,12 @@ """ import json -from typing import Literal, TypedDict +from typing import Literal import sqlalchemy as sa from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.sql import func +from typing_extensions import TypedDict from .base import metadata from .groups import groups diff --git a/packages/pytest-simcore/src/pytest_simcore/hypothesis_type_strategies.py b/packages/pytest-simcore/src/pytest_simcore/hypothesis_type_strategies.py index ad80ab57774..788a0d36dab 100644 --- a/packages/pytest-simcore/src/pytest_simcore/hypothesis_type_strategies.py +++ b/packages/pytest-simcore/src/pytest_simcore/hypothesis_type_strategies.py @@ -1,9 +1,13 @@ from hypothesis import provisional from hypothesis import strategies as st -from pydantic import AnyHttpUrl, AnyUrl, HttpUrl +from hypothesis.strategies import composite +from pydantic import TypeAdapter +from pydantic_core import Url -# FIXME: For now it seems the pydantic hypothesis plugin does not provide strategies for these types. -# therefore we currently provide it -st.register_type_strategy(AnyUrl, provisional.urls()) -st.register_type_strategy(HttpUrl, provisional.urls()) -st.register_type_strategy(AnyHttpUrl, provisional.urls()) + +@composite +def url_strategy(draw): + return TypeAdapter(Url).validate_python(draw(provisional.urls())) + + +st.register_type_strategy(Url, url_strategy()) diff --git a/packages/service-library/src/servicelib/aiohttp/requests_validation.py b/packages/service-library/src/servicelib/aiohttp/requests_validation.py index 5a588041682..0dd40949ba8 100644 --- a/packages/service-library/src/servicelib/aiohttp/requests_validation.py +++ b/packages/service-library/src/servicelib/aiohttp/requests_validation.py @@ -224,7 +224,7 @@ async def parse_request_body_as( except json.decoder.JSONDecodeError as err: raise web.HTTPBadRequest(reason=f"Invalid json in body: {err}") from err - if hasattr(model_schema_cls, "parse_obj"): + if hasattr(model_schema_cls, "model_validate"): # NOTE: model_schema can be 'list[T]' or 'dict[T]' which raise TypeError # with issubclass(model_schema, BaseModel) assert issubclass(model_schema_cls, BaseModel) # nosec diff --git a/packages/service-library/tests/test_logging_errors.py b/packages/service-library/tests/test_logging_errors.py index b6b652a46d7..8bbbee60d40 100644 --- a/packages/service-library/tests/test_logging_errors.py +++ b/packages/service-library/tests/test_logging_errors.py @@ -3,6 +3,7 @@ import logging import pytest + from common_library.error_codes import create_error_code from common_library.errors_classes import OsparcErrorMixin from servicelib.logging_errors import ( 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 d566b8d9112..9f0548262a8 100644 --- a/services/web/server/requirements/_base.txt +++ b/services/web/server/requirements/_base.txt @@ -26,17 +26,31 @@ aiofiles==0.8.0 # -r requirements/_base.in aiohttp==3.8.5 # via + # -c requirements/../../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -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 +85,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 @@ -105,17 +121,31 @@ captcha==0.5.0 # via -r requirements/_base.in certifi==2023.7.22 # via + # -c requirements/../../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -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 @@ -130,17 +160,31 @@ click==8.1.3 # via typer cryptography==41.0.7 # via + # -c requirements/../../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -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 @@ -154,7 +198,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 @@ -199,17 +243,31 @@ jinja-app-loader==1.0.2 # via -r requirements/_base.in jinja2==3.1.2 # via + # -c requirements/../../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -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 @@ -232,17 +290,31 @@ lazy-object-proxy==1.7.1 # via openapi-core mako==1.2.2 # via + # -c requirements/../../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -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 @@ -341,24 +413,52 @@ opentelemetry-util-http==0.47b0 # opentelemetry-instrumentation-requests orjson==3.10.0 # via + # -c requirements/../../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -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 + # -r requirements/../../../../packages/common-library/requirements/_base.in + # -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/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/settings-library/requirements/../../../packages/common-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/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/settings-library/requirements/../../../packages/common-library/requirements/_base.in # -r requirements/_base.in packaging==24.1 # via @@ -393,39 +493,101 @@ 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/common-library/requirements/../../../requirements/constraints.txt + # -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/common-library/requirements/_base.in + # -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/common-library/requirements/_base.in + # -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/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/settings-library/requirements/../../../packages/common-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/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/settings-library/requirements/../../../packages/common-library/requirements/_base.in +pydantic-settings==2.5.2 + # via + # -c requirements/./constraints.txt + # -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 @@ -440,6 +602,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 @@ -450,17 +614,31 @@ pytz==2022.1 # via twilio pyyaml==6.0.1 # via + # -c requirements/../../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -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 @@ -470,17 +648,31 @@ pyyaml==6.0.1 # openapi-spec-validator redis==5.0.4 # via + # -c requirements/../../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -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 @@ -521,17 +713,31 @@ sniffio==1.3.1 # via anyio sqlalchemy==1.4.47 # via + # -c requirements/../../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -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 @@ -574,37 +780,66 @@ typing-extensions==4.12.0 # opentelemetry-sdk # pint # pydantic + # pydantic-core # typer ujson==5.5.0 # via + # -c requirements/../../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -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/common-library/requirements/../../../requirements/constraints.txt + # -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 3aab7cde47d..d787c281d34 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 f9917eb3748..bf55a2ed211 100644 --- a/services/web/server/requirements/ci.txt +++ b/services/web/server/requirements/ci.txt @@ -14,6 +14,7 @@ --requirement _tools.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..7b48d86e2b2 100644 --- a/services/web/server/src/simcore_service_webserver/announcements/_models.py +++ b/services/web/server/src/simcore_service_webserver/announcements/_models.py @@ -1,15 +1,15 @@ 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, ValidationInfo, field_validator # NOTE: this model is used for BOTH # - parse+validate from redis # - schema in the response class Announcement(BaseModel): - id: str # noqa: A003 + id: str products: list[str] start: datetime end: datetime @@ -18,10 +18,10 @@ 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"): + def _check_start_before_end(cls, v, info: ValidationInfo): + if start := info.data.get("start"): end = v if end <= start: msg = f"end={end!r} is not before start={start!r}" @@ -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/announcements/_redis.py b/services/web/server/src/simcore_service_webserver/announcements/_redis.py index aad45ea8fee..785f954521f 100644 --- a/services/web/server/src/simcore_service_webserver/announcements/_redis.py +++ b/services/web/server/src/simcore_service_webserver/announcements/_redis.py @@ -37,7 +37,7 @@ async def list_announcements( announcements = [] for i, item in enumerate(items): try: - model = Announcement.parse_raw(item) + model = Announcement.model_validate_json(item) # filters if include_product not in model.products: continue 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/_db.py b/services/web/server/src/simcore_service_webserver/api_keys/_db.py index 4a51464e1a9..ec08ce5dd67 100644 --- a/services/web/server/src/simcore_service_webserver/api_keys/_db.py +++ b/services/web/server/src/simcore_service_webserver/api_keys/_db.py @@ -79,7 +79,7 @@ async def get( result: ResultProxy = await conn.execute(stmt) row: RowProxy | None = await result.fetchone() - return ApiKeyInDB.from_orm(row) if row else None + return ApiKeyInDB.model_validate(row) if row else None async def get_or_create( self, @@ -116,7 +116,7 @@ async def get_or_create( result = await conn.execute(insert_stmt) row = await result.fetchone() assert row # nosec - return ApiKeyInDB.from_orm(row) + return ApiKeyInDB.model_validate(row) async def delete_by_name( self, *, display_name: str, user_id: UserID, product_name: ProductName @@ -145,7 +145,7 @@ async def prune_expired(self) -> list[str]: stmt = ( api_keys.delete() .where( - (api_keys.c.expires_at != None) # noqa: E711 + (api_keys.c.expires_at.is_not(None)) & (api_keys.c.expires_at < sa.func.now()) ) .returning(api_keys.c.display_name) 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 627d733d9c7..6327a57ca54 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 @@ -32,7 +32,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, @@ -45,7 +45,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( @@ -67,7 +67,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 a105dcdcc1a..94d1be5358d 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,9 +12,17 @@ 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 pydantic_settings import SettingsConfigDict from servicelib.logging_utils_filtering import LoggerName, MessageSubstring from settings_library.base import BaseCustomSettings from settings_library.email import SMTPSettings @@ -54,7 +63,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 @@ -84,13 +93,15 @@ 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( @@ -106,13 +117,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!", ) WEBSERVER_LOG_FILTER_MAPPING: dict[LoggerName, list[MessageSubstring]] = Field( @@ -128,100 +140,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 @@ -250,57 +279,59 @@ 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") + def build_vcs_release_url_if_unset(cls, v): + release_url = v.SIMCORE_VCS_RELEASE_URL - if release_url is None and ( - vsc_release_tag := values.get("SIMCORE_VCS_RELEASE_TAG") - ): + if release_url is None and (vsc_release_tag := v.SIMCORE_VCS_RELEASE_TAG): if vsc_release_tag == "latest": release_url = ( "https://github.com/ITISFoundation/osparc-simcore/commits/master/" ) else: release_url = f"https://github.com/ITISFoundation/osparc-simcore/releases/tag/{vsc_release_tag}" - values["SIMCORE_VCS_RELEASE_URL"] = release_url + v.SIMCORE_VCS_RELEASE_URL = release_url - return values + return v - @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", pre=True) + @field_validator("WEBSERVER_LOGLEVEL") @classmethod - def valid_log_level(cls, value: str) -> str: + 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 @@ -356,7 +387,7 @@ def _export_by_alias(self, **kwargs) -> dict[str, Any]: def config_alias_generator(s): return s.lower() - data: dict[str, Any] = self.dict(**kwargs) + data: dict[str, Any] = self.model_dump(**kwargs) current_keys = list(data.keys()) for key in current_keys: @@ -418,7 +449,7 @@ def setup_settings(app: web.Application) -> ApplicationSettings: app[APP_SETTINGS_KEY] = settings _logger.debug( "Captured app settings:\n%s", - app[APP_SETTINGS_KEY].json(indent=1, sort_keys=True), + app[APP_SETTINGS_KEY].model_dump_json(indent=1), ) return settings diff --git a/services/web/server/src/simcore_service_webserver/application_settings_utils.py b/services/web/server/src/simcore_service_webserver/application_settings_utils.py index 9123c8ad574..9843e84afdd 100644 --- a/services/web/server/src/simcore_service_webserver/application_settings_utils.py +++ b/services/web/server/src/simcore_service_webserver/application_settings_utils.py @@ -10,6 +10,7 @@ from typing import Any from aiohttp import web +from common_library.pydantic_fields_extension import get_type, is_nullable from pydantic.types import SecretStr from servicelib.aiohttp.typing_extension import Handler @@ -200,10 +201,10 @@ def convert_to_environ_vars( # noqa: C901, PLR0915, PLR0912 def _set_if_disabled(field_name, section): # Assumes that by default is enabled enabled = section.get("enabled", True) - field = ApplicationSettings.__fields__[field_name] + field = ApplicationSettings.model_fields[field_name] if not enabled: - envs[field_name] = "null" if field.allow_none else "0" - elif field.type_ == bool: + envs[field_name] = "null" if is_nullable(field) else "0" + elif get_type(field) == bool: envs[field_name] = "1" if main := cfg.get("main"): 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..f2fc9be73a9 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()) # type: ignore[arg-type] ) # 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()) # type: ignore[arg-type] ) # 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()) # type: ignore[arg-type] ) # 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()) # type: ignore[arg-type] ) # 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..a9ba40b5378 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, field_validator from servicelib.aiohttp.requests_validation import ( parse_request_body_as, parse_request_path_parameters_as, @@ -54,12 +54,12 @@ 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 +90,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 +98,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 +133,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 +160,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 +182,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 +210,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 +265,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 +293,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 +387,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( + PricingPlanGet.model_validate(pricing_plan.model_dump()) + ) 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/cli.py b/services/web/server/src/simcore_service_webserver/cli.py index ca85670ad74..b09f4eca0c4 100644 --- a/services/web/server/src/simcore_service_webserver/cli.py +++ b/services/web/server/src/simcore_service_webserver/cli.py @@ -19,6 +19,7 @@ import typer from aiohttp import web +from common_library.json_serialization import json_dumps from settings_library.utils_cli import create_settings_command from typing_extensions import Annotated @@ -68,7 +69,8 @@ async def app_factory() -> web.Application: assert app_settings.SC_BUILD_TARGET # nosec _logger.info( - "Application settings: %s", app_settings.json(indent=2, sort_keys=True) + "Application settings: %s", + json_dumps(app_settings, indent=2, sort_keys=True), ) app, _ = _setup_app_from_settings(app_settings) 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..41d64a2d5ef 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 ClusterGet.model_validate(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 ClusterGet.model_validate(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,7 @@ 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 ClusterDetails.model_validate(cluster_details) is not None # nosec return envelope_json_response(cluster_details) @@ -199,7 +199,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 3171d0a80fb..bed2f77f7f2 100644 --- a/services/web/server/src/simcore_service_webserver/diagnostics/_handlers.py +++ b/services/web/server/src/simcore_service_webserver/diagnostics/_handlers.py @@ -9,7 +9,7 @@ from aiohttp import ClientError, ClientSession, web from models_library.app_diagnostics import AppStatusCheck -from pydantic import BaseModel, Field, parse_obj_as +from pydantic import BaseModel, Field 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 @@ -61,7 +61,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 StatusDiagnosticsGet.model_validate(data) is not None # nosec return envelope_json_response(data) @@ -98,7 +98,7 @@ def _get_client_session_info(): return info - check = AppStatusCheck.parse_obj( + check = AppStatusCheck.model_validate( { "app_name": APP_NAME, "version": API_VERSION, @@ -149,7 +149,7 @@ async def _check_resource_usage_tracker(): reraise=False, ) - return envelope_json_response(check.dict(exclude_unset=True)) + return envelope_json_response(check.model_dump(exclude_unset=True)) @routes.get(f"/{api_version_prefix}/status/{{service_name}}", name="get_service_status") 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..b9557f6d231 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,12 @@ from aiohttp.web import Application -from pydantic import Field, NonNegativeFloat, PositiveFloat, validator +from pydantic import ( + AliasChoices, + Field, + NonNegativeFloat, + PositiveFloat, + ValidationInfo, + field_validator, +) from servicelib.aiohttp.application_keys import APP_SETTINGS_KEY from settings_library.base import BaseCustomSettings @@ -11,7 +18,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,13 +41,13 @@ 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): + def _validate_max_task_delay(cls, v, info: ValidationInfo): # Sets an upper threshold for blocking functions, i.e. # settings.DIAGNOSTICS_SLOW_DURATION_SECS < settings.DIAGNOSTICS_MAX_TASK_DELAY # - slow_duration_secs = float(values["DIAGNOSTICS_SLOW_DURATION_SECS"]) + slow_duration_secs = float(info.data["DIAGNOSTICS_SLOW_DURATION_SECS"]) return max( 10 * slow_duration_secs, float(v), 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 5b8a69e33c3..c034f93a660 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 @@ -4,12 +4,12 @@ """ -import json import logging from typing import Any from uuid import UUID from aiohttp import web +from common_library.serialization import model_dump_with_secrets from models_library.api_schemas_directorv2.clusters import ( ClusterCreate, ClusterDetails, @@ -26,11 +26,10 @@ 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 -from settings_library.utils_encoders import create_json_encoder_wo_secrets from ..products.api import get_product from ._api_utils import get_wallet_info @@ -182,7 +181,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: @@ -245,16 +244,12 @@ async def create_cluster( "POST", url=(settings.base_url / "clusters").update_query(user_id=int(user_id)), expected_status=web.HTTPCreated, - data=json.loads( - new_cluster.json( - by_alias=True, - exclude_unset=True, - encoder=create_json_encoder_wo_secrets(ClusterCreate), - ) + data=model_dump_with_secrets( + new_cluster, show_secrets=True, by_alias=True, exclude_unset=True ), ) assert isinstance(cluster, dict) # nosec - assert parse_obj_as(ClusterGet, cluster) is not None # nosec + assert ClusterGet.model_validate(cluster) is not None # nosec return cluster @@ -268,7 +263,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 @@ -296,7 +291,7 @@ async def get_cluster( ) assert isinstance(cluster, dict) # nosec - assert parse_obj_as(ClusterGet, cluster) is not None # nosec + assert ClusterGet.model_validate(cluster) is not None # nosec return cluster @@ -324,7 +319,7 @@ async def get_cluster_details( }, ) assert isinstance(cluster, dict) # nosec - assert parse_obj_as(ClusterDetails, cluster) is not None # nosec + assert ClusterDetails.model_validate(cluster) is not None # nosec return cluster @@ -342,12 +337,8 @@ async def update_cluster( user_id=int(user_id) ), expected_status=web.HTTPOk, - data=json.loads( - cluster_patch.json( - by_alias=True, - exclude_unset=True, - encoder=create_json_encoder_wo_secrets(ClusterPatch), - ) + data=model_dump_with_secrets( + cluster_patch, show_secrets=True, by_alias=True, exclude_none=True ), on_error={ status.HTTP_404_NOT_FOUND: ( @@ -362,7 +353,7 @@ async def update_cluster( ) assert isinstance(cluster, dict) # nosec - assert parse_obj_as(ClusterGet, cluster) is not None # nosec + assert ClusterGet.model_validate(cluster) is not None # nosec return cluster @@ -397,12 +388,11 @@ async def ping_cluster(app: web.Application, cluster_ping: ClusterPing) -> None: "POST", url=settings.base_url / "clusters:ping", expected_status=web.HTTPNoContent, - data=json.loads( - cluster_ping.json( - by_alias=True, - exclude_unset=True, - encoder=create_json_encoder_wo_secrets(ClusterPing), - ) + data=model_dump_with_secrets( + cluster_ping, + show_secrets=True, + by_alias=True, + exclude_unset=True, ), on_error={ status.HTTP_422_UNPROCESSABLE_ENTITY: ( 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..21793b79376 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 @@ -33,7 +33,7 @@ async def list_dynamic_services( project_id: str | None = None, ) -> list[DynamicServiceGet]: params = _Params(user_id=user_id, project_id=project_id) - params_dict = params.dict(exclude_none=True) + params_dict = params.model_dump(exclude_none=True) settings: DirectorV2Settings = get_plugin_settings(app) if params_dict: # Update query doesnt work with no params to unwrap backend_url = (settings.base_url / "dynamic_services").update_query( @@ -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 daffd3be369..b7d973937ef 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.clusters import ClusterID from models_library.projects import ProjectID from models_library.users import UserID -from pydantic import BaseModel, Field, ValidationError, parse_obj_as +from pydantic import BaseModel, Field, TypeAdapter, ValidationError from pydantic.types import NonNegativeInt from servicelib.aiohttp import status from servicelib.aiohttp.rest_responses import create_http_error, exception_to_response @@ -64,7 +64,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) @@ -78,7 +78,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)) @@ -158,7 +160,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) @@ -186,7 +190,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 @@ -234,8 +238,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) @@ -251,7 +254,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..6d14d4b709d 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": f"{ClusterType.ON_PREMISE}", # 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": f"{ClusterType.AWS}", "owner": 154, "endpoint": "https://registry.osparc-development.fake.dev", "authentication": { @@ -74,6 +72,7 @@ class Config(BaseCluster.Config): }, ] } + ) 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..91dac1317b6 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,21 +1,18 @@ -from typing import Final +import datetime from aiohttp import web -from pydantic import Field, NonNegativeInt +from pydantic import AliasChoices, Field from settings_library.base import BaseCustomSettings from settings_library.utils_service import MixinServiceSettings from .._constants import APP_SETTINGS_KEY -_MINUTE: Final[NonNegativeInt] = 60 -_HOUR: Final[NonNegativeInt] = 60 * _MINUTE - class DynamicSchedulerSettings(BaseCustomSettings, MixinServiceSettings): - DYNAMIC_SCHEDULER_STOP_SERVICE_TIMEOUT: NonNegativeInt = Field( - _HOUR + 10, + DYNAMIC_SCHEDULER_STOP_SERVICE_TIMEOUT: datetime.timedelta = Field( + datetime.timedelta(hours=1, seconds=10), description=( - "Timeout on stop service request (seconds)" + "Timeout on stop service request" "ANE: The below will try to help explaining what is happening: " "webserver -(stop_service)-> dynamic-scheduler -(relays the stop)-> " "director-v* -(save_state)-> service_x" @@ -23,10 +20,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/email/_core.py b/services/web/server/src/simcore_service_webserver/email/_core.py index 0c2329cac54..269687a95a7 100644 --- a/services/web/server/src/simcore_service_webserver/email/_core.py +++ b/services/web/server/src/simcore_service_webserver/email/_core.py @@ -36,7 +36,7 @@ async def _do_send_mail( WARNING: _do_send_mail is mocked so be careful when changing the signature or name !! """ - _logger.debug("Email configuration %s", settings.json(indent=1)) + _logger.debug("Email configuration %s", settings.model_dump_json(indent=1)) if settings.SMTP_PORT == 587: # NOTE: aiosmtplib does not handle port 587 correctly this is a workaround diff --git a/services/web/server/src/simcore_service_webserver/email/_handlers.py b/services/web/server/src/simcore_service_webserver/email/_handlers.py index 829f58e4a0b..84126852347 100644 --- a/services/web/server/src/simcore_service_webserver/email/_handlers.py +++ b/services/web/server/src/simcore_service_webserver/email/_handlers.py @@ -100,7 +100,7 @@ async def test_email(request: web.Request): return envelope_json_response( EmailTestPassed( - fixtures=body.dict(), + fixtures=body.model_dump(), info={ "email-server": info, "email-headers": message.items(), @@ -111,7 +111,7 @@ async def test_email(request: web.Request): except Exception as err: # pylint: disable=broad-except logger.exception( "test_email failed for %s", - f"{settings.json(indent=1)}", + f"{settings.model_dump_json(indent=1)}", ) return envelope_json_response( EmailTestFailed.create_from_exception(error=err, test_name="test_email") diff --git a/services/web/server/src/simcore_service_webserver/errors.py b/services/web/server/src/simcore_service_webserver/errors.py index 1bc48eda031..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 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..65749210e77 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,6 @@ from typing import Any, Final from aiohttp import web -from pydantic import parse_obj_as from servicelib.pools import non_blocking_process_pool_executor from ...catalog.client import get_service @@ -79,8 +78,7 @@ async def _add_rrid_entries( continue rrid_entires.append( - parse_obj_as( - RRIDEntry, + RRIDEntry.model_validate( { "rrid_term": scicrunch_resource.name, "rrid_identifier": scicrunch_resource.rrid, @@ -158,8 +156,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 = DatasetDescriptionParams.model_validate( {"name": project_data["name"], "description": project_data["description"]}, ) diff --git a/services/web/server/src/simcore_service_webserver/folders/_folders_db.py b/services/web/server/src/simcore_service_webserver/folders/_folders_db.py index 5c1dcf4d47f..d2992ed30d5 100644 --- a/services/web/server/src/simcore_service_webserver/folders/_folders_db.py +++ b/services/web/server/src/simcore_service_webserver/folders/_folders_db.py @@ -70,7 +70,7 @@ async def create( .returning(*_SELECTION_ARGS) ) row = await result.first() - return FolderDB.from_orm(row) + return FolderDB.model_validate(row) async def list_( @@ -123,7 +123,7 @@ async def list_( result = await conn.execute(list_query) rows = await result.fetchall() or [] - results: list[FolderDB] = [FolderDB.from_orm(row) for row in rows] + results: list[FolderDB] = [FolderDB.model_validate(row) for row in rows] return cast(int, total_count), results @@ -149,7 +149,7 @@ async def get( raise FolderAccessForbiddenError( reason=f"Folder {folder_id} does not exist.", ) - return FolderDB.from_orm(row) + return FolderDB.model_validate(row) async def get_for_user_or_workspace( @@ -185,7 +185,7 @@ async def get_for_user_or_workspace( raise FolderAccessForbiddenError( reason=f"User does not have access to the folder {folder_id}. Or folder does not exist.", ) - return FolderDB.from_orm(row) + return FolderDB.model_validate(row) async def update( @@ -213,7 +213,7 @@ async def update( row = await result.first() if row is None: raise FolderNotFoundError(reason=f"Folder {folder_id} not found.") - return FolderDB.from_orm(row) + return FolderDB.model_validate(row) async def delete_recursively( 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 f331c98da4a..857eb56ef57 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 import status from servicelib.aiohttp.requests_validation import ( RequestParams, @@ -94,7 +94,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( @@ -106,7 +106,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 { @@ -120,16 +120,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) @@ -138,7 +137,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( @@ -158,7 +157,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 ) @@ -171,10 +170,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, @@ -184,7 +183,7 @@ async def list_folders(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, ) @@ -194,7 +193,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( @@ -215,7 +214,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) @@ -238,7 +237,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 5ac89e0ee94..68b9e2a5fdb 100644 --- a/services/web/server/src/simcore_service_webserver/groups/_classifiers.py +++ b/services/web/server/src/simcore_service_webserver/groups/_classifiers.py @@ -9,20 +9,19 @@ """ 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 pydantic import ( BaseModel, - ConstrainedStr, Field, HttpUrl, + StringConstraints, + TypeAdapter, ValidationError, - parse_obj_as, - validator, + field_validator, ) from simcore_postgres_database.models.classifiers import group_classifiers @@ -37,8 +36,11 @@ # DOMAIN MODELS --- -class TreePath(ConstrainedStr): - regex = re.compile(r"[\w:]+") # Examples 'a::b::c +TreePath: TypeAlias = Annotated[ + # Examples 'a::b::c + str, + StringConstraints(pattern=r"[\w:]+"), +] class ClassifierItem(BaseModel): @@ -50,10 +52,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: @@ -91,7 +93,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. " @@ -136,7 +140,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: @@ -144,4 +150,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..4eda0ff3e65 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,6 @@ 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 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 +116,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 [GroupAtDB.model_validate(row) for row in rows] async def get_user_group( @@ -409,5 +408,5 @@ async def get_group_from_gid(conn: SAConnection, gid: GroupID) -> GroupAtDB | No row: ResultProxy = await conn.execute(groups.select().where(groups.c.gid == gid)) result = await row.first() if result: - return GroupAtDB.from_orm(result) + return GroupAtDB.model_validate(result) return None 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 ba366aaef46..9baeb984d9f 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 pydantic import BaseModel, Extra, Field, parse_obj_as +from pydantic import BaseModel, ConfigDict, Field, TypeAdapter from servicelib.aiohttp import status from servicelib.aiohttp.requests_validation import ( parse_request_path_parameters_as, @@ -83,7 +83,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 @@ -104,15 +104,13 @@ async def list_groups(request: web.Request): product_gid=product.group_id, ) - assert parse_obj_as(AllUsersGroups, result) is not None # nosec + assert AllUsersGroups.model_validate(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") @@ -121,11 +119,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 UsersGroup.model_validate(group) is not None # nosec return group @@ -135,11 +133,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 UsersGroup.model_validate(created_group) is not None # nosec raise web.HTTPCreated( text=json_dumps({"data": created_group}), content_type=MIMETYPE_APPLICATION_JSON ) @@ -150,14 +148,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 UsersGroup.model_validate(updated_group) is not None # nosec return envelope_json_response(updated_group) @@ -166,7 +164,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) @@ -178,13 +176,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) @@ -196,7 +196,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() @@ -204,7 +204,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 ) @@ -222,9 +222,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") @@ -235,12 +233,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 GroupUserGet.model_validate(user) is not None # nosec return envelope_json_response(user) @@ -252,7 +250,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( @@ -262,7 +260,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 GroupUserGet.model_validate(user) is not None # nosec return envelope_json_response(user) @@ -271,7 +269,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 @@ -352,7 +350,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( @@ -376,7 +374,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( @@ -392,4 +390,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/groups/api.py b/services/web/server/src/simcore_service_webserver/groups/api.py index e0a837f8399..ec0a615874e 100644 --- a/services/web/server/src/simcore_service_webserver/groups/api.py +++ b/services/web/server/src/simcore_service_webserver/groups/api.py @@ -32,7 +32,7 @@ async def list_all_user_groups(app: web.Application, user_id: UserID) -> list[Gr async with get_database_engine(app).acquire() as conn: groups_db = await _db.get_all_user_groups(conn, user_id=user_id) - return [Group.construct(**group.dict()) for group in groups_db] + return [Group.construct(**group.model_dump()) for group in groups_db] async def get_user_group( @@ -199,5 +199,5 @@ async def get_group_from_gid(app: web.Application, gid: GroupID) -> Group | None group_db = await _db.get_group_from_gid(conn, gid=gid) if group_db: - return Group.construct(**group_db.dict()) + return Group.construct(**group_db.model_dump()) return None 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..4ca20894b0e 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 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 ApiInvitationContent.model_validate(await response.json()) async def generate_invitation( self, params: ApiInvitationInputs @@ -95,7 +95,7 @@ async def generate_invitation( url=self._url_vtag("/invitations"), json=jsonable_encoder(params), ) - return parse_obj_as(ApiInvitationContentAndLink, await response.json()) + return ApiInvitationContentAndLink.model_validate(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 2bf18487638..fcd9e619742 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 ..groups.api import is_user_by_email_in_group @@ -92,7 +92,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 @@ -143,7 +143,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..c0aef7a6015 100644 --- a/services/web/server/src/simcore_service_webserver/login/_models.py +++ b/services/web/server/src/simcore_service_webserver/login/_models.py @@ -1,25 +1,26 @@ -from typing import Any, Callable +from typing import Callable -from pydantic import BaseModel, Extra, SecretStr +from pydantic import BaseModel, ConfigDict, SecretStr, ValidationInfo 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( reference_field: str, -) -> Callable[[SecretStr, dict[str, Any]], SecretStr]: - def _check(v: SecretStr, values: dict[str, Any]): +) -> Callable[[SecretStr, ValidationInfo], SecretStr]: + def _check(v: SecretStr, info: ValidationInfo): if ( v is not None - and reference_field in values - and v.get_secret_value() != values[reference_field].get_secret_value() + and reference_field in info.data + and v.get_secret_value() != info.data[reference_field].get_secret_value() ): raise ValueError(MSG_PASSWORD_MISMATCH) return v 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 1cfc53396d2..1a66b5ba4e5 100644 --- a/services/web/server/src/simcore_service_webserver/login/_registration.py +++ b/services/web/server/src/simcore_service_webserver/login/_registration.py @@ -19,9 +19,9 @@ Field, Json, PositiveInt, + TypeAdapter, ValidationError, - parse_obj_as, - validator, + field_validator, ) from servicelib.logging_errors import create_troubleshotting_log_kwargs from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON @@ -78,7 +78,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): @@ -256,7 +256,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( @@ -286,7 +286,8 @@ async def check_and_consume_invitation( ) _logger.info( - "Consuming invitation from service:\n%s", content.json(indent=1) + "Consuming invitation from service:\n%s", + content.model_dump_json(indent=1), ) return InvitationData( issuer=content.issuer, @@ -299,7 +300,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 b11dca73662..22252f2dc86 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.emails import LowerCaseEmailStr from models_library.utils.fastapi_encoders import jsonable_encoder 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 @@ -80,7 +82,7 @@ async def send_account_request_email_to_support( context={ "host": request.host, "name": "support-team", - "product": product.dict( + "product": product.model_dump( include={ "name", "display_name", 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 869fa7a2973..42e8229e7a6 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 @@ -98,7 +98,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_change.py b/services/web/server/src/simcore_service_webserver/login/handlers_change.py index f8b71ce8763..75c93ff990e 100644 --- a/services/web/server/src/simcore_service_webserver/login/handlers_change.py +++ b/services/web/server/src/simcore_service_webserver/login/handlers_change.py @@ -3,7 +3,7 @@ from aiohttp import web from aiohttp.web import RouteTableDef from models_library.emails import LowerCaseEmailStr -from pydantic import SecretStr, validator +from pydantic import SecretStr, field_validator from servicelib.aiohttp.requests_validation import parse_request_body_as from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON from servicelib.request_keys import RQT_USERID_KEY @@ -188,7 +188,7 @@ class ChangePasswordBody(InputSchema): new: SecretStr confirm: SecretStr - _password_confirm_match = validator("confirm", allow_reuse=True)( + _password_confirm_match = field_validator("confirm")( create_password_match_validator(reference_field="new") ) 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 63c4505c647..2fe63036378 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 @@ -12,9 +12,9 @@ Field, PositiveInt, SecretStr, + TypeAdapter, ValidationError, - parse_obj_as, - validator, + field_validator, ) from servicelib.aiohttp import status from servicelib.aiohttp.requests_validation import ( @@ -73,7 +73,7 @@ def _parse_extra_credits_in_usd_or_none( ) -> PositiveInt | None: with suppress(ValidationError, JSONDecodeError): confirmation_data = confirmation.get("data", "EMPTY") or "EMPTY" - invitation = InvitationData.parse_raw(confirmation_data) + invitation = InvitationData.model_validate_json(confirmation_data) return invitation.extra_credits_in_usd return None @@ -110,7 +110,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, ) @@ -265,9 +269,7 @@ class ResetPasswordConfirmation(InputSchema): password: SecretStr confirm: SecretStr - _password_confirm_match = validator("confirm", allow_reuse=True)( - check_confirm_password_match - ) + _password_confirm_match = field_validator("confirm")(check_confirm_password_match) @routes.post("/v0/auth/reset-password/{code}", name="auth_reset_password_allowed") 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 7c140184767..3d00ab57c03 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,12 +1,19 @@ import logging -from datetime import datetime, timedelta -from typing import Any, ClassVar, Literal +from datetime import UTC, datetime, timedelta +from typing import Literal from aiohttp import web from aiohttp.web import RouteTableDef from common_library.error_codes import create_error_code 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.logging_errors import create_troubleshotting_log_kwargs @@ -115,12 +122,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", @@ -130,6 +134,7 @@ class Config: } ] } + ) @routes.post(f"/{API_VTAG}/auth/register", name="auth_register") @@ -204,7 +209,7 @@ 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(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) @@ -244,7 +249,7 @@ async def register(request: web.Request): if settings.LOGIN_REGISTRATION_CONFIRMATION_REQUIRED: # Confirmation required: send confirmation email _confirmation: ConfirmationTokenDict = await db.create_confirmation( - user["id"], REGISTRATION, data=invitation.json() if invitation else None + user["id"], REGISTRATION, data=invitation.model_dump_json() if invitation else None ) try: diff --git a/services/web/server/src/simcore_service_webserver/login/plugin.py b/services/web/server/src/simcore_service_webserver/login/plugin.py index 174bcd55f4a..ef0c77c2f18 100644 --- a/services/web/server/src/simcore_service_webserver/login/plugin.py +++ b/services/web/server/src/simcore_service_webserver/login/plugin.py @@ -68,7 +68,7 @@ def setup_login_storage(app: web.Application): def _setup_login_options(app: web.Application): settings: SMTPSettings = get_email_plugin_settings(app) - cfg = settings.dict() + cfg = settings.model_dump() if INDEX_RESOURCE_NAME in app.router: cfg["LOGIN_REDIRECT"] = f"{app.router[INDEX_RESOURCE_NAME].url_for()}" 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..91ee1041889 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, ValidationInfo, field_validator from pydantic.fields import Field from pydantic.types import PositiveFloat, PositiveInt, SecretStr from settings_library.base import BaseCustomSettings @@ -36,7 +36,7 @@ class LoginSettings(BaseCustomSettings): ) LOGIN_TWILIO: TwilioSettings | None = Field( - auto_default_from_env=True, + json_schema_extra={"auto_default_from_env": True}, description="Twilio service settings. Used to send SMS for 2FA", ) @@ -54,19 +54,19 @@ 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): + def _login_2fa_needs_email_registration(cls, v, info: ValidationInfo): # NOTE: this constraint ensures that a phone is registered in current workflow - if v and not values.get("LOGIN_REGISTRATION_CONFIRMATION_REQUIRED", False): + if v and not info.data.get("LOGIN_REGISTRATION_CONFIRMATION_REQUIRED", False): msg = "Cannot enable 2FA w/o email confirmation" 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: + def _login_2fa_needs_sms_service(cls, v, info: ValidationInfo): + if v and info.data.get("LOGIN_TWILIO") is None: msg = "Cannot enable 2FA w/o twilio settings which is used to send SMS" raise ValueError(msg) return v @@ -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..847395e6acd 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, @@ -302,7 +302,7 @@ async def list_project_iterations(request: web.Request) -> web.Response: ) ) return web.Response( - text=page.json(**RESPONSE_MODEL_POLICY), + text=page.model_dump_json(**RESPONSE_MODEL_POLICY), content_type="application/json", ) @@ -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, @@ -405,6 +405,6 @@ def _get_project_results(project_id) -> ExtractedResults: ) ) return web.Response( - text=page.json(**RESPONSE_MODEL_POLICY), + text=page.model_dump_json(**RESPONSE_MODEL_POLICY), content_type="application/json", ) 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 4cfd2bfca24..b8adef9bcc4 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 @@ -280,7 +280,7 @@ async def get_or_create_runnable_projects( project["workbench"].update( { # converts model in dict patching first thumbnail - nid: n.copy(update={"thumbnail": n.thumbnail or ""}).dict( + nid: n.copy(update={"thumbnail": n.thumbnail or ""}).model_dump( by_alias=True, exclude_unset=True ) for nid, n in updated_nodes.items() @@ -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..150c2b8f680 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 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 = 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..67c5d39b65b 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 from servicelib.logging_utils import log_catch, log_context from servicelib.rabbitmq import RabbitMQClient from servicelib.utils import logged_gather @@ -58,17 +58,19 @@ async def _convert_to_node_update_event( "data": project["workbench"][f"{message.node_id}"], }, ) - _logger.warning("node not found: '%s'", message.dict()) + _logger.warning("node not found: '%s'", message.model_dump()) except ProjectNotFoundError: - _logger.warning("project not found: '%s'", message.dict()) + _logger.warning("project not found: '%s'", message.model_dump()) return None 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): @@ -95,13 +97,13 @@ async def _progress_message_parser(app: web.Application, data: bytes) -> bool: async def _log_message_parser(app: web.Application, data: bytes) -> bool: - rabbit_message = LoggerRabbitMessage.parse_raw(data) + rabbit_message = LoggerRabbitMessage.model_validate_json(data) await send_message_to_user( app, rabbit_message.user_id, message=SocketMessageDict( event_type=SOCKET_IO_LOG_EVENT, - data=rabbit_message.dict(exclude={"user_id", "channel_name"}), + data=rabbit_message.model_dump(exclude={"user_id", "channel_name"}), ), ignore_queue=True, ) @@ -109,7 +111,7 @@ async def _log_message_parser(app: web.Application, data: bytes) -> bool: async def _events_message_parser(app: web.Application, data: bytes) -> bool: - rabbit_message = EventRabbitMessage.parse_raw(data) + rabbit_message = EventRabbitMessage.model_validate_json(data) await send_message_to_user( app, rabbit_message.user_id, @@ -126,7 +128,7 @@ async def _events_message_parser(app: web.Application, data: bytes) -> bool: async def _osparc_credits_message_parser(app: web.Application, data: bytes) -> bool: - rabbit_message = parse_raw_as(WalletCreditsMessage, data) + rabbit_message = TypeAdapter(WalletCreditsMessage).validate_json(data) wallet_groups = await wallets_api.list_wallet_groups_with_read_access_by_wallet( app, wallet_id=rabbit_message.wallet_id ) diff --git a/services/web/server/src/simcore_service_webserver/notifications/_rabbitmq_nonexclusive_queue_consumers.py b/services/web/server/src/simcore_service_webserver/notifications/_rabbitmq_nonexclusive_queue_consumers.py index 0a8e04b5e7f..b6271be822a 100644 --- a/services/web/server/src/simcore_service_webserver/notifications/_rabbitmq_nonexclusive_queue_consumers.py +++ b/services/web/server/src/simcore_service_webserver/notifications/_rabbitmq_nonexclusive_queue_consumers.py @@ -23,12 +23,12 @@ async def _instrumentation_message_parser(app: web.Application, data: bytes) -> bool: - rabbit_message = InstrumentationRabbitMessage.parse_raw(data) + rabbit_message = InstrumentationRabbitMessage.model_validate_json(data) if rabbit_message.metrics == "service_started": service_started( app, **{ - key: rabbit_message.dict()[key] + key: rabbit_message.model_dump()[key] for key in MONITOR_SERVICE_STARTED_LABELS }, ) @@ -36,7 +36,7 @@ async def _instrumentation_message_parser(app: web.Application, data: bytes) -> service_stopped( app, **{ - key: rabbit_message.dict()[key] + key: rabbit_message.model_dump()[key] for key in MONITOR_SERVICE_STOPPED_LABELS }, ) 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..813fa6b9eb1 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 @@ -24,9 +24,7 @@ class PaymentsAutorechargeDB(BaseModel): primary_payment_method_id: PaymentMethodID top_up_amount_in_usd: NonNegativeDecimal monthly_limit_in_usd: NonNegativeDecimal | None - - class Config: - orm_mode = True + model_config = ConfigDict(from_attributes=True) async def get_wallet_autorecharge( @@ -38,7 +36,7 @@ async def get_wallet_autorecharge( stmt = AutoRechargeStmts.get_wallet_autorecharge(wallet_id) result = await conn.execute(stmt) row = await result.first() - return PaymentsAutorechargeDB.from_orm(row) if row else None + return PaymentsAutorechargeDB.model_validate(row) if row else None async def replace_wallet_autorecharge( @@ -75,4 +73,4 @@ async def replace_wallet_autorecharge( result = await conn.execute(stmt) row = await result.first() assert row # nosec - return PaymentsAutorechargeDB.from_orm(row) + return PaymentsAutorechargeDB.model_validate(row) 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..d19313d5bcc 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, @@ -79,7 +79,7 @@ async def _fake_init_creation_of_wallet_payment_method( await asyncio.sleep(1) payment_method_id = PaymentMethodID(f"{_FAKE_PAYMENT_METHOD_ID_PREFIX}_{uuid4()}") form_link = ( - URL(settings.PAYMENTS_FAKE_GATEWAY_URL) + URL(f"{settings.PAYMENTS_FAKE_GATEWAY_URL}") .with_path("/payment-methods/form") .with_query(id=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..3b2bcf8ede8 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, @@ -35,9 +35,7 @@ class PaymentsMethodsDB(BaseModel): completed_at: datetime.datetime | None 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 +79,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 +102,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( @@ -113,11 +111,14 @@ async def get_pending_payment_methods_ids( async with get_database_engine(app).acquire() as conn: result = await conn.execute( sa.select(payments_methods.c.payment_method_id) - .where(payments_methods.c.completed_at == None) # noqa: E711 + .where(payments_methods.c.completed_at.is_(None)) .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 +169,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 f54f48403bb..903e14ad002 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, @@ -49,7 +49,7 @@ def _to_api_model( "osparc_credits": transaction.osparc_credits, "wallet_id": transaction.wallet_id, "created_at": transaction.initiated_at, - "state": transaction.state, + "state": f"{transaction.state}", "completed_at": transaction.completed_at, } @@ -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) ) @@ -128,7 +128,7 @@ async def _ack_creation_of_wallet_payment( assert transaction.completed_at is not None # nosec assert transaction.initiated_at < transaction.completed_at # nosec - _logger.info("Transaction completed: %s", transaction.json(indent=1)) + _logger.info("Transaction completed: %s", transaction.model_dump_json(indent=1)) payment = _to_api_model(transaction) @@ -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..d6146cd0f81 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( @@ -103,7 +101,7 @@ async def complete_payment_transaction( payment_id=payment_id, completion_state=completion_state, state_message=state_message, - **optional_kwargs, + **optional_kwargs, # type: ignore[arg-type] ) if isinstance(row, PaymentNotFound): @@ -113,4 +111,4 @@ async def complete_payment_transaction( raise PaymentCompletedError(payment_id=row.payment_id) assert row # nosec - return PaymentsTransactionsDB.from_orm(row) + return PaymentsTransactionsDB.model_validate(row) 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 f2b88bc1765..5401568953c 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..8553e508b76 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,15 @@ 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, + ValidationInfo, + field_validator, +) from settings_library.base import BaseCustomSettings from settings_library.basic_types import PortInt, VersionTag from settings_library.utils_service import ( @@ -18,7 +26,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 +50,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 +92,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,10 +100,10 @@ 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"] + def _monthly_limit_greater_than_top_up(cls, v, info: ValidationInfo): + top_up = info.data["PAYMENTS_AUTORECHARGE_DEFAULT_TOP_UP_AMOUNT"] if v < 2 * top_up: msg = "PAYMENTS_AUTORECHARGE_DEFAULT_MONTHLY_LIMIT (={v}) should be at least twice PAYMENTS_AUTORECHARGE_DEFAULT_TOP_UP_AMOUNT ({top_up})" raise ValueError(msg) 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 bfdabef6d6f..3f9db2d2cab 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 models_library.api_schemas_webserver.product import GetCreditPrice, GetProduct from models_library.basic_types import IDStr 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..fa2bb927405 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,16 +55,16 @@ 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, invitation_link=f"{invitation_link}", # type: ignore[arg-type] ) - return envelope_json_response(invitation.dict(exclude_none=True)) + return envelope_json_response(invitation.model_dump(exclude_none=True)) 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..48f9f20fa01 100644 --- a/services/web/server/src/simcore_service_webserver/products/_model.py +++ b/services/web/server/src/simcore_service_webserver/products/_model.py @@ -1,9 +1,9 @@ import logging +import re import string from typing import ( # noqa: UP035 # pydantic does not validate with re.Pattern + Annotated, Any, - ClassVar, - Pattern, ) from models_library.basic_regex import ( @@ -14,7 +14,15 @@ 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_serializer, + field_validator, +) from simcore_postgres_database.models.products import ( EmailFeedback, Forum, @@ -40,19 +48,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 +91,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 +118,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 +126,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 +134,22 @@ 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() + @field_serializer("issues", "vendor") + def _preserve_snake_case(self, v: Any) -> Any: 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 +238,8 @@ class Config: "is_payment_enabled": False, }, ] - } + }, + ) # helpers ---- @@ -247,7 +252,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 +271,11 @@ 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 + and field.json_schema_extra.get("x_template_name") == template_name # type: ignore[union-attr] + ): + 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_api.py b/services/web/server/src/simcore_service_webserver/projects/_comments_api.py index a3626d099bb..55cfedac30c 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_comments_api.py +++ b/services/web/server/src/simcore_service_webserver/projects/_comments_api.py @@ -43,7 +43,8 @@ async def list_project_comments( ProjectsCommentsDB ] = await db.list_project_comments(project_uuid, offset, limit) projects_comments_api_model = [ - ProjectsCommentsAPI(**comment.dict()) for comment in projects_comments_db_model + ProjectsCommentsAPI(**comment.model_dump()) + for comment in projects_comments_db_model ] return projects_comments_api_model @@ -70,7 +71,7 @@ async def update_project_comment( comment_id, project_uuid, contents ) projects_comments_api_model = ProjectsCommentsAPI( - **projects_comments_db_model.dict() + **projects_comments_db_model.model_dump() ) return projects_comments_api_model @@ -90,6 +91,6 @@ async def get_project_comment( comment_id ) projects_comments_api_model = ProjectsCommentsAPI( - **projects_comments_db_model.dict() + **projects_comments_db_model.model_dump() ) return projects_comments_api_model 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..0cc52bea1e7 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) + ProjectsCommentsDB.model_validate(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 ProjectsCommentsDB.model_validate(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 ProjectsCommentsDB.model_validate(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 5325f389e9a..6ad8b290ba0 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 import status from servicelib.aiohttp.requests_validation import ( parse_request_body_as, @@ -60,24 +60,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( @@ -87,7 +81,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) @@ -119,9 +113,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") @@ -129,7 +121,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 @@ -155,7 +147,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, @@ -165,7 +157,7 @@ async def list_project_comments(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, ) @@ -177,7 +169,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 ) @@ -207,7 +199,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 ) @@ -235,7 +227,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 073c012a8ac..aacec646f05 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 ConfigDict, BaseModel, Field from servicelib.request_keys import RQT_USERID_KEY from .._constants import RQ_PRODUCT_KEY @@ -19,11 +19,7 @@ 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") class RemoveQueryParams(BaseModel): force: bool = Field( 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 4886b3b6242..d26a63a9cf8 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.users import UserID from models_library.utils.fastapi_encoders import jsonable_encoder 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 ( @@ -139,7 +139,7 @@ def _mapped_node_id(node: ProjectNode) -> NodeID: 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 +157,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 +180,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 +417,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() + f"{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 f6c98c6e08e..793dd19b083 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 @@ -53,11 +53,11 @@ async def _append_fields( # replace project access rights (if project is in workspace) if workspace_access_rights: project["accessRights"] = { - gid: access.dict() for gid, access in workspace_access_rights.items() + gid: access.model_dump() for gid, access in workspace_access_rights.items() } # 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 adbf8b0682e..a6a32fbb0bc 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 @@ -8,6 +8,7 @@ import logging from aiohttp import web +from common_library.json_serialization import json_dumps from models_library.api_schemas_webserver.projects import ( EmptyModel, ProjectCopyOverride, @@ -21,7 +22,6 @@ from models_library.rest_pagination import Page from models_library.rest_pagination_utils import paginate_data from models_library.utils.fastapi_encoders import jsonable_encoder -from pydantic import parse_obj_as from servicelib.aiohttp import status from servicelib.aiohttp.long_running_tasks.server import start_long_running_task from servicelib.aiohttp.requests_validation import ( @@ -109,17 +109,15 @@ async def _wrapper(request: web.Request) -> web.StreamResponse: routes = web.RouteTableDef() -# -# - Create https://google.aip.dev/133 -# - - @routes.post(f"/{VTAG}/projects", name="create_project") @login_required @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) + # + # - Create https://google.aip.dev/133 + # + req_ctx = RequestContext.model_validate(request) query_params: ProjectCreateParams = parse_request_query_parameters_as( ProjectCreateParams, request ) @@ -143,7 +141,7 @@ async def create_project(request: web.Request): ProjectCreateNew | ProjectCopyOverride | EmptyModel, request # type: ignore[arg-type] # from pydantic v2 --> https://github.com/pydantic/pydantic/discussions/4950 ) predefined_project = ( - project_create.dict( + project_create.model_dump( exclude_unset=True, by_alias=True, exclude_none=True, @@ -171,8 +169,6 @@ async def create_project(request: web.Request): ) -# - List https://google.aip.dev/132 -# @routes.get(f"/{VTAG}/projects", name="list_projects") @@ -180,13 +176,16 @@ async def create_project(request: web.Request): @permission_required("project.read") @_handle_projects_exceptions async def list_projects(request: web.Request): + # + # - List https://google.aip.dev/132 + # """ Raises: 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 ) @@ -206,12 +205,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=OrderBy.model_validate(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, @@ -221,7 +220,7 @@ async def list_projects(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, ) @@ -231,7 +230,7 @@ async def list_projects(request: web.Request): @permission_required("project.read") @_handle_projects_exceptions async def list_projects_full_search(request: web.Request): - req_ctx = RequestContext.parse_obj(request) + req_ctx = RequestContext.model_validate(request) query_params: ProjectListFullSearchWithJsonStrParams = ( parse_request_query_parameters_as( ProjectListFullSearchWithJsonStrParams, request @@ -250,7 +249,7 @@ async def list_projects_full_search(request: web.Request): tag_ids_list=tag_ids_list, ) - page = Page[ProjectDict].parse_obj( + page = Page[ProjectDict].model_validate( paginate_data( chunk=projects, request_url=request.url, @@ -260,28 +259,26 @@ async def list_projects_full_search(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, ) -# -# - Get https://google.aip.dev/131 -# - Get active project: Singleton per-session resources https://google.aip.dev/156 -# - - @routes.get(f"/{VTAG}/projects/active", name="get_active_project") @login_required @permission_required("project.read") async def get_active_project(request: web.Request) -> web.Response: + # + # - Get https://google.aip.dev/131 + # - Get active project: Singleton per-session resources https://google.aip.dev/156 + # """ Raises: 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 ) @@ -306,7 +303,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) @@ -327,7 +324,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( @@ -362,7 +359,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: @@ -389,18 +386,16 @@ async def get_project_inactivity(request: web.Request): return web.json_response(Envelope(data=project_inactivity), dumps=json_dumps) -# -# - Update https://google.aip.dev/134 -# - - @routes.patch(f"/{VTAG}/projects/{{project_id}}", name="patch_project") @login_required @permission_required("project.update") @permission_required("services.pipeline.*") @_handle_projects_exceptions async def patch_project(request: web.Request): - req_ctx = RequestContext.parse_obj(request) + # + # Update https://google.aip.dev/134 + # + req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ProjectPathParams, request) project_patch = await parse_request_body_as(ProjectPatch, request) @@ -416,7 +411,7 @@ async def patch_project(request: web.Request): # -# - Delete https://google.aip.dev/135 + # @@ -424,6 +419,7 @@ async def patch_project(request: web.Request): @login_required @permission_required("project.delete") async def delete_project(request: web.Request): + # Delete https://google.aip.dev/135 """ Raises: @@ -436,8 +432,7 @@ async def delete_project(request: web.Request): web.HTTPConflict: Somethine went wrong while deleting 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: @@ -512,7 +507,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 b1c499fd3a9..1f67baeabc0 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 @@ -20,12 +20,12 @@ from models_library.workspaces import WorkspaceID from pydantic import ( BaseModel, - Extra, + ConfigDict, Field, Json, - parse_obj_as, - root_validator, - validator, + TypeAdapter, + field_validator, + model_validator, ) from servicelib.common_headers import ( UNDEFINED_DEFAULT_SIMCORE_USER_AGENT_VALUE, @@ -57,7 +57,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 ( @@ -71,8 +71,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): @@ -92,9 +91,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 ProjectFilters(Filters): @@ -113,7 +110,7 @@ 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, @@ -124,19 +121,19 @@ class ProjectListParams(PageQueryParameters): description="Filter projects in specific workspace. Default filtering is a private workspace.", ) - @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) @@ -144,11 +141,11 @@ class ProjectListSortParams(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 def validate_order_by_field(cls, v): if v.field not in { @@ -164,8 +161,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( @@ -183,15 +179,15 @@ class ProjectListFullSearchParams(PageQueryParameters): default=None, description="Multi column full text search, across all folders and workspaces", max_length=100, - example="My Project", + examples=["My Project"], ) tag_ids: str | None = Field( default=None, description="Search by tag ID (multiple tag IDs may be provided separated by column)", - example="1,3", + examples=["1,3"], ) - _empty_is_none = validator("text", allow_reuse=True, pre=True)( + _empty_is_none = field_validator("text", mode="before")( empty_str_to_none_pre_validator ) @@ -205,7 +201,7 @@ def tag_ids_list(self) -> list[int]: if self.tag_ids: tag_ids_list = list(map(int, self.tag_ids.split(","))) # Validate that the tag_ids_list is indeed a list of integers - parse_obj_as(list[int], tag_ids_list) + TypeAdapter(list[int]).validate_python(tag_ids_list) else: tag_ids_list = [] except ValueError as exc: 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 35be5c4056c..6a5dab54cf9 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 @@ -191,7 +191,7 @@ async def _execute_without_permission_check( assert isinstance(row, RowProxy) # nosec try: await asyncio.get_event_loop().run_in_executor( - None, ProjectAtDB.from_orm, row + None, ProjectAtDB.model_validate, row ) except ProjectInvalidRightsError: @@ -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..59ea8ebe282 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 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 ProjectToFolderDB.model_validate(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 ProjectToFolderDB.model_validate(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 0e22c5970b9..2e644a4d598 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 ConfigDict, BaseModel, field_validator from servicelib.aiohttp import status from servicelib.aiohttp.requests_validation import parse_request_path_parameters_as from servicelib.aiohttp.typing_extension import Handler @@ -41,13 +41,11 @@ 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 = field_validator( + "folder_id", mode="before" )(null_or_none_str_to_none_validator) @@ -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..7ae45f0f90c 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 from ..users import api as users_api from . import _groups_db as projects_groups_db @@ -53,7 +53,9 @@ async def create_project_group( write=write, delete=delete, ) - project_group_api: ProjectGroupGet = ProjectGroupGet(**project_group_db.dict()) + project_group_api: ProjectGroupGet = ProjectGroupGet( + **project_group_db.model_dump() + ) return project_group_api @@ -78,7 +80,7 @@ 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 + ProjectGroupGet.model_validate(group.model_dump()) for group in project_groups_db ] return project_groups_api @@ -127,7 +129,7 @@ async def replace_project_group( ) ) - project_api: ProjectGroupGet = ProjectGroupGet(**project_group_db.dict()) + project_api: ProjectGroupGet = ProjectGroupGet(**project_group_db.model_dump()) return project_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..5b963b90cdb 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 ProjectGroupGetDB.model_validate(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 ProjectGroupGetDB.model_validate(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 ProjectGroupGetDB.model_validate(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 85c71d0d62d..a747798100e 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 ConfigDict, BaseModel from servicelib.aiohttp import status from servicelib.aiohttp.requests_validation import ( parse_request_body_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..4815ae19d03 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 @@ -20,8 +20,7 @@ NonNegativeFloat, NonNegativeInt, ValidationError, - parse_obj_as, - root_validator, + model_validator, ) from servicelib.utils import logged_gather @@ -96,10 +95,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 +172,7 @@ 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}), + SimCoreFileLink.model_validate({"store": "0", "path": file_meta_data.file_id}), ) @@ -228,7 +227,7 @@ async def get_node_screenshots( assert node.outputs is not None # nosec - filelink = parse_obj_as(SimCoreFileLink, node.outputs[KeyIDStr("outFile")]) + filelink = SimCoreFileLink.model_validate(node.outputs[KeyIDStr("outFile")]) file_url = await get_download_link(app, user_id, filelink) screenshots.append( @@ -240,7 +239,7 @@ async def get_node_screenshots( except (KeyError, ValidationError, ClientError) as err: _logger.warning( "Skipping fake node. Unable to create link from file-picker %s: %s", - node.json(indent=1), + node.model_dump_json(indent=1), err, ) 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 daeb1e454e0..b1088b67873 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.services_resources import ServiceResourcesDict from models_library.users import GroupID from models_library.utils.fastapi_encoders import jsonable_encoder -from pydantic import BaseModel, Field, parse_obj_as +from pydantic import BaseModel, Field from servicelib.aiohttp import status from servicelib.aiohttp.long_running_tasks.server import ( TaskProgress, @@ -145,7 +145,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) @@ -177,7 +177,7 @@ async def create_node(request: web.Request) -> web.Response: body.service_id, ) } - assert parse_obj_as(NodeCreated, data) is not None # nosec + assert NodeCreated.model_validate(data) is not None # nosec return envelope_json_response(data, status_cls=web.HTTPCreated) @@ -188,7 +188,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 @@ -226,7 +226,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) @@ -247,7 +247,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 @@ -295,7 +295,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) @@ -323,7 +323,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( @@ -367,7 +367,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( @@ -430,7 +430,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 @@ -463,7 +463,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) @@ -524,7 +524,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 @@ -623,7 +623,7 @@ async def get_project_services_access_for_gid( inaccessible_services=project_inaccessible_services, ) - return envelope_json_response(project_group_access.dict(exclude_none=True)) + return envelope_json_response(project_group_access.model_dump(exclude_none=True)) class _ProjectNodePreview(BaseModel): @@ -640,7 +640,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 @@ -650,7 +650,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( @@ -680,7 +680,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 @@ -690,7 +690,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 95a42f32046..9ae42c397c8 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 @@ -24,7 +24,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 @@ -163,8 +163,7 @@ 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, from_attributes=True) class _OutputPortInfo(NamedTuple): @@ -181,7 +180,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 40e33893428..eaacd9c1aa3 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.users import UserID from models_library.utils.fastapi_encoders import jsonable_encoder 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) @@ -148,7 +148,7 @@ async def update_project_inputs(request: web.Request) -> web.Response: raise web.HTTPBadRequest(reason=f"Invalid input key [{node_id}]") workbench[node_id].outputs = {KeyIDStr("out_1"): input_update.value} - partial_workbench_data[node_id] = workbench[node_id].dict( + partial_workbench_data[node_id] = workbench[node_id].model_dump( include={"outputs"}, exclude_unset=True ) @@ -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..05bb2f8e767 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 @@ -87,7 +87,7 @@ async def get_project_node_pricing_unit(request: web.Request): webserver_pricing_unit_get = PricingUnitGet( pricing_unit_id=pricing_unit_get.pricing_unit_id, unit_name=pricing_unit_get.unit_name, - unit_extra_info=pricing_unit_get.unit_extra_info, # type: ignore[arg-type] + unit_extra_info=pricing_unit_get.unit_extra_info, current_cost_per_unit=pricing_unit_get.current_cost_per_unit, default=pricing_unit_get.default, ) @@ -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 ) 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 ca0725b37b9..b2f5e46381c 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 @@ -245,4 +245,4 @@ async def get_project_state(request: web.Request) -> web.Response: include_state=True, ) project_state = ProjectState(**validated_project["state"]) - return envelope_json_response(project_state.dict()) + return envelope_json_response(project_state.model_dump()) diff --git a/services/web/server/src/simcore_service_webserver/projects/_tags_api.py b/services/web/server/src/simcore_service_webserver/projects/_tags_api.py index ba4be3c5fb4..c8e0937dbdb 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_tags_api.py +++ b/services/web/server/src/simcore_service_webserver/projects/_tags_api.py @@ -45,7 +45,8 @@ async def add_tag( ) ) 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() } return project @@ -79,7 +80,8 @@ async def remove_tag( ) ) 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() } return project diff --git a/services/web/server/src/simcore_service_webserver/projects/_wallets_api.py b/services/web/server/src/simcore_service_webserver/projects/_wallets_api.py index 6c5a27bed9e..85bc9cf43a3 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_wallets_api.py +++ b/services/web/server/src/simcore_service_webserver/projects/_wallets_api.py @@ -12,7 +12,9 @@ async def get_project_wallet(app, project_id: ProjectID): db: ProjectDBAPI = ProjectDBAPI.get_from_app_context(app) wallet_db: WalletDB | None = await db.get_project_wallet(project_uuid=project_id) - wallet: WalletGet | None = WalletGet(**wallet_db.dict()) if wallet_db else None + wallet: WalletGet | None = ( + WalletGet(**wallet_db.model_dump()) if wallet_db else None + ) return wallet 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 6b553a6d3ba..ff881b418af 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 @@ -1,11 +1,12 @@ import functools import logging +from typing import Annotated from aiohttp import web 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, BeforeValidator, ConfigDict, Field from servicelib.aiohttp import status from servicelib.aiohttp.requests_validation import parse_request_path_parameters_as from servicelib.aiohttp.typing_extension import Handler @@ -50,15 +51,9 @@ async def wrapper(request: web.Request) -> web.StreamResponse: class _ProjectWorkspacesPathParams(BaseModel): project_id: ProjectID - workspace_id: WorkspaceID | None + workspace_id: Annotated[WorkspaceID | None, BeforeValidator(null_or_none_str_to_none_validator)] = Field(default=None) - class Config: - extra = 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) + model_config = ConfigDict(extra="forbid") @routes.put( @@ -69,7 +64,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 6cbe059dfb7..a74375640fd 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( { @@ -757,7 +759,7 @@ async def get_project_db(self, project_uuid: ProjectID) -> ProjectDB: row = await result.fetchone() if row is None: raise ProjectNotFoundError(project_uuid=project_uuid) - return ProjectDB.from_orm(row) + return ProjectDB.model_validate(row) async def get_user_specific_project_data_db( self, project_uuid: ProjectID, private_workspace_user_id_or_none: UserID | None @@ -785,7 +787,7 @@ async def get_user_specific_project_data_db( row = await result.fetchone() if row is None: raise ProjectNotFoundError(project_uuid=project_uuid) - return UserSpecificProjectDataDB.from_orm(row) + return UserSpecificProjectDataDB.model_validate(row) async def get_pure_project_access_rights_without_workspace( self, user_id: UserID, project_uuid: ProjectID @@ -831,7 +833,7 @@ async def get_pure_project_access_rights_without_workspace( raise ProjectInvalidRightsError( user_id=user_id, project_uuid=project_uuid ) - return UserProjectAccessRightsDB.from_orm(row) + return UserProjectAccessRightsDB.model_validate(row) async def replace_project( self, @@ -926,7 +928,7 @@ async def patch_project( row = await result.fetchone() if row is None: raise ProjectNotFoundError(project_uuid=project_uuid) - return ProjectDB.from_orm(row) + return ProjectDB.model_validate(row) async def get_project_product(self, project_uuid: ProjectID) -> ProductName: async with self.engine.acquire() as conn: @@ -1368,7 +1370,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 WalletDB.model_validate(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 3141b7bca8d..84b24c087e7 100644 --- a/services/web/server/src/simcore_service_webserver/projects/lock.py +++ b/services/web/server/src/simcore_service_webserver/projects/lock.py @@ -33,7 +33,7 @@ async def lock_project( PROJECT_REDIS_LOCK_KEY.format(project_uuid), timeout=PROJECT_LOCK_TIMEOUT.total_seconds(), ) - owner = Owner(user_id=user_id, **user_fullname) # type: ignore[arg-type] + owner = Owner(user_id=user_id, **user_fullname) async with common_lock_project( redis_lock, project_uuid=project_uuid, status=status, owner=owner @@ -63,5 +63,5 @@ async def get_project_locked_state( if lock_value := await redis_locks_client.get( PROJECT_REDIS_LOCK_KEY.format(project_uuid) ): - return ProjectLocked.parse_raw(lock_value) + return ProjectLocked.model_validate_json(lock_value) return None 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 37961a9aff4..5b3e900b531 100644 --- a/services/web/server/src/simcore_service_webserver/projects/models.py +++ b/services/web/server/src/simcore_service_webserver/projects/models.py @@ -4,7 +4,6 @@ from aiopg.sa.result import RowProxy from models_library.api_schemas_webserver.projects import ProjectPatch -from models_library.basic_types import HttpUrlWithCustomMinLength from models_library.folders import FolderID from models_library.projects import ClassifierID, ProjectID from models_library.projects_ui import StudyUI @@ -14,7 +13,7 @@ none_to_empty_str_pre_validator, ) from models_library.workspaces import WorkspaceID -from pydantic import BaseModel, Extra, validator +from pydantic import BaseModel, ConfigDict, HttpUrl, field_validator from simcore_postgres_database.models.projects import ProjectType, projects ProjectDict: TypeAlias = dict[str, Any] @@ -41,7 +40,7 @@ class ProjectDB(BaseModel): uuid: ProjectID name: str description: str - thumbnail: HttpUrlWithCustomMinLength | None + thumbnail: HttpUrl | None prj_owner: UserID creation_date: datetime last_change_date: datetime @@ -54,14 +53,13 @@ class ProjectDB(BaseModel): workspace_id: WorkspaceID | None trashed_at: datetime | 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 ) @@ -69,11 +67,10 @@ class Config: class UserSpecificProjectDataDB(ProjectDB): folder_id: FolderID | None - class Config: - orm_mode = True + model_config = ConfigDict(from_attributes=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"]} ) @@ -83,9 +80,7 @@ class UserProjectAccessRightsDB(BaseModel): read: bool write: bool delete: bool - - class Config: - orm_mode = True + model_config = ConfigDict(from_attributes=True) class UserProjectAccessRightsWithWorkspace(BaseModel): @@ -94,18 +89,14 @@ class UserProjectAccessRightsWithWorkspace(BaseModel): read: bool write: bool delete: bool - - class Config: - orm_mode = True + model_config = ConfigDict(from_attributes=True) class ProjectPatchExtended(ProjectPatch): # Only used internally trashed_at: datetime | None = None - class Config: - allow_population_by_field_name = True - extra = Extra.forbid + model_config = ConfigDict(populate_by_name=True, extra="forbid") __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 d1c97856c54..d86be96058d 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.fastapi_encoders import jsonable_encoder 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, @@ -216,10 +216,11 @@ async def get_project_for_user( ) ) project["accessRights"] = { - gid: access.dict() for gid, access in workspace_db.access_rights.items() + f"{gid}": access.model_dump() + 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 @@ -379,7 +380,7 @@ 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 @@ -636,8 +637,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, @@ -793,7 +794,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, @@ -1419,7 +1420,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=next(iter(set_user_ids)), **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 @@ -1430,7 +1431,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=next(iter(set_user_ids)), **usernames[0]), status=ProjectStatus.OPENED, ) @@ -1484,7 +1485,7 @@ async def add_project_states_for_user( if prj_node is None: continue node_state_dict = json.loads( - node_state.json(by_alias=True, exclude_unset=True) + node_state.model_dump_json(by_alias=True, exclude_unset=True) ) prj_node.setdefault("state", {}).update(node_state_dict) prj_node_progress = node_state_dict.get("progress", None) or 0 @@ -1492,7 +1493,7 @@ async def add_project_states_for_user( project["state"] = ProjectState( locked=lock_state, state=ProjectRunningState(value=running_state) - ).dict(by_alias=True, exclude_unset=True) + ).model_dump(by_alias=True, exclude_unset=True) return project @@ -1510,8 +1511,8 @@ 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_bool: bool = datetime.datetime.now(datetime.UTC) > datetime.datetime.fromisoformat(deprecation_date).replace(tzinfo=datetime.UTC) + return deprecation_date_bool return False @@ -1546,8 +1547,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 @@ -1576,8 +1577,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 @@ -1597,7 +1598,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}" @@ -1866,4 +1869,4 @@ async def get_project_inactivity( project_settings.PROJECTS_INACTIVITY_INTERVAL.total_seconds() ), ) - return parse_obj_as(GetProjectInactivityResponse, project_inactivity) + return GetProjectInactivityResponse.model_validate(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 6f422e5fc33..7490c87ff55 100644 --- a/services/web/server/src/simcore_service_webserver/projects/settings.py +++ b/services/web/server/src/simcore_service_webserver/projects/settings.py @@ -2,7 +2,7 @@ from aiohttp import web from common_library.pydantic_validators import validate_numeric_string_as_timedelta -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 @@ -10,7 +10,7 @@ class ProjectsSettings(BaseCustomSettings): PROJECTS_MAX_COPY_SIZE_BYTES: ByteSize = Field( - default=parse_obj_as(ByteSize, "30Gib"), + default=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..63d5187a7d5 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 from servicelib.aiohttp import status from servicelib.aiohttp.client_session import get_client_session from settings_library.resource_usage_tracker import ResourceUsageTrackerSettings @@ -101,7 +101,7 @@ 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 PricingPlanGet.model_validate(body) except ClientResponseError as e: if e.status == status.HTTP_404_NOT_FOUND: raise DefaultPricingPlanNotFoundError from e @@ -130,7 +130,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 PricingUnitGet.model_validate(body) async def sum_total_available_credits_in_the_wallet( @@ -151,7 +151,7 @@ async def sum_total_available_credits_in_the_wallet( async with session.post(url) as response: response.raise_for_status() body: dict = await response.json() - return WalletTotalCredits.construct(**body) + return WalletTotalCredits.model_construct(**body) async def add_credits_to_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..762796afc07 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,7 @@ class _RequestContext(BaseModel): class _GetPricingPlanPathParams(BaseModel): pricing_plan_id: PricingPlanId - - class Config: - extra = Extra.forbid + model_config = ConfigDict(extra="forbid") @routes.get( @@ -85,7 +83,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 +114,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( @@ -138,7 +136,7 @@ async def get_pricing_plan(request: web.Request): PricingUnitAdminGet( pricing_unit_id=pricing_unit.pricing_unit_id, unit_name=pricing_unit.unit_name, - unit_extra_info=pricing_unit.unit_extra_info, # type: ignore[arg-type] + unit_extra_info=pricing_unit.unit_extra_info, specific_info=pricing_unit.specific_info, current_cost_per_unit=pricing_unit.current_cost_per_unit, default=pricing_unit.default, @@ -159,7 +157,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( @@ -187,7 +185,7 @@ async def create_pricing_plan(request: web.Request): PricingUnitAdminGet( pricing_unit_id=pricing_unit.pricing_unit_id, unit_name=pricing_unit.unit_name, - unit_extra_info=pricing_unit.unit_extra_info, # type: ignore[arg-type] + unit_extra_info=pricing_unit.unit_extra_info, specific_info=pricing_unit.specific_info, current_cost_per_unit=pricing_unit.current_cost_per_unit, default=pricing_unit.default, @@ -208,7 +206,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) @@ -237,7 +235,7 @@ async def update_pricing_plan(request: web.Request): PricingUnitAdminGet( pricing_unit_id=pricing_unit.pricing_unit_id, unit_name=pricing_unit.unit_name, - unit_extra_info=pricing_unit.unit_extra_info, # type: ignore[arg-type] + unit_extra_info=pricing_unit.unit_extra_info, specific_info=pricing_unit.specific_info, current_cost_per_unit=pricing_unit.current_cost_per_unit, default=pricing_unit.default, @@ -256,9 +254,7 @@ 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 +265,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( @@ -282,7 +278,7 @@ async def get_pricing_unit(request: web.Request): webserver_pricing_unit_get = PricingUnitAdminGet( pricing_unit_id=pricing_unit_get.pricing_unit_id, unit_name=pricing_unit_get.unit_name, - unit_extra_info=pricing_unit_get.unit_extra_info, # type: ignore[arg-type] + unit_extra_info=pricing_unit_get.unit_extra_info, specific_info=pricing_unit_get.specific_info, current_cost_per_unit=pricing_unit_get.current_cost_per_unit, default=pricing_unit_get.default, @@ -299,7 +295,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) @@ -321,7 +317,7 @@ async def create_pricing_unit(request: web.Request): webserver_pricing_unit_get = PricingUnitAdminGet( pricing_unit_id=pricing_unit_get.pricing_unit_id, unit_name=pricing_unit_get.unit_name, - unit_extra_info=pricing_unit_get.unit_extra_info, # type: ignore[arg-type] + unit_extra_info=pricing_unit_get.unit_extra_info, specific_info=pricing_unit_get.specific_info, current_cost_per_unit=pricing_unit_get.current_cost_per_unit, default=pricing_unit_get.default, @@ -338,7 +334,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) @@ -360,7 +356,7 @@ async def update_pricing_unit(request: web.Request): webserver_pricing_unit_get = PricingUnitAdminGet( pricing_unit_id=pricing_unit_get.pricing_unit_id, unit_name=pricing_unit_get.unit_name, - unit_extra_info=pricing_unit_get.unit_extra_info, # type: ignore[arg-type] + unit_extra_info=pricing_unit_get.unit_extra_info, specific_info=pricing_unit_get.specific_info, current_cost_per_unit=pricing_unit_get.current_cost_per_unit, default=pricing_unit_get.default, @@ -380,7 +376,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 +405,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..41d9be1ce09 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,7 @@ 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 +60,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 ) @@ -77,7 +75,7 @@ async def get_pricing_plan_unit(request: web.Request): webserver_pricing_unit_get = PricingUnitGet( pricing_unit_id=pricing_unit_get.pricing_unit_id, unit_name=pricing_unit_get.unit_name, - unit_extra_info=pricing_unit_get.unit_extra_info, # type: ignore[arg-type] + unit_extra_info=pricing_unit_get.unit_extra_info, current_cost_per_unit=pricing_unit_get.current_cost_per_unit, default=pricing_unit_get.default, ) 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 f265e45faf1..5a9dbf05849 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,11 @@ 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=OrderBy.model_validate(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 +178,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 +191,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,9 +211,9 @@ 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, + chunk=[item.model_dump() for item in aggregated_services.items], request_url=request.url, total=aggregated_services.total, limit=query_params.limit, @@ -226,7 +221,7 @@ async def list_osparc_credits_aggregated_usages(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, ) @@ -236,7 +231,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 +242,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 085b69cb0f6..b874d441db0 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 typing import Any from aiohttp import web -from pydantic import BaseModel, parse_obj_as +from pydantic import BaseModel from servicelib.aiohttp import status from .._constants import APP_PUBLIC_CONFIG_PER_PRODUCT, APP_SETTINGS_KEY @@ -104,7 +104,7 @@ 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 + _ScheduledMaintenanceGet.model_validate(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..2b02576e980 100644 --- a/services/web/server/src/simcore_service_webserver/scicrunch/_resolver.py +++ b/services/web/server/src/simcore_service_webserver/scicrunch/_resolver.py @@ -41,7 +41,7 @@ class HitSource(BaseModel): def flatten_dict(self) -> dict[str, Any]: """Used as an output""" - return {**self.item.dict(), **self.rrid.dict()} + return {**self.item.model_dump(), **self.rrid.model_dump()} class HitDetail(BaseModel): @@ -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..fd2f3d243a1 100644 --- a/services/web/server/src/simcore_service_webserver/scicrunch/_rest.py +++ b/services/web/server/src/simcore_service_webserver/scicrunch/_rest.py @@ -18,7 +18,7 @@ from typing import Any from aiohttp import ClientSession -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, RootModel from yarl import URL from .models import ResourceHit @@ -49,7 +49,7 @@ class ResourceView(BaseModel): @classmethod def from_response_payload(cls, payload: dict): - assert payload["success"] == True # nosec + assert payload["success"] is True # nosec return cls(**payload["data"]) @property @@ -72,8 +72,8 @@ def get_resource_url(self): return URL(str(self._get_field("Resource URL"))) -class ListOfResourceHits(BaseModel): - __root__: list[ResourceHit] +class ListOfResourceHits(RootModel[list[ResourceHit]]): + ... # REQUESTS @@ -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/db.py b/services/web/server/src/simcore_service_webserver/scicrunch/db.py index 476e320f73d..57e19bbed35 100644 --- a/services/web/server/src/simcore_service_webserver/scicrunch/db.py +++ b/services/web/server/src/simcore_service_webserver/scicrunch/db.py @@ -39,7 +39,7 @@ async def list_resources(self) -> list[ResearchResource]: ) res: ResultProxy = await conn.execute(stmt) rows: list[RowProxy] = await res.fetchall() - return [ResearchResource.from_orm(row) for row in rows] if rows else [] + return [ResearchResource.model_validate(row) for row in rows] if rows else [] async def get(self, rrid: str) -> ResearchResourceAtdB | None: async with self._engine.acquire() as conn: @@ -53,12 +53,12 @@ async def get(self, rrid: str) -> ResearchResourceAtdB | None: async def get_resource(self, rrid: str) -> ResearchResource | None: resource: ResearchResourceAtdB | None = await self.get(rrid) if resource: - return ResearchResource(**resource.dict()) + return ResearchResource(**resource.model_dump()) return resource async def upsert(self, resource: ResearchResource): async with self._engine.acquire() as conn: - values = resource.dict(exclude_unset=True) + values = resource.model_dump(exclude_unset=True) stmt = ( sa_pg_insert(scicrunch_resources) 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..2140f88ea33 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 field_validator, ConfigDict, BaseModel, Field logger = logging.getLogger(__name__) @@ -58,19 +58,16 @@ 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..ec8f43283b3 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 @@ -171,4 +171,4 @@ async def search_resource(self, name_as: str) -> list[ResourceHit]: # Might be slow and timeout! # Might be good to know that scicrunch.org is not reachable and cannot perform search now? hits = await autocomplete_by_name(name_as, self.client, self.settings) - return hits.__root__ + return hits.root 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..300130b6f82 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/access_policies.py b/services/web/server/src/simcore_service_webserver/session/access_policies.py index 05736703563..8fd72f71caf 100644 --- a/services/web/server/src/simcore_service_webserver/session/access_policies.py +++ b/services/web/server/src/simcore_service_webserver/session/access_policies.py @@ -7,7 +7,7 @@ from aiohttp import web from aiohttp_session import Session -from pydantic import PositiveInt, validate_arguments +from pydantic import PositiveInt, validate_call from servicelib.aiohttp import status from servicelib.aiohttp.typing_extension import Handler @@ -64,7 +64,7 @@ def _access_tokens_cleanup_ctx(session: Session) -> Iterator[dict[str, _AccessTo session[_SESSION_GRANTED_ACCESS_TOKENS_KEY] = pruned_access_tokens -@validate_arguments +@validate_call def on_success_grant_session_access_to( name: str, *, 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..f6fec9a878e 100644 --- a/services/web/server/src/simcore_service_webserver/session/settings.py +++ b/services/web/server/src/simcore_service_webserver/session/settings.py @@ -1,10 +1,10 @@ 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 pydantic_settings import SettingsConfigDict from settings_library.base import BaseCustomSettings from settings_library.utils_session import MixinSessionSettings @@ -22,7 +22,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( @@ -52,13 +54,17 @@ class SessionSettings(BaseCustomSettings, MixinSessionSettings): default=True, description="This prevents JavaScript from accessing the session cookie", ) + + model_config = SettingsConfigDict( + extra="allow" + ) - @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..37bb942298b 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,21 @@ 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 ConfigDict, BaseModel, Field class WebSocketMessageBase(BaseModel): - event_type: str = Field(..., const=True) + event_type: str = Field(frozen=True) @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): @@ -87,7 +85,7 @@ class WebSocketNodeProgress( def from_rabbit_message( cls, message: ProgressRabbitMessageNode ) -> "WebSocketNodeProgress": - return cls.construct( + return cls.model_construct( user_id=message.user_id, project_id=message.project_id, node_id=message.node_id, 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 4348fa5ef3d..b34f7e8948a 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..7a9c13f4e33 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 @@ -93,7 +93,7 @@ class FrontEndAppSettings(BaseCustomSettings): # NOTE: for the moment, None but left here for future use def to_statics(self) -> dict[str, Any]: - data = self.dict( + data = self.model_dump( exclude_none=True, by_alias=True, ) @@ -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..83372296dd2 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 @@ -229,7 +229,7 @@ class _PathParams(BaseModel): parse_request_path_parameters_as(_PathParams, request) class _QueryParams(BaseModel): - file_size: ByteSize | None + file_size: ByteSize | None = None link_type: LinkType = LinkType.PRESIGNED is_directory: bool = False @@ -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 ) @@ -262,10 +262,10 @@ class _PathParams(BaseModel): body_item = await parse_request_body_as(FileUploadCompletionBody, request) payload, status = await _forward_request_to_storage( - request, "POST", body=body_item.dict() + request, "POST", body=body_item.model_dump() ) 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..26381218c0e 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,18 +14,18 @@ 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): - __root__: list[FileLocation] +class FileLocationArray(RootModel[list[FileLocation]]): + ... class Links(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): @@ -122,8 +122,8 @@ class FileMetaData(BaseModel): 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,10 +138,11 @@ class Config: "is_directory": False, } } + ) -class FileMetaDataArray(BaseModel): - __root__: list[FileMetaData] +class FileMetaDataArray(RootModel[list[FileMetaData]]): + ... class FileMetaEnvelope(BaseModel): @@ -152,8 +153,7 @@ 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..04ac00f61c3 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,6 @@ from aiohttp import web from models_library.basic_types import PortInt, VersionTag -from pydantic import parse_obj_as from settings_library.base import BaseCustomSettings from settings_library.utils_service import DEFAULT_AIOHTTP_PORT, MixinServiceSettings from yarl import URL @@ -12,8 +11,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 = DEFAULT_AIOHTTP_PORT + STORAGE_VTAG: VersionTag = "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..e9250b18a01 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, @@ -97,7 +97,7 @@ async def iter_latest_product_services( ) & (services_meta_data.c.deprecated.is_(None)) & (services_access_rights.c.gid == EVERYONE_GROUP_ID) - & (services_access_rights.c.execute_access == True) + & (services_access_rights.c.execute_access == sa.true()) & (services_access_rights.c.product_name == product_name) ) ) @@ -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, []), ) @@ -161,7 +161,7 @@ async def validate_requested_service( sa.select(services_consume_filetypes.c.is_guest_allowed) .where( (services_consume_filetypes.c.service_key == service_key) - & (services_consume_filetypes.c.is_guest_allowed == True) + & (services_consume_filetypes.c.is_guest_allowed == sa.true()) ) .limit(1) ) @@ -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..5cff412f415 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 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 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 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 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..53f61713a43 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 @@ -13,12 +13,12 @@ from aiohttp import web from models_library.projects import DateTimeStr, Project, ProjectID -from models_library.projects_access import AccessRights +from models_library.projects_access import AccessRights, GroupIDStr from models_library.projects_nodes import Node 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 + data["label"] = Path(output_label).name # type: ignore[assignment] elif url.path: - data["label"] = Path(url.path).name - output = DownloadLink.parse_obj(data) + data["label"] = Path(url.path).name # type: ignore[assignment] + 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] + accessRights={GroupIDStr(owner.primary_gid): access_rights}, 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, ) @@ -194,7 +196,9 @@ async def _add_new_project( db: ProjectDBAPI = app[APP_PROJECT_DBAPI] # validated project is transform in dict via json to use only primitive types - project_in: dict = json.loads(project.json(exclude_none=True, by_alias=True)) + project_in: dict = json.loads( + project.model_dump_json(exclude_none=True, by_alias=True) + ) # update metadata (uuid, timestamps, ownership) and save _project_db: dict = await db.insert_project( @@ -344,7 +348,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 237aed0f7fd..647a523f23c 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 @@ -12,7 +12,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 field_validator, ConfigDict, BaseModel, ValidationError from servicelib.aiohttp import status from servicelib.aiohttp.requests_validation import parse_request_query_parameters_as from servicelib.aiohttp.typing_extension import Handler @@ -153,15 +153,13 @@ 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 @@ -172,14 +170,13 @@ def ensure_extension_upper_and_dotless(cls, v): class ServiceAndFileParams(FileQueryParams, ServiceParams): - class Config: - # Optional configuration to exclude duplicates from schema - schema_extra = { + model_config = ConfigDict( + json_schema_extra={ "allOf": [ {"$ref": "#/definitions/FileParams"}, {"$ref": "#/definitions/ServiceParams"}, ] - } + }) class ViewerQueryParams(BaseModel): @@ -190,13 +187,13 @@ class ViewerQueryParams(BaseModel): @staticmethod def from_viewer(viewer: ViewerInfo) -> "ViewerQueryParams": # can safely construct w/o validation from a viewer - return ViewerQueryParams.construct( + return ViewerQueryParams.model_construct( file_type=viewer.filetype, viewer_key=viewer.key, 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..b003ad55963 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,19 @@ """ import logging from dataclasses import asdict -from typing import Any, ClassVar from aiohttp import web from aiohttp.web import Request 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 +38,11 @@ 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(HttpUrl).validate_python(f"{absolute_url}") return absolute_url_ @@ -46,11 +52,11 @@ def _compose_service_only_dispatcher_prefix_url( params = ViewerQueryParams( viewer_key=ServiceKey(service_key), viewer_version=ServiceVersion(service_version), - ).dict(exclude_none=True, exclude_unset=True) + ).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(HttpUrl).validate_python(f"{absolute_url}") return absolute_url_ @@ -125,15 +131,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 +149,7 @@ class Config: "view_url": "https://host.com/view?viewer_key=simcore/services/dynamic/raw-graphs&viewer_version=1.2.3", } } + ) # @@ -177,7 +184,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 +196,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..568a534832a 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 UTC, datetime 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,7 +80,9 @@ 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 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 a79c4865f12..00024ce2d23 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,10 +1,11 @@ from datetime import timedelta -from typing import Any, ClassVar +from typing import Annotated from aiohttp import web from common_library.pydantic_validators import validate_numeric_string_as_timedelta -from pydantic import ByteSize, HttpUrl, parse_obj_as, validator +from pydantic import ByteSize, HttpUrl, 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 @@ -22,22 +23,26 @@ class StudiesDispatcherSettings(BaseCustomSettings): ) STUDIES_DEFAULT_SERVICE_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" + ), 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"), + default=TypeAdapter(HttpUrl).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: @@ -55,13 +60,14 @@ def is_login_required(self): "STUDIES_GUEST_ACCOUNT_LIFETIME" ) - 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/_api.py b/services/web/server/src/simcore_service_webserver/tags/_api.py index 6f3a74853e7..dacedc603f7 100644 --- a/services/web/server/src/simcore_service_webserver/tags/_api.py +++ b/services/web/server/src/simcore_service_webserver/tags/_api.py @@ -22,7 +22,7 @@ async def create_tag( read=True, write=True, delete=True, - **new_tag.dict(exclude_unset=True), + **new_tag.model_dump(exclude_unset=True), ) return TagGet.from_db(tag) @@ -46,7 +46,7 @@ async def update_tag( tag = await repo.update( user_id=user_id, tag_id=tag_id, - **tag_updates.dict(exclude_unset=True), + **tag_updates.model_dump(exclude_unset=True), ) return TagGet.from_db(tag) 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 8862f0320c1..24dff16d066 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,7 @@ import functools from aiohttp import web -from pydantic import parse_obj_as +from pydantic import TypeAdapter from servicelib.aiohttp import status from servicelib.aiohttp.requests_validation import ( parse_request_body_as, @@ -58,7 +58,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( @@ -73,7 +73,7 @@ async def create_tag(request: web.Request): @_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) @@ -83,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) @@ -101,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( @@ -124,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 01663e0d337..0851c99f99a 100644 --- a/services/web/server/src/simcore_service_webserver/tags/schemas.py +++ b/services/web/server/src/simcore_service_webserver/tags/schemas.py @@ -1,9 +1,10 @@ import re from datetime import datetime +from typing import Annotated from models_library.api_schemas_webserver._base import InputSchema, OutputSchema from models_library.users import GroupID, UserID -from pydantic import ConstrainedStr, 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 @@ -17,8 +18,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 = 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..458366367f5 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(), ) @@ -116,7 +116,7 @@ async def pre_register_user( if found: raise AlreadyPreRegisteredError(num_found=len(found), email=profile.email) - details = profile.dict( + details = profile.model_dump( include={ "first_name", "last_name", diff --git a/services/web/server/src/simcore_service_webserver/users/_db.py b/services/web/server/src/simcore_service_webserver/users/_db.py index f7d8769f963..2071034d2e6 100644 --- a/services/web/server/src/simcore_service_webserver/users/_db.py +++ b/services/web/server/src/simcore_service_webserver/users/_db.py @@ -212,4 +212,4 @@ async def get_user_billing_details( user_billing_details = await UsersRepo.get_billing_details(conn, user_id) if not user_billing_details: raise BillingDetailsNotFoundError(user_id=user_id) - return UserBillingDetails.from_orm(user_billing_details) + return UserBillingDetails.model_validate(user_billing_details) 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 3462602f74b..4d69e9ffaab 100644 --- a/services/web/server/src/simcore_service_webserver/users/_handlers.py +++ b/services/web/server/src/simcore_service_webserver/users/_handlers.py @@ -68,7 +68,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 ) @@ -80,7 +80,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 @@ -105,7 +105,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( @@ -117,7 +117,7 @@ async def search_users(request: web.Request) -> web.Response: ) return envelope_json_response( - [_.dict(**_RESPONSE_MODEL_MINIMAL_POLICY) for _ in found] + [_.model_dump(**_RESPONSE_MODEL_MINIMAL_POLICY) for _ in found] ) @@ -126,7 +126,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: @@ -134,7 +134,7 @@ async def pre_register_user(request: web.Request) -> web.Response: request.app, profile=pre_user_profile, creator_user_id=req_ctx.user_id ) return envelope_json_response( - user_profile.dict(**_RESPONSE_MODEL_MINIMAL_POLICY) + user_profile.model_dump(**_RESPONSE_MODEL_MINIMAL_POLICY) ) except AlreadyPreRegisteredError as err: raise web.HTTPConflict(reason=f"{err}") from err 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..39e6fda9208 100644 --- a/services/web/server/src/simcore_service_webserver/users/_notifications.py +++ b/services/web/server/src/simcore_service_webserver/users/_notifications.py @@ -6,7 +6,7 @@ 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 field_validator, ConfigDict, BaseModel, NonNegativeInt 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,9 @@ class UserNotification(BaseUserNotification): def create_from_request_data( cls, request_data: UserNotificationCreate ) -> "UserNotification": - return cls.construct(id=f"{uuid4()}", read=False, **request_data.dict()) - - class Config: - schema_extra: ClassVar[dict[str, Any]] = { + return cls.model_construct(id=f"{uuid4()}", read=False, **request_data.model_dump()) + model_config = ConfigDict( + json_schema_extra={ "examples": [ { "id": "3fb96d89-ff5d-4d27-b5aa-d20d46e20eb8", @@ -120,3 +119,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 3a9588d39a5..58fb1a483e5 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 @@ -80,7 +80,7 @@ async def create_user_notification(request: web.Request) -> web.Response: # insert at the head of the list and discard extra notifications redis_client = get_redis_user_notifications_client(request.app) async with redis_client.pipeline(transaction=True) as pipe: - pipe.lpush(key, user_notification.json()) + pipe.lpush(key, user_notification.model_dump_json()) pipe.ltrim(key, 0, MAX_NOTIFICATIONS_FOR_USER_TO_KEEP - 1) await pipe.execute() @@ -99,21 +99,21 @@ 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) # NOTE: only the user's notifications can be patched key = get_notification_key(req_ctx.user_id) all_user_notifications: list[UserNotification] = [ - UserNotification.parse_raw(x) + UserNotification.model_validate_json(x) for x in await handle_redis_returns_union_types(redis_client.lrange(key, 0, -1)) ] for k, user_notification in enumerate(all_user_notifications): if req_path_params.notification_id == user_notification.id: user_notification.read = body.read await handle_redis_returns_union_types( - redis_client.lset(key, k, user_notification.json()) + redis_client.lset(key, k, user_notification.model_dump_json()) ) return web.json_response(status=status.HTTP_204_NO_CONTENT) @@ -124,13 +124,15 @@ 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 ) return envelope_json_response( [ - PermissionGet.construct(_fields_set=p.__fields_set__, **p.dict()) + PermissionGet.model_construct( + _fields_set=p.model_fields_set, **p.model_dump() + ) for p in list_permissions ] ) 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 0c537278f9c..9581b46b335 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/_preferences_models.py b/services/web/server/src/simcore_service_webserver/users/_preferences_models.py index 01b6b87e377..6a871bcfafe 100644 --- a/services/web/server/src/simcore_service_webserver/users/_preferences_models.py +++ b/services/web/server/src/simcore_service_webserver/users/_preferences_models.py @@ -132,7 +132,7 @@ class BillingCenterUsageColumnOrderFrontendUserPreference(FrontendUserPreference ] _PREFERENCE_NAME_TO_IDENTIFIER_MAPPING: dict[PreferenceName, PreferenceIdentifier] = { - p.get_preference_name(): p.__fields__["preference_identifier"].default + p.get_preference_name(): p.model_fields["preference_identifier"].default for p in ALL_FRONTEND_PREFERENCES } _PREFERENCE_IDENTIFIER_TO_NAME_MAPPING: dict[PreferenceIdentifier, PreferenceName] = { 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..8df6c890a8b 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, ValidationInfo, field_validator, model_validator from simcore_postgres_database.models.users import UserStatus @@ -33,7 +33,7 @@ class UserProfile(OutputSchema): ) # authorization - invited_by: str | None = None + invited_by: str | None = Field(default=None) # user status registered: bool @@ -43,10 +43,10 @@ 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"] + def _consistency_check(cls, v, info: ValidationInfo): + registered = info.data["registered"] status = v if not registered and status is not None: msg = f"{registered=} and {status=} is not allowed" @@ -61,12 +61,12 @@ class PreUserProfile(InputSchema): first_name: str last_name: str email: LowerCaseEmailStr - institution: str | None = Field(None, description="company, university, ...") + institution: str | None = Field(default=None, description="company, university, ...") phone: str | None # billing details address: str city: str - state: str | None + state: str | None = Field(default=None) postal_code: str country: str extras: dict[str, Any] = Field( @@ -74,11 +74,9 @@ 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 def _preprocess_aliases_and_extras(cls, values): # multiple aliases for "institution" @@ -92,8 +90,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 +109,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 +118,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 +129,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 40b884c4eb9..9f5dfc941b8 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) return web.json_response(status=status.HTTP_204_NO_CONTENT) 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 52736a1e8d8..50dfdc4e12d 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 models_library.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 @@ -159,7 +159,7 @@ async def update_user_profile( user_id = _parse_as_user(user_id) async with get_database_engine(app).acquire() as conn: - to_update = update.dict( + to_update = update.model_dump( include={ "first_name", "last_name", diff --git a/services/web/server/src/simcore_service_webserver/users/exceptions.py b/services/web/server/src/simcore_service_webserver/users/exceptions.py index 51fb1cc2b19..39791ea39fe 100644 --- a/services/web/server/src/simcore_service_webserver/users/exceptions.py +++ b/services/web/server/src/simcore_service_webserver/users/exceptions.py @@ -9,12 +9,16 @@ class UsersBaseError(WebServerBaseError): class UserNotFoundError(UsersBaseError): def __init__(self, *, uid: int | None = None, email: str | None = None, **ctx: Any): - super().__init__(**ctx) + super().__init__( + msg_template=( + "User id {uid} not found" + if uid + else f"User with email {email} not found" + ), + **ctx, + ) self.uid = uid self.email = email - self.msg_template = ( - "User id {uid} not found" if uid else f"User with email {email} not found" - ) class TokenNotFoundError(UsersBaseError): 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 cae3f5b7df5..0f2f8ee1684 100644 --- a/services/web/server/src/simcore_service_webserver/users/schemas.py +++ b/services/web/server/src/simcore_service_webserver/users/schemas.py @@ -8,7 +8,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 pydantic import BaseModel, Field, root_validator, validator +from pydantic import field_validator, model_validator, ConfigDict, BaseModel, Field from simcore_postgres_database.models.users import UserRole from ..utils import gravatar_hash @@ -28,13 +28,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): @@ -50,13 +51,14 @@ 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 +76,11 @@ class ProfileGet(BaseModel): ) preferences: AggregatedPreferences - class Config: + model_config = ConfigDict( # 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]] = { + populate_by_name=True, + json_schema_extra={ "examples": [ { "id": 1, @@ -92,14 +92,15 @@ class Config: { "id": 42, "login": "bla@foo.com", - "role": UserRole.ADMIN, + "role": UserRole.ADMIN.value, "expirationDate": "2022-09-14", "preferences": {}, }, ] } + ) - @root_validator(pre=True) + @model_validator(mode="before") @classmethod def _auto_generate_gravatar(cls, values): gravatar_id = values.get("gravatar_id") @@ -108,7 +109,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 657e347d1a6..0807cee96b1 100644 --- a/services/web/server/src/simcore_service_webserver/utils.py +++ b/services/web/server/src/simcore_service_webserver/utils.py @@ -11,11 +11,12 @@ import tracemalloc from datetime import datetime from pathlib import Path -from typing import Any, TypedDict, cast +from typing import Any, cast import orjson from common_library.error_codes import ErrorCodeStr from models_library.basic_types import SHA1Str +from typing_extensions import TypedDict _CURRENT_DIR = ( Path(sys.argv[0] if __name__ == "__main__" else __file__).resolve().parent @@ -190,4 +191,4 @@ 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 SHA1Str(hashlib.sha1(data_bytes).hexdigest()) # nosec # NOSONAR 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 4adaa5ef468..bb60b8a1b8f 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 common_library.json_serialization import json_dumps from models_library.generics import Envelope 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 @@ -71,7 +70,7 @@ def envelope_json_response( enveloped = Envelope[Any](data=obj) return web.Response( - text=json_dumps(enveloped.dict(**RESPONSE_MODEL_POLICY)), + text=json_dumps(enveloped.model_dump(**RESPONSE_MODEL_POLICY)), content_type=MIMETYPE_APPLICATION_JSON, status=status_cls.status_code, ) @@ -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..860d124ce48 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 @@ -12,7 +12,7 @@ from uuid import UUID from aiopg.sa.result import RowProxy -from pydantic import NonNegativeInt, PositiveInt, validate_arguments +from pydantic import NonNegativeInt, PositiveInt, validate_call from .db import VersionControlRepository from .errors import CleanRequiredError @@ -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) # @@ -146,10 +146,10 @@ async def get_workbench( _CONFIG = {"arbitrary_types_allowed": True} -list_repos_safe = validate_arguments(list_repos, config=_CONFIG) # type: ignore -list_checkpoints_safe = validate_arguments(list_checkpoints, config=_CONFIG) # type: ignore -create_checkpoint_safe = validate_arguments(create_checkpoint, config=_CONFIG) # type: ignore -get_checkpoint_safe = validate_arguments(get_checkpoint, config=_CONFIG) # type: ignore -update_checkpoint_safe = validate_arguments(update_checkpoint, config=_CONFIG) # type: ignore -checkout_checkpoint_safe = validate_arguments(checkout_checkpoint, config=_CONFIG) # type: ignore -get_workbench_safe = validate_arguments(get_workbench, config=_CONFIG) # type: ignore +list_repos_safe = validate_call(list_repos, config=_CONFIG) # type: ignore +list_checkpoints_safe = validate_call(list_checkpoints, config=_CONFIG) # type: ignore +create_checkpoint_safe = validate_call(create_checkpoint, config=_CONFIG) # type: ignore +get_checkpoint_safe = validate_call(get_checkpoint, config=_CONFIG) # type: ignore +update_checkpoint_safe = validate_call(update_checkpoint, config=_CONFIG) # type: ignore +checkout_checkpoint_safe = validate_call(checkout_checkpoint, config=_CONFIG) # type: ignore +get_workbench_safe = validate_call(get_workbench, config=_CONFIG) # type: ignore 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..22b6270019c 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, @@ -100,7 +100,7 @@ async def _list_repos_handler(request: web.Request): ) ) return web.Response( - text=page.json(**RESPONSE_MODEL_POLICY), + text=page.model_dump_json(**RESPONSE_MODEL_POLICY), content_type="application/json", ) @@ -116,22 +116,22 @@ 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, project_uuid=path_params.project_uuid, - **_body.dict(include={"tag", "message"}), + **_body.model_dump(include={"tag", "message"}), ) - data = CheckpointApiModel.parse_obj( + data = CheckpointApiModel.model_validate( { "url": url_for( "get_checkpoint", project_uuid=path_params.project_uuid, ref_id=checkpoint.id, ), - **checkpoint.dict(), + **checkpoint.model_dump(), } ) return envelope_json_response(data, status_cls=web.HTTPCreated) @@ -163,20 +163,20 @@ async def _list_checkpoints_handler(request: web.Request): # parse and validate checkpoints_list = [ - CheckpointApiModel.parse_obj( + CheckpointApiModel.model_validate( { "url": url_for( "get_checkpoint", project_uuid=path_params.project_uuid, ref_id=checkpoint.id, ), - **checkpoint.dict(), + **checkpoint.model_dump(), } ) for checkpoint in checkpoints ] - page = Page[CheckpointApiModel].parse_obj( + page = Page[CheckpointApiModel].model_validate( paginate_data( chunk=checkpoints_list, request_url=request.url, @@ -186,7 +186,7 @@ async def _list_checkpoints_handler(request: web.Request): ) ) return web.Response( - text=page.json(**RESPONSE_MODEL_POLICY), + text=page.model_dump_json(**RESPONSE_MODEL_POLICY), content_type="application/json", ) @@ -211,14 +211,14 @@ 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", project_uuid=path_params.project_uuid, ref_id=checkpoint.id, ), - **checkpoint.dict(**RESPONSE_MODEL_POLICY), + **checkpoint.model_dump(**RESPONSE_MODEL_POLICY), } ) return envelope_json_response(data) @@ -242,17 +242,17 @@ async def _update_checkpoint_annotations_handler(request: web.Request): vc_repo, project_uuid=path_params.project_uuid, ref_id=path_params.ref_id, - **update.dict(include={"tag", "message"}, exclude_none=True), + **update.model_dump(include={"tag", "message"}, exclude_none=True), ) - data = CheckpointApiModel.parse_obj( + data = CheckpointApiModel.model_validate( { "url": url_for( "get_checkpoint", project_uuid=path_params.project_uuid, ref_id=checkpoint.id, ), - **checkpoint.dict(**RESPONSE_MODEL_POLICY), + **checkpoint.model_dump(**RESPONSE_MODEL_POLICY), } ) return envelope_json_response(data) @@ -277,14 +277,14 @@ 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", project_uuid=path_params.project_uuid, ref_id=checkpoint.id, ), - **checkpoint.dict(**RESPONSE_MODEL_POLICY), + **checkpoint.model_dump(**RESPONSE_MODEL_POLICY), } ) return envelope_json_response(data) @@ -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( @@ -328,7 +328,7 @@ async def _view_project_workbench_handler(request: web.Request): project_uuid=path_params.project_uuid, ref_id=checkpoint.id, ), - **view.dict(**RESPONSE_MODEL_POLICY), + **view.model_dump(**RESPONSE_MODEL_POLICY), } ) diff --git a/services/web/server/src/simcore_service_webserver/version_control/_handlers_base.py b/services/web/server/src/simcore_service_webserver/version_control/_handlers_base.py index c40b27946f2..3424788fafa 100644 --- a/services/web/server/src/simcore_service_webserver/version_control/_handlers_base.py +++ b/services/web/server/src/simcore_service_webserver/version_control/_handlers_base.py @@ -4,7 +4,7 @@ from aiohttp import web from common_library.json_serialization import json_dumps -from pydantic.error_wrappers import ValidationError +from pydantic import ValidationError from servicelib.aiohttp.typing_extension import Handler from ..projects.exceptions import ProjectNotFoundError 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..451d302d90f 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,7 @@ 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 ConfigDict, BaseModel, Field, PositiveInt, StrictBool, StrictFloat, StrictInt from pydantic.networks import HttpUrl BuiltinTypes: TypeAlias = Union[StrictBool, StrictInt, StrictFloat, str] @@ -24,7 +24,7 @@ CommitID: TypeAlias = int BranchID: TypeAlias = int -RefID: TypeAlias = Union[CommitID, str] +RefID: TypeAlias = CommitID | str CheckpointID: TypeAlias = PositiveInt @@ -35,7 +35,7 @@ class Checkpoint(BaseModel): created_at: datetime tags: tuple[str, ...] message: str | None = None - parents_ids: tuple[PositiveInt, ...] = Field(default=None) + parents_ids: tuple[PositiveInt, ...] | None = Field(default=None) @classmethod def from_commit_log(cls, commit: RowProxy, tags: list[RowProxy]) -> "Checkpoint": @@ -44,16 +44,14 @@ def from_commit_log(cls, commit: RowProxy, tags: list[RowProxy]) -> "Checkpoint" checksum=commit.snapshot_checksum, tags=tuple(tag.name for tag in tags), message=commit.message, - parents_ids=(commit.parent_commit_id,) if commit.parent_commit_id else None, # type: ignore[arg-type] + parents_ids=(commit.parent_commit_id,) if commit.parent_commit_id else None, created_at=commit.created, ) 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/version_control/vc_changes.py b/services/web/server/src/simcore_service_webserver/version_control/vc_changes.py index 50d2cae1e76..cc3559c118b 100644 --- a/services/web/server/src/simcore_service_webserver/version_control/vc_changes.py +++ b/services/web/server/src/simcore_service_webserver/version_control/vc_changes.py @@ -31,7 +31,7 @@ def compute_workbench_checksum(workbench: dict[str, Any]) -> SHA1Str: checksum = compute_sha1_on_small_dataset( { - k: node.dict( + k: node.model_dump( exclude_unset=True, exclude_defaults=True, exclude_none=True, 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 8a528fe5db2..c2af4074378 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 = WalletGet.model_validate(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( @@ -178,7 +180,7 @@ async def update_wallet( user_id=user_id, wallet_id=wallet_id, product_name=product_name, - user_acces_rights_on_wallet=wallet.dict( + user_acces_rights_on_wallet=wallet.model_dump( include={"read", "write", "delete"} ), ) @@ -193,7 +195,7 @@ async def update_wallet( product_name=product_name, ) - wallet_api: WalletGet = parse_obj_as(WalletGet, wallet_db) + wallet_api: WalletGet = WalletGet.model_validate(wallet_db) return wallet_api @@ -212,7 +214,7 @@ async def delete_wallet( user_id=user_id, wallet_id=wallet_id, product_name=product_name, - user_acces_rights_on_wallet=wallet.dict( + user_acces_rights_on_wallet=wallet.model_dump( include={"read", "write", "delete"} ), ) @@ -235,7 +237,7 @@ async def get_wallet_by_user( user_id=user_id, wallet_id=wallet_id, product_name=product_name, - user_acces_rights_on_wallet=wallet.dict( + user_acces_rights_on_wallet=wallet.model_dump( include={"read", "write", "delete"} ), ) @@ -263,5 +265,5 @@ 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 = WalletGetPermissions.model_validate(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..413b68ff84f 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,6 @@ 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 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 +46,7 @@ async def create_wallet( .returning(literal_column("*")) ) row = await result.first() - return parse_obj_as(WalletDB, row) + return WalletDB.model_validate(row) _SELECTION_ARGS = ( @@ -98,7 +97,7 @@ 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] = [UserWalletDB.model_validate(row) for row in rows] return output @@ -160,7 +159,7 @@ async def get_wallet_for_user( wallet_id=wallet_id, product_name=product_name, ) - return parse_obj_as(UserWalletDB, row) + return UserWalletDB.model_validate(row) async def get_wallet( @@ -188,7 +187,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 WalletDB.model_validate(row) async def update_wallet( @@ -219,7 +218,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 WalletDB.model_validate(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..bdace14a9de 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, ConfigDict from ..users import api as users_api from . import _db as wallets_db @@ -23,6 +23,10 @@ class WalletGroupGet(BaseModel): delete: bool created: datetime modified: datetime + + model_config = ConfigDict( + from_attributes=True + ) async def create_wallet_group( @@ -45,7 +49,7 @@ async def create_wallet_group( user_id=user_id, wallet_id=wallet_id, product_name=product_name, - user_acces_rights_on_wallet=wallet.dict( + user_acces_rights_on_wallet=wallet.model_dump( include={"read", "write", "delete"} ), ) @@ -58,7 +62,7 @@ async def create_wallet_group( write=write, delete=delete, ) - wallet_group_api: WalletGroupGet = WalletGroupGet(**wallet_group_db.dict()) + wallet_group_api: WalletGroupGet = WalletGroupGet(**wallet_group_db.model_dump()) return wallet_group_api @@ -79,7 +83,7 @@ async def list_wallet_groups_by_user_and_wallet( user_id=user_id, wallet_id=wallet_id, product_name=product_name, - user_acces_rights_on_wallet=wallet.dict( + user_acces_rights_on_wallet=wallet.model_dump( include={"read", "write", "delete"} ), ) @@ -89,7 +93,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 + WalletGroupGet.model_validate(group) for group in wallet_groups_db ] return wallet_groups_api @@ -105,7 +109,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) + WalletGroupGet.model_validate(group) for group in wallet_groups_db if group.read is True ] @@ -140,7 +144,7 @@ async def update_wallet_group( user_id=user_id, wallet_id=wallet_id, product_name=product_name, - user_acces_rights_on_wallet=wallet.dict( + user_acces_rights_on_wallet=wallet.model_dump( include={"read", "write", "delete"} ), ) @@ -154,7 +158,7 @@ async def update_wallet_group( delete=delete, ) - wallet_api: WalletGroupGet = WalletGroupGet(**wallet_group_db.dict()) + wallet_api: WalletGroupGet = WalletGroupGet(**wallet_group_db.model_dump()) return wallet_api 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..949978a470f 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 WalletGroupGetDB.model_validate(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 WalletGroupGetDB.model_validate(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 WalletGroupGetDB.model_validate(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 1115a239d62..b8477396e26 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 import status from servicelib.aiohttp.requests_validation import ( parse_request_body_as, @@ -60,18 +60,14 @@ 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 +77,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 +100,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 +123,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 +147,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 3aa26158acb..90ac4572f6f 100644 --- a/services/web/server/src/simcore_service_webserver/wallets/_handlers.py +++ b/services/web/server/src/simcore_service_webserver/wallets/_handlers.py @@ -128,7 +128,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( @@ -148,7 +148,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 @@ -164,7 +164,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( @@ -179,7 +179,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 = ( @@ -202,7 +202,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 9a03bc2d2a5..66c73b5a293 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..cca4da82e4e 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, ConfigDict from ..users import api as users_api from . import _groups_db as workspaces_groups_db @@ -24,6 +24,10 @@ class WorkspaceGroupGet(BaseModel): delete: bool created: datetime modified: datetime + + model_config = ConfigDict( + from_attributes=True + ) async def create_workspace_group( @@ -56,7 +60,7 @@ async def create_workspace_group( ) ) workspace_group_api: WorkspaceGroupGet = WorkspaceGroupGet( - **workspace_group_db.dict() + **workspace_group_db.model_dump() ) return workspace_group_api @@ -84,7 +88,7 @@ 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 + WorkspaceGroupGet.model_validate(group) for group in workspace_groups_db ] return workspace_groups_api @@ -102,7 +106,7 @@ async def list_workspace_groups_with_read_access_by_workspace( ) workspace_groups_api: list[WorkspaceGroupGet] = [ - parse_obj_as(WorkspaceGroupGet, group) + WorkspaceGroupGet.model_validate(group) for group in workspace_groups_db if group.read is True ] @@ -147,7 +151,9 @@ async def update_workspace_group( ) ) - workspace_api: WorkspaceGroupGet = WorkspaceGroupGet(**workspace_group_db.dict()) + workspace_api: WorkspaceGroupGet = WorkspaceGroupGet( + **workspace_group_db.model_dump() + ) return workspace_api 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..c186786f603 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 ConfigDict, BaseModel from simcore_postgres_database.models.workspaces_access_rights import ( workspaces_access_rights, ) @@ -31,9 +31,7 @@ class WorkspaceGroupGetDB(BaseModel): delete: bool created: datetime modified: datetime - - class Config: - orm_mode = True + model_config = ConfigDict(from_attributes=True) ## DB API @@ -63,7 +61,7 @@ async def create_workspace_group( .returning(literal_column("*")) ) row = await result.first() - return WorkspaceGroupGetDB.from_orm(row) + return WorkspaceGroupGetDB.model_validate(row) async def list_workspace_groups( @@ -86,7 +84,7 @@ async def list_workspace_groups( async with get_database_engine(app).acquire() as conn: result = await conn.execute(stmt) rows = await result.fetchall() or [] - return [WorkspaceGroupGetDB.from_orm(row) for row in rows] + return [WorkspaceGroupGetDB.model_validate(row) for row in rows] async def get_workspace_group( @@ -117,7 +115,7 @@ async def get_workspace_group( raise WorkspaceGroupNotFoundError( workspace_id=workspace_id, group_id=group_id ) - return WorkspaceGroupGetDB.from_orm(row) + return WorkspaceGroupGetDB.model_validate(row) async def update_workspace_group( @@ -148,7 +146,7 @@ async def update_workspace_group( raise WorkspaceGroupNotFoundError( workspace_id=workspace_id, group_id=group_id ) - return WorkspaceGroupGetDB.from_orm(row) + return WorkspaceGroupGetDB.model_validate(row) async def delete_workspace_group( 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 d4ae7c4b74f..22572cee19c 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 import status from servicelib.aiohttp.requests_validation import ( parse_request_body_as, @@ -60,18 +60,14 @@ 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 +78,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 +101,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 +124,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 +148,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_db.py b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_db.py index 23de15c3b19..4f007bc7552 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_db.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_db.py @@ -66,7 +66,7 @@ async def create_workspace( .returning(*_SELECTION_ARGS) ) row = await result.first() - return WorkspaceDB.from_orm(row) + return WorkspaceDB.model_validate(row) access_rights_subquery = ( @@ -155,7 +155,7 @@ async def list_workspaces_for_user( result = await conn.execute(list_query) rows = await result.fetchall() or [] results: list[UserWorkspaceAccessRightsDB] = [ - UserWorkspaceAccessRightsDB.from_orm(row) for row in rows + UserWorkspaceAccessRightsDB.model_validate(row) for row in rows ] return cast(int, total_count), results @@ -191,7 +191,7 @@ async def get_workspace_for_user( raise WorkspaceAccessForbiddenError( reason=f"User {user_id} does not have access to the workspace {workspace_id}. Or workspace does not exist.", ) - return UserWorkspaceAccessRightsDB.from_orm(row) + return UserWorkspaceAccessRightsDB.model_validate(row) async def update_workspace( @@ -220,7 +220,7 @@ async def update_workspace( row = await result.first() if row is None: raise WorkspaceNotFoundError(reason=f"Workspace {workspace_id} not found.") - return WorkspaceDB.from_orm(row) + return WorkspaceDB.model_validate(row) async def delete_workspace( 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 fa9a2e4aa67..3a22237865b 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 field_validator, ConfigDict, Field, Json from servicelib.aiohttp import status from servicelib.aiohttp.requests_validation import ( RequestParams, @@ -75,11 +75,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,9 +92,7 @@ def validate_order_by_field(cls, v): if v.field == "modified_at": v.field = "modified" return v - - class Config: - extra = Extra.forbid + model_config = ConfigDict(extra="forbid") @routes.post(f"/{VTAG}/workspaces", name="create_workspace") @@ -102,7 +100,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( @@ -122,7 +120,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) ) @@ -133,10 +131,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=OrderBy.model_validate(query_params.order_by), ) - page = Page[WorkspaceGet].parse_obj( + page = Page[WorkspaceGet].model_validate( paginate_data( chunk=workspaces.items, request_url=request.url, @@ -146,7 +144,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, ) @@ -156,7 +154,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( @@ -177,7 +175,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) @@ -201,7 +199,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 8aa8ea86309..97a96fa2847 100644 --- a/services/web/server/tests/conftest.py +++ b/services/web/server/tests/conftest.py @@ -327,9 +327,9 @@ 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)}") + print(f"<-- status: {task_status.model_dump_json(indent=2)}") assert task_status.done, "task incomplete" print( f"-- project creation completed: {json.dumps(attempt.retry_state.retry_object.statistics, indent=2)}" @@ -403,7 +403,7 @@ async def _creator( # the access rights are set to use the logged user primary group + whatever was inside the project expected_data["accessRights"].update( { - str(primary_group["gid"]): { + f"{primary_group['gid']}": { "read": True, "write": True, "delete": True, diff --git a/services/web/server/tests/integration/02/notifications/test_rabbitmq_consumers.py b/services/web/server/tests/integration/02/notifications/test_rabbitmq_consumers.py index c15921c7d5d..f1d4aa62187 100644 --- a/services/web/server/tests/integration/02/notifications/test_rabbitmq_consumers.py +++ b/services/web/server/tests/integration/02/notifications/test_rabbitmq_consumers.py @@ -294,7 +294,7 @@ async def test_log_workflow_only_receives_messages_if_subscribed( log_message.user_id, message={ "event_type": SOCKET_IO_LOG_EVENT, - "data": log_message.dict(exclude={"user_id", "channel_name"}), + "data": log_message.model_dump(exclude={"user_id", "channel_name"}), }, ignore_queue=True, ), @@ -493,7 +493,7 @@ async def test_instrumentation_workflow( mocked_metrics_method, mock.call( client.app, - **rabbit_message.dict(include=set(included_labels)), + **rabbit_message.model_dump(include=set(included_labels)), ), ) 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..4ae3ca6a3e1 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", @@ -159,7 +159,7 @@ async def test_scicrunch_service_autocomplete_by_name(settings: SciCrunchSetting }, {"rid": "SCR_014398", "original_id": "SCR_014398", "name": "GNU Octave"}, ] - ).dict()["__root__"] + ).model_dump()["root"] async with ClientSession() as client: @@ -167,6 +167,6 @@ async def test_scicrunch_service_autocomplete_by_name(settings: SciCrunchSetting resource_hits = await autocomplete_by_name("octave", client, settings) - hits = resource_hits.dict()["__root__"] + hits = resource_hits.model_dump()["root"] assert expected == hits, f"for {guess_name}" diff --git a/services/web/server/tests/unit/isolated/conftest.py b/services/web/server/tests/unit/isolated/conftest.py index 9cc0948ff88..31f0aa33f98 100644 --- a/services/web/server/tests/unit/isolated/conftest.py +++ b/services/web/server/tests/unit/isolated/conftest.py @@ -96,6 +96,7 @@ def mock_env_deployer_pipeline(monkeypatch: pytest.MonkeyPatch) -> EnvVarsDict: def mock_env_devel_environment( mock_env_devel_environment: EnvVarsDict, # pylint: disable=redefined-outer-name monkeypatch: pytest.MonkeyPatch, + faker: Faker ) -> EnvVarsDict: # Overrides to ensure dev-features are enabled testings return mock_env_devel_environment | setenvs_from_dict( @@ -218,8 +219,8 @@ def mock_webserver_service_environment( "STORAGE_PORT": os.environ.get("STORAGE_PORT", "8080"), "SWARM_STACK_NAME": os.environ.get("SWARM_STACK_NAME", "simcore"), "WEBSERVER_LOGLEVEL": os.environ.get("LOG_LEVEL", "WARNING"), - "SESSION_COOKIE_MAX_AGE": str(7 * 24 * 60 * 60), - }, + "SESSION_COOKIE_MAX_AGE": str(7 * 24 * 60 * 60) + } ) return ( diff --git a/services/web/server/tests/unit/isolated/notifications/test_rabbitmq_consumers.py b/services/web/server/tests/unit/isolated/notifications/test_rabbitmq_consumers.py index 62840490d68..4a9bc655df2 100644 --- a/services/web/server/tests/unit/isolated/notifications/test_rabbitmq_consumers.py +++ b/services/web/server/tests/unit/isolated/notifications/test_rabbitmq_consumers.py @@ -32,7 +32,7 @@ node_id=UUID("6925403d-5464-4d92-9ec9-72c5793ca203"), progress_type=ProgressType.SERVICE_OUTPUTS_PULLING, report=ProgressReport(actual_value=0.4, total=1), - ).json(), + ).model_dump_json(), SocketMessageDict( event_type=WebSocketNodeProgress.get_event_type(), data={ @@ -56,7 +56,7 @@ user_id=123, progress_type=ProgressType.PROJECT_CLOSING, report=ProgressReport(actual_value=0.4, total=1), - ).json(), + ).model_dump_json(), SocketMessageDict( event_type=WebSocketNodeProgress.get_event_type(), data={ 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 17e3502bfa5..84c8ee46871 100644 --- a/services/web/server/tests/unit/isolated/test_application_settings.py +++ b/services/web/server/tests/unit/isolated/test_application_settings.py @@ -7,7 +7,7 @@ import pytest from aiohttp import web from common_library.json_serialization import json_dumps -from pydantic import HttpUrl, parse_obj_as +from pydantic import HttpUrl, TypeAdapter from pytest_simcore.helpers.typing_env import EnvVarsDict from simcore_service_webserver.application_settings import ( APP_SETTINGS_KEY, @@ -22,10 +22,11 @@ def app_settings( ) -> ApplicationSettings: app = web.Application() + print("envs\n", json.dumps(mock_webserver_service_environment, indent=1)) + # init and validation happens here settings = setup_settings(app) - print("envs\n", json.dumps(mock_webserver_service_environment, indent=1)) - print("settings:\n", settings.json(indent=1)) + print("settings:\n", settings.model_dump_json(indent=1)) assert APP_SETTINGS_KEY in app assert app[APP_SETTINGS_KEY] == settings @@ -97,7 +98,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_application_settings_utils.py b/services/web/server/tests/unit/isolated/test_application_settings_utils.py index f4f0f901199..2897b8bf358 100644 --- a/services/web/server/tests/unit/isolated/test_application_settings_utils.py +++ b/services/web/server/tests/unit/isolated/test_application_settings_utils.py @@ -19,7 +19,7 @@ def test_settings_infered_from_default_tests_config( settings = ApplicationSettings.create_from_envs() - print("settings=\n", settings.json(indent=1, sort_keys=True)) + print("settings=\n", settings.model_dump_json(indent=1, sort_keys=True)) infered_config = convert_to_app_config(settings) 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_diagnostics_healthcheck.py b/services/web/server/tests/unit/isolated/test_diagnostics_healthcheck.py index f35d7991539..b35b2b378f4 100644 --- a/services/web/server/tests/unit/isolated/test_diagnostics_healthcheck.py +++ b/services/web/server/tests/unit/isolated/test_diagnostics_healthcheck.py @@ -5,9 +5,10 @@ # pylint: disable=unused-variable import asyncio +import json import logging -import time from collections.abc import Callable, Coroutine +import time import pytest import simcore_service_webserver @@ -88,11 +89,13 @@ def mock_environment( { **mock_env_devel_environment, "AIODEBUG_SLOW_DURATION_SECS": f"{SLOW_HANDLER_DELAY_SECS / 10}", - "DIAGNOSTICS_MAX_TASK_DELAY": f"{SLOW_HANDLER_DELAY_SECS}", - "DIAGNOSTICS_MAX_AVG_LATENCY": f"{2.0}", - "DIAGNOSTICS_START_SENSING_DELAY": f"{0}", + "WEBSERVER_DIAGNOSTICS": json.dumps({ + "DIAGNOSTICS_MAX_AVG_LATENCY": "2.0", + "DIAGNOSTICS_MAX_TASK_DELAY": f"{SLOW_HANDLER_DELAY_SECS}", + "DIAGNOSTICS_START_SENSING_DELAY": f"{0}", + "DIAGNOSTICS_HEALTHCHECK_ENABLED": "1", + }), "SC_HEALTHCHECK_TIMEOUT": "2m", - "DIAGNOSTICS_HEALTHCHECK_ENABLED": "1", }, ) 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..944a958baf2 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,23 @@ 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(x) + for x in NodeGet.model_config["json_schema_extra"]["examples"] + ], + NodeGetIdle.model_validate( + NodeGetIdle.model_config["json_schema_extra"]["example"] + ), + DynamicServiceGet.model_validate( + DynamicServiceGet.model_config["json_schema_extra"]["examples"][0] ), ], ) @@ -98,9 +103,12 @@ 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(x) + for x in NodeGet.model_config["json_schema_extra"]["examples"] + ], + 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_statics.py b/services/web/server/tests/unit/isolated/test_isolated_statics.py similarity index 100% rename from services/web/server/tests/unit/isolated/test_statics.py rename to services/web/server/tests/unit/isolated/test_isolated_statics.py diff --git a/services/web/server/tests/unit/isolated/test_login_settings.py b/services/web/server/tests/unit/isolated/test_login_settings.py index b6872fce92d..0bfff446911 100644 --- a/services/web/server/tests/unit/isolated/test_login_settings.py +++ b/services/web/server/tests/unit/isolated/test_login_settings.py @@ -121,15 +121,15 @@ def test_smtp_settings(mock_env_devel_environment: dict[str, Any]): settings = SMTPSettings.create_from_envs() - cfg = settings.dict(exclude_unset=True) + cfg = settings.model_dump(exclude_unset=True) for env_name in cfg: assert env_name in os.environ - cfg = settings.dict() + cfg = settings.model_dump() config = LoginOptions(**cfg) - print(config.json(indent=1)) + print(config.model_dump_json(indent=1)) assert not hasattr(config, "SMTP_SENDER"), "was deprecated and now we use product" @@ -137,6 +137,6 @@ def test_smtp_settings(mock_env_devel_environment: dict[str, Any]): def test_product_login_settings_in_plugin_settings(): # pylint: disable=no-member customizable_attributes = set(ProductLoginSettingsDict.__annotations__.keys()) - settings_atrributes = set(LoginSettingsForProduct.__fields__.keys()) + settings_atrributes = set(LoginSettingsForProduct.model_fields.keys()) assert customizable_attributes.issubset(settings_atrributes) 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 31c4ac60699..147540adce6 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", @@ -78,7 +82,7 @@ def test_product_to_static(): def test_product_host_regex_with_spaces(): - data = Product.Config.schema_extra["examples"][2] + data = Product.model_config["json_schema_extra"]["examples"][2] # with leading and trailing spaces and uppercase (tests anystr_strip_whitespace ) data["support_email"] = " fOO@BaR.COM " @@ -88,7 +92,7 @@ def test_product_host_regex_with_spaces(): data["host_regex"] = expected + " " # parsing should strip all whitespaces and normalize email - product = Product.parse_obj(data) + product = Product.model_validate(data) assert product.host_regex.pattern == expected assert product.host_regex.search("osparc.bar.com") 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..e7e4bd8a926 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,6 @@ import pytest from models_library.api_schemas_storage import FileMetaDataGet -from pydantic import parse_obj_as from simcore_service_webserver.projects._nodes_api import ( _SUPPORTED_PREVIEW_FILE_EXTENSIONS, _FileWithThumbnail, @@ -12,13 +11,12 @@ _PROJECT_ID = uuid4() _NODE_ID = uuid4() -_UTC_NOW = datetime.datetime.now(tz=datetime.timezone.utc) +_UTC_NOW = datetime.datetime.now(tz=datetime.UTC) def _c(file_name: str) -> FileMetaDataGet: """simple converter utility""" - return parse_obj_as( - FileMetaDataGet, + return FileMetaDataGet.model_validate( { "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..70ca1ce3b9d 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,15 +54,19 @@ 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( resources: ServiceResourcesDict, ): new_resources = { - resource_name: resource_data.copy(update={"image": "some-invalid-image-name"}) + resource_name: resource_data.model_copy( + update={"image": "some-invalid-image-name"} + ) for resource_name, resource_data in resources.items() } with pytest.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 e60cab4985b..079fa68e529 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"]: @@ -219,7 +219,7 @@ def client( # mocks 'setup_session': patch to avoid setting up all ApplicationSettings session_settings = SessionSettings.create_from_envs() - print(session_settings.json(indent=1)) + print(session_settings.model_dump_json(indent=1)) mocker.patch( "simcore_service_webserver.session.plugin.get_plugin_settings", autospec=True, 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..ef842a25a98 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 @@ -46,11 +46,11 @@ async def test_create_project_with_viewer(view: dict[str, Any]): assert list(project.workbench.keys()) # converts into equivalent Dict - project_in: dict = json.loads(project.json(exclude_none=True, by_alias=True)) + project_in: dict = json.loads(project.model_dump_json(exclude_none=True, by_alias=True)) 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..24570916463 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 @@ -63,25 +63,25 @@ def test_download_link_validators_1(url_in: str, expected_download_link: str): @pytest.fixture def file_and_service_params() -> dict[str, Any]: - return dict( - file_name="dataset_description.slsx", - file_size=_SIZEBYTES, - file_type="MSExcel", - viewer_key="simcore/services/dynamic/fooo", - viewer_version="1.0.0", - download_link=_DOWNLOAD_LINK, - ) + return { + "file_name": "dataset_description.slsx", + "file_size": _SIZEBYTES, + "file_type": "MSExcel", + "viewer_key": "simcore/services/dynamic/fooo", + "viewer_version": "1.0.0", + "download_link": _DOWNLOAD_LINK, + } 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 - assert params.download_link.host and params.download_link.host.endswith( + assert params.download_link.host + assert params.download_link.host.endswith( "s3.amazonaws.com" ) - assert params.download_link.host_type == "domain" query = parse_qs(params.download_link.query) assert {"AWSAccessKeyId", "Signature", "Expires", "x-amz-request-payer"} == set( @@ -105,12 +105,12 @@ def test_file_and_service_params(file_and_service_params: dict[str, Any]): def test_file_only_params(): - request_params = dict( - file_name="dataset_description.slsx", - file_size=_SIZEBYTES, - file_type="MSExcel", - download_link=_DOWNLOAD_LINK, - ) + request_params = { + "file_name": "dataset_description.slsx", + "file_size": _SIZEBYTES, + "file_type": "MSExcel", + "download_link": _DOWNLOAD_LINK, + } file_params = parse_obj_or_none(FileParams, request_params) assert file_params @@ -125,10 +125,10 @@ def test_file_only_params(): def test_service_only_params(): - request_params = dict( - viewer_key="simcore/services/dynamic/fooo", - viewer_version="1.0.0", - ) + request_params = { + "viewer_key": "simcore/services/dynamic/fooo", + "viewer_version": "1.0.0", + } file_params = parse_obj_or_none(FileParams, request_params) assert not file_params diff --git a/services/web/server/tests/unit/isolated/test_studies_dispatcher_projects_permalinks.py b/services/web/server/tests/unit/isolated/test_studies_dispatcher_projects_permalinks.py index 03a0eb5920f..6858ac07ad9 100644 --- a/services/web/server/tests/unit/isolated/test_studies_dispatcher_projects_permalinks.py +++ b/services/web/server/tests/unit/isolated/test_studies_dispatcher_projects_permalinks.py @@ -44,13 +44,17 @@ def app_environment( "WEBSERVER_DIAGNOSTICS": "null", "WEBSERVER_DIRECTOR_V2": "null", "WEBSERVER_EXPORTER": "null", + "WEBSERVER_EMAIL": "null", "WEBSERVER_GARBAGE_COLLECTOR": "null", "WEBSERVER_GROUPS": "1", + "WEBSERVER_LOGIN": "null", "WEBSERVER_META_MODELING": "0", + "WEBSERVER_PAYMENTS": "null", "WEBSERVER_PRODUCTS": "1", "WEBSERVER_PUBLICATIONS": "0", "WEBSERVER_RABBITMQ": "null", "WEBSERVER_REMOTE_DEBUG": "0", + "WEBSERVER_SCICRUNCH": "null", "WEBSERVER_STORAGE": "null", "WEBSERVER_SOCKETIO": "0", "WEBSERVER_TAGS": "1", @@ -63,6 +67,7 @@ def app_environment( ) # NOTE: To see logs, use pytest -s --log-cli-level=DEBUG # setup_logging(level=logging.DEBUG) + print(env_vars) return env_vars 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..5c4377d56fd 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 @@ -37,8 +37,9 @@ def test_studies_dispatcher_settings(environment: EnvVarsDict): assert not settings.is_login_required() # 2 days 1h and 10 mins - assert settings.STUDIES_GUEST_ACCOUNT_LIFETIME == timedelta( - days=2, hours=1, minutes=10 + assert ( + timedelta(days=2, hours=1, minutes=10) + == settings.STUDIES_GUEST_ACCOUNT_LIFETIME ) @@ -50,10 +51,7 @@ def test_studies_dispatcher_settings_invalid_lifetime( with pytest.raises(ValidationError) as exc_info: StudiesDispatcherSettings.create_from_envs() - validation_error: ErrorDict = exc_info.value.errors()[0] + validation_error: ErrorDict = next(iter(exc_info.value.errors())) + assert validation_error["loc"] == ("STUDIES_GUEST_ACCOUNT_LIFETIME",) assert "-2" in validation_error["msg"] - assert validation_error == { - "loc": ("STUDIES_GUEST_ACCOUNT_LIFETIME",), - "type": "value_error", - "msg": validation_error["msg"], - } + assert validation_error["type"] == "value_error" 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..a5670b5054e 100644 --- a/services/web/server/tests/unit/isolated/test_users_models.py +++ b/services/web/server/tests/unit/isolated/test_users_models.py @@ -1,5 +1,5 @@ from copy import deepcopy -from datetime import datetime +from datetime import UTC, datetime from pprint import pformat from typing import Any @@ -26,12 +26,12 @@ def test_user_models_examples( assert model_instance, f"Failed with {name}" model_enveloped = Envelope[model_cls].from_data( - model_instance.dict(by_alias=True) + model_instance.model_dump(by_alias=True) ) model_array_enveloped = Envelope[list[model_cls]].from_data( [ - model_instance.dict(by_alias=True), - model_instance.dict(by_alias=True), + model_instance.model_dump(by_alias=True), + model_instance.model_dump(by_alias=True), ] ) @@ -40,19 +40,19 @@ def test_user_models_examples( def test_profile_get_expiration_date(faker: Faker): - fake_expiration = datetime.utcnow() + fake_expiration = datetime.now(UTC) profile = ProfileGet( id=1, login=faker.email(), role=UserRole.ADMIN, - expiration_date=fake_expiration, + expiration_date=fake_expiration.date(), preferences={}, ) assert fake_expiration.date() == profile.expiration_date - body = jsonable_encoder(profile.dict(exclude_unset=True, by_alias=True)) + body = jsonable_encoder(profile.model_dump(exclude_unset=True, by_alias=True)) assert body["expirationDate"] == fake_expiration.date().isoformat() @@ -68,7 +68,7 @@ def test_auto_compute_gravatar(faker: Faker): ) envelope = Envelope[Any](data=profile) - data = envelope.dict(**RESPONSE_MODEL_POLICY)["data"] + data = envelope.model_dump(**RESPONSE_MODEL_POLICY)["data"] assert data["gravatar_id"] assert data["id"] == profile.id @@ -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) - assert "password" not in profile.dict(exclude_unset=True) + profile = ProfileGet.model_validate(result_from_db_query_and_composition) + assert "password" not in profile.model_dump(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..e88d13474fe 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 @@ -12,6 +12,7 @@ from typing import Any import hypothesis +import hypothesis.provisional import pytest from aiohttp.test_utils import TestClient from faker import Faker @@ -21,7 +22,14 @@ ClusterPatch, ClusterPing, ) -from models_library.clusters import CLUSTER_ADMIN_RIGHTS, Cluster, SimpleAuthentication +from models_library.clusters import ( + CLUSTER_ADMIN_RIGHTS, + Cluster, + ClusterTypeInModel, + SimpleAuthentication, +) +from pydantic import HttpUrl, TypeAdapter +from pydantic_core import Url from pytest_mock import MockerFixture from pytest_simcore.helpers.assert_checks import assert_status from pytest_simcore.helpers.webserver_parametrizations import ( # nopycln: import @@ -39,25 +47,48 @@ ) +@st.composite +def http_url_strategy(draw): + return TypeAdapter(HttpUrl).validate_python(draw(hypothesis.provisional.urls())) + + +@st.composite +def cluster_patch_strategy(draw): + return ClusterPatch( + name=draw(st.text()), + description=draw(st.text()), + owner=draw(st.integers(min_value=1)), + type=draw(st.sampled_from(ClusterTypeInModel)), + thumbnail=draw(http_url_strategy()), + endpoint=draw(http_url_strategy()), + authentication=None, + accessRights={}, + ) + + +st.register_type_strategy(ClusterPatch, cluster_patch_strategy()) +st.register_type_strategy(Url, http_url_strategy()) + + @pytest.fixture def mocked_director_v2_api(mocker: MockerFixture): mocked_director_v2_api = mocker.patch( "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 = 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 = 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 = 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 @@ -94,6 +125,7 @@ def cluster_create(faker: Faker) -> ClusterCreate: name=faker.name(), endpoint=faker.uri(), type=random.choice(list(ClusterType)), + owner=faker.pyint(), authentication=SimpleAuthentication( username=faker.user_name(), password=faker.password() ), @@ -120,7 +152,9 @@ async def test_create_cluster( url = client.app.router["create_cluster"].url_for() rsp = await client.post( f"{url}", - json=json.loads(cluster_create.json(by_alias=True, exclude_unset=True)), + json=json.loads( + cluster_create.model_dump_json(by_alias=True, exclude_unset=True) + ), ) data, error = await assert_status( rsp, @@ -132,7 +166,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 @@ -259,7 +293,9 @@ async def test_ping_cluster( print(f"--> pinging {cluster_ping=!r}") assert client.app url = client.app.router["ping_cluster"].url_for() - rsp = await client.post(f"{url}", json=json.loads(cluster_ping.json(by_alias=True))) + rsp = await client.post( + f"{url}", json=json.loads(cluster_ping.model_dump_json(by_alias=True)) + ) data, error = await assert_status(rsp, expected.no_content) if not error: assert data is None @@ -307,7 +343,9 @@ async def test_create_cluster_with_error( url = client.app.router["create_cluster"].url_for() rsp = await client.post( f"{url}", - json=json.loads(cluster_create.json(by_alias=True, exclude_unset=True)), + json=json.loads( + cluster_create.model_dump_json(by_alias=True, exclude_unset=True) + ), ) data, error = await assert_status(rsp, expected_http_error) assert not data @@ -408,7 +446,7 @@ async def test_update_cluster_with_error( url = client.app.router["update_cluster"].url_for(cluster_id=f"{25}") rsp = await client.patch( f"{url}", - json=json.loads(ClusterPatch().json(**_PATCH_EXPORT)), + json=json.loads(ClusterPatch().model_dump_json(**_PATCH_EXPORT)), ) data, error = await assert_status(rsp, expected_http_error) assert not data diff --git a/services/web/server/tests/unit/with_dbs/01/folders/test_folders.py b/services/web/server/tests/unit/with_dbs/01/folders/test_folders.py index 345e3875628..bb0c830ef27 100644 --- a/services/web/server/tests/unit/with_dbs/01/folders/test_folders.py +++ b/services/web/server/tests/unit/with_dbs/01/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() @@ -76,9 +76,9 @@ async def test_folders_full_workflow( url = client.app.router["get_folder"].url_for( folder_id=f"{added_folder['folderId']}" ) - resp = await client.get(url) + resp = await client.get(f"{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() @@ -157,7 +157,7 @@ async def test_sub_folders_full_workflow( # list user specific folder base_url = client.app.router["list_folders"].url_for() url = base_url.with_query({"folder_id": f"{subfolder_folder['folderId']}"}) - resp = await client.get(url) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 0 @@ -175,7 +175,7 @@ async def test_sub_folders_full_workflow( # list user subfolder folders base_url = client.app.router["list_folders"].url_for() url = base_url.with_query({"folder_id": f"{subfolder_folder['folderId']}"}) - resp = await client.get(url) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 1 assert data[0]["name"] == "My sub sub folder" @@ -206,12 +206,12 @@ 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() url = base_url.with_query({"folder_id": "null"}) - resp = await client.get(url) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 2 @@ -298,7 +298,7 @@ async def test_project_listing_inside_of_private_folder( # list project in user private folder base_url = client.app.router["list_projects"].url_for() url = base_url.with_query({"folder_id": f"{original_user_folder['folderId']}"}) - resp = await client.get(url) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 1 assert data[0]["uuid"] == user_project["uuid"] @@ -310,7 +310,7 @@ async def test_project_listing_inside_of_private_folder( # Try to list folder that user doesn't have access to base_url = client.app.router["list_projects"].url_for() url = base_url.with_query({"folder_id": f"{original_user_folder['folderId']}"}) - resp = await client.get(url) + resp = await client.get(f"{url}") _, errors = await assert_status( resp, status.HTTP_403_FORBIDDEN, @@ -330,7 +330,7 @@ async def test_project_listing_inside_of_private_folder( # list new user root folder base_url = client.app.router["list_projects"].url_for() url = base_url.with_query({"folder_id": "null"}) - resp = await client.get(url) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 1 assert data[0]["uuid"] == user_project["uuid"] @@ -353,7 +353,7 @@ async def test_project_listing_inside_of_private_folder( # list new user specific folder base_url = client.app.router["list_projects"].url_for() url = base_url.with_query({"folder_id": f"{new_user_folder['folderId']}"}) - resp = await client.get(url) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 1 assert data[0]["uuid"] == user_project["uuid"] @@ -396,7 +396,7 @@ async def test_folders_deletion( url = client.app.router["create_folder"].url_for() resp = await client.post(url.path, json={"name": "My first folder"}) root_folder, _ = await assert_status(resp, status.HTTP_201_CREATED) - assert FolderGet.parse_obj(root_folder) + assert FolderGet.model_validate(root_folder) # create a subfolder folder url = client.app.router["create_folder"].url_for() @@ -448,14 +448,14 @@ async def test_folders_deletion( # list subfolder projects base_url = client.app.router["list_projects"].url_for() url = base_url.with_query({"folder_id": f"{subfolder_2['folderId']}"}) - resp = await client.get(url) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 1 assert data[0]["uuid"] == user_project["uuid"] # list root projects base_url = client.app.router["list_projects"].url_for() - resp = await client.get(base_url) + resp = await client.get(f"{base_url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 0 @@ -474,13 +474,13 @@ async def test_folders_deletion( await assert_status(resp, status.HTTP_204_NO_CONTENT) fire_and_forget_tasks = client.app[APP_FIRE_AND_FORGET_TASKS_KEY] - t: asyncio.Task = list(fire_and_forget_tasks)[0] + t: asyncio.Task = next(iter(fire_and_forget_tasks)) assert t.get_name().startswith("fire_and_forget_task_delete_project_task_") await t assert len(client.app[APP_FIRE_AND_FORGET_TASKS_KEY]) == 0 # list root projects (The project should have been deleted) base_url = client.app.router["list_projects"].url_for() - resp = await client.get(base_url) + resp = await client.get(f"{base_url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 0 diff --git a/services/web/server/tests/unit/with_dbs/01/products/test_products_db.py b/services/web/server/tests/unit/with_dbs/01/products/test_products_db.py index 4cd80c74a16..bd399948c14 100644 --- a/services/web/server/tests/unit/with_dbs/01/products/test_products_db.py +++ b/services/web/server/tests/unit/with_dbs/01/products/test_products_db.py @@ -138,9 +138,9 @@ async def test_product_repository_get_product( } # check RowProxy -> pydantic's Product - product = Product.from_orm(product_row) + product = Product.model_validate(product_row) - print(product.json(indent=1)) + print(product.model_dump_json(indent=1)) # product repo assert product_repository.engine diff --git a/services/web/server/tests/unit/with_dbs/01/products/test_products_rpc.py b/services/web/server/tests/unit/with_dbs/01/products/test_products_rpc.py index 3de1f7a95c8..4505a6f4e3e 100644 --- a/services/web/server/tests/unit/with_dbs/01/products/test_products_rpc.py +++ b/services/web/server/tests/unit/with_dbs/01/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 = CreditResultGet.model_validate(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 = CreditResultGet.model_validate(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/01/studies_dispatcher/conftest.py b/services/web/server/tests/unit/with_dbs/01/studies_dispatcher/conftest.py index a074c4d77e1..a03a5713e59 100644 --- a/services/web/server/tests/unit/with_dbs/01/studies_dispatcher/conftest.py +++ b/services/web/server/tests/unit/with_dbs/01/studies_dispatcher/conftest.py @@ -58,7 +58,7 @@ def app_environment(app_environment: EnvVarsDict, monkeypatch: pytest.MonkeyPatc ) plugin_settings = StudiesDispatcherSettings.create_from_envs() - print(plugin_settings.json(indent=1)) + print(plugin_settings.model_dump_json(indent=1)) return {**app_environment, **envs_plugins, **envs_studies_dispatcher} 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 420adf921ac..14f673ce5da 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 common_library.json_serialization import json_dumps from models_library.projects_state import ProjectLocked, ProjectStatus -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 @@ -78,7 +78,9 @@ def web_server(redis_service: RedisSettings, web_server: TestServer) -> TestServ # # Extends web_server to start redis_service # - print("Redis service started with settings: ", redis_service.json(indent=1)) + print( + "Redis service started with settings: ", redis_service.model_dump_json(indent=1) + ) return web_server @@ -241,7 +243,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 +255,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 +352,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 +368,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..35733d100e6 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,6 @@ PricingPlanGet, ) from models_library.utils.fastapi_encoders import jsonable_encoder -from pydantic import parse_obj_as 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 +29,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 = PricingPlanGet.model_validate( + 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..96ada757900 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 = ServiceGetV2.model_validate( + ServiceGetV2.model_config["json_schema_extra"]["examples"][0] ) got.version = service_version got.key = service_key @@ -100,12 +100,12 @@ async def _update( assert product_name assert user_id - got = parse_obj_as( - ServiceGetV2, ServiceGetV2.Config.schema_extra["examples"][0] + got = ServiceGetV2.model_validate( + ServiceGetV2.model_config["json_schema_extra"]["examples"][0] ) got.version = service_version got.key = service_key - return got.copy(update=update.dict(exclude_unset=True)) + return got.model_copy(update=update.model_dump(exclude_unset=True)) return { "list_services_paginated": mocker.patch( @@ -146,7 +146,7 @@ async def test_list_services_latest( assert data assert error is None - model = parse_obj_as(Page[CatalogServiceGet], data) + model = Page[CatalogServiceGet].model_validate(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 = CatalogServiceGet.model_validate(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 = CatalogServiceGet.model_validate(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/01/test_groups.py b/services/web/server/tests/unit/with_dbs/01/test_groups.py index f6e41225ff7..95682c21f56 100644 --- a/services/web/server/tests/unit/with_dbs/01/test_groups.py +++ b/services/web/server/tests/unit/with_dbs/01/test_groups.py @@ -64,7 +64,7 @@ def client( app = create_safe_application(cfg) settings = setup_settings(app) - print(settings.json(indent=1)) + print(settings.model_dump_json(indent=1)) setup_db(app) setup_session(app) diff --git a/services/web/server/tests/unit/with_dbs/01/test_resource_manager_user_sessions.py b/services/web/server/tests/unit/with_dbs/01/test_resource_manager_user_sessions.py index 9e46f5a27f7..00f3ce6d576 100644 --- a/services/web/server/tests/unit/with_dbs/01/test_resource_manager_user_sessions.py +++ b/services/web/server/tests/unit/with_dbs/01/test_resource_manager_user_sessions.py @@ -3,7 +3,7 @@ # pylint: disable=too-many-arguments # pylint: disable=unused-argument # pylint: disable=unused-variable - +import json import time from collections.abc import Callable from random import randint @@ -12,10 +12,12 @@ import pytest import redis.asyncio as aioredis from aiohttp import web +from faker import Faker from pytest_simcore.helpers.monkeypatch_envs import setenvs_from_dict from servicelib.aiohttp.application import create_safe_application from servicelib.aiohttp.application_setup import is_setup_completed from simcore_service_webserver.application_settings import setup_settings +from simcore_service_webserver.payments._methods_api import Faker from simcore_service_webserver.resource_manager.plugin import setup_resource_manager from simcore_service_webserver.resource_manager.registry import ( _ALIVE_SUFFIX, @@ -33,10 +35,12 @@ @pytest.fixture def mock_env_devel_environment( - mock_env_devel_environment: dict[str, str], monkeypatch: pytest.MonkeyPatch + mock_env_devel_environment: dict[str, str], monkeypatch: pytest.MonkeyPatch, faker: Faker ): return mock_env_devel_environment | setenvs_from_dict( - monkeypatch, {"RESOURCE_MANAGER_RESOURCE_TTL_S": "3"} + monkeypatch, { + "RESOURCE_MANAGER_RESOURCE_TTL_S": "3", + } ) diff --git a/services/web/server/tests/unit/with_dbs/01/test_statics.py b/services/web/server/tests/unit/with_dbs/01/test_statics.py index f6cd1191577..1edb437b20a 100644 --- a/services/web/server/tests/unit/with_dbs/01/test_statics.py +++ b/services/web/server/tests/unit/with_dbs/01/test_statics.py @@ -68,7 +68,7 @@ def client( app = create_safe_application(cfg) settings = setup_settings(app) - print(settings.json(indent=1)) + print(settings.model_dump_json(indent=1)) setup_rest(app) setup_db(app) diff --git a/services/web/server/tests/unit/with_dbs/01/wallets/conftest.py b/services/web/server/tests/unit/with_dbs/01/wallets/conftest.py index 1686507e13d..36164328005 100644 --- a/services/web/server/tests/unit/with_dbs/01/wallets/conftest.py +++ b/services/web/server/tests/unit/with_dbs/01/wallets/conftest.py @@ -7,6 +7,7 @@ from copy import deepcopy from pathlib import Path +from faker import Faker import pytest import sqlalchemy as sa from aioresponses import aioresponses @@ -23,6 +24,7 @@ def app_environment( app_environment: EnvVarsDict, env_devel_dict: EnvVarsDict, monkeypatch: pytest.MonkeyPatch, + faker: Faker ): new_envs = setenvs_from_dict( monkeypatch, diff --git a/services/web/server/tests/unit/with_dbs/01/wallets/payments/conftest.py b/services/web/server/tests/unit/with_dbs/01/wallets/payments/conftest.py index 2b59b77c3b5..5fce5fad9cb 100644 --- a/services/web/server/tests/unit/with_dbs/01/wallets/payments/conftest.py +++ b/services/web/server/tests/unit/with_dbs/01/wallets/payments/conftest.py @@ -10,6 +10,7 @@ from typing import Any, TypeAlias, cast from unittest.mock import MagicMock +import pycountry import pytest import sqlalchemy as sa from aiohttp import web @@ -79,7 +80,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 @@ -334,7 +335,7 @@ def setup_user_pre_registration_details_db( address=faker.address().replace("\n", ", "), city=faker.city(), state=faker.state(), - country=faker.country(), + country=faker.random_element([c.name for c in pycountry.countries]), postal_code=faker.postcode(), created_by=None, ) diff --git a/services/web/server/tests/unit/with_dbs/01/wallets/payments/test_payments.py b/services/web/server/tests/unit/with_dbs/01/wallets/payments/test_payments.py index f6519735ed1..719eb7e6dc8 100644 --- a/services/web/server/tests/unit/with_dbs/01/wallets/payments/test_payments.py +++ b/services/web/server/tests/unit/with_dbs/01/wallets/payments/test_payments.py @@ -17,7 +17,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 +105,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 +134,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 = Page[PaymentTransaction].model_validate(data) assert page.data assert page.meta.total == 1 @@ -200,7 +200,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 +233,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 = Page[PaymentTransaction].model_validate(data) assert page.meta.total == num_payments all_transactions = {t.payment_id: t for t in page.data} @@ -286,7 +286,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 +362,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 = PaymentTransaction.model_fields["state"].annotation 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/01/wallets/payments/test_payments_methods.py b/services/web/server/tests/unit/with_dbs/01/wallets/payments/test_payments_methods.py index 0980e45caa2..d10109a5295 100644 --- a/services/web/server/tests/unit/with_dbs/01/wallets/payments/test_payments_methods.py +++ b/services/web/server/tests/unit/with_dbs/01/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 = Page[PaymentTransaction].model_validate(data) assert page.data assert page.meta.total == 1 diff --git a/services/web/server/tests/unit/with_dbs/01/wallets/payments/test_payments_rpc.py b/services/web/server/tests/unit/with_dbs/01/wallets/payments/test_payments_rpc.py index 756c008adba..af0f7d304ca 100644 --- a/services/web/server/tests/unit/with_dbs/01/wallets/payments/test_payments_rpc.py +++ b/services/web/server/tests/unit/with_dbs/01/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 = InvoiceDataGet.model_validate(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/01/workspaces/test_workspaces.py b/services/web/server/tests/unit/with_dbs/01/workspaces/test_workspaces.py index e2ace9daa6a..25de918f878 100644 --- a/services/web/server/tests/unit/with_dbs/01/workspaces/test_workspaces.py +++ b/services/web/server/tests/unit/with_dbs/01/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() @@ -78,7 +78,7 @@ async def test_workspaces_workflow( url = client.app.router["get_workspace"].url_for( workspace_id=f"{added_workspace['workspaceId']}" ) - resp = await client.get(url) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert data["workspaceId"] == added_workspace["workspaceId"] assert data["name"] == "My first workspace" @@ -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/01/workspaces/test_workspaces__folders_and_projects_crud.py b/services/web/server/tests/unit/with_dbs/01/workspaces/test_workspaces__folders_and_projects_crud.py index c95aebe6fdd..0ebb5518b28 100644 --- a/services/web/server/tests/unit/with_dbs/01/workspaces/test_workspaces__folders_and_projects_crud.py +++ b/services/web/server/tests/unit/with_dbs/01/workspaces/test_workspaces__folders_and_projects_crud.py @@ -79,7 +79,7 @@ async def test_workspaces_full_workflow_with_folders_and_projects( # List project in workspace base_url = client.app.router["list_projects"].url_for() url = base_url.with_query({"workspace_id": f"{added_workspace['workspaceId']}"}) - resp = await client.get(url) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 1 assert data[0]["uuid"] == project["uuid"] @@ -88,7 +88,7 @@ async def test_workspaces_full_workflow_with_folders_and_projects( # Get project in workspace base_url = client.app.router["get_project"].url_for(project_id=project["uuid"]) - resp = await client.get(base_url) + resp = await client.get(f"{base_url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert data["uuid"] == project["uuid"] assert data["workspaceId"] == added_workspace["workspaceId"] @@ -110,7 +110,7 @@ async def test_workspaces_full_workflow_with_folders_and_projects( url = base_url.with_query( {"workspace_id": f"{added_workspace['workspaceId']}", "folder_id": "null"} ) - resp = await client.get(url) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 1 assert data[0]["folderId"] == first_folder["folderId"] @@ -131,7 +131,7 @@ async def test_workspaces_full_workflow_with_folders_and_projects( "folder_id": f"{first_folder['folderId']}", } ) - resp = await client.get(url) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 1 assert data[0]["uuid"] == project["uuid"] @@ -142,7 +142,7 @@ async def test_workspaces_full_workflow_with_folders_and_projects( # Try to list folder that user doesn't have access to base_url = client.app.router["list_projects"].url_for() url = base_url.with_query({"workspace_id": f"{added_workspace['workspaceId']}"}) - resp = await client.get(url) + resp = await client.get(f"{url}") _, errors = await assert_status( resp, status.HTTP_403_FORBIDDEN, @@ -164,7 +164,7 @@ async def test_workspaces_full_workflow_with_folders_and_projects( url = base_url.with_query( {"workspace_id": f"{added_workspace['workspaceId']}", "folder_id": "null"} ) - resp = await client.get(url) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 1 @@ -176,7 +176,7 @@ async def test_workspaces_full_workflow_with_folders_and_projects( "folder_id": "none", } ) - resp = await client.get(url) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 0 @@ -188,7 +188,7 @@ async def test_workspaces_full_workflow_with_folders_and_projects( "folder_id": f"{first_folder['folderId']}", } ) - resp = await client.get(url) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 1 assert data[0]["uuid"] == project["uuid"] @@ -230,7 +230,7 @@ async def test_workspaces_full_workflow_with_folders_and_projects( url = base_url.with_query( {"workspace_id": f"{added_workspace['workspaceId']}", "folder_id": "null"} ) - resp = await client.get(url) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 2 @@ -299,7 +299,7 @@ async def test_workspaces_delete_folders( # List project in workspace base_url = client.app.router["list_projects"].url_for() url = base_url.with_query({"workspace_id": f"{added_workspace['workspaceId']}"}) - resp = await client.get(url) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 2 @@ -362,6 +362,7 @@ async def test_workspaces_delete_folders( # List project in workspace (The projects should have been deleted) base_url = client.app.router["list_projects"].url_for() url = base_url.with_query({"workspace_id": f"{added_workspace['workspaceId']}"}) - resp = await client.get(url) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 0 + diff --git a/services/web/server/tests/unit/with_dbs/01/workspaces/test_workspaces__list_projects_full_search.py b/services/web/server/tests/unit/with_dbs/01/workspaces/test_workspaces__list_projects_full_search.py index a7efd64b485..6c146bb5a1f 100644 --- a/services/web/server/tests/unit/with_dbs/01/workspaces/test_workspaces__list_projects_full_search.py +++ b/services/web/server/tests/unit/with_dbs/01/workspaces/test_workspaces__list_projects_full_search.py @@ -82,7 +82,7 @@ async def test_workspaces__list_projects_full_search( # List project with full search base_url = client.app.router["list_projects_full_search"].url_for() url = base_url.with_query({"text": "solution"}) - resp = await client.get(url) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 1 assert data[0]["uuid"] == project_1["uuid"] @@ -104,7 +104,7 @@ async def test_workspaces__list_projects_full_search( # List project with full search base_url = client.app.router["list_projects_full_search"].url_for() url = base_url.with_query({"text": "Orion"}) - resp = await client.get(url) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 1 assert data[0]["uuid"] == project_2["uuid"] @@ -137,7 +137,7 @@ async def test_workspaces__list_projects_full_search( # List project with full search base_url = client.app.router["list_projects_full_search"].url_for() url = base_url.with_query({"text": "Skyline"}) - resp = await client.get(url) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 1 assert data[0]["uuid"] == project_3["uuid"] @@ -147,7 +147,7 @@ async def test_workspaces__list_projects_full_search( # List project with full search (it should return data across all workspaces/folders) base_url = client.app.router["list_projects_full_search"].url_for() url = base_url.with_query({"text": "solution"}) - resp = await client.get(url) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) sorted_data = sorted(data, key=lambda x: x["uuid"]) assert len(sorted_data) == 3 @@ -190,7 +190,7 @@ async def test__list_projects_full_search_with_query_parameters( # Full search with text base_url = client.app.router["list_projects_full_search"].url_for() url = base_url.with_query({"text": "Orion"}) - resp = await client.get(url) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 1 assert data[0]["uuid"] == project["uuid"] @@ -203,7 +203,7 @@ async def test__list_projects_full_search_with_query_parameters( "order_by": json.dumps({"field": "uuid", "direction": "desc"}), } ) - resp = await client.get(url) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 1 assert data[0]["uuid"] == project["uuid"] @@ -211,7 +211,7 @@ async def test__list_projects_full_search_with_query_parameters( # Full search with tag_ids base_url = client.app.router["list_projects_full_search"].url_for() url = base_url.with_query({"text": "Orion", "tag_ids": "1,2"}) - resp = await client.get(url) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 0 @@ -232,7 +232,7 @@ async def test__list_projects_full_search_with_query_parameters( # Full search with tag_ids base_url = client.app.router["list_projects_full_search"].url_for() url = base_url.with_query({"text": "Orion", "tag_ids": f"{added_tag['id']}"}) - resp = await client.get(url) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 1 assert data[0]["uuid"] == project["uuid"] diff --git a/services/web/server/tests/unit/with_dbs/01/workspaces/test_workspaces__moving_projects_between_workspaces.py b/services/web/server/tests/unit/with_dbs/01/workspaces/test_workspaces__moving_projects_between_workspaces.py index f583b9e3ecf..21b16ea9738 100644 --- a/services/web/server/tests/unit/with_dbs/01/workspaces/test_workspaces__moving_projects_between_workspaces.py +++ b/services/web/server/tests/unit/with_dbs/01/workspaces/test_workspaces__moving_projects_between_workspaces.py @@ -58,7 +58,7 @@ async def test_moving_between_workspaces_user_role_permissions( base_url = client.app.router["replace_project_workspace"].url_for( project_id=fake_project["uuid"], workspace_id="null" ) - resp = await client.put(base_url) + resp = await client.put(f"{base_url}") await assert_status(resp, expected.no_content) @@ -98,7 +98,7 @@ async def test_moving_between_private_and_shared_workspaces( # Get project in workspace base_url = client.app.router["get_project"].url_for(project_id=project["uuid"]) - resp = await client.get(base_url) + resp = await client.get(f"{base_url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert data["workspaceId"] == added_workspace["workspaceId"] # <-- Workspace ID @@ -106,12 +106,12 @@ async def test_moving_between_private_and_shared_workspaces( base_url = client.app.router["replace_project_workspace"].url_for( project_id=project["uuid"], workspace_id="null" ) - resp = await client.put(base_url) + resp = await client.put(f"{base_url}") await assert_status(resp, status.HTTP_204_NO_CONTENT) # Get project in workspace base_url = client.app.router["get_project"].url_for(project_id=project["uuid"]) - resp = await client.get(base_url) + resp = await client.get(f"{base_url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert data["workspaceId"] is None # <-- Workspace ID is None @@ -119,12 +119,12 @@ async def test_moving_between_private_and_shared_workspaces( base_url = client.app.router["replace_project_workspace"].url_for( project_id=project["uuid"], workspace_id=f"{added_workspace['workspaceId']}" ) - resp = await client.put(base_url) + resp = await client.put(f"{base_url}") await assert_status(resp, status.HTTP_204_NO_CONTENT) # Get project in workspace base_url = client.app.router["get_project"].url_for(project_id=project["uuid"]) - resp = await client.get(base_url) + resp = await client.get(f"{base_url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert data["workspaceId"] == added_workspace["workspaceId"] # <-- Workspace ID @@ -177,7 +177,7 @@ async def test_moving_between_shared_and_shared_workspaces( # Get project in workspace base_url = client.app.router["get_project"].url_for(project_id=project["uuid"]) - resp = await client.get(base_url) + resp = await client.get(f"{base_url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert data["workspaceId"] == added_workspace["workspaceId"] # <-- Workspace ID @@ -185,12 +185,12 @@ async def test_moving_between_shared_and_shared_workspaces( base_url = client.app.router["replace_project_workspace"].url_for( project_id=project["uuid"], workspace_id=f"{second_workspace['workspaceId']}" ) - resp = await client.put(base_url) + resp = await client.put(f"{base_url}") await assert_status(resp, status.HTTP_204_NO_CONTENT) # Get project in workspace base_url = client.app.router["get_project"].url_for(project_id=project["uuid"]) - resp = await client.get(base_url) + resp = await client.get(f"{base_url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert data["workspaceId"] == second_workspace["workspaceId"] # <-- Workspace ID @@ -257,7 +257,7 @@ async def test_moving_between_workspaces_check_removed_from_folder( # Get project in workspace base_url = client.app.router["get_project"].url_for(project_id=project["uuid"]) - resp = await client.get(base_url) + resp = await client.get(f"{base_url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert data["workspaceId"] == added_workspace["workspaceId"] # <-- Workspace ID @@ -265,12 +265,12 @@ async def test_moving_between_workspaces_check_removed_from_folder( base_url = client.app.router["replace_project_workspace"].url_for( project_id=project["uuid"], workspace_id="none" ) - resp = await client.put(base_url) + resp = await client.put(f"{base_url}") await assert_status(resp, status.HTTP_204_NO_CONTENT) # Get project in workspace base_url = client.app.router["get_project"].url_for(project_id=project["uuid"]) - resp = await client.get(base_url) + resp = await client.get(f"{base_url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert data["workspaceId"] is None # <-- Workspace ID is None 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 dbc3890f0b9..7e765694d3d 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], ) @@ -255,7 +254,7 @@ async def _assert_it( ) -> dict: # GET /v0/projects/{project_id} with a project owned by user url = client.app.router["get_project"].url_for(project_id=project["uuid"]) - resp = await client.get(url) + resp = await client.get(f"{url}") data, error = await assert_status(resp, expected) if not error: @@ -509,4 +508,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_project_lock.py b/services/web/server/tests/unit/with_dbs/02/test_project_lock.py index f70a44c7bbc..5c33586e151 100644 --- a/services/web/server/tests/unit/with_dbs/02/test_project_lock.py +++ b/services/web/server/tests/unit/with_dbs/02/test_project_lock.py @@ -12,7 +12,7 @@ from models_library.projects_access import Owner from models_library.projects_state import ProjectLocked, ProjectStatus from models_library.users import UserID -from pydantic import parse_raw_as +from pydantic import TypeAdapter from simcore_service_webserver.projects.exceptions import ProjectLockError from simcore_service_webserver.projects.lock import ( PROJECT_REDIS_LOCK_KEY, @@ -51,7 +51,7 @@ async def test_lock_project( PROJECT_REDIS_LOCK_KEY.format(project_uuid) ) assert redis_value - lock_value = parse_raw_as(ProjectLocked, redis_value) + lock_value = TypeAdapter(ProjectLocked).validate_json(redis_value) assert lock_value == ProjectLocked( value=True, owner=Owner(user_id=user_id, **user_fullname), @@ -137,7 +137,7 @@ async def test_is_project_locked( faker: Faker, ): assert client.app - assert await is_project_locked(client.app, project_uuid) == False + assert await is_project_locked(client.app, project_uuid) is False user_name: FullNameDict = { "first_name": faker.first_name(), "last_name": faker.last_name(), @@ -149,7 +149,7 @@ async def test_is_project_locked( user_id=user_id, user_fullname=user_name, ): - assert await is_project_locked(client.app, project_uuid) == True + assert await is_project_locked(client.app, project_uuid) is True @pytest.mark.parametrize( @@ -170,9 +170,9 @@ async def test_get_project_locked_state( ): assert client.app # no lock - assert await get_project_locked_state(client.app, project_uuid) == None + assert await get_project_locked_state(client.app, project_uuid) is None - assert await is_project_locked(client.app, project_uuid) == False + assert await is_project_locked(client.app, project_uuid) is False user_name: FullNameDict = { "first_name": faker.first_name(), "last_name": faker.last_name(), 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_comments_handlers.py b/services/web/server/tests/unit/with_dbs/02/test_projects_comments_handlers.py index 13fe3a7633a..604ee40308c 100644 --- a/services/web/server/tests/unit/with_dbs/02/test_projects_comments_handlers.py +++ b/services/web/server/tests/unit/with_dbs/02/test_projects_comments_handlers.py @@ -42,7 +42,7 @@ async def test_project_comments_user_role_access( base_url = client.app.router["list_project_comments"].url_for( project_uuid=user_project["uuid"] ) - resp = await client.get(base_url) + resp = await client.get(f"{base_url}") assert resp.status == 401 if user_role == UserRole.ANONYMOUS else 200 @@ -65,7 +65,7 @@ async def test_project_comments_full_workflow( base_url = client.app.router["list_project_comments"].url_for( project_uuid=user_project["uuid"] ) - resp = await client.get(base_url) + resp = await client.get(f"{base_url}") data, _, meta, links = await assert_status( resp, expected, @@ -78,7 +78,7 @@ async def test_project_comments_full_workflow( # Now we will add first comment body = {"contents": "My first comment"} - resp = await client.post(base_url, json=body) + resp = await client.post(f"{base_url}", json=body) data, _ = await assert_status( resp, status.HTTP_201_CREATED, @@ -86,7 +86,7 @@ async def test_project_comments_full_workflow( first_comment_id = data["comment_id"] # Now we will add second comment - resp = await client.post(base_url, json={"contents": "My second comment"}) + resp = await client.post(f"{base_url}", json={"contents": "My second comment"}) data, _ = await assert_status( resp, status.HTTP_201_CREATED, @@ -94,7 +94,7 @@ async def test_project_comments_full_workflow( second_comment_id = data["comment_id"] # Now we will list all comments for the project - resp = await client.get(base_url) + resp = await client.get(f"{base_url}") data, _, meta, links = await assert_status( resp, expected, @@ -108,7 +108,7 @@ async def test_project_comments_full_workflow( # Now we will update the second comment updated_comment = "Updated second comment" resp = await client.put( - base_url / f"{second_comment_id}", + f"{base_url}/{second_comment_id}", json={"contents": updated_comment}, ) data, _ = await assert_status( @@ -117,7 +117,7 @@ async def test_project_comments_full_workflow( ) # Now we will get the second comment - resp = await client.get(base_url / f"{second_comment_id}") + resp = await client.get(f"{base_url}/{second_comment_id}") data, _ = await assert_status( resp, expected, @@ -125,14 +125,14 @@ async def test_project_comments_full_workflow( assert data["contents"] == updated_comment # Now we will delete the second comment - resp = await client.delete(base_url / f"{second_comment_id}") + resp = await client.delete(f"{base_url}/{second_comment_id}") data, _ = await assert_status( resp, status.HTTP_204_NO_CONTENT, ) # Now we will list all comments for the project - resp = await client.get(base_url) + resp = await client.get(f"{base_url}") data, _, meta, links = await assert_status( resp, expected, @@ -146,14 +146,14 @@ async def test_project_comments_full_workflow( # Now we will log as a different user async with LoggedUser(client) as new_logged_user: # As this user does not have access to the project, they should get 403 - resp = await client.get(base_url) + resp = await client.get(f"{base_url}") _, errors = await assert_status( resp, status.HTTP_403_FORBIDDEN, ) assert errors - resp = await client.get(base_url / f"{first_comment_id}") + resp = await client.get(f"{base_url}/{first_comment_id}") _, errors = await assert_status( resp, status.HTTP_403_FORBIDDEN, @@ -173,7 +173,7 @@ async def test_project_comments_full_workflow( # Now the user should have access to the project now # New user will add comment resp = await client.post( - base_url, + f"{base_url}", json={"contents": "My first comment as a new user"}, ) data, _ = await assert_status( @@ -185,7 +185,7 @@ async def test_project_comments_full_workflow( # New user will modify the comment updated_comment = "Updated My first comment as a new user" resp = await client.put( - base_url / f"{new_user_comment_id}", + f"{base_url}/{new_user_comment_id}", json={"contents": updated_comment}, ) data, _ = await assert_status( @@ -195,7 +195,7 @@ async def test_project_comments_full_workflow( assert data["contents"] == updated_comment # New user will list all comments - resp = await client.get(base_url) + resp = await client.get(f"{base_url}") data, _, meta, links = await assert_status( resp, expected, @@ -209,7 +209,7 @@ async def test_project_comments_full_workflow( # New user will modify comment of the previous user updated_comment = "Updated comment of previous user" resp = await client.put( - base_url / f"{first_comment_id}", + f"{base_url}/{first_comment_id}", json={"contents": updated_comment}, ) data, _ = await assert_status( @@ -219,7 +219,7 @@ async def test_project_comments_full_workflow( assert data["contents"] == updated_comment # New user will delete comment of the previous user - resp = await client.delete(base_url / f"{first_comment_id}") + resp = await client.delete(f"{base_url}/{first_comment_id}") data, _ = await assert_status( resp, status.HTTP_204_NO_CONTENT, 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 bf69984d6af..99cf4e105d4 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 @@ -18,7 +18,9 @@ from faker import Faker from models_library.products import ProductName from models_library.projects_state import ProjectState -from pydantic import parse_obj_as +from models_library.services import ServiceKey +from models_library.utils.fastapi_encoders import jsonable_encoder +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 ( @@ -168,10 +170,10 @@ async def _assert_get_same_project( assert data == project if project_state: - assert parse_obj_as(ProjectState, project_state) + assert ProjectState.model_validate(project_state) if project_permalink: - assert parse_obj_as(ProjectPermalink, project_permalink) + assert ProjectPermalink.model_validate(project_permalink) assert folder_id is None @@ -209,7 +211,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 ProjectPermalink.model_validate(project_permalink) # standard project project_state = data[1].pop("state") @@ -252,7 +254,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 ProjectPermalink.model_validate(project_permalink) @pytest.fixture(scope="session") @@ -432,7 +434,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()) @@ -461,7 +463,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()) @@ -515,7 +517,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()) @@ -571,7 +573,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 = { @@ -631,7 +633,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) @pytest.fixture 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..18ba745eaee 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( @@ -114,7 +114,7 @@ async def test_clone_project( "folder_id": f"{create_workspace_and_folder[1]}", } url = base_url.with_query(**query_parameters) - resp = await client.get(url) + resp = await client.get(f"{url}") data = await resp.json() assert resp.status == 200 assert len(data["data"]) == 1 @@ -135,7 +135,7 @@ async def test_clone_project( "folder_id": f"{create_workspace_and_folder[1]}", } url = base_url.with_query(**query_parameters) - resp = await client.get(url) + resp = await client.get(f"{url}") data = await resp.json() assert resp.status == 200 assert len(data["data"]) == 2 diff --git a/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers__list.py b/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers__list.py index dae689d1974..a9f111e9c4a 100644 --- a/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers__list.py +++ b/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers__list.py @@ -55,7 +55,7 @@ async def _list_projects( if query_parameters: url = url.with_query(**query_parameters) - resp = await client.get(url) + resp = await client.get(f"{url}") data, errors, meta, links = await assert_status( resp, expected, @@ -121,9 +121,9 @@ def standard_user_role() -> tuple[str, tuple[UserRole, ExpectedResponse]]: @pytest.mark.parametrize( "limit, offset, expected_error_msg", [ - (-7, 0, "ensure this value is greater than or equal to 1"), - (0, 0, "ensure this value is greater than or equal to 1"), - (43, -2, "ensure this value is greater than or equal to 0"), + (-7, 0, "Input should be greater than or equal to 1"), + (0, 0, "Input should be greater than or equal to 1"), + (43, -2, "Input should be greater than or equal to 0"), ], ) @pytest.mark.parametrize(*standard_user_role()) @@ -145,7 +145,7 @@ async def test_list_projects_with_invalid_pagination_parameters( status.HTTP_422_UNPROCESSABLE_ENTITY, query_parameters={"limit": limit, "offset": offset}, expected_error_msg=expected_error_msg, - expected_error_code="value_error.number.not_ge", + expected_error_code="greater_than_equal", ) diff --git a/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers__list_with_query_params.py b/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers__list_with_query_params.py index 89b7fed1544..abb26a3f3e3 100644 --- a/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers__list_with_query_params.py +++ b/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers__list_with_query_params.py @@ -153,7 +153,7 @@ async def test_list_projects_with_search_parameter( base_url = client.app.router["list_projects"].url_for() assert f"{base_url}" == f"/{api_version_prefix}/projects" - resp = await client.get(base_url) + resp = await client.get(f"{base_url}") data = await resp.json() assert resp.status == 200 @@ -164,7 +164,7 @@ async def test_list_projects_with_search_parameter( url = base_url.with_query(**query_parameters) assert f"{url}" == f"/{api_version_prefix}/projects?search=" - resp = await client.get(url) + resp = await client.get(f"{url}") data = await resp.json() assert resp.status == 200 @@ -175,7 +175,7 @@ async def test_list_projects_with_search_parameter( url = base_url.with_query(**query_parameters) assert f"{url}" == f"/{api_version_prefix}/projects?search=nAmE+5" - resp = await client.get(url) + resp = await client.get(f"{url}") data = await resp.json() assert resp.status == 200 @@ -188,7 +188,7 @@ async def test_list_projects_with_search_parameter( url = base_url.with_query(**query_parameters) assert f"{url}" == f"/{api_version_prefix}/projects?search=2-fe1b-11ed-b038-cdb1" - resp = await client.get(url) + resp = await client.get(f"{url}") data = await resp.json() assert resp.status == 200 @@ -207,7 +207,7 @@ async def test_list_projects_with_search_parameter( == f"/{api_version_prefix}/projects?search={user_name_substring_query_parsed}" ) - resp = await client.get(url) + resp = await client.get(f"{url}") data = await resp.json() assert resp.status == 200 @@ -225,7 +225,7 @@ async def test_list_projects_with_search_parameter( url = base_url.with_query(**query_parameters) assert f"{url}" == f"/{api_version_prefix}/projects?search=oda" - resp = await client.get(url) + resp = await client.get(f"{url}") data = await resp.json() assert resp.status == 200 @@ -236,7 +236,7 @@ async def test_list_projects_with_search_parameter( url = base_url.with_query(**query_parameters) assert f"{url}" == f"/{api_version_prefix}/projects?search=does+not+exists" - resp = await client.get(url) + resp = await client.get(f"{url}") data = await resp.json() assert resp.status == 200 @@ -249,7 +249,7 @@ async def test_list_projects_with_search_parameter( url = base_url.with_query(**query_parameters) assert f"{url}" == f"/{api_version_prefix}/projects?search=oda&offset=0&limit=1" - resp = await client.get(url) + resp = await client.get(f"{url}") data = await resp.json() assert resp.status == 200 @@ -327,7 +327,7 @@ async def test_list_projects_with_order_by_parameter( f"{url}" == f"/{api_version_prefix}/projects?order_by=%7B%22field%22:+%22uuid%22,+%22direction%22:+%22asc%22%7D" ) - resp = await client.get(url) + resp = await client.get(f"{url}") data = await resp.json() assert resp.status == 200 assert [item["uuid"][0] for item in data["data"]] == _alphabetically_ordered_list @@ -337,7 +337,7 @@ async def test_list_projects_with_order_by_parameter( url = base_url.with_query( order_by=json.dumps({"field": "uuid", "direction": "desc"}) ) - resp = await client.get(url) + resp = await client.get(f"{url}") data = await resp.json() assert resp.status == 200 assert [item["uuid"][0] for item in data["data"]] == _alphabetically_ordered_list[ @@ -349,7 +349,7 @@ async def test_list_projects_with_order_by_parameter( url = base_url.with_query( order_by=json.dumps({"field": "name", "direction": "asc"}) ) - resp = await client.get(url) + resp = await client.get(f"{url}") data = await resp.json() assert resp.status == 200 assert [item["name"][0] for item in data["data"]] == _alphabetically_ordered_list @@ -359,7 +359,7 @@ async def test_list_projects_with_order_by_parameter( url = base_url.with_query( order_by=json.dumps({"field": "description", "direction": "asc"}) ) - resp = await client.get(url) + resp = await client.get(f"{url}") data = await resp.json() assert resp.status == 200 assert [ @@ -453,7 +453,7 @@ async def test_list_projects_for_specific_folder_id( base_url = client.app.router["list_projects"].url_for() assert f"{base_url}" == f"/{api_version_prefix}/projects" - resp = await client.get(base_url) + resp = await client.get(f"{base_url}") data = await resp.json() assert resp.status == 200 @@ -463,7 +463,7 @@ async def test_list_projects_for_specific_folder_id( query_parameters = {"folder_id": "null"} url = base_url.with_query(**query_parameters) - resp = await client.get(url) + resp = await client.get(f"{url}") data = await resp.json() assert resp.status == 200 @@ -476,7 +476,7 @@ async def test_list_projects_for_specific_folder_id( url = base_url.with_query(**query_parameters) assert f"{url}" == f"/{api_version_prefix}/projects?folder_id={setup_folders_db}" - resp = await client.get(url) + resp = await client.get(f"{url}") data = await resp.json() assert resp.status == 200 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..c960a86fa13 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 ( @@ -83,12 +83,12 @@ async def test_custom_metadata_handlers( project_id=user_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=expected.ok) - assert parse_obj_as(ProjectMetadataGet, data).custom == custom_metadata + assert ProjectMetadataGet.model_validate(data).custom == custom_metadata # delete project url = client.app.router["delete_project"].url_for(project_id=user_project["uuid"]) @@ -138,9 +138,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, @@ -175,10 +175,10 @@ async def test_new_project_with_parent_project_node( 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 ProjectMetadataGet.model_validate(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 +216,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 +235,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 +259,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 +320,10 @@ 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 ProjectMetadataGet.model_validate(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( @@ -362,7 +362,7 @@ async def test_update_project_metadata_backward_compatibility_with_same_project_ 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() ) await assert_status(response, expected_status_code=expected.ok) @@ -377,7 +377,7 @@ async def test_update_project_metadata_backward_compatibility_with_same_project_ 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() ) await assert_status(response, expected_status_code=expected.ok) @@ -427,10 +427,10 @@ async def test_update_project_metadata_s4lacad_backward_compatibility_passing_ni 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 ProjectMetadataGet.model_validate(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..8945e2c9296 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,13 +72,16 @@ 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] - ) + assert {k: v.model_dump() for k, v in node_resources.items()} == next( + iter( + ServiceResourcesDictHelpers.model_config["json_schema_extra"][ + "examples" + ] + ) + ) # type: ignore else: assert not data assert error @@ -145,18 +148,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 +190,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] + {k: v.model_dump() for k, v in node_resources.items()} + == ServiceResourcesDictHelpers.model_config["json_schema_extra"][ + "examples" + ][0] ) @@ -218,7 +229,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 +396,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 +941,7 @@ def mock_storage_calls(aioresponses_mocker: aioresponses, faker: Faker) -> None: payload=jsonable_encoder( Envelope[list[FileMetaDataGet]]( data=[ - parse_obj_as( - FileMetaDataGet, + FileMetaDataGet.model_validate( { "file_uuid": file_uuid, "location_id": 0, @@ -979,7 +991,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 +1007,4 @@ async def test_read_project_nodes_previews( status.HTTP_200_OK, ) - assert parse_obj_as(_ProjectNodePreview, data) == node_preview + assert _ProjectNodePreview.model_validate(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..a867d2e3c40 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,6 @@ PricingUnitGet, ) from models_library.utils.fastapi_encoders import jsonable_encoder -from pydantic import parse_obj_as from pytest_mock.plugin import MockerFixture from pytest_simcore.aioresponses_mocker import AioResponsesMock from pytest_simcore.helpers.assert_checks import assert_status @@ -53,7 +52,7 @@ async def test_project_node_pricing_unit_user_role_access( base_url = client.app.router["get_project_node_pricing_unit"].url_for( project_id=user_project["uuid"], node_id=node_id ) - resp = await client.get(base_url) + resp = await client.get(f"{base_url}") assert ( resp.status == status.HTTP_401_UNAUTHORIZED if user_role == UserRole.ANONYMOUS @@ -72,7 +71,7 @@ async def test_project_node_pricing_unit_user_project_access( base_url = client.app.router["get_project_node_pricing_unit"].url_for( project_id=user_project["uuid"], node_id=node_id ) - resp = await client.get(base_url) + resp = await client.get(f"{base_url}") data, _ = await assert_status(resp, expected) assert data == None @@ -81,7 +80,7 @@ async def test_project_node_pricing_unit_user_project_access( base_url = client.app.router["get_project_node_pricing_unit"].url_for( project_id=user_project["uuid"], node_id=node_id ) - resp = await client.get(base_url) + resp = await client.get(f"{base_url}") _, errors = await assert_status(resp, status.HTTP_403_FORBIDDEN) assert errors @@ -98,8 +97,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 = PricingUnitGet.model_validate( + 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 b5550400006..693372aab27 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 @@ -9,7 +9,7 @@ import time from collections.abc import Awaitable, Callable, Iterator from copy import deepcopy -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from http import HTTPStatus from typing import Any from unittest import mock @@ -228,7 +228,7 @@ async def _assert_project_state_updated( jsonable_encoder( { "project_uuid": shared_project["uuid"], - "data": p_state.dict(by_alias=True, exclude_unset=True), + "data": p_state.model_dump(by_alias=True, exclude_unset=True), } ) ) @@ -285,8 +285,8 @@ async def test_share_project( ) if new_project: assert new_project["accessRights"] == { - str(primary_group["gid"]): {"read": True, "write": True, "delete": True}, - str(all_group["gid"]): share_rights, + f'{primary_group["gid"]}': {"read": True, "write": True, "delete": True}, + f'{(all_group["gid"])}': share_rights, } # user 1 can always get to his project @@ -716,7 +716,7 @@ async def test_open_project_with_deprecated_services_ok_but_does_not_start_dynam mocked_notifications_plugin: dict[str, mock.Mock], ): mock_catalog_api["get_service"].return_value["deprecated"] = ( - datetime.utcnow() - timedelta(days=1) + datetime.now(UTC) - timedelta(days=1) ).isoformat() url = client.app.router["open_project"].url_for(project_id=user_project["uuid"]) resp = await client.post(url, json=client_session_id_factory()) @@ -1043,10 +1043,10 @@ 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"]["examples"][1]) 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", @@ -1065,7 +1065,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..30aaa89abbc 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,6 @@ 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 pytest_simcore.helpers.assert_checks import assert_status from pytest_simcore.helpers.webserver_login import LoggedUser, UserInfoDict from servicelib.aiohttp import status @@ -43,7 +42,7 @@ async def test_project_wallets_user_role_access( base_url = client.app.router["get_project_wallet"].url_for( project_id=user_project["uuid"] ) - resp = await client.get(base_url) + resp = await client.get(f"{base_url}") assert ( resp.status == status.HTTP_401_UNAUTHORIZED if user_role == UserRole.ANONYMOUS @@ -62,7 +61,7 @@ async def test_project_wallets_user_project_access( base_url = client.app.router["get_project_wallet"].url_for( project_id=user_project["uuid"] ) - resp = await client.get(base_url) + resp = await client.get(f"{base_url}") data, _ = await assert_status(resp, expected) assert data == None @@ -71,7 +70,7 @@ async def test_project_wallets_user_project_access( base_url = client.app.router["get_project_wallet"].url_for( project_id=user_project["uuid"] ) - resp = await client.get(base_url) + resp = await client.get(f"{base_url}") _, errors = await assert_status(resp, status.HTTP_403_FORBIDDEN) assert errors @@ -93,7 +92,7 @@ def setup_wallets_db( ) .returning(sa.literal_column("*")) ) - output.append(parse_obj_as(WalletGet, result.fetchone())) + output.append(WalletGet.model_validate(result.fetchone())) yield output con.execute(wallets.delete()) @@ -111,7 +110,7 @@ async def test_project_wallets_full_workflow( base_url = client.app.router["get_project_wallet"].url_for( project_id=user_project["uuid"] ) - resp = await client.get(base_url) + resp = await client.get(f"{base_url}") data, _ = await assert_status(resp, expected) assert data == None @@ -119,14 +118,14 @@ async def test_project_wallets_full_workflow( base_url = client.app.router["connect_wallet_to_project"].url_for( project_id=user_project["uuid"], wallet_id=f"{setup_wallets_db[0].wallet_id}" ) - resp = await client.put(base_url) + resp = await client.put(f"{base_url}") data, _ = await assert_status(resp, expected) assert data["walletId"] == setup_wallets_db[0].wallet_id base_url = client.app.router["get_project_wallet"].url_for( project_id=user_project["uuid"] ) - resp = await client.get(base_url) + resp = await client.get(f"{base_url}") data, _ = await assert_status(resp, expected) assert data["walletId"] == setup_wallets_db[0].wallet_id @@ -134,13 +133,13 @@ async def test_project_wallets_full_workflow( base_url = client.app.router["connect_wallet_to_project"].url_for( project_id=user_project["uuid"], wallet_id=f"{setup_wallets_db[1].wallet_id}" ) - resp = await client.put(base_url) + resp = await client.put(f"{base_url}") data, _ = await assert_status(resp, expected) assert data["walletId"] == setup_wallets_db[1].wallet_id base_url = client.app.router["get_project_wallet"].url_for( project_id=user_project["uuid"] ) - resp = await client.get(base_url) + resp = await client.get(f"{base_url}") data, _ = await assert_status(resp, expected) assert data["walletId"] == setup_wallets_db[1].wallet_id 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..014ed5db536 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" @@ -117,11 +117,11 @@ def mock_invitations_service_http_api( assert "/v1/invitations:extract" in oas["paths"] def _extract(url, **kwargs): - fake_code = URL(URL(kwargs["json"]["invitation_url"]).fragment).query[ + fake_code = URL(URL(f'{kwargs["json"]["invitation_url"]}').fragment).query[ "invitation" ] # if nothing is encoded in fake_code, just return fake_osparc_invitation - body = fake_osparc_invitation.dict() + body = fake_osparc_invitation.model_dump() with suppress(Exception): decoded = json.loads(binascii.unhexlify(fake_code).decode()) body.update(decoded) @@ -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, @@ -212,5 +212,5 @@ def app_environment( ) # tests envs - print(ApplicationSettings.create_from_envs().json(indent=2)) + print(ApplicationSettings.create_from_envs().model_dump_json(indent=2)) return envs 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..8c4daca29df 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 @@ -48,11 +48,11 @@ async def test_check_registration_invitation_when_not_required( response = await client.post( "/v0/auth/register/invitations:check", - json=InvitationCheck(invitation="*" * 100).dict(), + json=InvitationCheck(invitation="*" * 100).model_dump(), ) data, _ = await assert_status(response, status.HTTP_200_OK) - invitation = InvitationInfo.parse_obj(data) + invitation = InvitationInfo.model_validate(data) assert invitation.email is None @@ -70,11 +70,11 @@ async def test_check_registration_invitations_with_old_code( response = await client.post( "/v0/auth/register/invitations:check", - json=InvitationCheck(invitation="short-code").dict(), + json=InvitationCheck(invitation="short-code").model_dump(), ) data, _ = await assert_status(response, status.HTTP_200_OK) - invitation = InvitationInfo.parse_obj(data) + invitation = InvitationInfo.model_validate(data) assert invitation.email is None @@ -96,11 +96,11 @@ async def test_check_registration_invitation_and_get_email( response = await client.post( "/v0/auth/register/invitations:check", - json=InvitationCheck(invitation="*" * 105).dict(), + json=InvitationCheck(invitation="*" * 105).model_dump(), ) 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..71da6536363 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 @@ -92,22 +92,22 @@ async def test_product_owner_generates_invitation( # request response = await client.post( "/v0/invitation:generate", - json=request_model.dict(exclude_none=True), + json=request_model.model_dump(exclude_none=True), ) # checks 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), + **request_model.model_dump(exclude_none=True), } - assert got.dict(include=set(expected), by_alias=False) == expected + assert got.model_dump(include=set(expected), by_alias=False) == expected product_base_url = f"{client.make_url('/')}" - assert got.invitation_link.startswith(product_base_url) + assert f"{got.invitation_link}".startswith(product_base_url) assert before_dt < got.created assert got.created < datetime.now(tz=timezone.utc) @@ -150,7 +150,7 @@ async def test_pre_registration_and_invitation_workflow( guest=guest_email, trial_account_days=None, extra_credits_in_usd=10, - ).dict() + ).model_dump() # Search user -> nothing response = await client.get("/v0/users:search", params={"email": guest_email}) @@ -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/conftest.py b/services/web/server/tests/unit/with_dbs/03/login/conftest.py index 167315facb4..b9a458add16 100644 --- a/services/web/server/tests/unit/with_dbs/03/login/conftest.py +++ b/services/web/server/tests/unit/with_dbs/03/login/conftest.py @@ -20,7 +20,7 @@ @pytest.fixture -def app_environment(app_environment: EnvVarsDict, monkeypatch: pytest.MonkeyPatch): +def app_environment(app_environment: EnvVarsDict, monkeypatch: pytest.MonkeyPatch, faker: Faker): envs_plugins = setenvs_from_dict( monkeypatch, { @@ -41,6 +41,7 @@ def app_environment(app_environment: EnvVarsDict, monkeypatch: pytest.MonkeyPatc "WEBSERVER_TRACING": "null", "WEBSERVER_VERSION_CONTROL": "0", "WEBSERVER_WALLETS": "1", + "WEBSERVER_TRACING": "null" }, ) diff --git a/services/web/server/tests/unit/with_dbs/03/login/test_login_2fa.py b/services/web/server/tests/unit/with_dbs/03/login/test_login_2fa.py index d6dc34bcdfe..29324b2af23 100644 --- a/services/web/server/tests/unit/with_dbs/03/login/test_login_2fa.py +++ b/services/web/server/tests/unit/with_dbs/03/login/test_login_2fa.py @@ -50,7 +50,7 @@ def app_environment(app_environment: EnvVarsDict, monkeypatch: pytest.MonkeyPatc "LOGIN_2FA_CODE_EXPIRATION_SEC": "60", }, ) - print(ApplicationSettings.create_from_envs().json(indent=1)) + print(ApplicationSettings.create_from_envs().model_dump_json(indent=1)) return {**app_environment, **envs_login} @@ -148,7 +148,7 @@ def _get_confirmation_link_from_email(): url = _get_confirmation_link_from_email() # 2. confirmation - response = await client.get(url) + response = await client.get(f"{url}") assert response.status == status.HTTP_200_OK # check email+password registered 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..2cf5b63eb24 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,6 @@ import pytest import sqlalchemy as sa from aiohttp.test_utils import TestClient -from pydantic import parse_obj_as 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 @@ -29,7 +28,7 @@ def app_environment(app_environment: EnvVarsDict, monkeypatch: pytest.MonkeyPatc }, ) - print(ApplicationSettings.create_from_envs().json(indent=2)) + print(ApplicationSettings.create_from_envs().model_dump_json(indent=2)) return {**app_environment, **envs_login} @@ -106,7 +105,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 = NextPage[CodePageParams].model_validate(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_auth.py b/services/web/server/tests/unit/with_dbs/03/login/test_login_auth.py index c73020d0638..7d16e912414 100644 --- a/services/web/server/tests/unit/with_dbs/03/login/test_login_auth.py +++ b/services/web/server/tests/unit/with_dbs/03/login/test_login_auth.py @@ -30,7 +30,7 @@ def test_login_plugin_setup_succeeded(client: TestClient): assert client.app - print(client.app[APP_SETTINGS_KEY].json(indent=1, sort_keys=True)) + print(client.app[APP_SETTINGS_KEY].model_dump_json(indent=1)) # this should raise AssertionError if not succeedd settings = get_plugin_settings(client.app) diff --git a/services/web/server/tests/unit/with_dbs/03/login/test_login_change_password.py b/services/web/server/tests/unit/with_dbs/03/login/test_login_change_password.py index 41f90807925..a171ec63ae2 100644 --- a/services/web/server/tests/unit/with_dbs/03/login/test_login_change_password.py +++ b/services/web/server/tests/unit/with_dbs/03/login/test_login_change_password.py @@ -85,7 +85,7 @@ async def test_wrong_confirm_pass(client: TestClient, new_password: str): "errors": [ { "code": "value_error", - "message": MSG_PASSWORD_MISMATCH, + "message": f"Value error, {MSG_PASSWORD_MISMATCH}", "resource": "/v0/auth/change-password", "field": "confirm", } 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..d99f8f1f297 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 @@ -95,14 +95,14 @@ async def test_register_body_validation( "status": 422, "errors": [ { - "code": "value_error.email", - "message": "value is not a valid email address", + "code": "value_error", + "message": "value is not a valid email address: An email address must have an @-sign.", "resource": "/v0/auth/register", "field": "email", }, { "code": "value_error", - "message": MSG_PASSWORD_MISMATCH, + "message": f"Value error, {MSG_PASSWORD_MISMATCH}", "resource": "/v0/auth/register", "field": "confirm", }, @@ -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..655afde42b7 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(), @@ -197,7 +197,7 @@ async def test_request_an_account( response = await client.post( "/v0/auth/request-account", - json={"form": user_data, "captcha": 123456}, + json={"form": user_data, "captcha": "123456"}, ) await assert_status(response, status.HTTP_204_NO_CONTENT) 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 043176ae5ee..a9ea81e32fb 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 @@ -132,7 +132,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={ @@ -190,7 +190,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 @@ -233,7 +233,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 @@ -247,7 +247,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 @@ -295,7 +295,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/resource_usage/conftest.py b/services/web/server/tests/unit/with_dbs/03/resource_usage/conftest.py index 4dc9da94974..a4691fcc3a2 100644 --- a/services/web/server/tests/unit/with_dbs/03/resource_usage/conftest.py +++ b/services/web/server/tests/unit/with_dbs/03/resource_usage/conftest.py @@ -17,7 +17,7 @@ def app_environment(app_environment: EnvVarsDict, monkeypatch: pytest.MonkeyPatch): - # print( ApplicationSettings.create_from_envs().json(indent=1 ) + # print( ApplicationSettings.create_from_envs().model_dump_json((indent=1 ) return app_environment | setenvs_from_dict( monkeypatch, 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..ad508f523e4 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,6 @@ PricingUnitGet, ) from models_library.resource_tracker import PricingPlanClassification -from pydantic import parse_obj_as from pytest_mock import MockerFixture from pytest_simcore.helpers.assert_checks import assert_status from pytest_simcore.helpers.webserver_login import UserInfoDict @@ -35,52 +34,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] + PricingPlanGet.model_validate( + 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=PricingPlanGet.model_validate( + 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=PricingPlanGet.model_validate( + 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=PricingPlanGet.model_validate( + 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=PricingUnitGet.model_validate( + 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=PricingUnitGet.model_validate( + 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=PricingUnitGet.model_validate( + PricingUnitGet.model_config["json_schema_extra"]["examples"][0], ), ), ## Pricing plan to service @@ -88,18 +87,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], + PricingPlanToServiceGet.model_validate( + 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=PricingPlanToServiceGet.model_validate( + 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..70114820036 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,6 @@ PricingUnitGet, ) from models_library.utils.fastapi_encoders import jsonable_encoder -from pydantic import parse_obj_as 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 +31,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 = PricingUnitGet.model_validate( + 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 = PricingPlanGet.model_validate( + 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..3ac7ffaa665 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,5 @@ 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"] == OrderBy.model_validate(_order_by) + assert args["filters"] == ServiceResourceUsagesFilters.model_validate(_filter) diff --git a/services/web/server/tests/unit/with_dbs/03/resource_usage/test_usage_services__list.py b/services/web/server/tests/unit/with_dbs/03/resource_usage/test_usage_services__list.py index 33b9d9146f5..6e3a12bfe84 100644 --- a/services/web/server/tests/unit/with_dbs/03/resource_usage/test_usage_services__list.py +++ b/services/web/server/tests/unit/with_dbs/03/resource_usage/test_usage_services__list.py @@ -45,6 +45,8 @@ "started_at": "2023-08-26T14:18:17.600493+00:00", "stopped_at": "2023-08-26T14:18:19.358355+00:00", "service_run_status": "SUCCESS", + "credit_cost": None, + "transaction_status": None } ) ], @@ -199,7 +201,7 @@ async def test_list_service_usage_with_order_by_query_param( assert mock_list_usage_services.called assert error["status"] == status.HTTP_422_UNPROCESSABLE_ENTITY assert error["errors"][0]["message"].startswith( - "We do not support ordering by provided field" + "Value error, We do not support ordering by provided field" ) # with non-parsable field in order by query parameter @@ -237,7 +239,7 @@ async def test_list_service_usage_with_order_by_query_param( assert mock_list_usage_services.called assert error["status"] == status.HTTP_422_UNPROCESSABLE_ENTITY assert error["errors"][0]["message"].startswith( - "value is not a valid enumeration member" + "Input should be 'asc' or 'desc'" ) # without field @@ -251,7 +253,7 @@ async def test_list_service_usage_with_order_by_query_param( _, error = await assert_status(resp, status.HTTP_422_UNPROCESSABLE_ENTITY) assert mock_list_usage_services.called assert error["status"] == status.HTTP_422_UNPROCESSABLE_ENTITY - assert error["errors"][0]["message"].startswith("field required") + assert error["errors"][0]["message"].startswith("Field required") @pytest.mark.parametrize("user_role", [(UserRole.USER)]) diff --git a/services/web/server/tests/unit/with_dbs/03/test__openapi_specs.py b/services/web/server/tests/unit/with_dbs/03/test__openapi_specs.py index ebd268074ab..eafb6ed29b9 100644 --- a/services/web/server/tests/unit/with_dbs/03/test__openapi_specs.py +++ b/services/web/server/tests/unit/with_dbs/03/test__openapi_specs.py @@ -56,7 +56,7 @@ def app(app_environment: EnvVarsDict) -> web.Application: # - all plugins are setup but app is NOT started (i.e events are not triggered) # app_ = create_application() - print(get_application_settings(app_).json(indent=1)) + print(get_application_settings(app_).model_dump_json(indent=1)) return app_ 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..c208162d318 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,10 +136,10 @@ 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) - print(passed.json(indent=1)) + passed = EmailTestPassed.model_validate(data) + print(passed.model_dump_json(indent=1)) class IndexParser(HTMLParser): diff --git a/services/web/server/tests/unit/with_dbs/03/test_socketio.py b/services/web/server/tests/unit/with_dbs/03/test_socketio.py index 05be09f7749..699ff0ccef9 100644 --- a/services/web/server/tests/unit/with_dbs/03/test_socketio.py +++ b/services/web/server/tests/unit/with_dbs/03/test_socketio.py @@ -46,7 +46,7 @@ def app_environment(app_environment: EnvVarsDict, monkeypatch: pytest.MonkeyPatc }, ) - print(ApplicationSettings.create_from_envs().json(indent=1)) + print(ApplicationSettings.create_from_envs().model_dump_json(indent=1)) return app_environment | overrides 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..694d49a998b 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 = FileUploadSchema.model_validate( { "chunk_size": "5", "urls": ["s3://file_id"], @@ -90,8 +93,8 @@ def _resolve(*args, **kwargs) -> AnyUrl: ) -MOCK_FILE_UPLOAD_COMPLETE_RESPONSE = parse_obj_as( - FileUploadCompleteResponse, {"links": {"state": "http://private-url"}} +MOCK_FILE_UPLOAD_COMPLETE_RESPONSE = FileUploadCompleteResponse.model_validate( + {"links": {"state": "http://private-url"}} ) @@ -124,7 +127,7 @@ def _resolve(*args, **kwargs) -> AnyUrl: "PUT", "/v0/storage/locations/0/files/{file_id}", None, - json.loads(MOCK_FILE_UPLOAD_SCHEMA.json()), + json.loads(MOCK_FILE_UPLOAD_SCHEMA.model_dump_json()), id="upload_file", ), pytest.param( @@ -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(f"{storage_url}") ) assert storage_url.host != web_url.host @@ -216,4 +219,4 @@ def test_url_storage_resolver_helpers(faker: Faker, app_environment: EnvVarsDict assert isinstance(storage_url, URL) # this is a bit inconvenient assert isinstance(web_url, AnyUrl) - assert str(web_url) == str(web_request.url) + assert f"{web_url}" == f"{web_request.url}" 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..33d22b4e815 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,14 @@ 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 + assert ( + e.data.model_dump(**RESPONSE_MODEL_POLICY, mode="json") == 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}, @@ -105,7 +107,7 @@ async def test_get_profile( assert profile.role == user_role.name assert profile.groups - got_profile_groups = profile.groups.dict(**RESPONSE_MODEL_POLICY) + got_profile_groups = profile.groups.model_dump(**RESPONSE_MODEL_POLICY, mode="json") assert got_profile_groups["me"] == primary_group assert got_profile_groups["all"] == all_group @@ -147,7 +149,7 @@ async def test_update_profile( data, _ = await assert_status(resp, status.HTTP_200_OK) # This is a PUT! i.e. full replace of profile variable fields! - assert data["first_name"] == ProfileUpdate.__fields__["first_name"].default + assert data["first_name"] == ProfileUpdate.model_fields["first_name"].default assert data["last_name"] == "Foo" assert data["role"] == user_role.name @@ -250,7 +252,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 @@ -292,7 +296,7 @@ async def test_search_and_pre_registration( "registered": True, "status": UserStatus.ACTIVE, } - assert got.dict(include=set(expected)) == expected + assert got.model_dump(include=set(expected)) == expected # NOT in `users` and ONLY `users_pre_registration_details` @@ -307,7 +311,7 @@ async def test_search_and_pre_registration( assert len(found) == 1 got = UserProfile(**found[0]) - assert got.dict(include={"registered", "status"}) == { + assert got.model_dump(include={"registered", "status"}) == { "registered": False, "status": None, } @@ -329,7 +333,7 @@ async def test_search_and_pre_registration( found, _ = await assert_status(resp, status.HTTP_200_OK) assert len(found) == 1 got = UserProfile(**found[0]) - assert got.dict(include={"registered", "status"}) == { + assert got.model_dump(include={"registered", "status"}) == { "registered": True, "status": new_user["status"].name, } @@ -356,7 +360,7 @@ def test_preuserprofile_parse_model_from_request_form_data( # pre-processors pre_user_profile = PreUserProfile(**data) - print(pre_user_profile.json(indent=1)) + print(pre_user_profile.model_dump_json(indent=1)) # institution aliases assert pre_user_profile.institution == account_request_form["company"] @@ -377,7 +381,7 @@ def test_preuserprofile_parse_model_without_extras( account_request_form: dict[str, Any] ): required = { - f.alias or f.name for f in PreUserProfile.__fields__.values() if f.required + f.alias or f_name for f_name, f in PreUserProfile.model_fields.items() if f.is_required() } data = {k: account_request_form[k] for k in required} assert not PreUserProfile(**data).extras @@ -401,6 +405,6 @@ def test_preuserprofile_pre_given_names( account_request_form["lastName"] = given_name pre_user_profile = PreUserProfile(**account_request_form) - print(pre_user_profile.json(indent=1)) + print(pre_user_profile.model_dump_json(indent=1)) assert pre_user_profile.first_name in ["Pedro-Luis", "Pedro Luis"] assert pre_user_profile.first_name == pre_user_profile.last_name 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..048dc779c51 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), @@ -104,7 +104,7 @@ async def _create_notifications( redis_key = get_notification_key(user_id) if user_notifications: for notification in user_notifications: - await redis_client.lpush(redis_key, notification.json()) + await redis_client.lpush(redis_key, notification.model_dump_json()) yield user_notifications @@ -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"] + ) # noqa: F821 assert len(result) <= MAX_NOTIFICATIONS_FOR_USER_TO_SHOW assert result == list( reversed(created_notifications[:MAX_NOTIFICATIONS_FOR_USER_TO_SHOW]) @@ -377,7 +379,7 @@ async def test_update_user_notification_at_correct_index( async def _get_stored_notifications() -> list[UserNotification]: return [ - UserNotification.parse_raw(x) + UserNotification.model_validate_json(x) for x in await notification_redis_client.lrange( get_notification_key(user_id), 0, -1 ) @@ -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/test_users__preferences_api.py b/services/web/server/tests/unit/with_dbs/03/test_users__preferences_api.py index 3835883af8b..8db8935616d 100644 --- a/services/web/server/tests/unit/with_dbs/03/test_users__preferences_api.py +++ b/services/web/server/tests/unit/with_dbs/03/test_users__preferences_api.py @@ -11,12 +11,13 @@ from aiohttp import web from aiohttp.test_utils import TestClient from faker import Faker +from common_library.pydantic_fields_extension import get_type from models_library.api_schemas_webserver.users_preferences import Preference from models_library.products import ProductName from models_library.user_preferences import FrontendUserPreference from models_library.users import UserID from pydantic import BaseModel -from pydantic.fields import ModelField +from pydantic.fields import FieldInfo from pytest_simcore.helpers.monkeypatch_envs import EnvVarsDict, setenvs_from_dict from pytest_simcore.helpers.webserver_login import NewUser from simcore_postgres_database.models.groups_extra_properties import ( @@ -64,8 +65,8 @@ def product_name() -> ProductName: return "osparc" -def _get_model_field(model_class: type[BaseModel], field_name: str) -> ModelField: - return model_class.__dict__["__fields__"][field_name] +def _get_model_field(model_class: type[BaseModel], field_name: str) -> FieldInfo: + return model_class.model_fields[field_name] def _get_default_field_value(model_class: type[BaseModel]) -> Any: @@ -83,7 +84,7 @@ def _get_non_default_value( """given a default value transforms into something that is different""" model_field = _get_model_field(model_class, "value") - value_type = model_field.type_ + value_type = get_type(model_field) value = _get_default_field_value(model_class) if isinstance(value, bool): 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 ed8a2c2979f..679091f6e85 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 @@ -210,7 +210,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..ae95f95f9f9 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,13 @@ from models_library.projects import NodesDict -from pydantic import BaseModel +from pydantic import ConfigDict, RootModel 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 +class WorkbenchModel(RootModel[NodesDict]): + model_config = ConfigDict(populate_by_name=True) def test_compute_workbench_checksum(fake_project: ProjectDict): @@ -21,12 +18,12 @@ 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 # # e.g. order after parse maps order in BaseModel but not in dict # - sha1_w_model = compute_workbench_checksum(workbench.__root__) + sha1_w_model = compute_workbench_checksum(workbench.root) assert sha1_w_model == sha1_w_dict diff --git a/services/web/server/tests/unit/with_dbs/03/version_control/test_version_control_core.py b/services/web/server/tests/unit/with_dbs/03/version_control/test_version_control_core.py index 5b660286cea..705b0458188 100644 --- a/services/web/server/tests/unit/with_dbs/03/version_control/test_version_control_core.py +++ b/services/web/server/tests/unit/with_dbs/03/version_control/test_version_control_core.py @@ -71,9 +71,9 @@ async def test_workflow( vc_repo, project_uuid, HEAD, message="updated message" ) - assert checkpoint2_updated.dict(exclude={"message"}) == checkpoint2.dict( + assert checkpoint2_updated.model_dump( exclude={"message"} - ) + ) == checkpoint2.model_dump(exclude={"message"}) # ------------------------------------- # checking out to v1 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..5270a17c04c 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 @@ -108,8 +108,8 @@ async def test_workflow( resp = await client.get( f"/{VX}/repos/projects/{project_uuid}/checkpoints/{checkpoint1.id}" ) - assert str(resp.url) == checkpoint1.url - assert CheckpointApiModel.parse_obj(data) == checkpoint1 + assert f"{resp.url}" == f"{checkpoint1.url}" + 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 @@ -142,7 +142,7 @@ async def test_workflow( data, _ = await assert_status(resp, status.HTTP_200_OK) assert ( data["workbench"] - == project.dict(exclude_none=True, exclude_unset=True)["workbench"] + == project.model_dump(exclude_none=True, exclude_unset=True)["workbench"] ) # do some changes in project @@ -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/conftest.py b/services/web/server/tests/unit/with_dbs/conftest.py index 84ffd71830f..c943be8c76c 100644 --- a/services/web/server/tests/unit/with_dbs/conftest.py +++ b/services/web/server/tests/unit/with_dbs/conftest.py @@ -41,7 +41,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.faker_factories import random_product @@ -382,7 +382,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)