aas-standard-parser 0.1.5__tar.gz → 0.1.7__tar.gz

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.
Files changed (16) hide show
  1. {aas_standard_parser-0.1.5 → aas_standard_parser-0.1.7}/PKG-INFO +1 -1
  2. {aas_standard_parser-0.1.5 → aas_standard_parser-0.1.7}/aas_standard_parser/__init__.py +3 -4
  3. {aas_standard_parser-0.1.5 → aas_standard_parser-0.1.7}/aas_standard_parser/aid_parser.py +94 -37
  4. {aas_standard_parser-0.1.5 → aas_standard_parser-0.1.7}/aas_standard_parser/aimc_parser.py +138 -38
  5. aas_standard_parser-0.1.7/aas_standard_parser/submodel_parser.py +76 -0
  6. {aas_standard_parser-0.1.5 → aas_standard_parser-0.1.7}/aas_standard_parser.egg-info/PKG-INFO +1 -1
  7. {aas_standard_parser-0.1.5 → aas_standard_parser-0.1.7}/aas_standard_parser.egg-info/SOURCES.txt +1 -0
  8. {aas_standard_parser-0.1.5 → aas_standard_parser-0.1.7}/pyproject.toml +2 -5
  9. {aas_standard_parser-0.1.5 → aas_standard_parser-0.1.7}/LICENSE +0 -0
  10. {aas_standard_parser-0.1.5 → aas_standard_parser-0.1.7}/README.md +0 -0
  11. {aas_standard_parser-0.1.5 → aas_standard_parser-0.1.7}/aas_standard_parser/collection_helpers.py +0 -0
  12. {aas_standard_parser-0.1.5 → aas_standard_parser-0.1.7}/aas_standard_parser/reference_helpers.py +0 -0
  13. {aas_standard_parser-0.1.5 → aas_standard_parser-0.1.7}/aas_standard_parser.egg-info/dependency_links.txt +0 -0
  14. {aas_standard_parser-0.1.5 → aas_standard_parser-0.1.7}/aas_standard_parser.egg-info/requires.txt +0 -0
  15. {aas_standard_parser-0.1.5 → aas_standard_parser-0.1.7}/aas_standard_parser.egg-info/top_level.txt +0 -0
  16. {aas_standard_parser-0.1.5 → aas_standard_parser-0.1.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aas-standard-parser
3
- Version: 0.1.5
3
+ Version: 0.1.7
4
4
  Summary: Some auxiliary functions for parsing standard submodels
5
5
  Author-email: Daniel Klein <daniel.klein@em.ag>
6
6
  License: MIT License
@@ -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, Submodel,
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 get_base_url_from_interface(self, aid_interface: SubmodelElementCollection) -> str:
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 create_property_to_href_map(self, aid_interface: SubmodelElementCollection) -> Dict[str, PropertyDetails]:
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
- #raise ValueError(f"No first-level 'property' SMC not found in 'properties' SMC.")
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, key_path: List[str | int],
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
- local_key = idx # integer index
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 # string
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
- new_key_path = key_path # top-level: no key added
114
-
115
- # register this property
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[full_path] = PropertyDetails(href, new_key_path)
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 property key
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
- is_top_level=True
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(self.source, cls=basyx.aas.adapter.json.AASToJsonEncoder)
24
- dict_string = dict_string.replace("GlobalReference", "Submodel").replace("FragmentReference", "SubmodelElementCollection")
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(json.dumps(self.sink, cls=basyx.aas.adapter.json.AASToJsonEncoder))
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(self) -> model.SubmodelElementCollection | None:
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
- (elem for elem in self.aimc_submodel.submodel_element if elem.id_short == "MappingConfigurations"), None
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("'MappingConfigurations' element list not found in AIMC submodel.")
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(self) -> list[model.SubmodelElementCollection] | None:
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 = self.get_mapping_configuration_root_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 for element in self.mapping_configuration_element.value if isinstance(element, model.SubmodelElementCollection)
115
+ element
116
+ for element in self.mapping_configuration_element.value
117
+ if isinstance(element, model.SubmodelElementCollection)
97
118
  ]
98
119
 
99
- logger.debug(f"Found {len(mapping_configurations)} mapping configuration elements in AIMC submodel.")
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({mc.aid_submodel_id for mc in mapping_configurations})
151
+ mcs.aid_submodel_ids = list(
152
+ {mc.aid_submodel_id for mc in mapping_configurations}
153
+ )
129
154
 
