Skip to content

Commit 56ccda3

Browse files
author
Andrei Neagu
committed
ported dynamic-scheduler
1 parent 556b259 commit 56ccda3

File tree

14 files changed

+186
-77
lines changed

14 files changed

+186
-77
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import datetime
2+
3+
from pydantic import field_validator
4+
5+
6+
def _try_convert_str_to_float_or_return(
7+
v: datetime.timedelta | str | float,
8+
) -> datetime.timedelta | str | float:
9+
if isinstance(v, str):
10+
try:
11+
return float(v)
12+
except ValueError:
13+
# returns format like "1:00:00"
14+
return v
15+
return v
16+
17+
18+
def timedelta_try_convert_str_to_float(field: str):
19+
"""Transforms a float/int number into a valid datetime as it used to work in the past"""
20+
return field_validator(field, mode="before")(_try_convert_str_to_float_or_return)

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

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

3-
from pydantic import BaseModel, ByteSize, ConfigDict, Field
3+
from pydantic import BaseModel, ByteSize, ConfigDict, Field, TypeAdapter
44

55
from ..resource_tracker import HardwareInfo, PricingInfo
66
from ..services import ServicePortKey
@@ -75,6 +75,9 @@ class DynamicServiceCreate(ServiceDetails):
7575

7676

7777
DynamicServiceGet: TypeAlias = RunningDynamicServiceDetails
78+
DynamicServiceGetAdapter: Final[TypeAdapter[DynamicServiceGet]] = TypeAdapter(
79+
DynamicServiceGet
80+
)
7881

7982

8083
class GetProjectInactivityResponse(BaseModel):

packages/models-library/src/models_library/api_schemas_dynamic_scheduler/dynamic_services.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
from typing import Final
2+
13
from models_library.api_schemas_directorv2.dynamic_services import DynamicServiceCreate
24
from models_library.projects import ProjectID
35
from models_library.projects_nodes_io import NodeID
46
from models_library.resource_tracker import HardwareInfo, PricingInfo
57
from models_library.services_resources import ServiceResourcesDictHelpers
68
from models_library.users import UserID
79
from models_library.wallets import WalletInfo
8-
from pydantic import BaseModel, ConfigDict
10+
from pydantic import BaseModel, ConfigDict, TypeAdapter
911

1012

1113
class DynamicServiceStart(DynamicServiceCreate):
@@ -35,6 +37,11 @@ class DynamicServiceStart(DynamicServiceCreate):
3537
)
3638

3739

40+
DynamicServiceStartAdapter: Final[TypeAdapter[DynamicServiceStart]] = TypeAdapter(
41+
DynamicServiceStart
42+
)
43+
44+
3845
class DynamicServiceStop(BaseModel):
3946
user_id: UserID
4047
project_id: ProjectID
@@ -53,3 +60,8 @@ class DynamicServiceStop(BaseModel):
5360
}
5461
}
5562
)
63+
64+
65+
DynamicServiceStopAdapter: Final[TypeAdapter[DynamicServiceStop]] = TypeAdapter(
66+
DynamicServiceStop
67+
)

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# mypy: disable-error-code=truthy-function
2-
from typing import Any, Literal, TypeAlias
2+
from typing import Any, Final, Literal, TypeAlias
33

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

66
from ..api_schemas_directorv2.dynamic_services import RetrieveDataOut
77
from ..basic_types import PortInt
@@ -106,7 +106,7 @@ class NodeGet(OutputSchema):
106106
"service_basepath": "/x/E1O2E-LAH",
107107
"service_state": "pending",
108108
"service_message": "no suitable node (insufficient resources on 1 node)",
109-
"user_id": 123,
109+
"user_id": "123",
110110
},
111111
# dynamic
112112
{
@@ -120,13 +120,16 @@ class NodeGet(OutputSchema):
120120
"service_basepath": "/x/E1O2E-LAH",
121121
"service_state": "pending",
122122
"service_message": "no suitable node (insufficient resources on 1 node)",
123-
"user_id": 123,
123+
"user_id": "123",
124124
},
125125
]
126126
}
127127
)
128128

129129

130+
NodeGetAdapter: Final[TypeAdapter[NodeGet]] = TypeAdapter(NodeGet)
131+
132+
130133
class NodeGetIdle(OutputSchema):
131134
service_state: Literal["idle"]
132135
service_uuid: NodeID
@@ -145,6 +148,9 @@ def from_node_id(cls, node_id: NodeID) -> "NodeGetIdle":
145148
)
146149

