Skip to content

Commit cfad08d

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

File tree

10 files changed

+341
-131
lines changed

10 files changed

+341
-131
lines changed
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
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.Path):
36+
temp_graph.parse(input_source)
37+
else:
38+
temp_graph.parse(data=input_source, format="ttl") # type: ignore[arg-type]
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+
@classmethod
71+
def get_samm_version_from_input(cls, stream_input: pathlib.PurePath | str) -> str:
72+
"""Extract the SAMM version from the given TTL input (file path or string)."""
73+
temp_graph = Graph()
74+
parse_kwargs: dict[str, str | pathlib.PurePath] = {}
75+
version = ""
76+
77+
if isinstance(stream_input, pathlib.PurePath):
78+
parse_kwargs.update(source=stream_input, format="turtle")
79+
else:
80+
parse_kwargs.update(data=stream_input)
81+
82+
temp_graph.parse(**parse_kwargs) # type: ignore[arg-type]
83+
84+
for prefix, namespace in temp_graph.namespace_manager.namespaces():
85+
if prefix.startswith(cls._samm_namespace_prefix):
86+
urn_parts = namespace.split(":")
87+
current_version = urn_parts[-1].replace("#", "")
88+
89+
version = max(version, current_version)
90+
91+
return version
92+
93+
def set_samm_version(self, samm_version: str | None) -> None:
94+
"""Set the SAMM version for this graph."""
95+
self._samm_version = samm_version
96+
97+
def parse( # type: ignore[override]
98+
self,
99+
*,
100+
source: Optional[pathlib.PurePath] = None,
101+
data: Optional[str | bytes] = None,
102+
**kwargs,
103+
) -> "AdaptiveGraph":
104+
"""
105+
Parse a TTL file into this graph, upgrading via SAMM CLI if version mismatch detected.
106+
107+
If a SAMM version mismatch is detected, the TTL file will be upgraded using the SAMM CLI prettyprint
108+
before parsing into this graph.
109+
110+
Args:
111+
source: Path to the TTL file as pathlib.Path.
112+
data: RDF content as string or bytes.
113+
*args, **kwargs: Additional arguments passed to rdflib.Graph.parse().
114+
115+
Returns:
116+
self (AdaptiveGraph): The current graph instance with parsed data.
117+
118+
Raises:
119+
RuntimeError: If the SAMM CLI fails during the upgrade process.
120+
ValueError: If neither 'source' nor 'data' is provided, or if SAMM version is not set.
121+
"""
122+
if (source is None) == (data is None):
123+
raise ValueError("Either 'source' or 'data' must be provided.")
124+
if not self._samm_version:
125+
raise ValueError("SAMM version is not set.")
126+
127+
if self._has_version_mismatch_in_graph(input_source=data or pathlib.Path(source)): # type: ignore[arg-type]
128+
data = self._upgrade_data(data) if data else self._upgrade_source(source) # type: ignore[arg-type]
129+
source = None
130+
131+
super().parse(source=source, data=data, **kwargs)
132+
133+
return self
134+
135+
def __add__(self, other: Union["Graph", "AdaptiveGraph"]) -> "AdaptiveGraph":
136+
"""Override addition to propagate SAMM version to the resulting graph."""
137+
retval = super().__add__(other)
138+
139+
if isinstance(retval, AdaptiveGraph):
140+
if isinstance(other, AdaptiveGraph) and other._samm_version != self._samm_version:
141+
raise ValueError("SAMM version mismatch during addition.")
142+
143+
retval.set_samm_version(self._samm_version)
144+
145+
return retval # type: ignore[return-value]
146+
147+
def __sub__(self, other: Union["Graph", "AdaptiveGraph"]) -> "AdaptiveGraph":
148+
"""Override subtraction to propagate SAMM version to the resulting graph."""
149+
retval = super().__sub__(other)
150+
151+
if isinstance(retval, AdaptiveGraph):
152+
if isinstance(other, AdaptiveGraph) and other._samm_version != self._samm_version:
153+
raise ValueError("SAMM version mismatch during subtraction.")
154+
155+
retval.set_samm_version(self._samm_version)
156+
157+
return retval # type: ignore[return-value]
158+
159+
def __mul__(self, other: Union["Graph", "AdaptiveGraph"]) -> "AdaptiveGraph":
160+
"""Override multiplication to propagate SAMM version to the resulting graph."""
161+
retval = super().__mul__(other)
162+
163+
if isinstance(retval, AdaptiveGraph):
164+
if isinstance(other, AdaptiveGraph) and other._samm_version != self._samm_version:
165+
raise ValueError("SAMM version mismatch during multiplication.")
166+
167+
retval.set_samm_version(self._samm_version)
168+
169+
return retval # type: ignore[return-value]

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

