Skip to content

Commit 3d643eb

Browse files
authored
Merge pull request #33 from bci-oss/use-samm-cli
Add SAMM cli usage
2 parents 73df42f + ceea3af commit 3d643eb

File tree

8 files changed

+406
-8
lines changed

8 files changed

+406
-8
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,4 @@ dmypy.json
6969

7070
# SAMM
7171
core/esmf-aspect-meta-model-python/esmf_aspect_meta_model_python/samm_aspect_meta_model/samm/
72+
/core/esmf-aspect-meta-model-python/samm-cli/
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# Copyright (c) 2023 Robert Bosch Manufacturing Solutions GmbH
2+
#
3+
# See the AUTHORS file(s) distributed with this work for additional
4+
# information regarding authorship.
5+
#
6+
# This Source Code Form is subject to the terms of the Mozilla Public
7+
# License, v. 2.0. If a copy of the MPL was not distributed with this
8+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
9+
#
10+
# SPDX-License-Identifier: MPL-2.0
11+
12+
import subprocess
13+
14+
from os.path import exists, join
15+
from pathlib import Path
16+
17+
from scripts.download_samm_cli import download_samm_cli
18+
19+
20+
class SammCli:
21+
"""Class to execute SAMM CLI functions.
22+
23+
If there is no downloaded SAMM CLI, the code will identify the operating system and download a corresponding
24+
SAMM CLI version.
25+
"""
26+
27+
def __init__(self):
28+
self._samm = self._get_client_path()
29+
self._validate_client()
30+
31+
@staticmethod
32+
def _get_client_path():
33+
"""Get path to the SAMM CLI executable file.."""
34+
base_path = Path(__file__).resolve()
35+
cli_path = join(base_path.parents[1], "samm-cli", "samm.exe")
36+
37+
return cli_path
38+
39+
def _validate_client(self):
40+
"""Validate SAMM CLI.
41+
42+
If there is no SAMM CLI executable file, run a script for downloading.
43+
"""
44+
if not exists(self._samm):
45+
download_samm_cli()
46+
47+
def _call_function(self, function_name, path_to_model, *args, **kwargs):
48+
"""Run a SAMM CLI function as a subprocess."""
49+
call_args = [self._samm, "aspect", path_to_model] + function_name.split()
50+
51+
if args:
52+
call_args.extend([f"-{param}" for param in args])
53+
54+
if kwargs:
55+
for key, value in kwargs.items():
56+
if len(key) == 1:
57+
arg = f"-{key}={value}"
58+
else:
59+
key = key.replace("_", "-")
60+
arg = f"--{key}={value}"
61+
62+
call_args.append(arg)
63+
64+
subprocess.run(call_args, shell=True, check=True)
65+
66+
def validate(self, path_to_model, *args, **kwargs):
67+
"""Validate Aspect Model.
68+
69+
param path_to_model: local path to the aspect model file (*.ttl)
70+
possible arguments:
71+
custom-resolver: use an external resolver for the resolution of the model elements
72+
"""
73+
self._call_function("validate", path_to_model, *args, **kwargs)
74+
75+
def to_openapi(self, path_to_model, *args, **kwargs):
76+
"""Generate OpenAPI specification for an Aspect Model.
77+
78+
param path_to_model: local path to the aspect model file (*.ttl)
79+
possible arguments:
80+
- output, o: output file path (default: stdout)
81+
- api-base-url, b: the base url for the Aspect API used in the OpenAPI specification, b="http://localhost/"
82+
- json, j: generate a JSON specification for an Aspect Model (default format is YAML)
83+
- comment, c: only in combination with --json; generates $comment OpenAPI 3.1 keyword for all
84+
samm:see attributes
85+
- parameter-file, p: the path to a file including the parameter for the Aspect API endpoints.
86+
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
87+
- semantic-version, sv: use the full semantic version from the Aspect Model as the version for the Aspect API
88+
- resource-path, r: the resource path for the Aspect API endpoints
89+
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
90+
- include-query-api, q: include the path for the Query Aspect API Endpoint in the OpenAPI specification
91+
- paging-none, pn: exclude paging information for the Aspect API Endpoint in the OpenAPI specification
92+
- paging-cursor-based, pc: in case there is more than one paging possibility, it must be cursor based paging
93+
- paging-offset-based, po: in case there is more than one paging possibility, it must be offset based paging
94+
- paging-time-based, pt: in case there is more than one paging possibility, it must be time based paging
95+
- language, l: the language from the model for which an OpenAPI specification should be generated (default: en)
96+
custom-resolver: use an external resolver for the resolution of the model elements
97+
"""
98+
self._call_function("to openapi", path_to_model, *args, **kwargs)
99+
100+
def to_schema(self, path_to_model, *args, **kwargs):
101+
"""Generate JSON schema for an Aspect Model.
102+
103+
param path_to_model: local path to the aspect model file (*.ttl)
104+
possible arguments:
105+
- output, -o: output file path (default: stdout)
106+
- language, -l: the language from the model for which a JSON schema should be generated (default: en)
107+
- custom-resolver: use an external resolver for the resolution of the model elements
108+
"""
109+
self._call_function("to schema", path_to_model, *args, **kwargs)
110+
111+
def to_json(self, path_to_model, *args, **kwargs):
112+
"""Generate example JSON payload data for an Aspect Model.
113+
114+
param path_to_model: local path to the aspect model file (*.ttl)
115+
possible arguments:
116+
- output, -o: output file path (default: stdout)
117+
- custom-resolver: use an external resolver for the resolution of the model elements
118+
"""
119+
self._call_function("to json", path_to_model, *args, **kwargs)
120+
121+
def to_html(self, path_to_model, *args, **kwargs):
122+
"""Generate HTML documentation for an Aspect Model.
123+
124+
param path_to_model: local path to the aspect model file (*.ttl)
125+
possible arguments:
126+
- output, -o: the output will be saved to the given file
127+
- css, -c: CSS file with custom styles to be included in the generated HTML documentation
128+
- language, -l: the language from the model for which the HTML should be generated (default: en)
129+
- custom-resolver: use an external resolver for the resolution of the model elements
130+
"""
131+
self._call_function("to html", path_to_model, *args, **kwargs)
132+
133+
def to_png(self, path_to_model, *args, **kwargs):
134+
"""Generate PNG diagram for Aspect Model.
135+
136+
param path_to_model: local path to the aspect model file (*.ttl)
137+
possible arguments:
138+
- output, -o: output file path (default: stdout);
139+
as PNG is a binary format, it is strongly recommended to output the result to a file
140+
by using the -o option or the console redirection operator '>'
141+
- language, -l: the language from the model for which the diagram should be generated (default: en)
142+
- custom-resolver: use an external resolver for the resolution of the model elements
143+
"""
144+
self._call_function("to png", path_to_model, *args, **kwargs)
145+
146+
def to_svg(self, path_to_model, *args, **kwargs):
147+
"""Generate SVG diagram for Aspect Model.
148+
149+
param path_to_model: local path to the aspect model file (*.ttl)
150+
possible arguments:
151+
- output, -o: the output will be saved to the given file
152+
- language, -l: the language from the model for which the diagram should be generated (default: en)
153+
- custom-resolver: use an external resolver for the resolution of the model elements
154+
"""
155+
self._call_function("to svg", path_to_model, *args, **kwargs)

