Skip to content

Commit 203ab11

Browse files
Merge branch 'pydantic_v2_migration_do_not_squash_updates' into is4481/upgrade-director-v2-service
2 parents d629f95 + 0527b1e commit 203ab11

File tree

321 files changed

+2321
-1838
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

321 files changed

+2321
-1838
lines changed

packages/common-library/src/common_library/pydantic_validators.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,35 @@
11
import datetime
2+
import re
23
import warnings
34
from datetime import timedelta
45

56
from pydantic import TypeAdapter, field_validator
67

78

9+
def _validate_legacy_timedelta_str(time_str: str | timedelta) -> str | timedelta:
10+
if not isinstance(time_str, str):
11+
return time_str
12+
13+
# Match the format [-][DD ][HH:MM]SS[.ffffff]
14+
match = re.match(
15+
r"^(?P<sign>-)?(?:(?P<days>\d+)\s)?(?:(?P<hours>\d+):)?(?:(?P<minutes>\d+):)?(?P<seconds>\d+)(?P<fraction>\.\d+)?$",
16+
time_str,
17+
)
18+
if not match:
19+
return time_str
20+
21+
# Extract components with defaults if not present
22+
sign = match.group("sign") or ""
23+
days = match.group("days") or "0"
24+
hours = match.group("hours") or "0"
25+
minutes = match.group("minutes") or "0"
26+
seconds = match.group("seconds")
27+
fraction = match.group("fraction") or ""
28+
29+
# Convert to the format [-][DD]D[,][HH:MM:]SS[.ffffff]
30+
return f"{sign}{int(days)}D,{int(hours):02}:{int(minutes):02}:{seconds}{fraction}"
31+
32+
833
def validate_numeric_string_as_timedelta(field: str):
934
"""Transforms a float/int number into a valid datetime as it used to work in the past"""
1035

@@ -29,7 +54,7 @@ def _numeric_string_as_timedelta(
2954
return converted_value
3055
except ValueError:
3156
# returns format like "1:00:00"
32-
return v
57+
return _validate_legacy_timedelta_str(v)
3358
return v
3459

3560
return field_validator(field, mode="before")(_numeric_string_as_timedelta)

packages/common-library/tests/test_pydantic_validators.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,45 @@
11
from datetime import timedelta
2+
from typing import Annotated
23

34
import pytest
4-
from common_library.pydantic_validators import validate_numeric_string_as_timedelta
5+
from common_library.pydantic_validators import (
6+
validate_legacy_timedelta_str,
7+
validate_numeric_string_as_timedelta,
8+
)
59
from faker import Faker
6-
from pydantic import Field
10+
from pydantic import BeforeValidator, Field
711
from pydantic_settings import BaseSettings, SettingsConfigDict
812
from pytest_simcore.helpers.monkeypatch_envs import setenvs_from_dict
913

1014

15+
def test_validate_legacy_timedelta(monkeypatch: pytest.MonkeyPatch, faker: Faker):
16+
class Settings(BaseSettings):
17+
APP_NAME: str
18+
REQUEST_TIMEOUT: Annotated[
19+
timedelta, BeforeValidator(validate_legacy_timedelta_str)
20+
] = Field(default=timedelta(hours=1))
21+
22+
model_config = SettingsConfigDict()
23+
24+
app_name = faker.pystr()
25+
env_vars: dict[str, str | bool] = {"APP_NAME": app_name}
26+
27+
# without timedelta
28+
setenvs_from_dict(monkeypatch, env_vars)
29+
settings = Settings()
30+
print(settings.model_dump())
31+
assert app_name == settings.APP_NAME
32+
assert timedelta(hours=1) == settings.REQUEST_TIMEOUT
33+
34+
# with timedelta in seconds
35+
env_vars["REQUEST_TIMEOUT"] = "2 1:10:00"
36+
setenvs_from_dict(monkeypatch, env_vars)
37+
settings = Settings()
38+
print(settings.model_dump())
39+
assert app_name == settings.APP_NAME
40+
assert timedelta(days=2, hours=1, minutes=10) == settings.REQUEST_TIMEOUT
41+
42+
1143
def test_validate_timedelta_in_legacy_mode(
1244
monkeypatch: pytest.MonkeyPatch, faker: Faker
1345
):

packages/models-library/src/models_library/api_schemas_directorv2/clusters.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ class ClusterGet(Cluster):
9696
alias="accessRights", default_factory=dict
9797
)
9898