Lines changed: 24 additions & 21 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,25 +71,26 @@ 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
77+
# def _get_samm_version_from_rdf_graph(self) -> str:
78+
# """Extracts the SAMM version from the RDF graph.
79+
#
80+
# This method searches through the RDF graph namespaces to find a prefix that indicates the SAMM version.
81+
#
82+
# Returns:
83+
# str: The SAMM version as a string extracted from the graph. Returns an empty string if no version
84+
# can be conclusively identified.
85+
# """
86+
# version = ""
87+
#
88+
# for prefix, namespace in self.rdf_graph.namespace_manager.namespaces():
89+
# if prefix == self.samm_namespace_prefix:
90+
# urn_parts = namespace.split(":")
91+
# version = urn_parts[-1].replace("#", "")
92+
#
93+
# return version
9194

9295
def _get_samm_version(self):
9396
"""Retrieve and set the SAMM version from the RDF graph.
@@ -98,7 +101,7 @@ def _get_samm_version(self):
98101
Raises:
99102
ValueError: If the SAMM version is not found in the RDF graph.
100103
"""
101-
self.samm_version = self._get_samm_version_from_rdf_graph()
104+
self.samm_version = self._reader.samm_version
102105

103106
if not self.samm_version:
104107
raise ValueError(
@@ -239,7 +242,7 @@ def _validate_samm_namespace_version(self, graph: Graph) -> None:
239242
if prefix.startswith(self.samm_namespace_prefix):
240243
namespace_info = namespace.split(":") # [urn, namespace_id, namespace_specific_str, entity, version#]
241244

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

245248
if version != self.samm_version:

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

Lines changed: 52 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@
1313
from pathlib import Path
1414
from typing import Union
1515

16-
from rdflib import Graph
17-
16+
from esmf_aspect_meta_model_python.adaptive_graph import AdaptiveGraph
1817
from esmf_aspect_meta_model_python.samm_meta_model import SammUnitsGraph
1918

2019

@@ -33,7 +32,7 @@ class ResolverInterface(ABC):
3332
"""
3433

3534
def __init__(self):
36-
self.graph = Graph()
35+
self.graph = AdaptiveGraph()
3736
self.samm_graph = None
3837
self.samm_version = ""
3938

@@ -70,46 +69,63 @@ def _validate_samm_version(samm_version: str):
7069
elif samm_version > SammUnitsGraph.SAMM_VERSION:
7170
raise ValueError(f"{samm_version} is not supported SAMM version.")
7271

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.
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
91-
92-
def get_samm_version(self) -> str:
72+
# def _get_samm_version_from_graph(self) -> str:
73+
# """
74+
# Extracts the SAMM version from the RDF graph.
75+
#
76+
# This method searches through the RDF graph namespaces to find a prefix that indicate the SAMM version.
77+
#
78+
# Returns:
79+
# str: The SAMM version as a string extracted from the graph. Returns an empty string if no version
80+
# can be conclusively identified.
81+
# """
82+
# version = ""
83+
#
84+
# for prefix, namespace in self.graph.namespace_manager.namespaces():
85+
# if prefix == "samm":
86+
# urn_parts = namespace.split(":")
87+
# version = urn_parts[-1].replace("#", "")
88+
#
89+
# return version
90+
91+
# def get_samm_version(self) -> str:
92+
# """
93+
# Retrieves and validates the specified SAMM version from the provided Aspect model graph.
94+
#
95+
# This method attempts to extract the version information of the SAMM from a graph. There is also a validation
96+
# against known SAMM versions to ensure the version is supported and recognized.
97+
#
98+
#
99+
# Returns:
100+
# str: The validated version of SAMM if it is recognized and supported. If the version is not valid,
101+
# an appropriate message or value indicating non-recognition is returned.
102+
#
103+
# Raises:
104+
# ValueError: If the extracted version is not supported or if it is not found in the Graph.
105+
#
106+
# """
107+
# version = self._get_samm_version_from_graph()
108+
# self._validate_samm_version(version)
109+
# self.samm_version = version
110+
#
111+
# return version
112+
113+
def set_samm_version(self, steam_input: Union[str, Path]) -> None:
93114
"""
94-
Retrieves and validates the specified SAMM version from the provided Aspect model graph.
115+
Sets the SAMM version by extracting it from the specified file.
95116
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.
117+
This method uses the AdaptiveGraph class to extract the SAMM version from the given file.
118+
There is also a validation against known SAMM versions to ensure the version is supported and recognized.
98119
99-
100-
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.
120+
Args:
121+
steam_input (Union[str, Path]): The path to the file from which the SAMM version is to be extracted.
103122
104123
Raises:
105-
ValueError: If the extracted version is not supported or if it is not found in the Graph.
106-
124+
ValueError: If the extracted version is not supported or if it is not found in the file.
107125
"""
108-
version = self._get_samm_version_from_graph()
126+
version = AdaptiveGraph.get_samm_version_from_input(steam_input)
109127
self._validate_samm_version(version)
110128
self.samm_version = version
111129

112-
return version
113-
114-
def prepare_aspect_model(self, graph: Graph):
130+
def prepare_aspect_model(self, graph: AdaptiveGraph):
115131
"""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)