Skip to content

Commit f335bca

Browse files
giancarloromeoAndrei NeaguGitHKsanderegg
authored
WIP: ⬆️ Upgrade Director v2 service (Pydantic v2) (#6619)
Co-authored-by: Andrei Neagu <[email protected]> Co-authored-by: Andrei Neagu <[email protected]> Co-authored-by: Sylvain <[email protected]>
1 parent a00d55f commit f335bca

File tree

122 files changed

+1649
-991
lines changed

Some content is hidden

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

122 files changed

+1649
-991
lines changed

packages/aws-library/src/aws_library/s3/_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ async def create(
7373
session = aioboto3.Session()
7474
session_client = session.client(
7575
"s3",
76-
endpoint_url=settings.S3_ENDPOINT,
76+
endpoint_url=f"{settings.S3_ENDPOINT}",
7777
aws_access_key_id=settings.S3_ACCESS_KEY,
7878
aws_secret_access_key=settings.S3_SECRET_KEY,
7979
region_name=settings.S3_REGION,

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

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
import datetime
1+
import datetime as dt
22
import re
33
import warnings
4-
from datetime import timedelta
54

65
from pydantic import TypeAdapter, field_validator
76

87

9-
def _validate_legacy_timedelta_str(time_str: str | timedelta) -> str | timedelta:
8+
def _validate_legacy_timedelta_str(time_str: str | dt.timedelta) -> str | dt.timedelta:
109
if not isinstance(time_str, str):
1110
return time_str
1211

@@ -34,14 +33,14 @@ def validate_numeric_string_as_timedelta(field: str):
3433
"""Transforms a float/int number into a valid datetime as it used to work in the past"""
3534

3635
def _numeric_string_as_timedelta(
37-
v: datetime.timedelta | str | float,
38-
) -> datetime.timedelta | str | float:
36+
v: dt.timedelta | str | float,
37+
) -> dt.timedelta | str | float:
3938
if isinstance(v, str):
4039
try:
4140
converted_value = float(v)
4241

43-
iso8601_format = TypeAdapter(timedelta).dump_python(
44-
timedelta(seconds=converted_value), mode="json"
42+
iso8601_format = TypeAdapter(dt.timedelta).dump_python(
43+
dt.timedelta(seconds=converted_value), mode="json"
4544
)
4645
warnings.warn(
4746
f"{field}='{v}' -should be set to-> {field}='{iso8601_format}' (ISO8601 datetime format). "

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,9 @@ def model_dump_with_secrets(
2121
data[field_name] = field_data.total_seconds()
2222

2323
elif isinstance(field_data, SecretStr):
24-
if show_secrets:
25-
data[field_name] = field_data.get_secret_value()
26-
else:
27-
data[field_name] = str(field_data)
24+
data[field_name] = (
25+
field_data.get_secret_value() if show_secrets else str(field_data)
26+
)
2827

2928
elif isinstance(field_data, Url):
3029
data[field_name] = str(field_data)

packages/common-library/tests/test_serialization.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,31 @@
44

55

66
class Credentials(BaseModel):
7-
USERNAME: str | None = None
8-
PASSWORD: SecretStr | None = None
7+
username: str
8+
password: SecretStr
9+
10+
11+
class Access(BaseModel):
12+
credentials: Credentials
913

1014

1115
@pytest.mark.parametrize(
1216
"expected,show_secrets",
1317
[
1418
(
15-
{"USERNAME": "DeepThought", "PASSWORD": "42"},
19+
{"credentials": {"username": "DeepThought", "password": "42"}},
1620
True,
1721
),
1822
(
19-
{"USERNAME": "DeepThought", "PASSWORD": "**********"},
23+
{"credentials": {"username": "DeepThought", "password": "**********"}},
2024
False, # hide secrets
2125
),
2226
],
2327
)
2428
def test_model_dump_with_secrets(expected: dict, show_secrets: bool):
25-
assert expected == model_dump_with_secrets(Credentials(USERNAME="DeepThought", PASSWORD=SecretStr("42")), show_secrets=show_secrets)
29+
assert expected == model_dump_with_secrets(
30+
Access(
31+
credentials=Credentials(username="DeepThought", password=SecretStr("42"))
32+
),
33+
show_secrets=show_secrets,
34+
)

packages/models-library/src/models_library/aiodocker_api.py

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@
1111

1212

1313
class AioDockerContainerSpec(ContainerSpec):
14-
Env: dict[str, str | None] | None = Field(
14+
env: dict[str, str | None] | None = Field( # type: ignore[assignment]
1515
default=None,
16-
description="aiodocker expects here a dictionary and re-convert it back internally`.\n",
16+
alias="Env",
17+
description="aiodocker expects here a dictionary and re-convert it back internally",
1718
)
1819

19-
@field_validator("Env", mode="before")
20+
@field_validator("env", mode="before")
2021
@classmethod
2122
def convert_list_to_dict(cls, v):
2223
if v is not None and isinstance(v, list):
@@ -33,25 +34,22 @@ def convert_list_to_dict(cls, v):
3334
class AioDockerResources1(Resources1):
3435
# NOTE: The Docker REST API documentation is wrong!!!
3536
# Do not set that back to singular Reservation.
36-
Reservation: ResourceObject | None = Field(
37+
reservation: ResourceObject | None = Field(
3738
None, description="Define resources reservation.", alias="Reservations"
3839
)
3940

4041
model_config = ConfigDict(populate_by_name=True)
4142

4243

4344
class AioDockerTaskSpec(TaskSpec):
44-
ContainerSpec: AioDockerContainerSpec | None = Field(
45-
None,
45+
container_spec: AioDockerContainerSpec | None = Field(
46+
default=None, alias="ContainerSpec"
4647
)
4748

48-
Resources: AioDockerResources1 | None = Field(
49-
None,
50-
description="Resource requirements which apply to each individual container created\nas part of the service.\n",
51-
)
49+
resources: AioDockerResources1 | None = Field(default=None, alias="Resources")
5250

5351

5452
class AioDockerServiceSpec(ServiceSpec):
55-
TaskTemplate: AioDockerTaskSpec | None = None
53+
task_template: AioDockerTaskSpec | None = Field(default=None, alias="TaskTemplate")
5654

5755
model_config = ConfigDict(populate_by_name=True, alias_generator=camel_to_snake)

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import TypeAlias
1+
from typing import Any, TypeAlias
22

33
from pydantic import (
44
AnyHttpUrl,
@@ -48,13 +48,12 @@ class WorkerMetrics(BaseModel):
4848
class UsedResources(DictModel[str, NonNegativeFloat]):
4949
@model_validator(mode="before")
5050
@classmethod
51-
def ensure_negative_value_is_zero(cls, values):
51+
def ensure_negative_value_is_zero(cls, values: dict[str, Any]):
5252
# dasks adds/remove resource values and sometimes
5353
# they end up being negative instead of 0
54-
if v := values.get("__root__", {}):
55-
for res_key, res_value in v.items():
56-
if res_value < 0:
57-
v[res_key] = 0
54+
for res_key, res_value in values.items():
55+
if res_value < 0:
56+
values[res_key] = 0
5857
return values
5958

6059

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

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
from typing import Any, TypeAlias
22

33
from models_library.basic_types import IDStr
4-
from pydantic import AnyHttpUrl, AnyUrl, BaseModel, ConfigDict, Field, field_validator
4+
from pydantic import (
5+
AnyHttpUrl,
6+
AnyUrl,
7+
BaseModel,
8+
ConfigDict,
9+
Field,
10+
ValidationInfo,
11+
field_validator,
12+
)
513

614
from ..clusters import ClusterID
715
from ..projects import ProjectID
@@ -63,16 +71,18 @@ class ComputationCreate(BaseModel):
6371

6472
@field_validator("product_name")
6573
@classmethod
66-
def ensure_product_name_defined_if_computation_starts(cls, v, values):
67-
if "start_pipeline" in values and values["start_pipeline"] and v is None:
74+
def _ensure_product_name_defined_if_computation_starts(
75+
cls, v, info: ValidationInfo
76+
):
77+
if info.data.get("start_pipeline") and v is None:
6878
msg = "product_name must be set if computation shall start!"
6979
raise ValueError(msg)
7080
return v
7181

7282
@field_validator("use_on_demand_clusters")
7383
@classmethod
74-
def ensure_expected_options(cls, v, values):
75-
if v is True and ("cluster_id" in values and values["cluster_id"] is not None):
84+
def _ensure_expected_options(cls, v, info: ValidationInfo):
85+
if v and info.data.get("cluster_id") is not None:
7686
msg = "cluster_id cannot be set if use_on_demand_clusters is set"
7787
raise ValueError(msg)
7888
return v

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from pathlib import Path
33
from typing import Final, Literal, TypeAlias
44

5+
from models_library.utils._original_fastapi_encoders import jsonable_encoder
56
from pydantic import (
67
AnyUrl,
78
BaseModel,
@@ -224,18 +225,24 @@ class Cluster(BaseCluster):
224225
@model_validator(mode="before")
225226
@classmethod
226227
def check_owner_has_access_rights(cls, values):
228+
values = jsonable_encoder(values)
229+
227230
is_default_cluster = bool(values["id"] == DEFAULT_CLUSTER_ID)
228231
owner_gid = values["owner"]
229232

230233
# check owner is in the access rights, if not add it
231234
access_rights = values.get("access_rights", values.get("accessRights", {}))
232235
if owner_gid not in access_rights:
233236
access_rights[owner_gid] = (
234-
CLUSTER_USER_RIGHTS if is_default_cluster else CLUSTER_ADMIN_RIGHTS
237+
CLUSTER_USER_RIGHTS.model_dump()
238+
if is_default_cluster
239+
else CLUSTER_ADMIN_RIGHTS.model_dump()
235240
)
236241
# check owner has the expected access
237242
if access_rights[owner_gid] != (
238-
CLUSTER_USER_RIGHTS if is_default_cluster else CLUSTER_ADMIN_RIGHTS
243+
CLUSTER_USER_RIGHTS.model_dump()
244+
if is_default_cluster
245+
else CLUSTER_ADMIN_RIGHTS.model_dump()
239246
):
240247
msg = f"the cluster owner access rights are incorrectly set: {access_rights[owner_gid]}"
241248
raise ValueError(msg)

packages/models-library/src/models_library/docker.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,7 @@ def from_key(cls, key: str) -> "DockerLabelKey":
3737
str, StringConstraints(pattern=DOCKER_GENERIC_TAG_KEY_RE)
3838
]
3939

40-
41-
class DockerPlacementConstraint(ConstrainedStr):
42-
strip_whitespace = True
43-
regex = re.compile(
44-
r"^(?!-)(?![.])(?!.*--)(?!.*[.][.])[a-zA-Z0-9.-]*(?<!-)(?<![.])(!=|==)[a-zA-Z0-9_. -]*$"
45-
)
46-
40+
DockerPlacementConstraint: TypeAlias = Annotated[str, StringConstraints(strip_whitespace = True, pattern = re.compile(r"^(?!-)(?![.])(?!.*--)(?!.*[.][.])[a-zA-Z0-9.-]*(?<!-)(?<![.])(!=|==)[a-zA-Z0-9_. -]*$"))]
4741

4842
_SIMCORE_RUNTIME_DOCKER_LABEL_PREFIX: Final[str] = "io.simcore.runtime."
4943
_BACKWARDS_COMPATIBILITY_SIMCORE_RUNTIME_DOCKER_LABELS_MAP: Final[dict[str, str]] = {

packages/models-library/src/models_library/errors.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
from typing import Any, TypedDict
1+
from typing import Any
2+
3+
from typing_extensions import ( # https://docs.pydantic.dev/latest/api/standard_library_types/#typeddict
4+
TypedDict,
5+
)
26

37
Loc = tuple[int | str, ...]
48

0 commit comments

Comments
 (0)