From bbc01682dcc7ae9dc99de228deeb1de572622e41 Mon Sep 17 00:00:00 2001 From: Minh Nguyen Date: Tue, 12 Nov 2024 20:52:05 +0100 Subject: [PATCH] add model processing for event loops - add models for event loops, event and flag reactions, which query relevant info from an RDF graph - add relevant URIs and namespace - add unittest for event loop models - change SHACL check func to use Dataset --- src/rdf_utils/constraints.py | 4 +- src/rdf_utils/models/event_loop.py | 90 ++++++++++++++++++++ src/rdf_utils/namespace.py | 2 + src/rdf_utils/uri.py | 8 +- tests/test_event_loop_model.py | 128 +++++++++++++++++++++++++++++ tests/test_python_model.py | 10 +-- 6 files changed, 233 insertions(+), 9 deletions(-) create mode 100644 src/rdf_utils/models/event_loop.py create mode 100644 tests/test_event_loop_model.py diff --git a/src/rdf_utils/constraints.py b/src/rdf_utils/constraints.py index 80654a0..76c4c2f 100644 --- a/src/rdf_utils/constraints.py +++ b/src/rdf_utils/constraints.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: MPL-2.0 from typing import Dict -from rdflib import ConjunctiveGraph, Graph +from rdflib import Dataset, Graph import pyshacl @@ -20,7 +20,7 @@ def check_shacl_constraints(graph: Graph, shacl_dict: Dict[str, str], quiet=Fals :param shacl_dict: mapping from SHACL path to graph format, e.g. URL -> "turtle" :param quiet: if true will not throw an exception """ - shacl_g = ConjunctiveGraph() + shacl_g = Dataset() for mm_url, fmt in shacl_dict.items(): shacl_g.parse(mm_url, format=fmt) diff --git a/src/rdf_utils/models/event_loop.py b/src/rdf_utils/models/event_loop.py new file mode 100644 index 0000000..473b2bc --- /dev/null +++ b/src/rdf_utils/models/event_loop.py @@ -0,0 +1,90 @@ +# SPDX-License-Identifier: MPL-2.0 +from rdflib import Graph, URIRef +from rdf_utils.models.common import ModelBase +from rdf_utils.namespace import NS_MM_EL + + +URI_EL_TYPE_EVT_LOOP = NS_MM_EL["EventLoop"] +URI_EL_TYPE_EVT = NS_MM_EL["Event"] +URI_EL_TYPE_FLG = NS_MM_EL["Flag"] +URI_EL_TYPE_EVT_REACT = NS_MM_EL["EventReaction"] +URI_EL_TYPE_FLG_REACT = NS_MM_EL["FlagReaction"] +URI_EL_PRED_REF_EVT = NS_MM_EL["ref-event"] +URI_EL_PRED_HAS_EVT = NS_MM_EL["has-event"] +URI_EL_PRED_REF_FLG = NS_MM_EL["ref-flag"] +URI_EL_PRED_HAS_FLG = NS_MM_EL["has-flag"] +URI_EL_PRED_HAS_EVT_REACT = NS_MM_EL["has-evt-reaction"] +URI_EL_PRED_HAS_FLG_REACT = NS_MM_EL["has-flg-reaction"] + + +class EventReactionModel(ModelBase): + event_id: URIRef + + def __init__(self, graph: Graph, reaction_id: URIRef) -> None: + super().__init__(graph=graph, node_id=reaction_id) + + evt_uri = graph.value(subject=self.id, predicate=URI_EL_PRED_REF_EVT) + assert evt_uri is not None and isinstance( + evt_uri, URIRef + ), f"EventReaction '{self.id}' does not refer to a valid event URI: {evt_uri}" + self.event_id = evt_uri + + +class FlagReactionModel(ModelBase): + flag_id: URIRef + + def __init__(self, graph: Graph, reaction_id: URIRef) -> None: + super().__init__(graph=graph, node_id=reaction_id) + + flg_uri = graph.value(subject=self.id, predicate=URI_EL_PRED_REF_FLG) + assert flg_uri is not None and isinstance( + flg_uri, URIRef + ), f"FlagReaction '{self.id}' does not refer to a valid flag URI: {flg_uri}" + self.flag_id = flg_uri + + +class EventLoopModel(ModelBase): + events_triggered: dict[URIRef, bool] + flag_values: dict[URIRef, bool] + event_reactions: dict[URIRef, EventReactionModel] + flag_reactions: dict[URIRef, FlagReactionModel] + + def __init__(self, graph: Graph, el_id: URIRef) -> None: + super().__init__(graph=graph, node_id=el_id) + + self.events_triggered = {} + self.flag_values = {} + self.event_reactions = {} + self.flag_reactions = {} + + for evt_uri in graph.objects(subject=self.id, predicate=URI_EL_PRED_HAS_EVT): + assert isinstance( + evt_uri, URIRef + ), f"Event '{evt_uri}' is not of type URIRef: {type(evt_uri)}" + self.events_triggered[evt_uri] = False + + for flg_uri in graph.objects(subject=self.id, predicate=URI_EL_PRED_HAS_FLG): + assert isinstance( + flg_uri, URIRef + ), f"Flag '{flg_uri}' is not of type URIRef: {type(flg_uri)}" + self.flag_values[flg_uri] = False + + for evt_re_uri in graph.objects(subject=self.id, predicate=URI_EL_PRED_HAS_EVT_REACT): + assert isinstance( + evt_re_uri, URIRef + ), f"EventReaction '{evt_re_uri}' is not of type URIRef: {type(evt_re_uri)}" + evt_re_model = EventReactionModel(graph=graph, reaction_id=evt_re_uri) + assert ( + evt_re_model.event_id in self.events_triggered + ), f"'{evt_re_model.id}' reacts to event '{evt_re_model.event_id}', which is not in event loop '{self.id}'" + self.event_reactions[evt_re_model.event_id] = evt_re_model + + for flg_re_uri in graph.objects(subject=self.id, predicate=URI_EL_PRED_HAS_FLG_REACT): + assert isinstance( + flg_re_uri, URIRef + ), f"FlagReaction '{flg_re_uri}' is not of type URIRef: {type(flg_re_uri)}" + flg_re_model = FlagReactionModel(graph=graph, reaction_id=flg_re_uri) + assert ( + flg_re_model.flag_id in self.flag_values + ), f"'{flg_re_model.id}' reacts to flag '{flg_re_model.flag_id}', which is not in event loop '{self.id}'" + self.flag_reactions[flg_re_model.flag_id] = flg_re_model diff --git a/src/rdf_utils/namespace.py b/src/rdf_utils/namespace.py index 1cdca3b..e564711 100644 --- a/src/rdf_utils/namespace.py +++ b/src/rdf_utils/namespace.py @@ -8,6 +8,7 @@ URI_MM_PYTHON, URI_MM_ENV, URI_MM_TIME, + URI_MM_EL, ) @@ -20,3 +21,4 @@ NS_MM_ENV = Namespace(URI_MM_ENV) NS_MM_AGN = Namespace(URI_MM_AGN) NS_MM_TIME = Namespace(URI_MM_TIME) +NS_MM_EL = Namespace(URI_MM_EL) diff --git a/src/rdf_utils/uri.py b/src/rdf_utils/uri.py index 7f37e73..ddbb575 100644 --- a/src/rdf_utils/uri.py +++ b/src/rdf_utils/uri.py @@ -8,10 +8,14 @@ URI_MM_GEOM_REL = f"{URL_COMP_ROB2B}/metamodels/geometry/spatial-relations#" URI_MM_GEOM_COORD = f"{URL_COMP_ROB2B}/metamodels/geometry/coordinates#" -URL_MM_PYTHON_SHACL = f"{URL_SECORO_MM}/languages/python.shacl.ttl" +URI_MM_PYTHON = f"{URL_SECORO_MM}/languages/python#" URL_MM_PYTHON_JSON = f"{URL_SECORO_MM}/languages/python.json" +URL_MM_PYTHON_SHACL = f"{URL_SECORO_MM}/languages/python.shacl.ttl" -URI_MM_PYTHON = f"{URL_SECORO_MM}/languages/python#" URI_MM_ENV = f"{URL_SECORO_MM}/environment#" URI_MM_AGN = f"{URL_SECORO_MM}/agent#" URI_MM_TIME = f"{URL_SECORO_MM}/time#" + +URI_MM_EL = f"{URL_SECORO_MM}/behaviour/event_loop#" +URL_MM_EL_JSON = f"{URL_SECORO_MM}/behaviour/event_loop.json" +URL_MM_EL_SHACL = f"{URL_SECORO_MM}/behaviour/event_loop.shacl.ttl" diff --git a/tests/test_event_loop_model.py b/tests/test_event_loop_model.py new file mode 100644 index 0000000..8992f54 --- /dev/null +++ b/tests/test_event_loop_model.py @@ -0,0 +1,128 @@ +# SPDX-License-Identifier: MPL-2.0 +import unittest +from rdflib import Graph, URIRef +from rdf_utils.resolver import install_resolver +from rdf_utils.constraints import check_shacl_constraints +from rdf_utils.uri import URL_MM_EL_JSON, URL_MM_EL_SHACL, URL_SECORO_M +from rdf_utils.models.event_loop import ( + URI_EL_TYPE_EVT_LOOP, + URI_EL_TYPE_EVT, + URI_EL_TYPE_EVT_REACT, + URI_EL_TYPE_FLG, + URI_EL_TYPE_FLG_REACT, + EventLoopModel, +) + + +URI_TEST_EL = f"{URL_SECORO_M}/models/tests/el" +URI_TEST_LOOP = f"{URI_TEST_EL}/test-loop" +URIREF_TEST_LOOP = URIRef(URI_TEST_LOOP) + +EVT_LOOP_MODEL_NODES = f""" +{{ + "@context": [ "{URL_MM_EL_JSON}" ], + "@graph": [ + {{ "@id": "{URI_TEST_EL}/event1", "@type": "{URI_EL_TYPE_EVT.toPython()}" }}, + {{ "@id": "{URI_TEST_EL}/event2", "@type": "{URI_EL_TYPE_EVT.toPython()}" }}, + {{ "@id": "{URI_TEST_EL}/flag1", "@type": "{URI_EL_TYPE_FLG.toPython()}" }}, + {{ "@id": "{URI_TEST_EL}/flag2", "@type": "{URI_EL_TYPE_FLG.toPython()}" }}, + {{ "@id": "{URI_TEST_EL}/evt_reaction", "@type": "{URI_EL_TYPE_EVT_REACT.toPython()}" }}, + {{ "@id": "{URI_TEST_EL}/flg_reaction", "@type": "{URI_EL_TYPE_FLG_REACT.toPython()}" }}, + {{ "@id": "{URI_TEST_LOOP}", "@type": "{URI_EL_TYPE_EVT_LOOP.toPython()}" }} + ] +}} +""" +EVT_LOOP_MODEL_CORRECT_COMP = f""" +{{ +"@context": [ "{URL_MM_EL_JSON}" ], +"@graph": [ + {{ + "@id": "{URI_TEST_EL}/evt_reaction", "@type": "{URI_EL_TYPE_EVT_REACT.toPython()}", + "ref-event" : "{URI_TEST_EL}/event1" + }}, + {{ + "@id": "{URI_TEST_EL}/flg_reaction", "@type": "{URI_EL_TYPE_FLG_REACT.toPython()}", + "ref-flag" : "{URI_TEST_EL}/flag1" + }}, + {{ + "@id": "{URI_TEST_LOOP}", "@type": "{URI_EL_TYPE_EVT_LOOP.toPython()}", + "has-event": [ "{URI_TEST_EL}/event1", "{URI_TEST_EL}/event2" ], + "has-evt-reaction": "{URI_TEST_EL}/evt_reaction", + "has-flag": [ "{URI_TEST_EL}/flag1", "{URI_TEST_EL}/flag2" ], + "has-flg-reaction": "{URI_TEST_EL}/flg_reaction" + }} +] +}} +""" +EVT_LOOP_MODEL_WRONG_EVT = f""" +{{ +"@context": [ "{URL_MM_EL_JSON}" ], +"@graph": [ + {{ + "@id": "{URI_TEST_EL}/evt_reaction", "@type": "{URI_EL_TYPE_EVT_REACT.toPython()}", + "ref-event" : "{URI_TEST_EL}/event1" + }}, + {{ + "@id": "{URI_TEST_LOOP}", "@type": "{URI_EL_TYPE_EVT_LOOP.toPython()}", + "has-event": [ "{URI_TEST_EL}/event2" ], + "has-evt-reaction": "{URI_TEST_EL}/evt_reaction" + }} +] +}} +""" +EVT_LOOP_MODEL_WRONG_FLG = f""" +{{ +"@context": [ "{URL_MM_EL_JSON}" ], +"@graph": [ + {{ + "@id": "{URI_TEST_EL}/flg_reaction", "@type": "{URI_EL_TYPE_FLG_REACT.toPython()}", + "ref-flag" : "{URI_TEST_EL}/flag1" + }}, + {{ + "@id": "{URI_TEST_LOOP}", "@type": "{URI_EL_TYPE_EVT_LOOP.toPython()}", + "has-flag": [ "{URI_TEST_EL}/flag2" ], + "has-flg-reaction": "{URI_TEST_EL}/flg_reaction" + }} +] +}} +""" + + +class EventLoopModelTest(unittest.TestCase): + def setUp(self): + install_resolver() + + def test_correct_el_model(self): + graph = Graph() + graph.parse(data=EVT_LOOP_MODEL_NODES, format="json-ld") + + self.assertFalse( + check_shacl_constraints( + graph=graph, shacl_dict={URL_MM_EL_SHACL: "turtle"}, quiet=True + ), + "SHACL violation not raised for missing refs from reactions to events and flags", + ) + + graph.parse(data=EVT_LOOP_MODEL_CORRECT_COMP, format="json-ld") + + self.assertTrue( + check_shacl_constraints(graph=graph, shacl_dict={URL_MM_EL_SHACL: "turtle"}) + ) + + _ = EventLoopModel(graph=graph, el_id=URIREF_TEST_LOOP) + + def test_wrong_reactions(self): + wrong_evt_g = Graph() + wrong_evt_g.parse(data=EVT_LOOP_MODEL_WRONG_EVT, format="json-ld") + with self.assertRaises( + AssertionError, msg="not raised for reaction to an event not in loop" + ): + _ = EventLoopModel(graph=wrong_evt_g, el_id=URIREF_TEST_LOOP) + wrong_flg_g = Graph() + wrong_flg_g.parse(data=EVT_LOOP_MODEL_WRONG_FLG, format="json-ld") + with self.assertRaises(AssertionError, msg="not raised for reaction to a flag not in loop"): + _ = EventLoopModel(graph=wrong_flg_g, el_id=URIREF_TEST_LOOP) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_python_model.py b/tests/test_python_model.py index d0cba9a..d16a77f 100644 --- a/tests/test_python_model.py +++ b/tests/test_python_model.py @@ -2,7 +2,7 @@ import unittest from urllib.request import urlopen import pyshacl -from rdflib import Dataset, URIRef +from rdflib import Graph, URIRef from rdf_utils.models.common import ModelBase, ModelLoader from rdf_utils.uri import URL_MM_PYTHON_JSON, URL_MM_PYTHON_SHACL, URL_SECORO_M from rdf_utils.resolver import install_resolver @@ -13,8 +13,8 @@ ) -TEST_URL = f"{URL_SECORO_M}/models/tests" -URI_OS_PATH_EXISTS = f"{TEST_URL}/test-os-path-exists" +URI_TEST = f"{URL_SECORO_M}/models/tests" +URI_OS_PATH_EXISTS = f"{URI_TEST}/test-os-path-exists" PYTHON_MODEL = f""" {{ "@context": [ @@ -40,10 +40,10 @@ def setUp(self): self.model_loader.register(load_py_module_attr) def test_python_import(self): - graph = Dataset() + graph = Graph() graph.parse(data=PYTHON_MODEL, format="json-ld") - shacl_g = Dataset() + shacl_g = Graph() shacl_g.parse(URL_MM_PYTHON_SHACL, format="turtle") conforms, _, report_text = pyshacl.validate( graph,