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.
@@ -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__ = ["AIMCParser", "AIDParser"]
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, SubmodelElementList, Submodel,
9
+ SubmodelElementCollection,
10
+ SubmodelElementList,
9
11
  )
10
12
 
11
- from aas_standard_parser.collection_helpers import find_by_semantic_id, find_all_by_semantic_id, find_by_id_short
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 PropertyDetails:
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
- def __init__(self, href: str, keys: List[str]):
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 get_base_url_from_interface(self, aid_interface: SubmodelElementCollection) -> str:
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
- #raise ValueError(f"No first-level 'property' SMC not found in 'properties' SMC.")
99
+ print(f"WARN: No first-level 'property' SMC not found in 'properties' SMC.")
98
100
  return {}
99
101
 
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):
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
- local_key = idx # integer index
115
+ # is a nested "items" property -> add index to the list of keys
116
+ local_key = idx
106
117
  else:
107
- key_prop = find_by_semantic_id(
108
- smc.value, "https://admin-shell.io/idta/AssetInterfacesDescription/1/0/key"
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
- new_key_path = key_path # top-level: no key added
114
-
115
- # register this property
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[full_path] = PropertyDetails(href, new_key_path)
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 property key
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
- is_top_level=True
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
- self.source, cls=basyx.aas.adapter.json.AASToJsonEncoder
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
- json.dumps(self.sink, cls=basyx.aas.adapter.json.AASToJsonEncoder)
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
- class AIMCParser:
57
- """Parser for the AIMC submodel.
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
- :return: The parsed AIMC submodel.
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
- aimc_submodel: model.Submodel | None = None
63
- mapping_configuration_element: model.SubmodelElementCollection | None = None
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
- def __init__(self, aimc_submodel: model.Submodel):
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
- self.aimc_submodel = aimc_submodel
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
- def get_mapping_configuration_root_element(
76
- self,
77
- ) -> model.SubmodelElementCollection | None:
78
- """Get the mapping configuration root submodel element collection from the AIMC submodel.
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
- :return: The mapping configuration root submodel element collection or None if not found.
81
- """
82
- self.mapping_configuration_element = next(
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
- if self.mapping_configuration_element is None:
92
- logger.error(
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
- return self.mapping_configuration_element
101
+ logger.debug(f"Found {len(mapping_configurations)} mapping configuration elements in AIMC submodel.")
98
102
 
99
- def get_mapping_configuration_elements(
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
- return mapping_configurations
106
+ def parse_mapping_configurations(aimc_submodel: model.Submodel) -> MappingConfigurations:
107
+ """Parse all mapping configurations in the AIMC submodel.
125
108
 
126
- def parse_mapping_configurations(self) -> MappingConfigurations:
127
- """Parse all mapping configurations in the AIMC submodel.
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
- :return: A list of parsed mapping configurations.
130
- """
131
- logger.info("Parse mapping configurations from AIMC submodel.")
114
+ mapping_configurations: list[MappingConfiguration] = []
132
115
 
133
- mapping_configurations: list[MappingConfiguration] = []
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
- mc_elements = self.get_mapping_configuration_elements()
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
- if mc_elements is None:
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
- for mc_element in mc_elements:
142
- mc = self.parse_mapping_configuration(mc_element)
143
- if mc is not None:
144
- mapping_configurations.append(mc)
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
- logger.debug(f"Parsed {len(mapping_configurations)} mapping configurations.")
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
- mcs = MappingConfigurations()
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
- return mcs
141
+ def parse_mapping_configuration_element(
142
+ mapping_configuration_element: model.SubmodelElementCollection,
143
+ ) -> MappingConfiguration | None:
144
+ """Parse a mapping configuration element.
163
145
 
164
- def parse_mapping_configuration(
165
- self, mapping_configuration_element: model.SubmodelElementCollection
166
- ) -> MappingConfiguration | None:
167
- """Parse a mapping configuration element.
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
- :param mapping_configuration_element: The mapping configuration element to parse.
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
- logger.debug(f"Parse mapping configuration '{mapping_configuration_element}'")
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
- interface_reference = self._get_interface_reference(
179
- mapping_configuration_element
180
- )
160
+ source_sink_relations = _generate_source_sink_relations(mapping_configuration_element)
181
161
 
182
- if interface_reference is None:
183
- return None
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
- source_sink_relations = self._generate_source_sink_relations(
186
- mapping_configuration_element
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
- if len(source_sink_relations) == 0:
190
- logger.error(
191
- f"No source-sink relations found in mapping configuration '{mapping_configuration_element.id_short}'."
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
- if len(aid_submodel_ids) != 1:
204
- logger.error(
205
- f"Multiple AID submodel IDs found in mapping configuration '{mapping_configuration_element.id_short}': {aid_submodel_ids}. Expected exactly one AID submodel ID."
206
- )
207
- return None
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
- 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
- )
243
- return None
244
-
245
- if interface_ref.value is None or len(interface_ref.value.key) == 0:
246
- logger.error(
247
- f"'InterfaceReference' has no value in mapping configuration '{mapping_configuration_element.id_short}'."
248
- )
249
- return None
250
-
251
- return interface_ref
252
-
253
- def _generate_source_sink_relations(
254
- self, mapping_configuration_element: model.SubmodelElementCollection
255
- ) -> list[SourceSinkRelation]:
256
- source_sink_relations: list[SourceSinkRelation] = []
257
-
258
- logger.debug(
259
- f"Get 'MappingSourceSinkRelations' from mapping configuration '{mapping_configuration_element}'."
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
- relations_list: model.SubmodelElementList = next(
263
- (
264
- elem
265
- for elem in mapping_configuration_element.value
266
- if elem.id_short == "MappingSourceSinkRelations"
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 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
- )
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
- return source_sink_relations
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
- (Key(
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 get_submodel_element_by_path(
9
- submodel: model.Submodel, path: str
10
- ) -> model.SubmodelElement:
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
- (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
- )
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) or isinstance(
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aas-standard-parser
3
- Version: 0.1.6
3
+ Version: 0.1.8
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
@@ -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,,