Skip to content

Commit be73c21

Browse files
Merge branch 'master' into maintenance/improve-error-handling
2 parents 5d10bf5 + 890f1ae commit be73c21

File tree

44 files changed

+1180
-357
lines changed

Some content is hidden

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

44 files changed

+1180
-357
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from enum import auto
2+
3+
from models_library.projects import ProjectID
4+
from models_library.projects_nodes_io import NodeID
5+
from models_library.services_types import ServicePortKey
6+
from models_library.utils.enums import StrAutoEnum
7+
from pydantic import BaseModel
8+
9+
10+
class OutputStatus(StrAutoEnum):
11+
UPLOAD_STARTED = auto()
12+
UPLOAD_WAS_ABORTED = auto()
13+
UPLOAD_FINISHED_SUCCESSFULLY = auto()
14+
UPLOAD_FINISHED_WITH_ERRROR = auto()
15+
16+
17+
class InputStatus(StrAutoEnum):
18+
DOWNLOAD_STARTED = auto()
19+
DOWNLOAD_WAS_ABORTED = auto()
20+
DOWNLOAD_FINISHED_SUCCESSFULLY = auto()
21+
DOWNLOAD_FINISHED_WITH_ERRROR = auto()
22+
23+
24+
class _PortStatusCommon(BaseModel):
25+
project_id: ProjectID
26+
node_id: NodeID
27+
port_key: ServicePortKey
28+
29+
30+
class OutputPortStatus(_PortStatusCommon):
31+
status: OutputStatus
32+
33+
34+
class InputPortSatus(_PortStatusCommon):
35+
status: InputStatus
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
from typing import Final
22

33
SOCKET_IO_SERVICE_DISK_USAGE_EVENT: Final[str] = "serviceDiskUsage"
4+
SOCKET_IO_STATE_OUTPUT_PORTS_EVENT: Final[str] = "stateOutputPorts"
5+
SOCKET_IO_STATE_INPUT_PORTS_EVENT: Final[str] = "stateInputPorts"
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from pydantic import SecretStr
2+
3+
4+
def _mask(value):
5+
"""
6+
Mask the password, showing only the first and last characters
7+
or *** if very short passwords
8+
"""
9+
if len(value) > 2:
10+
masked_value = value[0] + "*" * (len(value) - 2) + value[-1]
11+
else:
12+
# In case of very short passwords
13+
masked_value = "*" * len(value)
14+
return masked_value
15+
16+
17+
def _hash(value):
18+
"""Uses hash number to mask the password"""
19+
return f"hash:{hash(value)}"
20+
21+
22+
class Secret4TestsStr(SecretStr):
23+
"""Prints a hint of the secret
24+
TIP: Can be handy for testing
25+
"""
26+
27+
def _display(self) -> str | bytes:
28+
# SEE overrides _SecretBase._display
29+
value = self.get_secret_value()
30+
return _mask(value) if value else ""
31+
32+
33+
assert str(Secret4TestsStr("123456890")) == "1*******0"
34+
assert "1*******0" in repr(Secret4TestsStr("123456890"))

packages/service-integration/requirements/_base.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ jsonschema # pytest-plugin
1313
pytest # pytest-plugin
1414
pyyaml
1515
typer[all]
16+
yarl

packages/service-integration/requirements/_base.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ idna==3.7
3535
# via
3636
# email-validator
3737
# requests
38+
# yarl
3839
iniconfig==2.0.0
3940
# via pytest
4041
jinja2==3.1.4
@@ -57,6 +58,8 @@ markupsafe==2.1.5
5758
# via jinja2
5859
mdurl==0.1.2
5960
# via markdown-it-py
61+
multidict==6.1.0
62+
# via yarl
6063
orjson==3.10.7
6164
# via
6265
# -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
@@ -121,3 +124,5 @@ urllib3==2.2.2
121124
# -c requirements/../../../requirements/constraints.txt
122125
# docker
123126
# requests
127+
yarl==1.12.1
128+
# via -r requirements/_base.in

