Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/push_request_check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ jobs:
cd core/esmf-aspect-meta-model-python
poetry install
poetry run download-samm-release
poetry run download-samm-cli
poetry build

- name: run tests
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -69,5 +69,5 @@ dmypy.json

# SAMM
core/esmf-aspect-meta-model-python/esmf_aspect_meta_model_python/samm_aspect_meta_model/samm/
/core/esmf-aspect-meta-model-python/samm-cli/
core/esmf-aspect-meta-model-python/esmf_aspect_meta_model_python/samm_cli/samm-cli/
/core/esmf-aspect-meta-model-python/tests/integration/aspect_model_loader/java_models/resources/
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import pathlib
import subprocess
import tempfile

from typing import Optional, Union

from rdflib import Graph

from esmf_aspect_meta_model_python import utils
from esmf_aspect_meta_model_python.constants import SAMM_VERSION
from esmf_aspect_meta_model_python.samm_cli import SammCli


class AdaptiveGraph(Graph): # TODO: avoid double parsing when an upgrade is not performed
"""An RDF graph that can adaptively upgrade SAMM files using the SAMM CLI."""

_samm_cli = SammCli()

def __init__(self, samm_version: str = SAMM_VERSION, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)

self._samm_version = samm_version

def _upgrade_ttl_file(self, file_path: pathlib.Path) -> str:
"""Run SAMM CLI prettyprint to upgrade a TTL file to the latest version."""
try:
return self._samm_cli.prettyprint(str(file_path), capture=True)
except subprocess.CalledProcessError as e:
raise RuntimeError(f"SAMM CLI failed for {file_path}:\n{e.stdout}\n{e.stderr}") from e

def _upgrade_source(self, source_path: pathlib.Path) -> str:
print(f"[INFO] SAMM version mismatch detected in {source_path}. Upgrading...")

return self._upgrade_ttl_file(source_path)

def _upgrade_data(self, data: str | bytes) -> str:
print( # TODO: improve logging
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imact 0:
You can create a task in backlog to analyze and implement a logging.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

f"[INFO] SAMM version mismatch detected in provided data (target v{self._samm_version}) Upgrading..."
)

with tempfile.NamedTemporaryFile("wb", suffix=".ttl", delete=False) as tmp:
tmp.write(data.encode("utf-8") if isinstance(data, str) else data)
tmp_path = pathlib.Path(tmp.name)

try:
return self._upgrade_ttl_file(tmp_path)
finally:
tmp_path.unlink(missing_ok=True)

def set_samm_version(self, samm_version: str) -> None:
"""Set the SAMM version for this graph."""
self._samm_version = samm_version

def parse( # type: ignore[override]
self,
*,
source: Optional[str | pathlib.Path] = None,
data: Optional[str | bytes] = None,
**kwargs,
) -> "AdaptiveGraph":
"""
Parse a TTL file into this graph, upgrading via SAMM CLI if version mismatch detected.

If a SAMM version mismatch is detected, the TTL file will be upgraded using the SAMM CLI prettyprint
before parsing into this graph.

Args:
source: Path to the TTL file as pathlib.Path or str.
data: RDF content as string or bytes.
**kwargs: Additional arguments passed to rdflib.Graph.parse().

Returns:
self (AdaptiveGraph): The current graph instance with parsed data.

Raises:
RuntimeError: If the SAMM CLI fails during the upgrade process.
ValueError: If neither 'source' nor 'data' is provided, or if both are provided.
"""
if (source is None) == (data is None):
raise ValueError("Either 'source' or 'data' must be provided.")

if source:
input_source = source = pathlib.Path(source)
upgrade_method = self._upgrade_source
else:
input_source = data # type: ignore[assignment]
upgrade_method = self._upgrade_data # type: ignore[assignment]

if utils.has_version_mismatch_from_input(input_source, samm_version=self._samm_version):
data = upgrade_method(input_source)
source = None

super().parse(source=source, data=data, **kwargs)

return self

def __add__(self, other: Union["Graph", "AdaptiveGraph"]) -> "AdaptiveGraph":
"""Override addition to propagate SAMM version to the resulting graph."""
retval = super().__add__(other)

