Skip to content

Commit 35e7753

Browse files
authored
Merge pull request #7 from pepkit/dev
v0.0.4
2 parents bf586a8 + bc1e0f7 commit 35e7753

8 files changed

+173
-17
lines changed

docs/changelog.md

+7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
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.
44

5+
## [0.0.4] - 2020-01-31
6+
### Added
7+
- `validate_sample` function for sample level validation
8+
- sample validation CLI support (via `-n`/`--sample-name` argument)
9+
- `validate_config` to facilitate samples exclusion in validation
10+
- config validation CLI support (via `-c`/`--just-config` argument)
11+
512
## [0.0.3] - 2020-01-30
613
### Added
714
- Option to exclude the validation case from error messages in both Python API and CLI app with `exclude_case` and `-e`/`--exclude-case`, respectively.

eido/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@
66
from .const import *
77
from .eido import *
88

9-
__all__ = ["validate_project"]
9+
__all__ = ["validate_project", "validate_sample", "validate_config"]
1010

1111
logmuse.init_logger(PKG_NAME)

eido/_version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.0.3"
1+
__version__ = "0.0.4"

eido/eido.py

+105-13
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import os
33
import jsonschema
44
import oyaml as yaml
5+
from copy import deepcopy as dpcpy
56

67
import logmuse
78
from ubiquerg import VersionInHelpParser
@@ -40,6 +41,17 @@ def build_argparser():
4041
help="Whether to exclude the validation case from an error. "
4142
"Only the human readable message explaining the error will be raised. "
4243
"Useful when validating large PEPs.")
44+
45+
group = parser.add_mutually_exclusive_group()
46+
47+
group.add_argument(
48+
"-n", "--sample-name", required=False,
49+
help="Name or index of the sample to validate. Only this sample will be validated.")
50+
51+
group.add_argument(
52+
"-c", "--just-config", required=False, action="store_true", default=False,
53+
help="Whether samples should be excluded from the validation.")
54+
4355
return parser
4456

4557

@@ -73,30 +85,97 @@ def _load_yaml(filepath):
7385
return data
7486

7587

76-
def validate_project(project, schema, exclude_case=False):
88+
def _read_schema(schema):
7789
"""
78-
Validate a project object against a schema
90+
Safely read schema from YAML-formatted file.
7991
80-
:param peppy.Project project: a project object to validate
92+
:param str | Mapping schema: path to the schema file
93+
or schema in a dict form
94+
:return dict: read schema
95+
:raise TypeError: if the schema arg is neither a Mapping nor a file path
96+
"""
97+
if isinstance(schema, str) and os.path.isfile(schema):
98+
return _load_yaml(schema)
99+
elif isinstance(schema, dict):
100+
return schema
101+
raise TypeError("schema has to be either a dict or a path to an existing file")
102+
103+
104+
def _validate_object(object, schema, exclude_case=False):
105+
"""
106+
Generic function to validate object against a schema
107+
108+
:param Mapping object: an object to validate
81109
:param str | dict schema: schema dict to validate against or a path to one
82110
:param bool exclude_case: whether to exclude validated objects from the error.
83111
Useful when used ith large projects
84112
"""
85-
if isinstance(schema, str) and os.path.isfile(schema):
86-
schema_dict = _load_yaml(schema)
87-
elif isinstance(schema, dict):
88-
schema_dict = schema
89-
else:
90-
raise TypeError("schema has to be either a dict or a path to an existing file")
91-
project_dict = project.to_dict()
92113
try:
93-
jsonschema.validate(project_dict, _preprocess_schema(schema_dict))
114+
jsonschema.validate(object, schema)
94115
except jsonschema.exceptions.ValidationError as e:
95116
if not exclude_case:
96117
raise e
97118
raise jsonschema.exceptions.ValidationError(e.message)
98119

99120