packages/service-integration/src/service_integration/cli/_compose_spec.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import yaml
99
from models_library.utils.labels_annotations import to_labels
1010
from rich.console import Console
11+
from yarl import URL
1112

1213
from ..compose_spec_model import ComposeSpecification
1314
from ..errors import UndefinedOciImageSpecError
@@ -34,6 +35,13 @@ def _run_git(*args) -> str:
3435
).stdout.strip()
3536

3637

38+
def _strip_credentials(url: str) -> str:
39+
if (yarl_url := URL(url)) and yarl_url.is_absolute():
40+
stripped_url = URL(url).with_user(None).with_password(None)
41+
return f"{stripped_url}"
42+
return url
43+
44+
3745
def _run_git_or_empty_string(*args) -> str:
3846
try:
3947
return _run_git(*args)
@@ -118,8 +126,8 @@ def create_docker_compose_image_spec(
118126
extra_labels[f"{LS_LABEL_PREFIX}.vcs-ref"] = _run_git_or_empty_string(
119127
"rev-parse", "HEAD"
120128
)
121-
extra_labels[f"{LS_LABEL_PREFIX}.vcs-url"] = _run_git_or_empty_string(
122-
"config", "--get", "remote.origin.url"
129+
extra_labels[f"{LS_LABEL_PREFIX}.vcs-url"] = _strip_credentials(
130+
_run_git_or_empty_string("config", "--get", "remote.origin.url")
123131
)
124132

125133
return create_image_spec(
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import pytest
2+
from service_integration.cli._compose_spec import _strip_credentials
3+
4+
5+
@pytest.mark.parametrize(
6+
"url, expected_url",
7+
[
8+
(
9+
"schema.veshttps://user:[email protected]/some/repo.git",
10+
"schema.veshttps://example.com/some/repo.git",
11+
),
12+
(
13+
"https://user:[email protected]/some/repo.git",
14+
"https://example.com/some/repo.git",
15+
),
16+
(
17+
"ssh://user:[email protected]/some/repo.git",
18+
"ssh://example.com/some/repo.git",
19+
),
20+
(
21+
"[email protected]:some/repo.git",
22+
"[email protected]:some/repo.git",
23+
),
24+
("any_str", "any_str"),
25+
],
26+
)
27+
def test__strip_credentials(url: str, expected_url: str):
28+
assert _strip_credentials(url) == expected_url

packages/simcore-sdk/src/simcore_sdk/node_ports_v2/nodeports_v2.py

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import logging
2+
from abc import ABC, abstractmethod
3+
from asyncio import CancelledError
24
from collections.abc import Callable, Coroutine
35
from pathlib import Path
46
from typing import Any
@@ -27,6 +29,20 @@
2729
log = logging.getLogger(__name__)
2830

2931

32+
class OutputsCallbacks(ABC):
33+
@abstractmethod
34+
async def aborted(self, key: ServicePortKey) -> None:
35+
pass
36+
37+
@abstractmethod
38+
async def finished_succesfully(self, key: ServicePortKey) -> None:
39+
pass
40+
41+
@abstractmethod
42+
async def finished_with_error(self, key: ServicePortKey) -> None:
43+
pass
44+
45+
3046
class Nodeports(BaseModel):
3147
"""
3248
Represents a node in a project and all its input/output ports
@@ -148,6 +164,7 @@ async def set_multiple(
148164
],
149165
*,
150166
progress_bar: ProgressBarData,
167+
outputs_callbacks: OutputsCallbacks | None,
151168
) -> None:
152169
"""
153170
Sets the provided values to the respective input or output ports
@@ -156,26 +173,44 @@ async def set_multiple(
156173
157174
raises ValidationError
158175
"""
176+
177+
async def _set_with_notifications(
178+
port_key: ServicePortKey,
179+
value: ItemConcreteValue | None,
180+
set_kwargs: SetKWargs | None,
181+
sub_progress: ProgressBarData,
182+
) -> None:
183+
try:
184+
# pylint: disable=protected-access
185+
await self.internal_outputs[port_key]._set( # noqa: SLF001
186+
value, set_kwargs=set_kwargs, progress_bar=sub_progress
187+
)
188+
if outputs_callbacks:
189+
await outputs_callbacks.finished_succesfully(port_key)
190+
except UnboundPortError:
191+
# not available try inputs
192+
# if this fails it will raise another exception
193+
# pylint: disable=protected-access
194+
await self.internal_inputs[port_key]._set( # noqa: SLF001
195+
value, set_kwargs=set_kwargs, progress_bar=sub_progress
196+
)
197+
except CancelledError:
198+
if outputs_callbacks:
199+
await outputs_callbacks.aborted(port_key)
200+
raise
201+
except Exception:
202+
if outputs_callbacks:
203+
await outputs_callbacks.finished_with_error(port_key)
204+
raise
205+
159206
tasks = []
160207
async with progress_bar.sub_progress(
161208
steps=len(port_values.items()), description=IDStr("set multiple")
162209
) as sub_progress:
163210
for port_key, (value, set_kwargs) in port_values.items():
164-
# pylint: disable=protected-access
165-
try:
166-
tasks.append(
167-
self.internal_outputs[port_key]._set(
168-
value, set_kwargs=set_kwargs, progress_bar=sub_progress
169-
)
170-
)
171-
except UnboundPortError:
172-
# not available try inputs
173-
# if this fails it will raise another exception
174-
tasks.append(
175-
self.internal_inputs[port_key]._set(
176-
value, set_kwargs=set_kwargs, progress_bar=sub_progress
177-
)
178-
)
211+
tasks.append(
212+
_set_with_notifications(port_key, value, set_kwargs, sub_progress)
213+
)
179214

180215
results = await logged_gather(*tasks)
181216
await self.save_to_db_cb(self)

packages/simcore-sdk/tests/integration/test_node_ports_v2_nodeports2.py

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from collections.abc import Awaitable, Callable, Iterable
1414
from pathlib import Path
1515
from typing import Any
16+
from unittest.mock import AsyncMock
1617
from uuid import uuid4
1718

1819
import np_helpers
@@ -28,13 +29,14 @@
2829
SimcoreS3FileID,
2930
)
3031
from models_library.services_types import ServicePortKey
32+
from pytest_mock import MockerFixture
3133
from servicelib.progress_bar import ProgressBarData
3234
from settings_library.r_clone import RCloneSettings
3335
from simcore_sdk import node_ports_v2
3436
from simcore_sdk.node_ports_common.exceptions import UnboundPortError
3537
from simcore_sdk.node_ports_v2 import exceptions
3638
from simcore_sdk.node_ports_v2.links import ItemConcreteValue, PortLink
37-
from simcore_sdk.node_ports_v2.nodeports_v2 import Nodeports
39+
from simcore_sdk.node_ports_v2.nodeports_v2 import Nodeports, OutputsCallbacks
3840
from simcore_sdk.node_ports_v2.port import Port
3941
from utils_port_v2 import CONSTANT_UUID
4042

@@ -749,6 +751,34 @@ async def _upload_create_task(item_key: str) -> None:
749751
)
750752

751753

754+
class _Callbacks(OutputsCallbacks):
755+
async def aborted(self, key: ServicePortKey) -> None:
756+
pass
757+
758+
async def finished_succesfully(self, key: ServicePortKey) -> None:
759+
pass
760+
761+
async def finished_with_error(self, key: ServicePortKey) -> None:
762+
pass
763+
764+
765+
@pytest.fixture
766+
async def output_callbacks() -> _Callbacks:
767+
return _Callbacks()
768+
769+
770+
@pytest.fixture
771+
async def spy_outputs_callbaks(
772+
mocker: MockerFixture, output_callbacks: _Callbacks
773+
) -> dict[str, AsyncMock]:
774+
return {
775+
"aborted": mocker.spy(output_callbacks, "aborted"),
776+
"finished_succesfully": mocker.spy(output_callbacks, "finished_succesfully"),
777+
"finished_with_error": mocker.spy(output_callbacks, "finished_with_error"),
778+
}
779+
780+
781+
@pytest.mark.parametrize("use_output_callbacks", [True, False])
752782
async def test_batch_update_inputs_outputs(
753783
user_id: int,
754784
project_id: str,
@@ -757,7 +787,12 @@ async def test_batch_update_inputs_outputs(
757787
port_count: int,
758788
option_r_clone_settings: RCloneSettings | None,
759789
faker: Faker,
790+
output_callbacks: _Callbacks,
791+
spy_outputs_callbaks: dict[str, AsyncMock],
792+
use_output_callbacks: bool,
760793
) -> None:
794+
callbacks = output_callbacks if use_output_callbacks else None
795+
761796
outputs = [(f"value_out_{i}", "integer", None) for i in range(port_count)]
762797
inputs = [(f"value_in_{i}", "integer", None) for i in range(port_count)]
763798
config_dict, _, _ = create_special_configuration(inputs=inputs, outputs=outputs)
@@ -771,12 +806,14 @@ async def test_batch_update_inputs_outputs(
771806
await check_config_valid(PORTS, config_dict)
772807

773808
async with ProgressBarData(num_steps=2, description=faker.pystr()) as progress_bar:
809+
port_values = (await PORTS.outputs).values()
774810
await PORTS.set_multiple(
775-
{
776-
ServicePortKey(port.key): (k, None)
777-
for k, port in enumerate((await PORTS.outputs).values())
778-
},
811+
{ServicePortKey(port.key): (k, None) for k, port in enumerate(port_values)},
779812
progress_bar=progress_bar,
813+
outputs_callbacks=callbacks,
814+
)
815+
assert len(spy_outputs_callbaks["finished_succesfully"].call_args_list) == (
816+
len(port_values) if use_output_callbacks else 0
780817
)
781818
# pylint: disable=protected-access
782819
assert progress_bar._current_steps == pytest.approx(1) # noqa: SLF001
@@ -786,6 +823,11 @@ async def test_batch_update_inputs_outputs(
786823
for k, port in enumerate((await PORTS.inputs).values(), start=1000)
787824
},
788825
progress_bar=progress_bar,
826+
outputs_callbacks=callbacks,
827+
)
828+
# inputs do not trigger callbacks
829+
assert len(spy_outputs_callbaks["finished_succesfully"].call_args_list) == (
830+
len(port_values) if use_output_callbacks else 0
789831
)
790832
assert progress_bar._current_steps == pytest.approx(2) # noqa: SLF001
791833

@@ -807,4 +849,11 @@ async def test_batch_update_inputs_outputs(
807849
await PORTS.set_multiple(
808850
{ServicePortKey("missing_key_in_both"): (123132, None)},
809851
progress_bar=progress_bar,
852+
outputs_callbacks=callbacks,
810853
)
854+
855+
assert len(spy_outputs_callbaks["finished_succesfully"].call_args_list) == (
856+
len(port_values) if use_output_callbacks else 0
857+
)
858+
assert len(spy_outputs_callbaks["aborted"].call_args_list) == 0
859+
assert len(spy_outputs_callbaks["finished_with_error"].call_args_list) == 0

packages/simcore-sdk/tests/unit/test_node_ports_v2_nodeports_v2.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from pathlib import Path
77
from typing import Any, Callable
8+
from unittest.mock import AsyncMock
89

910
import pytest
1011
from faker import Faker
@@ -138,6 +139,7 @@ async def mock_node_port_creator_cb(*args, **kwargs):
138139
+ list(original_outputs.values())
139140
},
140141
progress_bar=progress_bar,
142+
outputs_callbacks=AsyncMock(),
141143
)
142144
assert progress_bar._current_steps == pytest.approx(1) # noqa: SLF001
143145

0 commit comments

Comments
 (0)