From d7b0555b1f06b87e7279ef0f43e91bd40940f413 Mon Sep 17 00:00:00 2001
From: Matthias Bodenbenner <m.bodenbenner@wzl-mq.rwth-aachen.de>
Date: Wed, 17 Jan 2024 08:09:58 +0100
Subject: [PATCH] 9.1.0 - added license field to semantics

---
 README.md               |  5 ++++-
 setup.py                |  2 +-
 src/soil/component.py   | 12 +++++++++++-
 src/soil/element.py     |  4 ++++
 src/soil/measurement.py |  2 ++
 src/soil/semantics.py   | 25 +++++++++++++++++++++++++
 src/soil/stream.py      |  3 ++-
 src/utils/constants.py  | 12 +++++++++++-
 8 files changed, 60 insertions(+), 5 deletions(-)

diff --git a/README.md b/README.md
index d0ada77..b5322bb 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
 [![Build](https://git-ce.rwth-aachen.de/wzl-mq-ms/forschung-lehre/lava/unified-device-interface/python/badges/master/pipeline.svg)](https://git-ce.rwth-aachen.de/wzl-mq-ms/forschung-lehre/lava/unified-device-interface/python/commits/master)
 
 # Python Unified Device Interface
-Current stable version: 9.0.1
+Current stable version: 9.1.0
 
 ## Installation
 1. Install the WZL-UDI package via pip
@@ -58,6 +58,9 @@ Funded by the Deutsche Forschungsgemeinschaft (DFG, German Research Foundation)
 
 ## Recent changes
 
+**9.1.0** - 2024-01-17
+  - the license for profiles, metadata and data is now provided anc can be specified in the config file
+
 **9.0.1** - 2024-01-11
   - bug fix of semantic name resolution
 
diff --git a/setup.py b/setup.py
index d8f626c..dc65d01 100644
--- a/setup.py
+++ b/setup.py
@@ -4,7 +4,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
     long_description = fh.read()
 
 setup(name='wzl-udi',
-      version='9.0.1',
+      version='9.1.0',
       url='https://git-ce.rwth-aachen.de/wzl-mq-public/soil/python',
       project_urls={
           "Bug Tracker": "https://git-ce.rwth-aachen.de/wzl-mq-public/soil/python/-/issues",
diff --git a/src/soil/component.py b/src/soil/component.py
index daca80d..4767d0f 100644
--- a/src/soil/component.py
+++ b/src/soil/component.py
@@ -19,7 +19,7 @@ from .error import ChildNotFoundException
 from .function import Function
 from .measurement import Measurement
 from .parameter import Parameter
-from .semantics import Namespaces
+from .semantics import Namespaces, Semantics
 from ..utils import root_logger
 from ..utils.constants import HTTP_GET
 from ..utils.error import SerialisationException, DeviceException, UserException
@@ -399,12 +399,22 @@ class Component(Element):
         try:
             return super().resolve_semantic_path(suffix)
         except ChildNotFoundException:
+            # check if the path fits one of the components children
             for child in self.children:
                 try:
                     return child.resolve_semantic_path(suffix)
                 except ChildNotFoundException:
                     continue
 
+            # check if the profile of this component imports a shape which matches the path
+            imported_profiles = list(self._metadata_profile.objects(predicate=Namespaces.owl.imports))
+            for imported_profile in imported_profiles:
+                if imported_profile.toPython().replace(Semantics.namespace, '') == suffix:
+                    # TODO implement loading and returning of the base components profile
+                    #  (is not present as element, so might require adaption of the method signature,
+                    #  might also be called recursively, if the base component has own bases which are queried)
+                    raise ChildNotFoundException('Profiles of base components can currently not be returned.')
+
             raise ChildNotFoundException('Could not resolve the semantic path.')
 
     @property
diff --git a/src/soil/element.py b/src/soil/element.py
index cf0c30a..833b7db 100644
--- a/src/soil/element.py
+++ b/src/soil/element.py
@@ -6,6 +6,7 @@ from typing import Any, Dict, List
 import rdflib
 
 from .error import ChildNotFoundException
+from .semantics import Namespaces, Semantics
 from ..utils.constants import BASE_UUID_PATTERN, HTTP_GET
 from ..utils.error import SerialisationException
 
@@ -101,12 +102,15 @@ class Element(ABC):
         shape_filename = os.path.join(profiles_path, f"{self._profilename}.shacl.ttl")
         self._metadata_profile = rdflib.Graph()
         self._metadata_profile.parse(shape_filename)
+        self._metadata_profile.add((rdflib.URIRef(Semantics.namespace[self._profilename]), Namespaces.dcterms.license,
+                                    Semantics.profile_license))
 
         # load metadata
         self._semantic_name = f'{parent_name}{self.uuid[4:].capitalize()}'
         metadata_filename = os.path.join(metadata_path, f"{self._semantic_name}.ttl")
         self._metadata = rdflib.Graph()
         self._metadata.parse(metadata_filename)
+        self._metadata.add((rdflib.URIRef(self._semantic_name), Namespaces.schema.license, Semantics.metadata_license))
 
     @abstractmethod
     def serialize_semantics(self, kind: str) -> rdflib.Graph:
diff --git a/src/soil/measurement.py b/src/soil/measurement.py
index 217f46f..6624efa 100644
--- a/src/soil/measurement.py
+++ b/src/soil/measurement.py
@@ -197,6 +197,7 @@ class Measurement(Figure):
             data_graph.add((observation_subject, Namespaces.sosa.hasResult,
                             Semantics.namespace[f'{self._semantic_name}Measurement']))
             data_graph.add((observation_subject, Namespaces.sosa.madeBySensor, sensor_triples[0][2]))
+            data_graph.add((observation_subject, Namespaces.schema.license, Semantics.data_license))
 
             # create result node
             unit_triples = list(self._metadata.triples((None, Namespaces.qudt.applicableUnit, None)))
@@ -207,6 +208,7 @@ class Measurement(Figure):
             data_graph.add((measurement_subject, Namespaces.rdf.type, rdflib.URIRef(Namespaces.soil.Measurement)))
             data_graph.add((measurement_subject, Namespaces.sosa.isResultOf, observation_subject))
             data_graph.add((measurement_subject, Namespaces.qudt.unit, unit_triples[0][2]))
+            data_graph.add((measurement_subject, Namespaces.schema.license, Semantics.data_license))
 
             rdf_value = Figure.serialize_value(data_graph, self.__getitem__('value', 0))
 
diff --git a/src/soil/semantics.py b/src/soil/semantics.py
index 00a59c6..7a3f801 100644
--- a/src/soil/semantics.py
+++ b/src/soil/semantics.py
@@ -1,19 +1,44 @@
+import re
+import urllib
+
 import rdflib
 
+from ..utils.constants import URL_PATTERN
+
 
 class Semantics(object):
     prefix: str = None
     url: str = None
     namespace: rdflib.Namespace = None
+    profile_license: rdflib.term.Identifier = rdflib.URIRef("https://spdx.org/licenses/CC-BY-4.0.html")
+    metadata_license: rdflib.term.Identifier = rdflib.URIRef("https://spdx.org/licenses/CC-BY-NC-ND-4.0.html")
+    data_license: rdflib.term.Identifier = rdflib.Literal("All rights reserved.")
 
     def __init__(self, config: dict[str, str]):
         Semantics.prefix = config['prefix']
         Semantics.url = config['url']
         Semantics.namespace = rdflib.Namespace(config['url'])
+        if 'profile-license' in config:
+            if re.match(URL_PATTERN, config['profile-license']):
+                Semantics.profile_license = rdflib.URIRef(config['profile-license'])
+            else:
+                Semantics.profile_license = rdflib.Literal(config['profile-license'])
+        if 'metadata-license' in config:
+            if re.match(URL_PATTERN, config['metadata-license']):
+                Semantics.metadata_license = rdflib.URIRef(config['metadata-license'])
+            else:
+                Semantics.metadata_license = rdflib.Literal(config['metadata-license'])
+        if 'data-license' in config:
+            if re.match(URL_PATTERN, config['data-license']):
+                Semantics.data_license = rdflib.URIRef(config['data-license'])
+            else:
+                Semantics.data_license = rdflib.Literal(config['data-license'])
 
 
 class Namespaces(object):
+    dcterms = rdflib.namespace.DCTERMS
     m4i = rdflib.Namespace('http://w3id.org/nfdi4ing/metadata4ing#')
+    owl = rdflib.Namespace('http://www.w3.org/2002/07/owl#')
     quantitykind = rdflib.Namespace('http://qudt.org/vocab/quantitykind/')
     qudt = rdflib.Namespace('http://qudt.org/schema/qudt/')
     rdf = rdflib.namespace.RDF
diff --git a/src/soil/stream.py b/src/soil/stream.py
index 8675e01..0330d86 100644
--- a/src/soil/stream.py
+++ b/src/soil/stream.py
@@ -124,7 +124,7 @@ class Job(ABC):
         try:
             url, data = self._retrieve_semantic_metadata(model)
             measurement_subject = \
-            list((data.subjects(predicate=Namespaces.rdf.type, object=Namespaces.soil.Measurement)))[0]
+                list((data.subjects(predicate=Namespaces.rdf.type, object=Namespaces.soil.Measurement)))[0]
 
             # replace value
             data.remove((None, Namespaces.qudt.value, None))
@@ -298,6 +298,7 @@ class StreamScheduler(object):
                         # try to send semantic data package
                         try:
                             url, semantic_data = job.semantic_data(self._model)
+                            url = url.replace('https://', '').replace('http://', '')
                             if self._dataformat == 'json':
                                 message = semantic_data.serialize(format='json-ld')
                             elif self._dataformat == 'xml':
diff --git a/src/utils/constants.py b/src/utils/constants.py
index 8ac340e..6865dd0 100644
--- a/src/utils/constants.py
+++ b/src/utils/constants.py
@@ -1,3 +1,13 @@
+import re
+
 HTTP_GET = 0
 HTTP_OPTIONS = 1
-BASE_UUID_PATTERN = r'[0-9A-Za-z-_]{3,}'
\ No newline at end of file
+BASE_UUID_PATTERN = r'[0-9A-Za-z-_]{3,}'
+
+URL_PATTERN = re.compile(
+    r'^(?:http|ftp)s?://'  # http:// or https://
+    r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|'  # domain...
+    r'localhost|'  # localhost...
+    r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'  # ...or ip
+    r'(?::\d+)?'  # optional port
+    r'(?:/?|[/?]\S+)$', re.IGNORECASE)
-- 
GitLab