diff --git a/tools/ok_validator/README.md b/tools/ok_validator/README.md new file mode 100644 index 0000000..1c31d70 --- /dev/null +++ b/tools/ok_validator/README.md @@ -0,0 +1 @@ +# OPen Knowledge Validator \ No newline at end of file diff --git a/tools/ok_validator/__init__.py b/tools/ok_validator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/ok_validator/pyproject.toml b/tools/ok_validator/pyproject.toml new file mode 100644 index 0000000..96f3c83 --- /dev/null +++ b/tools/ok_validator/pyproject.toml @@ -0,0 +1,33 @@ +[tool.poetry] +name="okvalidator" +version="0.1.0" +description="An Open Knowledge validator" +authors = [ + "Elijah Ahianyo elijahahianyo@gmail.com", +] +readme="README.md" + +repository = "https://github.com/helpfulengineering/project-data-platform/tools/ok_validator" + +classifiers = [ + "Development Status :: 3 - Alpha", + + "Intended Audience :: Developers", + "Topic :: Software Development :: Open Knowledge", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3 :: Only", +] + + +[tool.poetry.dependencies] +python = "^3.7" +pyyaml = "6.0" + +[tool.poetry.group.dev.dependencies] +pytest = "^7.2.1" \ No newline at end of file diff --git a/tools/ok_validator/src/okh.py b/tools/ok_validator/src/okh.py new file mode 100644 index 0000000..d154d07 --- /dev/null +++ b/tools/ok_validator/src/okh.py @@ -0,0 +1,11 @@ +from .validate import OKValidator + +__REQUIRED_FIELDS__ = [ + "bom", + "title", +] + + +class OKHValidator(OKValidator): + def __init__(self): + super().__init__(__REQUIRED_FIELDS__) diff --git a/tools/ok_validator/src/okw.py b/tools/ok_validator/src/okw.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/ok_validator/src/validate.py b/tools/ok_validator/src/validate.py new file mode 100644 index 0000000..311dce0 --- /dev/null +++ b/tools/ok_validator/src/validate.py @@ -0,0 +1,88 @@ +"""Open knowledge validator""" +import yaml +from pathlib import Path +from typing import List, Union +from collections import namedtuple + +Error = namedtuple("Error", ["type", "msg"]) + + +class OKValidator: + def __init__(self, required_fields: List[str]): + self.required_fields = required_fields + + def _validate_yaml(self, yaml_content: dict) -> bool: + """ + Validate the YAML content to check if it contains the required fields. + """ + for field in self.required_fields: + if field not in yaml_content: + return False + return True + + def validate(self, src: Union[str, Path, dict], raise_exception=False) -> bool: + """ + Validate the YAML source, which can be a file path, YAML content string, + Path object, or YAML dictionary. + """ + # breakpoint() + if not isinstance(src, Union[str, dict, Path]): + return self.return_value_or_error( + Error( + ValueError, + "src should be one of the following: a string path, " + "a Path object, or Yaml dict", + ), + raise_exception, + ) + + if isinstance(src, dict): + return self._validate_yaml(src) + + if isinstance(src, Path): + src = str(src) + + try: + with open(src, "r") as yaml_file: + yaml_content = yaml.safe_load(yaml_file) + if yaml_content is None: + return self.return_value_or_error( + Error( + ValueError, + "The YAML file is empty or contains invalid " "syntax.", + ), + raise_exception, + ) + else: + return self._validate_yaml(yaml_content) + except FileNotFoundError: + return self.return_value_or_error( + Error( + FileNotFoundError, + "File not found. Please provide a valid YAML " "file path.", + ), + raise_exception, + ) + except yaml.YAMLError: + return self.return_value_or_error( + Error(yaml.YAMLError, ""), raise_exception + ) + + @staticmethod + def return_value_or_error(result: Error, raise_exception: bool = False): + """Return a bool or raise an exception. + + Args: + result: exception to raise. + raise_exception: If set to true, the provided exception will be raised. + """ + if not raise_exception: + return False + + if not isinstance(result, Error): + raise TypeError( + f"result arg needs to be of type,{type(Error)}. " + f"Got {type(result)} instead." + ) + + raise result.type(result.msg) diff --git a/tools/ok_validator/tests/__init__.py b/tools/ok_validator/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/ok_validator/tests/test_okh.py b/tools/ok_validator/tests/test_okh.py new file mode 100644 index 0000000..2eea3f3 --- /dev/null +++ b/tools/ok_validator/tests/test_okh.py @@ -0,0 +1,3 @@ +"""Unit tests for okh validator""" + +# TODO: add tests diff --git a/tools/ok_validator/tests/test_validate.py b/tools/ok_validator/tests/test_validate.py new file mode 100644 index 0000000..3b86c5e --- /dev/null +++ b/tools/ok_validator/tests/test_validate.py @@ -0,0 +1,109 @@ +"""Unit tests for ok validator""" + +import pytest +from tools.ok_validator.src.validate import OKValidator, Error + + +@pytest.fixture +def okh_string(): + return """ +title: mock okh title. +description: mock description for okh. +bom: mock bom field. + """ + + +@pytest.fixture +def okh_yaml_file(tmp_path, okh_string): + okh_file = tmp_path / "okh.yaml" + okh_file.touch() + okh_file.write_text(okh_string) + + return okh_file + + +@pytest.fixture +def okh_dict(): + return {"title": "mock okh title.", "description": "mock description for okh.", "bom": "mock bom field."} + + +@pytest.fixture +def ok_validator(): + return OKValidator(["bom", "title"]) + + +@pytest.mark.parametrize( + "fixture", ["okh_yaml_file", "okh_dict"] +) +def test_validate_yaml_file(fixture, request, ok_validator): + """Test that the validate method returns the correct boolean. + + Args: + fixture: fixtures of accepted src formats. + request: pytest request. + ok_validator: Validator instance. + """ + okh = request.getfixturevalue(fixture) + assert ok_validator.validate(okh) + + +def test_validate_with_non_existent_file(tmp_path, ok_validator): + """Test that the validate method returns False when file does not + exist. + + Args: + tmp_path: Test location. + ok_validator: Validator instance. + """ + file = tmp_path / "okh.yaml" + assert not ok_validator.validate(file) + + +def test_validate_with_invalid_file(tmp_path, ok_validator): + """Test that the validate method returns False when an invalid Yaml file + is passed. + + Args: + tmp_path: Test location. + ok_validator: Validator Instance. + """ + file = tmp_path / "okh.yaml" + file.touch() + assert not ok_validator.validate(file) + + file.write_text("""title: invalid_field: mock title +description: mock description. + """) + + assert not ok_validator.validate(file) + + +def test_validate_raise_exception(tmp_path, ok_validator): + """Test that an exception is raised when the raise_exception parameter + is set to True on the validator method. + + Args: + tmp_path: The test location. + ok_validator: Validator instance. + """ + file = tmp_path / "okh.yaml" + + with pytest.raises(FileNotFoundError): + ok_validator.validate(file, raise_exception=True) + + +def test_return_value_or_error(): + """Test that the right exception is raised.""" + + error = Error(type=ValueError, msg="raised a value error") + with pytest.raises(error.type) as err: + OKValidator.return_value_or_error(error, raise_exception=True) + assert err.value.args[0] == error.msg + + +def test_return_value_or_error_with_wrong_error_type(): + """Test that an exception is raised when the wrong type is provided + with the `raise_exception` flag set to True. + """ + with pytest.raises(TypeError): + OKValidator.return_value_or_error(str, raise_exception=True)