Skip to content

Commit ef10eeb

Browse files
authored
Merge branch 'master' into enh/model-organizations
2 parents ed64f76 + ac1af68 commit ef10eeb

File tree

16 files changed

+178
-85
lines changed

16 files changed

+178
-85
lines changed

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

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from pydantic import ConfigDict, Field, field_validator
1+
from pydantic import Field, field_validator
22

33
from .generated_models.docker_rest_api import (
44
ContainerSpec,
@@ -7,7 +7,6 @@
77
ServiceSpec,
88
TaskSpec,
99
)
10-
from .utils.change_case import camel_to_snake
1110

1211

1312
class AioDockerContainerSpec(ContainerSpec):
@@ -38,8 +37,6 @@ class AioDockerResources1(Resources1):
3837
None, description="Define resources reservation.", alias="Reservations"
3938
)
4039

41-
model_config = ConfigDict(populate_by_name=True)
42-
4340

4441
class AioDockerTaskSpec(TaskSpec):
4542
container_spec: AioDockerContainerSpec | None = Field(
@@ -51,5 +48,3 @@ class AioDockerTaskSpec(TaskSpec):
5148

5249
class AioDockerServiceSpec(ServiceSpec):
5350
task_template: AioDockerTaskSpec | None = Field(default=None, alias="TaskTemplate")
54-
55-
model_config = ConfigDict(populate_by_name=True, alias_generator=camel_to_snake)

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
Models node UI (legacy model, use instead projects.ui.py)
33
"""
44

5-
from pydantic import BaseModel, ConfigDict, Field
5+
from typing import Annotated
6+
7+
from pydantic import BaseModel, ConfigDict, Field, PlainSerializer
68
from pydantic_extra_types.color import Color
79

810

@@ -14,6 +16,6 @@ class Position(BaseModel):
1416

1517

1618
class Marker(BaseModel):
17-
color: Color = Field(...)
19+
color: Annotated[Color, PlainSerializer(str), Field(...)]
1820

1921
model_config = ConfigDict(extra="forbid")

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
Models Front-end UI
33
"""
44

5-
from typing import Literal
5+
from typing import Annotated, Literal
66

7-
from pydantic import BaseModel, ConfigDict, Field, field_validator
7+
from pydantic import BaseModel, ConfigDict, Field, PlainSerializer, field_validator
88
from pydantic_extra_types.color import Color
99
from typing_extensions import ( # https://docs.pydantic.dev/latest/api/standard_library_types/#typeddict
1010
TypedDict,
@@ -31,7 +31,7 @@ class Slideshow(_SlideshowRequired, total=False):
3131

3232
class Annotation(BaseModel):
3333
type: Literal["note", "rect", "text"] = Field(...)
34-
color: Color = Field(...)
34+
color: Annotated[Color, PlainSerializer(str), Field(...)]
3535
attributes: dict = Field(..., description="svg attributes")
3636
model_config = ConfigDict(
3737
extra="forbid",

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ def validate_volume_limits(cls, v, info: ValidationInfo) -> str | None:
226226
"outputs_path": "/tmp/outputs", # noqa: S108 nosec
227227
"inputs_path": "/tmp/inputs", # noqa: S108 nosec
228228
"state_paths": ["/tmp/save_1", "/tmp_save_2"], # noqa: S108 nosec
229-
"state_exclude": ["/tmp/strip_me/*", "*.py"], # noqa: S108 nosec
229+
"state_exclude": ["/tmp/strip_me/*"], # noqa: S108 nosec
230230
},
231231
{
232232
"outputs_path": "/t_out",
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from models_library.projects_nodes_ui import Marker
2+
from pydantic_extra_types.color import Color
3+
4+
5+
def test_marker_serialization():
6+
m = Marker(color=Color("#b7e28d"))
7+
8+
assert m.model_dump_json() == '{"color":"#b7e28d"}'

packages/models-library/tests/test_service_settings_labels.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,9 @@ def test_container_outgoing_permit_list_and_container_allow_internet_without_com
291291
)
292292
},
293293
):
294-
assert TypeAdapter(DynamicSidecarServiceLabels).validate_json(json.dumps(dict_data))
294+
assert TypeAdapter(DynamicSidecarServiceLabels).validate_json(
295+
json.dumps(dict_data)
296+
)
295297

