Skip to content

Commit d0fba0b

Browse files
authored
♻️🔨 Is3517/refactor service io and diagnostics tool concept (ITISFoundation#3537)
1 parent 5c17bca commit d0fba0b

File tree

14 files changed

+428
-85
lines changed

14 files changed

+428
-85
lines changed

packages/models-library/Makefile

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ docker_rest_api.py: ## auto-generates pydantic models for Docker REST API models
8181
# - NOTE: these tools require graphviz!
8282
# - SEE https://erdantic.drivendata.org/stable/
8383
#
84-
84+
.PHONY: _erdantic
8585
_erdantic: _check_venv_active
8686
# ensures erdantic installed
8787
@python3 -c "import erdantic" 2>/dev/null || pip install erdantic
@@ -101,3 +101,23 @@ erd-Node.svg: _erdantic
101101
erd-ServiceInput.svg: _erdantic
102102
erdantic models_library.services.ServiceInput \
103103
--out $@
104+
105+
106+
#
107+
# Test data
108+
#
109+
110+
DOWNLOADED_TEST_DATA_DIR = "$(CURDIR)/tests/data/.downloaded-ignore"
111+
112+
.PHONY: _httpx
113+
_httpx: _check_venv_active
114+
# ensures requirements installed
115+
@python3 -c "import httpx" 2>/dev/null || pip install httpx
116+
117+
PHONY: pull_test_data
118+
pull_test_data: $(DOT_ENV_FILE) _httpx ## downloads tests data from registry (this can take some time!)
119+
# downloading all metadata files
120+
@set -o allexport; \
121+
source $<; \
122+
set +o allexport; \
123+
python3 "$(PACKAGES_DIR)/pytest-simcore/src/pytest_simcore/helpers/utils_docker_registry.py" $(DOWNLOADED_TEST_DATA_DIR)

packages/models-library/setup.cfg

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,6 @@ test = pytest
1616

1717
[tool:pytest]
1818
asyncio_mode = auto
19+
20+
markers =
21+
diagnostics: "can be used to run diagnostics against deployed data (e.g. database, registry etc)"

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

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import mimetypes
12
from copy import deepcopy
23
from typing import Any, Literal, Optional, Union
34

@@ -14,6 +15,25 @@
1415
}
1516

1617

