From 9adc57764141648b608197c39d06915021f06af1 Mon Sep 17 00:00:00 2001 From: "Oleksandr Muzyka (EPAM)" Date: Thu, 28 Aug 2025 15:59:00 +0200 Subject: [PATCH 1/3] Fix SAMM CLI usage --- .../constants.py | 14 ++ .../samm_cli/base.py | 4 +- .../samm_cli/constants.py | 12 ++ .../samm_cli/download.py | 83 +++++++++ .../samm_meta_model.py | 4 +- .../scripts/constants.py | 12 +- .../scripts/download_samm_cli.py | 74 +------- .../tests/unit/samm_cli/test_base.py | 4 +- .../tests/unit/samm_cli/test_download.py | 163 ++++++++++++++++++ 9 files changed, 281 insertions(+), 89 deletions(-) create mode 100644 core/esmf-aspect-meta-model-python/esmf_aspect_meta_model_python/constants.py create mode 100644 core/esmf-aspect-meta-model-python/esmf_aspect_meta_model_python/samm_cli/download.py create mode 100644 core/esmf-aspect-meta-model-python/tests/unit/samm_cli/test_download.py diff --git a/core/esmf-aspect-meta-model-python/esmf_aspect_meta_model_python/constants.py b/core/esmf-aspect-meta-model-python/esmf_aspect_meta_model_python/constants.py new file mode 100644 index 0000000..b525ef8 --- /dev/null +++ b/core/esmf-aspect-meta-model-python/esmf_aspect_meta_model_python/constants.py @@ -0,0 +1,14 @@ +# Copyright (c) 2023 Robert Bosch Manufacturing Solutions GmbH +# +# See the AUTHORS file(s) distributed with this work for additional +# information regarding authorship. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 + + +SAMM_VERSION = "2.2.0" +JAVA_CLI_VERSION = "2.10.2" diff --git a/core/esmf-aspect-meta-model-python/esmf_aspect_meta_model_python/samm_cli/base.py b/core/esmf-aspect-meta-model-python/esmf_aspect_meta_model_python/samm_cli/base.py index 8e00636..d5fba53 100644 --- a/core/esmf-aspect-meta-model-python/esmf_aspect_meta_model_python/samm_cli/base.py +++ b/core/esmf-aspect-meta-model-python/esmf_aspect_meta_model_python/samm_cli/base.py @@ -15,7 +15,7 @@ from typing import Any from esmf_aspect_meta_model_python.samm_cli.constants import SAMMCLICommands, SAMMCLICommandTypes -from scripts.download_samm_cli import download_samm_cli +from esmf_aspect_meta_model_python.samm_cli.download import download_samm_cli class SammCli: @@ -33,7 +33,7 @@ def __init__(self): def _get_client_path(): """Get path to the SAMM CLI executable file.""" base_path = Path(__file__).resolve() - cli_path = join(base_path.parents[2], "samm-cli", "samm.exe") + cli_path = join(base_path.parents[0], "samm-cli", "samm.exe") return cli_path diff --git a/core/esmf-aspect-meta-model-python/esmf_aspect_meta_model_python/samm_cli/constants.py b/core/esmf-aspect-meta-model-python/esmf_aspect_meta_model_python/samm_cli/constants.py index c6338d6..57f93a9 100644 --- a/core/esmf-aspect-meta-model-python/esmf_aspect_meta_model_python/samm_cli/constants.py +++ b/core/esmf-aspect-meta-model-python/esmf_aspect_meta_model_python/samm_cli/constants.py @@ -10,6 +10,18 @@ # SPDX-License-Identifier: MPL-2.0 """Constants for SAMM CLI commands and types.""" +from string import Template + +from esmf_aspect_meta_model_python.constants import JAVA_CLI_VERSION, SAMM_VERSION + + +class SAMMCliConstants: + BASE_PATH = Template("https://github.com/eclipse-esmf/esmf-sdk/releases/download/v$version_number/$file_name") + JAVA_CLI_VERSION = JAVA_CLI_VERSION + LINUX_FILE_NAME = Template("samm-cli-$version_number-linux-x86_64.tar.gz") + SAMM_VERSION = SAMM_VERSION + WIN_FILE_NAME = Template("samm-cli-$version_number-windows-x86_64.zip") + class SAMMCLICommands: """SAMM CLI command names.""" diff --git a/core/esmf-aspect-meta-model-python/esmf_aspect_meta_model_python/samm_cli/download.py b/core/esmf-aspect-meta-model-python/esmf_aspect_meta_model_python/samm_cli/download.py new file mode 100644 index 0000000..2686376 --- /dev/null +++ b/core/esmf-aspect-meta-model-python/esmf_aspect_meta_model_python/samm_cli/download.py @@ -0,0 +1,83 @@ +"""Download SAMM CLI. + +Windows: https://github.com/eclipse-esmf/esmf-sdk/releases/download/v2.10.2/samm-cli-2.10.2-windows-x86_64.zip + Linux: https://github.com/eclipse-esmf/esmf-sdk/releases/download/v2.10.2/samm-cli-2.10.2-linux-x86_64.tar.gz + JAR: https://github.com/eclipse-esmf/esmf-sdk/releases/download/v2.10.2/samm-cli-2.10.2.jar +""" + +import os +import platform +import sys +import zipfile + +from pathlib import Path + +import requests + +from esmf_aspect_meta_model_python.samm_cli.constants import SAMMCliConstants as Const + + +def get_samm_cli_file_name(): + """Get a SAMM CLI file name for the current platform.""" + + if platform.system() == "Windows": + file_name = Const.WIN_FILE_NAME.substitute(version_number=Const.JAVA_CLI_VERSION) + elif platform.system() == "Linux": + file_name = Const.LINUX_FILE_NAME.substitute(version_number=Const.JAVA_CLI_VERSION) + else: + raise NotImplementedError( + f"Please download a SAMM CLI manually for your operation system from '{Const.BASE_PATH}'" + ) + + return file_name + + +def download_archive_file(url, archive_file): + """Download an archive file.""" + with open(archive_file, "wb") as f: + print("Downloading %s" % archive_file) + response = requests.get(url, allow_redirects=True, stream=True) + content_len = response.headers.get("content-length") + + if content_len is None: + f.write(response.content) + else: + total_len = int(content_len) + data_len = 0 + chunk = 4096 + progress_bar_len = 50 + + for content_data in response.iter_content(chunk_size=chunk): + data_len += len(content_data) + + f.write(content_data) + + curr_progress = int(50 * data_len / total_len) + sys.stdout.write(f"\r[{'*' * curr_progress}{' ' * (progress_bar_len - curr_progress)}]") + sys.stdout.flush() + + +def download_samm_cli(): + try: + samm_cli_file_name = get_samm_cli_file_name() + except NotImplementedError as error: + print(error) + else: + print(f"Start downloading SAMM CLI {samm_cli_file_name}") + url = Const.BASE_PATH.substitute(version_number=Const.JAVA_CLI_VERSION, file_name=samm_cli_file_name) + dir_path = Path(__file__).resolve().parents[0] + archive_file = os.path.join(dir_path, samm_cli_file_name) + + download_archive_file(url, archive_file) + print("\nSAMM CLI archive file downloaded") + + print("Start extracting files") + archive = zipfile.ZipFile(archive_file) + extracted_files_path = os.path.join(dir_path, "samm-cli") + for file in archive.namelist(): + archive.extract(file, extracted_files_path) + archive.close() + print("Done extracting files") + + print("Deleting SAMM CLI archive file") + os.remove(archive_file) diff --git a/core/esmf-aspect-meta-model-python/esmf_aspect_meta_model_python/samm_meta_model.py b/core/esmf-aspect-meta-model-python/esmf_aspect_meta_model_python/samm_meta_model.py index 9b68599..9b12f1f 100644 --- a/core/esmf-aspect-meta-model-python/esmf_aspect_meta_model_python/samm_meta_model.py +++ b/core/esmf-aspect-meta-model-python/esmf_aspect_meta_model_python/samm_meta_model.py @@ -16,11 +16,13 @@ import rdflib +import esmf_aspect_meta_model_python.constants as const + class SammUnitsGraph: """Model units graph.""" - SAMM_VERSION = "2.2.0" + SAMM_VERSION = const.SAMM_VERSION UNIT_FILE_PATH = f"samm_aspect_meta_model/samm/unit/{SAMM_VERSION}/units.ttl" QUERY_TEMPLATE = Template("SELECT ?key ?value WHERE {$unit ?key ?value .}") diff --git a/core/esmf-aspect-meta-model-python/scripts/constants.py b/core/esmf-aspect-meta-model-python/scripts/constants.py index 7b67b33..4b28500 100644 --- a/core/esmf-aspect-meta-model-python/scripts/constants.py +++ b/core/esmf-aspect-meta-model-python/scripts/constants.py @@ -12,17 +12,7 @@ from os.path import join from string import Template - -SAMM_VERSION = "2.2.0" -JAVA_CLI_VERSION = "2.10.2" - - -class SAMMCliConstants: - BASE_PATH = Template("https://github.com/eclipse-esmf/esmf-sdk/releases/download/v$version_number/$file_name") - JAVA_CLI_VERSION = JAVA_CLI_VERSION - LINUX_FILE_NAME = Template("samm-cli-$version_number-linux-x86_64.tar.gz") - SAMM_VERSION = SAMM_VERSION - WIN_FILE_NAME = Template("samm-cli-$version_number-windows-x86_64.zip") +from esmf_aspect_meta_model_python.constants import JAVA_CLI_VERSION, SAMM_VERSION class TestModelConstants: diff --git a/core/esmf-aspect-meta-model-python/scripts/download_samm_cli.py b/core/esmf-aspect-meta-model-python/scripts/download_samm_cli.py index 4f1fee1..c6a3ea9 100644 --- a/core/esmf-aspect-meta-model-python/scripts/download_samm_cli.py +++ b/core/esmf-aspect-meta-model-python/scripts/download_samm_cli.py @@ -5,79 +5,7 @@ JAR: https://github.com/eclipse-esmf/esmf-sdk/releases/download/v2.10.2/samm-cli-2.10.2.jar """ -import os -from pathlib import Path -import platform -import requests -import sys -import zipfile - -from scripts.constants import SAMMCliConstants as Const - - -def get_samm_cli_file_name(): - """Get a SAMM CLI file name for the current platform.""" - - if platform.system() == "Windows": - file_name = Const.WIN_FILE_NAME.substitute(version_number=Const.JAVA_CLI_VERSION) - elif platform.system() == "Linux": - file_name = Const.LINUX_FILE_NAME.substitute(version_number=Const.JAVA_CLI_VERSION) - else: - raise NotImplementedError( - f"Please download a SAMM CLI manually for your operation system from '{Const.BASE_PATH}'" - ) - - return file_name - - -def download_archive_file(url, archive_file): - """Download an archive file.""" - with open(archive_file, "wb") as f: - print("Downloading %s" % archive_file) - response = requests.get(url, allow_redirects=True, stream=True) - content_len = response.headers.get('content-length') - - if content_len is None: - f.write(response.content) - else: - total_len = int(content_len) - data_len = 0 - chunk = 4096 - progress_bar_len = 50 - - for content_data in response.iter_content(chunk_size=chunk): - data_len += len(content_data) - - f.write(content_data) - - curr_progress = int(50 * data_len / total_len) - sys.stdout.write(f"\r[{'*' * curr_progress}{' ' * (progress_bar_len - curr_progress)}]") - sys.stdout.flush() - - -def download_samm_cli(): - try: - samm_cli_file_name = get_samm_cli_file_name() - except NotImplementedError as error: - print(error) - else: - print(f"Start downloading SAMM CLI {samm_cli_file_name}") - url = Const.BASE_PATH.substitute(version_number=Const.JAVA_CLI_VERSION, file_name=samm_cli_file_name) - dir_path = Path(__file__).resolve().parents[1] - archive_file = os.path.join(dir_path, samm_cli_file_name) - - download_archive_file(url, archive_file) - print("\nSAMM CLI archive file downloaded") - - print("Start extracting files") - archive = zipfile.ZipFile(archive_file) - for file in archive.namelist(): - archive.extract(file, "samm-cli") - archive.close() - print("Done extracting files.") - - print("Deleting SAMM CLI archive file.") - os.remove(archive_file) +from esmf_aspect_meta_model_python.samm_cli.download import download_samm_cli if __name__ == "__main__": diff --git a/core/esmf-aspect-meta-model-python/tests/unit/samm_cli/test_base.py b/core/esmf-aspect-meta-model-python/tests/unit/samm_cli/test_base.py index 3a867d9..05c0043 100644 --- a/core/esmf-aspect-meta-model-python/tests/unit/samm_cli/test_base.py +++ b/core/esmf-aspect-meta-model-python/tests/unit/samm_cli/test_base.py @@ -43,7 +43,7 @@ def test_init(get_client_path_mock, validate_client_mock): @mock.patch(f"{CLASS_PATH}._validate_client") def test_get_client_path(_, path_mock, join_mock): base_path_mock = mock.MagicMock(name="base_path") - base_path_mock.parents = ["parent_0", "parent_1", "parent_2"] + base_path_mock.parents = ["parent", "parent_1", "parent_2"] path_mock.return_value = path_mock path_mock.resolve.return_value = base_path_mock join_mock.return_value = "cli_path" @@ -53,7 +53,7 @@ def test_get_client_path(_, path_mock, join_mock): assert result == "cli_path" path_mock.resolve.assert_called_once() - join_mock.assert_called_once_with("parent_2", "samm-cli", "samm.exe") + join_mock.assert_called_once_with("parent", "samm-cli", "samm.exe") @mock.patch(f"{BASE_PATH}.download_samm_cli") diff --git a/core/esmf-aspect-meta-model-python/tests/unit/samm_cli/test_download.py b/core/esmf-aspect-meta-model-python/tests/unit/samm_cli/test_download.py new file mode 100644 index 0000000..50306eb --- /dev/null +++ b/core/esmf-aspect-meta-model-python/tests/unit/samm_cli/test_download.py @@ -0,0 +1,163 @@ +"""SAMM client download script test suite.""" + +from unittest import mock + +import pytest + +import esmf_aspect_meta_model_python.samm_cli.download as download_module + +from esmf_aspect_meta_model_python.samm_cli.constants import SAMMCliConstants as Const +from esmf_aspect_meta_model_python.samm_cli.download import ( + download_archive_file, + download_samm_cli, + get_samm_cli_file_name, +) + +BASE_SCRIPT_PATH = "esmf_aspect_meta_model_python.samm_cli.download" + + +class TestGetSammCliFileNme: + @mock.patch(f"{BASE_SCRIPT_PATH}.platform.system") + def test_get_samm_cli_file_name_windows(self, system_mock): + system_mock.return_value = "Windows" + expected_file_name = Const.WIN_FILE_NAME.substitute(version_number=Const.JAVA_CLI_VERSION) + result = get_samm_cli_file_name() + + assert result == expected_file_name + + @mock.patch(f"{BASE_SCRIPT_PATH}.platform.system") + def test_get_samm_cli_file_name_linux(self, system_mock): + system_mock.return_value = "Linux" + expected_file_name = Const.LINUX_FILE_NAME.substitute(version_number=Const.JAVA_CLI_VERSION) + result = get_samm_cli_file_name() + + assert result == expected_file_name + + @mock.patch(f"{BASE_SCRIPT_PATH}.platform.system") + def test_get_samm_cli_file_name_raise_exception(self, system_mock): + system_mock.return_value = "MacOS" + with pytest.raises(NotImplementedError) as error: + get_samm_cli_file_name() + + assert str(error.value) == ( + f"Please download a SAMM CLI manually for your operation system from '{Const.BASE_PATH}'" + ) + + +class TestArchiveFileDownload: + @mock.patch(f"{BASE_SCRIPT_PATH}.requests.get") + @mock.patch(f"{BASE_SCRIPT_PATH}.print") + @mock.patch(f"{BASE_SCRIPT_PATH}.open") + def test_download_archive_file_no_content(self, open_mock, print_mock, requests_get_mock): + f_mock = mock.MagicMock(name="file_mock") + f_mock.__enter__.return_value = f_mock + open_mock.return_value = f_mock + response_mock = mock.MagicMock(name="response_mock") + response_mock.content = "response_content" + response_mock.headers.get.return_value = None + requests_get_mock.return_value = response_mock + result = download_archive_file("http://example.com/archive.zip", "archive_file_path") + + assert result is None + open_mock.assert_called_once_with("archive_file_path", "wb") + print_mock.assert_called_once_with("Downloading archive_file_path") + requests_get_mock.assert_called_once_with("http://example.com/archive.zip", allow_redirects=True, stream=True) + response_mock.headers.get.assert_called_once_with("content-length") + f_mock.__enter__.assert_called_once() + f_mock.write.assert_called_once_with("response_content") + + @mock.patch(f"{BASE_SCRIPT_PATH}.sys.stdout") + @mock.patch(f"{BASE_SCRIPT_PATH}.requests.get") + @mock.patch(f"{BASE_SCRIPT_PATH}.print") + @mock.patch(f"{BASE_SCRIPT_PATH}.open") + def test_download_archive_file(self, open_mock, print_mock, requests_get_mock, sys_stdout_mock): + f_mock = mock.MagicMock(name="file_mock") + f_mock.__enter__.return_value = f_mock + open_mock.return_value = f_mock + response_mock = mock.MagicMock(name="response_mock") + response_mock.content = "response_content" + response_mock.headers.get.return_value = "42" + response_mock.iter_content.return_value = ["content_data"] + requests_get_mock.return_value = response_mock + result = download_archive_file("http://example.com/archive.zip", "archive_file_path") + + assert result is None + open_mock.assert_called_once_with("archive_file_path", "wb") + print_mock.assert_called_once_with("Downloading archive_file_path") + requests_get_mock.assert_called_once_with("http://example.com/archive.zip", allow_redirects=True, stream=True) + response_mock.headers.get.assert_called_once_with("content-length") + response_mock.iter_content.assert_called_once_with(chunk_size=4096) + f_mock.__enter__.assert_called_once() + f_mock.write.assert_called_once_with("content_data") + sys_stdout_mock.write.assert_called_once_with(f"\r[{'*' * 14}{' ' * 36}]") + sys_stdout_mock.flush.assert_called_once() + + +class TestDownloadSammCli: + @mock.patch(f"{BASE_SCRIPT_PATH}.print") + @mock.patch(f"{BASE_SCRIPT_PATH}.get_samm_cli_file_name") + def test_download_samm_cli_error(self, get_samm_cli_file_name_mock, print_mock): + test_error = NotImplementedError("exception_message") + get_samm_cli_file_name_mock.side_effect = test_error + + result = download_samm_cli() + + assert result is None + get_samm_cli_file_name_mock.assert_called_once() + print_mock.assert_called_once_with(test_error) + + @mock.patch(f"{BASE_SCRIPT_PATH}.zipfile") + @mock.patch(f"{BASE_SCRIPT_PATH}.download_archive_file") + @mock.patch(f"{BASE_SCRIPT_PATH}.os") + @mock.patch(f"{BASE_SCRIPT_PATH}.Path") + @mock.patch(f"{BASE_SCRIPT_PATH}.print") + @mock.patch(f"{BASE_SCRIPT_PATH}.get_samm_cli_file_name") + def test_download_samm_cli( + self, + get_samm_cli_file_name_mock, + print_mock, + path_mock, + os_mock, + download_archive_file_mock, + zipfile_mock, + ): + get_samm_cli_file_name_mock.return_value = "samm_cli_file_name" + path_mock.return_value = path_mock + path_mock.resolve.return_value = path_mock + path_mock.parents = ["parent_dir"] + os_mock.path.join.side_effect = ("archive_file", "extracted_files_path") + archive_mock = mock.MagicMock(name="archive_mock") + archive_mock.namelist.return_value = ["file_1", "file_2"] + zipfile_mock.ZipFile.return_value = archive_mock + result = download_samm_cli() + + assert result is None + get_samm_cli_file_name_mock.assert_called_once() + print_mock.assert_has_calls( + [ + mock.call("Start downloading SAMM CLI samm_cli_file_name"), + mock.call("\nSAMM CLI archive file downloaded"), + mock.call("Start extracting files"), + mock.call("Done extracting files"), + mock.call("Deleting SAMM CLI archive file"), + ] + ) + path_mock.assert_called_once_with(download_module.__file__) + path_mock.resolve.assert_called_once() + os_mock.path.join.assert_has_calls( + [mock.call("parent_dir", "samm_cli_file_name"), mock.call("parent_dir", "samm-cli")] + ) + os_mock.remove.assert_called_once() + download_archive_file_mock.assert_called_once_with( + Const.BASE_PATH.substitute(version_number=Const.JAVA_CLI_VERSION, file_name="samm_cli_file_name"), + "archive_file", + ) + zipfile_mock.ZipFile.assert_called_once_with("archive_file") + archive_mock.namelist.assert_called_once() + archive_mock.extract.has_calls( + [ + mock.call("file_1", "extracted_files_path"), + mock.call("file_2", "extracted_files_path"), + ] + ) + archive_mock.close.assert_called_once() From 52a427d96de852fbb87b88b6cc4b278f8bc72e9e Mon Sep 17 00:00:00 2001 From: "Hanna Shalamitskaya (EPAM)" Date: Thu, 30 Oct 2025 15:05:55 +0500 Subject: [PATCH 2/3] Fix PR comments --- .../esmf_aspect_meta_model_python/constants.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/esmf-aspect-meta-model-python/esmf_aspect_meta_model_python/constants.py b/core/esmf-aspect-meta-model-python/esmf_aspect_meta_model_python/constants.py index b525ef8..b021688 100644 --- a/core/esmf-aspect-meta-model-python/esmf_aspect_meta_model_python/constants.py +++ b/core/esmf-aspect-meta-model-python/esmf_aspect_meta_model_python/constants.py @@ -1,4 +1,4 @@ -# Copyright (c) 2023 Robert Bosch Manufacturing Solutions GmbH +# Copyright (c) 2025 Robert Bosch Manufacturing Solutions GmbH # # See the AUTHORS file(s) distributed with this work for additional # information regarding authorship. @@ -11,4 +11,4 @@ SAMM_VERSION = "2.2.0" -JAVA_CLI_VERSION = "2.10.2" +JAVA_CLI_VERSION = "2.11.1" From 618a0d3ad2341c673f4fe09ef3705d4a93a5dfd3 Mon Sep 17 00:00:00 2001 From: "Hanna Shalamitskaya (EPAM)" Date: Mon, 3 Nov 2025 13:36:28 +0500 Subject: [PATCH 3/3] Update SAMM CLI version number --- .../scripts/download_samm_cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/esmf-aspect-meta-model-python/scripts/download_samm_cli.py b/core/esmf-aspect-meta-model-python/scripts/download_samm_cli.py index c6a3ea9..abd5c32 100644 --- a/core/esmf-aspect-meta-model-python/scripts/download_samm_cli.py +++ b/core/esmf-aspect-meta-model-python/scripts/download_samm_cli.py @@ -1,8 +1,8 @@ """Download SAMM CLI. -Windows: https://github.com/eclipse-esmf/esmf-sdk/releases/download/v2.10.2/samm-cli-2.10.2-windows-x86_64.zip - Linux: https://github.com/eclipse-esmf/esmf-sdk/releases/download/v2.10.2/samm-cli-2.10.2-linux-x86_64.tar.gz - JAR: https://github.com/eclipse-esmf/esmf-sdk/releases/download/v2.10.2/samm-cli-2.10.2.jar +Windows: https://github.com/eclipse-esmf/esmf-sdk/releases/download/v2.11.1/samm-cli-2.11.1-windows-x86_64.zip + Linux: https://github.com/eclipse-esmf/esmf-sdk/releases/download/v2.11.1/samm-cli-2.11.1-linux-x86_64.tar.gz + JAR: https://github.com/eclipse-esmf/esmf-sdk/releases/download/v2.11.1/samm-cli-2.11.1.jar """ from esmf_aspect_meta_model_python.samm_cli.download import download_samm_cli