From 1450b185d6bf4d721bcb0a218004d17dcd51b26f Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Mon, 23 Jun 2025 16:01:07 +0200 Subject: [PATCH 1/8] dask --- .../container_tasks/io.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/dask-task-models-library/src/dask_task_models_library/container_tasks/io.py b/packages/dask-task-models-library/src/dask_task_models_library/container_tasks/io.py index 0f443c57f68..a6031747eb0 100644 --- a/packages/dask-task-models-library/src/dask_task_models_library/container_tasks/io.py +++ b/packages/dask-task-models-library/src/dask_task_models_library/container_tasks/io.py @@ -75,13 +75,15 @@ def _update_json_schema_extra(schema: JsonDict) -> None: class FileUrl(BaseModel): url: AnyUrl - file_mapping: str | None = Field( - default=None, - description="Local file relpath name (if given), otherwise it takes the url filename", - ) - file_mime_type: str | None = Field( - default=None, description="the file MIME type", pattern=MIME_TYPE_RE - ) + file_mapping: Annotated[ + str | None, + Field( + description="Local file relpath name (if given), otherwise it takes the url filename" + ), + ] = None + file_mime_type: Annotated[ + str | None, Field(description="the file MIME type", pattern=MIME_TYPE_RE) + ] = None @staticmethod def _update_json_schema_extra(schema: JsonDict) -> None: From 11eefc4bf297be429ca44edf695dc97b5e965416 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Mon, 23 Jun 2025 16:01:50 +0200 Subject: [PATCH 2/8] =?UTF-8?q?=F0=9F=8E=A8=20[Backend]=20Refactor=20EC2In?= =?UTF-8?q?stanceBootSpecific=20fields=20to=20use=20Annotated=20with=20DEF?= =?UTF-8?q?AULT=5FFACTORY?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/aws_library/ec2/_models.py | 44 ++++++++++++------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/packages/aws-library/src/aws_library/ec2/_models.py b/packages/aws-library/src/aws_library/ec2/_models.py index 621adc0f4ee..ada7482452e 100644 --- a/packages/aws-library/src/aws_library/ec2/_models.py +++ b/packages/aws-library/src/aws_library/ec2/_models.py @@ -5,6 +5,7 @@ from typing import Annotated, Final, TypeAlias import sh # type: ignore[import-untyped] +from common_library.basic_types import DEFAULT_FACTORY from models_library.docker import DockerGenericTag from pydantic import ( BaseModel, @@ -143,23 +144,32 @@ class EC2InstanceConfig: class EC2InstanceBootSpecific(BaseModel): ami_id: AMIIdStr - custom_boot_scripts: list[CommandStr] = Field( - default_factory=list, - description="script(s) to run on EC2 instance startup (be careful!), " - "each entry is run one after the other using '&&' operator", - ) - pre_pull_images: list[DockerGenericTag] = Field( - default_factory=list, - description="a list of docker image/tags to pull on instance cold start", - ) - pre_pull_images_cron_interval: datetime.timedelta = Field( - default=datetime.timedelta(minutes=30), - description="time interval between pulls of images (minimum is 1 minute) " - "(default to seconds, or see https://pydantic-docs.helpmanual.io/usage/types/#datetime-types for string formating)", - ) - buffer_count: NonNegativeInt = Field( - default=0, description="number of buffer EC2s to keep (defaults to 0)" - ) + custom_boot_scripts: Annotated[ + list[CommandStr], + Field( + default_factory=list, + description="script(s) to run on EC2 instance startup (be careful!), " + "each entry is run one after the other using '&&' operator", + ), + ] = DEFAULT_FACTORY + pre_pull_images: Annotated[ + list[DockerGenericTag], + Field( + default_factory=list, + description="a list of docker image/tags to pull on instance cold start", + ), + ] = DEFAULT_FACTORY + pre_pull_images_cron_interval: Annotated[ + datetime.timedelta, + Field( + description="time interval between pulls of images (minimum is 1 minute) " + "(default to seconds, or see https://pydantic-docs.helpmanual.io/usage/types/#datetime-types for string formating)", + ), + ] = datetime.timedelta(minutes=30) + buffer_count: Annotated[ + NonNegativeInt, + Field(description="number of buffer EC2s to keep (defaults to 0)"), + ] = 0 @field_validator("custom_boot_scripts") @classmethod From 8cf39322a271c240199e83e850139916b7d8ca73 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Tue, 24 Jun 2025 14:21:36 +0200 Subject: [PATCH 3/8] =?UTF-8?q?=F0=9F=8E=A8=20Refactor=20models=20to=20use?= =?UTF-8?q?=20Annotated=20for=20field=20definitions=20across=20various=20s?= =?UTF-8?q?chemas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../aws-library/src/aws_library/s3/_models.py | 9 ++++--- .../tests/test_pydantic_fields_extension.py | 7 +++--- .../tests/test_pydantic_validators.py | 6 ++--- .../api_schemas__common/errors.py | 10 +++----- .../api_schemas__common/meta.py | 9 ++++--- .../api_schemas_api_server/api_keys.py | 4 ++- .../services_specifications.py | 25 +++++++++++-------- .../api_schemas_clusters_keeper/clusters.py | 3 ++- 8 files changed, 42 insertions(+), 31 deletions(-) diff --git a/packages/aws-library/src/aws_library/s3/_models.py b/packages/aws-library/src/aws_library/s3/_models.py index 4d722386526..e20ef13d0e3 100644 --- a/packages/aws-library/src/aws_library/s3/_models.py +++ b/packages/aws-library/src/aws_library/s3/_models.py @@ -1,6 +1,6 @@ import datetime from pathlib import Path -from typing import TypeAlias, cast +from typing import Annotated, TypeAlias, cast from models_library.api_schemas_storage.storage_schemas import ETag from models_library.basic_types import SHA256Str @@ -54,9 +54,10 @@ def as_path(self) -> Path: class S3DirectoryMetaData(BaseModel, frozen=True): prefix: S3ObjectPrefix - size: ByteSize | None = Field( - ..., description="Size of the directory if computed, None if unknown" - ) + size: Annotated[ + ByteSize | None, + Field(description="Size of the directory if computed, None if unknown"), + ] def as_path(self) -> Path: return self.prefix diff --git a/packages/common-library/tests/test_pydantic_fields_extension.py b/packages/common-library/tests/test_pydantic_fields_extension.py index 3461344062a..9c736f0f2ff 100644 --- a/packages/common-library/tests/test_pydantic_fields_extension.py +++ b/packages/common-library/tests/test_pydantic_fields_extension.py @@ -1,13 +1,14 @@ -from typing import Any, Callable, Literal +from collections.abc import Callable +from typing import Any, Literal import pytest from common_library.pydantic_fields_extension import get_type, is_literal, is_nullable -from pydantic import BaseModel, Field +from pydantic import BaseModel class MyModel(BaseModel): a: int - b: float | None = Field(...) + b: float | None c: str = "bla" d: bool | None = None e: Literal["bla"] diff --git a/packages/common-library/tests/test_pydantic_validators.py b/packages/common-library/tests/test_pydantic_validators.py index c1cfea84c67..47ad5d5367c 100644 --- a/packages/common-library/tests/test_pydantic_validators.py +++ b/packages/common-library/tests/test_pydantic_validators.py @@ -7,7 +7,7 @@ validate_numeric_string_as_timedelta, ) from faker import Faker -from pydantic import BeforeValidator, Field +from pydantic import BeforeValidator from pydantic_settings import BaseSettings, SettingsConfigDict from pytest_simcore.helpers.monkeypatch_envs import setenvs_from_dict @@ -17,7 +17,7 @@ class Settings(BaseSettings): APP_NAME: str REQUEST_TIMEOUT: Annotated[ timedelta, BeforeValidator(_validate_legacy_timedelta_str) - ] = Field(default=timedelta(hours=1)) + ] = timedelta(hours=1) model_config = SettingsConfigDict() @@ -45,7 +45,7 @@ def test_validate_timedelta_in_legacy_mode( ): class Settings(BaseSettings): APP_NAME: str - REQUEST_TIMEOUT: timedelta = Field(default=timedelta(seconds=40)) + REQUEST_TIMEOUT: timedelta = timedelta(seconds=40) _validate_request_timeout = validate_numeric_string_as_timedelta( "REQUEST_TIMEOUT" diff --git a/packages/models-library/src/models_library/api_schemas__common/errors.py b/packages/models-library/src/models_library/api_schemas__common/errors.py index d1f7d6aa34d..0e62cd62e9a 100644 --- a/packages/models-library/src/models_library/api_schemas__common/errors.py +++ b/packages/models-library/src/models_library/api_schemas__common/errors.py @@ -1,5 +1,5 @@ import http -from typing import Any +from typing import Annotated, Any from pydantic import BaseModel, Field @@ -7,12 +7,10 @@ class DefaultApiError(BaseModel): - name: IDStr = Field( - ..., - description="Error identifier as a code or a name. " - "Mainly for machine-machine communication purposes.", + name: Annotated[IDStr, Field(description="Exception's class name")] + detail: Annotated[Any | None, Field(description="Human readable error message")] = ( + None ) - detail: Any | None = Field(default=None, description="Human readable error message") @classmethod def from_status_code( diff --git a/packages/models-library/src/models_library/api_schemas__common/meta.py b/packages/models-library/src/models_library/api_schemas__common/meta.py index 514abdc7d6d..2dc0c4e9d32 100644 --- a/packages/models-library/src/models_library/api_schemas__common/meta.py +++ b/packages/models-library/src/models_library/api_schemas__common/meta.py @@ -1,3 +1,5 @@ +from typing import Annotated + from pydantic import BaseModel, ConfigDict, Field from ..basic_types import VersionStr @@ -6,9 +8,10 @@ class BaseMeta(BaseModel): name: str version: VersionStr - released: dict[str, VersionStr] | None = Field( - default=None, description="Maps every route's path tag with a released version" - ) + released: Annotated[ + dict[str, VersionStr] | None, + Field(description="Maps every route's path tag with a released version"), + ] = None model_config = ConfigDict( json_schema_extra={ diff --git a/packages/models-library/src/models_library/api_schemas_api_server/api_keys.py b/packages/models-library/src/models_library/api_schemas_api_server/api_keys.py index 999cb2f192c..534fd82329f 100644 --- a/packages/models-library/src/models_library/api_schemas_api_server/api_keys.py +++ b/packages/models-library/src/models_library/api_schemas_api_server/api_keys.py @@ -1,3 +1,5 @@ +from typing import Annotated + from pydantic import BaseModel, ConfigDict, Field, SecretStr @@ -10,7 +12,7 @@ class ApiKeyInDB(BaseModel): api_key: str api_secret: str - id_: int = Field(0, alias="id") + id_: Annotated[int, Field(alias="id")] = 0 display_name: str user_id: int product_name: str diff --git a/packages/models-library/src/models_library/api_schemas_catalog/services_specifications.py b/packages/models-library/src/models_library/api_schemas_catalog/services_specifications.py index 331ef23f83e..a95d5f622c2 100644 --- a/packages/models-library/src/models_library/api_schemas_catalog/services_specifications.py +++ b/packages/models-library/src/models_library/api_schemas_catalog/services_specifications.py @@ -1,18 +1,23 @@ +from typing import Annotated + from pydantic import BaseModel, Field from ..generated_models.docker_rest_api import ServiceSpec as DockerServiceSpec class ServiceSpecifications(BaseModel): - sidecar: DockerServiceSpec | None = Field( - default=None, - description="schedule-time specifications for the service sidecar (follows Docker Service creation API, see https://docs.docker.com/engine/api/v1.25/#operation/ServiceCreate)", - ) - service: DockerServiceSpec | None = Field( - default=None, - description="schedule-time specifications specifications for the service (follows Docker Service creation API (specifically only the Resources part), see https://docs.docker.com/engine/api/v1.41/#tag/Service/operation/ServiceCreate", - ) + sidecar: Annotated[ + DockerServiceSpec | None, + Field( + description="schedule-time specifications for the service sidecar (follows Docker Service creation API, see https://docs.docker.com/engine/api/v1.25/#operation/ServiceCreate)", + ), + ] = None + service: Annotated[ + DockerServiceSpec | None, + Field( + description="schedule-time specifications specifications for the service (follows Docker Service creation API (specifically only the Resources part), see https://docs.docker.com/engine/api/v1.41/#tag/Service/operation/ServiceCreate", + ), + ] = None -class ServiceSpecificationsGet(ServiceSpecifications): - ... +class ServiceSpecificationsGet(ServiceSpecifications): ... diff --git a/packages/models-library/src/models_library/api_schemas_clusters_keeper/clusters.py b/packages/models-library/src/models_library/api_schemas_clusters_keeper/clusters.py index 135b42188b8..dad13b9db24 100644 --- a/packages/models-library/src/models_library/api_schemas_clusters_keeper/clusters.py +++ b/packages/models-library/src/models_library/api_schemas_clusters_keeper/clusters.py @@ -1,5 +1,6 @@ import datetime from enum import auto +from typing import Annotated from pydantic import AnyUrl, BaseModel, Field @@ -17,7 +18,7 @@ class ClusterState(StrAutoEnum): class OnDemandCluster(BaseModel): endpoint: AnyUrl - authentication: ClusterAuthentication = Field(discriminator="type") + authentication: Annotated[ClusterAuthentication, Field(discriminator="type")] state: ClusterState user_id: UserID wallet_id: WalletID | None From 5f53193ab9e742905d2d4885021e0827bba0a795 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Tue, 24 Jun 2025 14:25:11 +0200 Subject: [PATCH 4/8] new round --- .../api_schemas_directorv2/clusters.py | 19 +++++++------------ .../api_schemas_directorv2/errors.py | 8 +++++--- .../api_schemas_long_running_tasks/base.py | 6 +++--- 3 files changed, 15 insertions(+), 18 deletions(-) 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 26b7d10d0be..f6c00b530b1 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 @@ -20,10 +20,10 @@ class TaskCounts(BaseModel): class WorkerMetrics(BaseModel): - cpu: float = Field(..., description="consumed % of cpus") - memory: ByteSize = Field(..., description="consumed memory") - num_fds: int = Field(..., description="consumed file descriptors") - task_counts: TaskCounts = Field(..., description="task details") + cpu: Annotated[float, Field(description="consumed % of cpus")] + memory: Annotated[ByteSize, Field(description="consumed memory")] + num_fds: Annotated[int, Field(description="consumed file descriptors")] + task_counts: Annotated[TaskCounts, Field(description="task details")] AvailableResources: TypeAlias = DictModel[str, PositiveFloat] @@ -54,7 +54,7 @@ class Worker(BaseModel): class Scheduler(BaseModel): - status: str = Field(..., description="The running status of the scheduler") + status: Annotated[str, Field(description="The running status of the scheduler")] workers: Annotated[WorkersDict | None, Field(default_factory=dict)] @field_validator("workers", mode="before") @@ -66,10 +66,5 @@ def ensure_workers_is_empty_dict(cls, v): class ClusterDetails(BaseModel): - scheduler: Scheduler = Field( - ..., - description="This contains dask scheduler information given by the underlying dask library", - ) - dashboard_link: AnyUrl = Field( - ..., description="Link to this scheduler's dashboard" - ) + scheduler: Annotated[Scheduler, Field(description="scheduler information")] + dashboard_link: Annotated[AnyUrl, Field(description="Link to the dask dashboard")] diff --git a/packages/models-library/src/models_library/api_schemas_directorv2/errors.py b/packages/models-library/src/models_library/api_schemas_directorv2/errors.py index ecf33eefd14..81b7e07e1fe 100644 --- a/packages/models-library/src/models_library/api_schemas_directorv2/errors.py +++ b/packages/models-library/src/models_library/api_schemas_directorv2/errors.py @@ -1,14 +1,16 @@ +from typing import Annotated + from pydantic import BaseModel, Field class Error(BaseModel): - code: str | None = Field(None, description="Server Exception") + code: Annotated[str | None, Field(description="Server Exception")] = None class ErrorType(BaseModel): - message: str = Field(..., description="Error message") + message: Annotated[str, Field(description="Error message")] + status: Annotated[int, Field(description="Error code")] errors: list[Error] | None = None - status: int = Field(..., description="Error code") class ErrorEnveloped(BaseModel): diff --git a/packages/models-library/src/models_library/api_schemas_long_running_tasks/base.py b/packages/models-library/src/models_library/api_schemas_long_running_tasks/base.py index a3bb93813dc..8007df5ef8a 100644 --- a/packages/models-library/src/models_library/api_schemas_long_running_tasks/base.py +++ b/packages/models-library/src/models_library/api_schemas_long_running_tasks/base.py @@ -18,9 +18,9 @@ class TaskProgress(BaseModel): defined as a float bound between 0.0 and 1.0 """ - task_id: TaskId | None = Field(default=None) - message: ProgressMessage = Field(default="") - percent: ProgressPercent = Field(default=0.0) + task_id: TaskId | None = None + message: ProgressMessage = "" + percent: ProgressPercent = 0.0 @validate_call def update( From f3fec65f056d49417a14d4eeb87732ec47177e67 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Tue, 24 Jun 2025 14:26:40 +0200 Subject: [PATCH 5/8] tuning --- .github/prompts/pydantic-annotated-fields.prompt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/prompts/pydantic-annotated-fields.prompt.md b/.github/prompts/pydantic-annotated-fields.prompt.md index cb2c24b851d..631a51a1b74 100644 --- a/.github/prompts/pydantic-annotated-fields.prompt.md +++ b/.github/prompts/pydantic-annotated-fields.prompt.md @@ -13,7 +13,7 @@ Follow these guidelines: 4. Add the import: `from common_library.basic_types import DEFAULT_FACTORY` if it's not already present. 5. If `Field()` has no parameters (empty), don't use Annotated at all. Just use: `field_name: field_type = default_value`. 6. Leave any model validations, `model_config` settings, and `field_validators` untouched. - +7. Must keep the original Field descriptions and validation parameters intact (except for the `default` parameter). ## Examples From f6a4b9400484647fcc264d2e04bc4aab97a6873f Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Wed, 25 Jun 2025 09:56:47 +0200 Subject: [PATCH 6/8] all settings --- .../utils_projects_nodes.py | 7 ++- .../src/settings_library/docker_api_proxy.py | 13 ++--- packages/settings-library/tests/conftest.py | 15 +++--- packages/settings-library/tests/test_base.py | 48 +++++++++---------- .../tests/test_base_w_postgres.py | 34 +++++++------ .../settings-library/tests/test_utils_cli.py | 4 +- .../tests/test_utils_logging.py | 21 ++++---- 7 files changed, 78 insertions(+), 64 deletions(-) diff --git a/packages/postgres-database/src/simcore_postgres_database/utils_projects_nodes.py b/packages/postgres-database/src/simcore_postgres_database/utils_projects_nodes.py index 6fc72990b30..f8248193952 100644 --- a/packages/postgres-database/src/simcore_postgres_database/utils_projects_nodes.py +++ b/packages/postgres-database/src/simcore_postgres_database/utils_projects_nodes.py @@ -1,12 +1,13 @@ import datetime import uuid from dataclasses import dataclass -from typing import Any +from typing import Annotated, Any import asyncpg.exceptions # type: ignore[import-untyped] import sqlalchemy import sqlalchemy.exc from common_library.async_tools import maybe_await +from common_library.basic_types import DEFAULT_FACTORY from common_library.errors_classes import OsparcErrorMixin from pydantic import BaseModel, ConfigDict, Field from simcore_postgres_database.utils_aiosqlalchemy import map_db_exception @@ -47,7 +48,9 @@ class ProjectNodesDuplicateNodeError(BaseProjectNodesError): class ProjectNodeCreate(BaseModel): node_id: uuid.UUID - required_resources: dict[str, Any] = Field(default_factory=dict) + required_resources: Annotated[dict[str, Any], Field(default_factory=dict)] = ( + DEFAULT_FACTORY + ) key: str version: str label: str diff --git a/packages/settings-library/src/settings_library/docker_api_proxy.py b/packages/settings-library/src/settings_library/docker_api_proxy.py index 14f66f0934e..cc002c5a318 100644 --- a/packages/settings-library/src/settings_library/docker_api_proxy.py +++ b/packages/settings-library/src/settings_library/docker_api_proxy.py @@ -1,4 +1,5 @@ from functools import cached_property +from typing import Annotated from pydantic import Field, SecretStr @@ -7,12 +8,12 @@ class DockerApiProxysettings(BaseCustomSettings): - DOCKER_API_PROXY_HOST: str = Field( - description="hostname of the docker-api-proxy service" - ) - DOCKER_API_PROXY_PORT: PortInt = Field( - 8888, description="port of the docker-api-proxy service" - ) + DOCKER_API_PROXY_HOST: Annotated[ + str, Field(description="hostname of the docker-api-proxy service") + ] + DOCKER_API_PROXY_PORT: Annotated[ + PortInt, Field(description="port of the docker-api-proxy service") + ] = 8888 DOCKER_API_PROXY_SECURE: bool = False DOCKER_API_PROXY_USER: str diff --git a/packages/settings-library/tests/conftest.py b/packages/settings-library/tests/conftest.py index 0431a6c6748..c2a02e3a9b4 100644 --- a/packages/settings-library/tests/conftest.py +++ b/packages/settings-library/tests/conftest.py @@ -4,6 +4,7 @@ import sys from pathlib import Path +from typing import Annotated import pytest import settings_library @@ -96,13 +97,15 @@ class _ApplicationSettings(BaseCustomSettings): # NOTE: by convention, an addon is disabled when APP_ADDON=None, so we make this # entry nullable as well - APP_OPTIONAL_ADDON: _ModuleSettings | None = Field( - json_schema_extra={"auto_default_from_env": True} - ) + APP_OPTIONAL_ADDON: Annotated[ + _ModuleSettings | None, + Field(json_schema_extra={"auto_default_from_env": True}), + ] # NOTE: example of a group that cannot be disabled (not nullable) - APP_REQUIRED_PLUGIN: PostgresSettings | None = Field( - json_schema_extra={"auto_default_from_env": True} - ) + APP_REQUIRED_PLUGIN: Annotated[ + PostgresSettings | None, + Field(json_schema_extra={"auto_default_from_env": True}), + ] return _ApplicationSettings diff --git a/packages/settings-library/tests/test_base.py b/packages/settings-library/tests/test_base.py index d4ebd987760..cee6d54cd84 100644 --- a/packages/settings-library/tests/test_base.py +++ b/packages/settings-library/tests/test_base.py @@ -6,7 +6,7 @@ import json from collections.abc import Callable -from typing import Any +from typing import Annotated, Any import pydantic import pytest @@ -74,12 +74,10 @@ class M1(BaseCustomSettings): VALUE_NULLABLE_DEFAULT_VALUE: S | None = S(S_VALUE=42) VALUE_NULLABLE_DEFAULT_NULL: S | None = None - VALUE_NULLABLE_DEFAULT_ENV: S | None = Field( - json_schema_extra={"auto_default_from_env": True} - ) - VALUE_DEFAULT_ENV: S = Field( - json_schema_extra={"auto_default_from_env": True} - ) + VALUE_NULLABLE_DEFAULT_ENV: Annotated[ + S | None, Field(auto_default_from_env=True) + ] + VALUE_DEFAULT_ENV: Annotated[S, Field(auto_default_from_env=True)] class M2(BaseCustomSettings): # @@ -91,14 +89,12 @@ class M2(BaseCustomSettings): VALUE_NULLABLE_DEFAULT_NULL: S | None = None # defaults enabled but if not exists, it disables - VALUE_NULLABLE_DEFAULT_ENV: S | None = Field( - json_schema_extra={"auto_default_from_env": True} - ) + VALUE_NULLABLE_DEFAULT_ENV: Annotated[ + S | None, Field(auto_default_from_env=True) + ] # cannot be disabled - VALUE_DEFAULT_ENV: S = Field( - json_schema_extra={"auto_default_from_env": True} - ) + VALUE_DEFAULT_ENV: Annotated[S, Field(auto_default_from_env=True)] # Changed in version 3.7: Dictionary order is guaranteed to be insertion order _classes = {"M1": M1, "M2": M2, "S": S} @@ -108,7 +104,7 @@ class M2(BaseCustomSettings): def test_create_settings_class( - create_settings_class: Callable[[str], type[BaseCustomSettings]] + create_settings_class: Callable[[str], type[BaseCustomSettings]], ): M = create_settings_class("M1") @@ -216,9 +212,12 @@ def test_auto_default_to_none_logs_a_warning( class SettingsClass(BaseCustomSettings): VALUE_NULLABLE_DEFAULT_NULL: S | None = None - VALUE_NULLABLE_DEFAULT_ENV: S | None = Field( - json_schema_extra={"auto_default_from_env": True}, - ) + VALUE_NULLABLE_DEFAULT_ENV: Annotated[ + S | None, + Field( + auto_default_from_env=True, + ), + ] = None instance = SettingsClass.create_from_envs() assert instance.VALUE_NULLABLE_DEFAULT_NULL is None @@ -245,9 +244,12 @@ def test_auto_default_to_not_none( class SettingsClass(BaseCustomSettings): VALUE_NULLABLE_DEFAULT_NULL: S | None = None - VALUE_NULLABLE_DEFAULT_ENV: S | None = Field( - json_schema_extra={"auto_default_from_env": True}, - ) + VALUE_NULLABLE_DEFAULT_ENV: Annotated[ + S | None, + Field( + auto_default_from_env=True, + ), + ] = None instance = SettingsClass.create_from_envs() assert instance.VALUE_NULLABLE_DEFAULT_NULL is None @@ -342,7 +344,7 @@ def test_issubclass_type_error_with_pydantic_models(): # here reproduces the problem with our settings that ANE and PC had class SettingsClassThatFailed(BaseCustomSettings): - FOO: dict[str, str] | None = Field(default=None) + FOO: dict[str, str] | None = None SettingsClassThatFailed(FOO={}) assert SettingsClassThatFailed(FOO=None) == SettingsClassThatFailed() @@ -352,9 +354,7 @@ def test_upgrade_failure_to_pydantic_settings_2_6( mock_env_devel_environment: EnvVarsDict, ): class ProblematicSettings(BaseCustomSettings): - WEBSERVER_EMAIL: SMTPSettings | None = Field( - json_schema_extra={"auto_default_from_env": True} - ) + WEBSERVER_EMAIL: SMTPSettings | None = None model_config = SettingsConfigDict(nested_model_default_partial_update=True) diff --git a/packages/settings-library/tests/test_base_w_postgres.py b/packages/settings-library/tests/test_base_w_postgres.py index 37329a4e9bb..1597e9edf99 100644 --- a/packages/settings-library/tests/test_base_w_postgres.py +++ b/packages/settings-library/tests/test_base_w_postgres.py @@ -5,6 +5,7 @@ import os from collections.abc import Callable +from typing import Annotated import pytest from pydantic import AliasChoices, Field, ValidationError, __version__ @@ -53,17 +54,18 @@ class _FakePostgresSettings(BaseCustomSettings): POSTGRES_USER: str POSTGRES_PASSWORD: str - POSTGRES_DB: str = Field(...) + POSTGRES_DB: str + POSTGRES_MINSIZE: Annotated[int, Field(ge=1)] = 1 + POSTGRES_MAXSIZE: Annotated[int, Field(ge=1)] = 50 - POSTGRES_MINSIZE: int = Field(1, ge=1) - POSTGRES_MAXSIZE: int = Field(50, ge=1) - - POSTGRES_CLIENT_NAME: str | None = Field( - None, - validation_alias=AliasChoices( - "HOST", "HOSTNAME", "POSTGRES_CLIENT_NAME" + POSTGRES_CLIENT_NAME: Annotated[ + str | None, + Field( + validation_alias=AliasChoices( + "HOST", "HOSTNAME", "POSTGRES_CLIENT_NAME" + ), ), - ) + ] = None # # Different constraints on WEBSERVER_POSTGRES subsettings @@ -77,15 +79,17 @@ class S2(BaseCustomSettings): class S3(BaseCustomSettings): # cannot be disabled!! - WEBSERVER_POSTGRES_DEFAULT_ENV: _FakePostgresSettings = Field( - json_schema_extra={"auto_default_from_env": True} - ) + WEBSERVER_POSTGRES_DEFAULT_ENV: Annotated[ + _FakePostgresSettings, + Field(json_schema_extra={"auto_default_from_env": True}), + ] class S4(BaseCustomSettings): # defaults enabled but if cannot be resolved, it disables - WEBSERVER_POSTGRES_NULLABLE_DEFAULT_ENV: _FakePostgresSettings | None = ( - Field(json_schema_extra={"auto_default_from_env": True}) - ) + WEBSERVER_POSTGRES_NULLABLE_DEFAULT_ENV: Annotated[ + _FakePostgresSettings | None, + Field(json_schema_extra={"auto_default_from_env": True}), + ] class S5(BaseCustomSettings): # defaults disabled but only explicit enabled diff --git a/packages/settings-library/tests/test_utils_cli.py b/packages/settings-library/tests/test_utils_cli.py index 49c684ea626..e5a89dd6c68 100644 --- a/packages/settings-library/tests/test_utils_cli.py +++ b/packages/settings-library/tests/test_utils_cli.py @@ -6,7 +6,7 @@ import logging from collections.abc import Callable from io import StringIO -from typing import Any +from typing import Annotated, Any import pytest import typer @@ -414,7 +414,7 @@ def test_cli_settings_exclude_unset_as_json( def test_print_as(capsys: pytest.CaptureFixture): class FakeSettings(BaseCustomSettings): - INTEGER: int = Field(..., description="Some info") + INTEGER: Annotated[int, Field(description="Some info")] SECRET: SecretStr URL: AnyHttpUrl diff --git a/packages/settings-library/tests/test_utils_logging.py b/packages/settings-library/tests/test_utils_logging.py index d63a8ae8538..f847a716e5f 100644 --- a/packages/settings-library/tests/test_utils_logging.py +++ b/packages/settings-library/tests/test_utils_logging.py @@ -1,4 +1,5 @@ import logging +from typing import Annotated from pydantic import AliasChoices, Field, field_validator from settings_library.base import BaseCustomSettings @@ -17,17 +18,19 @@ class Settings(BaseCustomSettings, MixinLoggingSettings): SC_BOOT_MODE: BootModeEnum | None = None # LOGGING - LOG_LEVEL: str = Field( - "WARNING", - validation_alias=AliasChoices( - "APPNAME_LOG_LEVEL", - "LOG_LEVEL", + LOG_LEVEL: Annotated[ + str, + Field( + validation_alias=AliasChoices( + "APPNAME_LOG_LEVEL", + "LOG_LEVEL", + ), ), - ) + ] = "WARNING" - APPNAME_DEBUG: bool = Field( - default=False, description="Starts app in debug mode" - ) + APPNAME_DEBUG: Annotated[ + bool, Field(description="Starts app in debug mode") + ] = False @field_validator("LOG_LEVEL", mode="before") @classmethod From 08ce9b7e53489959af54fa91b583d1918ce79859 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Wed, 25 Jun 2025 09:59:50 +0200 Subject: [PATCH 7/8] api --- api/specs/web-server/_auth.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/api/specs/web-server/_auth.py b/api/specs/web-server/_auth.py index 7860ef98f03..f17e46c65a2 100644 --- a/api/specs/web-server/_auth.py +++ b/api/specs/web-server/_auth.py @@ -4,7 +4,7 @@ # pylint: disable=too-many-arguments -from typing import Any +from typing import Annotated, Any from fastapi import APIRouter, status from models_library.api_schemas_webserver.auth import ( @@ -211,13 +211,16 @@ async def change_email(_body: ChangeEmailBody): class PasswordCheckSchema(BaseModel): - strength: confloat(ge=0.0, le=1.0) = Field( # type: ignore - ..., - description="The strength of the password ranges from 0 (extremely weak) and 1 (extremely strong)", - ) - rating: str | None = Field( - None, description="Human readable rating from infinitely weak to very strong" - ) + strength: Annotated[ + confloat(ge=0.0, le=1.0), + Field( + description="The strength of the password ranges from 0 (extremely weak) and 1 (extremely strong)", + ), + ] + rating: Annotated[ + str | None, + Field(description="Human readable rating from infinitely weak to very strong"), + ] = None improvements: Any | None = None From 97647817f3529c9afc831dbed0b2e785553c9655 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Wed, 25 Jun 2025 10:22:52 +0200 Subject: [PATCH 8/8] fix --- packages/settings-library/tests/test_base.py | 28 +++++++++++++++----- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/packages/settings-library/tests/test_base.py b/packages/settings-library/tests/test_base.py index cee6d54cd84..3a94448b510 100644 --- a/packages/settings-library/tests/test_base.py +++ b/packages/settings-library/tests/test_base.py @@ -75,9 +75,17 @@ class M1(BaseCustomSettings): VALUE_NULLABLE_DEFAULT_NULL: S | None = None VALUE_NULLABLE_DEFAULT_ENV: Annotated[ - S | None, Field(auto_default_from_env=True) + S | None, + Field( + json_schema_extra={"auto_default_from_env": True}, + ), + ] + VALUE_DEFAULT_ENV: Annotated[ + S, + Field( + json_schema_extra={"auto_default_from_env": True}, + ), ] - VALUE_DEFAULT_ENV: Annotated[S, Field(auto_default_from_env=True)] class M2(BaseCustomSettings): # @@ -90,11 +98,19 @@ class M2(BaseCustomSettings): # defaults enabled but if not exists, it disables VALUE_NULLABLE_DEFAULT_ENV: Annotated[ - S | None, Field(auto_default_from_env=True) + S | None, + Field( + json_schema_extra={"auto_default_from_env": True}, + ), ] # cannot be disabled - VALUE_DEFAULT_ENV: Annotated[S, Field(auto_default_from_env=True)] + VALUE_DEFAULT_ENV: Annotated[ + S, + Field( + json_schema_extra={"auto_default_from_env": True}, + ), + ] # Changed in version 3.7: Dictionary order is guaranteed to be insertion order _classes = {"M1": M1, "M2": M2, "S": S} @@ -215,7 +231,7 @@ class SettingsClass(BaseCustomSettings): VALUE_NULLABLE_DEFAULT_ENV: Annotated[ S | None, Field( - auto_default_from_env=True, + json_schema_extra={"auto_default_from_env": True}, ), ] = None @@ -247,7 +263,7 @@ class SettingsClass(BaseCustomSettings): VALUE_NULLABLE_DEFAULT_ENV: Annotated[ S | None, Field( - auto_default_from_env=True, + json_schema_extra={"auto_default_from_env": True}, ), ] = None