if isinstance(retval, AdaptiveGraph):
if isinstance(other, AdaptiveGraph) and other._samm_version != self._samm_version:
raise ValueError("SAMM version mismatch during addition.")

retval.set_samm_version(self._samm_version)

return retval # type: ignore[return-value]

def __sub__(self, other: Union["Graph", "AdaptiveGraph"]) -> "AdaptiveGraph":
"""Override subtraction to propagate SAMM version to the resulting graph."""
retval = super().__sub__(other)

if isinstance(retval, AdaptiveGraph):
if isinstance(other, AdaptiveGraph) and other._samm_version != self._samm_version:
raise ValueError("SAMM version mismatch during subtraction.")

retval.set_samm_version(self._samm_version)

return retval # type: ignore[return-value]

def __mul__(self, other: Union["Graph", "AdaptiveGraph"]) -> "AdaptiveGraph":
"""Override multiplication to propagate SAMM version to the resulting graph."""
retval = super().__mul__(other)

if isinstance(retval, AdaptiveGraph):
if isinstance(other, AdaptiveGraph) and other._samm_version != self._samm_version:
raise ValueError("SAMM version mismatch during multiplication.")

retval.set_samm_version(self._samm_version)

return retval # type: ignore[return-value]
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,7 @@


SAMM_VERSION = "2.2.0"
JAVA_CLI_VERSION = "2.11.1"
JAVA_CLI_VERSION = "2.12.0"

SAMM_NAMESPACE_PREFIX = "samm"
SAMM_ORG_IDENTIFIER = "org.eclipse.esmf.samm"
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
#
# SPDX-License-Identifier: MPL-2.0

from pathlib import Path
from typing import List, Optional, Union

from rdflib import RDF, Graph, Node

import esmf_aspect_meta_model_python.constants as const

from esmf_aspect_meta_model_python import utils
from esmf_aspect_meta_model_python.adaptive_graph import AdaptiveGraph
from esmf_aspect_meta_model_python.base.aspect import Aspect
from esmf_aspect_meta_model_python.base.base import Base
from esmf_aspect_meta_model_python.base.property import Property
Expand All @@ -28,26 +31,20 @@
class SAMMGraph:
"""Class representing the SAMM graph and its operations."""

samm_namespace_prefix = "samm"

def __init__(self):
self.rdf_graph = Graph()
self.rdf_graph = AdaptiveGraph()
self.samm_graph = Graph()
self._cache = DefaultElementCache()

self.samm_version = None
self.samm_version = const.SAMM_VERSION
self.aspect = None
self.model_elements = None
self._samm = None
self._reader = None

def __str__(self) -> str:
"""Object string representation."""
str_data = "SAMMGraph"
if self.samm_version:
str_data += f" v{self.samm_version}"

return str_data
return f"SAMMGraph v{self.samm_version}"

