Skip to content

Commit f882ba8

Browse files
committed
Merge branch 'master' into is280/validate-phone-numbers
2 parents 20ae11e + 49fe1e2 commit f882ba8

File tree

56 files changed

+881
-525
lines changed

Some content is hidden

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

56 files changed

+881
-525
lines changed

packages/models-library/src/models_library/api_schemas_long_running_tasks/tasks.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
from datetime import datetime
33
from typing import Any
44

5-
from pydantic import BaseModel, field_validator
5+
from common_library.exclude import Unset
6+
from pydantic import BaseModel, ConfigDict, model_validator
67

78
from .base import TaskId, TaskProgress
89

@@ -20,15 +21,30 @@ class TaskResult(BaseModel):
2021

2122
class TaskBase(BaseModel):
2223
task_id: TaskId
23-
task_name: str
24+
task_name: str | Unset = Unset.VALUE
25+
26+
@model_validator(mode="after")
27+
def try_populate_task_name_from_task_id(self) -> "TaskBase":
28+
# NOTE: currently this model is used to validate tasks coming from
29+
# the celery backend and form long_running_tasks
30+
# 1. if a task comes from Celery, it will keep it's given name
31+
# 2. if a task comes from long_running_tasks, it will extract it form
32+
# the task_id, which looks like "{PREFIX}.{TASK_NAME}.UNIQUE|{UUID}"
33+
34+
if self.task_id and self.task_name == Unset.VALUE:
35+
parts = self.task_id.split(".")
36+
if len(parts) > 1:
37+
self.task_name = urllib.parse.unquote(parts[1])
38+
39+
if self.task_name == Unset.VALUE:
40+
self.task_name = self.task_id
41+
42+
return self
43+
44+
model_config = ConfigDict(arbitrary_types_allowed=True)
2445

2546

2647
class TaskGet(TaskBase):
2748
status_href: str
2849
result_href: str
2950
abort_href: str
30-
31-
@field_validator("task_name")
32-
@classmethod
33-
def unquote_str(cls, v) -> str:
34-
return urllib.parse.unquote(v)

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

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,21 @@
1+
import os
12
from copy import deepcopy
2-
from typing import Any, TypeVar
3+
from typing import Annotated, Any, Final, TypeVar
34

45
from common_library.errors_classes import OsparcErrorMixin
56
from models_library.basic_types import ConstrainedStr
6-
7-
from pydantic import BaseModel
7+
from pydantic import BaseModel, Discriminator, PositiveInt, Tag
88

99
from .utils.string_substitution import OSPARC_IDENTIFIER_PREFIX
10+
from .utils.types import get_types_from_annotated_union
1011

1112
T = TypeVar("T")
1213

1314

14-
class OsparcVariableIdentifier(ConstrainedStr):
15+
class _BaseOsparcVariableIdentifier(ConstrainedStr):
1516
# NOTE: To allow parametrized value, set the type to Union[OsparcVariableIdentifier, ...]
1617
# NOTE: When dealing with str types, to avoid unexpected behavior, the following
1718
# order is suggested `OsparcVariableIdentifier | str`
18-
# NOTE: in below regex `{`` and `}` are respectively escaped with `{{` and `}}`
19-
pattern = (
20-
rf"^\${{1,2}}(?:\{{)?{OSPARC_IDENTIFIER_PREFIX}[A-Za-z0-9_]+(?:\}})?(:-.+)?$"
21-
)
2219

2320
def _get_without_template_markers(self) -> str:
2421
# $VAR
@@ -42,6 +39,40 @@ def default_value(self) -> str | None:
4239
parts = self._get_without_template_markers().split(":-")
4340
return parts[1] if len(parts) > 1 else None
4441

