aas-standard-parser 0.1.5__py3-none-any.whl → 0.1.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aas_standard_parser/__init__.py +3 -4
- aas_standard_parser/aid_parser.py +94 -37
- aas_standard_parser/aimc_parser.py +138 -38
- aas_standard_parser/submodel_parser.py +76 -0
- {aas_standard_parser-0.1.5.dist-info → aas_standard_parser-0.1.7.dist-info}/METADATA +1 -1
- aas_standard_parser-0.1.7.dist-info/RECORD +11 -0
- aas_standard_parser-0.1.5.dist-info/RECORD +0 -10
- {aas_standard_parser-0.1.5.dist-info → aas_standard_parser-0.1.7.dist-info}/WHEEL +0 -0
- {aas_standard_parser-0.1.5.dist-info → aas_standard_parser-0.1.7.dist-info}/licenses/LICENSE +0 -0
- {aas_standard_parser-0.1.5.dist-info → aas_standard_parser-0.1.7.dist-info}/top_level.txt +0 -0
aas_standard_parser/__init__.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
from datetime import datetime
|
|
2
1
|
import importlib.metadata
|
|
2
|
+
from datetime import datetime
|
|
3
3
|
|
|
4
4
|
# TODO: introduce MIT license
|
|
5
5
|
__copyright__ = f"Copyright (C) {datetime.now().year} :em engineering methods AG. All rights reserved."
|
|
@@ -9,12 +9,11 @@ try:
|
|
|
9
9
|
__version__ = importlib.metadata.version(__name__)
|
|
10
10
|
except importlib.metadata.PackageNotFoundError:
|
|
11
11
|
__version__ = "0.0.0-dev"
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
__project__ = "aas-standard-parser"
|
|
14
14
|
__package__ = "aas-standard-parser"
|
|
15
15
|
|
|
16
|
-
from aas_standard_parser.aimc_parser import AIMCParser
|
|
17
16
|
from aas_standard_parser.aid_parser import AIDParser
|
|
18
|
-
|
|
17
|
+
from aas_standard_parser.aimc_parser import AIMCParser
|
|
19
18
|
|
|
20
19
|
__all__ = ["AIMCParser", "AIDParser"]
|
|
@@ -5,17 +5,32 @@ from typing import Dict, List
|
|
|
5
5
|
from basyx.aas.model import (
|
|
6
6
|
Property,
|
|
7
7
|
SubmodelElement,
|
|
8
|
-
SubmodelElementCollection, SubmodelElementList,
|
|
8
|
+
SubmodelElementCollection, SubmodelElementList,
|
|
9
9
|
)
|
|
10
10
|
|
|
11
11
|
from aas_standard_parser.collection_helpers import find_by_semantic_id, find_all_by_semantic_id, find_by_id_short
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
class IProtocolBinding:
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class HttpProtocolBinding(IProtocolBinding):
|
|
21
|
+
|
|
22
|
+
def __init__(self, method_name: str, headers: Dict[str, str]):
|
|
23
|
+
super().__init__()
|
|
24
|
+
self.method_name = method_name
|
|
25
|
+
self.headers = headers
|
|
26
|
+
|
|
27
|
+
|
|
14
28
|
class PropertyDetails:
|
|
15
29
|
|
|
16
|
-
def __init__(self, href: str, keys: List[str]):
|
|
30
|
+
def __init__(self, href: str, keys: List[str], protocol_binding: IProtocolBinding = None):
|
|
17
31
|
self.href = href
|
|
18
32
|
self.keys = keys
|
|
33
|
+
self.protocol_binding = protocol_binding
|
|
19
34
|
|
|
20
35
|
|
|
21
36
|
class IAuthenticationDetails:
|
|
@@ -44,7 +59,7 @@ class AIDParser():
|
|
|
44
59
|
def __init__(self):
|
|
45
60
|
pass
|
|
46
61
|
|
|
47
|
-
def
|
|
62
|
+
def parse_base(self, aid_interface: SubmodelElementCollection) -> str:
|
|
48
63
|
"""Get the base address (EndpointMetadata.base) from a SMC describing an interface in the AID."""
|
|
49
64
|
|
|
50
65
|
endpoint_metadata: SubmodelElementCollection | None = find_by_semantic_id(
|
|
@@ -62,7 +77,7 @@ class AIDParser():
|
|
|
62
77
|
return base.value
|
|
63
78
|
|
|
64
79
|
|
|
65
|
-
def
|
|
80
|
+
def parse_properties(self, aid_interface: SubmodelElementCollection) -> Dict[str, PropertyDetails]:
|
|
66
81
|
"""Find all first-level and nested properties in a provided SMC describing one interface in the AID.
|
|
67
82
|
Map each property (either top-level or nested) to the according 'href' attribute.
|
|
68
83
|
Nested properties are further mapped to the hierarchical list of keys
|
|
@@ -88,38 +103,53 @@ class AIDParser():
|
|
|
88
103
|
fl_properties: List[SubmodelElement] = find_all_by_semantic_id(
|
|
89
104
|
properties.value, "https://admin-shell.io/idta/AssetInterfacesDescription/1/0/PropertyDefinition"
|
|
90
105
|
)
|
|
91
|
-
# TODO: some AIDs have typos in that semanticId but we only support the official ones
|
|
92
|
-
#fl_properties_alternative: List[SubmodelElement] = find_all_by_semantic_id(
|
|
93
|
-
# properties.value, "https://admin-shell.io/idta/AssetInterfaceDescription/1/0/PropertyDefinition"
|
|
94
|
-
#)
|
|
95
|
-
#fl_properties.extend(fl_properties_alternative)
|
|
96
106
|
if fl_properties is None:
|
|
97
|
-
|
|
107
|
+
print(f"WARN: No first-level 'property' SMC not found in 'properties' SMC.")
|
|
98
108
|
return {}
|
|
99
109
|
|
|
100
|
-
def traverse_property(smc: SubmodelElementCollection, parent_path: str, href: str,
|
|
101
|
-
is_items=False, idx=None, is_top_level=False
|
|
110
|
+
def traverse_property(smc: SubmodelElementCollection, parent_path: str, href: str,
|
|
111
|
+
key_path: List[str | int], is_items=False, idx=None, is_top_level=False,
|
|
112
|
+
protocol_binding: IProtocolBinding = None):
|
|
102
113
|
# determine local key only if not top-level
|
|
103
114
|
if not is_top_level:
|
|
104
115
|
if is_items and idx is not None:
|
|
105
|
-
|
|
116
|
+
# is a nested "items" property -> add index to the list of keys
|
|
117
|
+
local_key = idx
|
|
106
118
|
else:
|
|
119
|
+
# is a nested "properties" property -> add value of "key" attribute or idShort to list of keys
|
|
107
120
|
key_prop = find_by_semantic_id(
|
|
108
121
|
smc.value, "https://admin-shell.io/idta/AssetInterfacesDescription/1/0/key"
|
|
109
122
|
)
|
|
110
|
-
local_key = key_prop.value if key_prop else smc.id_short
|
|
123
|
+
local_key = key_prop.value if key_prop else smc.id_short
|
|
111
124
|
new_key_path = key_path + [local_key]
|
|
112
125
|
else:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
126
|
+
# TODO: use the key of the first-level property (or its idShort otherwise)
|
|
127
|
+
# is a top-level property
|
|
128
|
+
#key_prop: Property | None = find_by_semantic_id(
|
|
129
|
+
# smc.value, "https://admin-shell.io/idta/AssetInterfacesDescription/1/0/key"
|
|
130
|
+
#)
|
|
131
|
+
#local_key = key_prop.value if key_prop else smc.id_short
|
|
132
|
+
#new_key_path = [local_key]
|
|
133
|
+
|
|
134
|
+
new_key_path = key_path
|
|
135
|
+
# NOTE (Tom Gneuß, 2025-10-20)
|
|
136
|
+
# See GitHub Issue: https://github.com/admin-shell-io/submodel-templates/issues/197
|
|
137
|
+
# First-level properties are allowed to have a "key" attribute - otherwise the idShort path is used.
|
|
138
|
+
# However, complex first-level properties would represent, e.g., the JSON payload (object) itself.
|
|
139
|
+
# This JSON payload does only have keys for nested elements.
|
|
140
|
+
# So, using the key (or idShort) of the first-level property to get the JSON object from the payload
|
|
141
|
+
# is not possible.
|
|
142
|
+
# On the other hand: the first-level property could intentionally be something within the JSON object.
|
|
143
|
+
# In that case, having a "key" (or using the idSort) is entirely valid.
|
|
144
|
+
# How to distinguish both cases?
|
|
145
|
+
|
|
146
|
+
# create the idShort path of this property
|
|
116
147
|
full_path = f"{parent_path}.{smc.id_short}"
|
|
117
|
-
mapping
|
|
148
|
+
# add this property with all its details to the mapping -> href (from top-level parent if this is nested),
|
|
149
|
+
# protocol bindings (from top-level parent if this is nested), list of keys
|
|
150
|
+
mapping[full_path] = PropertyDetails(href, new_key_path, protocol_binding)
|
|
118
151
|
|
|
119
152
|
# traverse nested "properties" or "items"
|
|
120
|
-
# (nested properties = object members, nested items = array elements)
|
|
121
|
-
# TODO: some apparently use the wrong semanticId:
|
|
122
|
-
# "https://www.w3.org/2019/wot/td#PropertyAffordance"
|
|
123
153
|
for nested_sem_id in [
|
|
124
154
|
"https://www.w3.org/2019/wot/json-schema#properties",
|
|
125
155
|
"https://www.w3.org/2019/wot/json-schema#items",
|
|
@@ -133,26 +163,17 @@ class AIDParser():
|
|
|
133
163
|
nested_properties: List[SubmodelElement] = find_all_by_semantic_id(
|
|
134
164
|
nested_group.value, "https://www.w3.org/2019/wot/json-schema#propertyName"
|
|
135
165
|
)
|
|
136
|
-
# TODO: some AIDs have typos or use wrong semanticIds but we only support the official ones
|
|
137
|
-
#nested_properties_alternative1: List[SubmodelElement] = find_all_by_semantic_id(
|
|
138
|
-
# nested_group.value, "https://admin-shell.io/idta/AssetInterfaceDescription/1/0/PropertyDefinition"
|
|
139
|
-
#)
|
|
140
|
-
# nested_properties_alternative2: List[SubmodelElement] = find_all_by_semantic_id(
|
|
141
|
-
# nested_group.value, "https://admin-shell.io/idta/AssetInterfacesDescription/1/0/PropertyDefinition"
|
|
142
|
-
# )
|
|
143
|
-
#nested_properties.extend(nested_properties_alternative1)
|
|
144
|
-
#nested_properties.extend(nested_properties_alternative2)
|
|
145
166
|
|
|
146
167
|
# traverse all nested properties/items recursively
|
|
147
168
|
for idx, nested in enumerate(nested_properties):
|
|
148
169
|
if nested_sem_id.endswith("#items"):
|
|
149
|
-
# for arrays: append index instead of
|
|
170
|
+
# for arrays: append index instead of "key" attribute
|
|
150
171
|
traverse_property(nested, full_path, href, new_key_path, is_items=True, idx=idx)
|
|
151
172
|
else:
|
|
152
173
|
traverse_property(nested, full_path, href, new_key_path)
|
|
153
174
|
|
|
154
175
|
# process all first-level properties
|
|
155
|
-
for fl_property in fl_properties:
|
|
176
|
+
for fl_property in fl_properties: # type: SubmodelElementCollection
|
|
156
177
|
forms: SubmodelElementCollection | None = find_by_semantic_id(
|
|
157
178
|
fl_property.value, "https://www.w3.org/2019/wot/td#hasForm"
|
|
158
179
|
)
|
|
@@ -165,15 +186,51 @@ class AIDParser():
|
|
|
165
186
|
if href is None:
|
|
166
187
|
raise ValueError("'href' Property not found in 'forms' SMC.")
|
|
167
188
|
|
|
189
|
+
# get the href value of the first-level property
|
|
168
190
|
href_value = href.value
|
|
191
|
+
|
|
192
|
+
# construct the idShort path up to "Interface_.InteractionMetadata.properties"
|
|
193
|
+
# will be used as prefix for the idShort paths of the first-level and nested properties
|
|
169
194
|
idshort_path_prefix = f"{aid_interface.id_short}.{interaction_metadata.id_short}.{properties.id_short}"
|
|
170
195
|
|
|
196
|
+
# check which protocol-specific subtype of forms is used
|
|
197
|
+
# there is no clean solution for determining the subtype (e.g., a supplSemId)
|
|
198
|
+
# -> can only be figured out if the specific fields are present
|
|
199
|
+
protocol_binding: IProtocolBinding = None
|
|
200
|
+
|
|
201
|
+
# ... try HTTP ("htv_methodName" must be present)
|
|
202
|
+
htv_method_name: Property | None = find_by_semantic_id(
|
|
203
|
+
forms.value, "https://www.w3.org/2011/http#methodName"
|
|
204
|
+
)
|
|
205
|
+
if htv_method_name is not None:
|
|
206
|
+
protocol_binding: HttpProtocolBinding = HttpProtocolBinding(htv_method_name.value, {})
|
|
207
|
+
htv_headers: SubmodelElementCollection | None = find_by_semantic_id(
|
|
208
|
+
forms.value, "https://www.w3.org/2011/http#headers"
|
|
209
|
+
)
|
|
210
|
+
if htv_headers is not None:
|
|
211
|
+
for header in htv_headers.value: # type: SubmodelElementCollection
|
|
212
|
+
htv_field_name: Property | None = find_by_semantic_id(
|
|
213
|
+
header.value, "https://www.w3.org/2011/http#fieldName"
|
|
214
|
+
)
|
|
215
|
+
htv_field_value: Property | None = find_by_semantic_id(
|
|
216
|
+
header.value, "https://www.w3.org/2011/http#fieldValue"
|
|
217
|
+
)
|
|
218
|
+
protocol_binding.headers[htv_field_name.value] = htv_field_value.value
|
|
219
|
+
|
|
220
|
+
# TODO: the other protocols
|
|
221
|
+
# ... try Modbus
|
|
222
|
+
# ... try MQTT
|
|
223
|
+
|
|
224
|
+
# recursively parse the first-level property and its nested properties (if any)
|
|
171
225
|
traverse_property(
|
|
172
|
-
fl_property,
|
|
173
|
-
idshort_path_prefix,
|
|
174
|
-
href_value,
|
|
175
|
-
[],
|
|
176
|
-
|
|
226
|
+
smc=fl_property,
|
|
227
|
+
parent_path=idshort_path_prefix,
|
|
228
|
+
href=href_value,
|
|
229
|
+
key_path=[],
|
|
230
|
+
is_items=False,
|
|
231
|
+
idx=None,
|
|
232
|
+
is_top_level=True,
|
|
233
|
+
protocol_binding=protocol_binding
|
|
177
234
|
)
|
|
178
235
|
|
|
179
236
|
return mapping
|
|
@@ -20,8 +20,12 @@ class SourceSinkRelation:
|
|
|
20
20
|
|
|
21
21
|
:return: The source reference as a dictionary.
|
|
22
22
|
"""
|
|
23
|
-
dict_string = json.dumps(
|
|
24
|
-
|
|
23
|
+
dict_string = json.dumps(
|
|
24
|
+
self.source, cls=basyx.aas.adapter.json.AASToJsonEncoder
|
|
25
|
+
)
|
|
26
|
+
dict_string = dict_string.replace("GlobalReference", "Submodel").replace(
|
|
27
|
+
"FragmentReference", "SubmodelElementCollection"
|
|
28
|
+
)
|
|
25
29
|
return json.loads(dict_string)
|
|
26
30
|
|
|
27
31
|
def sink_as_dict(self) -> dict:
|
|
@@ -29,7 +33,9 @@ class SourceSinkRelation:
|
|
|
29
33
|
|
|
30
34
|
:return: The sink reference as a dictionary.
|
|
31
35
|
"""
|
|
32
|
-
return json.loads(
|
|
36
|
+
return json.loads(
|
|
37
|
+
json.dumps(self.sink, cls=basyx.aas.adapter.json.AASToJsonEncoder)
|
|
38
|
+
)
|
|
33
39
|
|
|
34
40
|
|
|
35
41
|
class MappingConfiguration:
|
|
@@ -66,37 +72,54 @@ class AIMCParser:
|
|
|
66
72
|
|
|
67
73
|
self.aimc_submodel = aimc_submodel
|
|
68
74
|
|
|
69
|
-
def get_mapping_configuration_root_element(
|
|
75
|
+
def get_mapping_configuration_root_element(
|
|
76
|
+
self,
|
|
77
|
+
) -> model.SubmodelElementCollection | None:
|
|
70
78
|
"""Get the mapping configuration root submodel element collection from the AIMC submodel.
|
|
71
79
|
|
|
72
80
|
:return: The mapping configuration root submodel element collection or None if not found.
|
|
73
81
|
"""
|
|
74
82
|
self.mapping_configuration_element = next(
|
|
75
|
-
(
|
|
83
|
+
(
|
|
84
|
+
elem
|
|
85
|
+
for elem in self.aimc_submodel.submodel_element
|
|
86
|
+
if elem.id_short == "MappingConfigurations"
|
|
87
|
+
),
|
|
88
|
+
None,
|
|
76
89
|
)
|
|
77
90
|
|
|
78
91
|
if self.mapping_configuration_element is None:
|
|
79
|
-
logger.error(
|
|
92
|
+
logger.error(
|
|
93
|
+
"'MappingConfigurations' element list not found in AIMC submodel."
|
|
94
|
+
)
|
|
80
95
|
return None
|
|
81
96
|
|
|
82
97
|
return self.mapping_configuration_element
|
|
83
98
|
|
|
84
|
-
def get_mapping_configuration_elements(
|
|
99
|
+
def get_mapping_configuration_elements(
|
|
100
|
+
self,
|
|
101
|
+
) -> list[model.SubmodelElementCollection] | None:
|
|
85
102
|
"""Get all mapping configurations elements from the AIMC submodel.
|
|
86
103
|
|
|
87
104
|
:return: A dictionary containing all mapping configurations elements.
|
|
88
105
|
"""
|
|
89
106
|
if self.mapping_configuration_element is None:
|
|
90
|
-
self.mapping_configuration_element =
|
|
107
|
+
self.mapping_configuration_element = (
|
|
108
|
+
self.get_mapping_configuration_root_element()
|
|
109
|
+
)
|
|
91
110
|
|
|
92
111
|
if self.mapping_configuration_element is None:
|
|
93
112
|
return None
|
|
94
113
|
|
|
95
114
|
mapping_configurations: list[model.SubmodelElementCollection] = [
|
|
96
|
-
element
|
|
115
|
+
element
|
|
116
|
+
for element in self.mapping_configuration_element.value
|
|
117
|
+
if isinstance(element, model.SubmodelElementCollection)
|
|
97
118
|
]
|
|
98
119
|
|
|
99
|
-
logger.debug(
|
|
120
|
+
logger.debug(
|
|
121
|
+
f"Found {len(mapping_configurations)} mapping configuration elements in AIMC submodel."
|
|
122
|
+
)
|
|
100
123
|
|
|
101
124
|
return mapping_configurations
|
|
102
125
|
|
|
@@ -125,14 +148,22 @@ class AIMCParser:
|
|
|
125
148
|
mcs = MappingConfigurations()
|
|
126
149
|
mcs.configurations = mapping_configurations
|
|
127
150
|
# add all unique AID submodel IDs from all mapping configurations
|
|
128
|
-
mcs.aid_submodel_ids = list(
|
|
151
|
+
mcs.aid_submodel_ids = list(
|
|
152
|
+
{mc.aid_submodel_id for mc in mapping_configurations}
|
|
153
|
+
)
|
|
129
154
|
|
|
130
|
-
logger.debug(
|
|
131
|
-
|
|
155
|
+
logger.debug(
|
|
156
|
+
f"Found {len(mcs.aid_submodel_ids)} unique AID submodel IDs in mapping configurations."
|
|
157
|
+
)
|
|
158
|
+
logger.debug(
|
|
159
|
+
f"Found {len(mcs.configurations)} mapping configurations in AIMC submodel."
|
|
160
|
+
)
|
|
132
161
|
|
|
133
162
|
return mcs
|
|
134
163
|
|
|
135
|
-
def parse_mapping_configuration(
|
|
164
|
+
def parse_mapping_configuration(
|
|
165
|
+
self, mapping_configuration_element: model.SubmodelElementCollection
|
|
166
|
+
) -> MappingConfiguration | None:
|
|
136
167
|
"""Parse a mapping configuration element.
|
|
137
168
|
|
|
138
169
|
:param mapping_configuration_element: The mapping configuration element to parse.
|
|
@@ -144,19 +175,30 @@ class AIMCParser:
|
|
|
144
175
|
|
|
145
176
|
logger.debug(f"Parse mapping configuration '{mapping_configuration_element}'")
|
|
146
177
|
|
|
147
|
-
interface_reference = self._get_interface_reference(
|
|
178
|
+
interface_reference = self._get_interface_reference(
|
|
179
|
+
mapping_configuration_element
|
|
180
|
+
)
|
|
148
181
|
|
|
149
182
|
if interface_reference is None:
|
|
150
183
|
return None
|
|
151
184
|
|
|
152
|
-
source_sink_relations = self._generate_source_sink_relations(
|
|
185
|
+
source_sink_relations = self._generate_source_sink_relations(
|
|
186
|
+
mapping_configuration_element
|
|
187
|
+
)
|
|
153
188
|
|
|
154
189
|
if len(source_sink_relations) == 0:
|
|
155
|
-
logger.error(
|
|
190
|
+
logger.error(
|
|
191
|
+
f"No source-sink relations found in mapping configuration '{mapping_configuration_element.id_short}'."
|
|
192
|
+
)
|
|
156
193
|
return None
|
|
157
194
|
|
|
158
195
|
# check if all relations have the same AID submodel
|
|
159
|
-
aid_submodel_ids = list(
|
|
196
|
+
aid_submodel_ids = list(
|
|
197
|
+
{
|
|
198
|
+
source_sink_relation.aid_submodel_id
|
|
199
|
+
for source_sink_relation in source_sink_relations
|
|
200
|
+
}
|
|
201
|
+
)
|
|
160
202
|
|
|
161
203
|
if len(aid_submodel_ids) != 1:
|
|
162
204
|
logger.error(
|
|
@@ -171,66 +213,124 @@ class AIMCParser:
|
|
|
171
213
|
mc.aid_submodel_id = aid_submodel_ids[0]
|
|
172
214
|
return mc
|
|
173
215
|
|
|
174
|
-
def _get_interface_reference(
|
|
216
|
+
def _get_interface_reference(
|
|
217
|
+
self, mapping_configuration_element: model.SubmodelElementCollection
|
|
218
|
+
) -> model.ReferenceElement | None:
|
|
175
219
|
"""Get the interface reference ID from the mapping configuration element.
|
|
176
220
|
|
|
177
221
|
:param mapping_configuration_element: The mapping configuration element to extract the interface reference ID from.
|
|
178
222
|
:return: The interface reference ID or None if not found.
|
|
179
223
|
"""
|
|
180
|
-
logger.debug(
|
|
224
|
+
logger.debug(
|
|
225
|
+
f"Get 'InterfaceReference' from mapping configuration '{mapping_configuration_element}'."
|
|
226
|
+
)
|
|
181
227
|
|
|
182
228
|
interface_ref: model.ReferenceElement = next(
|
|
183
|
-
(
|
|
229
|
+
(
|
|
230
|
+
elem
|
|
231
|
+
for elem in mapping_configuration_element.value
|
|
232
|
+
if elem.id_short == "InterfaceReference"
|
|
233
|
+
),
|
|
234
|
+
None,
|
|
184
235
|
)
|
|
185
236
|
|
|
186
|
-
if interface_ref is None or not isinstance(
|
|
187
|
-
|
|
237
|
+
if interface_ref is None or not isinstance(
|
|
238
|
+
interface_ref, model.ReferenceElement
|
|
239
|
+
):
|
|
240
|
+
logger.error(
|
|
241
|
+
f"'InterfaceReference' not found in mapping configuration '{mapping_configuration_element.id_short}'."
|
|
242
|
+
)
|
|
188
243
|
return None
|
|
189
244
|
|
|
190
245
|
if interface_ref.value is None or len(interface_ref.value.key) == 0:
|
|
191
|
-
logger.error(
|
|
246
|
+
logger.error(
|
|
247
|
+
f"'InterfaceReference' has no value in mapping configuration '{mapping_configuration_element.id_short}'."
|
|
248
|
+
)
|
|
192
249
|
return None
|
|
193
250
|
|
|
194
251
|
return interface_ref
|
|
195
252
|
|
|
196
|
-
def _generate_source_sink_relations(
|
|
253
|
+
def _generate_source_sink_relations(
|
|
254
|
+
self, mapping_configuration_element: model.SubmodelElementCollection
|
|
255
|
+
) -> list[SourceSinkRelation]:
|
|
197
256
|
source_sink_relations: list[SourceSinkRelation] = []
|
|
198
257
|
|
|
199
|
-
logger.debug(
|
|
258
|
+
logger.debug(
|
|
259
|
+
f"Get 'MappingSourceSinkRelations' from mapping configuration '{mapping_configuration_element}'."
|
|
260
|
+
)
|
|
200
261
|
|
|
201
262
|
relations_list: model.SubmodelElementList = next(
|
|
202
|
-
(
|
|
263
|
+
(
|
|
264
|
+
elem
|
|
265
|
+
for elem in mapping_configuration_element.value
|
|
266
|
+
if elem.id_short == "MappingSourceSinkRelations"
|
|
267
|
+
),
|
|
268
|
+
None,
|
|
203
269
|
)
|
|
204
270
|
|
|
205
|
-
if relations_list is None or not isinstance(
|
|
206
|
-
|
|
271
|
+
if relations_list is None or not isinstance(
|
|
272
|
+
relations_list, model.SubmodelElementList
|
|
273
|
+
):
|
|
274
|
+
logger.error(
|
|
275
|
+
f"'MappingSourceSinkRelations' not found in mapping configuration '{mapping_configuration_element.id_short}'."
|
|
276
|
+
)
|
|
207
277
|
return source_sink_relations
|
|
208
278
|
|
|
209
279
|
for source_sink_relation in relations_list.value:
|
|
210
280
|
logger.debug(f"Parse source-sink relation '{source_sink_relation}'.")
|
|
211
281
|
|
|
212
282
|
if not isinstance(source_sink_relation, model.RelationshipElement):
|
|
213
|
-
logger.warning(
|
|
283
|
+
logger.warning(
|
|
284
|
+
f"'{source_sink_relation.id_short}' is not a RelationshipElement"
|
|
285
|
+
)
|
|
214
286
|
continue
|
|
215
287
|
|
|
216
|
-
if
|
|
217
|
-
|
|
288
|
+
if (
|
|
289
|
+
source_sink_relation.first is None
|
|
290
|
+
or len(source_sink_relation.first.key) == 0
|
|
291
|
+
):
|
|
292
|
+
logger.warning(
|
|
293
|
+
f"'first' reference is missing in RelationshipElement '{source_sink_relation.id_short}'"
|
|
294
|
+
)
|
|
218
295
|
continue
|
|
219
296
|
|
|
220
|
-
if
|
|
221
|
-
|
|
297
|
+
if (
|
|
298
|
+
source_sink_relation.second is None
|
|
299
|
+
or len(source_sink_relation.second.key) == 0
|
|
300
|
+
):
|
|
301
|
+
logger.warning(
|
|
302
|
+
f"'second' reference is missing in RelationshipElement '{source_sink_relation.id_short}'"
|
|
303
|
+
)
|
|
222
304
|
continue
|
|
223
305
|
|
|
224
|
-
global_ref = next(
|
|
306
|
+
global_ref = next(
|
|
307
|
+
(
|
|
308
|
+
key
|
|
309
|
+
for key in source_sink_relation.first.key
|
|
310
|
+
if key.type == model.KeyTypes.GLOBAL_REFERENCE
|
|
311
|
+
),
|
|
312
|
+
None,
|
|
313
|
+
)
|
|
225
314
|
|
|
226
315
|
if global_ref is None:
|
|
227
|
-
logger.warning(
|
|
316
|
+
logger.warning(
|
|
317
|
+
f"No GLOBAL_REFERENCE key found in 'first' reference of RelationshipElement '{source_sink_relation.id_short}'"
|
|
318
|
+
)
|
|
228
319
|
continue
|
|
229
320
|
|
|
230
|
-
last_fragment_ref = next(
|
|
321
|
+
last_fragment_ref = next(
|
|
322
|
+
(
|
|
323
|
+
key
|
|
324
|
+
for key in reversed(source_sink_relation.first.key)
|
|
325
|
+
if key.type == model.KeyTypes.FRAGMENT_REFERENCE
|
|
326
|
+
),
|
|
327
|
+
None,
|
|
328
|
+
)
|
|
231
329
|
|
|
232
330
|
if last_fragment_ref is None:
|
|
233
|
-
logger.warning(
|
|
331
|
+
logger.warning(
|
|
332
|
+
f"No FRAGMENT_REFERENCE key found in 'first' reference of RelationshipElement '{source_sink_relation.id_short}'"
|
|
333
|
+
)
|
|
234
334
|
continue
|
|
235
335
|
|
|
236
336
|
relation = SourceSinkRelation()
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from basyx.aas import model
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_submodel_element_by_path(
|
|
9
|
+
submodel: model.Submodel, path: str
|
|
10
|
+
) -> model.SubmodelElement:
|
|
11
|
+
"""Returns a specific submodel element from the submodel at a specific path.
|
|
12
|
+
|
|
13
|
+
:param submodel: The submodel to search within.
|
|
14
|
+
:param path: IdShort path to the submodel element (dot-separated), e.g., "Element1.Element2[0].Element3".
|
|
15
|
+
:return: The found submodel element or None if not found.
|
|
16
|
+
"""
|
|
17
|
+
# Split the path by '.' and traverse the structure
|
|
18
|
+
parts = path.split(".")
|
|
19
|
+
current_elements = submodel.submodel_element
|
|
20
|
+
part_index = 0
|
|
21
|
+
for part in parts:
|
|
22
|
+
part_index += 1
|
|
23
|
+
# Handle indexed access like "Element[0]" for SubmodelElementLists
|
|
24
|
+
if "[" in part and "]" in part:
|
|
25
|
+
# Split SubmodelElementList name and index
|
|
26
|
+
base, idx = part[:-1].split("[")
|
|
27
|
+
idx = int(idx)
|
|
28
|
+
# Find the SubmodelElementList in the current elements
|
|
29
|
+
submodel_element = next(
|
|
30
|
+
(el for el in current_elements if el.id_short == base), None
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
if not submodel_element or not (
|
|
34
|
+
isinstance(submodel_element, model.SubmodelElementList)
|
|
35
|
+
or isinstance(submodel_element, model.SubmodelElementCollection)
|
|
36
|
+
):
|
|
37
|
+
logger.debug(
|
|
38
|
+
f"Submodel element '{base}' not found or is not a collection/list in current {current_elements}."
|
|
39
|
+
)
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
# Check if index is within range
|
|
43
|
+
if idx >= len(submodel_element.value):
|
|
44
|
+
logger.debug(
|
|
45
|
+
f"Index '{idx}' out of range for element '{base}' with length {len(submodel_element.value)}."
|
|
46
|
+
)
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
# get the element by its index from SubmodelElementList
|
|
50
|
+
submodel_element = submodel_element.value[idx]
|
|
51
|
+
|
|
52
|
+
else:
|
|
53
|
+
# Find the SubmodelElement in the current SubmodelElementCollection
|
|
54
|
+
submodel_element = next(
|
|
55
|
+
(el for el in current_elements if el.id_short == part), None
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if not submodel_element:
|
|
59
|
+
logger.debug(
|
|
60
|
+
f"Submodel element '{part}' not found in current {current_elements}."
|
|
61
|
+
)
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
# If we've reached the last part, return the found element
|
|
65
|
+
if part_index == len(parts):
|
|
66
|
+
return submodel_element
|
|
67
|
+
|
|
68
|
+
# If the found element is a collection or list, continue traversing
|
|
69
|
+
if isinstance(submodel_element, model.SubmodelElementCollection) or isinstance(
|
|
70
|
+
submodel_element, model.SubmodelElementList
|
|
71
|
+
):
|
|
72
|
+
current_elements = submodel_element.value
|
|
73
|
+
else:
|
|
74
|
+
return submodel_element
|
|
75
|
+
|
|
76
|
+
return submodel_element
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
aas_standard_parser/__init__.py,sha256=vOFawIqavj7AIeabmIOq8iBr69lKFkzZVNKcAeIlayk,609
|
|
2
|
+
aas_standard_parser/aid_parser.py,sha256=ZUAcGVVbwisICoYpsHbG92wG5JEp4DthIZDsOef2wMY,14679
|
|
3
|
+
aas_standard_parser/aimc_parser.py,sha256=QVTAGCdIvf9K-L97aaBbQzHA9QG3JOrgETCHEHLBw0c,11964
|
|
4
|
+
aas_standard_parser/collection_helpers.py,sha256=OYuy5lDEdy5rXw5L5-I2-KHF33efHsZUulo2rA58_zs,3287
|
|
5
|
+
aas_standard_parser/reference_helpers.py,sha256=UbqXaub5PTvt_W3VPntSSFcTS59PUwlTpoujny8rIRI,377
|
|
6
|
+
aas_standard_parser/submodel_parser.py,sha256=91YMIQEaXohNIwI3_b-sGnCvflQhXZrhyyDCpBD2hks,2871
|
|
7
|
+
aas_standard_parser-0.1.7.dist-info/licenses/LICENSE,sha256=simqYMD2P9Ikm0Kh9n7VGNpaVcm2TMVVQmECYZ_xVZ8,1065
|
|
8
|
+
aas_standard_parser-0.1.7.dist-info/METADATA,sha256=SlrH4h1FZKL6cptnpbPKhPevHUkldsvXPLqH5H7eUjk,1718
|
|
9
|
+
aas_standard_parser-0.1.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
10
|
+
aas_standard_parser-0.1.7.dist-info/top_level.txt,sha256=OQaK6cwYttR1-eKTz5u4M0jbwSfp4HqJ56chaf0nHnw,20
|
|
11
|
+
aas_standard_parser-0.1.7.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
aas_standard_parser/__init__.py,sha256=051isycE2NEcFAO-mScawNbv7NsLVhS9209bGQ27dxc,614
|
|
2
|
-
aas_standard_parser/aid_parser.py,sha256=c8fERs7fxA1n5KwSdL8fGEq_w7f7NJdWNd6_QDByFwg,11876
|
|
3
|
-
aas_standard_parser/aimc_parser.py,sha256=GR70DhhkcsBYSKOlD6bJ9jm0GF5u4uYijY7S79jgJLE,10571
|
|
4
|
-
aas_standard_parser/collection_helpers.py,sha256=OYuy5lDEdy5rXw5L5-I2-KHF33efHsZUulo2rA58_zs,3287
|
|
5
|
-
aas_standard_parser/reference_helpers.py,sha256=UbqXaub5PTvt_W3VPntSSFcTS59PUwlTpoujny8rIRI,377
|
|
6
|
-
aas_standard_parser-0.1.5.dist-info/licenses/LICENSE,sha256=simqYMD2P9Ikm0Kh9n7VGNpaVcm2TMVVQmECYZ_xVZ8,1065
|
|
7
|
-
aas_standard_parser-0.1.5.dist-info/METADATA,sha256=cD5O47siY5YgmYg3ckd6tLnhMxlINlWrFtVsEOvxp8Y,1718
|
|
8
|
-
aas_standard_parser-0.1.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
-
aas_standard_parser-0.1.5.dist-info/top_level.txt,sha256=OQaK6cwYttR1-eKTz5u4M0jbwSfp4HqJ56chaf0nHnw,20
|
|
10
|
-
aas_standard_parser-0.1.5.dist-info/RECORD,,
|
|
File without changes
|
{aas_standard_parser-0.1.5.dist-info → aas_standard_parser-0.1.7.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|