18+
def guess_media_type(io: Union[ServiceInput, ServiceOutput]) -> str:
19+
# SEE https://docs.python.org/3/library/mimetypes.html
20+
# SEE https://www.iana.org/assignments/media-types/media-types.xhtml
21+
media_type = io.property_type.removeprefix("data:")
22+
if media_type == "*/*" and io.file_to_key_map:
23+
filename = list(io.file_to_key_map.keys())[0]
24+
media_type, _ = mimetypes.guess_type(filename)
25+
if media_type is None:
26+
media_type = "*/*"
27+
return media_type
28+
29+
30+
def update_schema_doc(schema: dict[str, Any], port: Union[ServiceInput, ServiceOutput]):
31+
schema["title"] = port.label
32+
if port.label != port.description:
33+
schema["description"] = port.description
34+
return schema
35+
36+
1737
def get_service_io_json_schema(
1838
port: Union[ServiceInput, ServiceOutput]
1939
) -> Optional[dict[str, Any]]:
@@ -25,16 +45,15 @@ def get_service_io_json_schema(
2545
of BaseServiceIO once we proceed to a full deprecation of legacy fields like units, etc
2646
"""
2747
if port.content_schema:
48+
# NOTE this schema was already validated in BaseServiceIOModel
2849
return deepcopy(port.content_schema)
2950

3051
# converts legacy
3152
if schema := _PROPERTY_TYPE_TO_SCHEMAS.get(port.property_type):
3253
schema = deepcopy(schema)
3354

3455
# updates schema-doc, i.e description and title
35-
schema["title"] = port.label
36-
if port.label != port.description:
37-
schema["description"] = port.description
56+
update_schema_doc(schema=schema, port=port)
3857

3958
# new x_unit custom field in json-schema
4059
if port.unit:

packages/models-library/tests/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99
import pytest
1010

1111
pytest_plugins = [
12-
"pytest_simcore.repository_paths",
13-
"pytest_simcore.schemas",
1412
"pytest_simcore.pydantic_models",
1513
"pytest_simcore.pytest_global_environs",
14+
"pytest_simcore.repository_paths",
15+
"pytest_simcore.schemas",
1616
]
1717

1818
CURRENT_DIR = Path(sys.argv[0] if __name__ == "__main__" else __file__).resolve().parent
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
{
2+
"authors": [
3+
{
4+
"name": "Katie Zhuang",
5+
"email": "[email protected]",
6+
"affiliation": "IT'IS Foundation"
7+
}
8+
],
9+
"contact": "[email protected]",
10+
"description": "ASCENT is an open source platform for simulating peripheral nerve stimulation. To download the software, visit the [ASCENT GitHub repository](https://github.com/wmglab-duke/ascent). This implementation uses a limited set of user-defined parameters to run the pipeline and will output the results of the NEURON simulations * Musselman ED, Cariello JE, Grill WM, Pelot NA. ASCENT (Automated Simulations to Characterize Electrical Nerve Thresholds): A Pipeline for Sample-Specific Computational Modeling of Electrical Stimulation of Peripheral Nerves. PLoS Comput Biol [Internet]. 2021; Available from: https://doi.org/10.1371/journal.pcbi.1009285 * Musselman ED, Cariello JE, Grill WM, Pelot NA. ASCENT (Automated Simulations to Characterize Electrical Nerve Thresholds): A Pipeline for Sample-Specific Computational Modeling of Electrical Stimulation of Peripheral Nerves. PLoS Comput Biol [Internet]. 2021, DOI: 10.5281/zenodo.5500260\n",
11+
"inputs": {
12+
"input_1": {
13+
"displayOrder": 1,
14+
"label": "Nerve Morphology",
15+
"description": "Choice of Vagus nerve morphology - either Rat VN or Human VN",
16+
"type": "ref_contentSchema",
17+
"contentSchema": {
18+
"title": "Nerve Morphology",
19+
"default": "Rat",
20+
"enum": [
21+
"Rat",
22+
"Human"
23+
]
24+
}
25+
},
26+
"input_2": {
27+
"displayOrder": 2,
28+
"label": "Cuff geometry",
29+
"description": "45\u00b0 contact wrap monopolar cuff or 360\u00b0 contact wrap monopolar cuff or 45\u00b0 contact wrap, bipolar cuff or 360\u00b0 contact wrap, bipolar cuff \n",
30+
"type": "ref_contentSchema",
31+
"contentSchema": {
32+
"title": "Cuff Geometry",
33+
"default": "45\u00b0 monopolar",
34+
"enum": [
35+
"45\u00b0 monopolar",
36+
"360\u00b0 monopolar",
37+
"45\u00b0 bipolar",
38+
"360\u00b0 bipolar"
39+
]
40+
}
41+
},
42+
"input_3": {
43+
"displayOrder": 3,
44+
"label": "Fiber Locations",
45+
"description": "centroid: one fiber location at the centroid of each fascicle wheel: 6 spokes with 2 fibers per spoke, plus the centroid; 13 fiber locations per fascicle\n",
46+
"type": "ref_contentSchema",
47+
"contentSchema": {
48+
"title": "Fiber Locations",
49+
"default": "wheel",
50+
"enum": [
51+
"centroid",
52+
"wheel"
53+
]
54+
}
55+
},
56+
"fibers": {
57+
"displayOrder": 4,
58+
"label": "List of Fiber Diameters",
59+
"description": "Comma-separated list of fiber diameters (between 2 and 10), defaults units are \u03bcm. At least one value must be provided, maximum 5 values",
60+
"type": "ref_contentSchema",
61+
"contentSchema": {
62+
"title": "Fiber Diameters",
63+
"type": "array",
64+
"minItems": 1,
65+
"maxItems": 5,
66+
"default": [
67+
10
68+
],
69+
"x_unit": "micro-meter",
70+
"items": {
71+
"minimum": 2,
72+
"maximum": 10,
73+
"type": "number"
74+
}
75+
}
76+
},
77+
"input_4": {
78+
"displayOrder": 5,
79+
"label": "Waveform",
80+
"description": "monophasic: pulse (cathodic for monopolar cuff; cathodic phase first on contact closest to recording site for bipolar cuff) biphasic: symmetric biphasic pulse\n",
81+
"type": "ref_contentSchema",
82+
"contentSchema": {
83+
"title": "Waveform",
84+
"default": "biphasic",
85+
"enum": [
86+
"monophasic",
87+
"biphasic"
88+
]
89+
}
90+
},
91+
"durations": {
92+
"displayOrder": 6,
93+
"label": "List of Waveform Durations",
94+
"description": "Comma-separated list of pulse widths (between 0.05 and 2), default units are ms. At least one value must be provided, maximum 5 values.",
95+
"type": "ref_contentSchema",
96+
"contentSchema": {
97+
"title": "Waveform Durations",
98+
"type": "array",
99+
"minItems": 1,
100+
"maxItems": 5,
101+
"default": [
102+
0.5
103+
],
104+
"x_unit": "milli-second",
105+
"items": {
106+
"minimum": 0.05,
107+
"maximum": 2.0,
108+
"type": "number"
109+
}
110+
}
111+
}
112+
},
113+
"integration-version": "1.0.0",
114+
"key": "simcore/services/comp/ascent-runner",
115+
"name": "ascent-runner",
116+
"outputs": {
117+
"output_1": {
118+
"displayOrder": 1,
119+
"label": "Simulation Outputs",
120+
"description": "Resulting thresholds from simulations.",
121+
"type": "data:*/*",
122+
"fileToKeyMap": {
123+
"ascent_results.zip": "output_1"
124+
}
125+
},
126+
"output_2": {
127+
"displayOrder": 1,
128+
"label": "Environment Variables",
129+
"description": "Sample number, model number and simulation number for postprocessing.",
130+
"type": "data:*/*",
131+
"fileToKeyMap": {
132+
"envs": "output_2"
133+
}
134+
}
135+
},
136+
"thumbnail": "https://wmglab-duke-ascent.readthedocs.io/en/latest/_images/ascent_media_release_v2.png",
137+
"type": "computational",
138+
"version": "1.3.2"
139+
}

packages/models-library/tests/data/image-meta.yaml renamed to packages/models-library/tests/data/metadata-sleeper-2.0.2.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@ type: computational
44
integration-version: 1.0.0
55
version: 2.0.2
66
description: A service which awaits for time to pass.
7-
contact: neagu@itis.swiss
7+
contact: neagu@test.it
88
authors:
99
- name: "Manuel Guidon"
10-
email: guidon@itis.swiss
10+
email: guidon@test.it
1111
affiliation: "IT'IS Foundation"
1212
- name: "Odei Maiz"
13-
email: maiz@itis.swiss
13+
email: maiz@test.it
1414
affiliation: "IT'IS Foundation"
1515
- name: "Andrei Neagu"
16-
email: neagu@itis.swiss
16+
email: neagu@test.it
1717
affiliation: "IT'IS Foundation"
1818
inputs:
1919
input_1:

packages/models-library/tests/test_services_io.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
def test_service_port_units(project_tests_dir: Path):
1313
ureg = UnitRegistry()
1414

15-
data = yaml.safe_load((project_tests_dir / "data" / "image-meta.yaml").read_text())
15+
data = yaml.safe_load(
16+
(project_tests_dir / "data" / "metadata-sleeper-2.0.2.yaml").read_text()
17+
)
1618
print(ServiceDockerData.schema_json(indent=2))
1719

1820
service_meta = ServiceDockerData.parse_obj(data)

packages/models-library/tests/test_utils_service_io.py

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@
44
# pylint: disable=too-many-arguments
55

66

7+
import itertools
8+
import json
9+
import sys
10+
from pathlib import Path
711
from typing import Union
812

913
import pytest
10-
from models_library.services import ServiceInput, ServiceOutput
14+
from models_library.services import ServiceInput, ServiceOutput, ServicePortKey
1115
from models_library.utils.json_schema import jsonschema_validate_schema
1216
from models_library.utils.services_io import get_service_io_json_schema
17+
from pydantic import parse_obj_as
1318

1419
example_inputs_labels = [
1520
e for e in ServiceInput.Config.schema_extra["examples"] if e["label"]
@@ -20,7 +25,7 @@
2025

2126

2227
@pytest.fixture(params=example_inputs_labels + example_outputs_labels)
23-
def service_io(request: pytest.FixtureRequest) -> Union[ServiceInput, ServiceOutput]:
28+
def service_port(request: pytest.FixtureRequest) -> Union[ServiceInput, ServiceOutput]:
2429
try:
2530
index = example_inputs_labels.index(request.param)
2631
example = ServiceInput.Config.schema_extra["examples"][index]
@@ -31,17 +36,49 @@ def service_io(request: pytest.FixtureRequest) -> Union[ServiceInput, ServiceOut
3136
return ServiceOutput.parse_obj(example)
3237

3338

34-
def test_it(service_io: Union[ServiceInput, ServiceOutput]):
35-
print(service_io.json(indent=2))
39+
def test_get_schema_from_port(service_port: Union[ServiceInput, ServiceOutput]):
40+
print(service_port.json(indent=2))
3641

3742
# get
38-
schema = get_service_io_json_schema(service_io)
43+
schema = get_service_io_json_schema(service_port)
3944
print(schema)
4045

41-
if service_io.property_type.startswith("data"):
46+
if service_port.property_type.startswith("data"):
4247
assert not schema
4348
else:
4449
assert schema
45-
4650
# check valid jsons-schema
4751
jsonschema_validate_schema(schema)
52+
53+
54+
CURRENT_DIR = Path(sys.argv[0] if __name__ == "__main__" else __file__).resolve().parent
55+
TEST_DATA_FOLDER = CURRENT_DIR / "data"
56+
57+
58+
@pytest.mark.diagnostics
59+
@pytest.mark.parametrize(
60+
"metadata_path",
61+
TEST_DATA_FOLDER.rglob("metadata*.json"),
62+
ids=lambda p: f"{p.parent.name}/{p.name}",
63+
)
64+
def test_against_service_metadata_configs(metadata_path: Path):
65+
"""
66+
This tests can be used as well to validate all metadata in a given registry
67+
68+
SEE make pull_test_data to pull data from the registry specified in .env
69+
"""
70+
71+
meta = json.loads(metadata_path.read_text())
72+
73+
inputs = parse_obj_as(dict[ServicePortKey, ServiceInput], meta["inputs"])
74+
outputs = parse_obj_as(dict[ServicePortKey, ServiceOutput], meta["outputs"])
75+
76+
for port in itertools.chain(inputs.values(), outputs.values()):
77+
schema = get_service_io_json_schema(port)
78+
79+
if port.property_type.startswith("data"):
80+
assert not schema
81+
else:
82+
assert schema
83+
# check valid jsons-schema
84+
jsonschema_validate_schema(schema)

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

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,26 @@
33
# pylint: disable=unused-variable
44

55

6-
from copy import deepcopy
76
from pathlib import Path
87

9-
import dotenv
108
import pytest
119
from pytest import MonkeyPatch
1210

11+
from .helpers.typing_env import EnvVarsDict
12+
from .helpers.utils_envs import load_dotenv, setenvs_from_dict
13+
1314

1415
@pytest.fixture(scope="session")
15-
def env_devel_dict(env_devel_file: Path) -> dict[str, str]:
16+
def env_devel_dict(env_devel_file: Path) -> EnvVarsDict:
1617
assert env_devel_file.exists()
1718
assert env_devel_file.name == ".env-devel"
18-
environ = dotenv.dotenv_values(env_devel_file, verbose=True, interpolate=True)
19-
assert all(v is not None for v in environ.values())
20-
return environ # type: ignore
19+
envs = load_dotenv(env_devel_file, verbose=True, interpolate=True)
20+
return envs
2121

2222

2323
@pytest.fixture(scope="function")
2424
def mock_env_devel_environment(
2525
env_devel_dict: dict[str, str], monkeypatch: MonkeyPatch
26-
) -> dict[str, str]:
27-
for key, value in env_devel_dict.items():
28-
monkeypatch.setenv(key, str(value))
29-
return deepcopy(env_devel_dict)
26+
) -> EnvVarsDict:
27+
envs = setenvs_from_dict(monkeypatch, env_devel_dict)
28+
return envs

0 commit comments

Comments
 (0)