99-
model_config = ConfigDict(populate_by_name=True)
99+
model_config = ConfigDict(extra="allow", populate_by_name=True)
100100

101101
@model_validator(mode="before")
102102
@classmethod
@@ -112,7 +112,7 @@ class ClusterDetailsGet(ClusterDetails):
112112

113113

114114
class ClusterCreate(BaseCluster):
115-
owner: GroupID | None # type: ignore[assignment]
115+
owner: GroupID | None
116116
authentication: ExternalClusterAuthentication
117117
access_rights: dict[GroupID, ClusterAccessRights] = Field(
118118
alias="accessRights", default_factory=dict

packages/models-library/src/models_library/api_schemas_webserver/auth.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,10 @@ class ApiKeyGet(BaseModel):
8383
api_secret: str
8484

8585
model_config = ConfigDict(
86+
from_attributes=True,
8687
json_schema_extra={
8788
"examples": [
8889
{"display_name": "myapi", "api_key": "key", "api_secret": "secret"},
8990
]
90-
}
91+
},
9192
)

packages/models-library/src/models_library/api_schemas_webserver/clusters.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
class ClusterPathParams(BaseModel):
99
cluster_id: ClusterID
1010
model_config = ConfigDict(
11-
populate_by_name=True,
11+
populate_by_name=True,
1212
extra="forbid",
1313
)
1414

packages/models-library/src/models_library/api_schemas_webserver/folders_v2.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class CreateFolderBodyParams(InputSchema):
4545

4646
class PutFolderBodyParams(InputSchema):
4747
name: IDStr
48-
parent_folder_id: FolderID | None
48+
parent_folder_id: FolderID | None = None
4949
model_config = ConfigDict(extra="forbid")
5050