147150

151+
NodeGetIdleAdapter: Final[TypeAdapter[NodeGetIdle]] = TypeAdapter(NodeGetIdle)
152+
153+
148154
class NodeGetUnknown(OutputSchema):
149155
service_state: Literal["unknown"]
150156
service_uuid: NodeID

packages/settings-library/src/settings_library/basic_types.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,17 @@
44
# an extra dependency to a larger models_library (intra-repo library)
55

66
from enum import Enum
7-
from typing import Annotated, TypeAlias
7+
from typing import Annotated, Final, TypeAlias
88

9-
from pydantic import Field, StringConstraints
9+
from pydantic import Field, StringConstraints, TypeAdapter
1010

1111
# port number range
1212
PortInt: TypeAlias = Annotated[int, Field(gt=0, lt=65535)]
1313

1414

1515
# e.g. 'v5'
1616
VersionTag: TypeAlias = Annotated[str, StringConstraints(pattern=r"^v\d$")]
17+
VersionTagAdapter: Final[TypeAdapter[VersionTag]] = TypeAdapter(VersionTag)
1718

1819

1920
class LogLevel(str, Enum):

services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/core/settings.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import datetime
22
from functools import cached_property
33

4-
from pydantic import Field, parse_obj_as, validator
4+
from common_library.pydantic_validators import timedelta_try_convert_str_to_float
5+
from pydantic import AliasChoices, ConfigDict, Field, field_validator
56
from settings_library.application import BaseApplicationSettings
6-
from settings_library.basic_types import LogLevel, VersionTag
7+
from settings_library.basic_types import LogLevel, VersionTag, VersionTagAdapter
78
from settings_library.director_v2 import DirectorV2Settings
89
from settings_library.rabbit import RabbitSettings
910
from settings_library.redis import RedisSettings
@@ -19,20 +20,22 @@ class _BaseApplicationSettings(BaseApplicationSettings, MixinLoggingSettings):
1920
# CODE STATICS ---------------------------------------------------------
2021
API_VERSION: str = API_VERSION
2122
APP_NAME: str = PROJECT_NAME
22-
API_VTAG: VersionTag = parse_obj_as(VersionTag, API_VTAG)
23+
API_VTAG: VersionTag = VersionTagAdapter.validate_python(API_VTAG)
2324

2425
# RUNTIME -----------------------------------------------------------
2526

2627
DYNAMIC_SCHEDULER__LOGLEVEL: LogLevel = Field(
2728
default=LogLevel.INFO,
28-
env=["DYNAMIC_SCHEDULER__LOGLEVEL", "LOG_LEVEL", "LOGLEVEL"],
29+
validation_alias=AliasChoices(
30+
"DYNAMIC_SCHEDULER__LOGLEVEL", "LOG_LEVEL", "LOGLEVEL"
31+
),
2932
)
3033
DYNAMIC_SCHEDULER_LOG_FORMAT_LOCAL_DEV_ENABLED: bool = Field(
3134
default=False,
32-
env=[
35+
validation_alias=AliasChoices(
3336
"DYNAMIC_SCHEDULER__LOG_FORMAT_LOCAL_DEV_ENABLED",
3437
"LOG_FORMAT_LOCAL_DEV_ENABLED",
35-
],
38+
),
3639
description="Enables local development log format. WARNING: make sure it is disabled if you want to have structured logs!",
3740
)
3841

@@ -48,11 +51,17 @@ class _BaseApplicationSettings(BaseApplicationSettings, MixinLoggingSettings):
4851
def LOG_LEVEL(self): # noqa: N802
4952
return self.DYNAMIC_SCHEDULER__LOGLEVEL
5053

51-
@validator("DYNAMIC_SCHEDULER__LOGLEVEL")
54+
_try_convert_dynamic_scheduler_stop_service_timeout = (
55+
timedelta_try_convert_str_to_float("DYNAMIC_SCHEDULER_STOP_SERVICE_TIMEOUT")
56+
)
57+
58+
@field_validator("DYNAMIC_SCHEDULER__LOGLEVEL")
5259
@classmethod
5360
def valid_log_level(cls, value: str) -> str:
5461
return cls.validate_log_level(value)
5562

63+
model_config = ConfigDict(extra="allow")
64+
5665

