From a19f4bde9518b01581ad67596d00e62829a20796 Mon Sep 17 00:00:00 2001 From: muddymudskipper Date: Thu, 27 Jun 2024 09:42:49 +0100 Subject: [PATCH] rename reasoning plugin, add validate plugin --- .copier-answers.env | 2 +- .copier-answers.yml | 4 +- .gitignore | 2 +- ...robotreason.iml => cmem-plugin-reason.iml} | 2 +- README-public.md | 2 +- README.md | 4 +- TaskfileCustom.yaml | 2 +- cmem_plugin_reason/__init__.py | 1 + .../obofoundry.png | Bin .../plugin_reason.py | 6 +- cmem_plugin_reason/plugin_validate.py | 295 ++++++++++++++++++ cmem_plugin_robotreason/__init__.py | 1 - poetry.lock | 23 +- pyproject.toml | 7 +- tests/test_elk.ttl | 2 +- tests/test_emr.ttl | 2 +- tests/test_hermit.ttl | 2 +- tests/test_jfact.ttl | 2 +- tests/test_reason.py | 90 ++++++ tests/test_robotreason.py | 70 ----- tests/test_structural.ttl | 2 +- tests/test_validate.ttl | 36 +++ tests/test_whelk.ttl | 2 +- 23 files changed, 463 insertions(+), 96 deletions(-) rename .idea/{cmem-plugin-robotreason.iml => cmem-plugin-reason.iml} (83%) create mode 100644 cmem_plugin_reason/__init__.py rename {cmem_plugin_robotreason => cmem_plugin_reason}/obofoundry.png (100%) rename cmem_plugin_robotreason/plugin_robotreason.py => cmem_plugin_reason/plugin_reason.py (99%) create mode 100644 cmem_plugin_reason/plugin_validate.py delete mode 100644 cmem_plugin_robotreason/__init__.py create mode 100644 tests/test_reason.py delete mode 100644 tests/test_robotreason.py create mode 100644 tests/test_validate.ttl diff --git a/.copier-answers.env b/.copier-answers.env index a66daf9..7991766 100644 --- a/.copier-answers.env +++ b/.copier-answers.env @@ -1,2 +1,2 @@ # Changes here will be overwritten by Copier -project_slug=robotreason +project_slug=reason diff --git a/.copier-answers.yml b/.copier-answers.yml index c826aa9..4c0aa62 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -3,8 +3,8 @@ _commit: v6.3.1 _src_path: gh:eccenca/cmem-plugin-template author_mail: info@eccenca.com author_name: eccenca GmbH -github_page: https://github.com/eccenca/cmem-plugin-robotreason +github_page: https://github.com/eccenca/cmem-plugin-reason project_description: Reasoning with Robot -project_slug: robotreason +project_slug: reason pypi: false diff --git a/.gitignore b/.gitignore index 1dc9771..6bcba1f 100644 --- a/.gitignore +++ b/.gitignore @@ -149,4 +149,4 @@ artifacts .task # ROBOT -cmem_plugin_robotreason/bin/ \ No newline at end of file +cmem_plugin_reason/bin/ \ No newline at end of file diff --git a/.idea/cmem-plugin-robotreason.iml b/.idea/cmem-plugin-reason.iml similarity index 83% rename from .idea/cmem-plugin-robotreason.iml rename to .idea/cmem-plugin-reason.iml index 906fdef..15804b0 100644 --- a/.idea/cmem-plugin-robotreason.iml +++ b/.idea/cmem-plugin-reason.iml @@ -2,7 +2,7 @@ - + diff --git a/README-public.md b/README-public.md index 555f263..bfc2ad3 100644 --- a/README-public.md +++ b/README-public.md @@ -1,4 +1,4 @@ -# cmem-plugin-robotreason +# cmem-plugin-reason This [eccenca](https://eccenca.com) [Corporate Memory](https://documentation.eccenca.com) workflow plugin performs reasoning using [ROBOT](http://robot.obolibrary.org/) (ROBOT is an OBO Tool). It takes an OWL ontology and a data graph as inputs and writes the reasoning result to a specified graph. diff --git a/README.md b/README.md index 8d40e51..7c6fccb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# cmem-plugin-robotreason +# cmem-plugin-reason Reasoning with ROBOT @@ -13,7 +13,7 @@ an OBO Tool). It takes an OWL ontology and a data graph as inputs and writes the :bulb: Prior to the build process, the file _robot.jar_ (v1.9.6) is automatically downloaded from the [ROBOT GitHub repository](https://github.com/ontodev/robot). The file is downloaded to the directory -_cmem_plugin_robotreason/workflow/bin_ and is not removed automatically when running `task clean`. The file can be +_cmem_plugin_reason/workflow/bin_ and is not removed automatically when running `task clean`. The file can be removed with `task custom:clean_robot`. ``` diff --git a/TaskfileCustom.yaml b/TaskfileCustom.yaml index a073589..94bf6e8 100644 --- a/TaskfileCustom.yaml +++ b/TaskfileCustom.yaml @@ -2,7 +2,7 @@ version: '3' vars: - ROBOT_DIR: cmem_plugin_robotreason/bin + ROBOT_DIR: cmem_plugin_reason/bin tasks: diff --git a/cmem_plugin_reason/__init__.py b/cmem_plugin_reason/__init__.py new file mode 100644 index 0000000..093adf3 --- /dev/null +++ b/cmem_plugin_reason/__init__.py @@ -0,0 +1 @@ +"""reason - main package""" diff --git a/cmem_plugin_robotreason/obofoundry.png b/cmem_plugin_reason/obofoundry.png similarity index 100% rename from cmem_plugin_robotreason/obofoundry.png rename to cmem_plugin_reason/obofoundry.png diff --git a/cmem_plugin_robotreason/plugin_robotreason.py b/cmem_plugin_reason/plugin_reason.py similarity index 99% rename from cmem_plugin_robotreason/plugin_robotreason.py rename to cmem_plugin_reason/plugin_reason.py index cb0bafe..73cf10a 100644 --- a/cmem_plugin_robotreason/plugin_robotreason.py +++ b/cmem_plugin_reason/plugin_reason.py @@ -54,7 +54,7 @@ def convert_iri_to_filename(value: str) -> str: @Plugin( - label="Reasoning with ROBOT", + label="Reason", icon=Icon(file_name="obofoundry.png", package=__package__), description="Given a data and an ontology graph, this task performs reasoning using ROBOT.", documentation="""A task performing reasoning using ROBOT (ROBOT is an OBO Tool). @@ -208,7 +208,7 @@ def convert_iri_to_filename(value: str) -> str: ), ], ) -class RobotReasonPlugin(WorkflowPlugin): +class ReasonPlugin(WorkflowPlugin): """Robot reasoning plugin""" def __init__( # noqa: PLR0913 @@ -343,7 +343,7 @@ def reason(self, graphs: dict) -> None: f'"Reasoning result set of <{self.data_graph_iri}> and ' f'<{self.ontology_graph_iri}>" en ' f"--language-annotation prov:wasGeneratedBy " - f'"cmem-plugin-robotreason ({self.reasoner})" en ' + f'"cmem-plugin-reason ({self.reasoner})" en ' f'--link-annotation prov:wasDerivedFrom "{self.data_graph_iri}" ' f"--link-annotation prov:wasDerivedFrom " f'"{self.ontology_graph_iri}" ' diff --git a/cmem_plugin_reason/plugin_validate.py b/cmem_plugin_reason/plugin_validate.py new file mode 100644 index 0000000..eec3f74 --- /dev/null +++ b/cmem_plugin_reason/plugin_validate.py @@ -0,0 +1,295 @@ +"""Random values workflow plugin module""" + +import re +import shlex +import unicodedata +from collections import OrderedDict +from collections.abc import Sequence +from datetime import UTC, datetime +from pathlib import Path +from subprocess import run +from time import time +from uuid import uuid4 +from xml.etree.ElementTree import ( + Element, + SubElement, + tostring, +) + +import validators.url +from cmem.cmempy.dp.proxy.graph import get, get_graph_import_tree, post_streamed +from cmem.cmempy.workspace.projects.resources.resource import create_resource +from cmem_plugin_base.dataintegration.context import ExecutionContext +from cmem_plugin_base.dataintegration.description import Icon, Plugin, PluginParameter +from cmem_plugin_base.dataintegration.entity import ( + Entities, + Entity, + EntityPath, + EntitySchema, +) +from cmem_plugin_base.dataintegration.parameter.choice import ChoiceParameterType +from cmem_plugin_base.dataintegration.parameter.graph import GraphParameterType +from cmem_plugin_base.dataintegration.plugins import WorkflowPlugin +from cmem_plugin_base.dataintegration.types import BoolParameterType, StringParameterType +from cmem_plugin_base.dataintegration.utils import setup_cmempy_user_access +from defusedxml import minidom +from pathvalidate import validate_filename + +from . import __path__ + +ROBOT = Path(__path__[0]) / "bin" / "robot.jar" +REASONERS = OrderedDict( + { + "elk": "ELK", + "emr": "Expression Materializing Reasoner", + "hermit": "HermiT", + "jfact": "JFact", + "structural": "Structural Reasoner", + "whelk": "Whelk", + } +) + + +def convert_iri_to_filename(value: str) -> str: + """Convert IRI to filename""" + value = unicodedata.normalize("NFKD", value).encode("ascii", "ignore").decode("ascii") + value = re.sub(r"\.", "_", value.lower()) + value = re.sub(r"/", "_", value.lower()) + value = re.sub(r"[^\w\s-]", "", value.lower()) + value = re.sub(r"[-\s]+", "-", value).strip("-_") + return value + ".nt" + + +@Plugin( + label="Validate ontology consistency", + description="", + documentation="""""", + icon=Icon(package=__package__, file_name="obofoundry.png"), + parameters=[ + PluginParameter( + param_type=GraphParameterType(classes=["http://www.w3.org/2002/07/owl#Ontology"]), + name="ontology_graph_iri", + label="Ontology_graph_IRI", + description="The IRI of the input ontology graph.", + ), + PluginParameter( + param_type=ChoiceParameterType(REASONERS), + name="reasoner", + label="Reasoner", + description="Reasoner option.", + default_value="elk", + ), + PluginParameter( + param_type=BoolParameterType(), + name="write_md", + label="Write Markdown explanation file", + description="Write Markdownn file with explanation to project.", + default_value=False, + ), + PluginParameter( + param_type=BoolParameterType(), + name="produce_graph", + label="Produce output graph", + description="Produce graph with explanation.", + default_value=False, + ), + PluginParameter( + param_type=StringParameterType(), + name="output_graph_iri", + label="Output graph IRI", + description="The IRI of the output graph for the inconsistency validation.", + ), + PluginParameter( + param_type=StringParameterType(), + name="md_filename", + label="Output filename", + description="The filename of the Markdown file with the explanation of " + "inconsistencies.", + ), + PluginParameter( + param_type=BoolParameterType(), + name="stop_at_inconsistencies", + label="Stop at inconsistencies", + description="Raise an error if inconsistencies are found. If enabled, the plugin does " + "not output entities.", + default_value=False, + ), + ], +) +class ValidatePlugin(WorkflowPlugin): + """Example Workflow Plugin: Random Values""" + + def __init__( # noqa: PLR0913 + self, + ontology_graph_iri: str = "", + reasoner: str = "elk", + produce_graph: bool = False, + output_graph_iri: str = "", + write_md: bool = False, + md_filename: str = "", + stop_at_inconsistencies: bool = False, + ) -> None: + errors = "" + if not validators.url(ontology_graph_iri): + errors += "Invalid IRI for parameter Ontology graph IRI. " + if reasoner not in REASONERS: + errors += "Invalid value for parameter Reasoner. " + if produce_graph and not validators.url(output_graph_iri): + errors += "Invalid IRI for parameter Output graph IRI. " + if write_md: + try: + validate_filename(md_filename) + except: # noqa: E722 + errors += "Invalid filename for parameter Output filename. " + if errors: + raise ValueError(errors[:-1]) + + self.ontology_graph_iri = ontology_graph_iri + self.reasoner = reasoner + self.produce_graph = produce_graph + self.output_graph_iri = output_graph_iri + self.write_md = write_md + self.stop_at_inconsistencies = stop_at_inconsistencies + self.temp = f"robot_{uuid4().hex}" + self.md_filename = md_filename if md_filename and write_md else "mdfile.md" + + def create_xml_catalog_file(self, graphs: dict) -> None: + """Create XML catalog file""" + file_name = Path(self.temp) / "catalog-v001.xml" + catalog = Element("catalog") + catalog.set("prefer", "public") + catalog.set("xmlns", "urn:oasis:names:tc:entity:xmlns:xml:catalog") + for i, graph in enumerate(graphs): + uri = SubElement(catalog, "uri") + uri.set("id", f"id{i}") + uri.set("name", graph) + uri.set("uri", graphs[graph]) + reparsed = minidom.parseString(tostring(catalog, "utf-8")).toxml() + with Path(file_name).open("w", encoding="utf-8") as file: + file.truncate(0) + file.write(reparsed) + + def get_graphs(self, graphs: dict, context: ExecutionContext) -> None: + """Get graphs from CMEM""" + if not Path(self.temp).exists(): + Path(self.temp).mkdir(parents=True) + for graph in graphs: + with (Path(self.temp) / graphs[graph]).open("w", encoding="utf-8") as file: + setup_cmempy_user_access(context.user) + file.write(get(graph).text) + + def get_graphs_tree(self) -> dict: + """Get graph import tree""" + graphs = {self.ontology_graph_iri: convert_iri_to_filename(self.ontology_graph_iri)} + tree = get_graph_import_tree(self.ontology_graph_iri) + for value in tree["tree"].values(): + for iri in value: + if iri not in graphs: + graphs[iri] = convert_iri_to_filename(iri) + return graphs + + def validate(self, graphs: dict) -> None: + """Reason""" + data_location = f"{self.temp}/{graphs[self.ontology_graph_iri]}" + utctime = str(datetime.fromtimestamp(int(time()), tz=UTC))[:-6].replace(" ", "T") + "Z" + + cmd = ( + f'java -XX:MaxRAMPercentage=15 -jar {ROBOT} merge --input "{data_location}" ' + f"explain --reasoner {self.reasoner} -M inconsistency " + f'--explanation "{self.temp}/{self.md_filename}"' + ) + + if self.produce_graph: + cmd += ( + f' annotate --ontology-iri "{self.output_graph_iri}" ' + f'--language-annotation rdfs:label "Ontology Validation Result {utctime}" en ' + f"--language-annotation rdfs:comment " + f'"Ontology validation of <{self.ontology_graph_iri}>" en ' + f"--language-annotation prov:wasGeneratedBy " + f'"cmem-plugin-validate ({self.reasoner})" en ' + f'--link-annotation prov:wasDerivedFrom "{self.ontology_graph_iri}" ' + f'--typed-annotation dc:created "{utctime}" xsd:dateTime ' + f'--output "{self.temp}/output.ttl"' + ) + + response = run(shlex.split(cmd), check=False, capture_output=True) # noqa: S603 + if response.returncode != 0: + if response.stdout: + raise OSError(response.stdout.decode()) + if response.stderr: + raise OSError(response.stderr.decode()) + raise OSError("ROBOT error") + + def send_output_graph(self) -> None: + """Send result graph""" + post_streamed( + self.output_graph_iri, + str(Path(self.temp) / "output.ttl"), + replace=True, + content_type="text/turtle", + ) + + def make_resource(self, context: ExecutionContext) -> None: + """Make MD resource in project""" + create_resource( + project_name=context.task.project_id(), + resource_name=self.md_filename, + file_resource=(Path(self.temp) / self.md_filename).open("r"), + replace=True, + ) + + def clean_up(self, graphs: dict) -> None: + """Remove temporary files""" + files = ["catalog-v001.xml", "result.ttl", self.md_filename] + files += list(graphs.values()) + for file in files: + try: + (Path(self.temp) / file).unlink() + except (OSError, FileNotFoundError) as err: + self.log.warning(f"Cannot remove file {file} ({err})") + try: + Path(self.temp).rmdir() + except (OSError, FileNotFoundError) as err: + self.log.warning(f"Cannot remove directory {self.temp} ({err})") + + def execute( + self, + inputs: Sequence[Entities], # noqa: ARG002 + context: ExecutionContext, + ) -> Entities | None: + """Run the workflow operator.""" + setup_cmempy_user_access(context.user) + graphs = self.get_graphs_tree() + self.get_graphs(graphs, context) + self.create_xml_catalog_file(graphs) + self.validate(graphs) + + text = (Path(self.temp) / self.md_filename).read_text() + if text == "No explanations found.": + self.clean_up(graphs) + return None + + if self.produce_graph: + setup_cmempy_user_access(context.user) + self.send_output_graph() + + if self.write_md: + setup_cmempy_user_access(context.user) + self.make_resource(context) + + self.clean_up(graphs) + + if self.stop_at_inconsistencies: + raise RuntimeError("Inconsistencies found in Ontology.") + + entities = [ + Entity( + uri="https://eccenca.com/plugin_validateontology/md", + values=[[text]], + ) + ] + schema = EntitySchema( + type_uri="https://eccenca.com/plugin_validateontology/text", + paths=[EntityPath(path="text")], + ) + return Entities(entities=iter(entities), schema=schema) diff --git a/cmem_plugin_robotreason/__init__.py b/cmem_plugin_robotreason/__init__.py deleted file mode 100644 index 41d8786..0000000 --- a/cmem_plugin_robotreason/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""robotreason - main package""" diff --git a/poetry.lock b/poetry.lock index 8102a55..ce33ffb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -609,6 +609,21 @@ files = [ {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] +[[package]] +name = "pathvalidate" +version = "3.2.0" +description = "pathvalidate is a Python library to sanitize/validate a string such as filenames/file-paths/etc." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathvalidate-3.2.0-py3-none-any.whl", hash = "sha256:cc593caa6299b22b37f228148257997e2fa850eea2daf7e4cc9205cef6908dee"}, + {file = "pathvalidate-3.2.0.tar.gz", hash = "sha256:5e8378cf6712bff67fbe7a8307d99fa8c1a0cb28aa477056f8fc374f0dff24ad"}, +] + +[package.extras] +docs = ["Sphinx (>=2.4)", "sphinx-rtd-theme (>=1.2.2)", "urllib3 (<2)"] +test = ["Faker (>=1.0.8)", "allpairspy (>=2)", "click (>=6.2)", "pytest (>=6.0.1)", "pytest-discord (>=0.1.4)", "pytest-md-report (>=0.4.1)"] + [[package]] name = "pillow" version = "10.3.0" @@ -697,13 +712,13 @@ xmp = ["defusedxml"] [[package]] name = "pip" -version = "24.1" +version = "24.1.1" description = "The PyPA recommended tool for installing Python packages." optional = false python-versions = ">=3.8" files = [ - {file = "pip-24.1-py3-none-any.whl", hash = "sha256:a775837439bf5da2c1a0c2fa43d5744854497c689ddbd9344cf3ea6d00598540"}, - {file = "pip-24.1.tar.gz", hash = "sha256:bdae551038c0ce6a83030b4aedef27fc95f0daa683593fea22fa05e55ed8e317"}, + {file = "pip-24.1.1-py3-none-any.whl", hash = "sha256:efca15145a95e95c00608afeab66311d40bfb73bb2266a855befd705e6bb15a0"}, + {file = "pip-24.1.1.tar.gz", hash = "sha256:5aa64f65e1952733ee0a9a9b1f52496ebdb3f3077cc46f80a16d983b58d1180a"}, ] [[package]] @@ -1106,4 +1121,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "7a52a090492614f86790e76ad9ac8c30683a2a30653138b4bd437bce1df1f0fc" +content-hash = "ae1efe3e579bbb64c99746f66ee3582f56760e8c1499b99a46d72d4881133c38" diff --git a/pyproject.toml b/pyproject.toml index b2dcbb6..ca43004 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [tool.poetry] -name = "cmem-plugin-robotreason" +name = "cmem-plugin-reason" version = "0.0.0.post1.dev0+6211d4d" license = "Apache-2.0" description = "Reasoning with Robot" @@ -13,14 +13,15 @@ readme = "README-public.md" keywords = [ "eccenca Corporate Memory", "plugin" ] -include = ["cmem_plugin_robotreason/bin/*"] +include = ["cmem_plugin_reason/bin/*"] -homepage = "https://github.com/eccenca/cmem-plugin-robotreason" +homepage = "https://github.com/eccenca/cmem-plugin-reason" [tool.poetry.dependencies] # if you need to change python version here, change it also in .python-version python = "^3.11" validators = "^0.28.3" +pathvalidate = "^3.2.0" [tool.poetry.dependencies.cmem-plugin-base] version = "^4.5.0" diff --git a/tests/test_elk.ttl b/tests/test_elk.ttl index f766944..8eb0e56 100644 --- a/tests/test_elk.ttl +++ b/tests/test_elk.ttl @@ -13,7 +13,7 @@ rdfs:comment "Reasoning result set of and "@en ; prov:wasDerivedFrom , ; - prov:wasGeneratedBy "cmem-plugin-robotreason (elk)"@en . + prov:wasGeneratedBy "cmem-plugin-reason (elk)"@en . ################################################################# # Individuals diff --git a/tests/test_emr.ttl b/tests/test_emr.ttl index 3888f50..bb10014 100644 --- a/tests/test_emr.ttl +++ b/tests/test_emr.ttl @@ -13,7 +13,7 @@ rdfs:comment "Reasoning result set of and "@en ; prov:wasDerivedFrom , ; - prov:wasGeneratedBy "cmem-plugin-robotreason (emr)"@en . + prov:wasGeneratedBy "cmem-plugin-reason (emr)"@en . ################################################################# # Individuals diff --git a/tests/test_hermit.ttl b/tests/test_hermit.ttl index 77234d8..e1e865e 100644 --- a/tests/test_hermit.ttl +++ b/tests/test_hermit.ttl @@ -13,7 +13,7 @@ rdfs:comment "Reasoning result set of and "@en ; prov:wasDerivedFrom , ; - prov:wasGeneratedBy "cmem-plugin-robotreason (hermit)"@en . + prov:wasGeneratedBy "cmem-plugin-reason (hermit)"@en . ################################################################# # Individuals diff --git a/tests/test_jfact.ttl b/tests/test_jfact.ttl index a9995f9..f13d8f3 100644 --- a/tests/test_jfact.ttl +++ b/tests/test_jfact.ttl @@ -13,7 +13,7 @@ rdfs:comment "Reasoning result set of and "@en ; prov:wasDerivedFrom , ; - prov:wasGeneratedBy "cmem-plugin-robotreason (jfact)"@en . + prov:wasGeneratedBy "cmem-plugin-reason (jfact)"@en . ################################################################# # Individuals diff --git a/tests/test_reason.py b/tests/test_reason.py new file mode 100644 index 0000000..0a9a894 --- /dev/null +++ b/tests/test_reason.py @@ -0,0 +1,90 @@ +"""Plugin tests.""" + +from pathlib import Path + +import pytest +from cmem.cmempy.dp.proxy.graph import delete, get, post +from rdflib import DCTERMS, OWL, RDF, RDFS, Graph, URIRef +from rdflib.compare import to_isomorphic + +from cmem_plugin_reason.plugin_reason import ReasonPlugin +from cmem_plugin_reason.plugin_validate import ValidatePlugin +from tests.utils import TestExecutionContext, needs_cmem + +from . import __path__ + +UID = "e02aaed014c94e0c91bf960fed127750" +REASON_DATA_GRAPH_IRI = f"https://ns.eccenca.com/reasoning/{UID}/data/" +REASON_ONTOLOGY_GRAPH_IRI = f"https://ns.eccenca.com/reasoning/{UID}/vocab/" +REASON_RESULT_GRAPH_IRI = f"https://ns.eccenca.com/reasoning/{UID}/result/" +VALIDATE_ONTOLOGY_GRAPH_IRI = f"https://ns.eccenca.com/validateontology/{UID}/vocab/" + + +@pytest.fixture() +def _setup_reason(request: pytest.FixtureRequest) -> None: + """Set up""" + res = post(REASON_DATA_GRAPH_IRI, Path(__path__[0]) / "dataset_owl.ttl", replace=True) + if res.status_code != 204: # noqa: PLR2004 + raise ValueError(f"Response {res.status_code}: {res.url}") + res = post(REASON_ONTOLOGY_GRAPH_IRI, Path(__path__[0]) / "vocab.ttl", replace=True) + if res.status_code != 204: # noqa: PLR2004 + raise ValueError(f"Response {res.status_code}: {res.url}") + + request.addfinalizer(lambda: delete(REASON_DATA_GRAPH_IRI)) + request.addfinalizer(lambda: delete(REASON_ONTOLOGY_GRAPH_IRI)) + request.addfinalizer(lambda: delete(REASON_RESULT_GRAPH_IRI)) # noqa: PT021 + + +@pytest.fixture() +def _setup_validate(request: pytest.FixtureRequest) -> None: + """Set up""" + res = post(VALIDATE_ONTOLOGY_GRAPH_IRI, Path(__path__[0]) / "test_validate.ttl", replace=True) + if res.status_code != 204: # noqa: PLR2004 + raise ValueError(f"Response {res.status_code}: {res.url}") + + request.addfinalizer(lambda: delete(VALIDATE_ONTOLOGY_GRAPH_IRI)) # noqa: PT021 + + +@needs_cmem +def tests_reason(_setup_reason: None) -> None: + """Tests for reason plugin""" + + def test_reasoner(reasoner: str, err_list: list) -> list: + ReasonPlugin( + data_graph_iri=REASON_DATA_GRAPH_IRI, + ontology_graph_iri=REASON_ONTOLOGY_GRAPH_IRI, + result_graph_iri=REASON_RESULT_GRAPH_IRI, + reasoner=reasoner, + sub_class=False, + class_assertion=True, + property_assertion=True, + ).execute((), context=TestExecutionContext()) + + result = Graph().parse( + data=get(REASON_RESULT_GRAPH_IRI, owl_imports_resolution=False).text, + format="turtle", + ) + result.remove((URIRef(REASON_RESULT_GRAPH_IRI), DCTERMS.created, None)) + result.remove((URIRef(REASON_RESULT_GRAPH_IRI), RDFS.label, None)) + result.remove((None, RDF.type, OWL.AnnotationProperty)) + + test = Graph().parse(Path(__path__[0]) / f"test_{reasoner}.ttl", format="turtle") + if to_isomorphic(result) != to_isomorphic(test): + err_list.append(reasoner) + return err_list + + errors: list[str] = [] + reasoners = ["elk", "emr", "hermit", "jfact", "structural", "whelk"] + for reasoner in reasoners: + errors = test_reasoner(reasoner, errors) + + if errors: + raise AssertionError(f"Test failed for reasoners: {', '.join(errors)}") + + +@needs_cmem +def tests_validate(_setup_validate: None) -> None: + """Tests for validate plugin""" + ValidatePlugin( + ontology_graph_iri=VALIDATE_ONTOLOGY_GRAPH_IRI, + ).execute((), context=TestExecutionContext()) diff --git a/tests/test_robotreason.py b/tests/test_robotreason.py deleted file mode 100644 index 0017f0f..0000000 --- a/tests/test_robotreason.py +++ /dev/null @@ -1,70 +0,0 @@ -"""Plugin tests.""" - -from pathlib import Path - -import pytest -from cmem.cmempy.dp.proxy.graph import delete, get, post -from rdflib import DCTERMS, OWL, RDF, RDFS, Graph, URIRef -from rdflib.compare import to_isomorphic - -from cmem_plugin_robotreason.plugin_robotreason import RobotReasonPlugin -from tests.utils import TestExecutionContext, needs_cmem - -from . import __path__ - -UID = "e02aaed014c94e0c91bf960fed127750" -DATA_GRAPH_IRI = f"https://ns.eccenca.com/reasoning/{UID}/data/" -ONTOLOGY_GRAPH_IRI = f"https://ns.eccenca.com/reasoning/{UID}/vocab/" -RESULT_GRAPH_IRI = f"https://ns.eccenca.com/reasoning/{UID}/result/" - - -@pytest.fixture() -def _setup(request: pytest.FixtureRequest) -> None: - """Set up""" - res = post(DATA_GRAPH_IRI, Path(__path__[0]) / "dataset_owl.ttl", replace=True) - if res.status_code != 204: # noqa: PLR2004 - raise ValueError(f"Response {res.status_code}: {res.url}") - res = post(ONTOLOGY_GRAPH_IRI, Path(__path__[0]) / "vocab.ttl", replace=True) - if res.status_code != 204: # noqa: PLR2004 - raise ValueError(f"Response {res.status_code}: {res.url}") - - request.addfinalizer(lambda: delete(DATA_GRAPH_IRI)) - request.addfinalizer(lambda: delete(ONTOLOGY_GRAPH_IRI)) - request.addfinalizer(lambda: delete(RESULT_GRAPH_IRI)) # noqa: PT021 - - -@needs_cmem -def tests(_setup: None) -> None: - """Tests""" - - def test_reasoner(reasoner: str, err_list: list) -> list: - RobotReasonPlugin( - data_graph_iri=DATA_GRAPH_IRI, - ontology_graph_iri=ONTOLOGY_GRAPH_IRI, - result_graph_iri=RESULT_GRAPH_IRI, - reasoner=reasoner, - sub_class=False, - class_assertion=True, - property_assertion=True, - ).execute((), context=TestExecutionContext()) - - result = Graph().parse( - data=get(RESULT_GRAPH_IRI, owl_imports_resolution=False).text, - format="turtle", - ) - result.remove((URIRef(RESULT_GRAPH_IRI), DCTERMS.created, None)) - result.remove((URIRef(RESULT_GRAPH_IRI), RDFS.label, None)) - result.remove((None, RDF.type, OWL.AnnotationProperty)) - - test = Graph().parse(Path(__path__[0]) / f"test_{reasoner}.ttl", format="turtle") - if to_isomorphic(result) != to_isomorphic(test): - err_list.append(reasoner) - return err_list - - errors: list[str] = [] - reasoners = ["elk", "emr", "hermit", "jfact", "structural", "whelk"] - for reasoner in reasoners: - errors = test_reasoner(reasoner, errors) - - if errors: - raise AssertionError(f"Test failed for reasoners: {', '.join(errors)}") diff --git a/tests/test_structural.ttl b/tests/test_structural.ttl index 7aba062..8d7f980 100644 --- a/tests/test_structural.ttl +++ b/tests/test_structural.ttl @@ -13,7 +13,7 @@ rdfs:comment "Reasoning result set of and "@en ; prov:wasDerivedFrom , ; - prov:wasGeneratedBy "cmem-plugin-robotreason (structural)"@en . + prov:wasGeneratedBy "cmem-plugin-reason (structural)"@en . ################################################################# # Individuals diff --git a/tests/test_validate.ttl b/tests/test_validate.ttl new file mode 100644 index 0000000..e6fa516 --- /dev/null +++ b/tests/test_validate.ttl @@ -0,0 +1,36 @@ +@prefix : . +@prefix owl: . +@prefix rdf: . +@prefix rdfs: . +@base . + + rdf:type owl:Ontology . + +################################################################# +# Classes +################################################################# + +### https://ns.eccenca.com/validateontology/e02aaed014c94e0c91bf960fed127750/vocab/A +:A rdf:type owl:Class ; + rdfs:subClassOf :B ; + owl:disjointWith :B . + + +### https://ns.eccenca.com/validateontology/e02aaed014c94e0c91bf960fed127750/vocab/B +:B rdf:type owl:Class ; + rdfs:subClassOf :A . + + +################################################################# +# Individuals +################################################################# + +### https://ns.eccenca.com/validateontology/e02aaed014c94e0c91bf960fed127750/vocab/B_5 +:B_5 rdf:type owl:NamedIndividual , + :B . + + +### https://ns.eccenca.com/validateontology/e02aaed014c94e0c91bf960fed127750/vocab/D_6 +:D_6 rdf:type owl:NamedIndividual , + :A . + diff --git a/tests/test_whelk.ttl b/tests/test_whelk.ttl index f69364f..8212b53 100644 --- a/tests/test_whelk.ttl +++ b/tests/test_whelk.ttl @@ -13,7 +13,7 @@ rdfs:comment "Reasoning result set of and "@en ; prov:wasDerivedFrom , ; - prov:wasGeneratedBy "cmem-plugin-robotreason (whelk)"@en . + prov:wasGeneratedBy "cmem-plugin-reason (whelk)"@en . ################################################################# # Individuals