42+
@staticmethod
43+
def get_pattern(max_dollars: PositiveInt) -> str:
44+
# NOTE: in below regex `{`` and `}` are respectively escaped with `{{` and `}}`
45+
return rf"^\${{1,{max_dollars}}}(?:\{{)?{OSPARC_IDENTIFIER_PREFIX}[A-Za-z0-9_]+(?:\}})?(:-.+)?$"
46+
47+
48+
class PlatformOsparcVariableIdentifier(_BaseOsparcVariableIdentifier):
49+
pattern = _BaseOsparcVariableIdentifier.get_pattern(max_dollars=2)
50+
51+
52+
class OoilOsparcVariableIdentifier(_BaseOsparcVariableIdentifier):
53+
pattern = _BaseOsparcVariableIdentifier.get_pattern(max_dollars=4)
54+
55+
56+
_PLATFORM: Final[str] = "platform"
57+
_OOIL_VERSION: Final[str] = "ooil-version"
58+
59+
60+
def _get_discriminator_value(v: Any) -> str:
61+
_ = v
62+
if os.environ.get("ENABLE_OOIL_OSPARC_VARIABLE_IDENTIFIER", None):
63+
return _OOIL_VERSION
64+
65+
return _PLATFORM
66+
67+
68+
OsparcVariableIdentifier = Annotated[
69+
(
70+
Annotated[PlatformOsparcVariableIdentifier, Tag(_PLATFORM)]
71+
| Annotated[OoilOsparcVariableIdentifier, Tag(_OOIL_VERSION)]
72+
),
73+
Discriminator(_get_discriminator_value),
74+
]
75+
4576

4677
class UnresolvedOsparcVariableIdentifierError(OsparcErrorMixin, TypeError):
4778
msg_template = "Provided argument is unresolved: value={value}"
@@ -59,9 +90,9 @@ def example_func(par: OsparcVariableIdentifier | int) -> None:
5990
Raises:
6091
TypeError: if the the OsparcVariableIdentifier was unresolved
6192
"""
62-
if isinstance(var, OsparcVariableIdentifier):
93+
if isinstance(var, get_types_from_annotated_union(OsparcVariableIdentifier)):
6394
raise UnresolvedOsparcVariableIdentifierError(value=var)
64-
return var
95+
return var # type: ignore[return-value]
6596

6697

6798
def replace_osparc_variable_identifier( # noqa: C901
@@ -86,11 +117,11 @@ def replace_osparc_variable_identifier( # noqa: C901
86117
```
87118
"""
88119