5766
class ApplicationSettings(_BaseApplicationSettings):
5867
"""Web app's environment variables
@@ -61,24 +70,28 @@ class ApplicationSettings(_BaseApplicationSettings):
6170
"""
6271

6372
DYNAMIC_SCHEDULER_RABBITMQ: RabbitSettings = Field(
64-
auto_default_from_env=True, description="settings for service/rabbitmq"
73+
json_schema_extra={"auto_default_from_env": True},
74+
description="settings for service/rabbitmq",
6575
)
6676

6777
DYNAMIC_SCHEDULER_REDIS: RedisSettings = Field(
68-
auto_default_from_env=True, description="settings for service/redis"
78+
json_schema_extra={"auto_default_from_env": True},
79+
description="settings for service/redis",
6980
)
7081

7182
DYNAMIC_SCHEDULER_SWAGGER_API_DOC_ENABLED: bool = Field(
7283
default=True, description="If true, it displays swagger doc at /doc"
7384
)
7485

7586
DYNAMIC_SCHEDULER_DIRECTOR_V2_SETTINGS: DirectorV2Settings = Field(
76-
auto_default_from_env=True, description="settings for director-v2 service"
87+
json_schema_extra={"auto_default_from_env": True},
88+
description="settings for director-v2 service",
7789
)
7890

7991
DYNAMIC_SCHEDULER_PROMETHEUS_INSTRUMENTATION_ENABLED: bool = True
8092

8193
DYNAMIC_SCHEDULER_PROFILING: bool = False
8294
DYNAMIC_SCHEDULER_TRACING: TracingSettings | None = Field(
83-
auto_default_from_env=True, description="settings for opentelemetry tracing"
95+
json_schema_extra={"auto_default_from_env": True},
96+
description="settings for opentelemetry tracing",
8497
)
Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
from typing import Any, ClassVar
2-
31
from models_library.api_schemas__common.meta import BaseMeta
4-
from pydantic import HttpUrl
2+
from pydantic import ConfigDict, HttpUrl
53

64

