Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SAMM cli usage #33

Merged
merged 2 commits into from
Apr 22, 2024
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,4 @@ 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/
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# 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

import subprocess

from os.path import exists, join
from pathlib import Path

from scripts.download_samm_cli import download_samm_cli


class SammCli:
"""Class to execute SAMM CLI functions.

If there is no downloaded SAMM CLI, the code will identify the operating system and download a corresponding
SAMM CLI version.
"""

def __init__(self):
self._samm = self._get_client_path()
self._validate_client()

@staticmethod
def _get_client_path():
"""Get path to the SAMM CLI executable file.."""
base_path = Path(__file__).resolve()
cli_path = join(base_path.parents[1], "samm-cli", "samm.exe")

return cli_path

def _validate_client(self):
"""Validate SAMM CLI.

If there is no SAMM CLI executable file, run a script for downloading.
"""
if not exists(self._samm):
download_samm_cli()

def _call_function(self, function_name, path_to_model, *args, **kwargs):
"""Run a SAMM CLI function as a subprocess."""
call_args = [self._samm, "aspect", path_to_model] + function_name.split()

if args:
call_args.extend([f"-{param}" for param in args])

if kwargs:
for key, value in kwargs.items():
if len(key) == 1:
arg = f"-{key}={value}"
else:
key = key.replace("_", "-")
arg = f"--{key}={value}"

call_args.append(arg)

subprocess.run(call_args, shell=True, check=True)

def validate(self, path_to_model, *args, **kwargs):
"""Validate Aspect Model.

param path_to_model: local path to the aspect model file (*.ttl)
possible arguments:
custom-resolver: use an external resolver for the resolution of the model elements
"""
self._call_function("validate", path_to_model, *args, **kwargs)

def to_openapi(self, path_to_model, *args, **kwargs):
"""Generate OpenAPI specification for an Aspect Model.

param path_to_model: local path to the aspect model file (*.ttl)
possible arguments:
- output, o: output file path (default: stdout)
- api-base-url, b: the base url for the Aspect API used in the OpenAPI specification, b="http://localhost/"
- json, j: generate a JSON specification for an Aspect Model (default format is YAML)
- comment, c: only in combination with --json; generates $comment OpenAPI 3.1 keyword for all
samm:see attributes
- parameter-file, p: the path to a file including the parameter for the Aspect API endpoints.
For detailed description, please have a look at a SAMM CLI documentation (https://eclipse-esmf.github.io/esmf-developer-guide/tooling-guide/samm-cli.html#using-the-cli-to-create-a-json-openapi-specification) # noqa: E501
- semantic-version, sv: use the full semantic version from the Aspect Model as the version for the Aspect API
- resource-path, r: the resource path for the Aspect API endpoints
For detailed description, please have a look at a SAMM CLI documentation (https://eclipse-esmf.github.io/esmf-developer-guide/tooling-guide/samm-cli.html#using-the-cli-to-create-a-json-openapi-specification) # noqa: E501
- include-query-api, q: include the path for the Query Aspect API Endpoint in the OpenAPI specification
- paging-none, pn: exclude paging information for the Aspect API Endpoint in the OpenAPI specification
- paging-cursor-based, pc: in case there is more than one paging possibility, it must be cursor based paging
- paging-offset-based, po: in case there is more than one paging possibility, it must be offset based paging
- paging-time-based, pt: in case there is more than one paging possibility, it must be time based paging
- language, l: the language from the model for which an OpenAPI specification should be generated (default: en)
custom-resolver: use an external resolver for the resolution of the model elements
"""
self._call_function("to openapi", path_to_model, *args, **kwargs)

def to_schema(self, path_to_model, *args, **kwargs):
"""Generate JSON schema for an Aspect Model.

param path_to_model: local path to the aspect model file (*.ttl)
possible arguments:
- output, -o: output file path (default: stdout)
- language, -l: the language from the model for which a JSON schema should be generated (default: en)
- custom-resolver: use an external resolver for the resolution of the model elements
"""
self._call_function("to schema", path_to_model, *args, **kwargs)

