From 09aa7ef864da8970250d40a49e0dba84c3068ee1 Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Mon, 28 Feb 2022 16:54:04 +0800 Subject: [PATCH 01/24] [DLMED] complete temp test schema Signed-off-by: Nic Ma --- monai/apps/manifest/metadata.json | 246 ++++++++++++++++++++++++++++++ monai/apps/manifest/utils.py | 16 ++ 2 files changed, 262 insertions(+) create mode 100644 monai/apps/manifest/metadata.json create mode 100644 monai/apps/manifest/utils.py diff --git a/monai/apps/manifest/metadata.json b/monai/apps/manifest/metadata.json new file mode 100644 index 0000000000..318a8739ac --- /dev/null +++ b/monai/apps/manifest/metadata.json @@ -0,0 +1,246 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$id": "https://monai.io/metadata_schema.json", + "type": "object", + "properties": { + "version": { + "description": "version number of the model package.", + "type": "string" + }, + "changelog": { + "description": "dictionary relating previous version names to strings describing the version.", + "type": "object" + }, + "monai_version": { + "description": "version of MONAI the model package was generated on.", + "type": "string" + }, + "pytorch_version": { + "description": "version of PyTorch the model package was generated on.", + "type": "string" + }, + "numpy_version": { + "description": "version of NumPy the model package was generated on.", + "type": "string" + }, + "optional_packages_version": { + "description": "dictionary relating optional package names to their versions.", + "type": "object" + }, + "task": { + "description": "plain-language description of what the model is meant to do.", + "type": "string" + }, + "description": { + "description": "longer form plain-language description of what the model is, what it does, etc.", + "type": "string" + }, + "authorship": { + "description": "state author(s) of the model package.", + "type": "string" + }, + "copyright": { + "description": "state copyright of the model package.", + "type": "string" + }, + "data_source": { + "description": "where to download or prepare the data used in this model package.", + "type": "string" + }, + "data_type": { + "description": "type of the data, like: `dicom`, `nibabel`, etc.", + "type": "string" + }, + "dataset_dir": { + "description": "state the expected path of data in file system.", + "type": "string" + }, + "image_classes": { + "description": "description for every class of the input image.", + "type": "string" + }, + "label_classes": { + "description": "description for every class of the input label.", + "type": "string" + }, + "pred_classes": { + "description": "description for every class of the output prediction.", + "type": "string" + }, + "eval_metrics": { + "description": "dictionary relating evaluation metrics to the achieved scores.", + "type": "object" + }, + "intended_use": { + "description": "what the model package is to be used for, ie. what task it accomplishes.", + "type": "string" + }, + "references": { + "description": "list of published referenced relating to the model package.", + "type": "array" + }, + "network_data_format": { + "description": "defines the format, shape, and meaning of inputs and outputs to the model.", + "type": "object", + "properties": { + "inputs": { + "type": "object", + "properties": { + "image": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "format": { + "type": "string" + }, + "num_channels": { + "type": "integer" + }, + "spatial_shape": { + "type": "array" + }, + "dtype": { + "type": "string" + }, + "value_range": { + "type": "array", + "items": { + "type": "number" + } + }, + "is_patch_data": { + "type": "boolean" + }, + "channel_def": { + "type": "object" + } + }, + "required": [ + "type", + "format", + "num_channels", + "spatial_shape", + "dtype", + "value_range" + ] + }, + "label": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "format": { + "type": "string" + }, + "num_channels": { + "type": "integer" + }, + "spatial_shape": { + "type": "array" + }, + "dtype": { + "type": "string" + }, + "value_range": { + "type": "array", + "items": { + "type": "number" + } + }, + "is_patch_data": { + "type": "boolean" + }, + "channel_def": { + "type": "object" + } + }, + "required": [ + "type", + "format", + "num_channels", + "spatial_shape", + "dtype", + "value_range" + ] + } + }, + "required": [ + "image" + ] + }, + "outputs": { + "type": "object", + "properties": { + "pred": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "format": { + "type": "string" + }, + "num_channels": { + "type": "integer" + }, + "spatial_shape": { + "type": "array" + }, + "dtype": { + "type": "string" + }, + "value_range": { + "type": "array", + "items": { + "type": "number" + } + }, + "is_patch_data": { + "type": "boolean" + }, + "channel_def": { + "type": "object" + } + }, + "required": [ + "type", + "format", + "num_channels", + "spatial_shape", + "dtype", + "value_range" + ] + } + }, + "required": [ + "pred" + ] + } + }, + "required": [ + "inputs", + "outputs" + ] + } + }, + "required": [ + "version", + "monai_version", + "pytorch_version", + "numpy_version", + "optional_packages_version", + "task", + "description", + "authorship", + "copyright", + "dataset_dir", + "image_classes", + "label_classes", + "pred_classes", + "eval_metrics", + "network_data_format" + ] +} diff --git a/monai/apps/manifest/utils.py b/monai/apps/manifest/utils.py new file mode 100644 index 0000000000..a344ee40db --- /dev/null +++ b/monai/apps/manifest/utils.py @@ -0,0 +1,16 @@ +# Copyright (c) MONAI Consortium +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Dict + + +def verify_metadata(metadata: Dict, schema_id: str): + pass From 67bd705da0490cbd00b5f784519a5f477084b1e6 Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Mon, 28 Feb 2022 17:45:23 +0800 Subject: [PATCH 02/24] [DLMED] add cmd line API Signed-off-by: Nic Ma --- monai/apps/__init__.py | 9 ++- monai/apps/manifest/__init__.py | 1 + monai/apps/manifest/utils.py | 37 ++++++++- monai/apps/manifest/verify_meta.py | 36 +++++++++ tests/test_manifest_verify_meta.py | 125 +++++++++++++++++++++++++++++ 5 files changed, 204 insertions(+), 4 deletions(-) create mode 100644 monai/apps/manifest/verify_meta.py create mode 100644 tests/test_manifest_verify_meta.py diff --git a/monai/apps/__init__.py b/monai/apps/__init__.py index 0f233bc3ef..1dfd2fa453 100644 --- a/monai/apps/__init__.py +++ b/monai/apps/__init__.py @@ -10,6 +10,13 @@ # limitations under the License. from .datasets import CrossValidation, DecathlonDataset, MedNISTDataset -from .manifest import ComponentLocator, ConfigComponent, ConfigExpression, ConfigItem, ReferenceResolver +from .manifest import ( + ComponentLocator, + ConfigComponent, + ConfigExpression, + ConfigItem, + ReferenceResolver, + verify_metadata, +) from .mmars import MODEL_DESC, RemoteMMARKeys, download_mmar, get_model_spec, load_from_mmar from .utils import SUPPORTED_HASH_TYPES, check_hash, download_and_extract, download_url, extractall, get_logger, logger diff --git a/monai/apps/manifest/__init__.py b/monai/apps/manifest/__init__.py index 79c4376d5c..7cde658db0 100644 --- a/monai/apps/manifest/__init__.py +++ b/monai/apps/manifest/__init__.py @@ -11,3 +11,4 @@ from .config_item import ComponentLocator, ConfigComponent, ConfigExpression, ConfigItem from .reference_resolver import ReferenceResolver +from .utils import verify_metadata diff --git a/monai/apps/manifest/utils.py b/monai/apps/manifest/utils.py index a344ee40db..fa408d016e 100644 --- a/monai/apps/manifest/utils.py +++ b/monai/apps/manifest/utils.py @@ -9,8 +9,39 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Dict +import json +from pathlib import Path +from typing import Dict, Optional +from monai.apps.utils import download_and_extract +from monai.config import PathLike +from monai.utils import optional_import -def verify_metadata(metadata: Dict, schema_id: str): - pass +validate, _ = optional_import("jsonschema", name="validate") + + +def verify_metadata( + metadata: Dict, schema_url: str, filepath: PathLike, create_dir: bool = True, hash_val: Optional[str] = None +): + filepath = Path(filepath) + path_dir = filepath.parent + if not path_dir.exists(): + if create_dir: + path_dir.mkdir(parents=True) + else: + raise ValueError(f"the directory of specified path is not existing: {path_dir}.") + + download_and_extract( + url=schema_url, + filepath=filepath, + output_dir=path_dir, + hash_val=hash_val, + hash_type="md5", + progress=True, + ) + + # FIXME: will update to use `load_config_file()` when PR 3832 is merged + with open(filepath) as f: + schema = json.load(f) + + validate(instance=metadata, schema=schema) diff --git a/monai/apps/manifest/verify_meta.py b/monai/apps/manifest/verify_meta.py new file mode 100644 index 0000000000..b3ea17ead4 --- /dev/null +++ b/monai/apps/manifest/verify_meta.py @@ -0,0 +1,36 @@ +# Copyright (c) MONAI Consortium +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import argparse + +from monai.apps.manifest.utils import verify_metadata + + +def verify(): + parser = argparse.ArgumentParser() + parser.add_argument("--metadata", "-m", type=str, help="filepath of the metadata file.", required=True) + parser.add_argument("--schema_url", "-u", type=str, help="filepath of the config file.", required=True) + parser.add_argument("--filepath", "-f", type=str, help="filepath to store downloaded schema.", required=True) + parser.add_argument("--hash_val", "-h", type=str, help="MD5 hash value to verify schema file.", required=False) + + args = parser.parse_args() + verify_metadata( + metadata=args.metadata, + schema_url=args.schema_url, + filepath=args.filepath, + create_dir=True, + hash_val=args.hash_val, + ) + + +if __name__ == "__main__": + verify() diff --git a/tests/test_manifest_verify_meta.py b/tests/test_manifest_verify_meta.py new file mode 100644 index 0000000000..b98455db27 --- /dev/null +++ b/tests/test_manifest_verify_meta.py @@ -0,0 +1,125 @@ +# Copyright (c) MONAI Consortium +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import logging +import os +import sys +import tempfile +import unittest + +import nibabel as nib +import numpy as np +import yaml +from parameterized import parameterized + +from monai.transforms import LoadImage + +TEST_CASE_1 = [ + { + "version": "0.1.0", + "changelog": { + "0.1.0": "complete the model package", + "0.0.1": "initialize the model package structure" + }, + "monai_version": "0.8.0", + "pytorch_version": "1.10.0", + "numpy_version": "1.21.2", + "optional_packages_version": { + "nibabel": "3.2.1" + }, + "task": "Decathlon spleen segmentation", + "description": "A pre-trained model for volumetric (3D) segmentation of the spleen from CT image", + "authorship": "MONAI team", + "copyright": "Copyright (c) MONAI Consortium", + "data_source": "Task09_Spleen.tar from http://medicaldecathlon.com/", + "data_type": "dicom", + "dataset_dir": "/workspace/data/Task09_Spleen", + "image_classes": "single channel data, intensity scaled to [0, 1]", + "label_classes": "single channel data, 1 is spleen, 0 is everything else", + "pred_classes": "2 channels OneHot data, channel 1 is spleen, channel 0 is background", + "eval_metrics": { + "mean_dice": 0.96 + }, + "intended_use": "This is an example, not to be used for diagnostic purposes", + "references": [ + "Xia, Yingda, et al. '3D Semi-Supervised Learning with Uncertainty-Aware Multi-View Co-Training.'" + " arXiv preprint arXiv:1811.12506 (2018). https://arxiv.org/abs/1811.12506.", + "Kerfoot E., Clough J., Oksuz I., Lee J., King A.P., Schnabel J.A. (2019)" + " Left-Ventricle Quantification Using Residual U-Net. In: Pop M. et al. (eds) Statistical Atlases" + " and Computational Models of the Heart. Atrial Segmentation and LV Quantification Challenges. STACOM 2018." + " Lecture Notes in Computer Science, vol 11395. Springer, Cham. https://doi.org/10.1007/978-3-030-12029-0_40" + ], + "network_data_format": { + "inputs": { + "image": { + "type": "image", + "format": "magnitude", + "num_channels": 1, + "spatial_shape": [ + 160, + 160, + 160 + ], + "dtype": "float32", + "value_range": [ + 0, + 1 + ], + "is_patch_data": False, + "channel_def": { + "0": "image" + } + } + }, + "outputs": { + "pred": { + "type": "image", + "format": "segmentation", + "num_channels": 2, + "spatial_shape": [ + 160, + 160, + 160 + ], + "dtype": "float32", + "value_range": [ + 0, + 1 + ], + "is_patch_data": False, + "channel_def": { + "0": "background", + "1": "spleen" + } + } + } + } + } +] + + +class TestVerifyMeta(unittest.TestCase): + @parameterized.expand([TEST_CASE_1]) + def test_verify(self, meta_data): + with tempfile.TemporaryDirectory() as tempdir: + #filepath = os.path.join(tempdir, "schema.json") + filepath = "/workspace/data/medical/MONAI/monai/apps/manifest/metadata.json" + + metafile = os.path.join(tempdir, "metadata.json") + with open(metafile, "w") as f: + json.dump(meta_data, f) + + os.system(f"python -m monai.apps.manifest.verify_meta -m {metafile} -u fsdfsfs -f {filepath}") + + +if __name__ == "__main__": + unittest.main() From c72088f71e2acd34a302627e07f80487a6d25ffe Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Mon, 28 Feb 2022 17:57:41 +0800 Subject: [PATCH 03/24] [DLMED] fix typo Signed-off-by: Nic Ma --- monai/apps/manifest/utils.py | 11 ++--------- monai/apps/manifest/verify_meta.py | 2 +- tests/test_manifest_verify_meta.py | 7 ------- 3 files changed, 3 insertions(+), 17 deletions(-) diff --git a/monai/apps/manifest/utils.py b/monai/apps/manifest/utils.py index fa408d016e..1b918ce77f 100644 --- a/monai/apps/manifest/utils.py +++ b/monai/apps/manifest/utils.py @@ -13,7 +13,7 @@ from pathlib import Path from typing import Dict, Optional -from monai.apps.utils import download_and_extract +from monai.apps.utils import download_url from monai.config import PathLike from monai.utils import optional_import @@ -31,14 +31,7 @@ def verify_metadata( else: raise ValueError(f"the directory of specified path is not existing: {path_dir}.") - download_and_extract( - url=schema_url, - filepath=filepath, - output_dir=path_dir, - hash_val=hash_val, - hash_type="md5", - progress=True, - ) + download_url(url=schema_url, filepath=filepath, hash_val=hash_val, hash_type="md5", progress=True) # FIXME: will update to use `load_config_file()` when PR 3832 is merged with open(filepath) as f: diff --git a/monai/apps/manifest/verify_meta.py b/monai/apps/manifest/verify_meta.py index b3ea17ead4..9997df5403 100644 --- a/monai/apps/manifest/verify_meta.py +++ b/monai/apps/manifest/verify_meta.py @@ -20,7 +20,7 @@ def verify(): parser.add_argument("--metadata", "-m", type=str, help="filepath of the metadata file.", required=True) parser.add_argument("--schema_url", "-u", type=str, help="filepath of the config file.", required=True) parser.add_argument("--filepath", "-f", type=str, help="filepath to store downloaded schema.", required=True) - parser.add_argument("--hash_val", "-h", type=str, help="MD5 hash value to verify schema file.", required=False) + parser.add_argument("--hash_val", "-v", type=str, help="MD5 hash value to verify schema file.", required=False) args = parser.parse_args() verify_metadata( diff --git a/tests/test_manifest_verify_meta.py b/tests/test_manifest_verify_meta.py index b98455db27..addbca365b 100644 --- a/tests/test_manifest_verify_meta.py +++ b/tests/test_manifest_verify_meta.py @@ -10,19 +10,12 @@ # limitations under the License. import json -import logging import os -import sys import tempfile import unittest -import nibabel as nib -import numpy as np -import yaml from parameterized import parameterized -from monai.transforms import LoadImage - TEST_CASE_1 = [ { "version": "0.1.0", From 9ed45df872dc030f9555fef2843c15ebe75c80a3 Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Mon, 28 Feb 2022 18:46:28 +0800 Subject: [PATCH 04/24] [DLMED] add more tests Signed-off-by: Nic Ma --- monai/apps/manifest/metadata.json | 1 - monai/apps/manifest/utils.py | 44 +++++++++++++++++++++++------- monai/apps/manifest/verify_meta.py | 2 ++ tests/test_manifest_verify_meta.py | 2 +- 4 files changed, 37 insertions(+), 12 deletions(-) diff --git a/monai/apps/manifest/metadata.json b/monai/apps/manifest/metadata.json index 318a8739ac..506558247b 100644 --- a/monai/apps/manifest/metadata.json +++ b/monai/apps/manifest/metadata.json @@ -1,6 +1,5 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "$id": "https://monai.io/metadata_schema.json", "type": "object", "properties": { "version": { diff --git a/monai/apps/manifest/utils.py b/monai/apps/manifest/utils.py index 1b918ce77f..3cc906dbd7 100644 --- a/monai/apps/manifest/utils.py +++ b/monai/apps/manifest/utils.py @@ -11,30 +11,54 @@ import json from pathlib import Path -from typing import Dict, Optional +from typing import Dict, Optional, Union from monai.apps.utils import download_url from monai.config import PathLike from monai.utils import optional_import validate, _ = optional_import("jsonschema", name="validate") +ValidationError, _ = optional_import("jsonschema.exceptions", name="ValidationError") def verify_metadata( - metadata: Dict, schema_url: str, filepath: PathLike, create_dir: bool = True, hash_val: Optional[str] = None + metadata: Union[Dict, str], + schema_url: str, + filepath: PathLike, + create_dir: bool = True, + hash_val: Optional[str] = None, + result_path: Optional[PathLike] = None, + **kwargs, ): - filepath = Path(filepath) - path_dir = filepath.parent - if not path_dir.exists(): - if create_dir: - path_dir.mkdir(parents=True) - else: - raise ValueError(f"the directory of specified path is not existing: {path_dir}.") + """ + For more details: https://python-jsonschema.readthedocs.io/en/stable/validate/#jsonschema.validate. + """ + def _check_dir(path: Path): + path_dir = path.parent + if not path_dir.exists(): + if create_dir: + path_dir.mkdir(parents=True) + else: + raise ValueError(f"the directory of specified path is not existing: {path_dir}.") + filepath = Path(filepath) + _check_dir(path=filepath) download_url(url=schema_url, filepath=filepath, hash_val=hash_val, hash_type="md5", progress=True) # FIXME: will update to use `load_config_file()` when PR 3832 is merged with open(filepath) as f: schema = json.load(f) + meta = metadata + if isinstance(metadata, str): + with open(metadata) as f: + meta = json.load(f) - validate(instance=metadata, schema=schema) + try: + validate(instance=meta, schema=schema, **kwargs) + except ValidationError as e: + if result_path is not None: + result_path = Path(result_path) + _check_dir(result_path) + with open(result_path, "w") as f: + f.write(str(e)) + raise e diff --git a/monai/apps/manifest/verify_meta.py b/monai/apps/manifest/verify_meta.py index 9997df5403..1e7d6ce66e 100644 --- a/monai/apps/manifest/verify_meta.py +++ b/monai/apps/manifest/verify_meta.py @@ -20,6 +20,7 @@ def verify(): parser.add_argument("--metadata", "-m", type=str, help="filepath of the metadata file.", required=True) parser.add_argument("--schema_url", "-u", type=str, help="filepath of the config file.", required=True) parser.add_argument("--filepath", "-f", type=str, help="filepath to store downloaded schema.", required=True) + parser.add_argument("--result_path", "-r", type=str, help="filepath to store verification result.", required=False) parser.add_argument("--hash_val", "-v", type=str, help="MD5 hash value to verify schema file.", required=False) args = parser.parse_args() @@ -28,6 +29,7 @@ def verify(): schema_url=args.schema_url, filepath=args.filepath, create_dir=True, + result_path=args.result_path, hash_val=args.hash_val, ) diff --git a/tests/test_manifest_verify_meta.py b/tests/test_manifest_verify_meta.py index addbca365b..16c68006a4 100644 --- a/tests/test_manifest_verify_meta.py +++ b/tests/test_manifest_verify_meta.py @@ -111,7 +111,7 @@ def test_verify(self, meta_data): with open(metafile, "w") as f: json.dump(meta_data, f) - os.system(f"python -m monai.apps.manifest.verify_meta -m {metafile} -u fsdfsfs -f {filepath}") + os.system(f"python -m monai.apps.manifest.verify_meta -m {metafile} -u fsdfsfs -f {filepath}") if __name__ == "__main__": From 2cf9d3a7ec393bd3faa3ad63ad0d6f60f0943786 Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Mon, 28 Feb 2022 20:09:18 +0800 Subject: [PATCH 05/24] [DLMED] add optional import Signed-off-by: Nic Ma --- docs/requirements.txt | 1 + docs/source/installation.md | 5 ++--- environment-dev.yml | 1 + monai/apps/manifest/utils.py | 5 ++--- requirements-dev.txt | 1 + setup.cfg | 3 +++ tests/min_tests.py | 1 + tests/test_manifest_verify_meta.py | 12 +++++++++++- 8 files changed, 22 insertions(+), 7 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index dcf5ef5b2a..40143344fc 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -25,3 +25,4 @@ mlflow tensorboardX imagecodecs; platform_system == "Linux" tifffile; platform_system == "Linux" +jsonschema diff --git a/docs/source/installation.md b/docs/source/installation.md index 15c372c385..58c3859880 100644 --- a/docs/source/installation.md +++ b/docs/source/installation.md @@ -190,10 +190,9 @@ Since MONAI v0.2.0, the extras syntax such as `pip install 'monai[nibabel]'` is - The options are ``` -[nibabel, skimage, pillow, tensorboard, gdown, ignite, torchvision, itk, tqdm, lmdb, psutil, cucim, openslide, pandas, einops, transformers, mlflow, matplotlib, tensorboardX, tifffile, imagecodecs] +[nibabel, skimage, pillow, tensorboard, gdown, ignite, torchvision, itk, tqdm, lmdb, psutil, cucim, openslide, pandas, einops, transformers, mlflow, matplotlib, tensorboardX, tifffile, imagecodecs, jsonschema] ``` which correspond to `nibabel`, `scikit-image`, `pillow`, `tensorboard`, -`gdown`, `pytorch-ignite`, `torchvision`, `itk`, `tqdm`, `lmdb`, `psutil`, `cucim`, `openslide-python`, `pandas`, `einops`, `transformers`, `mlflow`, `matplotlib`, `tensorboardX`, -`tifffile`, `imagecodecs`, respectively. +`gdown`, `pytorch-ignite`, `torchvision`, `itk`, `tqdm`, `lmdb`, `psutil`, `cucim`, `openslide-python`, `pandas`, `einops`, `transformers`, `mlflow`, `matplotlib`, `tensorboardX`, `tifffile`, `imagecodecs`, `jsonschema`, respectively. - `pip install 'monai[all]'` installs all the optional dependencies. diff --git a/environment-dev.yml b/environment-dev.yml index ae41f21f1f..0060f31822 100644 --- a/environment-dev.yml +++ b/environment-dev.yml @@ -42,6 +42,7 @@ dependencies: - transformers - mlflow - tensorboardX + - jsonschema - pip - pip: # pip for itk as conda-forge version only up to v5.1 diff --git a/monai/apps/manifest/utils.py b/monai/apps/manifest/utils.py index 3cc906dbd7..b930d2ada1 100644 --- a/monai/apps/manifest/utils.py +++ b/monai/apps/manifest/utils.py @@ -48,13 +48,12 @@ def _check_dir(path: Path): # FIXME: will update to use `load_config_file()` when PR 3832 is merged with open(filepath) as f: schema = json.load(f) - meta = metadata if isinstance(metadata, str): with open(metadata) as f: - meta = json.load(f) + metadata = json.load(f) try: - validate(instance=meta, schema=schema, **kwargs) + validate(instance=metadata, schema=schema, **kwargs) except ValidationError as e: if result_path is not None: result_path = Path(result_path) diff --git a/requirements-dev.txt b/requirements-dev.txt index eaf363fbe4..61cc0c8a81 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -43,3 +43,4 @@ transformers mlflow matplotlib!=3.5.0 tensorboardX +jsonschema diff --git a/setup.cfg b/setup.cfg index a9cfa09ccc..ddbf7e7272 100644 --- a/setup.cfg +++ b/setup.cfg @@ -50,6 +50,7 @@ all = mlflow matplotlib tensorboardX + jsonschema nibabel = nibabel skimage = @@ -92,6 +93,8 @@ matplotlib = matplotlib tensorboardX = tensorboardX +jsonschema = + jsonschema [flake8] select = B,C,E,F,N,P,T4,W,B9 diff --git a/tests/min_tests.py b/tests/min_tests.py index e0710a93ec..de60fbb6f8 100644 --- a/tests/min_tests.py +++ b/tests/min_tests.py @@ -159,6 +159,7 @@ def run_testsuit(): "test_zoomd", "test_prepare_batch_default_dist", "test_parallel_execution_dist", + "test_manifest_verify_meta" ] assert sorted(exclude_cases) == sorted(set(exclude_cases)), f"Duplicated items in {exclude_cases}" diff --git a/tests/test_manifest_verify_meta.py b/tests/test_manifest_verify_meta.py index 16c68006a4..24468d3edc 100644 --- a/tests/test_manifest_verify_meta.py +++ b/tests/test_manifest_verify_meta.py @@ -10,7 +10,9 @@ # limitations under the License. import json +import logging import os +import sys import tempfile import unittest @@ -103,6 +105,7 @@ class TestVerifyMeta(unittest.TestCase): @parameterized.expand([TEST_CASE_1]) def test_verify(self, meta_data): + logging.basicConfig(stream=sys.stdout, level=logging.INFO) with tempfile.TemporaryDirectory() as tempdir: #filepath = os.path.join(tempdir, "schema.json") filepath = "/workspace/data/medical/MONAI/monai/apps/manifest/metadata.json" @@ -110,8 +113,15 @@ def test_verify(self, meta_data): metafile = os.path.join(tempdir, "metadata.json") with open(metafile, "w") as f: json.dump(meta_data, f) + resultfile = os.path.join(tempdir, "results.txt") + hash_val = "486c581cca90293d1a7f41a57991ff35" - os.system(f"python -m monai.apps.manifest.verify_meta -m {metafile} -u fsdfsfs -f {filepath}") + ret = os.system( + f"python -m monai.apps.manifest.verify_meta -m {metafile} -u fsdfsfs" + f" -f {filepath} -r {resultfile} -v {hash_val}" + ) + self.assertEqual(ret, 0) + self.assertFalse(os.path.exists(resultfile)) if __name__ == "__main__": From d3801eef0a44e5b10c197856d329b81856f15329 Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Mon, 28 Feb 2022 21:12:04 +0800 Subject: [PATCH 06/24] [DLMED] test with download link Signed-off-by: Nic Ma --- monai/._.DS_Store | Bin 0 -> 4096 bytes monai/apps/._.DS_Store | Bin 0 -> 4096 bytes monai/apps/manifest/metadata.json | 245 ----------------------------- tests/test_manifest_verify_meta.py | 13 +- 4 files changed, 8 insertions(+), 250 deletions(-) create mode 100644 monai/._.DS_Store create mode 100644 monai/apps/._.DS_Store delete mode 100644 monai/apps/manifest/metadata.json diff --git a/monai/._.DS_Store b/monai/._.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..ab257e59d349be9e90547f9ad53744c255a9e23d GIT binary patch literal 4096 zcmZQz6=P>$Vqox1Ojhs@R)|o50+1L3ClDJkFz{^v(m+1nBL)UWIhYCu0iY;W;207T z#K6FM38I6c0;{4?!O;*H4S~@R7!85Z5Eu=C(GVC7fzc2c4S~@R7!85Z5TJ4hFapg3 zVK9&j$;d2LC`v8PFD*(=RY=P(%2vqCD@n~O$;{77%*m-#$Vp8rQAo;3%*zILb)mY3 QG==JaxL0Ht$Vqox1Ojhs@R)|o50+1L3ClDJkFz{^v(m+1nBL)UWIhYCu0iY;W;207T z#K6FM0iuJU0;{4?!O;*H4S~@R7!85Z5Eu=C(GVC7fzc2c4S~@R7!85Z5TJ4hFapg3 zVK9&j$;d2LC`v8PFD*(=RY=P(%2vqCD@n~O$;{77%*m-#$Vp8rQAo;3%*zILb)mY3 QG==JaxL0Ht Date: Mon, 28 Feb 2022 22:06:29 +0800 Subject: [PATCH 07/24] [DLMED] enhance doc-string Signed-off-by: Nic Ma --- monai/apps/manifest/utils.py | 16 ++++++++-- monai/apps/manifest/verify_meta.py | 7 ++++- tests/min_tests.py | 2 +- tests/test_manifest_verify_meta.py | 50 +++++++----------------------- 4 files changed, 33 insertions(+), 42 deletions(-) diff --git a/monai/apps/manifest/utils.py b/monai/apps/manifest/utils.py index b930d2ada1..db4c8e6404 100644 --- a/monai/apps/manifest/utils.py +++ b/monai/apps/manifest/utils.py @@ -25,14 +25,26 @@ def verify_metadata( metadata: Union[Dict, str], schema_url: str, filepath: PathLike, + result_path: Optional[PathLike] = None, create_dir: bool = True, hash_val: Optional[str] = None, - result_path: Optional[PathLike] = None, **kwargs, ): """ - For more details: https://python-jsonschema.readthedocs.io/en/stable/validate/#jsonschema.validate. + Verify the provided metadata dictonary or file based on the predefined schema. + + Args: + metadata: source meta data to verify. + schema_url: URL to download the expected schema file. + filepath: file path to store the downloaded schema. + result_path: if not None, save the validation error into result file. + create_dir: whether to create directories if not existing. + hash_val: if not None, define the hash value to verify the downloaded schema file. + kwargs: other arguments for `jsonschema.validate()`. for more details: + https://python-jsonschema.readthedocs.io/en/stable/validate/#jsonschema.validate. + """ + def _check_dir(path: Path): path_dir = path.parent if not path_dir.exists(): diff --git a/monai/apps/manifest/verify_meta.py b/monai/apps/manifest/verify_meta.py index 1e7d6ce66e..8639c506a8 100644 --- a/monai/apps/manifest/verify_meta.py +++ b/monai/apps/manifest/verify_meta.py @@ -16,6 +16,11 @@ def verify(): + """ + Verify the provided `metadata` file based on the predefined `schema`. + The schema standard follows: http://json-schema.org/. + + """ parser = argparse.ArgumentParser() parser.add_argument("--metadata", "-m", type=str, help="filepath of the metadata file.", required=True) parser.add_argument("--schema_url", "-u", type=str, help="filepath of the config file.", required=True) @@ -28,8 +33,8 @@ def verify(): metadata=args.metadata, schema_url=args.schema_url, filepath=args.filepath, - create_dir=True, result_path=args.result_path, + create_dir=True, hash_val=args.hash_val, ) diff --git a/tests/min_tests.py b/tests/min_tests.py index de60fbb6f8..78d06c9f54 100644 --- a/tests/min_tests.py +++ b/tests/min_tests.py @@ -159,7 +159,7 @@ def run_testsuit(): "test_zoomd", "test_prepare_batch_default_dist", "test_parallel_execution_dist", - "test_manifest_verify_meta" + "test_manifest_verify_meta", ] assert sorted(exclude_cases) == sorted(set(exclude_cases)), f"Duplicated items in {exclude_cases}" diff --git a/tests/test_manifest_verify_meta.py b/tests/test_manifest_verify_meta.py index bf922ae738..a6b23a6ffa 100644 --- a/tests/test_manifest_verify_meta.py +++ b/tests/test_manifest_verify_meta.py @@ -21,16 +21,11 @@ TEST_CASE_1 = [ { "version": "0.1.0", - "changelog": { - "0.1.0": "complete the model package", - "0.0.1": "initialize the model package structure" - }, + "changelog": {"0.1.0": "complete the model package", "0.0.1": "initialize the model package structure"}, "monai_version": "0.8.0", "pytorch_version": "1.10.0", "numpy_version": "1.21.2", - "optional_packages_version": { - "nibabel": "3.2.1" - }, + "optional_packages_version": {"nibabel": "3.2.1"}, "task": "Decathlon spleen segmentation", "description": "A pre-trained model for volumetric (3D) segmentation of the spleen from CT image", "authorship": "MONAI team", @@ -41,9 +36,7 @@ "image_classes": "single channel data, intensity scaled to [0, 1]", "label_classes": "single channel data, 1 is spleen, 0 is everything else", "pred_classes": "2 channels OneHot data, channel 1 is spleen, channel 0 is background", - "eval_metrics": { - "mean_dice": 0.96 - }, + "eval_metrics": {"mean_dice": 0.96}, "intended_use": "This is an example, not to be used for diagnostic purposes", "references": [ "Xia, Yingda, et al. '3D Semi-Supervised Learning with Uncertainty-Aware Multi-View Co-Training.'" @@ -51,7 +44,7 @@ "Kerfoot E., Clough J., Oksuz I., Lee J., King A.P., Schnabel J.A. (2019)" " Left-Ventricle Quantification Using Residual U-Net. In: Pop M. et al. (eds) Statistical Atlases" " and Computational Models of the Heart. Atrial Segmentation and LV Quantification Challenges. STACOM 2018." - " Lecture Notes in Computer Science, vol 11395. Springer, Cham. https://doi.org/10.1007/978-3-030-12029-0_40" + " Lecture Notes in Computer Science, vol 11395. Springer, Cham. https://doi.org/10.1007/978-3-030-12029-0_40", ], "network_data_format": { "inputs": { @@ -59,20 +52,11 @@ "type": "image", "format": "magnitude", "num_channels": 1, - "spatial_shape": [ - 160, - 160, - 160 - ], + "spatial_shape": [160, 160, 160], "dtype": "float32", - "value_range": [ - 0, - 1 - ], + "value_range": [0, 1], "is_patch_data": False, - "channel_def": { - "0": "image" - } + "channel_def": {"0": "image"}, } }, "outputs": { @@ -80,24 +64,14 @@ "type": "image", "format": "segmentation", "num_channels": 2, - "spatial_shape": [ - 160, - 160, - 160 - ], + "spatial_shape": [160, 160, 160], "dtype": "float32", - "value_range": [ - 0, - 1 - ], + "value_range": [0, 1], "is_patch_data": False, - "channel_def": { - "0": "background", - "1": "spleen" - } + "channel_def": {"0": "background", "1": "spleen"}, } - } - } + }, + }, } ] From c77888adba5252695e1cb53f74e0773b6488f4b9 Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Mon, 28 Feb 2022 23:27:32 +0800 Subject: [PATCH 08/24] [DLMED] fix mypy Signed-off-by: Nic Ma --- monai/apps/manifest/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monai/apps/manifest/utils.py b/monai/apps/manifest/utils.py index db4c8e6404..8f320b6e2b 100644 --- a/monai/apps/manifest/utils.py +++ b/monai/apps/manifest/utils.py @@ -72,4 +72,4 @@ def _check_dir(path: Path): _check_dir(result_path) with open(result_path, "w") as f: f.write(str(e)) - raise e + raise ValueError("detected content with incorrect format in the meta data.") from e From eccc3ab5da6216434cca23e056fb5ffc5ed915c3 Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Mon, 7 Mar 2022 12:27:20 +0800 Subject: [PATCH 09/24] [DLMED] remove DS_Store Signed-off-by: Nic Ma --- monai/._.DS_Store | Bin 4096 -> 0 bytes monai/apps/._.DS_Store | Bin 4096 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 monai/._.DS_Store delete mode 100644 monai/apps/._.DS_Store diff --git a/monai/._.DS_Store b/monai/._.DS_Store deleted file mode 100644 index ab257e59d349be9e90547f9ad53744c255a9e23d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4096 zcmZQz6=P>$Vqox1Ojhs@R)|o50+1L3ClDJkFz{^v(m+1nBL)UWIhYCu0iY;W;207T z#K6FM38I6c0;{4?!O;*H4S~@R7!85Z5Eu=C(GVC7fzc2c4S~@R7!85Z5TJ4hFapg3 zVK9&j$;d2LC`v8PFD*(=RY=P(%2vqCD@n~O$;{77%*m-#$Vp8rQAo;3%*zILb)mY3 QG==JaxL0Ht$Vqox1Ojhs@R)|o50+1L3ClDJkFz{^v(m+1nBL)UWIhYCu0iY;W;207T z#K6FM0iuJU0;{4?!O;*H4S~@R7!85Z5Eu=C(GVC7fzc2c4S~@R7!85Z5TJ4hFapg3 zVK9&j$;d2LC`v8PFD*(=RY=P(%2vqCD@n~O$;{77%*m-#$Vp8rQAo;3%*zILb)mY3 QG==JaxL0Ht Date: Wed, 9 Mar 2022 17:44:05 +0800 Subject: [PATCH 10/24] [DLMED] update according to comments Signed-off-by: Nic Ma --- monai/bundle/utils.py | 4 +- tests/test_bundle_verify_meta.py | 89 ++++++++------------------------ tests/testing_data/metadata.json | 76 +++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 69 deletions(-) create mode 100644 tests/testing_data/metadata.json diff --git a/monai/bundle/utils.py b/monai/bundle/utils.py index 8f320b6e2b..5da21c87fd 100644 --- a/monai/bundle/utils.py +++ b/monai/bundle/utils.py @@ -51,7 +51,7 @@ def _check_dir(path: Path): if create_dir: path_dir.mkdir(parents=True) else: - raise ValueError(f"the directory of specified path is not existing: {path_dir}.") + raise ValueError(f"the directory of specified path `{path_dir}` does not exist.") filepath = Path(filepath) _check_dir(path=filepath) @@ -72,4 +72,4 @@ def _check_dir(path: Path): _check_dir(result_path) with open(result_path, "w") as f: f.write(str(e)) - raise ValueError("detected content with incorrect format in the meta data.") from e + raise ValueError(f"metadata failed to validate against schema `{schema_url}`.") from e diff --git a/tests/test_bundle_verify_meta.py b/tests/test_bundle_verify_meta.py index a9d03f299f..cfbdbb5d1d 100644 --- a/tests/test_bundle_verify_meta.py +++ b/tests/test_bundle_verify_meta.py @@ -12,94 +12,49 @@ import json import logging import os +import subprocess import sys import tempfile import unittest from parameterized import parameterized -TEST_CASE_1 = [ - { - "version": "0.1.0", - "changelog": {"0.1.0": "complete the model package", "0.0.1": "initialize the model package structure"}, - "monai_version": "0.8.0", - "pytorch_version": "1.10.0", - "numpy_version": "1.21.2", - "optional_packages_version": {"nibabel": "3.2.1"}, - "task": "Decathlon spleen segmentation", - "description": "A pre-trained model for volumetric (3D) segmentation of the spleen from CT image", - "authorship": "MONAI team", - "copyright": "Copyright (c) MONAI Consortium", - "data_source": "Task09_Spleen.tar from http://medicaldecathlon.com/", - "data_type": "dicom", - "dataset_dir": "/workspace/data/Task09_Spleen", - "image_classes": "single channel data, intensity scaled to [0, 1]", - "label_classes": "single channel data, 1 is spleen, 0 is everything else", - "pred_classes": "2 channels OneHot data, channel 1 is spleen, channel 0 is background", - "eval_metrics": {"mean_dice": 0.96}, - "intended_use": "This is an example, not to be used for diagnostic purposes", - "references": [ - "Xia, Yingda, et al. '3D Semi-Supervised Learning with Uncertainty-Aware Multi-View Co-Training.'" - " arXiv preprint arXiv:1811.12506 (2018). https://arxiv.org/abs/1811.12506.", - "Kerfoot E., Clough J., Oksuz I., Lee J., King A.P., Schnabel J.A. (2019)" - " Left-Ventricle Quantification Using Residual U-Net. In: Pop M. et al. (eds) Statistical Atlases" - " and Computational Models of the Heart. Atrial Segmentation and LV Quantification Challenges. STACOM 2018." - " Lecture Notes in Computer Science, vol 11395. Springer, Cham. https://doi.org/10.1007/978-3-030-12029-0_40", - ], - "network_data_format": { - "inputs": { - "image": { - "type": "image", - "format": "magnitude", - "num_channels": 1, - "spatial_shape": [160, 160, 160], - "dtype": "float32", - "value_range": [0, 1], - "is_patch_data": False, - "channel_def": {"0": "image"}, - } - }, - "outputs": { - "pred": { - "type": "image", - "format": "segmentation", - "num_channels": 2, - "spatial_shape": [160, 160, 160], - "dtype": "float32", - "value_range": [0, 1], - "is_patch_data": False, - "channel_def": {"0": "background", "1": "spleen"}, - } - }, - }, - } -] +TEST_CASE_1 = [os.path.join(os.path.dirname(__file__), "testing_data", "metadata.json")] + +url = "https://github.com/Project-MONAI/MONAI-extra-test-data/releases/download/0.8.1/" "meta_schema_202202281232.json" class TestVerifyMeta(unittest.TestCase): @parameterized.expand([TEST_CASE_1]) - def test_verify(self, meta_data): + def test_verify(self, metafile): logging.basicConfig(stream=sys.stdout, level=logging.INFO) with tempfile.TemporaryDirectory() as tempdir: filepath = os.path.join(tempdir, "schema.json") - metafile = os.path.join(tempdir, "metadata.json") - with open(metafile, "w") as f: - json.dump(meta_data, f) - - url = ( - "https://github.com/Project-MONAI/MONAI-extra-test-data/releases/download/0.8.1/" - "meta_schema_202202281232.json" - ) resultfile = os.path.join(tempdir, "results.txt") hash_val = "486c581cca90293d1a7f41a57991ff35" - ret = os.system( - f"python -m monai.bundle.verify_meta -m {metafile} -u {url} -f {filepath}" + cmd = ( + f"{sys.executable} -m monai.bundle.verify_meta -m {metafile} -u {url} -f {filepath}" f" -r {resultfile} -v {hash_val}" ) + ret = subprocess.check_call(cmd.split(" ")) self.assertEqual(ret, 0) self.assertFalse(os.path.exists(resultfile)) + def test_verify_error(self): + logging.basicConfig(stream=sys.stdout, level=logging.INFO) + with tempfile.TemporaryDirectory() as tempdir: + filepath = os.path.join(tempdir, "schema.json") + metafile = os.path.join(tempdir, "metadata.json") + with open(metafile, "w") as f: + json.dump({"wrong_meta": "wrong content"}, f) + resultfile = os.path.join(tempdir, "results.txt") + + cmd = f"{sys.executable} -m monai.bundle.verify_meta -m {metafile} -u {url} -f {filepath} -r {resultfile}" + with self.assertRaises(subprocess.CalledProcessError): + subprocess.check_call(cmd.split(" ")) + self.assertTrue(os.path.exists(resultfile)) + if __name__ == "__main__": unittest.main() diff --git a/tests/testing_data/metadata.json b/tests/testing_data/metadata.json new file mode 100644 index 0000000000..4430b9bde2 --- /dev/null +++ b/tests/testing_data/metadata.json @@ -0,0 +1,76 @@ +{ + "version": "0.1.0", + "changelog": { + "0.1.0": "complete the model package", + "0.0.1": "initialize the model package structure" + }, + "monai_version": "0.8.0", + "pytorch_version": "1.10.0", + "numpy_version": "1.21.2", + "optional_packages_version": { + "nibabel": "3.2.1" + }, + "task": "Decathlon spleen segmentation", + "description": "A pre-trained model for volumetric (3D) segmentation of the spleen from CT image", + "authorship": "MONAI team", + "copyright": "Copyright (c) MONAI Consortium", + "data_source": "Task09_Spleen.tar from http://medicaldecathlon.com/", + "data_type": "dicom", + "dataset_dir": "/workspace/data/Task09_Spleen", + "image_classes": "single channel data, intensity scaled to [0, 1]", + "label_classes": "single channel data, 1 is spleen, 0 is everything else", + "pred_classes": "2 channels OneHot data, channel 1 is spleen, channel 0 is background", + "eval_metrics": { + "mean_dice": 0.96 + }, + "intended_use": "This is an example, not to be used for diagnostic purposes", + "references": [ + "Xia, Yingda, et al. '3D Semi-Supervised Learning with Uncertainty-Aware Multi-View Co-Training. arXiv preprint arXiv:1811.12506 (2018). https://arxiv.org/abs/1811.12506.", + "Kerfoot E., Clough J., Oksuz I., Lee J., King A.P., Schnabel J.A. (2019) Left-Ventricle Quantification Using Residual U-Net. In: Pop M. et al. (eds) Statistical Atlases and Computational Models of the Heart. Atrial Segmentation and LV Quantification Challenges. STACOM 2018. Lecture Notes in Computer Science, vol 11395. Springer, Cham. https://doi.org/10.1007/978-3-030-12029-0_40" + ], + "network_data_format": { + "inputs": { + "image": { + "type": "image", + "format": "magnitude", + "num_channels": 1, + "spatial_shape": [ + 160, + 160, + 160 + ], + "dtype": "float32", + "value_range": [ + 0, + 1 + ], + "is_patch_data": false, + "channel_def": { + "0": "image" + } + } + }, + "outputs": { + "pred": { + "type": "image", + "format": "segmentation", + "num_channels": 2, + "spatial_shape": [ + 160, + 160, + 160 + ], + "dtype": "float32", + "value_range": [ + 0, + 1 + ], + "is_patch_data": false, + "channel_def": { + "0": "background", + "1": "spleen" + } + } + } + } +} From 608612d2c0ea7718a73cd9f872ab5cec4a62c5a7 Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Fri, 11 Mar 2022 19:42:17 +0800 Subject: [PATCH 11/24] [DLMED] update according to comments Signed-off-by: Nic Ma --- monai/bundle/__main__.py | 2 +- monai/bundle/scripts.py | 68 ++++++++++++++++++- monai/bundle/utils.py | 65 ------------------ monai/bundle/verify_meta.py | 43 ------------ monai/utils/__init__.py | 1 + monai/utils/misc.py | 27 ++++++-- tests/min_tests.py | 2 +- ...meta.py => test_bundle_verify_metadata.py} | 41 ++++++++--- 8 files changed, 124 insertions(+), 125 deletions(-) delete mode 100644 monai/bundle/verify_meta.py rename tests/{test_bundle_verify_meta.py => test_bundle_verify_metadata.py} (67%) diff --git a/monai/bundle/__main__.py b/monai/bundle/__main__.py index 7a87030bec..45cd89bfdd 100644 --- a/monai/bundle/__main__.py +++ b/monai/bundle/__main__.py @@ -10,7 +10,7 @@ # limitations under the License. -from monai.bundle.scripts import run +from monai.bundle.scripts import run, verify_metadata if __name__ == "__main__": from monai.utils import optional_import diff --git a/monai/bundle/scripts.py b/monai/bundle/scripts.py index ebfd3e54ac..2ee9679cf0 100644 --- a/monai/bundle/scripts.py +++ b/monai/bundle/scripts.py @@ -12,7 +12,13 @@ import pprint from typing import Dict, Optional, Sequence, Union +from monai.apps.utils import download_url from monai.bundle.config_parser import ConfigParser +from monai.config import PathLike +from monai.utils import optional_import, verify_parent_dir + +validate, _ = optional_import("jsonschema", name="validate") +ValidationError, _ = optional_import("jsonschema.exceptions", name="ValidationError") def _update_default_args(args: Optional[Union[str, Dict]] = None, **kwargs) -> Dict: @@ -102,7 +108,7 @@ def run( parser.read_meta(f=_args.pop("meta_file")) id = _args.pop("target_id", "") - # the rest key-values in the args are to override config content + # the rest key-values in the _args are to override config content for k, v in _args.items(): parser[k] = v @@ -110,3 +116,63 @@ def run( if not hasattr(workflow, "run"): raise ValueError(f"The parsed workflow {type(workflow)} does not have a `run` method.\n{run.__doc__}") workflow.run() + + +def verify_metadata( + meta_file: Optional[Union[str, Sequence[str]]] = None, + schema_url: Optional[str] = None, + filepath: Optional[PathLike] = None, + result_path: Optional[PathLike] = None, + create_dir: Optional[bool] = None, + hash_val: Optional[str] = None, + args_file: Optional[str] = None, + **kwargs, +): + """ + Verify the provided `metadata` file based on the predefined `schema`. + The schema standard follows: http://json-schema.org/. + + Args: + meta_file: filepath of the metadata file to verify, if `None`, must be provided in `args_file`. + if it is a list of file paths, the content of them will be merged. + schema_url: URL to download the expected schema file. + filepath: file path to store the downloaded schema. + result_path: if not None, save the validation error into the result file. + create_dir: whether to create directories if not existing, default to `True`. + hash_val: if not None, define the hash value to verify the downloaded schema file. + args_file: a JSON or YAML file to provide default values for all the args in this function. + so that the command line inputs can be simplified. + kwargs: other arguments for `jsonschema.validate()`. for more details: + https://python-jsonschema.readthedocs.io/en/stable/validate/#jsonschema.validate. + + """ + + k_v = zip( + ["meta_file", "schema_url", "filepath", "result_path", "create_dir", "hash_val"], + [meta_file, schema_url, filepath, result_path, create_dir, hash_val], + ) + for k, v in k_v: + if v is not None: + kwargs[k] = v + _args = _update_default_args(args=args_file, **kwargs) + + filepath_ = _args.pop("filepath") + create_dir_ = _args.pop("create_dir", True) + verify_parent_dir(path=filepath_, create_dir=create_dir_) + url_ = _args.pop("schema_url", None) + download_url(url=url_, filepath=filepath_, hash_val=_args.pop("hash_val", None), hash_type="md5", progress=True) + + schema = ConfigParser.load_config_file(filepath=filepath_) + + metadata = ConfigParser.load_config_files(files=_args.pop("meta_file")) + result_path_ = _args.pop("result_path", None) + + try: + # the rest key-values in the _args are for `validate` API + validate(instance=metadata, schema=schema, **_args) + except ValidationError as e: + if result_path_ is not None: + verify_parent_dir(result_path_, create_dir=create_dir_) + with open(result_path_, "w") as f: + f.write(str(e)) + raise ValueError(f"metadata failed to validate against schema `{url_}`.") from e diff --git a/monai/bundle/utils.py b/monai/bundle/utils.py index 1923be6ab0..ba5c2729e7 100644 --- a/monai/bundle/utils.py +++ b/monai/bundle/utils.py @@ -9,17 +9,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json -from pathlib import Path -from typing import Dict, Optional, Union - -from monai.apps.utils import download_url -from monai.config import PathLike -from monai.utils import optional_import - -validate, _ = optional_import("jsonschema", name="validate") -ValidationError, _ = optional_import("jsonschema.exceptions", name="ValidationError") - __all__ = ["ID_REF_KEY", "ID_SEP_KEY", "EXPR_KEY", "MACRO_KEY"] @@ -27,57 +16,3 @@ ID_SEP_KEY = "#" # separator for the ID of a ConfigItem EXPR_KEY = "$" # start of a ConfigExpression MACRO_KEY = "%" # start of a macro of a config - - -def verify_metadata( - metadata: Union[Dict, str], - schema_url: str, - filepath: PathLike, - result_path: Optional[PathLike] = None, - create_dir: bool = True, - hash_val: Optional[str] = None, - **kwargs, -): - """ - Verify the provided metadata dictonary or file based on the predefined schema. - - Args: - metadata: source meta data to verify. - schema_url: URL to download the expected schema file. - filepath: file path to store the downloaded schema. - result_path: if not None, save the validation error into result file. - create_dir: whether to create directories if not existing. - hash_val: if not None, define the hash value to verify the downloaded schema file. - kwargs: other arguments for `jsonschema.validate()`. for more details: - https://python-jsonschema.readthedocs.io/en/stable/validate/#jsonschema.validate. - - """ - - def _check_dir(path: Path): - path_dir = path.parent - if not path_dir.exists(): - if create_dir: - path_dir.mkdir(parents=True) - else: - raise ValueError(f"the directory of specified path `{path_dir}` does not exist.") - - filepath = Path(filepath) - _check_dir(path=filepath) - download_url(url=schema_url, filepath=filepath, hash_val=hash_val, hash_type="md5", progress=True) - - # FIXME: will update to use `load_config_file()` when PR 3832 is merged - with open(filepath) as f: - schema = json.load(f) - if isinstance(metadata, str): - with open(metadata) as f: - metadata = json.load(f) - - try: - validate(instance=metadata, schema=schema, **kwargs) - except ValidationError as e: - if result_path is not None: - result_path = Path(result_path) - _check_dir(result_path) - with open(result_path, "w") as f: - f.write(str(e)) - raise ValueError(f"metadata failed to validate against schema `{schema_url}`.") from e diff --git a/monai/bundle/verify_meta.py b/monai/bundle/verify_meta.py deleted file mode 100644 index 1e589fb851..0000000000 --- a/monai/bundle/verify_meta.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright (c) MONAI Consortium -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# http://www.apache.org/licenses/LICENSE-2.0 -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import argparse - -from monai.bundle.utils import verify_metadata - - -def verify(): - """ - Verify the provided `metadata` file based on the predefined `schema`. - The schema standard follows: http://json-schema.org/. - - """ - parser = argparse.ArgumentParser() - parser.add_argument("--metadata", "-m", type=str, help="filepath of the metadata file.", required=True) - parser.add_argument("--schema_url", "-u", type=str, help="filepath of the config file.", required=True) - parser.add_argument("--filepath", "-f", type=str, help="filepath to store downloaded schema.", required=True) - parser.add_argument("--result_path", "-r", type=str, help="filepath to store verification result.", required=False) - parser.add_argument("--hash_val", "-v", type=str, help="MD5 hash value to verify schema file.", required=False) - - args = parser.parse_args() - verify_metadata( - metadata=args.metadata, - schema_url=args.schema_url, - filepath=args.filepath, - result_path=args.result_path, - create_dir=True, - hash_val=args.hash_val, - ) - - -if __name__ == "__main__": - verify() diff --git a/monai/utils/__init__.py b/monai/utils/__init__.py index 636ea15c8d..9e25645de3 100644 --- a/monai/utils/__init__.py +++ b/monai/utils/__init__.py @@ -59,6 +59,7 @@ save_obj, set_determinism, star_zip_with, + verify_parent_dir, zip_with, ) from .module import ( diff --git a/monai/utils/misc.py b/monai/utils/misc.py index 1c79562f07..f9f7cc502a 100644 --- a/monai/utils/misc.py +++ b/monai/utils/misc.py @@ -50,6 +50,7 @@ "is_module_ver_at_least", "has_option", "sample_slices", + "verify_parent_dir", "save_obj", ] @@ -400,6 +401,25 @@ def sample_slices(data: NdarrayOrTensor, dim: int = 1, as_indices: bool = True, return data[tuple(slices)] +def verify_parent_dir(path: PathLike, create_dir: bool = True): + """ + Utility to verify whether the parent directory of the `path` exists. + + Args: + path: input path to verify the parent directory. + create_dir: if True, when the parent directory doesn't exist, create the directory, + otherwise, raise exception. + + """ + path = Path(path) + path_dir = path.parent + if not path_dir.exists(): + if create_dir: + path_dir.mkdir(parents=True) + else: + raise ValueError(f"the directory of specified path does not exist: `{path_dir}`.") + + def save_obj( obj, path: PathLike, create_dir: bool = True, atomic: bool = True, func: Optional[Callable] = None, **kwargs ): @@ -421,12 +441,7 @@ def save_obj( """ path = Path(path) - path_dir = path.parent - if not path_dir.exists(): - if create_dir: - path_dir.mkdir(parents=True) - else: - raise ValueError(f"the directory of specified path is not existing: {path_dir}.") + verify_parent_dir(path=path, create_dir=create_dir) if path.exists(): # remove the existing file os.remove(path) diff --git a/tests/min_tests.py b/tests/min_tests.py index 109bb32ad7..bb47403090 100644 --- a/tests/min_tests.py +++ b/tests/min_tests.py @@ -160,7 +160,7 @@ def run_testsuit(): "test_prepare_batch_default_dist", "test_parallel_execution_dist", "test_bundle_run", - "test_bundle_verify_meta", + "test_bundle_verify_metadata", ] assert sorted(exclude_cases) == sorted(set(exclude_cases)), f"Duplicated items in {exclude_cases}" diff --git a/tests/test_bundle_verify_meta.py b/tests/test_bundle_verify_metadata.py similarity index 67% rename from tests/test_bundle_verify_meta.py rename to tests/test_bundle_verify_metadata.py index cfbdbb5d1d..f57befbc63 100644 --- a/tests/test_bundle_verify_meta.py +++ b/tests/test_bundle_verify_metadata.py @@ -24,7 +24,7 @@ url = "https://github.com/Project-MONAI/MONAI-extra-test-data/releases/download/0.8.1/" "meta_schema_202202281232.json" -class TestVerifyMeta(unittest.TestCase): +class TestVerifyMetaData(unittest.TestCase): @parameterized.expand([TEST_CASE_1]) def test_verify(self, metafile): logging.basicConfig(stream=sys.stdout, level=logging.INFO) @@ -33,11 +33,23 @@ def test_verify(self, metafile): resultfile = os.path.join(tempdir, "results.txt") hash_val = "486c581cca90293d1a7f41a57991ff35" - cmd = ( - f"{sys.executable} -m monai.bundle.verify_meta -m {metafile} -u {url} -f {filepath}" - f" -r {resultfile} -v {hash_val}" - ) - ret = subprocess.check_call(cmd.split(" ")) + cmd = [ + sys.executable, + "-m", + "monai.bundle", + "verify_metadata", + "--meta_file", + metafile, + "--schema_url", + url, + "--filepath", + filepath, + "--result_path", + resultfile, + "--hash_val", + hash_val, + ] + ret = subprocess.check_call(cmd) self.assertEqual(ret, 0) self.assertFalse(os.path.exists(resultfile)) @@ -50,9 +62,22 @@ def test_verify_error(self): json.dump({"wrong_meta": "wrong content"}, f) resultfile = os.path.join(tempdir, "results.txt") - cmd = f"{sys.executable} -m monai.bundle.verify_meta -m {metafile} -u {url} -f {filepath} -r {resultfile}" + cmd = [ + sys.executable, + "-m", + "monai.bundle", + "verify_metadata", + "--meta_file", + metafile, + "--schema_url", + url, + "--filepath", + filepath, + "--result_path", + resultfile, + ] with self.assertRaises(subprocess.CalledProcessError): - subprocess.check_call(cmd.split(" ")) + subprocess.check_call(cmd) self.assertTrue(os.path.exists(resultfile)) From 82bfb8914ea612101500492609ccce64de3b2b75 Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Fri, 11 Mar 2022 19:48:35 +0800 Subject: [PATCH 12/24] [DLMED] add more tests Signed-off-by: Nic Ma --- tests/test_bundle_verify_metadata.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_bundle_verify_metadata.py b/tests/test_bundle_verify_metadata.py index f57befbc63..22c6aa25db 100644 --- a/tests/test_bundle_verify_metadata.py +++ b/tests/test_bundle_verify_metadata.py @@ -19,6 +19,8 @@ from parameterized import parameterized +from monai.bundle import ConfigParser + TEST_CASE_1 = [os.path.join(os.path.dirname(__file__), "testing_data", "metadata.json")] url = "https://github.com/Project-MONAI/MONAI-extra-test-data/releases/download/0.8.1/" "meta_schema_202202281232.json" @@ -29,6 +31,10 @@ class TestVerifyMetaData(unittest.TestCase): def test_verify(self, metafile): logging.basicConfig(stream=sys.stdout, level=logging.INFO) with tempfile.TemporaryDirectory() as tempdir: + def_args = {"meta_file": "will be replaced by `meta_file` arg"} + def_args_file = os.path.join(tempdir, "def_args.json") + ConfigParser.export_config_file(config=def_args, filepath=def_args_file) + filepath = os.path.join(tempdir, "schema.json") resultfile = os.path.join(tempdir, "results.txt") hash_val = "486c581cca90293d1a7f41a57991ff35" @@ -48,6 +54,8 @@ def test_verify(self, metafile): resultfile, "--hash_val", hash_val, + "--args_file", + def_args_file, ] ret = subprocess.check_call(cmd) self.assertEqual(ret, 0) From f41c8309e5942368581d8cbdcbb177e6c3b446ef Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Fri, 11 Mar 2022 20:17:32 +0800 Subject: [PATCH 13/24] [DLMED] change "target_id" to "runner_id" Signed-off-by: Nic Ma --- monai/bundle/scripts.py | 16 ++++++++-------- tests/test_bundle_run.py | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/monai/bundle/scripts.py b/monai/bundle/scripts.py index 2ee9679cf0..d99e7b4774 100644 --- a/monai/bundle/scripts.py +++ b/monai/bundle/scripts.py @@ -45,7 +45,7 @@ def _update_default_args(args: Optional[Union[str, Dict]] = None, **kwargs) -> D def run( meta_file: Optional[Union[str, Sequence[str]]] = None, config_file: Optional[Union[str, Sequence[str]]] = None, - target_id: Optional[str] = None, + runner_id: Optional[str] = None, args_file: Optional[str] = None, **override, ): @@ -57,7 +57,7 @@ def run( .. code-block:: bash # Execute this module as a CLI entry: - python -m monai.bundle run --meta_file --config_file --target_id trainer + python -m monai.bundle run --meta_file --config_file --runner_id trainer # Override config values at runtime by specifying the component id and its new value: python -m monai.bundle run --net#input_chns 1 ... @@ -77,21 +77,21 @@ def run( if it is a list of file paths, the content of them will be merged. config_file: filepath of the config file, if `None`, must be provided in `args_file`. if it is a list of file paths, the content of them will be merged. - target_id: ID name of the target component or workflow, it must have a `run` method. + runner_id: ID name of the runner component or workflow, it must have a `run` method. args_file: a JSON or YAML file to provide default values for `meta_file`, `config_file`, - `target_id` and override pairs. so that the command line inputs can be simplified. + `runner_id` and override pairs. so that the command line inputs can be simplified. override: id-value pairs to override or add the corresponding config content. e.g. ``--net#input_chns 42``. """ - k_v = zip(["meta_file", "config_file", "target_id"], [meta_file, config_file, target_id]) + k_v = zip(["meta_file", "config_file", "runner_id"], [meta_file, config_file, runner_id]) for k, v in k_v: if v is not None: override[k] = v full_kv = zip( - ("meta_file", "config_file", "target_id", "args_file", "override"), - (meta_file, config_file, target_id, args_file, override), + ("meta_file", "config_file", "runner_id", "args_file", "override"), + (meta_file, config_file, runner_id, args_file, override), ) print("\n--- input summary of monai.bundle.scripts.run ---") for name, val in full_kv: @@ -106,7 +106,7 @@ def run( parser = ConfigParser() parser.read_config(f=_args.pop("config_file")) parser.read_meta(f=_args.pop("meta_file")) - id = _args.pop("target_id", "") + id = _args.pop("runner_id", "") # the rest key-values in the _args are to override config content for k, v in _args.items(): diff --git a/tests/test_bundle_run.py b/tests/test_bundle_run.py index 75002d3631..43e9156e5e 100644 --- a/tests/test_bundle_run.py +++ b/tests/test_bundle_run.py @@ -64,7 +64,7 @@ def test_shape(self, config_file, expected_shape): else: override = f"--network %{overridefile1}#move_net --dataset#_target_ %{overridefile2}" # test with `monai.bundle` as CLI entry directly - cmd = "-m monai.bundle run --target_id evaluator" + cmd = "-m monai.bundle run --runner_id evaluator" cmd += f" --postprocessing#transforms#2#output_postfix seg {override}" la = [f"{sys.executable}"] + cmd.split(" ") + ["--meta_file", meta_file] + ["--config_file", config_file] ret = subprocess.check_call(la + ["--args_file", def_args_file]) @@ -72,7 +72,7 @@ def test_shape(self, config_file, expected_shape): self.assertTupleEqual(saver(os.path.join(tempdir, "image", "image_seg.nii.gz")).shape, expected_shape) # here test the script with `google fire` tool as CLI - cmd = "-m fire monai.bundle.scripts run --target_id evaluator" + cmd = "-m fire monai.bundle.scripts run --runner_id evaluator" cmd += f" --evaluator#amp False {override}" la = [f"{sys.executable}"] + cmd.split(" ") + ["--meta_file", meta_file] + ["--config_file", config_file] ret = subprocess.check_call(la) From 488de479cd16206d4b23c8fcb363732e0590a9da Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Fri, 11 Mar 2022 20:53:23 +0800 Subject: [PATCH 14/24] [DLMED] simplify command line Signed-off-by: Nic Ma --- monai/bundle/scripts.py | 18 +++++++++--------- tests/test_bundle_run.py | 3 +-- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/monai/bundle/scripts.py b/monai/bundle/scripts.py index d99e7b4774..4679ca4706 100644 --- a/monai/bundle/scripts.py +++ b/monai/bundle/scripts.py @@ -43,9 +43,9 @@ def _update_default_args(args: Optional[Union[str, Dict]] = None, **kwargs) -> D def run( + runner_id: Optional[str] = None, meta_file: Optional[Union[str, Sequence[str]]] = None, config_file: Optional[Union[str, Sequence[str]]] = None, - runner_id: Optional[str] = None, args_file: Optional[str] = None, **override, ): @@ -57,41 +57,41 @@ def run( .. code-block:: bash # Execute this module as a CLI entry: - python -m monai.bundle run --meta_file --config_file --runner_id trainer + python -m monai.bundle run trainer --meta_file --config_file # Override config values at runtime by specifying the component id and its new value: - python -m monai.bundle run --net#input_chns 1 ... + python -m monai.bundle run trainer --net#input_chns 1 ... # Override config values with another config file `/path/to/another.json`: - python -m monai.bundle run --net %/path/to/another.json ... + python -m monai.bundle run evaluator --net %/path/to/another.json ... # Override config values with part content of another config file: - python -m monai.bundle run --net %/data/other.json#net_arg ... + python -m monai.bundle run trainer --net %/data/other.json#net_arg ... # Set default args of `run` in a JSON / YAML file, help to record and simplify the command line. # Other args still can override the default args at runtime: python -m monai.bundle run --args_file "/workspace/data/args.json" --config_file Args: + runner_id: ID name of the runner component or workflow, it must have a `run` method. meta_file: filepath of the metadata file, if `None`, must be provided in `args_file`. if it is a list of file paths, the content of them will be merged. config_file: filepath of the config file, if `None`, must be provided in `args_file`. if it is a list of file paths, the content of them will be merged. - runner_id: ID name of the runner component or workflow, it must have a `run` method. args_file: a JSON or YAML file to provide default values for `meta_file`, `config_file`, `runner_id` and override pairs. so that the command line inputs can be simplified. override: id-value pairs to override or add the corresponding config content. e.g. ``--net#input_chns 42``. """ - k_v = zip(["meta_file", "config_file", "runner_id"], [meta_file, config_file, runner_id]) + k_v = zip(["runner_id", "meta_file", "config_file"], [runner_id, meta_file, config_file]) for k, v in k_v: if v is not None: override[k] = v full_kv = zip( - ("meta_file", "config_file", "runner_id", "args_file", "override"), - (meta_file, config_file, runner_id, args_file, override), + ("runner_id", "meta_file", "config_file", "args_file", "override"), + (runner_id, meta_file, config_file, args_file, override), ) print("\n--- input summary of monai.bundle.scripts.run ---") for name, val in full_kv: diff --git a/tests/test_bundle_run.py b/tests/test_bundle_run.py index 43e9156e5e..b0ce353240 100644 --- a/tests/test_bundle_run.py +++ b/tests/test_bundle_run.py @@ -64,8 +64,7 @@ def test_shape(self, config_file, expected_shape): else: override = f"--network %{overridefile1}#move_net --dataset#_target_ %{overridefile2}" # test with `monai.bundle` as CLI entry directly - cmd = "-m monai.bundle run --runner_id evaluator" - cmd += f" --postprocessing#transforms#2#output_postfix seg {override}" + cmd = f"-m monai.bundle run evaluator --postprocessing#transforms#2#output_postfix seg {override}" la = [f"{sys.executable}"] + cmd.split(" ") + ["--meta_file", meta_file] + ["--config_file", config_file] ret = subprocess.check_call(la + ["--args_file", def_args_file]) self.assertEqual(ret, 0) From 99b1c63604386634380cac1810111cacd28a2fa3 Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Fri, 11 Mar 2022 21:08:51 +0800 Subject: [PATCH 15/24] [DLMED] fix print summary logic Signed-off-by: Nic Ma --- monai/bundle/scripts.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/monai/bundle/scripts.py b/monai/bundle/scripts.py index 4679ca4706..929a7e4dae 100644 --- a/monai/bundle/scripts.py +++ b/monai/bundle/scripts.py @@ -42,6 +42,13 @@ def _update_default_args(args: Optional[Union[str, Dict]] = None, **kwargs) -> D return args_ +def _log_input_summary(tag: str, args: Dict): + print(f"\n--- input summary of monai.bundle.scripts.{tag} ---") + for name, val in args.items(): + print(f"> {name}: {pprint.pformat(val)}") + print("---\n\n") + + def run( runner_id: Optional[str] = None, meta_file: Optional[Union[str, Sequence[str]]] = None, @@ -89,19 +96,11 @@ def run( if v is not None: override[k] = v - full_kv = zip( - ("runner_id", "meta_file", "config_file", "args_file", "override"), - (runner_id, meta_file, config_file, args_file, override), - ) - print("\n--- input summary of monai.bundle.scripts.run ---") - for name, val in full_kv: - print(f"> {name}: {pprint.pformat(val)}") - print("---\n\n") - _args = _update_default_args(args=args_file, **override) for k in ("meta_file", "config_file"): if k not in _args: raise ValueError(f"{k} is required for 'monai.bundle run'.\n{run.__doc__}") + _log_input_summary(tag="run", args=_args) parser = ConfigParser() parser.read_config(f=_args.pop("config_file")) @@ -155,6 +154,7 @@ def verify_metadata( if v is not None: kwargs[k] = v _args = _update_default_args(args=args_file, **kwargs) + _log_input_summary(tag="run", args=_args) filepath_ = _args.pop("filepath") create_dir_ = _args.pop("create_dir", True) From 8b4065650c186bf41908e9f29ba587b8ceec175a Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Sat, 12 Mar 2022 00:39:30 +0800 Subject: [PATCH 16/24] [DLMED] update log print to logging Signed-off-by: Nic Ma --- monai/bundle/scripts.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/monai/bundle/scripts.py b/monai/bundle/scripts.py index 929a7e4dae..b697749ad8 100644 --- a/monai/bundle/scripts.py +++ b/monai/bundle/scripts.py @@ -12,7 +12,7 @@ import pprint from typing import Dict, Optional, Sequence, Union -from monai.apps.utils import download_url +from monai.apps.utils import download_url, get_logger from monai.bundle.config_parser import ConfigParser from monai.config import PathLike from monai.utils import optional_import, verify_parent_dir @@ -20,6 +20,8 @@ validate, _ = optional_import("jsonschema", name="validate") ValidationError, _ = optional_import("jsonschema.exceptions", name="ValidationError") +logger = get_logger(module_name=__name__) + def _update_default_args(args: Optional[Union[str, Dict]] = None, **kwargs) -> Dict: """ @@ -43,10 +45,10 @@ def _update_default_args(args: Optional[Union[str, Dict]] = None, **kwargs) -> D def _log_input_summary(tag: str, args: Dict): - print(f"\n--- input summary of monai.bundle.scripts.{tag} ---") + logger.info(f"\n--- input summary of monai.bundle.scripts.{tag} ---") for name, val in args.items(): - print(f"> {name}: {pprint.pformat(val)}") - print("---\n\n") + logger.info(f"> {name}: {pprint.pformat(val)}") + logger.info("---\n\n") def run( @@ -176,3 +178,4 @@ def verify_metadata( with open(result_path_, "w") as f: f.write(str(e)) raise ValueError(f"metadata failed to validate against schema `{url_}`.") from e + logger.info("metadata verification completed.") From 4d097d70f29f36e53eadf639394ecef9ab42d5b1 Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Sat, 12 Mar 2022 00:48:51 +0800 Subject: [PATCH 17/24] [DLMED] compact commands Signed-off-by: Nic Ma --- tests/test_bundle_verify_metadata.py | 37 ++++------------------------ 1 file changed, 5 insertions(+), 32 deletions(-) diff --git a/tests/test_bundle_verify_metadata.py b/tests/test_bundle_verify_metadata.py index 22c6aa25db..68c888e47b 100644 --- a/tests/test_bundle_verify_metadata.py +++ b/tests/test_bundle_verify_metadata.py @@ -39,24 +39,9 @@ def test_verify(self, metafile): resultfile = os.path.join(tempdir, "results.txt") hash_val = "486c581cca90293d1a7f41a57991ff35" - cmd = [ - sys.executable, - "-m", - "monai.bundle", - "verify_metadata", - "--meta_file", - metafile, - "--schema_url", - url, - "--filepath", - filepath, - "--result_path", - resultfile, - "--hash_val", - hash_val, - "--args_file", - def_args_file, - ] + cmd = [sys.executable, "-m", "monai.bundle", "verify_metadata", "--meta_file", metafile] + cmd += ["--schema_url", url, "--filepath", filepath, "--result_path", resultfile] + cmd += ["--hash_val", hash_val, "--args_file", def_args_file] ret = subprocess.check_call(cmd) self.assertEqual(ret, 0) self.assertFalse(os.path.exists(resultfile)) @@ -70,20 +55,8 @@ def test_verify_error(self): json.dump({"wrong_meta": "wrong content"}, f) resultfile = os.path.join(tempdir, "results.txt") - cmd = [ - sys.executable, - "-m", - "monai.bundle", - "verify_metadata", - "--meta_file", - metafile, - "--schema_url", - url, - "--filepath", - filepath, - "--result_path", - resultfile, - ] + cmd = [sys.executable, "-m", "monai.bundle", "verify_metadata", "--meta_file", metafile] + cmd += ["--schema_url", url, "--filepath", filepath, "--result_path", resultfile] with self.assertRaises(subprocess.CalledProcessError): subprocess.check_call(cmd) self.assertTrue(os.path.exists(resultfile)) From 8bf43713f42900e3428846ea04ee0fac831e3ab3 Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Sat, 12 Mar 2022 01:04:18 +0800 Subject: [PATCH 18/24] [DLMED] unify args update Signed-off-by: Nic Ma --- monai/bundle/scripts.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/monai/bundle/scripts.py b/monai/bundle/scripts.py index b697749ad8..1819c9a677 100644 --- a/monai/bundle/scripts.py +++ b/monai/bundle/scripts.py @@ -23,13 +23,14 @@ logger = get_logger(module_name=__name__) -def _update_default_args(args: Optional[Union[str, Dict]] = None, **kwargs) -> Dict: +def _update_args(args: Optional[Union[str, Dict]] = None, ignore_none: bool = True, **kwargs) -> Dict: """ Update the `args` with the input `kwargs`. For dict data, recursively update the content based on the keys. Args: args: source args to update. + ignore_none: whether to ignore input args with None value, default to `True`. kwargs: destination args to update. """ @@ -40,7 +41,12 @@ def _update_default_args(args: Optional[Union[str, Dict]] = None, **kwargs) -> D # recursively update the default args with new args for k, v in kwargs.items(): - args_[k] = _update_default_args(args_[k], **v) if isinstance(v, dict) and isinstance(args_.get(k), dict) else v + if ignore_none and v is None: + continue + if isinstance(v, dict) and isinstance(args_.get(k), dict): + args_[k] = _update_args(args_[k], ignore_none, **v) + else: + args_[k] = v return args_ @@ -93,12 +99,8 @@ def run( e.g. ``--net#input_chns 42``. """ - k_v = zip(["runner_id", "meta_file", "config_file"], [runner_id, meta_file, config_file]) - for k, v in k_v: - if v is not None: - override[k] = v - _args = _update_default_args(args=args_file, **override) + _args = _update_args(args=args_file, runner_id=runner_id, meta_file=meta_file, config_file=config_file, **override) for k in ("meta_file", "config_file"): if k not in _args: raise ValueError(f"{k} is required for 'monai.bundle run'.\n{run.__doc__}") @@ -148,14 +150,16 @@ def verify_metadata( """ - k_v = zip( - ["meta_file", "schema_url", "filepath", "result_path", "create_dir", "hash_val"], - [meta_file, schema_url, filepath, result_path, create_dir, hash_val], + _args = _update_args( + args=args_file, + meta_file=meta_file, + schema_url=schema_url, + filepath=filepath, + result_path=result_path, + create_dir=create_dir, + hash_val=hash_val, + **kwargs, ) - for k, v in k_v: - if v is not None: - kwargs[k] = v - _args = _update_default_args(args=args_file, **kwargs) _log_input_summary(tag="run", args=_args) filepath_ = _args.pop("filepath") From 9be9daa873f5678fa3846dca7103c4fd0bd0391e Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Sun, 13 Mar 2022 10:09:12 +0800 Subject: [PATCH 19/24] [DLMED] update according to comments Signed-off-by: Nic Ma --- monai/bundle/scripts.py | 15 +- tests/test_bundle_verify_metadata.py | 27 +-- tests/testing_data/metadata.json | 1 + tests/testing_data/schema.json | 250 +++++++++++++++++++++++++++ 4 files changed, 275 insertions(+), 18 deletions(-) create mode 100644 tests/testing_data/schema.json diff --git a/monai/bundle/scripts.py b/monai/bundle/scripts.py index 1819c9a677..d1af443d8f 100644 --- a/monai/bundle/scripts.py +++ b/monai/bundle/scripts.py @@ -123,7 +123,6 @@ def run( def verify_metadata( meta_file: Optional[Union[str, Sequence[str]]] = None, - schema_url: Optional[str] = None, filepath: Optional[PathLike] = None, result_path: Optional[PathLike] = None, create_dir: Optional[bool] = None, @@ -133,12 +132,12 @@ def verify_metadata( ): """ Verify the provided `metadata` file based on the predefined `schema`. + `metadata` content must contain the `schema` field for the URL of shcema file to download. The schema standard follows: http://json-schema.org/. Args: meta_file: filepath of the metadata file to verify, if `None`, must be provided in `args_file`. if it is a list of file paths, the content of them will be merged. - schema_url: URL to download the expected schema file. filepath: file path to store the downloaded schema. result_path: if not None, save the validation error into the result file. create_dir: whether to create directories if not existing, default to `True`. @@ -153,7 +152,6 @@ def verify_metadata( _args = _update_args( args=args_file, meta_file=meta_file, - schema_url=schema_url, filepath=filepath, result_path=result_path, create_dir=create_dir, @@ -165,12 +163,13 @@ def verify_metadata( filepath_ = _args.pop("filepath") create_dir_ = _args.pop("create_dir", True) verify_parent_dir(path=filepath_, create_dir=create_dir_) - url_ = _args.pop("schema_url", None) - download_url(url=url_, filepath=filepath_, hash_val=_args.pop("hash_val", None), hash_type="md5", progress=True) - - schema = ConfigParser.load_config_file(filepath=filepath_) metadata = ConfigParser.load_config_files(files=_args.pop("meta_file")) + url = metadata.get("schema") + if url is None: + raise ValueError("must provide the `schema` field in the metadata for the URL of schema file.") + download_url(url=url, filepath=filepath_, hash_val=_args.pop("hash_val", None), hash_type="md5", progress=True) + schema = ConfigParser.load_config_file(filepath=filepath_) result_path_ = _args.pop("result_path", None) try: @@ -181,5 +180,5 @@ def verify_metadata( verify_parent_dir(result_path_, create_dir=create_dir_) with open(result_path_, "w") as f: f.write(str(e)) - raise ValueError(f"metadata failed to validate against schema `{url_}`.") from e + raise ValueError(f"metadata failed to validate against schema `{url}`.") from e logger.info("metadata verification completed.") diff --git a/tests/test_bundle_verify_metadata.py b/tests/test_bundle_verify_metadata.py index 68c888e47b..a77c0a7dbb 100644 --- a/tests/test_bundle_verify_metadata.py +++ b/tests/test_bundle_verify_metadata.py @@ -21,26 +21,26 @@ from monai.bundle import ConfigParser -TEST_CASE_1 = [os.path.join(os.path.dirname(__file__), "testing_data", "metadata.json")] - -url = "https://github.com/Project-MONAI/MONAI-extra-test-data/releases/download/0.8.1/" "meta_schema_202202281232.json" +TEST_CASE_1 = [ + os.path.join(os.path.dirname(__file__), "testing_data", "metadata.json"), + os.path.join(os.path.dirname(__file__), "testing_data", "schema.json"), +] class TestVerifyMetaData(unittest.TestCase): @parameterized.expand([TEST_CASE_1]) - def test_verify(self, metafile): + def test_verify(self, meta_file, schema_file): logging.basicConfig(stream=sys.stdout, level=logging.INFO) with tempfile.TemporaryDirectory() as tempdir: def_args = {"meta_file": "will be replaced by `meta_file` arg"} def_args_file = os.path.join(tempdir, "def_args.json") ConfigParser.export_config_file(config=def_args, filepath=def_args_file) - filepath = os.path.join(tempdir, "schema.json") resultfile = os.path.join(tempdir, "results.txt") - hash_val = "486c581cca90293d1a7f41a57991ff35" + hash_val = "b11acc946148c0186924f8234562b947" - cmd = [sys.executable, "-m", "monai.bundle", "verify_metadata", "--meta_file", metafile] - cmd += ["--schema_url", url, "--filepath", filepath, "--result_path", resultfile] + cmd = [sys.executable, "-m", "monai.bundle", "verify_metadata", "--meta_file", meta_file] + cmd += ["--filepath", schema_file, "--result_path", resultfile] cmd += ["--hash_val", hash_val, "--args_file", def_args_file] ret = subprocess.check_call(cmd) self.assertEqual(ret, 0) @@ -52,11 +52,18 @@ def test_verify_error(self): filepath = os.path.join(tempdir, "schema.json") metafile = os.path.join(tempdir, "metadata.json") with open(metafile, "w") as f: - json.dump({"wrong_meta": "wrong content"}, f) + json.dump( + { + "schema": "https://github.com/Project-MONAI/MONAI-extra-test-data/releases/" + "download/0.8.1/meta_schema_202203130950.json", + "wrong_meta": "wrong content", + }, + f, + ) resultfile = os.path.join(tempdir, "results.txt") cmd = [sys.executable, "-m", "monai.bundle", "verify_metadata", "--meta_file", metafile] - cmd += ["--schema_url", url, "--filepath", filepath, "--result_path", resultfile] + cmd += ["--filepath", filepath, "--result_path", resultfile] with self.assertRaises(subprocess.CalledProcessError): subprocess.check_call(cmd) self.assertTrue(os.path.exists(resultfile)) diff --git a/tests/testing_data/metadata.json b/tests/testing_data/metadata.json index 4430b9bde2..97bc218f5e 100644 --- a/tests/testing_data/metadata.json +++ b/tests/testing_data/metadata.json @@ -1,4 +1,5 @@ { + "schema": "https://github.com/Project-MONAI/MONAI-extra-test-data/releases/download/0.8.1/meta_schema_202203130950.json", "version": "0.1.0", "changelog": { "0.1.0": "complete the model package", diff --git a/tests/testing_data/schema.json b/tests/testing_data/schema.json new file mode 100644 index 0000000000..f90b74d1f9 --- /dev/null +++ b/tests/testing_data/schema.json @@ -0,0 +1,250 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "schema": { + "description": "URL of the schema file.", + "type": "string" + }, + "version": { + "description": "version number of the model package.", + "type": "string" + }, + "changelog": { + "description": "dictionary relating previous version names to strings describing the version.", + "type": "object" + }, + "monai_version": { + "description": "version of MONAI the model package was generated on.", + "type": "string" + }, + "pytorch_version": { + "description": "version of PyTorch the model package was generated on.", + "type": "string" + }, + "numpy_version": { + "description": "version of NumPy the model package was generated on.", + "type": "string" + }, + "optional_packages_version": { + "description": "dictionary relating optional package names to their versions.", + "type": "object" + }, + "task": { + "description": "plain-language description of what the model is meant to do.", + "type": "string" + }, + "description": { + "description": "longer form plain-language description of what the model is, what it does, etc.", + "type": "string" + }, + "authorship": { + "description": "state author(s) of the model package.", + "type": "string" + }, + "copyright": { + "description": "state copyright of the model package.", + "type": "string" + }, + "data_source": { + "description": "where to download or prepare the data used in this model package.", + "type": "string" + }, + "data_type": { + "description": "type of the data, like: `dicom`, `nibabel`, etc.", + "type": "string" + }, + "dataset_dir": { + "description": "state the expected path of data in file system.", + "type": "string" + }, + "image_classes": { + "description": "description for every class of the input image.", + "type": "string" + }, + "label_classes": { + "description": "description for every class of the input label.", + "type": "string" + }, + "pred_classes": { + "description": "description for every class of the output prediction.", + "type": "string" + }, + "eval_metrics": { + "description": "dictionary relating evaluation metrics to the achieved scores.", + "type": "object" + }, + "intended_use": { + "description": "what the model package is to be used for, ie. what task it accomplishes.", + "type": "string" + }, + "references": { + "description": "list of published referenced relating to the model package.", + "type": "array" + }, + "network_data_format": { + "description": "defines the format, shape, and meaning of inputs and outputs to the model.", + "type": "object", + "properties": { + "inputs": { + "type": "object", + "properties": { + "image": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "format": { + "type": "string" + }, + "num_channels": { + "type": "integer" + }, + "spatial_shape": { + "type": "array" + }, + "dtype": { + "type": "string" + }, + "value_range": { + "type": "array", + "items": { + "type": "number" + } + }, + "is_patch_data": { + "type": "boolean" + }, + "channel_def": { + "type": "object" + } + }, + "required": [ + "type", + "format", + "num_channels", + "spatial_shape", + "dtype", + "value_range" + ] + }, + "label": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "format": { + "type": "string" + }, + "num_channels": { + "type": "integer" + }, + "spatial_shape": { + "type": "array" + }, + "dtype": { + "type": "string" + }, + "value_range": { + "type": "array", + "items": { + "type": "number" + } + }, + "is_patch_data": { + "type": "boolean" + }, + "channel_def": { + "type": "object" + } + }, + "required": [ + "type", + "format", + "num_channels", + "spatial_shape", + "dtype", + "value_range" + ] + } + }, + "required": [ + "image" + ] + }, + "outputs": { + "type": "object", + "properties": { + "pred": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "format": { + "type": "string" + }, + "num_channels": { + "type": "integer" + }, + "spatial_shape": { + "type": "array" + }, + "dtype": { + "type": "string" + }, + "value_range": { + "type": "array", + "items": { + "type": "number" + } + }, + "is_patch_data": { + "type": "boolean" + }, + "channel_def": { + "type": "object" + } + }, + "required": [ + "type", + "format", + "num_channels", + "spatial_shape", + "dtype", + "value_range" + ] + } + }, + "required": [ + "pred" + ] + } + }, + "required": [ + "inputs", + "outputs" + ] + } + }, + "required": [ + "schema", + "version", + "monai_version", + "pytorch_version", + "numpy_version", + "optional_packages_version", + "task", + "description", + "authorship", + "copyright", + "dataset_dir", + "image_classes", + "label_classes", + "pred_classes", + "eval_metrics", + "network_data_format" + ] +} From caf34210a3392bae4bbfad40f7cadadbfc03d140 Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Sun, 13 Mar 2022 17:43:03 +0800 Subject: [PATCH 20/24] [DLMED] skip windows Signed-off-by: Nic Ma --- tests/test_bundle_verify_metadata.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_bundle_verify_metadata.py b/tests/test_bundle_verify_metadata.py index a77c0a7dbb..0f3d199c25 100644 --- a/tests/test_bundle_verify_metadata.py +++ b/tests/test_bundle_verify_metadata.py @@ -20,6 +20,7 @@ from parameterized import parameterized from monai.bundle import ConfigParser +from tests.utils import skip_if_windows TEST_CASE_1 = [ os.path.join(os.path.dirname(__file__), "testing_data", "metadata.json"), @@ -27,6 +28,7 @@ ] +@skip_if_windows class TestVerifyMetaData(unittest.TestCase): @parameterized.expand([TEST_CASE_1]) def test_verify(self, meta_file, schema_file): From 0ce58d9f1bd1386784e03406309c73f286adb9d9 Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Mon, 14 Mar 2022 14:07:17 +0800 Subject: [PATCH 21/24] [DLMED] fix typo Signed-off-by: Nic Ma --- monai/bundle/scripts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monai/bundle/scripts.py b/monai/bundle/scripts.py index d1af443d8f..04405a3928 100644 --- a/monai/bundle/scripts.py +++ b/monai/bundle/scripts.py @@ -158,7 +158,7 @@ def verify_metadata( hash_val=hash_val, **kwargs, ) - _log_input_summary(tag="run", args=_args) + _log_input_summary(tag="verify_metadata", args=_args) filepath_ = _args.pop("filepath") create_dir_ = _args.pop("create_dir", True) From e7e1d3e07451e4093ad9c1fee5231ba0f319df68 Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Mon, 14 Mar 2022 20:10:26 +0800 Subject: [PATCH 22/24] [DLMED] optimize error log Signed-off-by: Nic Ma --- monai/bundle/scripts.py | 15 +++++---------- tests/test_bundle_verify_metadata.py | 16 ++++++---------- 2 files changed, 11 insertions(+), 20 deletions(-) diff --git a/monai/bundle/scripts.py b/monai/bundle/scripts.py index 04405a3928..fda66da114 100644 --- a/monai/bundle/scripts.py +++ b/monai/bundle/scripts.py @@ -10,6 +10,7 @@ # limitations under the License. import pprint +import re from typing import Dict, Optional, Sequence, Union from monai.apps.utils import download_url, get_logger @@ -124,7 +125,6 @@ def run( def verify_metadata( meta_file: Optional[Union[str, Sequence[str]]] = None, filepath: Optional[PathLike] = None, - result_path: Optional[PathLike] = None, create_dir: Optional[bool] = None, hash_val: Optional[str] = None, args_file: Optional[str] = None, @@ -139,7 +139,6 @@ def verify_metadata( meta_file: filepath of the metadata file to verify, if `None`, must be provided in `args_file`. if it is a list of file paths, the content of them will be merged. filepath: file path to store the downloaded schema. - result_path: if not None, save the validation error into the result file. create_dir: whether to create directories if not existing, default to `True`. hash_val: if not None, define the hash value to verify the downloaded schema file. args_file: a JSON or YAML file to provide default values for all the args in this function. @@ -153,7 +152,6 @@ def verify_metadata( args=args_file, meta_file=meta_file, filepath=filepath, - result_path=result_path, create_dir=create_dir, hash_val=hash_val, **kwargs, @@ -170,15 +168,12 @@ def verify_metadata( raise ValueError("must provide the `schema` field in the metadata for the URL of schema file.") download_url(url=url, filepath=filepath_, hash_val=_args.pop("hash_val", None), hash_type="md5", progress=True) schema = ConfigParser.load_config_file(filepath=filepath_) - result_path_ = _args.pop("result_path", None) try: # the rest key-values in the _args are for `validate` API validate(instance=metadata, schema=schema, **_args) except ValidationError as e: - if result_path_ is not None: - verify_parent_dir(result_path_, create_dir=create_dir_) - with open(result_path_, "w") as f: - f.write(str(e)) - raise ValueError(f"metadata failed to validate against schema `{url}`.") from e - logger.info("metadata verification completed.") + # as the error message is very long, only extract the key information + logger.info(re.compile(r".*Failed validating", re.S).findall(str(e))[0] + f" against schema `{url}`.") + return + logger.info("metadata is verified with no error.") diff --git a/tests/test_bundle_verify_metadata.py b/tests/test_bundle_verify_metadata.py index 0f3d199c25..dbc8e3de7d 100644 --- a/tests/test_bundle_verify_metadata.py +++ b/tests/test_bundle_verify_metadata.py @@ -38,15 +38,12 @@ def test_verify(self, meta_file, schema_file): def_args_file = os.path.join(tempdir, "def_args.json") ConfigParser.export_config_file(config=def_args, filepath=def_args_file) - resultfile = os.path.join(tempdir, "results.txt") hash_val = "b11acc946148c0186924f8234562b947" cmd = [sys.executable, "-m", "monai.bundle", "verify_metadata", "--meta_file", meta_file] - cmd += ["--filepath", schema_file, "--result_path", resultfile] - cmd += ["--hash_val", hash_val, "--args_file", def_args_file] + cmd += ["--filepath", schema_file, "--hash_val", hash_val, "--args_file", def_args_file] ret = subprocess.check_call(cmd) self.assertEqual(ret, 0) - self.assertFalse(os.path.exists(resultfile)) def test_verify_error(self): logging.basicConfig(stream=sys.stdout, level=logging.INFO) @@ -62,13 +59,12 @@ def test_verify_error(self): }, f, ) - resultfile = os.path.join(tempdir, "results.txt") - cmd = [sys.executable, "-m", "monai.bundle", "verify_metadata", "--meta_file", metafile] - cmd += ["--filepath", filepath, "--result_path", resultfile] - with self.assertRaises(subprocess.CalledProcessError): - subprocess.check_call(cmd) - self.assertTrue(os.path.exists(resultfile)) + cmd = [ + sys.executable, "-m", "monai.bundle", "verify_metadata", metafile, "--filepath", filepath + ] + ret = subprocess.check_call(cmd) + self.assertEqual(ret, 0) if __name__ == "__main__": From 7e4c5068947336214502bec3bcbe17b0da3b4b31 Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Mon, 14 Mar 2022 21:18:08 +0800 Subject: [PATCH 23/24] [DLMED] update according to comments Signed-off-by: Nic Ma --- .gitignore | 1 + monai/bundle/scripts.py | 7 +- tests/test_bundle_verify_metadata.py | 4 +- tests/testing_data/schema.json | 250 --------------------------- 4 files changed, 3 insertions(+), 259 deletions(-) delete mode 100644 tests/testing_data/schema.json diff --git a/.gitignore b/.gitignore index fafc7c1cef..542e08e3b6 100644 --- a/.gitignore +++ b/.gitignore @@ -130,6 +130,7 @@ temp/ tests/testing_data/MedNIST* tests/testing_data/*Hippocampus* tests/testing_data/*.tiff +tests/testing_data/schema.json # clang format tool .clang-format-bin/ diff --git a/monai/bundle/scripts.py b/monai/bundle/scripts.py index fda66da114..c600aac69b 100644 --- a/monai/bundle/scripts.py +++ b/monai/bundle/scripts.py @@ -149,12 +149,7 @@ def verify_metadata( """ _args = _update_args( - args=args_file, - meta_file=meta_file, - filepath=filepath, - create_dir=create_dir, - hash_val=hash_val, - **kwargs, + args=args_file, meta_file=meta_file, filepath=filepath, create_dir=create_dir, hash_val=hash_val, **kwargs ) _log_input_summary(tag="verify_metadata", args=_args) diff --git a/tests/test_bundle_verify_metadata.py b/tests/test_bundle_verify_metadata.py index dbc8e3de7d..7e2bd02209 100644 --- a/tests/test_bundle_verify_metadata.py +++ b/tests/test_bundle_verify_metadata.py @@ -60,9 +60,7 @@ def test_verify_error(self): f, ) - cmd = [ - sys.executable, "-m", "monai.bundle", "verify_metadata", metafile, "--filepath", filepath - ] + cmd = [sys.executable, "-m", "monai.bundle", "verify_metadata", metafile, "--filepath", filepath] ret = subprocess.check_call(cmd) self.assertEqual(ret, 0) diff --git a/tests/testing_data/schema.json b/tests/testing_data/schema.json deleted file mode 100644 index f90b74d1f9..0000000000 --- a/tests/testing_data/schema.json +++ /dev/null @@ -1,250 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "type": "object", - "properties": { - "schema": { - "description": "URL of the schema file.", - "type": "string" - }, - "version": { - "description": "version number of the model package.", - "type": "string" - }, - "changelog": { - "description": "dictionary relating previous version names to strings describing the version.", - "type": "object" - }, - "monai_version": { - "description": "version of MONAI the model package was generated on.", - "type": "string" - }, - "pytorch_version": { - "description": "version of PyTorch the model package was generated on.", - "type": "string" - }, - "numpy_version": { - "description": "version of NumPy the model package was generated on.", - "type": "string" - }, - "optional_packages_version": { - "description": "dictionary relating optional package names to their versions.", - "type": "object" - }, - "task": { - "description": "plain-language description of what the model is meant to do.", - "type": "string" - }, - "description": { - "description": "longer form plain-language description of what the model is, what it does, etc.", - "type": "string" - }, - "authorship": { - "description": "state author(s) of the model package.", - "type": "string" - }, - "copyright": { - "description": "state copyright of the model package.", - "type": "string" - }, - "data_source": { - "description": "where to download or prepare the data used in this model package.", - "type": "string" - }, - "data_type": { - "description": "type of the data, like: `dicom`, `nibabel`, etc.", - "type": "string" - }, - "dataset_dir": { - "description": "state the expected path of data in file system.", - "type": "string" - }, - "image_classes": { - "description": "description for every class of the input image.", - "type": "string" - }, - "label_classes": { - "description": "description for every class of the input label.", - "type": "string" - }, - "pred_classes": { - "description": "description for every class of the output prediction.", - "type": "string" - }, - "eval_metrics": { - "description": "dictionary relating evaluation metrics to the achieved scores.", - "type": "object" - }, - "intended_use": { - "description": "what the model package is to be used for, ie. what task it accomplishes.", - "type": "string" - }, - "references": { - "description": "list of published referenced relating to the model package.", - "type": "array" - }, - "network_data_format": { - "description": "defines the format, shape, and meaning of inputs and outputs to the model.", - "type": "object", - "properties": { - "inputs": { - "type": "object", - "properties": { - "image": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "format": { - "type": "string" - }, - "num_channels": { - "type": "integer" - }, - "spatial_shape": { - "type": "array" - }, - "dtype": { - "type": "string" - }, - "value_range": { - "type": "array", - "items": { - "type": "number" - } - }, - "is_patch_data": { - "type": "boolean" - }, - "channel_def": { - "type": "object" - } - }, - "required": [ - "type", - "format", - "num_channels", - "spatial_shape", - "dtype", - "value_range" - ] - }, - "label": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "format": { - "type": "string" - }, - "num_channels": { - "type": "integer" - }, - "spatial_shape": { - "type": "array" - }, - "dtype": { - "type": "string" - }, - "value_range": { - "type": "array", - "items": { - "type": "number" - } - }, - "is_patch_data": { - "type": "boolean" - }, - "channel_def": { - "type": "object" - } - }, - "required": [ - "type", - "format", - "num_channels", - "spatial_shape", - "dtype", - "value_range" - ] - } - }, - "required": [ - "image" - ] - }, - "outputs": { - "type": "object", - "properties": { - "pred": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "format": { - "type": "string" - }, - "num_channels": { - "type": "integer" - }, - "spatial_shape": { - "type": "array" - }, - "dtype": { - "type": "string" - }, - "value_range": { - "type": "array", - "items": { - "type": "number" - } - }, - "is_patch_data": { - "type": "boolean" - }, - "channel_def": { - "type": "object" - } - }, - "required": [ - "type", - "format", - "num_channels", - "spatial_shape", - "dtype", - "value_range" - ] - } - }, - "required": [ - "pred" - ] - } - }, - "required": [ - "inputs", - "outputs" - ] - } - }, - "required": [ - "schema", - "version", - "monai_version", - "pytorch_version", - "numpy_version", - "optional_packages_version", - "task", - "description", - "authorship", - "copyright", - "dataset_dir", - "image_classes", - "label_classes", - "pred_classes", - "eval_metrics", - "network_data_format" - ] -} From f80d9a848a65978bc62706eb085ec418709f7289 Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Mon, 14 Mar 2022 23:22:51 +0800 Subject: [PATCH 24/24] [DLMED] update accoding to comments Signed-off-by: Nic Ma --- monai/bundle/scripts.py | 4 ++-- monai/utils/__init__.py | 2 +- monai/utils/misc.py | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/monai/bundle/scripts.py b/monai/bundle/scripts.py index c600aac69b..1f3165dee3 100644 --- a/monai/bundle/scripts.py +++ b/monai/bundle/scripts.py @@ -16,7 +16,7 @@ from monai.apps.utils import download_url, get_logger from monai.bundle.config_parser import ConfigParser from monai.config import PathLike -from monai.utils import optional_import, verify_parent_dir +from monai.utils import check_parent_dir, optional_import validate, _ = optional_import("jsonschema", name="validate") ValidationError, _ = optional_import("jsonschema.exceptions", name="ValidationError") @@ -155,7 +155,7 @@ def verify_metadata( filepath_ = _args.pop("filepath") create_dir_ = _args.pop("create_dir", True) - verify_parent_dir(path=filepath_, create_dir=create_dir_) + check_parent_dir(path=filepath_, create_dir=create_dir_) metadata = ConfigParser.load_config_files(files=_args.pop("meta_file")) url = metadata.get("schema") diff --git a/monai/utils/__init__.py b/monai/utils/__init__.py index 9e25645de3..b8c462a8b7 100644 --- a/monai/utils/__init__.py +++ b/monai/utils/__init__.py @@ -41,6 +41,7 @@ from .misc import ( MAX_SEED, ImageMetaKey, + check_parent_dir, copy_to_device, ensure_tuple, ensure_tuple_rep, @@ -59,7 +60,6 @@ save_obj, set_determinism, star_zip_with, - verify_parent_dir, zip_with, ) from .module import ( diff --git a/monai/utils/misc.py b/monai/utils/misc.py index f9f7cc502a..36ba7722b8 100644 --- a/monai/utils/misc.py +++ b/monai/utils/misc.py @@ -50,7 +50,7 @@ "is_module_ver_at_least", "has_option", "sample_slices", - "verify_parent_dir", + "check_parent_dir", "save_obj", ] @@ -401,12 +401,12 @@ def sample_slices(data: NdarrayOrTensor, dim: int = 1, as_indices: bool = True, return data[tuple(slices)] -def verify_parent_dir(path: PathLike, create_dir: bool = True): +def check_parent_dir(path: PathLike, create_dir: bool = True): """ - Utility to verify whether the parent directory of the `path` exists. + Utility to check whether the parent directory of the `path` exists. Args: - path: input path to verify the parent directory. + path: input path to check the parent directory. create_dir: if True, when the parent directory doesn't exist, create the directory, otherwise, raise exception. @@ -441,7 +441,7 @@ def save_obj( """ path = Path(path) - verify_parent_dir(path=path, create_dir=create_dir) + check_parent_dir(path=path, create_dir=create_dir) if path.exists(): # remove the existing file os.remove(path)