Skip to content

Commit e805af6

Browse files
GitHKAndrei Neagumergify[bot]
authored
🐛 ooil can now escape quadruple $ used by OsparcVariableIdentifier (ITISFoundation#8118)
Co-authored-by: Andrei Neagu <[email protected]> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent 289f936 commit e805af6

File tree

8 files changed

+201
-49
lines changed

8 files changed

+201
-49
lines changed

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)

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

packages/service-integration/tests/test_osparc_image_specs.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import pytest
99
import yaml
1010
from pydantic import BaseModel
11+
from pytest_simcore.helpers.monkeypatch_envs import setenvs_from_dict
1112
from service_integration.compose_spec_model import BuildItem, Service
1213
from service_integration.osparc_config import (
1314
DockerComposeOverwriteConfig,
@@ -19,7 +20,13 @@
1920

2021

2122
@pytest.fixture
22-
def settings() -> AppSettings:
23+
def settings(monkeypatch: pytest.MonkeyPatch) -> AppSettings:
24+
setenvs_from_dict(
25+
monkeypatch,
26+
{
27+
"ENABLE_OOIL_OSPARC_VARIABLE_IDENTIFIER": "true",
28+
},
29+
)
2330
return AppSettings()
2431

2532

0 commit comments

Comments
 (0)