Skip to content

Commit e0d251b

Browse files
authored
Merge pull request #36 from bci-oss/add_get_units_function
Added SAMM meta model class for units
2 parents 94ee7b7 + 90713dc commit e0d251b

File tree

3 files changed

+269
-0
lines changed

3 files changed

+269
-0
lines changed

core/esmf-aspect-meta-model-python/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,30 @@ This script can be executed with
6565
poetry run download-samm-branch
6666
```
6767
to download and start working with the Aspect Model Loader.
68+
69+
## Semantic Aspect Meta Model
70+
71+
To be able to use SAMM meta model classes you need to download the corresponding files.
72+
Details are described in [Set up SAMM Aspect Meta Model for development](#set-up-samm-aspect-meta-model-for-development).
73+
74+
This module contains classes for working with Aspect data.
75+
76+
SAMM meta model contains:
77+
- SammUnitsGraph: provide a functionality for working with units([units.ttl](./esmf_aspect_meta_model_python/samm_aspect_meta_model/samm/unit/2.1.0/units.ttl)) data
78+
79+
### SammUnitsGraph
80+
81+
The class contains functions for accessing units of measurement.
82+
```python
83+
from esmf_aspect_meta_model_python.samm_meta_model import SammUnitsGraph
84+
85+
units_graph = SammUnitsGraph()
86+
unit_data = units_graph.get_info("unit:volt")
87+
# {'preferredName': rdflib.term.Literal('volt', lang='en'), 'commonCode': rdflib.term.Literal('VLT'), ... }
88+
89+
units_graph.print_description(unit_data)
90+
# preferredName: volt
91+
# commonCode: VLT
92+
# ...
93+
# symbol: V
94+
```
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Copyright (c) 2024 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+
from os.path import exists, join
13+
from pathlib import Path
14+
from string import Template
15+
from typing import Dict, Union
16+
17+
import rdflib
18+
19+
20+
class SammUnitsGraph:
21+
"""Model units graph."""
22+
23+
SAMM_VERSION = "2.1.0"
24+
UNIT_FILE_PATH = f"samm_aspect_meta_model/samm/unit/{SAMM_VERSION}/units.ttl"
25+
QUERY_TEMPLATE = Template("SELECT ?key ?value WHERE {$unit ?key ?value .}")
26+
27+
def __init__(self):
28+
self.unit_file_path = self._get_file_path()
29+
self._validate_path()
30+
self._graph = self._get_units()
31+
32+
@property
33+
def graph(self) -> rdflib.Graph:
34+
"""Getter for the units graph."""
35+
return self._graph
36+
37+
def _get_file_path(self) -> str:
38+
"""Get a path to the units.ttl file"""
39+
base_path = Path(__file__).resolve()
40+
file_path = join(base_path.parents[0], self.UNIT_FILE_PATH)
41+
42+
return file_path
43+
44+
def _validate_path(self):
45+
"""Checking the path to the units.ttl file."""
46+
if not exists(self.unit_file_path):
47+
raise ValueError(f"There is no such file {self.unit_file_path}")
48+
49+
def _get_units(self) -> rdflib.Graph:
50+
"""Parse a units to graph."""
51+
graph = rdflib.Graph()
52+
graph.parse(self.unit_file_path, format="turtle")
53+
54+
return graph
55+
56+
def _get_nested_data(self, value: str) -> tuple[str, Union[str, Dict]]:
57+
"""Get data of the nested node."""
58+
node_type = value.split("#")[1]
59+
node_value: Union[str, Dict] = value
60+
61+
if node_type != "Unit":
62+
node_value = self.get_info(f"unit:{node_type}")
63+
64+
return node_type, node_value
65+
66+
def get_info(self, unit: str) -> Dict:
67+
"""Get a description of the unit."""
68+
unit_data: Dict = {}
69+
query = self.QUERY_TEMPLATE.substitute(unit=unit)
70+
res = self._graph.query(query)
71+
72+
for row in res:
73+
key = row.key.split("#")[1]
74+
value = row.value
75+
if isinstance(value, rdflib.term.URIRef):
76+
sub_key, value = self._get_nested_data(value)
77+
if key != "type":
78+
unit_data.setdefault(key, []).append({sub_key: value})
79+
else:
80+
unit_data[key] = value
81+
82+
return unit_data
83+
84+
def print_description(self, unit_data: Dict, tabs: int = 0):
85+
"""Pretty print a unit data."""
86+
for key, value in unit_data.items():
87+
if isinstance(value, dict):
88+
print("\t" * tabs + f"{key}:")
89+
self.print_description(value, tabs + 1)
90+
elif isinstance(value, list):
91+
print("\t" * tabs + f"{key}:")
92+
for node in value:
93+
for key, sub_value in node.items():
94+
print("\t" * (tabs + 1) + f"{key}:")
95+
self.print_description(sub_value, tabs + 2)
96+
else:
97+
print("\t" * tabs + f"{key}: {value}")
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
"""SAMM Meta Model functions test suite."""
2+
3+
from unittest import mock
4+
5+
import pytest
6+
7+
from rdflib.term import URIRef
8+
9+
from esmf_aspect_meta_model_python.samm_meta_model import SammUnitsGraph
10+
11+
12+
class TestSammCli:
13+
"""SAMM Units Graph tests."""
14+
15+
@mock.patch("esmf_aspect_meta_model_python.samm_meta_model.SammUnitsGraph._get_units")
16+
@mock.patch("esmf_aspect_meta_model_python.samm_meta_model.SammUnitsGraph._validate_path")
17+
@mock.patch("esmf_aspect_meta_model_python.samm_meta_model.SammUnitsGraph._get_file_path")
18+
def test_init(self, get_file_path_mock, validate_path_mock, get_units_mock):
19+
get_file_path_mock.return_value = "unit_file_path"
20+
get_units_mock.return_value = "graph"
21+
result = SammUnitsGraph()
22+
23+
assert result.graph == "graph"
24+
get_file_path_mock.assert_called_once()
25+
validate_path_mock.assert_called_once()
26+
get_units_mock.assert_called_once()
27+
28+
@mock.patch("esmf_aspect_meta_model_python.samm_meta_model.SammUnitsGraph._get_units")
29+
@mock.patch("esmf_aspect_meta_model_python.samm_meta_model.SammUnitsGraph._validate_path")
30+
@mock.patch("esmf_aspect_meta_model_python.samm_meta_model.join")
31+
@mock.patch("esmf_aspect_meta_model_python.samm_meta_model.Path")
32+
def test_get_file_path(self, path_mock, join_mock, _, get_units_mock):
33+
base_path_mock = mock.MagicMock()
34+
base_path_mock.parents = ["parent", "child"]
35+
path_mock.return_value = path_mock
36+
path_mock.resolve.return_value = base_path_mock
37+
join_mock.return_value = "file_path"
38+
get_units_mock.return_value = "graph"
39+
result = SammUnitsGraph()
40+
41+
assert result.unit_file_path == "file_path"
42+
path_mock.assert_called_once()
43+
path_mock.resolve.assert_called_once()
44+
join_mock.assert_called_once_with("parent", SammUnitsGraph.UNIT_FILE_PATH)
45+
46+
@mock.patch("esmf_aspect_meta_model_python.samm_meta_model.SammUnitsGraph._get_units")
47+
@mock.patch("esmf_aspect_meta_model_python.samm_meta_model.exists")
48+
@mock.patch("esmf_aspect_meta_model_python.samm_meta_model.SammUnitsGraph._get_file_path")
49+
def test_validate_path(self, get_file_path_mock, exists_mock, get_units_mock):
50+
get_file_path_mock.return_value = "unit_file_path"
51+
exists_mock.return_value = True
52+
get_units_mock.return_value = "graph"
53+
result = SammUnitsGraph()
54+
55+
assert result is not None
56+
exists_mock.assert_called_once_with("unit_file_path")
57+
58+
@mock.patch("esmf_aspect_meta_model_python.samm_meta_model.exists")
59+
@mock.patch("esmf_aspect_meta_model_python.samm_meta_model.SammUnitsGraph._get_file_path")
60+
def test_validate_path_with_exception(self, get_file_path_mock, exists_mock):
61+
get_file_path_mock.return_value = "unit_file_path"
62+
exists_mock.return_value = False
63+
with pytest.raises(ValueError) as error:
64+
SammUnitsGraph()
65+
66+
assert str(error.value) == "There is no such file unit_file_path"
67+
exists_mock.assert_called_once_with("unit_file_path")
68+
69+
@mock.patch("esmf_aspect_meta_model_python.samm_meta_model.rdflib.Graph")
70+
@mock.patch("esmf_aspect_meta_model_python.samm_meta_model.SammUnitsGraph._validate_path")
71+
@mock.patch("esmf_aspect_meta_model_python.samm_meta_model.SammUnitsGraph._get_file_path")
72+
def test_get_units(self, get_file_path_mock, validate_path_mock, rdflib_graph_mock):
73+
get_file_path_mock.return_value = "unit_file_path"
74+
graph_mock = mock.MagicMock()
75+
rdflib_graph_mock.return_value = graph_mock
76+
result = SammUnitsGraph()
77+
78+
assert result._graph == graph_mock
79+
rdflib_graph_mock.assert_called_once()
80+
graph_mock.parse.assert_called_once_with("unit_file_path", format="turtle")
81+
82+
@mock.patch("esmf_aspect_meta_model_python.samm_meta_model.SammUnitsGraph._get_units")
83+
@mock.patch("esmf_aspect_meta_model_python.samm_meta_model.SammUnitsGraph._validate_path")
84+
@mock.patch("esmf_aspect_meta_model_python.samm_meta_model.SammUnitsGraph._get_file_path")
85+
def test_get_nested_data_unit(self, get_file_path_mock, _, get_units_mock):
86+
get_file_path_mock.return_value = "unit_file_path"
87+
get_units_mock.return_value = "graph"
88+
units_graph = SammUnitsGraph()
89+
result = units_graph._get_nested_data("prefix#Unit")
90+
91+
assert result == ("Unit", "prefix#Unit")
92+
93+
@mock.patch("esmf_aspect_meta_model_python.samm_meta_model.SammUnitsGraph.get_info")
94+
@mock.patch("esmf_aspect_meta_model_python.samm_meta_model.SammUnitsGraph._get_units")
95+
@mock.patch("esmf_aspect_meta_model_python.samm_meta_model.SammUnitsGraph._validate_path")
96+
@mock.patch("esmf_aspect_meta_model_python.samm_meta_model.SammUnitsGraph._get_file_path")
97+
def test_get_nested_data_not_unit(self, get_file_path_mock, _, get_units_mock, get_info_mock):
98+
get_file_path_mock.return_value = "unit_file_path"
99+
get_units_mock.return_value = "graph"
100+
get_info_mock.return_value = "nested_value"
101+
units_graph = SammUnitsGraph()
102+
result = units_graph._get_nested_data("prefix#unitType")
103+
104+
assert result == ("unitType", "nested_value")
105+
get_info_mock.assert_called_once_with("unit:unitType")
106+
107+
@mock.patch("esmf_aspect_meta_model_python.samm_meta_model.SammUnitsGraph._get_nested_data")
108+
@mock.patch("esmf_aspect_meta_model_python.samm_meta_model.isinstance")
109+
@mock.patch("esmf_aspect_meta_model_python.samm_meta_model.SammUnitsGraph._get_units")
110+
@mock.patch("esmf_aspect_meta_model_python.samm_meta_model.SammUnitsGraph._validate_path")
111+
@mock.patch("esmf_aspect_meta_model_python.samm_meta_model.SammUnitsGraph._get_file_path")
112+
def test_get_info(self, get_file_path_mock, _, get_units_mock, isinstance_mock, get_nested_data_mock):
113+
get_file_path_mock.return_value = "unit_file_path"
114+
isinstance_mock.side_effect = (False, URIRef, URIRef)
115+
get_nested_data_mock.side_effect = [("type_key", "type_description"), ("sub_unit", "sub_unit_description")]
116+
row_1_mock = mock.MagicMock()
117+
row_1_mock.key = "prefix#unitType"
118+
row_1_mock.value = "unit_1"
119+
row_2_mock = mock.MagicMock()
120+
row_2_mock.key = "prefix#type"
121+
row_2_mock.value = "unit_2"
122+
row_3_mock = mock.MagicMock()
123+
row_3_mock.key = "prefix#otherUnit"
124+
row_3_mock.value = "unit_3"
125+
graph_mock = mock.MagicMock()
126+
graph_mock.query.return_value = [row_1_mock, row_2_mock, row_3_mock]
127+
get_units_mock.return_value = graph_mock
128+
units_graph = SammUnitsGraph()
129+
result = units_graph.get_info("unit:unit_name")
130+
131+
assert "unitType" in result
132+
assert result["unitType"] == "unit_1"
133+
assert "otherUnit" in result
134+
assert len(result["otherUnit"]) == 1
135+
assert "sub_unit" in result["otherUnit"][0]
136+
assert result["otherUnit"][0]["sub_unit"] == "sub_unit_description"
137+
graph_mock.query.assert_called_once_with("SELECT ?key ?value WHERE {unit:unit_name ?key ?value .}")
138+
isinstance_mock.assert_has_calls(
139+
[
140+
mock.call("unit_1", URIRef),
141+
mock.call("unit_2", URIRef),
142+
mock.call("unit_3", URIRef),
143+
]
144+
)
145+
get_nested_data_mock.assert_has_calls([mock.call("unit_2"), mock.call("unit_3")])

0 commit comments

Comments
 (0)