121+
def validate_project(project, schema, exclude_case=False):
122+
"""
123+
Validate a project object against a schema
124+
125+
:param peppy.Sample project: a project object to validate
126+
:param str | dict schema: schema dict to validate against or a path to one
127+
:param bool exclude_case: whether to exclude validated objects from the error.
128+
Useful when used ith large projects
129+
"""
130+
schema_dict = _read_schema(schema=schema)
131+
project_dict = project.to_dict()
132+
_validate_object(project_dict, _preprocess_schema(schema_dict), exclude_case)
133+
_LOGGER.debug("Project validation successful")
134+
135+
136+
def validate_sample(project, sample_name, schema, exclude_case=False):
137+
"""
138+
Validate the selected sample object against a schema
139+
140+
:param peppy.Project project: a project object to validate
141+
:param str | int sample_name: name or index of the sample to validate
142+
:param str | dict schema: schema dict to validate against or a path to one
143+
:param bool exclude_case: whether to exclude validated objects from the error.
144+
Useful when used ith large projects
145+
"""
146+
schema_dict = _read_schema(schema=schema)
147+
sample_dict = project.samples[sample_name] if isinstance(sample_name, int) \
148+
else project.get_sample(sample_name)
149+
sample_schema_dict = schema_dict["properties"]["samples"]["items"]
150+
_validate_object(sample_dict, sample_schema_dict, exclude_case)
151+
_LOGGER.debug("'{}' sample validation successful".format(sample_name))
152+
153+
154+
def validate_config(project, schema, exclude_case=False):
155+
"""
156+
Validate the config part of the Project object against a schema
157+
158+
:param peppy.Project project: a project object to validate
159+
:param str | dict schema: schema dict to validate against or a path to one
160+
:param bool exclude_case: whether to exclude validated objects from the error.
161+
Useful when used ith large projects
162+
"""
163+
schema_dict = _read_schema(schema=schema)
164+
schema_cpy = dpcpy(schema_dict)
165+
try:
166+
del schema_cpy["properties"]["samples"]
167+
except KeyError:
168+
pass
169+
if "required" in schema_cpy:
170+
try:
171+
schema_cpy["required"].remove("samples")
172+
except ValueError:
173+
pass
174+
project_dict = project.to_dict()
175+
_validate_object(project_dict, schema_cpy, exclude_case)
176+
_LOGGER.debug("Config validation successful")
177+
178+
100179
def main():
101180
""" Primary workflow """
102181
parser = logmuse.add_logging_options(build_argparser())
@@ -108,5 +187,18 @@ def main():
108187
_LOGGER = logmuse.logger_via_cli(args)
109188
_LOGGER.debug("Creating a Project object from: {}".format(args.pep))
110189
p = Project(args.pep)
111-
_LOGGER.debug("Comparing the Project ('{}') against a schema: {}.".format(args.pep, args.schema))
112-
validate_project(p, args.schema, args.exclude_case)
190+
if args.sample_name:
191+
try:
192+
args.sample_name = int(args.sample_name)
193+
except ValueError:
194+
pass
195+
_LOGGER.debug("Comparing Sample ('{}') in the Project "
196+
"('{}') against a schema: {}.".format(args.sample_name, args.pep, args.schema))
197+
validate_sample(p, args.sample_name, args.schema, args.exclude_case)
198+
elif args.just_config:
199+
_LOGGER.debug("Comparing config ('{}') against a schema: {}.".format(args.pep, args.schema))
200+
validate_config(p, args.schema, args.exclude_case)
201+
else:
202+
_LOGGER.debug("Comparing Project ('{}') against a schema: {}.".format(args.pep, args.schema))
203+
validate_project(p, args.schema, args.exclude_case)
204+
_LOGGER.info("Validation successful")

tests/conftest.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,8 @@ def schema_samples_file_path(schemas_path):
4040

4141
@pytest.fixture
4242
def schema_invalid_file_path(schemas_path):
43-
return os.path.join(schemas_path, "test_schema_invalid.yaml")
43+
return os.path.join(schemas_path, "test_schema_invalid.yaml")
44+
45+
@pytest.fixture
46+
def schema_sample_invalid_file_path(schemas_path):
47+
return os.path.join(schemas_path, "test_schema_sample_invalid.yaml")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
description: test PEP schema
2+
3+
properties:
4+
dcc:
5+
type: object
6+
properties:
7+
compute_packages:
8+
type: object
9+
samples:
10+
type: array
11+
items:
12+
type: object
13+
properties:
14+
sample_name:
15+
type: string
16+
protocol:
17+
type: string
18+
genome:
19+
type: string
20+
newattr:
21+
type: string
22+
required:
23+
- newattr
24+
25+
required:
26+
- dcc
27+
- samples

tests/test_cli.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import subprocess
2+
import pytest
23

34
class TestCLI:
45
def test_cli_help(self):
@@ -7,4 +8,8 @@ def test_cli_help(self):
78

89
def test_cli_works(self, project_file_path, schema_file_path):
910
out = subprocess.check_call(['eido', '-p', project_file_path, '-s', schema_file_path])
10-
assert out == 0
11+
assert out == 0
12+
13+
def test_cli_exclusiveness(self, project_file_path, schema_file_path):
14+
with pytest.raises(subprocess.CalledProcessError):
15+
subprocess.check_call(['eido', '-p', project_file_path, '-s', schema_file_path, '-s', 'name', '-c'])

tests/test_project_validation.py tests/test_validations.py

+21
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,24 @@ def test_validate_works_with_dict_schema(self, project_object, schema_file_path)
2626
def test_validate_raises_error_for_incorrect_schema_type(self, project_object, schema_arg):
2727
with pytest.raises(TypeError):
2828
validate_project(project=project_object, schema=schema_arg)
29+
30+
31+
class TestSampleValidation:
32+
@pytest.mark.parametrize("sample_name", [0, 1, "GSM1558746"])
33+
def test_validate_works(self, project_object, sample_name, schema_samples_file_path):
34+
validate_sample(project=project_object, sample_name=sample_name, schema=schema_samples_file_path)
35+
36+
@pytest.mark.parametrize("sample_name", [22, "bogus_sample_name"])
37+
def test_validate_raises_error_for_incorrect_sample_name(self, project_object, sample_name, schema_samples_file_path):
38+
with pytest.raises((ValueError, IndexError)):
39+
validate_sample(project=project_object, sample_name=sample_name, schema=schema_samples_file_path)
40+
41+
@pytest.mark.parametrize("sample_name", [0, 1, "GSM1558746"])
42+
def test_validate_detects_invalid(self, project_object, sample_name, schema_sample_invalid_file_path):
43+
with pytest.raises(ValidationError):
44+
validate_sample(project=project_object, sample_name=sample_name, schema=schema_sample_invalid_file_path)
45+
46+
47+
class TestConfigValidation:
48+
def test_validate_succeeds_on_invalid_sample(self, project_object, schema_sample_invalid_file_path):
49+
validate_config(project=project_object, schema=schema_sample_invalid_file_path)

0 commit comments

Comments
 (0)