Skip to content

Commit 323835c

Browse files
JaFeKls-heppner
andauthored
adapter: Add adapter for RDF (#308)
This adds adapters to serialize and deserialize RDF files, following the specification of the Asset Administration Shell. While there are still currently issues with the specified RDF schema (see #308), this adapter works to read and write AAS RDF files. Due to these issues, we decided to keep this implementation on a separate branch, until we can be sure it is not subject to big changes anymore. --------- Co-authored-by: s-heppner <[email protected]>
1 parent dfca376 commit 323835c

File tree

7 files changed

+983
-0
lines changed

7 files changed

+983
-0
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ jobs:
3939
mkdir -p ./test/adapter/schema
4040
curl -sSL -o ./test/adapter/schema/aasJSONSchema.json https://raw.githubusercontent.com/admin-shell-io/aas-specs/${{ env.AAS_SPECS_RELEASE_TAG }}/schemas/json/aas.json
4141
curl -sSL -o ./test/adapter/schema/aasXMLSchema.xsd https://raw.githubusercontent.com/admin-shell-io/aas-specs/${{ env.AAS_SPECS_RELEASE_TAG }}/schemas/xml/AAS.xsd
42+
curl -sSL -o ./test/adapter/schema/aasRDFOntology.ttl https://raw.githubusercontent.com/admin-shell-io/aas-specs/${{ env.AAS_SPECS_RELEASE_TAG }}/schemas/rdf/rdf-ontology.ttl
43+
# The shacl file in its current version cannot be sufficiently used to validate: https://github.com/admin-shell-io/aas-specs/issues/421
44+
# curl -sSL -o ./test/adapter/schema/aasRDFShaclSchema.ttl https://raw.githubusercontent.com/admin-shell-io/aas-specs/${{ env.AAS_SPECS_RELEASE_TAG }}/schemas/rdf/shacl-schema.ttl
4245
- name: Install Python dependencies
4346
run: |
4447
python -m pip install --upgrade pip

basyx/aas/adapter/_generic.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
implementation to the respective string and vice versa.
1010
"""
1111
import os
12+
import pathlib
1213
from typing import BinaryIO, Dict, IO, Type, Union
1314

1415
from basyx.aas import model
@@ -18,6 +19,8 @@
1819
Path = Union[str, bytes, os.PathLike]
1920
PathOrBinaryIO = Union[Path, BinaryIO]
2021
PathOrIO = Union[Path, IO] # IO is TextIO or BinaryIO
22+
PathOrIOGraph = Union[str, pathlib.PurePath, IO[bytes]]
23+
2124

2225
# XML Namespace definition
2326
XML_NS_MAP = {"aas": "https://admin-shell.io/aas/3/0"}

basyx/aas/adapter/rdf/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""
2+
.. _adapter.rdf.__init__:
3+
4+
This package contains functionality for serialization and deserialization of BaSyx Python SDK objects into RDF.
5+
6+
:ref:`rdf_serialization <adapter.xml.rdf_serialization>`: The module offers a function to write an
7+
:class:`ObjectStore <basyx.aas.model.provider.AbstractObjectStore>` to a given file.
8+
"""
9+
10+
from .rdf_serialization import AASToRDFEncoder, object_store_to_rdf, write_aas_rdf_file

basyx/aas/adapter/rdf/rdf_serialization.py

Lines changed: 835 additions & 0 deletions
Large diffs are not rendered by default.

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ dependencies = [
4545
"schemathesis~=3.7",
4646
"hypothesis~=6.13",
4747
"lxml-stubs~=0.5.1",
48+
"rdflib~=7.0.0",
49+
"pyshacl~=0.26.0"
4850
]
4951

5052
[project.optional-dependencies]

test/adapter/rdf/__init__.py

Whitespace-only changes.
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# Copyright (c) 2023 the Eclipse BaSyx Authors
2+
#
3+
# This program and the accompanying materials are made available under the terms of the MIT License, available in
4+
# the LICENSE file of this project.
5+
#
6+
# SPDX-License-Identifier: MIT
7+
import io
8+
import os
9+
import unittest
10+
11+
from rdflib import Graph, Namespace
12+
from pyshacl import validate
13+
14+
from basyx.aas import model
15+
from basyx.aas.adapter.rdf import write_aas_rdf_file
16+
17+
from basyx.aas.examples.data import example_submodel_template, example_aas_mandatory_attributes, example_aas_missing_attributes, example_aas
18+
19+
RDF_ONTOLOGY_FILE = os.path.join(os.path.dirname(__file__), '../schemas/aasRDFOntology.ttl')
20+
RDF_SHACL_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), '../schemas/aasRDFShaclSchema.ttl')
21+
22+
23+
class RDFSerializationTest(unittest.TestCase):
24+
def test_serialize_object(self) -> None:
25+
test_object = model.Property("test_id_short", model.datatypes.String, category="PARAMETER",
26+
description=model.MultiLanguageTextType({"en-US": "Germany", "de": "Deutschland"}))
27+
# TODO: The serialization of a single object to rdf is currently not supported.
28+
29+
def test_random_object_serialization(self) -> None:
30+
aas_identifier = "AAS1"
31+
submodel_key = (model.Key(model.KeyTypes.SUBMODEL, "SM1"),)
32+
submodel_identifier = submodel_key[0].get_identifier()
33+
assert (submodel_identifier is not None)
34+
submodel_reference = model.ModelReference(submodel_key, model.Submodel)
35+
submodel = model.Submodel(submodel_identifier)
36+
test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id="Test"),
37+
aas_identifier, submodel={submodel_reference})
38+
39+
# TODO: The serialization of a single object to rdf is currently not supported.
40+
41+
42+
def validate_graph(data_graph: io.BytesIO):
43+
# load schema
44+
data_graph.seek(0)
45+
shacl_graph = Graph()
46+
shacl_graph.parse(RDF_SHACL_SCHEMA_FILE, format="turtle")
47+
48+
# TODO: We need to remove the Sparql constraints on Abstract classes because
49+
# it somehow fails when using pychacl as validator
50+
SH = Namespace("http://www.w3.org/ns/shacl#")
51+
shacl_graph.remove((None, SH.sparql, None))
52+
53+
# load aas ontology
54+
aas_graph = Graph()
55+
aas_graph.parse(RDF_ONTOLOGY_FILE, format="turtle")
56+
57+
# validate serialization against schema
58+
conforms, results_graph, results_text = validate(
59+
data_graph=data_graph, # Passing the BytesIO object here
60+
shacl_graph=shacl_graph, # The SHACL graph
61+
ont_graph=aas_graph,
62+
data_graph_format="turtle", # Specify the format for the data graph (since it's serialized)
63+
inference='both', # Optional: perform RDFS inference
64+
abort_on_first=True, # Don't continue validation after finding an error
65+
allow_infos=True, # Allow informational messages
66+
allow_warnings=True, # Allow warnings
67+
advanced=True)
68+
# print("Conforms:", conforms)
69+
# print("Validation Results:\n", results_text)
70+
assert conforms is True
71+
72+
73+
class RDFSerializationSchemaTest(unittest.TestCase):
74+
@classmethod
75+
def setUpClass(cls):
76+
if not os.path.exists(RDF_SHACL_SCHEMA_FILE):
77+
raise unittest.SkipTest(f"Shacl Schema does not exist at {RDF_SHACL_SCHEMA_FILE}, skipping test")
78+
79+
def test_random_object_serialization(self) -> None:
80+
aas_identifier = "AAS1"
81+
submodel_key = (model.Key(model.KeyTypes.SUBMODEL, "SM1"),)
82+
submodel_identifier = submodel_key[0].get_identifier()
83+
assert submodel_identifier is not None
84+
submodel_reference = model.ModelReference(submodel_key, model.Submodel)
85+
submodel = model.Submodel(submodel_identifier,
86+
semantic_id=model.ExternalReference((model.Key(model.KeyTypes.GLOBAL_REFERENCE,
87+
"http://acplt.org/TestSemanticId"),)))
88+
test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id="test"),
89+
aas_identifier, submodel={submodel_reference})
90+
91+
# serialize object to rdf
92+
test_data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore()
93+
test_data.add(test_aas)
94+
test_data.add(submodel)
95+
96+
test_file = io.BytesIO()
97+
write_aas_rdf_file(file=test_file, data=test_data)
98+
validate_graph(test_file)
99+
100+
def test_full_example_serialization(self) -> None:
101+
data = example_aas.create_full_example()
102+
file = io.BytesIO()
103+
write_aas_rdf_file(file=file, data=data)
104+
validate_graph(file)
105+
106+
def test_submodel_template_serialization(self) -> None:
107+
data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore()
108+
data.add(example_submodel_template.create_example_submodel_template())
109+
file = io.BytesIO()
110+
write_aas_rdf_file(file=file, data=data)
111+
validate_graph(file)
112+
113+
def test_full_empty_example_serialization(self) -> None:
114+
data = example_aas_mandatory_attributes.create_full_example()
115+
file = io.BytesIO()
116+
write_aas_rdf_file(file=file, data=data)
117+
validate_graph(file)
118+
119+
def test_missing_serialization(self) -> None:
120+
data = example_aas_missing_attributes.create_full_example()
121+
file = io.BytesIO()
122+
write_aas_rdf_file(file=file, data=data)
123+
validate_graph(file)
124+
125+
def test_concept_description(self) -> None:
126+
data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore()
127+
data.add(example_aas.create_example_concept_description())
128+
file = io.BytesIO()
129+
write_aas_rdf_file(file=file, data=data)
130+
validate_graph(file)

0 commit comments

Comments
 (0)