Skip to content

Commit 9cb09a8

Browse files
Oleksandr Muzyka (EPAM)Oleksandr Muzyka (EPAM)
authored andcommitted
Add the capability to upgrade a .ttl file version using AdaptiveGraph
1 parent a264760 commit 9cb09a8

File tree

16 files changed

+804
-143
lines changed

16 files changed

+804
-143
lines changed
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import pathlib
2+
import subprocess
3+
import tempfile
4+
5+
from typing import Optional, Union
6+
7+
from rdflib import Graph
8+
9+
from esmf_aspect_meta_model_python.constants import SAMM_NAMESPACE_PREFIX, SAMM_ORG_IDENTIFIER
10+
from esmf_aspect_meta_model_python.samm_cli import SammCli
11+
12+
13+
class AdaptiveGraph(Graph): # TODO: avoid double parsing when an upgrade is not performed
14+
"""An RDF graph that can adaptively upgrade SAMM files using the SAMM CLI."""
15+
16+
_samm_cli = SammCli()
17+
18+
def __init__(self, samm_version: str | None = None, *args, **kwargs) -> None:
19+
super().__init__(*args, **kwargs)
20+
21+
self._samm_version = samm_version
22+
23+
def _upgrade_ttl_file(self, file_path: pathlib.Path) -> str:
24+
"""Run SAMM CLI prettyprint to upgrade a TTL file to the latest version."""
25+
try:
26+
return self._samm_cli.prettyprint(str(file_path), capture=True)
27+
except subprocess.CalledProcessError as e:
28+
raise RuntimeError(f"SAMM CLI failed for {file_path}:\n{e.stdout}\n{e.stderr}") from e
29+
30+
def _has_version_mismatch_in_graph(self, input_source: str | bytes | pathlib.Path) -> bool:
31+
"""Parse into a temporary graph to detect mismatch BEFORE loading into self."""
32+
temp_graph = Graph()
33+
34+
if isinstance(input_source, pathlib.Path):
35+
temp_graph.parse(input_source)
36+
else: # str | bytes
37+
temp_graph.parse(data=input_source, format="ttl")
38+
39+
for prefix, namespace in temp_graph.namespace_manager.namespaces():
40+
if prefix.startswith(SAMM_NAMESPACE_PREFIX):
41+
parts = namespace.strip("#").split(":")
42+
if len(parts) >= 5 and parts[2] == SAMM_ORG_IDENTIFIER:
43+
version = parts[-1]
44+
if version != self._samm_version:
45+
return True
46+
return False
47+
48+
def _upgrade_source(self, source_path: pathlib.Path) -> str:
49+
print(f"[INFO] SAMM version mismatch detected in {source_path}. Upgrading...")
50+
51+
return self._upgrade_ttl_file(source_path)
52+
53+
def _upgrade_data(self, data: str | bytes) -> str:
54+
print( # TODO: improve logging
55+
f"[INFO] SAMM version mismatch detected in provided data (target v{self._samm_version}) Upgrading..."
56+
)
57+
58+
with tempfile.NamedTemporaryFile("wb", suffix=".ttl", delete=False) as tmp:
59+
tmp.write(data.encode("utf-8") if isinstance(data, str) else data)
60+
tmp_path = pathlib.Path(tmp.name)
61+
62+
try:
63+
return self._upgrade_ttl_file(tmp_path)
64+
finally:
65+
tmp_path.unlink(missing_ok=True)
66+
67+
def set_samm_version(self, samm_version: str | None) -> None:
68+
"""Set the SAMM version for this graph."""
69+
self._samm_version = samm_version
70+
71+
def parse( # type: ignore[override]
72+
self,
73+
*,
74+
source: Optional[str | pathlib.Path] = None,
75+
data: Optional[str | bytes] = None,
76+
**kwargs,
77+
) -> "AdaptiveGraph":
78+
"""
79+
Parse a TTL file into this graph, upgrading via SAMM CLI if version mismatch detected.
80+
81+
If a SAMM version mismatch is detected, the TTL file will be upgraded using the SAMM CLI prettyprint
82+
before parsing into this graph.
83+
84+
Args:
85+
source: Path to the TTL file as pathlib.Path or str.
86+
data: RDF content as string or bytes.
87+
**kwargs: Additional arguments passed to rdflib.Graph.parse().
88+
89+
Returns:
90+
self (AdaptiveGraph): The current graph instance with parsed data.
91+
92+
Raises:
93+
RuntimeError: If the SAMM CLI fails during the upgrade process.
94+
ValueError: If neither 'source' nor 'data' is provided, or if SAMM version is not set.
95+
"""
96+
if (source is None) == (data is None):
97+
raise ValueError("Either 'source' or 'data' must be provided.")
98+
if not self._samm_version:
99+
raise ValueError("SAMM version is not set.")
100+
101+
if source:
102+
input_source = source = pathlib.Path(source)
103+
upgrade_method = self._upgrade_source
104+
else:
105+
input_source = data
106+
upgrade_method = self._upgrade_data
107+
108+
if self._has_version_mismatch_in_graph(input_source=input_source): # type: ignore[arg-type]
109+
data = upgrade_method(input_source) # type: ignore[arg-type]
110+
source = None
111+
112+
super().parse(source=source, data=data, **kwargs)
113+
114+
return self
115+
116+
def __add__(self, other: Union["Graph", "AdaptiveGraph"]) -> "AdaptiveGraph":
117+
"""Override addition to propagate SAMM version to the resulting graph."""
118+
retval = super().__add__(other)
119+
120+
if isinstance(retval, AdaptiveGraph):
121+
if isinstance(other, AdaptiveGraph) and other._samm_version != self._samm_version:
122+
raise ValueError("SAMM version mismatch during addition.")
123+
124+
retval.set_samm_version(self._samm_version)
125+
126+
return retval # type: ignore[return-value]
127+
128+
def __sub__(self, other: Union["Graph", "AdaptiveGraph"]) -> "AdaptiveGraph":
129+
"""Override subtraction to propagate SAMM version to the resulting graph."""
130+
retval = super().__sub__(other)
131+
132+
if isinstance(retval, AdaptiveGraph):
133+
if isinstance(other, AdaptiveGraph) and other._samm_version != self._samm_version:
134+
raise ValueError("SAMM version mismatch during subtraction.")
135+
136+
retval.set_samm_version(self._samm_version)
137+
138+
return retval # type: ignore[return-value]
139+
140+
def __mul__(self, other: Union["Graph", "AdaptiveGraph"]) -> "AdaptiveGraph":
141+
"""Override multiplication to propagate SAMM version to the resulting graph."""
142+
retval = super().__mul__(other)
143+
144+
if isinstance(retval, AdaptiveGraph):
145+
if isinstance(other, AdaptiveGraph) and other._samm_version != self._samm_version:
146+
raise ValueError("SAMM version mismatch during multiplication.")
147+
148+
retval.set_samm_version(self._samm_version)
149+
150+
return retval # type: ignore[return-value]