def to_json(self, path_to_model, *args, **kwargs):
"""Generate example JSON payload data for an Aspect Model.

param path_to_model: local path to the aspect model file (*.ttl)
possible arguments:
- output, -o: output file path (default: stdout)
- custom-resolver: use an external resolver for the resolution of the model elements
"""
self._call_function("to json", path_to_model, *args, **kwargs)

def to_html(self, path_to_model, *args, **kwargs):
"""Generate HTML documentation for an Aspect Model.

param path_to_model: local path to the aspect model file (*.ttl)
possible arguments:
- output, -o: the output will be saved to the given file
- css, -c: CSS file with custom styles to be included in the generated HTML documentation
- language, -l: the language from the model for which the HTML should be generated (default: en)
- custom-resolver: use an external resolver for the resolution of the model elements
"""
self._call_function("to html", path_to_model, *args, **kwargs)

def to_png(self, path_to_model, *args, **kwargs):
"""Generate PNG diagram for Aspect Model.

param path_to_model: local path to the aspect model file (*.ttl)
possible arguments:
- output, -o: output file path (default: stdout);
as PNG is a binary format, it is strongly recommended to output the result to a file
by using the -o option or the console redirection operator '>'
- language, -l: the language from the model for which the diagram should be generated (default: en)
- custom-resolver: use an external resolver for the resolution of the model elements
"""
self._call_function("to png", path_to_model, *args, **kwargs)

def to_svg(self, path_to_model, *args, **kwargs):
"""Generate SVG diagram for Aspect Model.

param path_to_model: local path to the aspect model file (*.ttl)
possible arguments:
- output, -o: the output will be saved to the given file
- language, -l: the language from the model for which the diagram should be generated (default: en)
- custom-resolver: use an external resolver for the resolution of the model elements
"""
self._call_function("to svg", path_to_model, *args, **kwargs)
1 change: 1 addition & 0 deletions core/esmf-aspect-meta-model-python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ cache_dir = ".pytest_cache"
[tool.poetry.scripts]
download-samm-release = "esmf_aspect_meta_model_python.samm_aspect_meta_model.download_samm_release:main"
download-samm-branch = "esmf_aspect_meta_model_python.samm_aspect_meta_model.download_samm_branch:main"
download-samm-cli = "scripts.download_samm_cli:download_samm_cli"

[build-system]
requires = ["poetry>=0.12"]
Expand Down
Empty file.
89 changes: 89 additions & 0 deletions core/esmf-aspect-meta-model-python/scripts/download_samm_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""Download SAMM CLI.

