-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
98e94de
commit eb8d269
Showing
9 changed files
with
297 additions
and
245 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,9 @@ | ||
# Changes here will be overwritten by Copier | ||
_commit: v6.1.0 | ||
_commit: v6.3.1 | ||
_src_path: gh:eccenca/cmem-plugin-template | ||
author_mail: [email protected] | ||
author_name: eccenca GmbH | ||
github_page: '' | ||
github_page: https://github.com/eccenca/cmem-plugin-robotreason | ||
project_description: Reasoning with Robot | ||
project_slug: robotreason | ||
pypi: false | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -149,4 +149,4 @@ artifacts | |
.task | ||
|
||
# ROBOT | ||
cmem_plugin_robotreason/workflow/bin/ | ||
cmem_plugin_robotreason/bin/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,215 @@ | ||
"""robotreason - main package""" | ||
"""Reasoning with robot 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 # nosec | ||
from time import time | ||
from uuid import uuid4 | ||
from xml.etree.ElementTree import ( | ||
Element, | ||
SubElement, | ||
tostring, | ||
) | ||
|
||
from cmem.cmempy.dp.proxy.graph import get, get_graph_import_tree, post_streamed | ||
from cmem_plugin_base.dataintegration.context import ExecutionContext | ||
from cmem_plugin_base.dataintegration.description import Plugin, PluginParameter | ||
from cmem_plugin_base.dataintegration.entity import Entities | ||
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 StringParameterType | ||
from cmem_plugin_base.dataintegration.utils import setup_cmempy_user_access | ||
from defusedxml import minidom | ||
|
||
ROBOT = Path(__path__[0]) / "bin" / "robot" | ||
|
||
|
||
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="Reasoning with ROBOT", | ||
description="Given a data and an ontology grpah, this task performs reasoning " "using ROBOT.", | ||
documentation="""A task performing reasoning using ROBOT (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. The following reasoner options are supported: ELK, Expression | ||
Materializing Reasoner, HermiT, JFact, Structural Reasoner and Whelk.""", | ||
parameters=[ | ||
PluginParameter( | ||
param_type=GraphParameterType( | ||
classes=[ | ||
"http://www.w3.org/2002/07/owl#Ontology", | ||
"https://vocab.eccenca.com/di/Dataset", | ||
"http://rdfs.org/ns/void#Dataset", | ||
] | ||
), | ||
name="data_graph_iri", | ||
label="Data graph IRI", | ||
description="The IRI of the input data graph.", | ||
), | ||
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=StringParameterType(), | ||
name="result_iri", | ||
label="Result graph IRI", | ||
description="The IRI of the output graph for the reasoning result. " | ||
"WARNING: existing graph will be overwritten!", | ||
), | ||
PluginParameter( | ||
param_type=ChoiceParameterType( | ||
OrderedDict( | ||
{ | ||
"elk": "ELK", | ||
"emr": "Expression Materializing Reasoner", | ||
"hermit": "HermiT", | ||
"jfact": "JFact", | ||
"structural": "Structural Reasoner", | ||
"whelk": "Whelk", | ||
} | ||
) | ||
), | ||
name="reasoner", | ||
label="Reasoner", | ||
description="Reasoner option.", | ||
default_value="elk", | ||
), | ||
], | ||
) | ||
class RobotReasonPlugin(WorkflowPlugin): | ||
"""Robot reasoning plugin""" | ||
|
||
def __init__( | ||
self, data_graph_iri: str, ontology_graph_iri: str, result_iri: str, reasoner: str | ||
) -> None: | ||
"""Init""" | ||
self.data_graph_iri = data_graph_iri | ||
self.ontology_graph_iri = ontology_graph_iri | ||
self.result_iri = result_iri | ||
self.reasoner = reasoner | ||
self.temp = f"robot_{uuid4().hex}" | ||
if not Path(self.temp).exists(): | ||
Path(self.temp).mkdir(parents=True) | ||
|
||
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 graph in graphs: | ||
uri = SubElement(catalog, "uri") | ||
uri.set("id", "Auto-generated import resolution by cmem-plugin-robotreason") | ||
uri.set("name", graph) | ||
uri.set("uri", graphs[graph]) | ||
reparsed = minidom.parseString(tostring(catalog, "utf-8")).toxml() # nosec | ||
with Path(file_name).open("w", encoding="utf-8") as file: | ||
file.truncate(0) | ||
file.write(reparsed) | ||
|
||
def get_graphs(self, graphs: dict) -> None: | ||
"""Get graphs from CMEM""" | ||
for graph in graphs: | ||
with (Path(self.temp) / graphs[graph]).open("w", encoding="utf-8") as file: | ||
file.write(get(graph).text) | ||
file.write( | ||
f"<{graph}> " | ||
f"<http://www.w3.org/1999/02/22-rdf-syntax-ns#type> " | ||
f"<http://www.w3.org/2002/07/owl#Ontology> .\n" | ||
) | ||
|
||
def get_graphs_tree(self) -> dict: | ||
"""Get graph import tree""" | ||
graphs = {} | ||
for graph_iri in (self.data_graph_iri, self.ontology_graph_iri): | ||
if graph_iri not in graphs: | ||
graphs[graph_iri] = convert_iri_to_filename(graph_iri) | ||
tree = get_graph_import_tree(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 reason(self, graphs: dict) -> None: | ||
"""Reason""" | ||
inputs = "" | ||
for value in graphs.values(): | ||
inputs += f' --input "{self.temp}/{value}"' | ||
utctime = str(datetime.fromtimestamp(int(time()), tz=UTC))[:-6].replace(" ", "T") + "Z" | ||
cmd = ( | ||
f"{ROBOT} merge{inputs} --collapse-import-closure false " | ||
f"reason --reasoner {self.reasoner} " | ||
f'--axiom-generators "ClassAssertion PropertyAssertion" ' | ||
f"--include-indirect true " | ||
f"--exclude-duplicate-axioms true " | ||
f"--exclude-owl-thing true " | ||
f"--exclude-tautologies all " | ||
f"--exclude-external-entities " | ||
f"reduce --reasoner {self.reasoner} " | ||
f"unmerge{inputs} " | ||
f'annotate --ontology-iri "{self.result_iri}" ' | ||
f"--remove-annotations " | ||
f'--language-annotation rdfs:label "Eccenca Reasoning Result {utctime}" en ' | ||
f"--language-annotation rdfs:comment " | ||
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'--link-annotation prov:wasDerivedFrom "{self.data_graph_iri}" ' | ||
f"--link-annotation prov:wasDerivedFrom " | ||
f'"{self.ontology_graph_iri}" ' | ||
f'--typed-annotation dc:created "{utctime}" xsd:dateTime ' | ||
f'--output "{self.temp}/result.ttl"' | ||
) | ||
run(shlex.split(cmd), check=False) # noqa: S603 | ||
|
||
def send_result(self) -> None: | ||
"""Send result""" | ||
post_streamed( | ||
self.result_iri, | ||
str(Path(self.temp) / "result.ttl"), | ||
replace=True, | ||
content_type="text/turtle", | ||
) | ||
|
||
def clean_up(self, graphs: dict) -> None: | ||
"""Remove temporary files""" | ||
files = ["catalog-v001.xml", "result.ttl"] | ||
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], context: ExecutionContext) -> None: # noqa: ARG002 | ||
"""Execute plugin""" | ||
setup_cmempy_user_access(context.user) | ||
graphs = self.get_graphs_tree() | ||
self.get_graphs(graphs) | ||
self.create_xml_catalog_file(graphs) | ||
self.reason(graphs) | ||
self.send_result() | ||
self.clean_up(graphs) |
Oops, something went wrong.