296298

297299
def test_container_allow_internet_no_compose_spec_not_ok():
@@ -414,7 +416,7 @@ def service_labels() -> dict[str, str]:
414416
"inputs_path": "/tmp/inputs", # noqa: S108
415417
"outputs_path": "/tmp/outputs", # noqa: S108
416418
"state_paths": ["/tmp/save_1", "/tmp_save_2"], # noqa: S108
417-
"state_exclude": ["/tmp/strip_me/*", "*.py"], # noqa: S108
419+
"state_exclude": ["/tmp/strip_me/*"], # noqa: S108
418420
}
419421
),
420422
"simcore.service.compose-spec": json.dumps(

services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/scheduler/_core/_event_create_sidecars.py

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,34 @@
5858

5959
_DYNAMIC_SIDECAR_SERVICE_EXTENDABLE_SPECS: Final[tuple[list[str], ...]] = (
6060
["labels"],
61-
["task_template", "Resources", "Limits"],
62-
["task_template", "Resources", "Reservation", "MemoryBytes"],
63-
["task_template", "Resources", "Reservation", "NanoCPUs"],
64-
["task_template", "Placement", "Constraints"],
65-
["task_template", "ContainerSpec", "Env"],
66-
["task_template", "Resources", "Reservation", "GenericResources"],
61+
["task_template", "container_spec", "env"],
62+
["task_template", "placement", "constraints"],
63+
["task_template", "resources", "reservation", "generic_resources"],
64+
["task_template", "resources", "limits"],
65+
["task_template", "resources", "reservation", "memory_bytes"],
66+
["task_template", "resources", "reservation", "nano_cp_us"],
6767
)
6868

6969

70+
def _merge_service_base_and_user_specs(
71+
dynamic_sidecar_service_spec_base: AioDockerServiceSpec,
72+
user_specific_service_spec: AioDockerServiceSpec,
73+
) -> AioDockerServiceSpec:
74+
# NOTE: since user_specific_service_spec follows Docker Service Spec and not Aio
75+
# we do not use aliases when exporting dynamic_sidecar_service_spec_base
76+
return AioDockerServiceSpec.model_validate(
77+
nested_update(
78+
jsonable_encoder(
79+
dynamic_sidecar_service_spec_base, exclude_unset=True, by_alias=False
80+
),
81+
jsonable_encoder(
82+
user_specific_service_spec, exclude_unset=True, by_alias=False
83+
),
84+
include=_DYNAMIC_SIDECAR_SERVICE_EXTENDABLE_SPECS,
85+
)
86+
)
87+
88+
7089
async def _create_proxy_service(
7190
app,
7291
*,
@@ -245,14 +264,8 @@ async def action(cls, app: FastAPI, scheduler_data: SchedulerData) -> None:
245264
user_specific_service_spec = AioDockerServiceSpec.model_validate(
246265
user_specific_service_spec
247266
)
248-
# NOTE: since user_specific_service_spec follows Docker Service Spec and not Aio
249-
# we do not use aliases when exporting dynamic_sidecar_service_spec_base
250-
dynamic_sidecar_service_final_spec = AioDockerServiceSpec.model_validate(
251-
nested_update(
252-
jsonable_encoder(dynamic_sidecar_service_spec_base, exclude_unset=True),
253-
jsonable_encoder(user_specific_service_spec, exclude_unset=True),
254-
include=_DYNAMIC_SIDECAR_SERVICE_EXTENDABLE_SPECS,
255-
)
267+
dynamic_sidecar_service_final_spec = _merge_service_base_and_user_specs(
268+
dynamic_sidecar_service_spec_base, user_specific_service_spec
256269
)
257270
rabbit_message = ProgressRabbitMessageNode.model_construct(
258271
user_id=scheduler_data.user_id,

services/director-v2/tests/unit/with_dbs/test_modules_dynamic_sidecar_docker_service_specs.py

Lines changed: 67 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@
4343
from simcore_service_director_v2.modules.dynamic_sidecar.docker_service_specs import (
4444
get_dynamic_sidecar_spec,
4545
)
46+
from simcore_service_director_v2.modules.dynamic_sidecar.scheduler._core._event_create_sidecars import (
47+
_DYNAMIC_SIDECAR_SERVICE_EXTENDABLE_SPECS,
48+
_merge_service_base_and_user_specs,
49+
)
4650
from simcore_service_director_v2.utils.dict_utils import nested_update
4751

4852

@@ -180,7 +184,7 @@ def expected_dynamic_sidecar_spec(
180184
"paths_mapping": {
181185
"inputs_path": "/tmp/inputs", # noqa: S108
182186
"outputs_path": "/tmp/outputs", # noqa: S108
183-
"state_exclude": ["/tmp/strip_me/*", "*.py"], # noqa: S108
187+
"state_exclude": ["/tmp/strip_me/*"], # noqa: S108
184188
"state_paths": ["/tmp/save_1", "/tmp_save_2"], # noqa: S108
185189
},
186190
"callbacks_mapping": CallbacksMapping.model_config[
@@ -239,7 +243,7 @@ def expected_dynamic_sidecar_spec(
239243
"DY_SIDECAR_PATH_OUTPUTS": "/tmp/outputs", # noqa: S108
240244
"DY_SIDECAR_PROJECT_ID": "dd1d04d9-d704-4f7e-8f0f-1ca60cc771fe",
241245
"DY_SIDECAR_STATE_EXCLUDE": json_dumps(
242-
["*.py", "/tmp/strip_me/*"] # noqa: S108
246+
["/tmp/strip_me/*"] # noqa: S108
243247
),
244248
"DY_SIDECAR_STATE_PATHS": json_dumps(
245249
["/tmp/save_1", "/tmp_save_2"] # noqa: S108
@@ -614,14 +618,66 @@ async def test_merge_dynamic_sidecar_specs_with_user_specific_specs(
614618
another_merged_dict = nested_update(
615619
orig_dict,
616620
user_dict,
617-
include=(
618-
["labels"],
619-
["task_template", "Resources", "Limits"],
620-
["task_template", "Resources", "Reservation", "MemoryBytes"],
621-
["task_template", "Resources", "Reservation", "NanoCPUs"],
622-
["task_template", "Placement", "Constraints"],
623-
["task_template", "ContainerSpec", "Env"],
624-
["task_template", "Resources", "Reservation", "GenericResources"],
625-
),
621+
include=_DYNAMIC_SIDECAR_SERVICE_EXTENDABLE_SPECS,
626622
)
627623
assert another_merged_dict
624+
625+
626+
def test_regression__merge_service_base_and_user_specs():
627+
mock_service_spec = AioDockerServiceSpec.model_validate(
628+
{"Labels": {"l1": "false", "l0": "a"}}
629+
)
630+
mock_catalog_constraints = AioDockerServiceSpec.model_validate(
631+
{
632+
"Labels": {"l1": "true", "l2": "a"},
633+
"TaskTemplate": {
634+
"Placement": {
635+
"Constraints": [
636+
"c1==true",
637+
"c2==true",
638+
],
639+
},
640+
"Resources": {
641+
"Limits": {"MemoryBytes": 1, "NanoCPUs": 1},
642+
"Reservations": {
643+
"GenericResources": [
644+
{"DiscreteResourceSpec": {"Kind": "VRAM", "Value": 1}}
645+
],
646+
"MemoryBytes": 2,
647+
"NanoCPUs": 2,
648+
},
649+
},
650+
"ContainerSpec": {
651+
"Env": [
652+
"key-1=value-1",
653+
"key2-value2=a",
654+
]
655+
},
656+
},
657+
}
658+
)
659+
result = _merge_service_base_and_user_specs(
660+
mock_service_spec, mock_catalog_constraints
661+
)
662+
assert result.model_dump(by_alias=True, exclude_unset=True) == {
663+
"Labels": {"l1": "true", "l2": "a", "l0": "a"},
664+
"TaskTemplate": {
665+
"Placement": {
666+
"Constraints": [
667+
"c1==true",
668+
"c2==true",
669+
],
670+
},
671+
"Resources": {
672+
"Limits": {"MemoryBytes": 1, "NanoCPUs": 1},
673+
"Reservations": {
674+
"GenericResources": [
675+
{"DiscreteResourceSpec": {"Kind": "VRAM", "Value": 1}}
676+
],
677+
"MemoryBytes": 2,
678+
"NanoCPUs": 2,
679+
},
680+
},
681+
"ContainerSpec": {"Env": {"key-1": "value-1", "key2-value2": "a"}},
682+
},
683+
}

services/static-webserver/client/source/class/osparc/dashboard/StudyBrowser.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -631,16 +631,18 @@ qx.Class.define("osparc.dashboard.StudyBrowser", {
631631
// check the entries in currentParams are the same as the reqParams
632632
let sameContext = true;
633633
Object.entries(currentParams).forEach(([key, value]) => {
634-
sameContext &= key in reqParams && reqParams[key] === value;
634+
// loose equality: will do a Number to String conversion if necessary
635+
sameContext &= key in reqParams && reqParams[key] == value;
635636
});
636637
return !sameContext;
637638
},
638639

639640
__getNextPageParams: function() {
640-
if (this._resourcesContainer.getFlatList() && this._resourcesContainer.getFlatList().nextRequest) {
641+
const studiesContainer = this._resourcesContainer.getFlatList();
642+
if (studiesContainer && studiesContainer.nextRequest) {
641643
// Context might have been changed while waiting for the response.
642644
// The new call is on the way, therefore this response can be ignored.
643-
const url = new URL(this._resourcesContainer.getFlatList().nextRequest);
645+
const url = new URL(studiesContainer.nextRequest);
644646
const urlSearchParams = new URLSearchParams(url.search);
645647
const urlParams = {};
646648
for (const [snakeKey, value] of urlSearchParams.entries()) {
@@ -650,12 +652,12 @@ qx.Class.define("osparc.dashboard.StudyBrowser", {
650652
const contextChanged = this.__didContextChange(urlParams);
651653
if (
652654
!contextChanged &&
653-
osparc.utils.Utils.hasParamFromURL(this._resourcesContainer.getFlatList().nextRequest, "offset") &&
654-
osparc.utils.Utils.hasParamFromURL(this._resourcesContainer.getFlatList().nextRequest, "limit")
655+
osparc.utils.Utils.hasParamFromURL(studiesContainer.nextRequest, "offset") &&
656+
osparc.utils.Utils.hasParamFromURL(studiesContainer.nextRequest, "limit")
655657
) {
656658
return {
657-
offset: osparc.utils.Utils.getParamFromURL(this._resourcesContainer.getFlatList().nextRequest, "offset"),
658-
limit: osparc.utils.Utils.getParamFromURL(this._resourcesContainer.getFlatList().nextRequest, "limit")
659+
offset: osparc.utils.Utils.getParamFromURL(studiesContainer.nextRequest, "offset"),
660+
limit: osparc.utils.Utils.getParamFromURL(studiesContainer.nextRequest, "limit")
659661
};
660662
}
661663
}

services/static-webserver/client/source/class/osparc/data/model/Workbench.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -797,7 +797,10 @@ qx.Class.define("osparc.data.model.Workbench", {
797797
} else {
798798
// patch only what was changed
799799
Object.keys(workbenchDiffs[nodeId]).forEach(changedFieldKey => {
800-
patchData[changedFieldKey] = nodeData[changedFieldKey];
800+
if (nodeData[changedFieldKey] !== undefined) {
801+
// do not patch if it's undefined
802+
patchData[changedFieldKey] = nodeData[changedFieldKey];
803+
}
801804
});
802805
}
803806
const params = {
@@ -807,7 +810,9 @@ qx.Class.define("osparc.data.model.Workbench", {
807810
},
808811
data: patchData
809812
};
810-
promises.push(osparc.data.Resources.fetch("studies", "patchNode", params));
813+
if (Object.keys(patchData).length) {
814+
promises.push(osparc.data.Resources.fetch("studies", "patchNode", params));
815+
}
811816
})
812817
return Promise.all(promises);
813818
}

0 commit comments

Comments
 (0)