core/esmf-aspect-meta-model-python/esmf_aspect_meta_model_python/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,6 @@
1212

1313
SAMM_VERSION = "2.2.0"
1414
JAVA_CLI_VERSION = "2.11.1"
15+
16+
SAMM_NAMESPACE_PREFIX = "samm"
17+
SAMM_ORG_IDENTIFIER = "org.eclipse.esmf.samm"

core/esmf-aspect-meta-model-python/esmf_aspect_meta_model_python/loader/samm_graph.py

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414

1515
from rdflib import RDF, Graph, Node
1616

17+
from esmf_aspect_meta_model_python.adaptive_graph import AdaptiveGraph
1718
from esmf_aspect_meta_model_python.base.aspect import Aspect
1819
from esmf_aspect_meta_model_python.base.base import Base
1920
from esmf_aspect_meta_model_python.base.property import Property
21+
from esmf_aspect_meta_model_python.constants import SAMM_NAMESPACE_PREFIX, SAMM_ORG_IDENTIFIER
2022
from esmf_aspect_meta_model_python.impl.base_impl import BaseImpl
2123
from esmf_aspect_meta_model_python.loader.default_element_cache import DefaultElementCache
2224
from esmf_aspect_meta_model_python.loader.model_element_factory import ModelElementFactory
@@ -28,10 +30,8 @@
2830
class SAMMGraph:
2931
"""Class representing the SAMM graph and its operations."""
3032

31-
samm_namespace_prefix = "samm"
32-
3333
def __init__(self):
34-
self.rdf_graph = Graph()
34+
self.rdf_graph = AdaptiveGraph()
3535
self.samm_graph = Graph()
3636
self._cache = DefaultElementCache()
3737