def __repr__(self) -> str:
"""Object representation."""
Expand All @@ -71,40 +68,6 @@ def _get_rdf_graph(self, input_data: Union[str, Path], input_type: Optional[str]
self._reader = InputHandler(input_data, input_type).get_reader()
self.rdf_graph = self._reader.read(input_data)

def _get_samm_version_from_rdf_graph(self) -> str:
"""Extracts the SAMM version from the RDF graph.

This method searches through the RDF graph namespaces to find a prefix that indicates the SAMM version.

Returns:
str: The SAMM version as a string extracted from the graph. Returns an empty string if no version
can be conclusively identified.
"""
version = ""

for prefix, namespace in self.rdf_graph.namespace_manager.namespaces():
if prefix == self.samm_namespace_prefix:
urn_parts = namespace.split(":")
version = urn_parts[-1].replace("#", "")

return version

def _get_samm_version(self):
"""Retrieve and set the SAMM version from the RDF graph.

This method extracts the SAMM version from the RDF graph and assigns it to the `samm_version` attribute.
If the SAMM version is not found, it raises a ValueError.

Raises:
ValueError: If the SAMM version is not found in the RDF graph.
"""
self.samm_version = self._get_samm_version_from_rdf_graph()

if not self.samm_version:
raise ValueError(
f"SAMM version number was not found in graph. Could not process RDF graph {self.rdf_graph}."
)

def _get_samm(self):
"""Initialize the SAMM object with the current SAMM version."""
self._samm = SAMM(self.samm_version)
Expand Down Expand Up @@ -132,7 +95,6 @@ def parse(self, input_data: Union[str, Path], input_type: Optional[str] = None):
SAMMGraph: The instance of the SAMMGraph with the parsed data.
"""
self._get_rdf_graph(input_data, input_type)
self._get_samm_version()
self._get_samm()
self._get_samm_graph()

Expand Down Expand Up @@ -213,12 +175,30 @@ def load_aspect_model(self) -> Aspect:

graph = self.rdf_graph + self.samm_graph
self._reader.prepare_aspect_model(graph)
self._validate_samm_namespace_version(graph)

model_element_factory = ModelElementFactory(self.samm_version, graph, self._cache)
self.aspect = model_element_factory.create_element(aspect_urn)

return self.aspect

def _validate_samm_namespace_version(self, graph: AdaptiveGraph) -> None:
"""
Validate that the SAMM version in the graph's namespace matches the detected SAMM version.

Args:
graph: The RDF graph whose namespaces are to be validated.

Raises:
ValueError: If the SAMM version in the graph's namespace does not match the detected SAMM version.
"""
for version in utils.get_samm_versions_from_graph(graph):
if version != self.samm_version:
raise ValueError(
f"SAMM version mismatch. Found '{version}', but expected '{self.samm_version}'. "
"Ensure all RDF files use a single, consistent SAMM version"
)

def _get_aspect_from_elements(self):
"""Geta and save the Aspect element from the model elements."""
if self.model_elements:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
from pathlib import Path
from typing import Union

from rdflib import Graph
import esmf_aspect_meta_model_python.constants as const

from esmf_aspect_meta_model_python.adaptive_graph import AdaptiveGraph
from esmf_aspect_meta_model_python.samm_meta_model import SammUnitsGraph


Expand All @@ -33,9 +34,9 @@ class ResolverInterface(ABC):
"""

def __init__(self):
self.graph = Graph()
self.graph = AdaptiveGraph()
self.samm_graph = None
self.samm_version = ""
self.samm_version = const.SAMM_VERSION

@abstractmethod
def read(self, input_data: Union[str, Path]):
Expand Down Expand Up @@ -70,46 +71,5 @@ def _validate_samm_version(samm_version: str):
elif samm_version > SammUnitsGraph.SAMM_VERSION:
raise ValueError(f"{samm_version} is not supported SAMM version.")

def _get_samm_version_from_graph(self) -> str:
"""
Extracts the SAMM version from the RDF graph.

This method searches through the RDF graph namespaces to find a prefix that indicate the SAMM version.

Returns:
str: The SAMM version as a string extracted from the graph. Returns an empty string if no version
can be conclusively identified.
"""
version = ""

for prefix, namespace in self.graph.namespace_manager.namespaces():
if prefix == "samm":
urn_parts = namespace.split(":")
version = urn_parts[-1].replace("#", "")

return version

def get_samm_version(self) -> str:
"""
Retrieves and validates the specified SAMM version from the provided Aspect model graph.

This method attempts to extract the version information of the SAMM from a graph. There is also a validation
against known SAMM versions to ensure the version is supported and recognized.


Returns:
str: The validated version of SAMM if it is recognized and supported. If the version is not valid,
an appropriate message or value indicating non-recognition is returned.

Raises:
ValueError: If the extracted version is not supported or if it is not found in the Graph.

"""
version = self._get_samm_version_from_graph()
self._validate_samm_version(version)
self.samm_version = version

return version

def prepare_aspect_model(self, graph: Graph):
def prepare_aspect_model(self, graph: AdaptiveGraph):
"""Resolve all additional graph elements if needed."""
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@
from pathlib import Path
from typing import Union

from rdflib import Graph

from esmf_aspect_meta_model_python.adaptive_graph import AdaptiveGraph
from esmf_aspect_meta_model_python.resolver.base import ResolverInterface


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

return self.graph
Loading