130
- logger.debug(f"Found {len(mcs.aid_submodel_ids)} unique AID submodel IDs in mapping configurations.")
131
- logger.debug(f"Found {len(mcs.configurations)} mapping configurations in AIMC submodel.")
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(self, mapping_configuration_element: model.SubmodelElementCollection) -> MappingConfiguration | None:
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(mapping_configuration_element)
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(mapping_configuration_element)
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(f"No source-sink relations found in mapping configuration '{mapping_configuration_element.id_short}'.")
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({source_sink_relation.aid_submodel_id for source_sink_relation in source_sink_relations})
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(self, mapping_configuration_element: model.SubmodelElementCollection) -> model.ReferenceElement | None:
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(f"Get 'InterfaceReference' from mapping configuration '{mapping_configuration_element}'.")
224
+ logger.debug(
225
+ f"Get 'InterfaceReference' from mapping configuration '{mapping_configuration_element}'."
226
+ )
181
227
 
182
228
  interface_ref: model.ReferenceElement = next(
183
- (elem for elem in mapping_configuration_element.value if elem.id_short == "InterfaceReference"), None
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(interface_ref, model.ReferenceElement):
187
- logger.error(f"'InterfaceReference' not found in mapping configuration '{mapping_configuration_element.id_short}'.")
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(f"'InterfaceReference' has no value in mapping configuration '{mapping_configuration_element.id_short}'.")
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(self, mapping_configuration_element: model.SubmodelElementCollection) -> list[SourceSinkRelation]:
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(f"Get 'MappingSourceSinkRelations' from mapping configuration '{mapping_configuration_element}'.")
258
+ logger.debug(
259
+ f"Get 'MappingSourceSinkRelations' from mapping configuration '{mapping_configuration_element}'."
260
+ )
200
261
 
201
262
  relations_list: model.SubmodelElementList = next(
202
- (elem for elem in mapping_configuration_element.value if elem.id_short == "MappingSourceSinkRelations"), None
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(relations_list, model.SubmodelElementList):
206
- logger.error(f"'MappingSourceSinkRelations' not found in mapping configuration '{mapping_configuration_element.id_short}'.")
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(f"'{source_sink_relation.id_short}' is not a RelationshipElement")
283
+ logger.warning(
284
+ f"'{source_sink_relation.id_short}' is not a RelationshipElement"
285
+ )
214
286
  continue
215
287
 
216
- if source_sink_relation.first is None or len(source_sink_relation.first.key) == 0:
217
- logger.warning(f"'first' reference is missing in RelationshipElement '{source_sink_relation.id_short}'")
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 source_sink_relation.second is None or len(source_sink_relation.second.key) == 0:
221
- logger.warning(f"'second' reference is missing in RelationshipElement '{source_sink_relation.id_short}'")
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((key for key in source_sink_relation.first.key if key.type == model.KeyTypes.GLOBAL_REFERENCE), None)
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(f"No GLOBAL_REFERENCE key found in 'first' reference of RelationshipElement '{source_sink_relation.id_short}'")
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((key for key in reversed(source_sink_relation.first.key) if key.type == model.KeyTypes.FRAGMENT_REFERENCE), None)
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(f"No FRAGMENT_REFERENCE key found in 'first' reference of RelationshipElement '{source_sink_relation.id_short}'")
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aas-standard-parser
3
- Version: 0.1.5
3
+ Version: 0.1.7
4
4
  Summary: Some auxiliary functions for parsing standard submodels
5
5
  Author-email: Daniel Klein <daniel.klein@em.ag>
6
6
  License: MIT License
@@ -6,6 +6,7 @@ aas_standard_parser/aid_parser.py
6
6
  aas_standard_parser/aimc_parser.py
7
7
  aas_standard_parser/collection_helpers.py
8
8
  aas_standard_parser/reference_helpers.py
9
+ aas_standard_parser/submodel_parser.py
9
10
  aas_standard_parser.egg-info/PKG-INFO
10
11
  aas_standard_parser.egg-info/SOURCES.txt
11
12
  aas_standard_parser.egg-info/dependency_links.txt
@@ -4,15 +4,12 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "aas-standard-parser"
7
- version = "0.1.5"
7
+ version = "0.1.7"
8
8
  description = "Some auxiliary functions for parsing standard submodels"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
11
11
  authors = [{ name = "Daniel Klein", email = "daniel.klein@em.ag" }]
12
- dependencies = [
13
- "typing>=3.7.4.3",
14
- "basyx-python-sdk>=1.2.1",
15
- ]
12
+ dependencies = ["typing>=3.7.4.3", "basyx-python-sdk>=1.2.1"]
16
13
  [project.urls]
17
14
  Homepage = "https://github.com/fluid40/aas-http-client"
18
15