@@ -59,7 +59,7 @@ def _get_rdf_graph(self, input_data: Union[str, Path], input_type: Optional[str]
5959
"""Read the RDF graph from the given input data.
6060
6161
This method initializes the `InputHandler` with the provided input data and type,
62-
retrieves the reader, and reads the RDF graph into `self.rdf_graph`.
62+
retrieves the reader, sets the SAMM version, and reads the RDF graph into `self.rdf_graph`.
6363
6464
Args:
6565
input_data (Union[str, Path]): The input data to read the RDF graph from. This can be a file path or a str.
@@ -69,26 +69,9 @@ def _get_rdf_graph(self, input_data: Union[str, Path], input_type: Optional[str]
6969
None
7070
"""
7171
self._reader = InputHandler(input_data, input_type).get_reader()
72+
self._reader.set_samm_version(input_data)
7273
self.rdf_graph = self._reader.read(input_data)
7374

74-
def _get_samm_version_from_rdf_graph(self) -> str:
75-
"""Extracts the SAMM version from the RDF graph.
76-
77-
This method searches through the RDF graph namespaces to find a prefix that indicates the SAMM version.
78-
79-
Returns:
80-
str: The SAMM version as a string extracted from the graph. Returns an empty string if no version
81-
can be conclusively identified.
82-
"""
83-
version = ""
84-
85-
for prefix, namespace in self.rdf_graph.namespace_manager.namespaces():
86-
if prefix == self.samm_namespace_prefix:
87-
urn_parts = namespace.split(":")
88-
version = urn_parts[-1].replace("#", "")
89-
90-
return version
91-
9275
def _get_samm_version(self):
9376
"""Retrieve and set the SAMM version from the RDF graph.
9477
@@ -98,7 +81,7 @@ def _get_samm_version(self):
9881
Raises:
9982
ValueError: If the SAMM version is not found in the RDF graph.
10083
"""
101-
self.samm_version = self._get_samm_version_from_rdf_graph()
84+
self.samm_version = self._reader.samm_version
10285

10386
if not self.samm_version:
10487
raise ValueError(
@@ -220,7 +203,7 @@ def load_aspect_model(self) -> Aspect:
220203

221204
return self.aspect
222205

223-
def _validate_samm_namespace_version(self, graph: Graph) -> None:
206+
def _validate_samm_namespace_version(self, graph: AdaptiveGraph) -> None:
224207
"""
225208
Validates that the SAMM version in the given RDF graph matches the detected SAMM version.
226209
@@ -230,16 +213,16 @@ def _validate_samm_namespace_version(self, graph: Graph) -> None:
230213
do not match, a `ValueError` is raised.
231214
232215
Args:
233-
graph (Graph): The RDF graph whose namespaces are to be validated.
216+
graph (AdaptiveGraph): The RDF graph whose namespaces are to be validated.
234217
235218
Raises:
236219
ValueError: If the SAMM version in the graph's namespace does not match the detected SAMM version.
237220
"""
238221
for prefix, namespace in graph.namespace_manager.namespaces():
239-
if prefix.startswith(self.samm_namespace_prefix):
222+
if prefix.startswith(SAMM_NAMESPACE_PREFIX):
240223
namespace_info = namespace.split(":") # [urn, namespace_id, namespace_specific_str, entity, version#]
241224

242-
if len(namespace_info) == 5 and namespace_info[2] == "org.eclipse.esmf.samm":
225+
if len(namespace_info) == 5 and namespace_info[2] == SAMM_ORG_IDENTIFIER:
243226
version = namespace_info[-1].replace("#", "")
244227

245228
if version != self.samm_version:

core/esmf-aspect-meta-model-python/esmf_aspect_meta_model_python/resolver/base.py

Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
from rdflib import Graph
1717

18+
from esmf_aspect_meta_model_python.adaptive_graph import AdaptiveGraph
19+
from esmf_aspect_meta_model_python.constants import SAMM_NAMESPACE_PREFIX
1820
from esmf_aspect_meta_model_python.samm_meta_model import SammUnitsGraph
1921

2022

@@ -33,7 +35,7 @@ class ResolverInterface(ABC):
3335
"""
3436

3537
def __init__(self):
36-
self.graph = Graph()
38+
self.graph = AdaptiveGraph()
3739
self.samm_graph = None
3840
self.samm_version = ""
3941

@@ -70,46 +72,47 @@ def _validate_samm_version(samm_version: str):
7072
elif samm_version > SammUnitsGraph.SAMM_VERSION:
7173
raise ValueError(f"{samm_version} is not supported SAMM version.")
7274

73-
def _get_samm_version_from_graph(self) -> str:
75+
def set_samm_version(self, steam_input: Union[str, Path]) -> None:
7476
"""
75-
Extracts the SAMM version from the RDF graph.
77+
Sets the SAMM version by extracting it from the specified file.
7678
77-
This method searches through the RDF graph namespaces to find a prefix that indicate the SAMM version.
79+
This method uses the AdaptiveGraph class to extract the SAMM version from the given file.
80+
There is also a validation against known SAMM versions to ensure the version is supported and recognized.
7881
79-
Returns:
80-
str: The SAMM version as a string extracted from the graph. Returns an empty string if no version
81-
can be conclusively identified.
82-
"""
83-
version = ""
84-
85-
for prefix, namespace in self.graph.namespace_manager.namespaces():
86-
if prefix == "samm":
87-
urn_parts = namespace.split(":")
88-
version = urn_parts[-1].replace("#", "")
89-
90-
return version
82+
Args:
83+
steam_input (Union[str, Path]): The path to the file from which the SAMM version is to be extracted.
9184
92-
def get_samm_version(self) -> str:
85+
Raises:
86+
ValueError: If the extracted version is not supported or if it is not found in the file.
9387
"""
94-
Retrieves and validates the specified SAMM version from the provided Aspect model graph.
88+
version = self.get_samm_version_from_input(steam_input)
89+
self._validate_samm_version(version)
90+
self.samm_version = version
9591

96-
This method attempts to extract the version information of the SAMM from a graph. There is also a validation
97-
against known SAMM versions to ensure the version is supported and recognized.
92+
def get_samm_version_from_input(self, stream_input: Union[str, Path]) -> str:
93+
"""
94+
Retrieves the latest SAMM version from the specified stream input.
9895
96+
Args:
97+
stream_input (Union[str, Path]): The path to the file from which the SAMM version is to be extracted.
9998
10099
Returns:
101-
str: The validated version of SAMM if it is recognized and supported. If the version is not valid,
102-
an appropriate message or value indicating non-recognition is returned.
100+
str: The extracted SAMM version.
101+
"""
102+
temp_graph = Graph()
103+
version = ""
103104

104-
Raises:
105-
ValueError: If the extracted version is not supported or if it is not found in the Graph.
105+
if isinstance(stream_input, Path):
106+
temp_graph.parse(source=stream_input, format="turtle")
107+
else:
108+
temp_graph.parse(data=stream_input)
106109

107-
"""
108-
version = self._get_samm_version_from_graph()
109-
self._validate_samm_version(version)
110-
self.samm_version = version
110+
for prefix, namespace in temp_graph.namespace_manager.namespaces():
111+
if prefix.startswith(SAMM_NAMESPACE_PREFIX):
112+
urn_parts = namespace.split(":")
113+
version = urn_parts[-1].replace("#", "")
111114

112115
return version
113116

114-
def prepare_aspect_model(self, graph: Graph):
117+
def prepare_aspect_model(self, graph: AdaptiveGraph):
115118
"""Resolve all additional graph elements if needed."""

core/esmf-aspect-meta-model-python/esmf_aspect_meta_model_python/resolver/data_string.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@
1212
from pathlib import Path
1313
from typing import Union
1414

15-
from rdflib import Graph
16-
15+
from esmf_aspect_meta_model_python.adaptive_graph import AdaptiveGraph
1716
from esmf_aspect_meta_model_python.resolver.base import ResolverInterface
1817

1918

@@ -33,7 +32,7 @@ def read(self, data_string: Union[str, Path]):
3332
Returns:
3433
RDFGraph: An object representing the RDF graph constructed from the input data.
3534
"""
36-
self.graph = Graph()
35+
self.graph = AdaptiveGraph(samm_version=self.samm_version)
3736
self.graph.parse(data=str(data_string) if isinstance(data_string, Path) else data_string)
3837

3938
return self.graph

0 commit comments

Comments
 (0)