Windows: https://github.com/eclipse-esmf/esmf-sdk/releases/download/v2.6.1/samm-cli-2.6.1-windows-x86_64.zip
Linux: https://github.com/eclipse-esmf/esmf-sdk/releases/download/v2.6.1/samm-cli-2.6.1-linux-x86_64.tar.gz
JAR: https://github.com/eclipse-esmf/esmf-sdk/releases/download/v2.6.1/samm-cli-2.6.1.jar
"""

import os
from pathlib import Path
import platform
import requests
import sys
import zipfile

from string import Template

BASE_PATH = Template("https://github.com/eclipse-esmf/esmf-sdk/releases/download/v$version_number/$file_name")
LINUX_FILE_NAME = Template("samm-cli-$version_number-linux-x86_64.tar.gz")
SAMM_CLI_VERSION = "2.6.1"
WIN_FILE_NAME = Template("samm-cli-$version_number-windows-x86_64.zip")


def get_samm_cli_file_name():
"""Get a SAMM CLI file name for the current platform."""

if platform.system() == "Windows":
file_name = WIN_FILE_NAME.substitute(version_number=SAMM_CLI_VERSION)
elif platform.system() == "Linux":
file_name = LINUX_FILE_NAME.substitute(version_number=SAMM_CLI_VERSION)
else:
raise NotImplementedError(
f"Please download a SAMM CLI manually for your operation system from '{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 = BASE_PATH.substitute(version_number=SAMM_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)


if __name__ == "__main__":
download_samm_cli()
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def test_init(self, super_mock, set_parent_element_on_child_elements_mock):
assert result._is_collection_aspect == self.is_collection_aspect
set_parent_element_on_child_elements_mock.assert_called_once()

@mock.patch("esmf_aspect_meta_model_python.impl.default_aspect.super")
@mock.patch("esmf_aspect_meta_model_python.impl.default_aspect.BaseImpl.__init__")
def test_set_parent_element_on_child_elements(self, _):
aspect = DefaultAspect(
self.meta_model_mock,
Expand All @@ -46,7 +46,7 @@ def test_set_parent_element_on_child_elements(self, _):
self.operation_mock.append_parent_element.assert_called_once_with(aspect)
self.event_mock.append_parent_element.assert_called_once_with(aspect)

@mock.patch("esmf_aspect_meta_model_python.impl.default_aspect.super")
@mock.patch("esmf_aspect_meta_model_python.impl.default_aspect.BaseImpl.__init__")
def test_operations(self, _):
aspect = DefaultAspect(
self.meta_model_mock,
Expand All @@ -59,7 +59,7 @@ def test_operations(self, _):

assert result == [self.operation_mock]

@mock.patch("esmf_aspect_meta_model_python.impl.default_aspect.super")
@mock.patch("esmf_aspect_meta_model_python.impl.default_aspect.BaseImpl.__init__")
def test_properties(self, _):
aspect = DefaultAspect(
self.meta_model_mock,
Expand All @@ -72,7 +72,7 @@ def test_properties(self, _):

assert result == [self.property_mock]

@mock.patch("esmf_aspect_meta_model_python.impl.default_aspect.super")
@mock.patch("esmf_aspect_meta_model_python.impl.default_aspect.BaseImpl.__init__")
def test_events(self, _):
aspect = DefaultAspect(
self.meta_model_mock,
Expand All @@ -85,7 +85,7 @@ def test_events(self, _):

assert result == [self.event_mock]

@mock.patch("esmf_aspect_meta_model_python.impl.default_aspect.super")
@mock.patch("esmf_aspect_meta_model_python.impl.default_aspect.BaseImpl.__init__")
def test_is_collection_aspect(self, _):
aspect = DefaultAspect(
self.meta_model_mock,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,21 @@ def test_init(self, super_mock, set_parent_element_on_child_elements_mock):
assert result._output_property == self.output_property_mock
set_parent_element_on_child_elements_mock.assert_called_once()

@mock.patch("esmf_aspect_meta_model_python.impl.default_operation.super")
@mock.patch("esmf_aspect_meta_model_python.impl.default_operation.BaseImpl.__init__")
def test_set_parent_element_on_child_elements(self, _):
operation = DefaultOperation(self.meta_model_mock, [self.input_property_mock], self.output_property_mock)

self.input_property_mock.append_parent_element.assert_called_once_with(operation)
self.output_property_mock.append_parent_element.assert_called_once_with(operation)

@mock.patch("esmf_aspect_meta_model_python.impl.default_operation.super")
@mock.patch("esmf_aspect_meta_model_python.impl.default_operation.BaseImpl.__init__")
def test_input_properties(self, _):
operation = DefaultOperation(self.meta_model_mock, [self.input_property_mock], self.output_property_mock)
result = operation.input_properties

assert result == [self.input_property_mock]

@mock.patch("esmf_aspect_meta_model_python.impl.default_operation.super")
@mock.patch("esmf_aspect_meta_model_python.impl.default_operation.BaseImpl.__init__")
def test_output_property(self, _):
operation = DefaultOperation(self.meta_model_mock, [self.input_property_mock], self.output_property_mock)
result = operation.output_property
Expand Down
Loading
Loading