From 9c2cdeb306649d03f44a2564d3d65bfb71b8ad46 Mon Sep 17 00:00:00 2001 From: Michal Stolarczyk Date: Thu, 30 Jan 2020 16:43:32 -0500 Subject: [PATCH 1/9] bump dev version --- docs/changelog.md | 2 ++ eido/_version.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index 63f7df30..870f1e30 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) and [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) format. +## [0.0.4] - unreleased + ## [0.0.3] - 2020-01-30 ### Added - Option to exclude the validation case from error messages in both Python API and CLI app with `exclude_case` and `-e`/`--exclude-case`, respectively. diff --git a/eido/_version.py b/eido/_version.py index 27fdca49..b288a61a 100644 --- a/eido/_version.py +++ b/eido/_version.py @@ -1 +1 @@ -__version__ = "0.0.3" +__version__ = "0.0.4-dev" From b5866cda7e6183884e605d41e561dfc37182975c Mon Sep 17 00:00:00 2001 From: Michal Stolarczyk Date: Thu, 30 Jan 2020 16:53:16 -0500 Subject: [PATCH 2/9] implement sample specific validation --- eido/__init__.py | 2 +- eido/eido.py | 49 ++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/eido/__init__.py b/eido/__init__.py index d4ef2db5..70f87dc8 100644 --- a/eido/__init__.py +++ b/eido/__init__.py @@ -6,6 +6,6 @@ from .const import * from .eido import * -__all__ = ["validate_project"] +__all__ = ["validate_project", "validate_sample"] logmuse.init_logger(PKG_NAME) diff --git a/eido/eido.py b/eido/eido.py index 49411012..72b1f831 100644 --- a/eido/eido.py +++ b/eido/eido.py @@ -73,30 +73,63 @@ def _load_yaml(filepath): return data +def _read_schema(schema): + if isinstance(schema, str) and os.path.isfile(schema): + return _load_yaml(schema) + elif isinstance(schema, dict): + return schema + raise TypeError("schema has to be either a dict or a path to an existing file") + + def validate_project(project, schema, exclude_case=False): """ Validate a project object against a schema - :param peppy.Project project: a project object to validate + :param peppy.Sample project: a project object to validate :param str | dict schema: schema dict to validate against or a path to one :param bool exclude_case: whether to exclude validated objects from the error. Useful when used ith large projects """ - if isinstance(schema, str) and os.path.isfile(schema): - schema_dict = _load_yaml(schema) - elif isinstance(schema, dict): - schema_dict = schema - else: - raise TypeError("schema has to be either a dict or a path to an existing file") + schema_dict = _read_schema(schema=schema) project_dict = project.to_dict() + _validate_object(project_dict, _preprocess_schema(schema_dict), exclude_case) + + +def _validate_object(object, schema, exclude_case=False): + """ + Generic function to validate object against a schema + + :param Mapping object: an object to validate + :param str | dict schema: schema dict to validate against or a path to one + :param bool exclude_case: whether to exclude validated objects from the error. + Useful when used ith large projects + """ try: - jsonschema.validate(project_dict, _preprocess_schema(schema_dict)) + jsonschema.validate(object, schema) except jsonschema.exceptions.ValidationError as e: if not exclude_case: raise e raise jsonschema.exceptions.ValidationError(e.message) +def validate_sample(project, sample_name, schema, exclude_case=False): + """ + Validate the selected sample object against a schema + + :param peppy.Project project: a project object to validate + :param str | int sample_name: name or index of the sample to validate + :param str | dict schema: schema dict to validate against or a path to one + :param bool exclude_case: whether to exclude validated objects from the error. + Useful when used ith large projects + :return: + """ + schema_dict = _read_schema(schema=schema) + sample_dict = project.samples[sample_name] if isinstance(sample_name, int) \ + else project.get_sample(sample_name) + sample_schema_dict = schema_dict["properties"]["samples"]["items"] + _validate_object(sample_dict, sample_schema_dict, exclude_case) + + def main(): """ Primary workflow """ parser = logmuse.add_logging_options(build_argparser()) From d0e0f4e04c08690bc498d22db90e082b615cfd92 Mon Sep 17 00:00:00 2001 From: Michal Stolarczyk Date: Thu, 30 Jan 2020 17:10:32 -0500 Subject: [PATCH 3/9] add cli hook for sample level validation --- docs/changelog.md | 3 +++ eido/eido.py | 26 ++++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 870f1e30..79da8650 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -3,6 +3,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) and [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) format. ## [0.0.4] - unreleased +### Added +- `validate_sample` function for sample level validation +- sample validation CLI support (via `-n`/`--sample-name` argument) ## [0.0.3] - 2020-01-30 ### Added diff --git a/eido/eido.py b/eido/eido.py index 72b1f831..a361f8cf 100644 --- a/eido/eido.py +++ b/eido/eido.py @@ -35,6 +35,10 @@ def build_argparser(): "-s", "--schema", required=True, help="PEP schema file in yaml format.") + parser.add_argument( + "-n", "--sample-name", required=False, + help="Name or index of the sample to validate. Only this sample will be validated.") + parser.add_argument( "-e", "--exclude-case", default=False, action="store_true", help="Whether to exclude the validation case from an error. " @@ -74,6 +78,14 @@ def _load_yaml(filepath): def _read_schema(schema): + """ + Safely read schema from YAML-formatted file. + + :param str | Mapping schema: path to the schema file + or schema in a dict form + :return dict: read schema + :raise TypeError: if the schema arg is neither a Mapping nor a file path + """ if isinstance(schema, str) and os.path.isfile(schema): return _load_yaml(schema) elif isinstance(schema, dict): @@ -141,5 +153,15 @@ def main(): _LOGGER = logmuse.logger_via_cli(args) _LOGGER.debug("Creating a Project object from: {}".format(args.pep)) p = Project(args.pep) - _LOGGER.debug("Comparing the Project ('{}') against a schema: {}.".format(args.pep, args.schema)) - validate_project(p, args.schema, args.exclude_case) + if args.sample_name: + try: + sn = int(args.sample_name) + except ValueError: + pass + _LOGGER.debug("Comparing Sample ('{}') in the Project " + "('{}') against a schema: {}.".format(sn, args.pep, args.schema)) + validate_sample(p, sn, args.schema, args.exclude_case) + else: + _LOGGER.debug("Comparing the Project ('{}') against a schema: {}.".format(args.pep, args.schema)) + validate_project(p, args.schema, args.exclude_case) + _LOGGER.info("Validation successful") From b8df01087462da0b3fd2f139166f9c6d04370c2c Mon Sep 17 00:00:00 2001 From: Michal Stolarczyk Date: Thu, 30 Jan 2020 17:38:08 -0500 Subject: [PATCH 4/9] fix arg type casting --- eido/eido.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/eido/eido.py b/eido/eido.py index a361f8cf..bec1a06f 100644 --- a/eido/eido.py +++ b/eido/eido.py @@ -155,12 +155,12 @@ def main(): p = Project(args.pep) if args.sample_name: try: - sn = int(args.sample_name) + args.sample_name = int(args.sample_name) except ValueError: pass _LOGGER.debug("Comparing Sample ('{}') in the Project " - "('{}') against a schema: {}.".format(sn, args.pep, args.schema)) - validate_sample(p, sn, args.schema, args.exclude_case) + "('{}') against a schema: {}.".format(args.sample_name, args.pep, args.schema)) + validate_sample(p, args.sample_name, args.schema, args.exclude_case) else: _LOGGER.debug("Comparing the Project ('{}') against a schema: {}.".format(args.pep, args.schema)) validate_project(p, args.schema, args.exclude_case) From 1198ce185b1f07909e38a95674ee7fddec3f9ac7 Mon Sep 17 00:00:00 2001 From: Michal Stolarczyk Date: Fri, 31 Jan 2020 10:52:48 -0500 Subject: [PATCH 5/9] implement config validation method --- eido/eido.py | 57 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/eido/eido.py b/eido/eido.py index bec1a06f..f5e46027 100644 --- a/eido/eido.py +++ b/eido/eido.py @@ -2,6 +2,7 @@ import os import jsonschema import oyaml as yaml +from copy import deepcopy as dpcpy import logmuse from ubiquerg import VersionInHelpParser @@ -93,20 +94,6 @@ def _read_schema(schema): raise TypeError("schema has to be either a dict or a path to an existing file") -def validate_project(project, schema, exclude_case=False): - """ - Validate a project object against a schema - - :param peppy.Sample project: a project object to validate - :param str | dict schema: schema dict to validate against or a path to one - :param bool exclude_case: whether to exclude validated objects from the error. - Useful when used ith large projects - """ - schema_dict = _read_schema(schema=schema) - project_dict = project.to_dict() - _validate_object(project_dict, _preprocess_schema(schema_dict), exclude_case) - - def _validate_object(object, schema, exclude_case=False): """ Generic function to validate object against a schema @@ -124,6 +111,21 @@ def _validate_object(object, schema, exclude_case=False): raise jsonschema.exceptions.ValidationError(e.message) +def validate_project(project, schema, exclude_case=False): + """ + Validate a project object against a schema + + :param peppy.Sample project: a project object to validate + :param str | dict schema: schema dict to validate against or a path to one + :param bool exclude_case: whether to exclude validated objects from the error. + Useful when used ith large projects + """ + schema_dict = _read_schema(schema=schema) + project_dict = project.to_dict() + _validate_object(project_dict, _preprocess_schema(schema_dict), exclude_case) + _LOGGER.debug("Project validation successful") + + def validate_sample(project, sample_name, schema, exclude_case=False): """ Validate the selected sample object against a schema @@ -133,13 +135,38 @@ def validate_sample(project, sample_name, schema, exclude_case=False): :param str | dict schema: schema dict to validate against or a path to one :param bool exclude_case: whether to exclude validated objects from the error. Useful when used ith large projects - :return: """ schema_dict = _read_schema(schema=schema) sample_dict = project.samples[sample_name] if isinstance(sample_name, int) \ else project.get_sample(sample_name) sample_schema_dict = schema_dict["properties"]["samples"]["items"] _validate_object(sample_dict, sample_schema_dict, exclude_case) + _LOGGER.debug("'{}' sample validation successful".format(sample_name)) + + +def validate_config(project, schema, exclude_case=False): + """ + Validate the config part of the Project object against a schema + + :param peppy.Project project: a project object to validate + :param str | dict schema: schema dict to validate against or a path to one + :param bool exclude_case: whether to exclude validated objects from the error. + Useful when used ith large projects + """ + schema_dict = _read_schema(schema=schema) + schema_cpy = dpcpy(schema_dict) + try: + del schema_cpy["properties"]["samples"] + except KeyError: + pass + if "required" in schema_cpy: + try: + schema_cpy["required"].remove("samples") + except ValueError: + pass + project_dict = project.to_dict() + _validate_object(project_dict, schema_cpy, exclude_case) + _LOGGER.debug("Config validation successful") def main(): From 2a0606c36bb9938e72aa29c754dea461d52a635d Mon Sep 17 00:00:00 2001 From: Michal Stolarczyk Date: Fri, 31 Jan 2020 10:59:46 -0500 Subject: [PATCH 6/9] add cli hook for just config validation --- eido/__init__.py | 2 +- eido/eido.py | 20 +++++++++++++++----- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/eido/__init__.py b/eido/__init__.py index 70f87dc8..d2f9e5b6 100644 --- a/eido/__init__.py +++ b/eido/__init__.py @@ -6,6 +6,6 @@ from .const import * from .eido import * -__all__ = ["validate_project", "validate_sample"] +__all__ = ["validate_project", "validate_sample", "validate_config"] logmuse.init_logger(PKG_NAME) diff --git a/eido/eido.py b/eido/eido.py index f5e46027..542fde5f 100644 --- a/eido/eido.py +++ b/eido/eido.py @@ -36,15 +36,22 @@ def build_argparser(): "-s", "--schema", required=True, help="PEP schema file in yaml format.") - parser.add_argument( - "-n", "--sample-name", required=False, - help="Name or index of the sample to validate. Only this sample will be validated.") - parser.add_argument( "-e", "--exclude-case", default=False, action="store_true", help="Whether to exclude the validation case from an error. " "Only the human readable message explaining the error will be raised. " "Useful when validating large PEPs.") + + group = parser.add_mutually_exclusive_group() + + group.add_argument( + "-n", "--sample-name", required=False, + help="Name or index of the sample to validate. Only this sample will be validated.") + + group.add_argument( + "-c", "--just-config", required=False, action="store_true", default=False, + help="Whether samples should be excluded from the validation.") + return parser @@ -188,7 +195,10 @@ def main(): _LOGGER.debug("Comparing Sample ('{}') in the Project " "('{}') against a schema: {}.".format(args.sample_name, args.pep, args.schema)) validate_sample(p, args.sample_name, args.schema, args.exclude_case) + elif args.just_config: + _LOGGER.debug("Comparing config ('{}') against a schema: {}.".format(args.pep, args.schema)) + validate_config(p, args.schema, args.exclude_case) else: - _LOGGER.debug("Comparing the Project ('{}') against a schema: {}.".format(args.pep, args.schema)) + _LOGGER.debug("Comparing Project ('{}') against a schema: {}.".format(args.pep, args.schema)) validate_project(p, args.schema, args.exclude_case) _LOGGER.info("Validation successful") From 2f0dbbd27b4724cdb15eb9f96601b72b0637c1b4 Mon Sep 17 00:00:00 2001 From: Michal Stolarczyk Date: Fri, 31 Jan 2020 13:11:21 -0500 Subject: [PATCH 7/9] work on unit tests --- tests/conftest.py | 6 ++++- .../schemas/test_schema_sample_invalid.yaml | 27 +++++++++++++++++++ tests/test_cli.py | 7 ++++- ...ject_validation.py => test_validations.py} | 21 +++++++++++++++ 4 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 tests/data/schemas/test_schema_sample_invalid.yaml rename tests/{test_project_validation.py => test_validations.py} (53%) diff --git a/tests/conftest.py b/tests/conftest.py index 967d3005..0b92b5d9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -40,4 +40,8 @@ def schema_samples_file_path(schemas_path): @pytest.fixture def schema_invalid_file_path(schemas_path): - return os.path.join(schemas_path, "test_schema_invalid.yaml") \ No newline at end of file + return os.path.join(schemas_path, "test_schema_invalid.yaml") + +@pytest.fixture +def schema_sample_invalid_file_path(schemas_path): + return os.path.join(schemas_path, "test_schema_sample_invalid.yaml") \ No newline at end of file diff --git a/tests/data/schemas/test_schema_sample_invalid.yaml b/tests/data/schemas/test_schema_sample_invalid.yaml new file mode 100644 index 00000000..eb23e23f --- /dev/null +++ b/tests/data/schemas/test_schema_sample_invalid.yaml @@ -0,0 +1,27 @@ +description: test PEP schema + +properties: + dcc: + type: object + properties: + compute_packages: + type: object + samples: + type: array + items: + type: object + properties: + sample_name: + type: string + protocol: + type: string + genome: + type: string + newattr: + type: string + required: + - newattr + +required: + - dcc + - samples diff --git a/tests/test_cli.py b/tests/test_cli.py index 923272d6..4174c2e0 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,4 +1,5 @@ import subprocess +import pytest class TestCLI: def test_cli_help(self): @@ -7,4 +8,8 @@ def test_cli_help(self): def test_cli_works(self, project_file_path, schema_file_path): out = subprocess.check_call(['eido', '-p', project_file_path, '-s', schema_file_path]) - assert out == 0 \ No newline at end of file + assert out == 0 + + def test_cli_exclusiveness(self, project_file_path, schema_file_path): + with pytest.raises(subprocess.CalledProcessError): + subprocess.check_call(['eido', '-p', project_file_path, '-s', schema_file_path, '-s', 'name', '-c']) \ No newline at end of file diff --git a/tests/test_project_validation.py b/tests/test_validations.py similarity index 53% rename from tests/test_project_validation.py rename to tests/test_validations.py index 3f37f71a..dee1541f 100644 --- a/tests/test_project_validation.py +++ b/tests/test_validations.py @@ -26,3 +26,24 @@ def test_validate_works_with_dict_schema(self, project_object, schema_file_path) def test_validate_raises_error_for_incorrect_schema_type(self, project_object, schema_arg): with pytest.raises(TypeError): validate_project(project=project_object, schema=schema_arg) + + +class TestSampleValidation: + @pytest.mark.parametrize("sample_name", [0, 1, "GSM1558746"]) + def test_validate_works(self, project_object, sample_name, schema_samples_file_path): + validate_sample(project=project_object, sample_name=sample_name, schema=schema_samples_file_path) + + @pytest.mark.parametrize("sample_name", [22, "bogus_sample_name"]) + def test_validate_raises_error_for_incorrect_sample_name(self, project_object, sample_name, schema_samples_file_path): + with pytest.raises((ValueError, IndexError)): + validate_sample(project=project_object, sample_name=sample_name, schema=schema_samples_file_path) + + @pytest.mark.parametrize("sample_name", [0, 1, "GSM1558746"]) + def test_validate_detects_invalid(self, project_object, sample_name, schema_sample_invalid_file_path): + with pytest.raises(ValidationError): + validate_sample(project=project_object, sample_name=sample_name, schema=schema_sample_invalid_file_path) + + +class TestConfigValidation: + def test_validate_succeeds_on_invalid_sample(self, project_object, schema_sample_invalid_file_path): + validate_config(project=project_object, schema=schema_sample_invalid_file_path) \ No newline at end of file From 1545cdc9199a1a5e7a67fec0ffbe317b7d70940c Mon Sep 17 00:00:00 2001 From: Michal Stolarczyk Date: Fri, 31 Jan 2020 13:16:01 -0500 Subject: [PATCH 8/9] update chagngelog --- docs/changelog.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index 79da8650..c26f319d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -5,7 +5,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [0.0.4] - unreleased ### Added - `validate_sample` function for sample level validation -- sample validation CLI support (via `-n`/`--sample-name` argument) +- sample validation CLI support (via `-n`/`--sample-name` argument) +- `validate_config` to facilitate samples exclusion in validation +- config validation CLI support (via `-c`/`--just-config` argument) ## [0.0.3] - 2020-01-30 ### Added From bc1e0f7122ea23df9bbb21d518794a91de05f561 Mon Sep 17 00:00:00 2001 From: Michal Stolarczyk Date: Fri, 31 Jan 2020 13:44:14 -0500 Subject: [PATCH 9/9] prep release --- docs/changelog.md | 2 +- eido/_version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index c26f319d..00302ce7 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,7 +2,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) and [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) format. -## [0.0.4] - unreleased +## [0.0.4] - 2020-01-31 ### Added - `validate_sample` function for sample level validation - sample validation CLI support (via `-n`/`--sample-name` argument) diff --git a/eido/_version.py b/eido/_version.py index b288a61a..81f0fdec 100644 --- a/eido/_version.py +++ b/eido/_version.py @@ -1 +1 @@ -__version__ = "0.0.4-dev" +__version__ = "0.0.4"