89-
if isinstance(obj, OsparcVariableIdentifier):
90-
if obj.name in osparc_variables:
91-
return deepcopy(osparc_variables[obj.name]) # type: ignore
92-
if obj.default_value is not None:
93-
return deepcopy(obj.default_value) # type: ignore
120+
if isinstance(obj, get_types_from_annotated_union(OsparcVariableIdentifier)):
121+
if obj.name in osparc_variables: # type: ignore[attr-defined]
122+
return deepcopy(osparc_variables[obj.name]) # type: ignore[no-any-return,attr-defined]
123+
if obj.default_value is not None: # type: ignore[attr-defined]
124+
return deepcopy(obj.default_value) # type: ignore[no-any-return,attr-defined]
94125
elif isinstance(obj, dict):
95126
for key, value in obj.items():
96127
obj[key] = replace_osparc_variable_identifier(value, osparc_variables)
@@ -124,7 +155,7 @@ def raise_if_unresolved_osparc_variable_identifier_found(obj: Any) -> None:
124155
UnresolvedOsparcVariableIdentifierError: if not all instances of
125156
`OsparcVariableIdentifier` were replaced
126157
"""
127-
if isinstance(obj, OsparcVariableIdentifier):
158+
if isinstance(obj, get_types_from_annotated_union(OsparcVariableIdentifier)):
128159
raise_if_unresolved(obj)
129160
elif isinstance(obj, dict):
130161
for key, value in obj.items():

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

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
11
from collections.abc import Generator
22
from typing import Final
33

4-
from pydantic import BaseModel, ConfigDict, Field, TypeAdapter, ValidationInfo, field_validator
4+
from pydantic import (
5+
BaseModel,
6+
ConfigDict,
7+
Field,
8+
TypeAdapter,
9+
ValidationInfo,
10+
field_validator,
11+
)
512

613
from .basic_types import PortInt
7-
from .osparc_variable_identifier import OsparcVariableIdentifier, raise_if_unresolved
14+
from .osparc_variable_identifier import (
15+
OsparcVariableIdentifier,
16+
raise_if_unresolved,
17+
)
18+
from .utils.types import get_types_from_annotated_union
819

920
# Cloudflare DNS server address
1021
DEFAULT_DNS_SERVER_ADDRESS: Final[str] = "1.1.1.1" # NOSONAR
@@ -20,13 +31,15 @@ class _PortRange(BaseModel):
2031
@field_validator("upper")
2132
@classmethod
2233
def lower_less_than_upper(cls, v, info: ValidationInfo) -> PortInt:
23-
if isinstance(v, OsparcVariableIdentifier):
34+
if isinstance(v, get_types_from_annotated_union(OsparcVariableIdentifier)):
2435
return v # type: ignore # bypass validation if unresolved
2536

2637
upper = v
2738
lower: PortInt | OsparcVariableIdentifier | None = info.data.get("lower")
2839

29-
if lower and isinstance(lower, OsparcVariableIdentifier):
40+
if lower and isinstance(
41+
lower, get_types_from_annotated_union(OsparcVariableIdentifier)
42+
):
3043
return v # type: ignore # bypass validation if unresolved
3144

3245
if lower is None or lower >= upper:
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from functools import lru_cache
2+
from typing import Annotated, Any, Union, get_args, get_origin
3+
4+
5+
@lru_cache
6+
def get_types_from_annotated_union(annotated_alias: Any) -> tuple[type, ...]:
7+
"""
8+
Introspects a complex Annotated alias to extract the base types from its inner Union.
9+
"""
10+
if get_origin(annotated_alias) is not Annotated:
11+
msg = "Expected an Annotated type."
12+
raise TypeError(msg)
13+
14+
# Get the contents of Annotated, e.g., (Union[...], Discriminator(...))
15+
annotated_args = get_args(annotated_alias)
16+
union_type = annotated_args[0]
17+
18+
# The Union can be from typing.Union or the | operator
19+
if get_origin(union_type) is not Union:
20+
msg = "Expected a Union inside the Annotated type."
21+
raise TypeError(msg)
22+
23+
# Get the members of the Union, e.g., (Annotated[TypeA, ...], Annotated[TypeB, ...])
24+
union_members = get_args(union_type)
25+
26+
extracted_types = []
27+
for member in union_members:
28+
# Each member is also Annotated, so we extract its base type
29+
if get_origin(member) is Annotated:
30+
extracted_types.append(get_args(member)[0])
31+
else:
32+
extracted_types.append(member) # Handle non-annotated members in the union
33+
34+
return tuple(extracted_types)
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import pytest
2+
from models_library.api_schemas_long_running_tasks.tasks import TaskGet
3+
from pydantic import TypeAdapter
4+
5+
6+
def _get_data_without_task_name(task_id: str) -> dict:
7+
return {
8+
"task_id": task_id,
9+
"status_href": "",
10+
"result_href": "",
11+
"abort_href": "",
12+
}
13+
14+
15+
@pytest.mark.parametrize(
16+
"data, expected_task_name",
17+
[
18+
(_get_data_without_task_name("a.b.c.d"), "b"),
19+
(_get_data_without_task_name("a.b.c"), "b"),
20+
(_get_data_without_task_name("a.b"), "b"),
21+
(_get_data_without_task_name("a"), "a"),
22+
],
23+
)
24+
def test_try_extract_task_name(data: dict, expected_task_name: str) -> None:
25+
task_get = TaskGet(**data)
26+
assert task_get.task_name == expected_task_name
27+
28+
task_get = TypeAdapter(TaskGet).validate_python(data)
29+
assert task_get.task_name == expected_task_name
30+
31+
32+
def _get_data_with_task_name(task_id: str, task_name: str) -> dict:
33+
return {
34+
"task_id": task_id,
35+
"task_name": task_name,
36+
"status_href": "",
37+
"result_href": "",
38+
"abort_href": "",
39+
}
40+
41+
42+
@pytest.mark.parametrize(
43+
"data, expected_task_name",
44+
[
45+
(_get_data_with_task_name("a.b.c.d", "a_name"), "a_name"),
46+
(_get_data_with_task_name("a.b.c", "a_name"), "a_name"),
47+
(_get_data_with_task_name("a.b", "a_name"), "a_name"),
48+
(_get_data_with_task_name("a", "a_name"), "a_name"),
49+
],
50+
)
51+
def test_task_name_is_provided(data: dict, expected_task_name: str) -> None:
52+
task_get = TaskGet(**data)
53+
assert task_get.task_name == expected_task_name
54+
55+
task_get = TypeAdapter(TaskGet).validate_python(data)
56+
assert task_get.task_name == expected_task_name

packages/models-library/tests/test_service_settings_nat_rule.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
replace_osparc_variable_identifier,
1010
)
1111
from models_library.service_settings_nat_rule import NATRule
12+
from models_library.utils.types import get_types_from_annotated_union
1213
from pydantic import TypeAdapter
1314

1415
SUPPORTED_TEMPLATES: set[str] = {
@@ -111,13 +112,13 @@ def test_______(replace_with_value: Any):
111112
a_var = TypeAdapter(OsparcVariableIdentifier).validate_python(
112113
"$OSPARC_VARIABLE_some_var"
113114
)
114-
assert isinstance(a_var, OsparcVariableIdentifier)
115+
assert isinstance(a_var, get_types_from_annotated_union(OsparcVariableIdentifier))
115116

116117
replaced_var = replace_osparc_variable_identifier(
117118
a_var, {"OSPARC_VARIABLE_some_var": replace_with_value}
118119
)
119120
# NOTE: after replacement the original reference still points
120-
assert isinstance(a_var, OsparcVariableIdentifier)
121+
assert isinstance(a_var, get_types_from_annotated_union(OsparcVariableIdentifier))
121122
assert replaced_var == replace_with_value
122123

123124

@@ -154,15 +155,15 @@ def test_replace_an_instance_of_osparc_variable_identifier(
154155
formatted_template = var_template
155156

156157
a_var = TypeAdapter(OsparcVariableIdentifier).validate_python(formatted_template)
157-
assert isinstance(a_var, OsparcVariableIdentifier)
158+
assert isinstance(a_var, get_types_from_annotated_union(OsparcVariableIdentifier))
158159

159160
replace_with_identifier_default = identifier_has_default and replace_with_default
160161
replacement_content = (
161162
{} if replace_with_identifier_default else {a_var.name: replace_with_value}
162163
)
163164
replaced_var = replace_osparc_variable_identifier(a_var, replacement_content)
164165
# NOTE: after replacement the original reference still points
165-
assert isinstance(a_var, OsparcVariableIdentifier)
166+
assert isinstance(a_var, get_types_from_annotated_union(OsparcVariableIdentifier))
166167
if replace_with_identifier_default:
167168
assert replaced_var == default_value
168169
else:
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
""" Library to facilitate the integration of user services running in osparc-simcore
2-
3-
"""
1+
"""Library to facilitate the integration of user services running in osparc-simcore"""
42

53
from ._meta import __version__
Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,3 @@
1-
paths-mapping:
2-
inputs_path: "/config/workspace/inputs"
3-
outputs_path: "/config/workspace/outputs"
4-
state_paths:
5-
- "/config"
61
settings:
72
- name: resources
83
type: Resources
@@ -16,7 +11,32 @@ settings:
1611
type: string
1712
value:
1813
- node.platform.os == linux
19-
# # https://docs.docker.com/compose/compose-file/compose-file-v3/#environment
20-
# - name: environment
21-
# type: string
22-
# -
14+
paths-mapping:
15+
inputs_path: "/config/workspace/inputs"
16+
outputs_path: "/config/workspace/outputs"
17+
state_paths:
18+
- "/config"
19+
callbacks-mapping:
20+
inactivity:
21+
service: container
22+
command: ["python", "/usr/local/bin/service-monitor/activity.py"]
23+
timeout: 1
24+
compose-spec:
25+
version: "3.7"
26+
services:
27+
jupyter-math:
28+
image: $$$${SIMCORE_REGISTRY}/simcore/services/dynamic/jupyter-math:$$$${SERVICE_VERSION}
29+
environment:
30+
- OSPARC_API_HOST=$$$${OSPARC_VARIABLE_API_HOST}
31+
- OSPARC_API_KEY=$$$${OSPARC_VARIABLE_API_KEY}
32+
- OSPARC_API_SECRET=$$$${OSPARC_VARIABLE_API_SECRET}
33+
container-http-entrypoint: jupyter-math
34+
containers-allowed-outgoing-permit-list:
35+
jupyter-math:
36+
- hostname: $$$${OSPARC_VARIABLE_VENDOR_SECRET_LICENSE_SERVER_HOST}
37+
tcp_ports: [$$OSPARC_VARIABLE_VENDOR_SECRET_LICENSE_SERVER_PRIMARY_PORT, $$OSPARC_VARIABLE_VENDOR_SECRET_LICENSE_SERVER_SECONDARY_PORT]
38+
dns_resolver:
39+
address: $$$${OSPARC_VARIABLE_VENDOR_SECRET_LICENSE_DNS_RESOLVER_IP}
40+
port: $$$${OSPARC_VARIABLE_VENDOR_SECRET_LICENSE_DNS_RESOLVER_PORT}
41+
containers-allowed-outgoing-internet:
42+
- jupyter-math

0 commit comments

Comments
 (0)