Skip to content

Commit b85b814

Browse files
authored
node locking (#8170)
1 parent 133458b commit b85b814

File tree

49 files changed

+1036
-456
lines changed

Some content is hidden

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

49 files changed

+1036
-456
lines changed

Makefile

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,31 @@ export DOCKER_REGISTRY ?= itisfoundation
8989

9090
MAKEFILES_WITH_OPENAPI_SPECS := $(shell find . -mindepth 2 -type f -name 'Makefile' -not -path '*/.*' -exec grep -l '^openapi-specs:' {} \; | xargs realpath)
9191

92+
# WSL 2 tricks
93+
define _check_wsl_mirroring
94+
$(shell \
95+
if [ "$(IS_WSL2)" = "WSL2" ]; then \
96+
win_user=$$(powershell.exe '$$env:UserName' | tr -d '\r' | tail -n 1 | xargs); \
97+
config_path="/mnt/c/Users/$$win_user/.wslconfig"; \
98+
if [ -f "$$config_path" ] && grep -q "networkingMode.*=.*mirrored" "$$config_path" 2>/dev/null; then \
99+
echo "true"; \
100+
else \
101+
echo "false"; \
102+
fi; \
103+
else \
104+
echo "false"; \
105+
fi \
106+
)
107+
endef
108+
109+
WSL_MIRRORED := $(_check_wsl_mirroring)
110+
111+
112+
ifeq ($(WSL_MIRRORED),true)
113+
get_my_ip := 127.0.0.1
114+
else
92115
get_my_ip := $(shell (hostname --all-ip-addresses || hostname -i) 2>/dev/null | cut --delimiter=" " --fields=1)
116+
endif
93117

94118
# NOTE: this is only for WSL2 as the WSL2 subsystem IP is changing on each reboot
95119
ifeq ($(IS_WSL2),WSL2)

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

Lines changed: 38 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from pathlib import Path
33

44
from pydantic import BaseModel, ConfigDict, Field
5+
from pydantic.config import JsonDict
56

67
from ..basic_types import PortInt
78
from ..projects import ProjectID
@@ -88,40 +89,45 @@ class RunningDynamicServiceDetails(ServiceDetails):
8889
alias="service_message",
8990
)
9091

92+
@staticmethod
93+
def _update_json_schema_extra(schema: JsonDict) -> None:
94+
schema.update(
95+
{
96+
"examples": [ # legacy
97+
{
98+
"service_key": "simcore/services/dynamic/raw-graphs",
99+
"service_version": "2.10.6",
100+
"user_id": 1,
101+
"project_id": "32fb4eb6-ab30-11ef-9ee4-0242ac140008",
102+
"service_uuid": "0cd049ba-cd6b-4a12-b416-a50c9bc8e7bb",
103+
"service_basepath": "/x/0cd049ba-cd6b-4a12-b416-a50c9bc8e7bb",
104+
"service_host": "raw-graphs_0cd049ba-cd6b-4a12-b416-a50c9bc8e7bb",
105+
"service_port": 4000,
106+
"published_port": None,
107+
"entry_point": "",
108+
"service_state": "running",
109+
"service_message": "",
110+
},
111+
# new style
112+
{
113+
"service_key": "simcore/services/dynamic/jupyter-math",
114+
"service_version": "3.0.3",
115+
"user_id": 1,
116+
"project_id": "32fb4eb6-ab30-11ef-9ee4-0242ac140008",
117+
"service_uuid": "6e3cad3a-eb64-43de-b476-9ac3c413fd9c",
118+
"boot_type": "V2",
119+
"service_host": "dy-sidecar_6e3cad3a-eb64-43de-b476-9ac3c413fd9c",
120+
"service_port": 8888,
121+
"service_state": "running",
122+
"service_message": "",
123+
},
124+
]
125+
}
126+
)
127+
91128
model_config = ConfigDict(
92129
ignored_types=(cached_property,),
93-
json_schema_extra={
94-
"examples": [
95-
# legacy
96-
{
97-
"service_key": "simcore/services/dynamic/raw-graphs",
98-
"service_version": "2.10.6",
99-
"user_id": 1,
100-
"project_id": "32fb4eb6-ab30-11ef-9ee4-0242ac140008",
101-
"service_uuid": "0cd049ba-cd6b-4a12-b416-a50c9bc8e7bb",
102-
"service_basepath": "/x/0cd049ba-cd6b-4a12-b416-a50c9bc8e7bb",
103-
"service_host": "raw-graphs_0cd049ba-cd6b-4a12-b416-a50c9bc8e7bb",
104-
"service_port": 4000,
105-
"published_port": None,
106-
"entry_point": "",
107-
"service_state": "running",
108-
"service_message": "",
109-
},
110-
# new style
111-
{
112-
"service_key": "simcore/services/dynamic/jupyter-math",
113-
"service_version": "3.0.3",
114-
"user_id": 1,
115-
"project_id": "32fb4eb6-ab30-11ef-9ee4-0242ac140008",
116-
"service_uuid": "6e3cad3a-eb64-43de-b476-9ac3c413fd9c",
117-
"boot_type": "V2",
118-
"service_host": "dy-sidecar_6e3cad3a-eb64-43de-b476-9ac3c413fd9c",
119-
"service_port": 8888,
120-
"service_state": "running",
121-
"service_message": "",
122-
},
123-
]
124-
},
130+
json_schema_extra=_update_json_schema_extra,
125131
)
126132

