Skip to content

Commit cc5c704

Browse files
Add SAMM CLI usage
1 parent 73df42f commit cc5c704

File tree

8 files changed

+399
-8
lines changed

8 files changed

+399
-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 eecute SAMM CLI functions.
22+
23+
If there is no downloaded SAMM CLI, code will identify operation system and download a corresponding client.
24+
"""
25+
26+
def __init__(self):
27+
self._samm = self._get_client_path()
28+
self._validate_client()
29+
30+
@staticmethod
31+
def _get_client_path():
32+
"""Get path to the SAMM Cli executable file.."""
33+
base_path = Path(__file__).resolve()
34+
cli_path = join(base_path.parents[1], "samm-cli", "samm.exe")
35+
36+
return cli_path
37+
38+
def _validate_client(self):
39+
"""Validate SAMM Cli.
40+
41+
If there is no SAMM Cli executable file, run a script for downloading.
42+
"""
43+
if not exists(self._samm):
44+
# download SAMM CLI
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://mysite.de"
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: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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+
BASE_PATH = "https://github.com/eclipse-esmf/esmf-sdk/releases/download/v2.6.1/"
16+
17+
18+
def get_samm_cli_file_name():
19+
"""Get a SAMM CLI file name for the current platform."""
20+
21+
if platform.system() == "Windows":
22+
file_name = "samm-cli-2.6.1-windows-x86_64.zip"
23+
elif platform.system() == "Linux":
24+
file_name = "samm-cli-2.6.1-linux-x86_64.tar.gz"
25+
else:
26+
raise NotImplementedError(f"Please download a SAMM CLI manually for your operation system from '{BASE_PATH}'")
27+
28+
return file_name
29+
30+
31+
def download_archive_file(url, archive_file):
32+
"""Download an archive file."""
33+
with open(archive_file, "wb") as f:
34+
print("Downloading %s" % archive_file)
35+
response = requests.get(url, allow_redirects=True, stream=True)
36+
content_len = response.headers.get('content-length')
37+
38+
if content_len is None:
39+
f.write(response.content)
40+
else:
41+
total_len = int(content_len)
42+
data_len = 0
43+
chunk = 4096
44+
progress_bar_len = 50
45+
46+
for content_data in response.iter_content(chunk_size=chunk):
47+
data_len += len(content_data)
48+
49+
f.write(content_data)
50+
51+
curr_progress = int(50 * data_len / total_len)
52+
sys.stdout.write(f"\r[{'*' * curr_progress}{' ' * (progress_bar_len - curr_progress)}]")
53+
sys.stdout.flush()
54+
55+
56+
def download_samm_cli():
57+
try:
58+
samm_cli_file_name = get_samm_cli_file_name()
59+
except NotImplementedError as error:
60+
print(error)
61+
else:
62+
print(f"Start downloading SAMM CLI {samm_cli_file_name}")
63+
url = BASE_PATH + samm_cli_file_name
64+
dir_path = Path(__file__).resolve().parent
65+
archive_file = os.path.join(dir_path, f".\\{samm_cli_file_name}")
66+
67+
download_archive_file(url, archive_file)
68+
print("SAMM CLI archive file downloaded")
69+
70+
print("Start extracting files")
71+
archive = zipfile.ZipFile(archive_file)
72+
for file in archive.namelist():
73+
archive.extract(file, ".\\samm-cli")
74+
archive.close()
75+
print("Done extracting files.")
76+
77+
print("Deleting SAMM CLI archive file.")
78+
os.remove(archive_file)
79+
80+
81+
if __name__ == "__main__":
82+
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)