Skip to content

Commit 703e820

Browse files
Oleksandr Muzyka (EPAM)Oleksandr Muzyka (EPAM)
authored andcommitted
Add the capability to upgrade a .ttl file version using AdaptiveGraph
1 parent 1885bf9 commit 703e820

File tree

15 files changed

+777
-138
lines changed

15 files changed

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

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

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
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
@@ -29,9 +30,10 @@ class SAMMGraph:
2930
"""Class representing the SAMM graph and its operations."""
3031

3132
samm_namespace_prefix = "samm"
33+
samm_org_identifier = "org.eclipse.esmf.samm"
3234

3335
def __init__(self):
34-
self.rdf_graph = Graph()
36+
self.rdf_graph = AdaptiveGraph()
3537
self.samm_graph = Graph()
3638
self._cache = DefaultElementCache()
3739

@@ -59,7 +61,7 @@ def _get_rdf_graph(self, input_data: Union[str, Path], input_type: Optional[str]
5961
"""Read the RDF graph from the given input data.
6062
6163
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`.
64+
retrieves the reader, sets the SAMM version, and reads the RDF graph into `self.rdf_graph`.
6365
6466
Args:
6567
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 +71,9 @@ def _get_rdf_graph(self, input_data: Union[str, Path], input_type: Optional[str]
6971
None
7072
"""
7173
self._reader = InputHandler(input_data, input_type).get_reader()
74+
self._reader.set_samm_version(input_data)
7275
self.rdf_graph = self._reader.read(input_data)
7376

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-
9277
def _get_samm_version(self):
9378
"""Retrieve and set the SAMM version from the RDF graph.
9479
@@ -98,7 +83,7 @@ def _get_samm_version(self):
9883
Raises:
9984
ValueError: If the SAMM version is not found in the RDF graph.
10085
"""
101-
self.samm_version = self._get_samm_version_from_rdf_graph()
86+
self.samm_version = self._reader.samm_version
10287

10388
if not self.samm_version:
10489
raise ValueError(
@@ -220,7 +205,7 @@ def load_aspect_model(self) -> Aspect:
220205

221206
return self.aspect
222207

223-
def _validate_samm_namespace_version(self, graph: Graph) -> None:
208+
def _validate_samm_namespace_version(self, graph: AdaptiveGraph) -> None:
224209
"""
225210
Validates that the SAMM version in the given RDF graph matches the detected SAMM version.
226211
@@ -230,7 +215,7 @@ def _validate_samm_namespace_version(self, graph: Graph) -> None:
230215
do not match, a `ValueError` is raised.
231216
232217
Args:
233-
graph (Graph): The RDF graph whose namespaces are to be validated.
218+
graph (AdaptiveGraph): The RDF graph whose namespaces are to be validated.
234219
235220
Raises:
236221
ValueError: If the SAMM version in the graph's namespace does not match the detected SAMM version.
@@ -239,7 +224,7 @@ def _validate_samm_namespace_version(self, graph: Graph) -> None:
239224
if prefix.startswith(self.samm_namespace_prefix):
240225
namespace_info = namespace.split(":") # [urn, namespace_id, namespace_specific_str, entity, version#]
241226

242-
if len(namespace_info) == 5 and namespace_info[2] == "org.eclipse.esmf.samm":
227+
if len(namespace_info) == 5 and namespace_info[2] == self.samm_org_identifier:
243228
version = namespace_info[-1].replace("#", "")
244229

245230
if version != self.samm_version:

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

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

1616
from rdflib import Graph
1717

18+
from esmf_aspect_meta_model_python.adaptive_graph import AdaptiveGraph
1819
from esmf_aspect_meta_model_python.samm_meta_model import SammUnitsGraph
1920

2021

@@ -32,8 +33,10 @@ class ResolverInterface(ABC):
3233
get_samm_version(): Method to find a SAMM version.
3334
"""
3435

36+
samm_namespace_prefix = "samm"
37+
3538
def __init__(self):
36-
self.graph = Graph()
39+
self.graph = AdaptiveGraph()
3740
self.samm_graph = None
3841
self.samm_version = ""
3942

@@ -70,46 +73,47 @@ def _validate_samm_version(samm_version: str):
7073
elif samm_version > SammUnitsGraph.SAMM_VERSION:
7174
raise ValueError(f"{samm_version} is not supported SAMM version.")
7275

73-
def _get_samm_version_from_graph(self) -> str:
74-
"""
75-
Extracts the SAMM version from the RDF graph.
76-
77-
This method searches through the RDF graph namespaces to find a prefix that indicate 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.
76+
def set_samm_version(self, steam_input: Union[str, Path]) -> None:
8277
"""
83-
version = ""
78+
Sets the SAMM version by extracting it from the specified file.
8479
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("#", "")
80+
This method uses the AdaptiveGraph class to extract the SAMM version from the given file.
81+
There is also a validation against known SAMM versions to ensure the version is supported and recognized.
8982
90-
return version
83+
Args:
84+
steam_input (Union[str, Path]): The path to the file from which the SAMM version is to be extracted.
9185
92-
def get_samm_version(self) -> str:
86+
Raises:
87+
ValueError: If the extracted version is not supported or if it is not found in the file.
9388
"""
94-
Retrieves and validates the specified SAMM version from the provided Aspect model graph.
89+
version = self.get_samm_version_from_input(steam_input)
90+
self._validate_samm_version(version)
91+
self.samm_version = version
9592

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.
93+
def get_samm_version_from_input(self, stream_input: Union[str, Path]) -> str:
94+
"""
95+
Retrieves the latest SAMM version from the specified stream input.
9896
97+
Args:
98+
stream_input (Union[str, Path]): The path to the file from which the SAMM version is to be extracted.
9999
100100
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.
101+
str: The extracted SAMM version.
102+
"""
103+
temp_graph = Graph()
104+
version = ""
103105

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

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

112116
return version
113117

114-
def prepare_aspect_model(self, graph: Graph):
118+
def prepare_aspect_model(self, graph: AdaptiveGraph):
115119
"""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)