127133
@cached_property

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

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from typing import Annotated, Any, Literal, TypeAlias
33

44
from pydantic import ConfigDict, Field
5+
from pydantic.config import JsonDict
56

67
from ..access_rights import ExecutableAccessRights
78
from ..api_schemas_directorv2.dynamic_services import RetrieveDataOut
@@ -163,14 +164,20 @@ class NodeGetIdle(OutputSchema):
163164
def from_node_id(cls, node_id: NodeID) -> "NodeGetIdle":
164165
return cls(service_state="idle", service_uuid=node_id)
165166

166-
model_config = ConfigDict(
167-
json_schema_extra={
168-
"example": {
169-
"service_uuid": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
170-
"service_state": "idle",
167+
@staticmethod
168+
def _update_json_schema_extra(schema: JsonDict) -> None:
169+
schema.update(
170+
{
171+
"examples": [
172+
{
173+
"service_uuid": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
174+
"service_state": "idle",
175+
}
176+
]
171177
}
172-
}
173-
)
178+
)
179+
180+
model_config = ConfigDict(json_schema_extra=_update_json_schema_extra)
174181

175182

176183
class NodeGetUnknown(OutputSchema):

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

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
Models Node as a central element in a project's pipeline
33
"""
44

5-
from typing import Annotated, Any, TypeAlias, Union
5+
from enum import auto
6+
from typing import Annotated, Any, Self, TypeAlias, Union
67

78
from common_library.basic_types import DEFAULT_FACTORY
89
from pydantic import (
@@ -16,10 +17,12 @@
1617
StrictInt,
1718
StringConstraints,
1819
field_validator,
20+
model_validator,
1921
)
2022
from pydantic.config import JsonDict
2123

2224
from .basic_types import EnvVarKey, KeyIDStr
25+
from .groups import GroupID
2326
from .projects_access import AccessEnum
2427
from .projects_nodes_io import (
2528
DatCoreFileLink,
@@ -31,8 +34,9 @@
3134
from .projects_nodes_layout import Position
3235
from .projects_state import RunningState
3336
from .services import ServiceKey, ServiceVersion
37+
from .utils.enums import StrAutoEnum
3438

35-
InputTypes = Union[
39+
InputTypes = Union[ # noqa: UP007
3640
# NOTE: WARNING the order in Union[*] below matters!
3741
StrictBool,
3842
StrictInt,
@@ -44,7 +48,7 @@
4448
DownloadLink,
4549
list[Any] | dict[str, Any], # arrays | object
4650
]
47-
OutputTypes = Union[
51+
OutputTypes = Union[ # noqa: UP007
4852
# NOTE: WARNING the order in Union[*] below matters!
4953
StrictBool,
5054
StrictInt,
@@ -71,6 +75,69 @@
7175
UnitStr: TypeAlias = Annotated[str, StringConstraints(strip_whitespace=True)]
7276

7377

78+
class NodeShareStatus(StrAutoEnum):
79+
OPENING = auto()
80+
OPENED = auto()
81+
CLOSING = auto()
82+
83+
84+
class NodeShareState(BaseModel):
85+
locked: Annotated[
86+
bool,
87+
Field(
88+
description="True if the node is locked, False otherwise",
89+
),
90+
]
91+
92+
current_user_groupids: Annotated[
93+
list[GroupID] | None,
94+
Field(
95+
description="Group(s) that currently have access to the node (or locked it)"
96+
),
97+
] = None
98+
99+
status: Annotated[
100+
NodeShareStatus | None,
101+
Field(
102+
description="Reason why the node is locked, None if not locked",
103+
),
104+
] = None
105+
106+
@model_validator(mode="after")
107+
def _validate_lock_state(self) -> Self:
108+
if self.locked and (self.current_user_groupids is None or self.status is None):
109+
msg = "If the node is locked, both 'current_user_groupids' and 'status' must be set"
110+
raise ValueError(msg)
111+
112+
return self
113+
114+
@staticmethod
115+
def _update_json_schema_extra(schema: JsonDict) -> None:
116+
schema.update(
117+
{
118+
"examples": [
119+
{
120+
"locked": False,
121+
},
122+
{
123+
"locked": True,
124+
"current_user_groupids": [666],
125+
"status": "OPENING",
126+
},
127+
{
128+
"locked": False,
129+
"current_user_groupids": [666, 4563],
130+
"status": "OPENED",
131+
},
132+
]
133+
}
134+
)
135+
136+
model_config = ConfigDict(
137+
extra="forbid", json_schema_extra=_update_json_schema_extra
138+
)
139+
140+
74141
class NodeState(BaseModel):
75142
modified: Annotated[
76143
bool,
@@ -104,6 +171,10 @@ class NodeState(BaseModel):
104171
),
105172
] = 0
106173

174+
lock_state: Annotated[
175+
NodeShareState | None, Field(description="the node's lock state")
176+
] = None
177+
107178
model_config = ConfigDict(
108179
extra="forbid",
109180
populate_by_name=True,
@@ -192,7 +263,7 @@ class Node(BaseModel):
192263
] = DEFAULT_FACTORY
193264

194265
inputs_required: Annotated[
195-
list[InputID],
266+
list[InputID] | None,
196267
Field(
197268
default_factory=list,
198269
description="Defines inputs that are required in order to run the service",
@@ -286,7 +357,6 @@ def _convert_empty_str_to_none(cls, v):
286357
@classmethod
287358
def _convert_from_enum(cls, v):
288359
if isinstance(v, str):
289-
290360
# the old version of state was a enum of RunningState
291361
running_state_value = _convert_old_enum_name(v)
292362
return NodeState(current_status=running_state_value)

packages/models-library/src/models_library/utils/enums.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
@unique
77
class StrAutoEnum(StrEnum):
88
@staticmethod
9-
def _generate_next_value_(name, start, count, last_values):
9+
def _generate_next_value_(
10+
name: str, start: int, count: int, last_values: list[str] # noqa: ARG004
11+
) -> str:
1012
return name.upper()
1113

1214

packages/models-library/tests/test_project_nodes.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def test_create_minimal_node(minimal_node_data_sample: dict[str, Any]):
2929
assert node.state.current_status == RunningState.NOT_STARTED
3030
assert node.state.modified is True
3131
assert node.state.dependencies == set()
32+
assert node.state.lock_state is None
3233

3334
assert node.parent is None
3435
assert node.progress is None
@@ -37,7 +38,7 @@ def test_create_minimal_node(minimal_node_data_sample: dict[str, Any]):
3738

3839

3940
def test_create_minimal_node_with_new_data_type(
40-
minimal_node_data_sample: dict[str, Any]
41+
minimal_node_data_sample: dict[str, Any],
4142
):
4243
old_node_data = minimal_node_data_sample
4344
# found some old data with this aspect

packages/pytest-simcore/src/pytest_simcore/db_entries_mocks.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ def create_registered_user(
4141
with contextlib.ExitStack() as stack:
4242

4343
def _(**user_kwargs) -> dict[str, Any]:
44-
4544
user_id = len(created_user_ids) + 1
4645
user = stack.enter_context(
4746
sync_insert_and_get_user_and_secrets_lifespan(
@@ -122,8 +121,11 @@ async def _(
122121
await project_nodes_repo.add(
123122
con,
124123
nodes=[
125-
ProjectNodeCreate(node_id=NodeID(node_id), **default_node_config)
126-
for node_id in inserted_project.workbench
124+
ProjectNodeCreate(
125+
node_id=NodeID(node_id),
126+
**(default_node_config | node_data.model_dump(mode="json")),
127+
)
128+
for node_id, node_data in inserted_project.workbench.items()
127129
],
128130
)
129131
await con.execute(

0 commit comments

Comments
 (0)