aas-standard-parser 0.1.6__py3-none-any.whl → 0.1.8__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 +2 -2
- aas_standard_parser/aid_parser.py +103 -70
- aas_standard_parser/aimc_parser.py +196 -268
- aas_standard_parser/collection_helpers.py +26 -23
- aas_standard_parser/demo/demo_process.py +18 -0
- aas_standard_parser/demo/logging_handler.py +197 -0
- aas_standard_parser/submodel_parser.py +24 -26
- aas_standard_parser/utils.py +25 -0
- {aas_standard_parser-0.1.6.dist-info → aas_standard_parser-0.1.8.dist-info}/METADATA +1 -1
- aas_standard_parser-0.1.8.dist-info/RECORD +14 -0
- aas_standard_parser-0.1.6.dist-info/RECORD +0 -11
- {aas_standard_parser-0.1.6.dist-info → aas_standard_parser-0.1.8.dist-info}/WHEEL +0 -0
- {aas_standard_parser-0.1.6.dist-info → aas_standard_parser-0.1.8.dist-info}/licenses/LICENSE +0 -0
- {aas_standard_parser-0.1.6.dist-info → aas_standard_parser-0.1.8.dist-info}/top_level.txt +0 -0
aas_standard_parser/__init__.py
CHANGED
|
@@ -13,7 +13,7 @@ except importlib.metadata.PackageNotFoundError:
|
|
|
13
13
|
__project__ = "aas-standard-parser"
|
|
14
14
|
__package__ = "aas-standard-parser"
|
|
15
15
|
|
|
16
|
+
from aas_standard_parser import aimc_parser
|
|
16
17
|
from aas_standard_parser.aid_parser import AIDParser
|
|
17
|
-
from aas_standard_parser.aimc_parser import AIMCParser
|
|
18
18
|
|
|
19
|
-
__all__ = ["
|
|
19
|
+
__all__ = ["AIDParser", "aimc_parser"]
|
|
@@ -1,32 +1,44 @@
|
|
|
1
1
|
"""This module provides functions to parse AID Submodels and extract MQTT interface descriptions."""
|
|
2
|
+
|
|
2
3
|
import base64
|
|
3
4
|
from typing import Dict, List
|
|
4
5
|
|
|
5
6
|
from basyx.aas.model import (
|
|
6
7
|
Property,
|
|
7
8
|
SubmodelElement,
|
|
8
|
-
SubmodelElementCollection,
|
|
9
|
+
SubmodelElementCollection,
|
|
10
|
+
SubmodelElementList,
|
|
9
11
|
)
|
|
10
12
|
|
|
11
|
-
from aas_standard_parser.collection_helpers import
|
|
13
|
+
from aas_standard_parser.collection_helpers import find_all_by_semantic_id, find_by_id_short, find_by_semantic_id
|
|
12
14
|
|
|
13
15
|
|
|
14
|
-
class
|
|
16
|
+
class IProtocolBinding:
|
|
17
|
+
def __init__(self):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HttpProtocolBinding(IProtocolBinding):
|
|
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
|
+
|
|
15
27
|
|
|
16
|
-
|
|
28
|
+
class PropertyDetails:
|
|
29
|
+
def __init__(self, href: str, keys: List[str], protocol_binding: IProtocolBinding = None):
|
|
17
30
|
self.href = href
|
|
18
31
|
self.keys = keys
|
|
32
|
+
self.protocol_binding = protocol_binding
|
|
19
33
|
|
|
20
34
|
|
|
21
35
|
class IAuthenticationDetails:
|
|
22
|
-
|
|
23
36
|
def __init__(self):
|
|
24
37
|
# TODO: different implementations for different AID versions
|
|
25
38
|
pass
|
|
26
39
|
|
|
27
40
|
|
|
28
41
|
class BasicAuthenticationDetails(IAuthenticationDetails):
|
|
29
|
-
|
|
30
42
|
def __init__(self, user: str, password: str):
|
|
31
43
|
self.user = user
|
|
32
44
|
self.password = password
|
|
@@ -34,17 +46,15 @@ class BasicAuthenticationDetails(IAuthenticationDetails):
|
|
|
34
46
|
|
|
35
47
|
|
|
36
48
|
class NoAuthenticationDetails(IAuthenticationDetails):
|
|
37
|
-
|
|
38
49
|
def __init__(self):
|
|
39
50
|
super().__init__()
|
|
40
51
|
|
|
41
52
|
|
|
42
|
-
class AIDParser
|
|
43
|
-
|
|
53
|
+
class AIDParser:
|
|
44
54
|
def __init__(self):
|
|
45
55
|
pass
|
|
46
56
|
|
|
47
|
-
def
|
|
57
|
+
def parse_base(self, aid_interface: SubmodelElementCollection) -> str:
|
|
48
58
|
"""Get the base address (EndpointMetadata.base) from a SMC describing an interface in the AID."""
|
|
49
59
|
|
|
50
60
|
endpoint_metadata: SubmodelElementCollection | None = find_by_semantic_id(
|
|
@@ -53,16 +63,13 @@ class AIDParser():
|
|
|
53
63
|
if endpoint_metadata is None:
|
|
54
64
|
raise ValueError(f"'EndpointMetadata' SMC not found in the provided '{aid_interface.id_short}' SMC.")
|
|
55
65
|
|
|
56
|
-
base: Property | None = find_by_semantic_id(
|
|
57
|
-
endpoint_metadata.value, "https://www.w3.org/2019/wot/td#baseURI"
|
|
58
|
-
)
|
|
66
|
+
base: Property | None = find_by_semantic_id(endpoint_metadata.value, "https://www.w3.org/2019/wot/td#baseURI")
|
|
59
67
|
if base is None:
|
|
60
68
|
raise ValueError("'base' Property not found in 'EndpointMetadata' SMC.")
|
|
61
69
|
|
|
62
70
|
return base.value
|
|
63
71
|
|
|
64
|
-
|
|
65
|
-
def create_property_to_href_map(self, aid_interface: SubmodelElementCollection) -> Dict[str, PropertyDetails]:
|
|
72
|
+
def parse_properties(self, aid_interface: SubmodelElementCollection) -> Dict[str, PropertyDetails]:
|
|
66
73
|
"""Find all first-level and nested properties in a provided SMC describing one interface in the AID.
|
|
67
74
|
Map each property (either top-level or nested) to the according 'href' attribute.
|
|
68
75
|
Nested properties are further mapped to the hierarchical list of keys
|
|
@@ -88,38 +95,58 @@ class AIDParser():
|
|
|
88
95
|
fl_properties: List[SubmodelElement] = find_all_by_semantic_id(
|
|
89
96
|
properties.value, "https://admin-shell.io/idta/AssetInterfacesDescription/1/0/PropertyDefinition"
|
|
90
97
|
)
|
|
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
98
|
if fl_properties is None:
|
|
97
|
-
|
|
99
|
+
print(f"WARN: No first-level 'property' SMC not found in 'properties' SMC.")
|
|
98
100
|
return {}
|
|
99
101
|
|
|
100
|
-
def traverse_property(
|
|
101
|
-
|
|
102
|
+
def traverse_property(
|
|
103
|
+
smc: SubmodelElementCollection,
|
|
104
|
+
parent_path: str,
|
|
105
|
+
href: str,
|
|
106
|
+
key_path: List[str | int],
|
|
107
|
+
is_items=False,
|
|
108
|
+
idx=None,
|
|
109
|
+
is_top_level=False,
|
|
110
|
+
protocol_binding: IProtocolBinding = None,
|
|
111
|
+
):
|
|
102
112
|
# determine local key only if not top-level
|
|
103
113
|
if not is_top_level:
|
|
104
114
|
if is_items and idx is not None:
|
|
105
|
-
|
|
115
|
+
# is a nested "items" property -> add index to the list of keys
|
|
116
|
+
local_key = idx
|
|
106
117
|
else:
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
local_key = key_prop.value if key_prop else smc.id_short # string
|
|
118
|
+
# is a nested "properties" property -> add value of "key" attribute or idShort to list of keys
|
|
119
|
+
key_prop = find_by_semantic_id(smc.value, "https://admin-shell.io/idta/AssetInterfacesDescription/1/0/key")
|
|
120
|
+
local_key = key_prop.value if key_prop else smc.id_short
|
|
111
121
|
new_key_path = key_path + [local_key]
|
|
112
122
|
else:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
123
|
+
# TODO: use the key of the first-level property (or its idShort otherwise)
|
|
124
|
+
# is a top-level property
|
|
125
|
+
# key_prop: Property | None = find_by_semantic_id(
|
|
126
|
+
# smc.value, "https://admin-shell.io/idta/AssetInterfacesDescription/1/0/key"
|
|
127
|
+
# )
|
|
128
|
+
# local_key = key_prop.value if key_prop else smc.id_short
|
|
129
|
+
# new_key_path = [local_key]
|
|
130
|
+
|
|
131
|
+
new_key_path = key_path
|
|
132
|
+
# NOTE (Tom Gneuß, 2025-10-20)
|
|
133
|
+
# See GitHub Issue: https://github.com/admin-shell-io/submodel-templates/issues/197
|
|
134
|
+
# First-level properties are allowed to have a "key" attribute - otherwise the idShort path is used.
|
|
135
|
+
# However, complex first-level properties would represent, e.g., the JSON payload (object) itself.
|
|
136
|
+
# This JSON payload does only have keys for nested elements.
|
|
137
|
+
# So, using the key (or idShort) of the first-level property to get the JSON object from the payload
|
|
138
|
+
# is not possible.
|
|
139
|
+
# On the other hand: the first-level property could intentionally be something within the JSON object.
|
|
140
|
+
# In that case, having a "key" (or using the idSort) is entirely valid.
|
|
141
|
+
# How to distinguish both cases?
|
|
142
|
+
|
|
143
|
+
# create the idShort path of this property
|
|
116
144
|
full_path = f"{parent_path}.{smc.id_short}"
|
|
117
|
-
mapping
|
|
145
|
+
# add this property with all its details to the mapping -> href (from top-level parent if this is nested),
|
|
146
|
+
# protocol bindings (from top-level parent if this is nested), list of keys
|
|
147
|
+
mapping[full_path] = PropertyDetails(href, new_key_path, protocol_binding)
|
|
118
148
|
|
|
119
149
|
# 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
150
|
for nested_sem_id in [
|
|
124
151
|
"https://www.w3.org/2019/wot/json-schema#properties",
|
|
125
152
|
"https://www.w3.org/2019/wot/json-schema#items",
|
|
@@ -133,52 +160,66 @@ class AIDParser():
|
|
|
133
160
|
nested_properties: List[SubmodelElement] = find_all_by_semantic_id(
|
|
134
161
|
nested_group.value, "https://www.w3.org/2019/wot/json-schema#propertyName"
|
|
135
162
|
)
|
|
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
163
|
|
|
146
164
|
# traverse all nested properties/items recursively
|
|
147
165
|
for idx, nested in enumerate(nested_properties):
|
|
148
166
|
if nested_sem_id.endswith("#items"):
|
|
149
|
-
# for arrays: append index instead of
|
|
167
|
+
# for arrays: append index instead of "key" attribute
|
|
150
168
|
traverse_property(nested, full_path, href, new_key_path, is_items=True, idx=idx)
|
|
151
169
|
else:
|
|
152
170
|
traverse_property(nested, full_path, href, new_key_path)
|
|
153
171
|
|
|
154
172
|
# process all first-level properties
|
|
155
|
-
for fl_property in fl_properties:
|
|
156
|
-
forms: SubmodelElementCollection | None = find_by_semantic_id(
|
|
157
|
-
fl_property.value, "https://www.w3.org/2019/wot/td#hasForm"
|
|
158
|
-
)
|
|
173
|
+
for fl_property in fl_properties: # type: SubmodelElementCollection
|
|
174
|
+
forms: SubmodelElementCollection | None = find_by_semantic_id(fl_property.value, "https://www.w3.org/2019/wot/td#hasForm")
|
|
159
175
|
if forms is None:
|
|
160
176
|
raise ValueError(f"'forms' SMC not found in '{fl_property.id_short}' SMC.")
|
|
161
177
|
|
|
162
|
-
href: Property | None = find_by_semantic_id(
|
|
163
|
-
forms.value, "https://www.w3.org/2019/wot/hypermedia#hasTarget"
|
|
164
|
-
)
|
|
178
|
+
href: Property | None = find_by_semantic_id(forms.value, "https://www.w3.org/2019/wot/hypermedia#hasTarget")
|
|
165
179
|
if href is None:
|
|
166
180
|
raise ValueError("'href' Property not found in 'forms' SMC.")
|
|
167
181
|
|
|
182
|
+
# get the href value of the first-level property
|
|
168
183
|
href_value = href.value
|
|
184
|
+
|
|
185
|
+
# construct the idShort path up to "Interface_.InteractionMetadata.properties"
|
|
186
|
+
# will be used as prefix for the idShort paths of the first-level and nested properties
|
|
169
187
|
idshort_path_prefix = f"{aid_interface.id_short}.{interaction_metadata.id_short}.{properties.id_short}"
|
|
170
188
|
|
|
189
|
+
# check which protocol-specific subtype of forms is used
|
|
190
|
+
# there is no clean solution for determining the subtype (e.g., a supplSemId)
|
|
191
|
+
# -> can only be figured out if the specific fields are present
|
|
192
|
+
protocol_binding: IProtocolBinding = None
|
|
193
|
+
|
|
194
|
+
# ... try HTTP ("htv_methodName" must be present)
|
|
195
|
+
htv_method_name: Property | None = find_by_semantic_id(forms.value, "https://www.w3.org/2011/http#methodName")
|
|
196
|
+
if htv_method_name is not None:
|
|
197
|
+
protocol_binding: HttpProtocolBinding = HttpProtocolBinding(htv_method_name.value, {})
|
|
198
|
+
htv_headers: SubmodelElementCollection | None = find_by_semantic_id(forms.value, "https://www.w3.org/2011/http#headers")
|
|
199
|
+
if htv_headers is not None:
|
|
200
|
+
for header in htv_headers.value: # type: SubmodelElementCollection
|
|
201
|
+
htv_field_name: Property | None = find_by_semantic_id(header.value, "https://www.w3.org/2011/http#fieldName")
|
|
202
|
+
htv_field_value: Property | None = find_by_semantic_id(header.value, "https://www.w3.org/2011/http#fieldValue")
|
|
203
|
+
protocol_binding.headers[htv_field_name.value] = htv_field_value.value
|
|
204
|
+
|
|
205
|
+
# TODO: the other protocols
|
|
206
|
+
# ... try Modbus
|
|
207
|
+
# ... try MQTT
|
|
208
|
+
|
|
209
|
+
# recursively parse the first-level property and its nested properties (if any)
|
|
171
210
|
traverse_property(
|
|
172
|
-
fl_property,
|
|
173
|
-
idshort_path_prefix,
|
|
174
|
-
href_value,
|
|
175
|
-
[],
|
|
176
|
-
|
|
211
|
+
smc=fl_property,
|
|
212
|
+
parent_path=idshort_path_prefix,
|
|
213
|
+
href=href_value,
|
|
214
|
+
key_path=[],
|
|
215
|
+
is_items=False,
|
|
216
|
+
idx=None,
|
|
217
|
+
is_top_level=True,
|
|
218
|
+
protocol_binding=protocol_binding,
|
|
177
219
|
)
|
|
178
220
|
|
|
179
221
|
return mapping
|
|
180
222
|
|
|
181
|
-
|
|
182
223
|
def parse_security(self, aid_interface: SubmodelElementCollection) -> IAuthenticationDetails:
|
|
183
224
|
"""Extract the authentication details (EndpointMetadata.security) from the provided interface in the AID.
|
|
184
225
|
|
|
@@ -191,9 +232,7 @@ class AIDParser():
|
|
|
191
232
|
if endpoint_metadata is None:
|
|
192
233
|
raise ValueError(f"'EndpointMetadata' SMC not found in the provided '{aid_interface.id_short}' SMC.")
|
|
193
234
|
|
|
194
|
-
security: SubmodelElementList | None = find_by_semantic_id(
|
|
195
|
-
endpoint_metadata.value, "https://www.w3.org/2019/wot/td#hasSecurityConfiguration"
|
|
196
|
-
)
|
|
235
|
+
security: SubmodelElementList | None = find_by_semantic_id(endpoint_metadata.value, "https://www.w3.org/2019/wot/td#hasSecurityConfiguration")
|
|
197
236
|
if security is None:
|
|
198
237
|
raise ValueError("'security' SML not found in 'EndpointMetadata' SMC.")
|
|
199
238
|
|
|
@@ -212,16 +251,12 @@ class AIDParser():
|
|
|
212
251
|
raise ValueError("'securityDefinitions' SMC not found in 'EndpointMetadata' SMC.")
|
|
213
252
|
|
|
214
253
|
# find the security scheme SMC with the same idShort as mentioned in the reference "sc"
|
|
215
|
-
referenced_security: SubmodelElementCollection | None = find_by_id_short(
|
|
216
|
-
security_definitions.value, sc_idshort
|
|
217
|
-
)
|
|
254
|
+
referenced_security: SubmodelElementCollection | None = find_by_id_short(security_definitions.value, sc_idshort)
|
|
218
255
|
if referenced_security is None:
|
|
219
256
|
raise ValueError(f"Referenced security scheme '{sc_idshort}' SMC not found in 'securityDefinitions' SMC")
|
|
220
257
|
|
|
221
258
|
# get the name of the security scheme
|
|
222
|
-
scheme: Property | None = find_by_semantic_id(
|
|
223
|
-
referenced_security.value, "https://www.w3.org/2019/wot/security#SecurityScheme"
|
|
224
|
-
)
|
|
259
|
+
scheme: Property | None = find_by_semantic_id(referenced_security.value, "https://www.w3.org/2019/wot/security#SecurityScheme")
|
|
225
260
|
if scheme is None:
|
|
226
261
|
raise ValueError(f"'scheme' Property not found in referenced security scheme '{sc_idshort}' SMC.")
|
|
227
262
|
|
|
@@ -232,9 +267,7 @@ class AIDParser():
|
|
|
232
267
|
auth_details = NoAuthenticationDetails()
|
|
233
268
|
|
|
234
269
|
case "basic":
|
|
235
|
-
basic_sc_name: Property | None = find_by_semantic_id(
|
|
236
|
-
referenced_security.value, "https://www.w3.org/2019/wot/security#name"
|
|
237
|
-
)
|
|
270
|
+
basic_sc_name: Property | None = find_by_semantic_id(referenced_security.value, "https://www.w3.org/2019/wot/security#name")
|
|
238
271
|
if basic_sc_name is None:
|
|
239
272
|
raise ValueError("'name' Property not found in 'basic_sc' SMC")
|
|
240
273
|
|
|
@@ -1,31 +1,33 @@
|
|
|
1
|
+
"""Parser for Mapping Configurations in AIMC Submodel."""
|
|
2
|
+
|
|
1
3
|
import json
|
|
2
4
|
import logging
|
|
5
|
+
from dataclasses import dataclass, field
|
|
3
6
|
|
|
4
7
|
import basyx.aas.adapter.json
|
|
5
8
|
from basyx.aas import model
|
|
6
9
|
|
|
10
|
+
import aas_standard_parser.collection_helpers as ch
|
|
11
|
+
|
|
7
12
|
logger = logging.getLogger(__name__)
|
|
8
13
|
|
|
9
14
|
|
|
10
15
|
class SourceSinkRelation:
|
|
11
16
|
"""Class representing a source-sink relation in the mapping configuration."""
|
|
12
17
|
|
|
13
|
-
aid_submodel_id: str
|
|
14
|
-
source: model.ExternalReference
|
|
15
|
-
sink: model.ExternalReference
|
|
16
|
-
property_name: str
|
|
18
|
+
aid_submodel_id: str = field(metadata={"description": "Identifier of the AID submodel used by the source reference."})
|
|
19
|
+
source: model.ExternalReference = field(metadata={"description": "Reference to the source property in the AID submodel."})
|
|
20
|
+
sink: model.ExternalReference = field(metadata={"description": "Reference to the sink property in the target submodel."})
|
|
21
|
+
property_name: str = field(metadata={"description": "Name of the mapped property."})
|
|
22
|
+
source_parent_path: list[str] = field(metadata={"description": "List of idShorts representing the parent path of the reference."})
|
|
17
23
|
|
|
18
24
|
def source_as_dict(self) -> dict:
|
|
19
25
|
"""Convert the source reference to a dictionary.
|
|
20
26
|
|
|
21
27
|
:return: The source reference as a dictionary.
|
|
22
28
|
"""
|
|
23
|
-
dict_string = json.dumps(
|
|
24
|
-
|
|
25
|
-
)
|
|
26
|
-
dict_string = dict_string.replace("GlobalReference", "Submodel").replace(
|
|
27
|
-
"FragmentReference", "SubmodelElementCollection"
|
|
28
|
-
)
|
|
29
|
+
dict_string = json.dumps(self.source, cls=basyx.aas.adapter.json.AASToJsonEncoder)
|
|
30
|
+
dict_string = dict_string.replace("GlobalReference", "Submodel").replace("FragmentReference", "SubmodelElementCollection")
|
|
29
31
|
return json.loads(dict_string)
|
|
30
32
|
|
|
31
33
|
def sink_as_dict(self) -> dict:
|
|
@@ -33,312 +35,238 @@ class SourceSinkRelation:
|
|
|
33
35
|
|
|
34
36
|
:return: The sink reference as a dictionary.
|
|
35
37
|
"""
|
|
36
|
-
return json.loads(
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
return json.loads(json.dumps(self.sink, cls=basyx.aas.adapter.json.AASToJsonEncoder))
|
|
39
|
+
|
|
40
|
+
def get_source_parent_property_group_name(self) -> str:
|
|
41
|
+
"""Get the name of the parent property group from the source. Ignore 'properties' entries from the path."""
|
|
42
|
+
if len(self.source_parent_path) == 0:
|
|
43
|
+
return ""
|
|
44
|
+
|
|
45
|
+
return next((n for n in reversed(self.source_parent_path) if n != "properties"), "")
|
|
39
46
|
|
|
40
47
|
|
|
41
48
|
class MappingConfiguration:
|
|
42
49
|
"""Class representing a mapping configuration."""
|
|
43
50
|
|
|
44
|
-
interface_reference: model.ReferenceElement
|
|
45
|
-
aid_submodel_id: str
|
|
46
|
-
source_sink_relations: list[SourceSinkRelation]
|
|
51
|
+
interface_reference: model.ReferenceElement = field(metadata={"description": "Reference to the interface in the AID submodel."})
|
|
52
|
+
aid_submodel_id: str = field(metadata={"description": "Identifier of the AID submodel used by the interface reference."})
|
|
53
|
+
source_sink_relations: list[SourceSinkRelation] = field(metadata={"description": "List of source-sink relations in the mapping configuration."})
|
|
47
54
|
|
|
48
55
|
|
|
49
56
|
class MappingConfigurations:
|
|
50
57
|
"""Class representing mapping configurations from AIMC submodel."""
|
|
51
58
|
|
|
52
|
-
configurations: list[MappingConfiguration]
|
|
53
|
-
aid_submodel_ids: list[str]
|
|
59
|
+
configurations: list[MappingConfiguration] = field(metadata={"description": "List of mapping configurations."})
|
|
60
|
+
aid_submodel_ids: list[str] = field(metadata={"description": "List of AID submodel IDs used in the mapping configurations."})
|
|
54
61
|
|
|
55
62
|
|
|
56
|
-
|
|
57
|
-
"""
|
|
63
|
+
def get_mapping_configuration_root_element(aimc_submodel: model.Submodel) -> model.SubmodelElementCollection | None:
|
|
64
|
+
"""Get the mapping configuration root submodel element collection from the AIMC submodel.
|
|
58
65
|
|
|
59
|
-
:
|
|
66
|
+
:param aimc_submodel: The AIMC submodel to extract the mapping configuration root from.
|
|
67
|
+
:return: The mapping configuration root submodel element collection or None if not found.
|
|
60
68
|
"""
|
|
69
|
+
# check if AIMC submodel is None
|
|
70
|
+
if aimc_submodel is None:
|
|
71
|
+
logger.error("AIMC submodel is None.")
|
|
72
|
+
return None
|
|
61
73
|
|
|
62
|
-
|
|
63
|
-
|
|
74
|
+
# get 'MappingConfigurations' element list by its semantic ID
|
|
75
|
+
mc_element = ch.find_by_in_semantic_id(aimc_submodel.submodel_element, "idta/AssetInterfacesMappingConfiguration/1/0/MappingConfigurations")
|
|
76
|
+
if mc_element is None:
|
|
77
|
+
logger.error("'MappingConfigurations' element list not found in AIMC submodel.")
|
|
64
78
|
|
|
65
|
-
|
|
66
|
-
"""Initialize the AIMC parser.
|
|
79
|
+
return mc_element
|
|
67
80
|
|
|
68
|
-
:param aimc_submodel: The AIMC submodel to parse.
|
|
69
|
-
"""
|
|
70
|
-
if aimc_submodel is None:
|
|
71
|
-
raise ValueError("AIMC submodel cannot be None.")
|
|
72
81
|
|
|
73
|
-
|
|
82
|
+
def get_mapping_configuration_elements(aimc_submodel: model.Submodel) -> list[model.SubmodelElementCollection] | None:
|
|
83
|
+
"""Get all mapping configurations from the AIMC submodel.
|
|
74
84
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
85
|
+
:param aimc_submodel: The AIMC submodel to extract mapping configurations from.
|
|
86
|
+
:return: A dictionary containing all mapping configurations.
|
|
87
|
+
"""
|
|
88
|
+
# check if AIMC submodel is None
|
|
89
|
+
if aimc_submodel is None:
|
|
90
|
+
logger.error("AIMC submodel is None.")
|
|
91
|
+
return None
|
|
79
92
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
elem
|
|
85
|
-
for elem in self.aimc_submodel.submodel_element
|
|
86
|
-
if elem.id_short == "MappingConfigurations"
|
|
87
|
-
),
|
|
88
|
-
None,
|
|
89
|
-
)
|
|
93
|
+
# get mapping configuration root element
|
|
94
|
+
root_element = get_mapping_configuration_root_element(aimc_submodel)
|
|
95
|
+
if root_element is None:
|
|
96
|
+
return None
|
|
90
97
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
"'MappingConfigurations' element list not found in AIMC submodel."
|
|
94
|
-
)
|
|
95
|
-
return None
|
|
98
|
+
# find all 'MappingConfiguration' elements by their semantic ID
|
|
99
|
+
mapping_configurations = ch.find_all_by_in_semantic_id(root_element.value, "idta/AssetInterfacesMappingConfiguration/1/0/MappingConfiguration")
|
|
96
100
|
|
|
97
|
-
|
|
101
|
+
logger.debug(f"Found {len(mapping_configurations)} mapping configuration elements in AIMC submodel.")
|
|
98
102
|
|
|
99
|
-
|
|
100
|
-
self,
|
|
101
|
-
) -> list[model.SubmodelElementCollection] | None:
|
|
102
|
-
"""Get all mapping configurations elements from the AIMC submodel.
|
|
103
|
+
return mapping_configurations
|
|
103
104
|
|
|
104
|
-
:return: A dictionary containing all mapping configurations elements.
|
|
105
|
-
"""
|
|
106
|
-
if self.mapping_configuration_element is None:
|
|
107
|
-
self.mapping_configuration_element = (
|
|
108
|
-
self.get_mapping_configuration_root_element()
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
if self.mapping_configuration_element is None:
|
|
112
|
-
return None
|
|
113
|
-
|
|
114
|
-
mapping_configurations: list[model.SubmodelElementCollection] = [
|
|
115
|
-
element
|
|
116
|
-
for element in self.mapping_configuration_element.value
|
|
117
|
-
if isinstance(element, model.SubmodelElementCollection)
|
|
118
|
-
]
|
|
119
|
-
|
|
120
|
-
logger.debug(
|
|
121
|
-
f"Found {len(mapping_configurations)} mapping configuration elements in AIMC submodel."
|
|
122
|
-
)
|
|
123
105
|
|
|
124
|
-
|
|
106
|
+
def parse_mapping_configurations(aimc_submodel: model.Submodel) -> MappingConfigurations:
|
|
107
|
+
"""Parse all mapping configurations in the AIMC submodel.
|
|
125
108
|
|
|
126
|
-
|
|
127
|
-
|
|
109
|
+
:param aimc_submodel: The AIMC submodel to parse mapping configurations from.
|
|
110
|
+
:return: A list of parsed mapping configurations.
|
|
111
|
+
"""
|
|
112
|
+
logger.info("Parse mapping configurations from AIMC submodel.")
|
|
128
113
|
|
|
129
|
-
|
|
130
|
-
"""
|
|
131
|
-
logger.info("Parse mapping configurations from AIMC submodel.")
|
|
114
|
+
mapping_configurations: list[MappingConfiguration] = []
|
|
132
115
|
|
|
133
|
-
|
|
116
|
+
# get all mapping configuration elements
|
|
117
|
+
mapping_configurations_elements = get_mapping_configuration_elements(aimc_submodel)
|
|
118
|
+
if mapping_configurations_elements is None:
|
|
119
|
+
logger.error("No mapping configuration elements found in AIMC submodel.")
|
|
120
|
+
return mapping_configurations_elements
|
|
134
121
|
|
|
135
|
-
|
|
122
|
+
# parse each mapping configuration element
|
|
123
|
+
for mc_element in mapping_configurations_elements:
|
|
124
|
+
mc = parse_mapping_configuration_element(mc_element)
|
|
125
|
+
if mc is not None:
|
|
126
|
+
mapping_configurations.append(mc)
|
|
136
127
|
|
|
137
|
-
|
|
138
|
-
logger.error("No mapping configuration elements found in AIMC submodel.")
|
|
139
|
-
return mapping_configurations
|
|
128
|
+
logger.debug(f"Parsed {len(mapping_configurations)} mapping configurations.")
|
|
140
129
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
130
|
+
mcs = MappingConfigurations()
|
|
131
|
+
mcs.configurations = mapping_configurations
|
|
132
|
+
# add all unique AID submodel IDs from all mapping configurations
|
|
133
|
+
mcs.aid_submodel_ids = list({mc.aid_submodel_id for mc in mapping_configurations})
|
|
145
134
|
|
|
146
|
-
|
|
135
|
+
logger.debug(f"Found {len(mcs.aid_submodel_ids)} unique AID submodel IDs in mapping configurations.")
|
|
136
|
+
logger.debug(f"Found {len(mcs.configurations)} mapping configurations in AIMC submodel.")
|
|
147
137
|
|
|
148
|
-
|
|
149
|
-
mcs.configurations = mapping_configurations
|
|
150
|
-
# add all unique AID submodel IDs from all mapping configurations
|
|
151
|
-
mcs.aid_submodel_ids = list(
|
|
152
|
-
{mc.aid_submodel_id for mc in mapping_configurations}
|
|
153
|
-
)
|
|
138
|
+
return mcs
|
|
154
139
|
|
|
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
|
-
)
|
|
161
140
|
|
|
162
|
-
|
|
141
|
+
def parse_mapping_configuration_element(
|
|
142
|
+
mapping_configuration_element: model.SubmodelElementCollection,
|
|
143
|
+
) -> MappingConfiguration | None:
|
|
144
|
+
"""Parse a mapping configuration element.
|
|
163
145
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
146
|
+
:param mapping_configuration_element: The mapping configuration element to parse.
|
|
147
|
+
:return: The parsed mapping configuration or None if parsing failed.
|
|
148
|
+
"""
|
|
149
|
+
if mapping_configuration_element is None:
|
|
150
|
+
logger.error("Mapping configuration element is None.")
|
|
151
|
+
return None
|
|
168
152
|
|
|
169
|
-
|
|
170
|
-
:return: The parsed mapping configuration or None if parsing failed.
|
|
171
|
-
"""
|
|
172
|
-
if mapping_configuration_element is None:
|
|
173
|
-
logger.error("Mapping configuration element is None.")
|
|
174
|
-
return None
|
|
153
|
+
logger.debug(f"Parse mapping configuration '{mapping_configuration_element}'")
|
|
175
154
|
|
|
176
|
-
|
|
155
|
+
# get interface reference element
|
|
156
|
+
interface_reference = _get_interface_reference_element(mapping_configuration_element)
|
|
157
|
+
if interface_reference is None:
|
|
158
|
+
return None
|
|
177
159
|
|
|
178
|
-
|
|
179
|
-
mapping_configuration_element
|
|
180
|
-
)
|
|
160
|
+
source_sink_relations = _generate_source_sink_relations(mapping_configuration_element)
|
|
181
161
|
|
|
182
|
-
|
|
183
|
-
|
|
162
|
+
if len(source_sink_relations) == 0:
|
|
163
|
+
logger.error(f"No source-sink relations found in mapping configuration '{mapping_configuration_element.id_short}'.")
|
|
164
|
+
return None
|
|
184
165
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
)
|
|
166
|
+
# check if all relations have the same AID submodel
|
|
167
|
+
aid_submodel_ids = list({source_sink_relation.aid_submodel_id for source_sink_relation in source_sink_relations})
|
|
188
168
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
)
|
|
193
|
-
return None
|
|
194
|
-
|
|
195
|
-
# check if all relations have the same AID submodel
|
|
196
|
-
aid_submodel_ids = list(
|
|
197
|
-
{
|
|
198
|
-
source_sink_relation.aid_submodel_id
|
|
199
|
-
for source_sink_relation in source_sink_relations
|
|
200
|
-
}
|
|
169
|
+
if len(aid_submodel_ids) != 1:
|
|
170
|
+
logger.error(
|
|
171
|
+
f"Multiple AID submodel IDs found in mapping configuration '{mapping_configuration_element.id_short}': {aid_submodel_ids}. Expected exactly one AID submodel ID."
|
|
201
172
|
)
|
|
173
|
+
return None
|
|
202
174
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
mc = MappingConfiguration()
|
|
210
|
-
mc.interface_reference = interface_reference
|
|
211
|
-
mc.source_sink_relations = source_sink_relations
|
|
212
|
-
# add all unique AID submodel IDs from source-sink relations
|
|
213
|
-
mc.aid_submodel_id = aid_submodel_ids[0]
|
|
214
|
-
return mc
|
|
215
|
-
|
|
216
|
-
def _get_interface_reference(
|
|
217
|
-
self, mapping_configuration_element: model.SubmodelElementCollection
|
|
218
|
-
) -> model.ReferenceElement | None:
|
|
219
|
-
"""Get the interface reference ID from the mapping configuration element.
|
|
220
|
-
|
|
221
|
-
:param mapping_configuration_element: The mapping configuration element to extract the interface reference ID from.
|
|
222
|
-
:return: The interface reference ID or None if not found.
|
|
223
|
-
"""
|
|
224
|
-
logger.debug(
|
|
225
|
-
f"Get 'InterfaceReference' from mapping configuration '{mapping_configuration_element}'."
|
|
226
|
-
)
|
|
175
|
+
mc = MappingConfiguration()
|
|
176
|
+
mc.interface_reference = interface_reference
|
|
177
|
+
mc.source_sink_relations = source_sink_relations
|
|
178
|
+
# add all unique AID submodel IDs from source-sink relations
|
|
179
|
+
mc.aid_submodel_id = aid_submodel_ids[0]
|
|
180
|
+
return mc
|
|
227
181
|
|
|
228
|
-
interface_ref: model.ReferenceElement = next(
|
|
229
|
-
(
|
|
230
|
-
elem
|
|
231
|
-
for elem in mapping_configuration_element.value
|
|
232
|
-
if elem.id_short == "InterfaceReference"
|
|
233
|
-
),
|
|
234
|
-
None,
|
|
235
|
-
)
|
|
236
182
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
)
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
183
|
+
def _get_interface_reference_element(
|
|
184
|
+
mapping_configuration_element: model.SubmodelElementCollection,
|
|
185
|
+
) -> model.ReferenceElement | None:
|
|
186
|
+
"""Get the interface reference ID from the mapping configuration element.
|
|
187
|
+
|
|
188
|
+
:param mapping_configuration_element: The mapping configuration element to extract the interface reference ID from.
|
|
189
|
+
:return: The interface reference ID or None if not found.
|
|
190
|
+
"""
|
|
191
|
+
logger.debug(f"Get 'InterfaceReference' from mapping configuration '{mapping_configuration_element}'.")
|
|
192
|
+
|
|
193
|
+
interface_ref: model.ReferenceElement = ch.find_by_in_semantic_id(
|
|
194
|
+
mapping_configuration_element, "idta/AssetInterfacesMappingConfiguration/1/0/InterfaceReference"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
if interface_ref is None or not isinstance(interface_ref, model.ReferenceElement):
|
|
198
|
+
logger.error(f"'InterfaceReference' not found in mapping configuration '{mapping_configuration_element.id_short}'.")
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
if interface_ref.value is None or len(interface_ref.value.key) == 0:
|
|
202
|
+
logger.error(f"'InterfaceReference' has no value in mapping configuration '{mapping_configuration_element.id_short}'.")
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
return interface_ref
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _generate_source_sink_relations(mapping_configuration_element: model.SubmodelElementCollection) -> list[SourceSinkRelation]:
|
|
209
|
+
source_sink_relations: list[SourceSinkRelation] = []
|
|
210
|
+
|
|
211
|
+
logger.debug(f"Get 'MappingSourceSinkRelations' from mapping configuration '{mapping_configuration_element}'.")
|
|
212
|
+
|
|
213
|
+
relations_list: model.SubmodelElementList = ch.find_by_in_semantic_id(
|
|
214
|
+
mapping_configuration_element, "/idta/AssetInterfacesMappingConfiguration/1/0/MappingSourceSinkRelations"
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
if relations_list is None or not isinstance(relations_list, model.SubmodelElementList):
|
|
218
|
+
logger.error(f"'MappingSourceSinkRelations' not found in mapping configuration '{mapping_configuration_element.id_short}'.")
|
|
219
|
+
return source_sink_relations
|
|
220
|
+
|
|
221
|
+
for source_sink_relation in relations_list.value:
|
|
222
|
+
logger.debug(f"Parse source-sink relation '{source_sink_relation}'.")
|
|
261
223
|
|
|
262
|
-
|
|
263
|
-
(
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
)
|
|
224
|
+
if not isinstance(source_sink_relation, model.RelationshipElement):
|
|
225
|
+
logger.warning(f"'{source_sink_relation}' is not a RelationshipElement")
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
if source_sink_relation.first is None or len(source_sink_relation.first.key) == 0:
|
|
229
|
+
logger.warning(f"'first' reference is missing in RelationshipElement '{source_sink_relation.id_short}'")
|
|
230
|
+
continue
|
|
231
|
+
|
|
232
|
+
if source_sink_relation.second is None or len(source_sink_relation.second.key) == 0:
|
|
233
|
+
logger.warning(f"'second' reference is missing in RelationshipElement '{source_sink_relation.id_short}'")
|
|
234
|
+
continue
|
|
235
|
+
|
|
236
|
+
source_ref = source_sink_relation.first
|
|
237
|
+
|
|
238
|
+
global_ref = next((key for key in source_ref.key if key.type == model.KeyTypes.GLOBAL_REFERENCE), None)
|
|
239
|
+
|
|
240
|
+
if global_ref is None:
|
|
241
|
+
logger.warning(f"No GLOBAL_REFERENCE key found in 'first' reference of RelationshipElement '{source_sink_relation.id_short}'")
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
last_fragment_ref = next(
|
|
245
|
+
(key for key in reversed(source_ref.key) if key.type == model.KeyTypes.FRAGMENT_REFERENCE),
|
|
268
246
|
None,
|
|
269
247
|
)
|
|
270
248
|
|
|
271
|
-
if
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
logger.error(
|
|
275
|
-
f"'MappingSourceSinkRelations' not found in mapping configuration '{mapping_configuration_element.id_short}'."
|
|
276
|
-
)
|
|
277
|
-
return source_sink_relations
|
|
278
|
-
|
|
279
|
-
for source_sink_relation in relations_list.value:
|
|
280
|
-
logger.debug(f"Parse source-sink relation '{source_sink_relation}'.")
|
|
281
|
-
|
|
282
|
-
if not isinstance(source_sink_relation, model.RelationshipElement):
|
|
283
|
-
logger.warning(
|
|
284
|
-
f"'{source_sink_relation.id_short}' is not a RelationshipElement"
|
|
285
|
-
)
|
|
286
|
-
continue
|
|
287
|
-
|
|
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
|
-
)
|
|
295
|
-
continue
|
|
296
|
-
|
|
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
|
-
)
|
|
304
|
-
continue
|
|
305
|
-
|
|
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
|
-
)
|
|
314
|
-
|
|
315
|
-
if global_ref is None:
|
|
316
|
-
logger.warning(
|
|
317
|
-
f"No GLOBAL_REFERENCE key found in 'first' reference of RelationshipElement '{source_sink_relation.id_short}'"
|
|
318
|
-
)
|
|
319
|
-
continue
|
|
320
|
-
|
|
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
|
-
)
|
|
329
|
-
|
|
330
|
-
if last_fragment_ref is None:
|
|
331
|
-
logger.warning(
|
|
332
|
-
f"No FRAGMENT_REFERENCE key found in 'first' reference of RelationshipElement '{source_sink_relation.id_short}'"
|
|
333
|
-
)
|
|
334
|
-
continue
|
|
335
|
-
|
|
336
|
-
relation = SourceSinkRelation()
|
|
337
|
-
relation.source = source_sink_relation.first
|
|
338
|
-
relation.sink = source_sink_relation.second
|
|
339
|
-
relation.aid_submodel_id = global_ref.value
|
|
340
|
-
relation.property_name = last_fragment_ref.value
|
|
341
|
-
|
|
342
|
-
source_sink_relations.append(relation)
|
|
249
|
+
if last_fragment_ref is None:
|
|
250
|
+
logger.warning(f"No FRAGMENT_REFERENCE key found in 'first' reference of RelationshipElement '{source_sink_relation.id_short}'")
|
|
251
|
+
continue
|
|
343
252
|
|
|
344
|
-
|
|
253
|
+
relation = SourceSinkRelation()
|
|
254
|
+
relation.source = source_sink_relation.first
|
|
255
|
+
relation.sink = source_sink_relation.second
|
|
256
|
+
relation.aid_submodel_id = global_ref.value
|
|
257
|
+
relation.property_name = last_fragment_ref.value
|
|
258
|
+
relation.source_parent_path = _get_reference_parent_path(source_ref)
|
|
259
|
+
|
|
260
|
+
source_sink_relations.append(relation)
|
|
261
|
+
|
|
262
|
+
return source_sink_relations
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _get_reference_parent_path(reference: model.ExternalReference) -> list[str]:
|
|
266
|
+
"""Get the parent path of a reference as a list of idShorts.
|
|
267
|
+
|
|
268
|
+
:param reference: The reference to extract the parent path from.
|
|
269
|
+
:return: A list of idShorts representing the parent path.
|
|
270
|
+
"""
|
|
271
|
+
# Exclude the last key which is the actual element
|
|
272
|
+
return [key.value for key in reference.key[:-1] if key.type == model.KeyTypes.FRAGMENT_REFERENCE]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
from basyx.aas.model import NamespaceSet, SubmodelElement, ExternalReference, Reference, Key, KeyTypes
|
|
2
1
|
from typing import List
|
|
3
2
|
|
|
3
|
+
from basyx.aas.model import ExternalReference, Key, KeyTypes, NamespaceSet, Reference, SubmodelElement
|
|
4
|
+
|
|
4
5
|
|
|
5
6
|
def find_all_by_semantic_id(parent: NamespaceSet[SubmodelElement], semantic_id_value: str) -> List[SubmodelElement]:
|
|
6
7
|
"""Find all SubmodelElements having a specific Semantic ID.
|
|
@@ -9,15 +10,8 @@ def find_all_by_semantic_id(parent: NamespaceSet[SubmodelElement], semantic_id_v
|
|
|
9
10
|
:param semantic_id_value: The semantic ID value to search for.
|
|
10
11
|
:return: The found SubmodelElement(s) or an empty list if not found.
|
|
11
12
|
"""
|
|
12
|
-
reference: Reference = ExternalReference(
|
|
13
|
-
|
|
14
|
-
type_=KeyTypes.GLOBAL_REFERENCE,
|
|
15
|
-
value=semantic_id_value
|
|
16
|
-
),)
|
|
17
|
-
)
|
|
18
|
-
found_elements: list[SubmodelElement] = [
|
|
19
|
-
element for element in parent if element.semantic_id.__eq__(reference)
|
|
20
|
-
]
|
|
13
|
+
reference: Reference = ExternalReference((Key(type_=KeyTypes.GLOBAL_REFERENCE, value=semantic_id_value),))
|
|
14
|
+
found_elements: list[SubmodelElement] = [element for element in parent if element.semantic_id.__eq__(reference)]
|
|
21
15
|
return found_elements
|
|
22
16
|
|
|
23
17
|
|
|
@@ -29,14 +23,8 @@ def find_by_semantic_id(parent: NamespaceSet[SubmodelElement], semantic_id_value
|
|
|
29
23
|
:return: The first found SubmodelElement, or None if not found.
|
|
30
24
|
@rtype: object
|
|
31
25
|
"""
|
|
32
|
-
|
|
33
26
|
# create a Reference that acts like the to-be-matched semanticId
|
|
34
|
-
reference: Reference = ExternalReference(
|
|
35
|
-
(Key(
|
|
36
|
-
type_=KeyTypes.GLOBAL_REFERENCE,
|
|
37
|
-
value=semantic_id_value
|
|
38
|
-
),)
|
|
39
|
-
)
|
|
27
|
+
reference: Reference = ExternalReference((Key(type_=KeyTypes.GLOBAL_REFERENCE, value=semantic_id_value),))
|
|
40
28
|
|
|
41
29
|
# check if the constructed Reference appears as semanticId of the child elements
|
|
42
30
|
for element in parent:
|
|
@@ -45,6 +33,26 @@ def find_by_semantic_id(parent: NamespaceSet[SubmodelElement], semantic_id_value
|
|
|
45
33
|
return None
|
|
46
34
|
|
|
47
35
|
|
|
36
|
+
def find_by_in_semantic_id(parent: NamespaceSet[SubmodelElement], semantic_id_part: str) -> SubmodelElement | None:
|
|
37
|
+
"""Find a SubmodelElement by checking if its semantic ID contains the given value.
|
|
38
|
+
|
|
39
|
+
:param parent: The NamespaceSet to search within.
|
|
40
|
+
:param semantic_id_value: The semantic ID value to search for.
|
|
41
|
+
:return: The first found SubmodelElement, or None if not found.
|
|
42
|
+
"""
|
|
43
|
+
return next((el for el in parent if any(semantic_id_part in key.value for key in el.semantic_id.key)), None)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def find_all_by_in_semantic_id(parent: NamespaceSet[SubmodelElement], semantic_id_part: str) -> SubmodelElement | None:
|
|
47
|
+
"""Find a SubmodelElement by checking if its semantic ID contains the given value.
|
|
48
|
+
|
|
49
|
+
:param parent: The NamespaceSet to search within.
|
|
50
|
+
:param semantic_id_value: The semantic ID value to search for.
|
|
51
|
+
:return: The first found SubmodelElement, or None if not found.
|
|
52
|
+
"""
|
|
53
|
+
return [el for el in parent if any(semantic_id_part in key.value for key in el.semantic_id.key)]
|
|
54
|
+
|
|
55
|
+
|
|
48
56
|
def find_by_id_short(parent: NamespaceSet[SubmodelElement], id_short_value: str) -> SubmodelElement | None:
|
|
49
57
|
"""Find a SubmodelElement by its idShort.
|
|
50
58
|
|
|
@@ -79,10 +87,5 @@ def contains_supplemental_semantic_id(element: SubmodelElement, semantic_id_valu
|
|
|
79
87
|
:param semantic_id_value: The supplemental semantic ID value to search for.
|
|
80
88
|
:return: True if the element contains the supplemental semantic ID, False otherwise.
|
|
81
89
|
"""
|
|
82
|
-
reference: Reference = ExternalReference(
|
|
83
|
-
(Key(
|
|
84
|
-
type_=KeyTypes.GLOBAL_REFERENCE,
|
|
85
|
-
value=semantic_id_value
|
|
86
|
-
),)
|
|
87
|
-
)
|
|
90
|
+
reference: Reference = ExternalReference((Key(type_=KeyTypes.GLOBAL_REFERENCE, value=semantic_id_value),))
|
|
88
91
|
return element.supplemental_semantic_id.__contains__(reference)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import aas_standard_parser.aimc_parser as aimc_parser
|
|
4
|
+
from aas_standard_parser.utils import create_submodel_from_file
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def start() -> None:
|
|
10
|
+
logger.info("Demo process started.")
|
|
11
|
+
|
|
12
|
+
aimc_submodel = create_submodel_from_file("tests/test_data/aimc_submodel.json")
|
|
13
|
+
|
|
14
|
+
tmp = aimc_parser.parse_mapping_configurations(aimc_submodel)
|
|
15
|
+
|
|
16
|
+
tmp2 = tmp.configurations[0].source_sink_relations[0].get_source_parent_property_group_name()
|
|
17
|
+
|
|
18
|
+
logger.info("Demo process finished.")
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Logging handler.
|
|
3
|
+
|
|
4
|
+
This module contains all methods and functions to handle the logging.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import queue
|
|
10
|
+
import sys
|
|
11
|
+
import uuid
|
|
12
|
+
from datetime import UTC, datetime
|
|
13
|
+
from logging.handlers import QueueHandler
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import ClassVar
|
|
16
|
+
|
|
17
|
+
from pythonjsonlogger import jsonlogger
|
|
18
|
+
|
|
19
|
+
LOG_FOLDER: str = "./_log"
|
|
20
|
+
LOG_FILE_SUFFIX: str = "_log.json"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ColorCodes:
|
|
24
|
+
"""Define the color codes for the console output."""
|
|
25
|
+
|
|
26
|
+
grey = "\x1b[38;21m"
|
|
27
|
+
green = "\x1b[1;32m"
|
|
28
|
+
yellow = "\x1b[33;21m"
|
|
29
|
+
red = "\x1b[31;21m"
|
|
30
|
+
bold_red = "\x1b[31;1m"
|
|
31
|
+
blue = "\x1b[1;34m"
|
|
32
|
+
light_blue = "\x1b[1;36m"
|
|
33
|
+
purple = "\x1b[1;35m"
|
|
34
|
+
reset = "\x1b[0m"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CustomConsoleFormatter(logging.Formatter):
|
|
38
|
+
"""Custom console formatter for logging with colored level.
|
|
39
|
+
|
|
40
|
+
:param logging: formatter
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
FORMATS: ClassVar[dict] = {
|
|
44
|
+
logging.DEBUG: ColorCodes.blue
|
|
45
|
+
+ "%(levelname)s"
|
|
46
|
+
+ ColorCodes.reset
|
|
47
|
+
+ ": %(message)s (%(filename)s:%(lineno)d)",
|
|
48
|
+
logging.INFO: ColorCodes.green
|
|
49
|
+
+ "%(levelname)s"
|
|
50
|
+
+ ColorCodes.reset
|
|
51
|
+
+ ": %(message)s",
|
|
52
|
+
logging.WARNING: ColorCodes.yellow
|
|
53
|
+
+ "%(levelname)s"
|
|
54
|
+
+ ColorCodes.reset
|
|
55
|
+
+ ": %(message)s",
|
|
56
|
+
logging.ERROR: ColorCodes.red
|
|
57
|
+
+ "%(levelname)s"
|
|
58
|
+
+ ColorCodes.reset
|
|
59
|
+
+ ": %(message)s (%(filename)s:%(lineno)d)",
|
|
60
|
+
logging.CRITICAL: ColorCodes.bold_red
|
|
61
|
+
+ "%(levelname)s: %(message)s (%(filename)s:%(lineno)d)"
|
|
62
|
+
+ ColorCodes.reset,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
def format(self, record) -> str:
|
|
66
|
+
"""Format the log record.
|
|
67
|
+
|
|
68
|
+
:param record: record to format
|
|
69
|
+
:return: formatted record
|
|
70
|
+
"""
|
|
71
|
+
log_fmt = self.FORMATS.get(record.levelno)
|
|
72
|
+
formatter = logging.Formatter(log_fmt)
|
|
73
|
+
return formatter.format(record)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _handle_file_rotation(log_file_path: Path, max_file_count: int = 5) -> None:
|
|
77
|
+
log_folder: Path = log_file_path.resolve()
|
|
78
|
+
|
|
79
|
+
if max_file_count < 1:
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
if not log_folder.exists():
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
existing_log_files: list[Path] = [
|
|
86
|
+
file for file in log_folder.iterdir() if file.name.endswith(LOG_FILE_SUFFIX)
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
if len(existing_log_files) < max_file_count:
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
existing_log_files.sort(key=lambda x: x.stat().st_ctime)
|
|
93
|
+
|
|
94
|
+
files_to_delete: int = len(existing_log_files) - (max_file_count - 1)
|
|
95
|
+
|
|
96
|
+
for file in existing_log_files[:files_to_delete]:
|
|
97
|
+
file.unlink()
|
|
98
|
+
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def initialize_logging(console_level=logging.INFO) -> Path:
|
|
103
|
+
"""Initialize the standard logging.
|
|
104
|
+
|
|
105
|
+
:param debug_mode_status: Status of the debug mode
|
|
106
|
+
:param log_file_name: Name of the (path and extension)
|
|
107
|
+
"""
|
|
108
|
+
log_path = Path(LOG_FOLDER).resolve()
|
|
109
|
+
log_path.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
|
|
111
|
+
log_file_path = log_path / "api.log"
|
|
112
|
+
|
|
113
|
+
log_file_format = (
|
|
114
|
+
"%(asctime)s %(levelname)s: %(message)s (%(filename)s:%(lineno)d)"
|
|
115
|
+
)
|
|
116
|
+
logging.basicConfig(
|
|
117
|
+
filename=log_file_path,
|
|
118
|
+
level=logging.DEBUG,
|
|
119
|
+
format=log_file_format,
|
|
120
|
+
filemode="w",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# set console logging
|
|
124
|
+
console_handler = logging.StreamHandler()
|
|
125
|
+
console_handler.setLevel(console_level)
|
|
126
|
+
console_handler.setFormatter(CustomConsoleFormatter())
|
|
127
|
+
logging.getLogger("").addHandler(console_handler)
|
|
128
|
+
|
|
129
|
+
# set queue logging
|
|
130
|
+
log_queue: queue.Queue = queue.Queue(-1) # Use default max size
|
|
131
|
+
queue_handler = QueueHandler(log_queue)
|
|
132
|
+
logging.getLogger("").addHandler(queue_handler)
|
|
133
|
+
|
|
134
|
+
logger = logging.getLogger(__name__)
|
|
135
|
+
script_path = Path(sys.argv[0])
|
|
136
|
+
python_version = sys.version.replace("\n", "")
|
|
137
|
+
|
|
138
|
+
print("")
|
|
139
|
+
logger.info(f"Run script '{script_path.name.replace('.py', '')}'")
|
|
140
|
+
logger.info(f"Script executed by Python v{python_version}")
|
|
141
|
+
logger.info("Logging initialized")
|
|
142
|
+
|
|
143
|
+
return log_file_path.resolve()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def read_log_file_as_list(log_file_path: Path) -> list[dict]:
|
|
147
|
+
"""Read the log file as a list of dictionaries (Json conform).
|
|
148
|
+
|
|
149
|
+
:param log_file_path: Path to the log file
|
|
150
|
+
:return: list of dictionaries (Json conform)
|
|
151
|
+
"""
|
|
152
|
+
with Path.open(log_file_path, "r", encoding="utf-8") as f:
|
|
153
|
+
return [json.loads(line) for line in f if line.strip()]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def set_log_file(
|
|
157
|
+
max_log_files: int = 10,
|
|
158
|
+
) -> Path:
|
|
159
|
+
"""Set the log file.
|
|
160
|
+
|
|
161
|
+
:param max_log_files: max number of log files in folder, defaults to 5
|
|
162
|
+
:return: log file path
|
|
163
|
+
"""
|
|
164
|
+
logger = logging.getLogger() # Get the root logger
|
|
165
|
+
|
|
166
|
+
# Remove all existing file handlers
|
|
167
|
+
for handler in logger.handlers[:]:
|
|
168
|
+
if isinstance(handler, logging.FileHandler):
|
|
169
|
+
logger.removeHandler(handler)
|
|
170
|
+
handler.close()
|
|
171
|
+
|
|
172
|
+
now = datetime.now(tz=UTC)
|
|
173
|
+
time_string = now.strftime("%Y-%m-%d_%H-%M-%S")
|
|
174
|
+
|
|
175
|
+
# handle log file and folder
|
|
176
|
+
log_path: Path = Path(LOG_FOLDER).resolve()
|
|
177
|
+
log_path = Path(LOG_FOLDER, "runtime").resolve()
|
|
178
|
+
|
|
179
|
+
log_path.mkdir(parents=True, exist_ok=True)
|
|
180
|
+
log_file_name = f"{uuid.uuid4().hex}{LOG_FILE_SUFFIX}"
|
|
181
|
+
log_file_path = log_path / f"{time_string}_{log_file_name}"
|
|
182
|
+
|
|
183
|
+
_handle_file_rotation(log_path, max_log_files)
|
|
184
|
+
|
|
185
|
+
# Add a new file handler with the new log file path
|
|
186
|
+
json_formatter = jsonlogger.JsonFormatter(
|
|
187
|
+
"%(asctime)s %(levelname)s %(name)s %(message)s %(filename)s %(lineno)d"
|
|
188
|
+
)
|
|
189
|
+
json_file_handler = logging.FileHandler(log_file_path, mode="w")
|
|
190
|
+
json_file_handler.setFormatter(json_formatter)
|
|
191
|
+
json_file_handler.setLevel(logging.DEBUG)
|
|
192
|
+
logger.addHandler(json_file_handler)
|
|
193
|
+
|
|
194
|
+
logging.info(f"Maximum log file number is: {max_log_files}") # noqa: LOG015
|
|
195
|
+
logging.info(f"Write log file to: '{log_file_path}'") # noqa: LOG015
|
|
196
|
+
|
|
197
|
+
return log_file_path.resolve()
|
|
@@ -1,13 +1,26 @@
|
|
|
1
|
+
"""Module for parsing submodels."""
|
|
2
|
+
|
|
1
3
|
import logging
|
|
2
4
|
|
|
3
5
|
from basyx.aas import model
|
|
6
|
+
from basyx.aas.model import ExternalReference, Key, KeyTypes, NamespaceSet, Reference, SubmodelElement
|
|
7
|
+
|
|
8
|
+
import collection_helpers
|
|
4
9
|
|
|
5
10
|
logger = logging.getLogger(__name__)
|
|
6
11
|
|
|
7
12
|
|
|
8
|
-
def
|
|
9
|
-
|
|
10
|
-
|
|
13
|
+
def get_element_by_semantic_id(collection: NamespaceSet[SubmodelElement], semantic_id: str) -> SubmodelElement | None:
|
|
14
|
+
"""Get an element from parent collection by its semantic ID (not recursive).
|
|
15
|
+
|
|
16
|
+
:param parent: parent collection to search within
|
|
17
|
+
:param semantic_id: semantic ID to search for
|
|
18
|
+
:return: the found submodel element or None if not found
|
|
19
|
+
"""
|
|
20
|
+
return collection_helpers.find_by_semantic_id(collection, semantic_id)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_submodel_element_by_path(submodel: model.Submodel, path: str) -> model.SubmodelElement:
|
|
11
24
|
"""Returns a specific submodel element from the submodel at a specific path.
|
|
12
25
|
|
|
13
26
|
:param submodel: The submodel to search within.
|
|
@@ -26,24 +39,15 @@ def get_submodel_element_by_path(
|
|
|
26
39
|
base, idx = part[:-1].split("[")
|
|
27
40
|
idx = int(idx)
|
|
28
41
|
# Find the SubmodelElementList in the current elements
|
|
29
|
-
submodel_element = next(
|
|
30
|
-
|
|
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
|
-
)
|
|
42
|
+
submodel_element = next((el for el in current_elements if el.id_short == base), None)
|
|
43
|
+
|
|
44
|
+
if not submodel_element or not (isinstance(submodel_element, (model.SubmodelElementList, model.SubmodelElementCollection))):
|
|
45
|
+
logger.debug(f"Submodel element '{base}' not found or is not a collection/list in current {current_elements}.")
|
|
40
46
|
return None
|
|
41
47
|
|
|
42
48
|
# Check if index is within range
|
|
43
49
|
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
|
-
)
|
|
50
|
+
logger.debug(f"Index '{idx}' out of range for element '{base}' with length {len(submodel_element.value)}.")
|
|
47
51
|
return None
|
|
48
52
|
|
|
49
53
|
# get the element by its index from SubmodelElementList
|
|
@@ -51,14 +55,10 @@ def get_submodel_element_by_path(
|
|
|
51
55
|
|
|
52
56
|
else:
|
|
53
57
|
# 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
|
-
)
|
|
58
|
+
submodel_element = next((el for el in current_elements if el.id_short == part), None)
|
|
57
59
|
|
|
58
60
|
if not submodel_element:
|
|
59
|
-
logger.debug(
|
|
60
|
-
f"Submodel element '{part}' not found in current {current_elements}."
|
|
61
|
-
)
|
|
61
|
+
logger.debug(f"Submodel element '{part}' not found in current {current_elements}.")
|
|
62
62
|
return None
|
|
63
63
|
|
|
64
64
|
# If we've reached the last part, return the found element
|
|
@@ -66,9 +66,7 @@ def get_submodel_element_by_path(
|
|
|
66
66
|
return submodel_element
|
|
67
67
|
|
|
68
68
|
# If the found element is a collection or list, continue traversing
|
|
69
|
-
if isinstance(submodel_element, model.SubmodelElementCollection)
|
|
70
|
-
submodel_element, model.SubmodelElementList
|
|
71
|
-
):
|
|
69
|
+
if isinstance(submodel_element, (model.SubmodelElementCollection, model.SubmodelElementList)):
|
|
72
70
|
current_elements = submodel_element.value
|
|
73
71
|
else:
|
|
74
72
|
return submodel_element
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from aas_http_client import sdk_tools
|
|
5
|
+
from basyx.aas import model
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def create_submodel_from_file(file_path: str) -> model.Submodel:
|
|
9
|
+
"""Creates a Submodel from a given file path.
|
|
10
|
+
|
|
11
|
+
:param file_path: Path to the file containing the submodel data.
|
|
12
|
+
:return: The created Submodel object.
|
|
13
|
+
"""
|
|
14
|
+
file = Path(file_path)
|
|
15
|
+
if not file.exists():
|
|
16
|
+
raise FileNotFoundError(f"Submodel template file not found: {file}")
|
|
17
|
+
|
|
18
|
+
template_data = {}
|
|
19
|
+
|
|
20
|
+
# Load the template JSON file
|
|
21
|
+
with open(file, "r", encoding="utf-8") as f:
|
|
22
|
+
template_data = json.load(f)
|
|
23
|
+
|
|
24
|
+
# Load the template JSON into a Submodel object
|
|
25
|
+
return sdk_tools.convert_to_object(template_data)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
aas_standard_parser/__init__.py,sha256=3iBTUUlJ97ImkAHdss_GP970BANEU1uNeg0HlRzzois,599
|
|
2
|
+
aas_standard_parser/aid_parser.py,sha256=9vp8_kJRfExTmw8tjmO32n1eHFAE5ItmfPlZ2ORLZMs,14314
|
|
3
|
+
aas_standard_parser/aimc_parser.py,sha256=yl94hOtqoOHYFP_8cladJho8u8QZ3MNUXR0OpXl5bLU,12298
|
|
4
|
+
aas_standard_parser/collection_helpers.py,sha256=fTQGQiC-bGD41Qx0FDDBtWhvkeZtEe6Sh-aT93ePwro,4153
|
|
5
|
+
aas_standard_parser/reference_helpers.py,sha256=UbqXaub5PTvt_W3VPntSSFcTS59PUwlTpoujny8rIRI,377
|
|
6
|
+
aas_standard_parser/submodel_parser.py,sha256=4cPziJbFh3GjZ0ClSlCqIFJcYn-1tCg2Bmp7NrnMs_I,3189
|
|
7
|
+
aas_standard_parser/utils.py,sha256=5iIPpM_ob2V9MwFL_vTbXd23doYKot30xenRmTPAquo,723
|
|
8
|
+
aas_standard_parser/demo/demo_process.py,sha256=Xn9uD0xLoNx0ZvB3Lk0oYkGBRHaEfUbfqv5_SpPWYck,530
|
|
9
|
+
aas_standard_parser/demo/logging_handler.py,sha256=x-eX4XDU3sZTJE22rzJSiaWQ8wRMbtaFpFBAeAcbIzI,5752
|
|
10
|
+
aas_standard_parser-0.1.8.dist-info/licenses/LICENSE,sha256=simqYMD2P9Ikm0Kh9n7VGNpaVcm2TMVVQmECYZ_xVZ8,1065
|
|
11
|
+
aas_standard_parser-0.1.8.dist-info/METADATA,sha256=5Sn4uRjsyj1k1yEIdfKXPCuLdHWkTlfglAi--InR3O0,1718
|
|
12
|
+
aas_standard_parser-0.1.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
13
|
+
aas_standard_parser-0.1.8.dist-info/top_level.txt,sha256=OQaK6cwYttR1-eKTz5u4M0jbwSfp4HqJ56chaf0nHnw,20
|
|
14
|
+
aas_standard_parser-0.1.8.dist-info/RECORD,,
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
aas_standard_parser/__init__.py,sha256=vOFawIqavj7AIeabmIOq8iBr69lKFkzZVNKcAeIlayk,609
|
|
2
|
-
aas_standard_parser/aid_parser.py,sha256=c8fERs7fxA1n5KwSdL8fGEq_w7f7NJdWNd6_QDByFwg,11876
|
|
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.6.dist-info/licenses/LICENSE,sha256=simqYMD2P9Ikm0Kh9n7VGNpaVcm2TMVVQmECYZ_xVZ8,1065
|
|
8
|
-
aas_standard_parser-0.1.6.dist-info/METADATA,sha256=kWoMVNQ9HOySA_JIwpGrZzfMMilAPdqoTNmZ-AxLQwo,1718
|
|
9
|
-
aas_standard_parser-0.1.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
10
|
-
aas_standard_parser-0.1.6.dist-info/top_level.txt,sha256=OQaK6cwYttR1-eKTz5u4M0jbwSfp4HqJ56chaf0nHnw,20
|
|
11
|
-
aas_standard_parser-0.1.6.dist-info/RECORD,,
|
|
File without changes
|
{aas_standard_parser-0.1.6.dist-info → aas_standard_parser-0.1.8.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|