Skip to content

Commit acd66e6

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 acd66e6

File tree

16 files changed

+783
-143
lines changed

16 files changed

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