core/esmf-aspect-meta-model-python/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ cache_dir = ".pytest_cache"
4949
[tool.poetry.scripts]
5050
download-samm-release = "esmf_aspect_meta_model_python.samm_aspect_meta_model.download_samm_release:main"
5151
download-samm-branch = "esmf_aspect_meta_model_python.samm_aspect_meta_model.download_samm_branch:main"
52+
download-samm-cli = "scripts.download_samm_cli:download_samm_cli"
5253

5354
[build-system]
5455
requires = ["poetry>=0.12"]

core/esmf-aspect-meta-model-python/scripts/__init__.py

Whitespace-only changes.
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
"""Download SAMM CLI.
2+
3+
Windows: https://github.com/eclipse-esmf/esmf-sdk/releases/download/v2.6.1/samm-cli-2.6.1-windows-x86_64.zip
4+
Linux: https://github.com/eclipse-esmf/esmf-sdk/releases/download/v2.6.1/samm-cli-2.6.1-linux-x86_64.tar.gz
5+
JAR: https://github.com/eclipse-esmf/esmf-sdk/releases/download/v2.6.1/samm-cli-2.6.1.jar
6+
"""
7+
8+
import os
9+
from pathlib import Path
10+
import platform
11+
import requests
12+
import sys
13+
import zipfile
14+
15+
from string import Template
16+
17+
BASE_PATH = Template("https://github.com/eclipse-esmf/esmf-sdk/releases/download/v$version_number/$file_name")
18+
LINUX_FILE_NAME = Template("samm-cli-$version_number-linux-x86_64.tar.gz")
19+
SAMM_CLI_VERSION = "2.6.1"
20+
WIN_FILE_NAME = Template("samm-cli-$version_number-windows-x86_64.zip")
21+
22+
23+
def get_samm_cli_file_name():
24+
"""Get a SAMM CLI file name for the current platform."""
25+
26+
if platform.system() == "Windows":
27+
file_name = WIN_FILE_NAME.substitute(version_number=SAMM_CLI_VERSION)
28+
elif platform.system() == "Linux":
29+
file_name = LINUX_FILE_NAME.substitute(version_number=SAMM_CLI_VERSION)
30+
else:
31+
raise NotImplementedError(
32+
f"Please download a SAMM CLI manually for your operation system from '{BASE_PATH}'"
33+
)
34+
35+
return file_name
36+
37+
38+
def download_archive_file(url, archive_file):
39+
"""Download an archive file."""
40+
with open(archive_file, "wb") as f:
41+
print("Downloading %s" % archive_file)
42+
response = requests.get(url, allow_redirects=True, stream=True)
43+
content_len = response.headers.get('content-length')
44+
45+
if content_len is None:
46+
f.write(response.content)
47+
else:
48+
total_len = int(content_len)
49+
data_len = 0
50+
chunk = 4096
51+
progress_bar_len = 50
52+
53+
for content_data in response.iter_content(chunk_size=chunk):
54+
data_len += len(content_data)
55+
56+
f.write(content_data)
57+
58+
curr_progress = int(50 * data_len / total_len)
59+
sys.stdout.write(f"\r[{'*' * curr_progress}{' ' * (progress_bar_len - curr_progress)}]")
60+
sys.stdout.flush()
61+
62+
63+
def download_samm_cli():
64+
try:
65+
samm_cli_file_name = get_samm_cli_file_name()
66+
except NotImplementedError as error:
67+
print(error)
68+
else:
69+
print(f"Start downloading SAMM CLI {samm_cli_file_name}")
70+
url = BASE_PATH.substitute(version_number=SAMM_CLI_VERSION, file_name=samm_cli_file_name)
71+
dir_path = Path(__file__).resolve().parents[1]
72+
archive_file = os.path.join(dir_path, samm_cli_file_name)
73+
74+
download_archive_file(url, archive_file)
75+
print("\nSAMM CLI archive file downloaded")
76+
77+
print("Start extracting files")
78+
archive = zipfile.ZipFile(archive_file)
79+
for file in archive.namelist():
80+
archive.extract(file, "samm-cli")
81+
archive.close()
82+
print("Done extracting files.")
83+
84+
print("Deleting SAMM CLI archive file.")
85+
os.remove(archive_file)
86+
87+
88+
if __name__ == "__main__":
89+
download_samm_cli()