5151
_null_or_none_str_to_none_validator = field_validator(

packages/models-library/src/models_library/api_schemas_webserver/groups.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from contextlib import suppress
22

33
from pydantic import (
4+
AnyHttpUrl,
45
AnyUrl,
56
BaseModel,
67
ConfigDict,
@@ -90,7 +91,7 @@ def _sanitize_legacy_data(cls, v):
9091
if v:
9192
# Enforces null if thumbnail is not valid URL or empty
9293
with suppress(ValidationError):
93-
return TypeAdapter(AnyUrl).validate_python(v)
94+
return TypeAdapter(AnyHttpUrl).validate_python(v)
9495
return None
9596

9697

@@ -143,12 +144,14 @@ class AllUsersGroups(BaseModel):
143144

144145

145146
class GroupUserGet(BaseModel):
146-
id: str | None = Field(None, description="the user id")
147+
id: str | None = Field(None, description="the user id", coerce_numbers_to_str=True)
147148
login: LowerCaseEmailStr | None = Field(None, description="the user login email")
148149
first_name: str | None = Field(None, description="the user first name")
149150
last_name: str | None = Field(None, description="the user last name")
150151
gravatar_id: str | None = Field(None, description="the user gravatar id hash")
151-
gid: str | None = Field(None, description="the user primary gid")
152+
gid: str | None = Field(
153+
None, description="the user primary gid", coerce_numbers_to_str=True
154+
)
152155
access_rights: GroupAccessRights = Field(..., alias="accessRights")
153156

154157
model_config = ConfigDict(

packages/models-library/src/models_library/api_schemas_webserver/projects.py

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
"""
77

88
from datetime import datetime
9-
from typing import Any, Literal, TypeAlias
9+
from typing import Annotated, Any, Literal, TypeAlias
1010

11-
from pydantic import ConfigDict, Field, HttpUrl, field_validator
11+
from models_library.folders import FolderID
12+
from models_library.workspaces import WorkspaceID
13+
from pydantic import BeforeValidator, ConfigDict, Field, HttpUrl, field_validator
1214

1315
from ..api_schemas_long_running_tasks.tasks import TaskGet
1416
from ..basic_types import LongTruncatedStr, ShortTruncatedStr
@@ -105,7 +107,7 @@ class ProjectReplace(InputSchema):
105107
uuid: ProjectID
106108
name: ShortTruncatedStr
107109
description: LongTruncatedStr
108-
thumbnail: HttpUrl | None
110+
thumbnail: Annotated[HttpUrl | None, BeforeValidator(empty_str_to_none_pre_validator)] = Field(default=None)
109111
creation_date: DateTimeStr
110112
last_change_date: DateTimeStr
111113
workbench: NodesDict
@@ -121,25 +123,17 @@ class ProjectReplace(InputSchema):
121123
default_factory=dict, json_schema_extra={"default": {}}
122124
)
123125

124-
_empty_is_none = field_validator("thumbnail", mode="before")(
125-
empty_str_to_none_pre_validator
126-
)
127-
128126

129127
class ProjectPatch(InputSchema):
130128
name: ShortTruncatedStr | None = Field(default=None)
131129
description: LongTruncatedStr | None = Field(default=None)
132-
thumbnail: HttpUrl | None = Field(default=None)
130+
thumbnail: Annotated[HttpUrl | None, BeforeValidator(empty_str_to_none_pre_validator)] = Field(default=None)
133131
access_rights: dict[GroupIDStr, AccessRights] | None = Field(default=None)
134132
classifiers: list[ClassifierID] | None = Field(default=None)
135133
dev: dict | None = Field(default=None)
136134
ui: StudyUI | None = Field(default=None)
137135
quality: dict[str, Any] | None = Field(default=None)
138-
139-
_empty_is_none = field_validator("thumbnail", mode="before")(
140-
empty_str_to_none_pre_validator
141-
)
142-
136+
143137

144138
__all__: tuple[str, ...] = (
145139
"EmptyModel",

packages/models-library/src/models_library/api_schemas_webserver/resource_usage.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from datetime import datetime
22
from decimal import Decimal
33

4-
from pydantic import BaseModel, ConfigDict
4+
from pydantic import BaseModel, ConfigDict, Field
55

66
from ..projects import ProjectID
77
from ..projects_nodes_io import NodeID
@@ -48,7 +48,7 @@ class ServiceRunGet(
4848
class PricingUnitGet(OutputSchema):
4949
pricing_unit_id: PricingUnitId
5050
unit_name: str
51-
unit_extra_info: dict
51+
unit_extra_info: UnitExtraInfo
5252
current_cost_per_unit: Decimal
5353
default: bool
5454

@@ -131,7 +131,7 @@ class UpdatePricingUnitBodyParams(InputSchema):
131131
unit_extra_info: UnitExtraInfo
132132
default: bool
133133
specific_info: SpecificInfo
134-
pricing_unit_cost_update: PricingUnitCostUpdate | None
134+
pricing_unit_cost_update: PricingUnitCostUpdate | None = Field(default=None)
135135

136136
model_config = ConfigDict(
137137
str_strip_whitespace=True,

packages/models-library/src/models_library/api_schemas_webserver/wallets.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from decimal import Decimal
33
from typing import Annotated, Literal, TypeAlias
44

5-
from pydantic import ConfigDict, Field, HttpUrl, PlainSerializer, field_validator
5+
from pydantic import ConfigDict, Field, HttpUrl, PlainSerializer, ValidationInfo, field_validator
66

77
from ..basic_types import AmountDecimal, IDStr, NonNegativeDecimal
88
from ..users import GroupID
@@ -20,7 +20,10 @@ class WalletGet(OutputSchema):
2020
created: datetime
2121
modified: datetime
2222

23-
model_config = ConfigDict(frozen=False)
23+
model_config = ConfigDict(
24+
from_attributes=True,
25+
frozen=False
26+
)
2427

2528

2629
class WalletGetWithAvailableCredits(WalletGet):
@@ -139,6 +142,7 @@ class PaymentMethodGet(OutputSchema):
139142
)
140143

141144
model_config = ConfigDict(
145+
frozen=False,
142146
json_schema_extra={
143147
"examples": [
144148
{
@@ -200,8 +204,8 @@ class ReplaceWalletAutoRecharge(InputSchema):
200204

201205
@field_validator("monthly_limit_in_usd")
202206
@classmethod
203-
def _monthly_limit_greater_than_top_up(cls, v, values):
204-
top_up = values["top_up_amount_in_usd"]
207+
def _monthly_limit_greater_than_top_up(cls, v, info: ValidationInfo):
208+
top_up = info.data["top_up_amount_in_usd"]
205209
if v is not None and v < top_up:
206210
msg = "Monthly limit ({v} USD) should be greater than top up amount ({top_up} USD)"
207211
raise ValueError(msg)

0 commit comments

Comments
 (0)