75
class Meta(BaseMeta):
86
docs_url: HttpUrl
9-
10-
class Config:
11-
schema_extra: ClassVar[dict[str, Any]] = {
12-
"example": {
13-
"name": "simcore_service_dynamic_scheduler",
14-
"version": "2.4.45",
15-
"docs_url": "https://foo.io/doc",
16-
}
7+
model_config = ConfigDict(
8+
json_schema_extra={
9+
"examples": [
10+
{
11+
"name": "simcore_service_dynamic_scheduler",
12+
"version": "2.4.45",
13+
"docs_url": "https://foo.io/doc",
14+
}
15+
]
1716
}
17+
)

services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/services/director_v2/_public_client.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,18 @@
22
from typing import Any
33

44
from fastapi import FastAPI, status
5-
from models_library.api_schemas_directorv2.dynamic_services import DynamicServiceGet
5+
from models_library.api_schemas_directorv2.dynamic_services import (
6+
DynamicServiceGet,
7+
DynamicServiceGetAdapter,
8+
)
69
from models_library.api_schemas_dynamic_scheduler.dynamic_services import (
710
DynamicServiceStart,
811
)
9-
from models_library.api_schemas_webserver.projects_nodes import NodeGet, NodeGetIdle
12+
from models_library.api_schemas_webserver.projects_nodes import (
13+
NodeGet,
14+
NodeGetAdapter,
15+
NodeGetIdle,
16+
)
1017
from models_library.projects_nodes_io import NodeID
1118
from servicelib.fastapi.app_state import SingletonInAppStateMixin
1219
from servicelib.fastapi.http_client import AttachLifespanMixin, HasClientSetupInterface
@@ -43,9 +50,9 @@ async def get_status(
4350
# in case of legacy version
4451
# we need to transfer the correct format!
4552
if "data" in dict_response:
46-
return NodeGet.parse_obj(dict_response["data"])
53+
return NodeGetAdapter.validate_python(dict_response["data"])
4754

48-
return DynamicServiceGet.parse_obj(dict_response)
55+
return DynamicServiceGetAdapter.validate_python(dict_response)
4956
except UnexpectedStatusError as e:
5057
if (
5158
e.response.status_code # type: ignore[attr-defined] # pylint:disable=no-member
@@ -62,9 +69,9 @@ async def run_dynamic_service(
6269

6370
# legacy services
6471
if "data" in dict_response:
65-
return NodeGet.parse_obj(dict_response["data"])
72+
return NodeGetAdapter.validate_python(dict_response["data"])
6673

67-
return DynamicServiceGet.parse_obj(dict_response)
74+
return DynamicServiceGetAdapter.validate_python(dict_response)
6875

6976
async def stop_dynamic_service(
7077
self,

services/dynamic-scheduler/tests/unit/api_rpc/test_api_rpc__services.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,20 @@
99
from faker import Faker
1010
from fastapi import FastAPI, status
1111
from fastapi.encoders import jsonable_encoder
12-
from models_library.api_schemas_directorv2.dynamic_services import DynamicServiceGet
12+
from models_library.api_schemas_directorv2.dynamic_services import (
13+
DynamicServiceGet,
14+
DynamicServiceGetAdapter,
15+
)
1316
from models_library.api_schemas_dynamic_scheduler.dynamic_services import (
1417
DynamicServiceStart,
18+
DynamicServiceStartAdapter,
1519
DynamicServiceStop,
1620
)
17-
from models_library.api_schemas_webserver.projects_nodes import NodeGet, NodeGetIdle
21+
from models_library.api_schemas_webserver.projects_nodes import (
22+
NodeGet,
23+
NodeGetAdapter,
24+
NodeGetIdle,
25+
)
1826
from models_library.projects import ProjectID
1927
from models_library.projects_nodes_io import NodeID
2028
from models_library.users import UserID
@@ -52,14 +60,16 @@ def node_not_found(faker: Faker) -> NodeID:
5260

5361
@pytest.fixture
5462
def service_status_new_style() -> DynamicServiceGet:
55-
return DynamicServiceGet.parse_obj(
56-
DynamicServiceGet.Config.schema_extra["examples"][1]
63+
return DynamicServiceGetAdapter.validate_python(
64+
DynamicServiceGet.model_config["json_schema_extra"]["examples"][1]
5765
)
5866

5967

6068
@pytest.fixture
6169
def service_status_legacy() -> NodeGet:
62-
return NodeGet.parse_obj(NodeGet.Config.schema_extra["examples"][1])
70+
return NodeGetAdapter.validate_python(
71+
NodeGet.model_config["json_schema_extra"]["examples"][1]
72+
)
6373

6474

6575
@pytest.fixture
@@ -173,8 +183,8 @@ async def test_get_state(
173183
@pytest.fixture
174184
def dynamic_service_start() -> DynamicServiceStart:
175185
# one for legacy and one for new style?
176-
return DynamicServiceStart.parse_obj(
177-
DynamicServiceStart.Config.schema_extra["example"]
186+
return DynamicServiceStartAdapter.validate_python(
187+
DynamicServiceStart.model_config["json_schema_extra"]["example"]
178188
)
179189

180190

services/dynamic-scheduler/tests/unit/conftest.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,32 @@
44
import pytest
55
from models_library.api_schemas_dynamic_scheduler.dynamic_services import (
66
DynamicServiceStart,
7+
DynamicServiceStartAdapter,
78
DynamicServiceStop,
9+
DynamicServiceStopAdapter,
810
)
911
from models_library.projects_nodes_io import NodeID
1012

1113

1214
@pytest.fixture
1315
def get_dynamic_service_start() -> Callable[[NodeID], DynamicServiceStart]:
1416
def _(node_id: NodeID) -> DynamicServiceStart:
15-
dict_data = deepcopy(DynamicServiceStart.Config.schema_extra["example"])
17+
dict_data = deepcopy(
18+
DynamicServiceStart.model_config["json_schema_extra"]["example"]
19+
)
1620
dict_data["service_uuid"] = f"{node_id}"
17-
return DynamicServiceStart.parse_obj(dict_data)
21+
return DynamicServiceStartAdapter.validate_python(dict_data)
1822

1923
return _
2024

2125

2226
@pytest.fixture
2327
def get_dynamic_service_stop() -> Callable[[NodeID], DynamicServiceStop]:
2428
def _(node_id: NodeID) -> DynamicServiceStop:
25-
dict_data = deepcopy(DynamicServiceStop.Config.schema_extra["example"])
29+
dict_data = deepcopy(
30+
DynamicServiceStop.model_config["json_schema_extra"]["example"]
31+
)
2632
dict_data["node_id"] = f"{node_id}"
27-
return DynamicServiceStop.parse_obj(dict_data)
33+
return DynamicServiceStopAdapter.validate_python(dict_data)
2834

2935
return _

0 commit comments

Comments
 (0)