core/esmf-aspect-meta-model-python/tests/unit/impl/test_aspect.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def test_init(self, super_mock, set_parent_element_on_child_elements_mock):
3232
assert result._is_collection_aspect == self.is_collection_aspect
3333
set_parent_element_on_child_elements_mock.assert_called_once()
3434

35-
@mock.patch("esmf_aspect_meta_model_python.impl.default_aspect.super")
35+
@mock.patch("esmf_aspect_meta_model_python.impl.default_aspect.BaseImpl.__init__")
3636
def test_set_parent_element_on_child_elements(self, _):
3737
aspect = DefaultAspect(
3838
self.meta_model_mock,
@@ -46,7 +46,7 @@ def test_set_parent_element_on_child_elements(self, _):
4646
self.operation_mock.append_parent_element.assert_called_once_with(aspect)
4747
self.event_mock.append_parent_element.assert_called_once_with(aspect)
4848

49-
@mock.patch("esmf_aspect_meta_model_python.impl.default_aspect.super")
49+
@mock.patch("esmf_aspect_meta_model_python.impl.default_aspect.BaseImpl.__init__")
5050
def test_operations(self, _):
5151
aspect = DefaultAspect(
5252
self.meta_model_mock,
@@ -59,7 +59,7 @@ def test_operations(self, _):
5959

6060
assert result == [self.operation_mock]
6161

62-
@mock.patch("esmf_aspect_meta_model_python.impl.default_aspect.super")
62+
@mock.patch("esmf_aspect_meta_model_python.impl.default_aspect.BaseImpl.__init__")
6363
def test_properties(self, _):
6464
aspect = DefaultAspect(
6565
self.meta_model_mock,
@@ -72,7 +72,7 @@ def test_properties(self, _):
7272

7373
assert result == [self.property_mock]
7474

75-
@mock.patch("esmf_aspect_meta_model_python.impl.default_aspect.super")
75+
@mock.patch("esmf_aspect_meta_model_python.impl.default_aspect.BaseImpl.__init__")
7676
def test_events(self, _):
7777
aspect = DefaultAspect(
7878
self.meta_model_mock,
@@ -85,7 +85,7 @@ def test_events(self, _):
8585

8686
assert result == [self.event_mock]
8787

88-
@mock.patch("esmf_aspect_meta_model_python.impl.default_aspect.super")
88+
@mock.patch("esmf_aspect_meta_model_python.impl.default_aspect.BaseImpl.__init__")
8989
def test_is_collection_aspect(self, _):
9090
aspect = DefaultAspect(
9191
self.meta_model_mock,

core/esmf-aspect-meta-model-python/tests/unit/impl/test_operation.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,21 @@ def test_init(self, super_mock, set_parent_element_on_child_elements_mock):
2424
assert result._output_property == self.output_property_mock
2525
set_parent_element_on_child_elements_mock.assert_called_once()
2626

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

3131
self.input_property_mock.append_parent_element.assert_called_once_with(operation)
3232
self.output_property_mock.append_parent_element.assert_called_once_with(operation)
3333

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

3939
assert result == [self.input_property_mock]
4040

41-
@mock.patch("esmf_aspect_meta_model_python.impl.default_operation.super")
41+
@mock.patch("esmf_aspect_meta_model_python.impl.default_operation.BaseImpl.__init__")
4242
def test_output_property(self, _):
4343
operation = DefaultOperation(self.meta_model_mock, [self.input_property_mock], self.output_property_mock)
4444
result = operation.output_property

0 commit comments

Comments
 (0)