stepxmlclient 1.0.0__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.
Files changed (70) hide show
  1. stepxmlclient/__init__.py +22 -0
  2. stepxmlclient/client.py +373 -0
  3. stepxmlclient/creator.py +317 -0
  4. stepxmlclient/model/__init__.py +418 -0
  5. stepxmlclient/model/asset.py +250 -0
  6. stepxmlclient/model/attribute.py +288 -0
  7. stepxmlclient/model/business.py +233 -0
  8. stepxmlclient/model/classification.py +409 -0
  9. stepxmlclient/model/complex_elements.py +260 -0
  10. stepxmlclient/model/deletion.py +87 -0
  11. stepxmlclient/model/entity.py +368 -0
  12. stepxmlclient/model/metadata.py +199 -0
  13. stepxmlclient/model/organization.py +167 -0
  14. stepxmlclient/model/product.py +519 -0
  15. stepxmlclient/model/relations.py +105 -0
  16. stepxmlclient/model/replacement.py +417 -0
  17. stepxmlclient/parser.py +2805 -0
  18. stepxmlclient/rest_client/__init__.py +16 -0
  19. stepxmlclient/rest_client/base.py +388 -0
  20. stepxmlclient/rest_client/client.py +127 -0
  21. stepxmlclient/rest_client/exceptions.py +97 -0
  22. stepxmlclient/rest_client/models/__init__.py +175 -0
  23. stepxmlclient/rest_client/models/asset.py +253 -0
  24. stepxmlclient/rest_client/models/attribute.py +112 -0
  25. stepxmlclient/rest_client/models/background_process.py +23 -0
  26. stepxmlclient/rest_client/models/batch.py +41 -0
  27. stepxmlclient/rest_client/models/classification.py +57 -0
  28. stepxmlclient/rest_client/models/collection.py +26 -0
  29. stepxmlclient/rest_client/models/common.py +192 -0
  30. stepxmlclient/rest_client/models/context.py +16 -0
  31. stepxmlclient/rest_client/models/data_type_group.py +15 -0
  32. stepxmlclient/rest_client/models/entity.py +63 -0
  33. stepxmlclient/rest_client/models/event_processor.py +15 -0
  34. stepxmlclient/rest_client/models/import_export.py +43 -0
  35. stepxmlclient/rest_client/models/integration_endpoint.py +16 -0
  36. stepxmlclient/rest_client/models/product.py +200 -0
  37. stepxmlclient/rest_client/models/relations.py +280 -0
  38. stepxmlclient/rest_client/models/system_information.py +11 -0
  39. stepxmlclient/rest_client/models/user.py +28 -0
  40. stepxmlclient/rest_client/models/workflow.py +30 -0
  41. stepxmlclient/rest_client/operations/__init__.py +54 -0
  42. stepxmlclient/rest_client/operations/assets.py +652 -0
  43. stepxmlclient/rest_client/operations/attributes.py +252 -0
  44. stepxmlclient/rest_client/operations/background_processes.py +84 -0
  45. stepxmlclient/rest_client/operations/batch.py +28 -0
  46. stepxmlclient/rest_client/operations/classifications.py +248 -0
  47. stepxmlclient/rest_client/operations/collection_groups.py +46 -0
  48. stepxmlclient/rest_client/operations/collections.py +67 -0
  49. stepxmlclient/rest_client/operations/contexts.py +88 -0
  50. stepxmlclient/rest_client/operations/data_type_groups.py +68 -0
  51. stepxmlclient/rest_client/operations/entities.py +295 -0
  52. stepxmlclient/rest_client/operations/event_processors.py +109 -0
  53. stepxmlclient/rest_client/operations/exports.py +153 -0
  54. stepxmlclient/rest_client/operations/gateway.py +53 -0
  55. stepxmlclient/rest_client/operations/inbound.py +472 -0
  56. stepxmlclient/rest_client/operations/outbound.py +64 -0
  57. stepxmlclient/rest_client/operations/products.py +489 -0
  58. stepxmlclient/rest_client/operations/relations.py +349 -0
  59. stepxmlclient/rest_client/operations/reports.py +27 -0
  60. stepxmlclient/rest_client/operations/system_information.py +28 -0
  61. stepxmlclient/rest_client/operations/user_groups.py +79 -0
  62. stepxmlclient/rest_client/operations/users.py +119 -0
  63. stepxmlclient/rest_client/operations/workflow_tasks.py +61 -0
  64. stepxmlclient/rest_client/operations/workflows.py +50 -0
  65. stepxmlclient/upload.py +293 -0
  66. stepxmlclient-1.0.0.dist-info/METADATA +666 -0
  67. stepxmlclient-1.0.0.dist-info/RECORD +70 -0
  68. stepxmlclient-1.0.0.dist-info/WHEEL +5 -0
  69. stepxmlclient-1.0.0.dist-info/licenses/LICENSE +21 -0
  70. stepxmlclient-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,2805 @@
1
+ """
2
+ STEP-XML Parser module.
3
+
4
+ This module provides the Parser class for deserializing STEP-XML documents into Python dictionaries.
5
+ It handles both namespaced and unnamespaced XML elements and supports parsing all STEP entity types
6
+ including Products, Assets, Classifications, Attributes, Business Rules, and advanced configuration elements.
7
+
8
+ The parser uses a declarative approach with separate lists for each entity type, following the
9
+ STEP-XML schema structure. It handles namespace-aware parsing automatically, supporting both
10
+ prefixed namespaces (e.g., step:Product) and default namespaces.
11
+
12
+ Example:
13
+ Basic parsing workflow::
14
+
15
+ from stepxmlclient.parser import Parser
16
+
17
+ parser = Parser()
18
+ parser.parse_file('step_data.xml')
19
+
20
+ # Access parsed entities
21
+ for product in parser.products:
22
+ print(product['ID'])
23
+
24
+ for asset in parser.assets:
25
+ print(asset['ID'])
26
+ """
27
+
28
+ import xml.etree.ElementTree as ET
29
+ from typing import Any, Callable, List, Optional
30
+ from . import STEPXML_NS
31
+
32
+
33
+ def _str_to_bool(value: str) -> bool:
34
+ """Convert XML boolean string ('true'/'false') to Python bool.
35
+
36
+ Args:
37
+ value: String value from XML ('true' or 'false', case-insensitive)
38
+
39
+ Returns:
40
+ Boolean representation of the string value
41
+ """
42
+ return value.lower() == 'true'
43
+
44
+
45
+ def _get_local_tag(element: ET.Element) -> str:
46
+ """Extract local tag name from potentially namespaced tag.
47
+
48
+ Handles both namespaced tags (e.g., '{http://...}Value') and
49
+ unnamespaced tags (e.g., 'Value'). This is essential for robust XML parsing
50
+ since different XML sources may or may not include namespace prefixes.
51
+
52
+ Args:
53
+ element: XML Element to extract tag name from
54
+
55
+ Returns:
56
+ Local tag name without namespace prefix
57
+
58
+ Example:
59
+ For an element with tag '{http://www.stibosystems.com/step}Product',
60
+ returns 'Product'
61
+ """
62
+ tag = element.tag
63
+ if '}' in tag:
64
+ return tag.split('}', 1)[1]
65
+ return tag
66
+
67
+
68
+ def _get_namespace_from_tag(tag: str) -> Optional[str]:
69
+ """Extract namespace URL from a namespaced tag.
70
+
71
+ Handles both namespaced tags (e.g., '{http://...}Value') and
72
+ unnamespaced tags (e.g., 'Value'). Returns None if tag is unnamespaced.
73
+
74
+ Args:
75
+ tag: Tag string from XML Element
76
+
77
+ Returns:
78
+ Namespace URL if tag is namespaced, None otherwise
79
+
80
+ Example:
81
+ For tag '{http://www.stibosystems.com/step}Product',
82
+ returns 'http://www.stibosystems.com/step'
83
+ """
84
+ if not tag or '}' not in tag:
85
+ return None
86
+ # Extract namespace from {namespace}localname format
87
+ return tag[1:tag.index('}')]
88
+
89
+
90
+ class Parser:
91
+ """Parser for STEP-XML documents.
92
+
93
+ Deserializes STEP-XML documents into Python dictionaries organized by entity type.
94
+ The parser maintains separate lists for each STEP entity type (products, assets,
95
+ classifications, etc.) and handles namespace-aware XML parsing automatically.
96
+
97
+ Attributes:
98
+ products: List of parsed Product entities
99
+ assets: List of parsed Asset entities
100
+ classifications: List of parsed Classification entities
101
+ entities: List of parsed Entity (business entity) objects
102
+ attributes: List of parsed Attribute definitions
103
+ attribute_groups: List of parsed Attribute groups
104
+ list_of_values_groups: List of parsed List-of-Values group definitions
105
+ list_of_values: List of parsed List-of-Values items
106
+ user_types: List of parsed User Type definitions
107
+ contexts: List of parsed STEP Contexts
108
+ link_types: List of parsed Link Type definitions
109
+ delete_products: List of Product deletion specifications
110
+ delete_assets: List of Asset deletion specifications
111
+ delete_classifications: List of Classification deletion specifications
112
+ delete_entities: List of Entity deletion specifications
113
+ business_rules: List of parsed Business Rule definitions
114
+ event_queues: List of parsed Event Queue configurations
115
+ workflows: List of parsed Workflow definitions
116
+ export_configurations: List of parsed Export Configuration definitions
117
+ cross_reference_types: List of parsed Cross Reference Type definitions
118
+ qualifiers: List of parsed Qualifier definitions
119
+ dimensions: List of parsed Dimension definitions
120
+ tags: List of parsed Tag definitions
121
+ tag_groups: List of parsed Tag Group definitions
122
+ units: List of parsed Unit definitions
123
+ replacement_rules: Dictionary of replacement rules for various entity types
124
+ collections: List of parsed Collection entities
125
+ collection_groups: List of parsed Collection Group entities
126
+ bulk_update_configurations: List of parsed Bulk Update Configuration definitions
127
+ derived_event_types: List of parsed Derived Event Type definitions
128
+ global_settings: Dictionary of global system settings
129
+ import_configurations: List of parsed Import Configuration definitions
130
+ portal_configurations: List of parsed Portal Configuration definitions
131
+ keys: List of parsed Key definitions (Phase 14)
132
+ cross_contexts: List of parsed Cross-Context definitions (Phase 14)
133
+ business_libraries: List of parsed Business Library definitions (Phase 14)
134
+ setup_groups: List of parsed Setup Group definitions (Phase 14)
135
+ action_sets: List of parsed Action Set definitions (Phase 14)
136
+ status_flags: List of parsed Status Flag definitions (Phase 14)
137
+ integration_endpoints: List of parsed Integration Endpoint definitions (Phase 14)
138
+ """
139
+
140
+ def __init__(self):
141
+ """Initialize a new Parser instance with empty entity lists.
142
+
143
+ Creates a fresh parser with all entity collection lists initialized as empty.
144
+ Call parse_file(), parse_str(), or parse() to populate these lists with parsed data.
145
+ """
146
+ self._tree = None
147
+ self._root: Optional[ET.Element] = None
148
+ self.products: List[dict] = []
149
+ self.assets: List[dict] = []
150
+ self.classifications: List[dict] = []
151
+ self.entities: List[dict] = []
152
+ self.attributes: List[dict] = []
153
+ self.attribute_groups: List[dict] = []
154
+ self.list_of_values_groups: List[dict] = []
155
+ self.list_of_values: List[dict] = []
156
+ self.user_types: List[dict] = []
157
+ self.contexts: List[dict] = []
158
+ self.link_types: List[dict] = []
159
+ self.delete_products: List[dict] = []
160
+ self.delete_assets: List[dict] = []
161
+ self.delete_classifications: List[dict] = []
162
+ self.delete_entities: List[dict] = []
163
+ self.business_rules: List[dict] = []
164
+ self.event_queues: List[dict] = []
165
+ self.workflows: List[dict] = []
166
+ self.export_configurations: List[dict] = []
167
+ self.cross_reference_types: List[dict] = []
168
+ self.qualifiers: List[dict] = []
169
+ self.dimensions: List[dict] = []
170
+ self.tags: List[dict] = []
171
+ self.tag_groups: List[dict] = []
172
+ self.units: List[dict] = []
173
+ self.replacement_rules: dict = {}
174
+ self.collections: List[dict] = []
175
+ self.collection_groups: List[dict] = []
176
+ self.bulk_update_configurations: List[dict] = []
177
+ self.derived_event_types: List[dict] = []
178
+ self.global_settings: dict = {}
179
+ self.import_configurations: List[dict] = []
180
+ self.portal_configurations: List[dict] = []
181
+ # Phase 14 - Advanced Configuration & System Elements
182
+ self.keys: List[dict] = []
183
+ self.cross_contexts: List[dict] = []
184
+ self.business_libraries: List[dict] = []
185
+ self.setup_groups: List[dict] = []
186
+ self.action_sets: List[dict] = []
187
+ self.status_flags: List[dict] = []
188
+ self.integration_endpoints: List[dict] = []
189
+ return
190
+
191
+ def parse_file(self, file_path: str):
192
+ """Parse a STEP-XML file from disk.
193
+
194
+ Reads and parses an XML file, populating all entity lists from the file contents.
195
+
196
+ Args:
197
+ file_path: Path to the STEP-XML file to parse
198
+
199
+ Returns:
200
+ None (populates instance attributes with parsed data)
201
+
202
+ Raises:
203
+ FileNotFoundError: If the file does not exist
204
+ ParseError: If the XML is malformed
205
+ AssertionError: If the root element is not STEP-ProductInformation
206
+
207
+ Example:
208
+ parser = Parser()
209
+ parser.parse_file('/path/to/step_data.xml')
210
+ print(f"Parsed {len(parser.products)} products")
211
+ """
212
+ self._tree = ET.parse(file_path)
213
+ return self.parse(self._tree.getroot())
214
+
215
+ def parse_str(self, xml_str: str):
216
+ """Parse a STEP-XML document from a string.
217
+
218
+ Parses XML content provided as a string, populating all entity lists.
219
+
220
+ Args:
221
+ xml_str: XML content as a string
222
+
223
+ Returns:
224
+ None (populates instance attributes with parsed data)
225
+
226
+ Raises:
227
+ ParseError: If the XML is malformed
228
+ AssertionError: If the root element is not STEP-ProductInformation
229
+
230
+ Example:
231
+ parser = Parser()
232
+ parser.parse_str(xml_content)
233
+ assets = parser.assets
234
+ """
235
+ return self.parse(ET.fromstring(xml_str))
236
+
237
+ def parse(self, root: ET.Element):
238
+ """Parse a STEP-XML ElementTree root element.
239
+
240
+ Deserializes a STEP-XML document from an ElementTree root element, parsing all
241
+ entity types and organizing them into their respective lists.
242
+
243
+ This is the core parsing method called by parse_file() and parse_str().
244
+ It orchestrates parsing of all STEP entity types in the correct order.
245
+
246
+ Args:
247
+ root: The root ElementTree Element of a STEP-XML document
248
+
249
+ Raises:
250
+ AssertionError: If the root element is not STEP-ProductInformation
251
+
252
+ Note:
253
+ The root element must be 'STEP-ProductInformation'. Namespace handling is
254
+ automatic - the parser works with both namespaced and unnamespaced XML.
255
+ """
256
+ # Handle both namespaced and unnamespaced root elements
257
+ local_tag = _get_local_tag(root)
258
+ assert local_tag == 'STEP-ProductInformation', f"Expected STEP-ProductInformation, got {local_tag}"
259
+ self._root = root
260
+ self.assets = self._parse_item_list(
261
+ 'Assets', 'Asset', _parse_asset)
262
+ self.products = self._parse_item_list(
263
+ 'Products', 'Product', _parse_product)
264
+ self.classifications = self._parse_item_list(
265
+ 'Classifications', 'Classification', _parse_classification
266
+ )
267
+ self.entities = self._parse_item_list(
268
+ 'Entities', 'Entity', _parse_entity
269
+ )
270
+ self.attributes = self._parse_item_list(
271
+ 'AttributeList', 'Attribute', _parse_attribute
272
+ )
273
+ self.attribute_groups = self._parse_item_list(
274
+ 'AttributeGroupList', 'AttributeGroup', _parse_attribute_group
275
+ )
276
+ self.list_of_values_groups = self._parse_item_list(
277
+ 'ListOfValuesGroupList', 'ListOfValuesGroup', _parse_list_of_values_group
278
+ )
279
+ self.list_of_values = self._parse_item_list(
280
+ 'ListsOfValues', 'ListOfValue', _parse_list_of_value
281
+ )
282
+ self.user_types = self._parse_item_list(
283
+ 'UserTypes', 'UserType', _parse_user_type
284
+ )
285
+ self.contexts = self._parse_item_list(
286
+ 'ContextList', 'Context', _parse_context
287
+ )
288
+ self.link_types = self._parse_item_list(
289
+ 'EdgeTypes', 'LinkType', _parse_link_type
290
+ )
291
+ self.delete_assets = self._parse_item_list(
292
+ 'DeleteAssets', 'DeleteAsset', _parse_delete_asset
293
+ )
294
+ self.delete_products = self._parse_item_list(
295
+ 'DeleteProducts', 'DeleteProduct', _parse_delete_product
296
+ )
297
+ self.delete_classifications = self._parse_item_list(
298
+ 'DeleteClassifications', 'DeleteClassification', _parse_delete_classification
299
+ )
300
+ self.delete_entities = self._parse_item_list(
301
+ 'DeleteEntities', 'DeleteEntity', _parse_delete_entity
302
+ )
303
+ self.business_rules = self._parse_item_list(
304
+ 'BusinessRules', 'BusinessRule', _parse_business_rule
305
+ )
306
+ self.event_queues = self._parse_item_list(
307
+ 'EventQueues', 'EventQueue', _parse_event_queue
308
+ )
309
+ self.workflows = self._parse_item_list(
310
+ 'STEPWorkflows', 'STEPWorkflow', _parse_workflow
311
+ )
312
+ self.export_configurations = self._parse_item_list(
313
+ 'ExportConfigurations', 'ExportConfiguration', _parse_export_configuration
314
+ )
315
+ self.cross_reference_types = self._parse_item_list(
316
+ 'CrossReferenceTypes', 'CrossReferenceType', _parse_cross_reference_type
317
+ )
318
+ self.qualifiers = self._parse_item_list(
319
+ 'Qualifiers', 'Qualifier', _parse_qualifier
320
+ )
321
+ self.dimensions = self._parse_item_list(
322
+ 'DimensionList', 'Dimension', _parse_dimension
323
+ )
324
+ self.tags = self._parse_item_list(
325
+ 'TagList', 'Tag', _parse_tag
326
+ )
327
+ self.tag_groups = self._parse_item_list(
328
+ 'TagGroupList', 'TagGroup', _parse_tag_group
329
+ )
330
+ self.units = self._parse_item_list(
331
+ 'UnitList', 'Unit', _parse_unit
332
+ )
333
+ self.replacement_rules = self._parse_replacement_rules()
334
+ self.collections, self.collection_groups = self._parse_collections()
335
+ self.bulk_update_configurations = self._parse_item_list(
336
+ 'BulkUpdateConfigurations', 'BulkUpdateConfiguration', _parse_bulk_update_configuration
337
+ )
338
+ self.derived_event_types = self._parse_derived_event_types()
339
+ self.global_settings = self._parse_global_settings()
340
+ self.import_configurations = self._parse_item_list(
341
+ 'ImportConfigurations', 'ImportConfiguration', _parse_import_configuration
342
+ )
343
+ self.portal_configurations = self._parse_item_list(
344
+ 'PortalConfigurations', 'PortalConfiguration', _parse_portal_configuration
345
+ )
346
+ # Phase 14 - Advanced Configuration & System Elements
347
+ self.keys = self._parse_item_list(
348
+ 'Keys', 'Key', _parse_key
349
+ )
350
+ self.cross_contexts = self._parse_item_list(
351
+ 'CrossContexts', 'Context', _parse_cross_context
352
+ )
353
+ self.business_libraries = self._parse_item_list(
354
+ 'BusinessLibraries', 'BusinessRule', _parse_business_library
355
+ )
356
+ self.setup_groups = self._parse_item_list(
357
+ 'SetupGroups', 'SetupGroup', _parse_setup_group
358
+ )
359
+ self.action_sets = self._parse_item_list(
360
+ 'ActionSetList', 'ActionSet', _parse_action_set
361
+ )
362
+ self.status_flags = self._parse_item_list(
363
+ 'StatusFlags', 'StatusFlag', _parse_status_flag
364
+ )
365
+ self.integration_endpoints = self._parse_integration_endpoints()
366
+ return
367
+
368
+ def _parse_item_list(self,
369
+ list_name: str,
370
+ item_name: str,
371
+ parse_fct: Callable[[Any], Any]
372
+ ) -> List[Any]:
373
+ """Parse a list of items from the XML document.
374
+
375
+ Generic method for parsing repeating elements in the STEP-XML document.
376
+ Handles namespace-aware lookup and applies a custom parser function to each item.
377
+
378
+ Args:
379
+ list_name: XML element name containing the items (e.g., 'Products')
380
+ item_name: XML element name for individual items (e.g., 'Product')
381
+ parse_fct: Callable that parses a single item element and returns a dictionary
382
+
383
+ Returns:
384
+ List of parsed item dictionaries, empty list if the container element not found
385
+
386
+ Note:
387
+ This method automatically handles both namespaced and unnamespaced XML by:
388
+ 1. First attempting namespace-aware lookup using STEPXML_NS
389
+ 2. Falling back to local tag name matching for default namespaces
390
+ """
391
+ results: List[Any] = []
392
+ if self._root is None:
393
+ return results
394
+
395
+ # Try to find the item list by local tag name, ignoring namespace
396
+ item_list = None
397
+
398
+ # First try with explicit namespace (for prefixed XML like step:Products)
399
+ item_list = self._root.find(list_name, STEPXML_NS)
400
+
401
+ # If not found, search by local tag name (handles default namespaces)
402
+ if item_list is None:
403
+ for child in self._root:
404
+ if _get_local_tag(child) == list_name:
405
+ item_list = child
406
+ break
407
+
408
+ if item_list is not None:
409
+ # Collect items, trying both namespace-aware and namespace-agnostic approaches
410
+ items = []
411
+
412
+ # Try with explicit namespace first
413
+ items = list(item_list.findall(item_name, STEPXML_NS))
414
+
415
+ # If no results, search by local tag name for each child
416
+ if not items:
417
+ for child in item_list:
418
+ if _get_local_tag(child) == item_name:
419
+ items.append(child)
420
+
421
+ for item in items:
422
+ result = parse_fct(item)
423
+ if result is not None:
424
+ results.append(result)
425
+
426
+ return results
427
+
428
+ def _parse_replacement_rules(self) -> dict:
429
+ """Parse replacement rules for products, classifications, entities, assets, attributes, etc."""
430
+ replacement_rules_elem = None
431
+
432
+ if self._root is None:
433
+ return {}
434
+
435
+ for child in self._root:
436
+ local_tag = _get_local_tag(child)
437
+ if local_tag == "ReplacementRules":
438
+ replacement_rules_elem = child
439
+ break
440
+
441
+ if replacement_rules_elem is None:
442
+ return {}
443
+
444
+ result = {}
445
+
446
+ for child in replacement_rules_elem:
447
+ local_tag = _get_local_tag(child)
448
+
449
+ if local_tag == "Attributes":
450
+ if "Attributes" not in result:
451
+ result["Attributes"] = []
452
+ result["Attributes"].append(_parse_replace_attributes(child))
453
+
454
+ elif local_tag == "Products":
455
+ if "Products" not in result:
456
+ result["Products"] = []
457
+ result["Products"].append(_parse_replace_products(child))
458
+
459
+ elif local_tag == "Classifications":
460
+ if "Classifications" not in result:
461
+ result["Classifications"] = []
462
+ result["Classifications"].append(_parse_replace_classifications(child))
463
+
464
+ elif local_tag == "Entities":
465
+ if "Entities" not in result:
466
+ result["Entities"] = []
467
+ result["Entities"].append(_parse_replace_entities(child))
468
+
469
+ elif local_tag == "Assets":
470
+ if "Assets" not in result:
471
+ result["Assets"] = []
472
+ result["Assets"].append(_parse_replace_assets(child))
473
+
474
+ elif local_tag == "LinkTypes":
475
+ if "LinkTypes" not in result:
476
+ result["LinkTypes"] = []
477
+ result["LinkTypes"].append(_parse_replace_link_types(child))
478
+
479
+ elif local_tag == "UserTypes":
480
+ if "UserTypes" not in result:
481
+ result["UserTypes"] = []
482
+ result["UserTypes"].append(_parse_replace_user_types(child))
483
+
484
+ elif local_tag == "UserGroups":
485
+ if "UserGroups" not in result:
486
+ result["UserGroups"] = []
487
+ result["UserGroups"].append(_parse_replace_user_groups(child))
488
+
489
+ return result
490
+
491
+ def _parse_collections(self) -> tuple:
492
+ """Parse collections and collection groups, returns (collections, collection_groups)."""
493
+ collection_list_elem = None
494
+
495
+ if self._root is None:
496
+ return [], []
497
+
498
+ for child in self._root:
499
+ local_tag = _get_local_tag(child)
500
+ if local_tag == "CollectionList":
501
+ collection_list_elem = child
502
+ break
503
+
504
+ collections = []
505
+ collection_groups = []
506
+
507
+ if collection_list_elem is not None:
508
+ for child in collection_list_elem:
509
+ local_tag = _get_local_tag(child)
510
+ if local_tag == "Collection":
511
+ collections.append(_parse_collection_item(child, is_group=False))
512
+ elif local_tag == "CollectionGroup":
513
+ collection_groups.append(_parse_collection_item(child, is_group=True))
514
+
515
+ return collections, collection_groups
516
+
517
+ def _parse_derived_event_types(self) -> list:
518
+ """Parse derived event types (simple text elements)."""
519
+ result = []
520
+
521
+ if self._root is None:
522
+ return result
523
+
524
+ for child in self._root:
525
+ local_tag = _get_local_tag(child)
526
+ if local_tag == "DerivedEventTypes":
527
+ # DerivedEventTypes contains DerivedEventType text elements
528
+ for event_child in child:
529
+ event_tag = _get_local_tag(event_child)
530
+ if event_tag == "DerivedEventType":
531
+ event_text = event_child.text
532
+ if event_text:
533
+ result.append({"value": event_text})
534
+ break
535
+
536
+ return result
537
+
538
+ def _parse_global_settings(self) -> dict:
539
+ """Parse global settings (system configuration)."""
540
+ if self._root is None:
541
+ return {}
542
+
543
+ for child in self._root:
544
+ local_tag = _get_local_tag(child)
545
+ if local_tag == "GlobalSettings":
546
+ result = {}
547
+ # Parse CalculatedAttributeSettings
548
+ for settings_child in child:
549
+ settings_tag = _get_local_tag(settings_child)
550
+ if settings_tag == "CalculatedAttributeSettings":
551
+ result["CalculatedAttributeSettings"] = _parse_element(settings_child, attributes=[], texts=[])
552
+ elif settings_tag == "AssetSettings":
553
+ result["AssetSettings"] = _parse_element(settings_child, attributes=[], texts=[])
554
+ return result
555
+
556
+ return {}
557
+
558
+ def _parse_integration_endpoints(self) -> list:
559
+ """Parse integration endpoints (OutBound, InBound, Gateway variants)."""
560
+ results = []
561
+ if self._root is None:
562
+ return results
563
+
564
+ for child in self._root:
565
+ local_tag = _get_local_tag(child)
566
+ if local_tag == "IntegrationEndpoints":
567
+ for endpoint_child in child:
568
+ endpoint_tag = _get_local_tag(endpoint_child)
569
+ if endpoint_tag in ("OutBoundIntegrationEndpoint", "InBoundIntegrationEndpoint", "GatewayIntegrationEndpoint"):
570
+ endpoint_data = _parse_element(
571
+ endpoint_child,
572
+ attributes=["ID"],
573
+ texts=[]
574
+ )
575
+ endpoint_data["Type"] = endpoint_tag # Add type to distinguish variants
576
+
577
+ # Parse Name elements (multiple)
578
+ names = []
579
+ for ep_child in endpoint_child:
580
+ ep_tag = _get_local_tag(ep_child)
581
+ if ep_tag == "Name":
582
+ name_text = ep_child.text
583
+ if name_text:
584
+ names.append({"value": name_text})
585
+ if names:
586
+ endpoint_data["Name"] = names
587
+
588
+ results.append(endpoint_data)
589
+
590
+ return results
591
+
592
+
593
+ def _parse_element(
594
+ element, attributes = [], texts = [],
595
+ specials = {}, conversions={}):
596
+ result = {}
597
+ for attr_name in attributes:
598
+ result[attr_name] = element.get(attr_name)
599
+ # Apply conversions to attributes after parsing
600
+ for attr_name in attributes:
601
+ if attr_name in conversions:
602
+ conv_fct, default = conversions[attr_name]
603
+ if result[attr_name] is not None:
604
+ result[attr_name] = conv_fct(result[attr_name])
605
+ elif default is not None:
606
+ result[attr_name] = default
607
+ for child in element:
608
+ # Use local tag name to handle both namespaced and unnamespaced XML
609
+ local_tag = _get_local_tag(child)
610
+ key = local_tag
611
+
612
+ if key in texts:
613
+ result[key] = child.text
614
+ elif key in specials:
615
+ result[key] = specials[key](child)
616
+ # Apply conversions to child elements
617
+ if key in conversions:
618
+ conv_fct, default = conversions[key]
619
+ if result.get(key):
620
+ result[key] = conv_fct(result[key])
621
+ elif default is not None:
622
+ result[key] = default
623
+ return result
624
+
625
+ def _parse_leaf_element(element, name=None, attributes = []):
626
+ result = {}
627
+ for attr_name in attributes:
628
+ result[attr_name] = element.get(attr_name)
629
+ if name is None:
630
+ name = element.tag
631
+ result[name] = element.text
632
+ return result
633
+
634
+
635
+ def _parse_asset(element):
636
+ return _parse_element(
637
+ element,
638
+ attributes=("ID",),
639
+ texts= ('Name', 'KeyValue'),
640
+ specials= {"Values" : _parse_values}
641
+ )
642
+
643
+ def _parse_delete_asset(element):
644
+ return _parse_element(
645
+ element,
646
+ attributes=("ID",)
647
+ )
648
+
649
+ def _parse_product(element):
650
+ """Parse Product element with support for multiple cross-references.
651
+
652
+ Handles:
653
+ - Attributes: ID, ParentId, UserTypeID
654
+ - Child: Name (text)
655
+ - Child: Values (complex)
656
+ - Children: Multiple AssetCrossReference, ProductCrossReference, etc.
657
+ """
658
+ result = _parse_element(
659
+ element,
660
+ attributes = ("ID", "ParentId", "UserTypeID"),
661
+ texts = ("Name",),
662
+ specials = {
663
+ "Values": _parse_values,
664
+ }
665
+ )
666
+
667
+ # Handle cross-references that can appear multiple times
668
+ cross_ref_types = {
669
+ "AssetCrossReference": _parse_asset_reference,
670
+ "DeleteAssetCrossReference": _parse_asset_reference,
671
+ "ProductCrossReference": _parse_product_cross_reference,
672
+ "DeleteProductCrossReference": _parse_product_cross_reference,
673
+ "ClassificationReference": _parse_classification_reference,
674
+ "DeleteClassificationReference": _parse_classification_reference,
675
+ "ClassificationCrossReference": _parse_classification_reference,
676
+ "DeleteClassificationCrossReference": _parse_classification_reference,
677
+ "EntityCrossReference": _parse_entity_cross_reference,
678
+ "DeleteEntityCrossReference": _parse_entity_cross_reference,
679
+ }
680
+
681
+ # Process cross-references, aggregating multiples into lists
682
+ for child in element:
683
+ # Use local tag name to handle both namespaced and unnamespaced XML
684
+ local_tag = _get_local_tag(child)
685
+ if local_tag in cross_ref_types:
686
+ parser_func = cross_ref_types[local_tag]
687
+ parsed = parser_func(child)
688
+
689
+ if local_tag in result:
690
+ # Multiple elements of same type - convert to list
691
+ if not isinstance(result[local_tag], list):
692
+ result[local_tag] = [result[local_tag]]
693
+ result[local_tag].append(parsed)
694
+ else:
695
+ # First element of this type
696
+ result[local_tag] = parsed
697
+
698
+ return result
699
+
700
+ def _parse_delete_product(element):
701
+ return _parse_element(
702
+ element,
703
+ attributes=("ID",)
704
+ )
705
+
706
+ def _parse_values(element):
707
+ values = {}
708
+ for child in element:
709
+ attribute_id = child.get('AttributeID')
710
+ if attribute_id is not None:
711
+ local_tag = _get_local_tag(child)
712
+ if local_tag == "Value":
713
+ values[attribute_id] = _parse_value(child)
714
+ elif local_tag == "MultiValue":
715
+ multi_value_list = []
716
+ for sub_child in child.findall('Value'):
717
+ multi_value_list.append(_parse_value(sub_child))
718
+ # Also check for namespaced Value elements
719
+ for sub_child in child.findall('{http://www.stibosystems.com/step}Value'):
720
+ multi_value_list.append(_parse_value(sub_child))
721
+ if multi_value_list:
722
+ values[attribute_id] = multi_value_list
723
+ return values
724
+
725
+ def _parse_value(element):
726
+ return _parse_leaf_element(
727
+ element,
728
+ name = "value",
729
+ attributes = ("ID",))
730
+
731
+ def _parse_asset_reference(element):
732
+ """Parse AssetCrossReference or DeleteAssetCrossReference elements.
733
+
734
+ Handles:
735
+ - Attributes: AssetID, Type, Changed, QualifierID
736
+ - Children: KeyValue (multiple), MetaData
737
+ """
738
+ result = _parse_element(
739
+ element,
740
+ attributes=("AssetID", "Changed", "Type", "QualifierID"),
741
+ specials={"MetaData": _parse_metadata},
742
+ conversions={"Changed": (_str_to_bool, False)}
743
+ )
744
+
745
+ # Parse KeyValue elements (can be multiple)
746
+ key_values = []
747
+ for child in element:
748
+ if child.tag == "KeyValue":
749
+ key_values.append(_parse_key_value(child))
750
+ if key_values:
751
+ result["KeyValue"] = key_values
752
+
753
+ return result
754
+
755
+
756
+ def _parse_product_cross_reference(element):
757
+ """Parse ProductCrossReference or DeleteProductCrossReference elements.
758
+
759
+ Handles:
760
+ - Attributes: ProductID, Type, Changed, QualifierID, MountLocation
761
+ - Children: KeyValue (multiple), MetaData
762
+ """
763
+ result = _parse_element(
764
+ element,
765
+ attributes=("ProductID", "Changed", "Type", "QualifierID", "MountLocation"),
766
+ specials={"MetaData": _parse_metadata},
767
+ conversions={"Changed": (_str_to_bool, False)}
768
+ )
769
+
770
+ # Parse KeyValue elements (can be multiple)
771
+ key_values = []
772
+ for child in element:
773
+ if child.tag == "KeyValue":
774
+ key_values.append(_parse_key_value(child))
775
+ if key_values:
776
+ result["KeyValue"] = key_values
777
+
778
+ return result
779
+
780
+
781
+ def _parse_classification_reference(element):
782
+ """Parse ClassificationReference or DeleteClassificationReference elements.
783
+
784
+ Handles:
785
+ - Attributes: ClassificationID, Type, Changed, QualifierID
786
+ - Children: KeyValue (multiple), MetaData
787
+ """
788
+ result = _parse_element(
789
+ element,
790
+ attributes=("ClassificationID", "Changed", "Type", "QualifierID"),
791
+ specials={"MetaData": _parse_metadata},
792
+ conversions={"Changed": (_str_to_bool, False)}
793
+ )
794
+
795
+ # Parse KeyValue elements (can be multiple)
796
+ key_values = []
797
+ for child in element:
798
+ if child.tag == "KeyValue":
799
+ key_values.append(_parse_key_value(child))
800
+ if key_values:
801
+ result["KeyValue"] = key_values
802
+
803
+ return result
804
+
805
+
806
+ def _parse_entity_cross_reference(element):
807
+ """Parse EntityCrossReference or DeleteEntityCrossReference elements.
808
+
809
+ Handles:
810
+ - Attributes: EntityID, Type, Changed, QualifierID
811
+ - Children: KeyValue (multiple), MetaData
812
+ """
813
+ result = _parse_element(
814
+ element,
815
+ attributes=("EntityID", "Changed", "Type", "QualifierID"),
816
+ specials={"MetaData": _parse_metadata},
817
+ conversions={"Changed": (_str_to_bool, False)}
818
+ )
819
+
820
+ # Parse KeyValue elements (can be multiple)
821
+ key_values = []
822
+ for child in element:
823
+ if child.tag == "KeyValue":
824
+ key_values.append(_parse_key_value(child))
825
+ if key_values:
826
+ result["KeyValue"] = key_values
827
+
828
+ return result
829
+
830
+
831
+ def _parse_key_value(element):
832
+ """Parse KeyValue element used for alternate object identification.
833
+
834
+ Returns dict with KeyID attribute and text value.
835
+ """
836
+ return {
837
+ "KeyID": element.get("KeyID"),
838
+ "value": element.text
839
+ }
840
+
841
+
842
+ def _parse_metadata(element):
843
+ """Parse MetaData element with nested Value/MultiValue/ValueGroup.
844
+
845
+ Returns dict with attribute IDs as keys and values.
846
+ """
847
+ metadata = {}
848
+ for child in element:
849
+ attribute_id = child.get('AttributeID')
850
+ if attribute_id is not None:
851
+ if child.tag == "Value":
852
+ metadata[attribute_id] = _parse_value(child)
853
+ elif child.tag == "MultiValue":
854
+ multi_value_list = []
855
+ for sub_child in child.findall('Value', STEPXML_NS):
856
+ multi_value_list.append(_parse_value(sub_child))
857
+ if multi_value_list:
858
+ metadata[attribute_id] = multi_value_list
859
+ return metadata if metadata else None
860
+
861
+
862
+ def _parse_classification(element):
863
+ """Parse Classification element with support for nested classifications and cross-references.
864
+
865
+ Handles:
866
+ - Attributes: ID, ParentID, UserTypeID, QualifierID, Changed, Republished, Selected, Referenced
867
+ - Children: KeyValue (multiple), ParentKeyValue (multiple)
868
+ - Children: Name (text), nested Classification (hierarchical)
869
+ - Cross-references: ProductReference, ProductCrossReference (suppressed/delete variants)
870
+ - Cross-references: ClassificationCrossReference (classification-to-classification)
871
+ - Cross-references: EntityCrossReference
872
+ - Cross-references: AssetCrossReference
873
+ - Other: MetaData, AttributeLink, DataContainerTypeLink
874
+ """
875
+ result = _parse_element(
876
+ element,
877
+ attributes=("ID", "ParentID", "UserTypeID", "QualifierID", "Changed", "Republished", "Selected", "Referenced"),
878
+ texts=("Name",),
879
+ conversions={
880
+ "Changed": (_str_to_bool, False),
881
+ "Republished": (_str_to_bool, False),
882
+ "Selected": (_str_to_bool, True),
883
+ "Referenced": (_str_to_bool, False)
884
+ }
885
+ )
886
+
887
+ # Parse KeyValue and ParentKeyValue elements (can be multiple)
888
+ key_values = []
889
+ parent_key_values = []
890
+ nested_classifications = []
891
+
892
+ # Define all cross-reference types supported by classifications
893
+ cross_ref_types = {
894
+ "ProductReference": _parse_product_cross_reference,
895
+ "DeleteProductReference": _parse_product_cross_reference,
896
+ "SuppressedProductReference": _parse_product_cross_reference,
897
+ "DeleteSuppressedProductReference": _parse_product_cross_reference,
898
+ "ProductCrossReference": _parse_product_cross_reference,
899
+ "DeleteProductCrossReference": _parse_product_cross_reference,
900
+ "SuppressedProductCrossReference": _parse_product_cross_reference,
901
+ "DeleteSuppressedProductCrossReference": _parse_product_cross_reference,
902
+ "ClassificationCrossReference": _parse_classification_reference,
903
+ "DeleteClassificationCrossReference": _parse_classification_reference,
904
+ "SuppressedClassificationCrossReference": _parse_classification_reference,
905
+ "DeleteSuppressedClassificationCrossReference": _parse_classification_reference,
906
+ "EntityCrossReference": _parse_entity_cross_reference,
907
+ "DeleteEntityCrossReference": _parse_entity_cross_reference,
908
+ "SuppressedEntityCrossReference": _parse_entity_cross_reference,
909
+ "DeleteSuppressedEntityCrossReference": _parse_entity_cross_reference,
910
+ "AssetCrossReference": _parse_asset_reference,
911
+ "DeleteAssetCrossReference": _parse_asset_reference,
912
+ "SuppressedAssetCrossReference": _parse_asset_reference,
913
+ "DeleteSuppressedAssetCrossReference": _parse_asset_reference,
914
+ }
915
+
916
+ # Process child elements, handling special cases
917
+ for child in element:
918
+ if child.tag == "KeyValue":
919
+ key_values.append(_parse_key_value(child))
920
+ elif child.tag == "ParentKeyValue":
921
+ parent_key_values.append(_parse_key_value(child))
922
+ elif child.tag == "Classification":
923
+ # Nested classifications (hierarchical)
924
+ nested_classifications.append(_parse_classification(child))
925
+ elif child.tag == "MetaData":
926
+ result["MetaData"] = _parse_metadata(child)
927
+ elif child.tag in cross_ref_types:
928
+ # Cross-references - aggregate multiple into lists
929
+ parser_func = cross_ref_types[child.tag]
930
+ parsed = parser_func(child)
931
+
932
+ if child.tag in result:
933
+ # Multiple elements of same type - convert to list
934
+ if not isinstance(result[child.tag], list):
935
+ result[child.tag] = [result[child.tag]]
936
+ result[child.tag].append(parsed)
937
+ else:
938
+ # First element of this type
939
+ result[child.tag] = parsed
940
+
941
+ # Add aggregated lists to result if present
942
+ if key_values:
943
+ result["KeyValue"] = key_values
944
+ if parent_key_values:
945
+ result["ParentKeyValue"] = parent_key_values
946
+ if nested_classifications:
947
+ result["Classification"] = nested_classifications
948
+
949
+ return result
950
+
951
+ def _parse_entity(element):
952
+ """Parse Entity element with support for nested entities and cross-references.
953
+
954
+ Handles:
955
+ - Attributes: ID, ParentID, UserTypeID, QualifierID, Changed, Republished, Selected, Referenced
956
+ - Children: KeyValue (multiple), ParentKeyValue (multiple), Name (text)
957
+ - Children: Nested Entity (hierarchical)
958
+ - Cross-references: ProductCrossReference (suppressed/delete variants)
959
+ - Cross-references: ClassificationCrossReference (suppressed/delete variants)
960
+ - Cross-references: EntityCrossReference (entity-to-entity)
961
+ - Cross-references: AssetCrossReference (suppressed/delete variants)
962
+ - Cross-references: ContextCrossReference, WorkspaceCrossReference
963
+ - Other: MetaData, Values, DataContainers
964
+ """
965
+ result = _parse_element(
966
+ element,
967
+ attributes=("ID", "ParentID", "UserTypeID", "QualifierID", "Changed", "Republished", "Selected", "Referenced"),
968
+ texts=("Name",),
969
+ conversions={
970
+ "Changed": (_str_to_bool, False),
971
+ "Republished": (_str_to_bool, False),
972
+ "Selected": (_str_to_bool, True),
973
+ "Referenced": (_str_to_bool, False)
974
+ }
975
+ )
976
+
977
+ # Parse KeyValue and ParentKeyValue elements (can be multiple)
978
+ key_values = []
979
+ parent_key_values = []
980
+ nested_entities = []
981
+
982
+ # Define all cross-reference types supported by entities
983
+ cross_ref_types = {
984
+ "ProductCrossReference": _parse_product_cross_reference,
985
+ "DeleteProductCrossReference": _parse_product_cross_reference,
986
+ "SuppressedProductCrossReference": _parse_product_cross_reference,
987
+ "DeleteSuppressedProductCrossReference": _parse_product_cross_reference,
988
+ "ClassificationCrossReference": _parse_classification_reference,
989
+ "DeleteClassificationCrossReference": _parse_classification_reference,
990
+ "SuppressedClassificationCrossReference": _parse_classification_reference,
991
+ "DeleteSuppressedClassificationCrossReference": _parse_classification_reference,
992
+ "EntityCrossReference": _parse_entity_cross_reference,
993
+ "DeleteEntityCrossReference": _parse_entity_cross_reference,
994
+ "SuppressedEntityCrossReference": _parse_entity_cross_reference,
995
+ "DeleteSuppressedEntityCrossReference": _parse_entity_cross_reference,
996
+ "AssetCrossReference": _parse_asset_reference,
997
+ "DeleteAssetCrossReference": _parse_asset_reference,
998
+ "SuppressedAssetCrossReference": _parse_asset_reference,
999
+ "DeleteSuppressedAssetCrossReference": _parse_asset_reference,
1000
+ "ContextCrossReference": _parse_context_cross_reference,
1001
+ "SuppressedContextCrossReference": _parse_context_cross_reference,
1002
+ "WorkspaceCrossReference": _parse_workspace_cross_reference,
1003
+ "SuppressedWorkspaceCrossReference": _parse_workspace_cross_reference,
1004
+ }
1005
+
1006
+ # Process child elements, handling special cases
1007
+ for child in element:
1008
+ if child.tag == "KeyValue":
1009
+ key_values.append(_parse_key_value(child))
1010
+ elif child.tag == "ParentKeyValue":
1011
+ parent_key_values.append(_parse_key_value(child))
1012
+ elif child.tag == "Entity":
1013
+ # Nested entities (hierarchical)
1014
+ nested_entities.append(_parse_entity(child))
1015
+ elif child.tag == "MetaData":
1016
+ result["MetaData"] = _parse_metadata(child)
1017
+ elif child.tag in cross_ref_types:
1018
+ # Cross-references - aggregate multiple into lists
1019
+ parser_func = cross_ref_types[child.tag]
1020
+ parsed = parser_func(child)
1021
+
1022
+ if child.tag in result:
1023
+ # Multiple elements of same type - convert to list
1024
+ if not isinstance(result[child.tag], list):
1025
+ result[child.tag] = [result[child.tag]]
1026
+ result[child.tag].append(parsed)
1027
+ else:
1028
+ # First element of this type
1029
+ result[child.tag] = parsed
1030
+
1031
+ # Add aggregated lists to result if present
1032
+ if key_values:
1033
+ result["KeyValue"] = key_values
1034
+ if parent_key_values:
1035
+ result["ParentKeyValue"] = parent_key_values
1036
+ if nested_entities:
1037
+ result["Entity"] = nested_entities
1038
+
1039
+ return result
1040
+
1041
+
1042
+ def _parse_context_cross_reference(element):
1043
+ """Parse ContextCrossReference or SuppressedContextCrossReference elements.
1044
+
1045
+ Handles:
1046
+ - Attributes: ContextID, Type, QualifierID
1047
+ - Children: KeyValue (multiple), MetaData
1048
+ """
1049
+ result = _parse_element(
1050
+ element,
1051
+ attributes=("ContextID", "Type", "QualifierID"),
1052
+ specials={"MetaData": _parse_metadata}
1053
+ )
1054
+
1055
+ # Parse KeyValue elements (can be multiple)
1056
+ key_values = []
1057
+ for child in element:
1058
+ if child.tag == "KeyValue":
1059
+ key_values.append(_parse_key_value(child))
1060
+ if key_values:
1061
+ result["KeyValue"] = key_values
1062
+
1063
+ return result
1064
+
1065
+
1066
+ def _parse_workspace_cross_reference(element):
1067
+ """Parse WorkspaceCrossReference or SuppressedWorkspaceCrossReference elements.
1068
+
1069
+ Handles:
1070
+ - Attributes: WorkspaceID, Type, QualifierID
1071
+ - Children: KeyValue (multiple), MetaData
1072
+ """
1073
+ result = _parse_element(
1074
+ element,
1075
+ attributes=("WorkspaceID", "Type", "QualifierID"),
1076
+ specials={"MetaData": _parse_metadata}
1077
+ )
1078
+
1079
+ # Parse KeyValue elements (can be multiple)
1080
+ key_values = []
1081
+ for child in element:
1082
+ if child.tag == "KeyValue":
1083
+ key_values.append(_parse_key_value(child))
1084
+ if key_values:
1085
+ result["KeyValue"] = key_values
1086
+
1087
+ return result
1088
+
1089
+
1090
+ def _parse_attribute(element: ET.Element) -> dict:
1091
+ """Parse Attribute element with ID, names, validation/LOV link, and metadata.
1092
+
1093
+ Attributes:
1094
+ - ID, DefaultUnitID, ProductMode (Normal/Property), CryptographicKeyID
1095
+ - HierarchicalFiltering, ClassificationHierarchicalFiltering, ExternallyMaintained
1096
+ - FullTextIndexed, MultiValued, Mandatory, Derived
1097
+ - Selected (default True), Referenced (default False), Changed, Deleted
1098
+
1099
+ Children:
1100
+ - Name (text), Validation OR ListOfValueLink
1101
+ - ValueTemplate/UnitTemplate (multiple), DimensionLink (multiple)
1102
+ - MetaData, AttributeGroupLink (multiple), UserTypeLink (multiple)
1103
+ - LinkType (multiple), ValueFilter, CurrentTasks
1104
+ """
1105
+ result = _parse_element(
1106
+ element,
1107
+ attributes=("ID", "DefaultUnitID", "ProductMode", "CryptographicKeyID",
1108
+ "HierarchicalFiltering", "ClassificationHierarchicalFiltering",
1109
+ "ExternallyMaintained", "FullTextIndexed", "MultiValued",
1110
+ "Mandatory", "Derived", "Selected", "Referenced", "Changed", "Deleted"),
1111
+ texts=("Name",),
1112
+ conversions={
1113
+ "HierarchicalFiltering": (_str_to_bool, False),
1114
+ "ClassificationHierarchicalFiltering": (_str_to_bool, False),
1115
+ "ExternallyMaintained": (_str_to_bool, False),
1116
+ "FullTextIndexed": (_str_to_bool, False),
1117
+ "MultiValued": (_str_to_bool, False),
1118
+ "Mandatory": (_str_to_bool, False),
1119
+ "Derived": (_str_to_bool, False),
1120
+ "Selected": (_str_to_bool, True),
1121
+ "Referenced": (_str_to_bool, False),
1122
+ "Changed": (_str_to_bool, False),
1123
+ "Deleted": (_str_to_bool, False),
1124
+ }
1125
+ )
1126
+
1127
+ # Parse Validation or ListOfValueLink (choice)
1128
+ for child in element:
1129
+ if child.tag.endswith("Validation"):
1130
+ result["Validation"] = child.attrib.get("Base", "")
1131
+ elif child.tag.endswith("ListOfValueLink"):
1132
+ result["ListOfValueLink"] = child.attrib.get("LOVID", "")
1133
+
1134
+ # Parse DimensionLink elements (multiple)
1135
+ dim_links = []
1136
+ for child in element:
1137
+ if child.tag.endswith("DimensionLink"):
1138
+ dim_links.append(child.attrib.get("DimensionID", ""))
1139
+ if dim_links:
1140
+ result["DimensionLink"] = dim_links
1141
+
1142
+ # Parse MetaData
1143
+ for child in element:
1144
+ if child.tag.endswith("MetaData"):
1145
+ result["MetaData"] = _parse_metadata(child)
1146
+ break
1147
+
1148
+ # Parse AttributeGroupLink elements (multiple)
1149
+ attr_group_links = []
1150
+ for child in element:
1151
+ if child.tag.endswith("AttributeGroupLink"):
1152
+ attr_group_links.append(child.attrib.get("AttributeGroupID", ""))
1153
+ if attr_group_links:
1154
+ result["AttributeGroupLink"] = attr_group_links
1155
+
1156
+ # Parse UserTypeLink elements (multiple)
1157
+ user_type_links = []
1158
+ for child in element:
1159
+ if child.tag.endswith("UserTypeLink"):
1160
+ user_type_links.append(child.attrib.get("UserTypeID", ""))
1161
+ if user_type_links:
1162
+ result["UserTypeLink"] = user_type_links
1163
+
1164
+ # Parse LinkType elements (multiple)
1165
+ link_types = []
1166
+ for child in element:
1167
+ if child.tag.endswith("LinkType"):
1168
+ link_types.append(child.attrib.get("ID", ""))
1169
+ if link_types:
1170
+ result["LinkType"] = link_types
1171
+
1172
+ return result
1173
+
1174
+
1175
+ def _parse_attribute_group(element: ET.Element) -> dict:
1176
+ """Parse AttributeGroup element with ID, ParentID, names, and nested groups.
1177
+
1178
+ Attributes:
1179
+ - ID, ParentID
1180
+ - ShowInWorkbench (default True), ExcludeFromProfiling
1181
+ - Referenced (default False), ManuallySorted (default False)
1182
+ - Selected (default True), Changed (default False), Deleted (default False)
1183
+
1184
+ Children:
1185
+ - Name (text), MetaData, Configuration
1186
+ - AttributeSequence, nested AttributeGroup (multiple)
1187
+ """
1188
+ result = _parse_element(
1189
+ element,
1190
+ attributes=("ID", "ParentID", "ShowInWorkbench", "ExcludeFromProfiling",
1191
+ "Referenced", "ManuallySorted", "Selected", "Changed", "Deleted"),
1192
+ texts=("Name",),
1193
+ conversions={
1194
+ "ShowInWorkbench": (_str_to_bool, True),
1195
+ "ExcludeFromProfiling": (_str_to_bool, False),
1196
+ "Referenced": (_str_to_bool, False),
1197
+ "ManuallySorted": (_str_to_bool, False),
1198
+ "Selected": (_str_to_bool, True),
1199
+ "Changed": (_str_to_bool, False),
1200
+ "Deleted": (_str_to_bool, False),
1201
+ }
1202
+ )
1203
+
1204
+ # Parse MetaData
1205
+ for child in element:
1206
+ if child.tag.endswith("MetaData"):
1207
+ result["MetaData"] = _parse_metadata(child)
1208
+ break
1209
+
1210
+ # Parse Configuration (encoded view definition)
1211
+ for child in element:
1212
+ if child.tag.endswith("Configuration") and child.text:
1213
+ result["Configuration"] = child.text
1214
+ break
1215
+
1216
+ # Parse nested AttributeGroup elements (hierarchical)
1217
+ nested_groups = []
1218
+ for child in element:
1219
+ if child.tag.endswith("AttributeGroup"):
1220
+ nested_groups.append(_parse_attribute_group(child))
1221
+ if nested_groups:
1222
+ result["AttributeGroup"] = nested_groups
1223
+
1224
+ return result
1225
+
1226
+
1227
+ def _parse_list_of_values_group(element: ET.Element) -> dict:
1228
+ """Parse ListOfValuesGroup element with ID, ParentID, names, and nested groups.
1229
+
1230
+ Attributes:
1231
+ - ID, ParentID
1232
+ - Selected (default True), Referenced (default False), Changed (default False)
1233
+
1234
+ Children:
1235
+ - Name (text), MetaData
1236
+ - AttributeGroupLink (multiple), nested ListOfValuesGroup (multiple)
1237
+ """
1238
+ result = _parse_element(
1239
+ element,
1240
+ attributes=("ID", "ParentID", "Selected", "Referenced", "Changed"),
1241
+ texts=("Name",),
1242
+ conversions={
1243
+ "Selected": (_str_to_bool, True),
1244
+ "Referenced": (_str_to_bool, False),
1245
+ "Changed": (_str_to_bool, False),
1246
+ }
1247
+ )
1248
+
1249
+ # Parse MetaData
1250
+ for child in element:
1251
+ if child.tag.endswith("MetaData"):
1252
+ result["MetaData"] = _parse_metadata(child)
1253
+ break
1254
+
1255
+ # Parse AttributeGroupLink elements (multiple)
1256
+ attr_group_links = []
1257
+ for child in element:
1258
+ if child.tag.endswith("AttributeGroupLink"):
1259
+ attr_group_links.append(child.attrib.get("AttributeGroupID", ""))
1260
+ if attr_group_links:
1261
+ result["AttributeGroupLink"] = attr_group_links
1262
+
1263
+ # Parse nested ListOfValuesGroup elements (hierarchical)
1264
+ nested_groups = []
1265
+ for child in element:
1266
+ if child.tag.endswith("ListOfValuesGroup"):
1267
+ nested_groups.append(_parse_list_of_values_group(child))
1268
+ if nested_groups:
1269
+ result["ListOfValuesGroup"] = nested_groups
1270
+
1271
+ return result
1272
+
1273
+
1274
+ def _parse_list_of_value(element: ET.Element) -> dict:
1275
+ """Parse ListOfValue element with ID, ParentID, validation, and value items.
1276
+
1277
+ Attributes:
1278
+ - ID, ParentID
1279
+ - AllowUserValueAddition (default: False), UseValueID (default: False)
1280
+ - IDPattern, ReplaceListOfValuesValues (default: False), DefaultUnitID
1281
+
1282
+ Children:
1283
+ - Name (text, multiple), Validation
1284
+ - DimensionLink (multiple), MetaData, AttributeGroupLink (multiple)
1285
+ - Value (multiple), ValueGroup (multiple) - choice
1286
+ - DeleteValue (multiple)
1287
+ """
1288
+ result = _parse_element(
1289
+ element,
1290
+ attributes=("ID", "ParentID", "AllowUserValueAddition", "UseValueID",
1291
+ "IDPattern", "ReplaceListOfValuesValues", "DefaultUnitID"),
1292
+ texts=("Name",),
1293
+ conversions={
1294
+ "AllowUserValueAddition": (_str_to_bool, False),
1295
+ "UseValueID": (_str_to_bool, False),
1296
+ "ReplaceListOfValuesValues": (_str_to_bool, False),
1297
+ }
1298
+ )
1299
+
1300
+ # Parse Validation element
1301
+ validation_elem = None
1302
+ for child in element:
1303
+ if child.tag.endswith("Validation"):
1304
+ result["Validation"] = child.attrib.get("Base", "")
1305
+ validation_elem = child
1306
+ break
1307
+
1308
+ # Parse DimensionLink elements (multiple)
1309
+ dim_links = []
1310
+ for child in element:
1311
+ if child.tag.endswith("DimensionLink"):
1312
+ dim_links.append(child.attrib.get("DimensionID", ""))
1313
+ if dim_links:
1314
+ result["DimensionLink"] = dim_links
1315
+
1316
+ # Parse MetaData
1317
+ metadata_elem = None
1318
+ for child in element:
1319
+ if child.tag.endswith("MetaData"):
1320
+ result["MetaData"] = _parse_metadata(child)
1321
+ metadata_elem = child
1322
+ break
1323
+
1324
+ # Parse AttributeGroupLink elements (multiple)
1325
+ attr_group_links = []
1326
+ for child in element:
1327
+ if child.tag.endswith("AttributeGroupLink"):
1328
+ attr_group_links.append(child.attrib.get("AttributeGroupID", ""))
1329
+ if attr_group_links:
1330
+ result["AttributeGroupLink"] = attr_group_links
1331
+
1332
+ # Parse Value and ValueGroup elements (choice - not from MetaData or other containers)
1333
+ # We need to exclude Value elements that are direct children of containers like MetaData
1334
+ values = []
1335
+ value_groups = []
1336
+ for child in element:
1337
+ # Skip elements we've already processed above
1338
+ if child.tag.endswith(("Validation", "DimensionLink", "MetaData", "AttributeGroupLink",
1339
+ "Name", "DeleteValue")):
1340
+ continue
1341
+
1342
+ # Now parse Value and ValueGroup from direct children only
1343
+ if child.tag.endswith("Value"):
1344
+ values.append(_parse_list_of_value_item(child))
1345
+ elif child.tag.endswith("ValueGroup"):
1346
+ value_groups.append(_parse_value_group(child))
1347
+
1348
+ if values:
1349
+ result["Value"] = values
1350
+ if value_groups:
1351
+ result["ValueGroup"] = value_groups
1352
+
1353
+ # Parse DeleteValue elements (multiple)
1354
+ delete_values = []
1355
+ for child in element:
1356
+ if child.tag.endswith("DeleteValue"):
1357
+ delete_values.append(_parse_delete_value(child))
1358
+ if delete_values:
1359
+ result["DeleteValue"] = delete_values
1360
+
1361
+ return result
1362
+
1363
+
1364
+ def _parse_list_of_value_item(element: ET.Element) -> dict:
1365
+ """Parse Value element within a LOV.
1366
+
1367
+ Attributes:
1368
+ - ID (value ID), UnitID, UnitName
1369
+ - AttributeID, LOVQualifierID, QualifierID
1370
+ - Changed (default: False), Derived (default: False)
1371
+ - DerivedContextID, InheritedFrom, Inherited
1372
+
1373
+ Text Content:
1374
+ - The actual value (mixed content)
1375
+ """
1376
+ result = _parse_element(
1377
+ element,
1378
+ attributes=("ID", "UnitID", "UnitName", "AttributeID", "LOVQualifierID",
1379
+ "QualifierID", "DerivedContextID", "InheritedFrom", "Inherited"),
1380
+ conversions={
1381
+ "Changed": (_str_to_bool, False),
1382
+ "Derived": (_str_to_bool, False),
1383
+ }
1384
+ )
1385
+
1386
+ # Capture text content (mixed type element)
1387
+ if element.text and element.text.strip():
1388
+ result["value"] = element.text.strip()
1389
+
1390
+ return result
1391
+
1392
+
1393
+ def _parse_value_group(element: ET.Element) -> dict:
1394
+ """Parse ValueGroup element (container for qualifier-specific values).
1395
+
1396
+ Attributes:
1397
+ - ID (group ID)
1398
+ - Changed (default: False), Inherited, AttributeID
1399
+
1400
+ Children:
1401
+ - Value (multiple) - values within this group
1402
+ """
1403
+ result = _parse_element(
1404
+ element,
1405
+ attributes=("ID", "Inherited", "AttributeID"),
1406
+ conversions={
1407
+ "Changed": (_str_to_bool, False),
1408
+ }
1409
+ )
1410
+
1411
+ # Parse Value elements (multiple within the group)
1412
+ values = []
1413
+ for child in element:
1414
+ if child.tag.endswith("Value"):
1415
+ values.append(_parse_list_of_value_item(child))
1416
+
1417
+ if values:
1418
+ result["Value"] = values
1419
+
1420
+ return result
1421
+
1422
+
1423
+ def _parse_delete_value(element: ET.Element) -> dict:
1424
+ """Parse DeleteValue element (deletion instruction for LOV value).
1425
+
1426
+ Attributes:
1427
+ - Force (default: False) - delete despite usage
1428
+ - GenerateEvents (default: False) - generate events during deletion
1429
+
1430
+ Text Content:
1431
+ - The value ID to delete (mixed content)
1432
+ """
1433
+ result = _parse_element(
1434
+ element,
1435
+ attributes=(),
1436
+ conversions={
1437
+ "Force": (_str_to_bool, False),
1438
+ "GenerateEvents": (_str_to_bool, False),
1439
+ }
1440
+ )
1441
+
1442
+ # Parse boolean attributes manually since they're not in attributes tuple
1443
+ result["Force"] = _str_to_bool(element.attrib.get("Force", "false"))
1444
+ result["GenerateEvents"] = _str_to_bool(element.attrib.get("GenerateEvents", "false"))
1445
+
1446
+ # Capture text content (value ID to delete)
1447
+ if element.text and element.text.strip():
1448
+ result["value"] = element.text.strip()
1449
+
1450
+ return result
1451
+
1452
+
1453
+ def _parse_user_type(element: ET.Element) -> dict:
1454
+ """Parse UserType element (object type definition).
1455
+
1456
+ Attributes (15 total):
1457
+ - ID (required): Object type identifier
1458
+ - AllowOverlappingTerms, AllowQuarkTemplate, AllowInDesignTemplate (optional booleans)
1459
+ - Revisability (optional enum)
1460
+ - IsCategory (optional boolean, default False)
1461
+ - DefaultProfileConfiguration, NamePattern, IDPattern (optional strings)
1462
+ - ClassificationOwnsProductLinks (optional boolean)
1463
+ - ManuallySorted (optional boolean, default False)
1464
+ - ReferenceTargetLockPolicy (optional enum)
1465
+ - Selected (optional boolean, default True)
1466
+ - Referenced (optional boolean, default False)
1467
+ - Changed (optional boolean, default False)
1468
+ - Deleted (optional boolean, default False)
1469
+
1470
+ Child Elements:
1471
+ - Name (text, multiple) - UserType display name
1472
+ - Icon (base64) - Custom icon
1473
+ - VisibleTermFields (element) - Term configuration
1474
+ - MetaData (element) - Metadata
1475
+ - DimensionLink (multiple) - Associated dimensions
1476
+ - UserTypeLink (multiple) - Parent object type links
1477
+ - AttributeLink (multiple) - Valid attributes for this type
1478
+ - Validation (element) - Validation rules
1479
+ - MIMEType (multiple) - Supported MIME types
1480
+ """
1481
+ result = _parse_element(
1482
+ element,
1483
+ attributes=("ID", "AllowOverlappingTerms", "AllowQuarkTemplate",
1484
+ "AllowInDesignTemplate", "Revisability", "IsCategory",
1485
+ "DefaultProfileConfiguration", "ClassificationOwnsProductLinks",
1486
+ "NamePattern", "IDPattern", "ManuallySorted",
1487
+ "ReferenceTargetLockPolicy", "Selected", "Referenced",
1488
+ "Changed", "Deleted"),
1489
+ specials={
1490
+ "MetaData": _parse_metadata,
1491
+ "Validation": lambda elem: elem.attrib.get("Base", "")
1492
+ },
1493
+ conversions={
1494
+ "AllowOverlappingTerms": (_str_to_bool, False),
1495
+ "AllowQuarkTemplate": (_str_to_bool, False),
1496
+ "AllowInDesignTemplate": (_str_to_bool, False),
1497
+ "IsCategory": (_str_to_bool, False),
1498
+ "ClassificationOwnsProductLinks": (_str_to_bool, False),
1499
+ "ManuallySorted": (_str_to_bool, False),
1500
+ "Selected": (_str_to_bool, True),
1501
+ "Referenced": (_str_to_bool, False),
1502
+ "Changed": (_str_to_bool, False),
1503
+ "Deleted": (_str_to_bool, False),
1504
+ }
1505
+ )
1506
+
1507
+ # Parse Name elements (text, multiple)
1508
+ names = []
1509
+ for child in element:
1510
+ if child.tag.endswith("Name"):
1511
+ if child.text:
1512
+ names.append(child.text)
1513
+ if names:
1514
+ result["Name"] = names
1515
+
1516
+ # Parse Icon (base64)
1517
+ for child in element:
1518
+ if child.tag.endswith("Icon"):
1519
+ if child.text:
1520
+ result["Icon"] = child.text
1521
+ break
1522
+
1523
+ # Parse VisibleTermFields
1524
+ for child in element:
1525
+ if child.tag.endswith("VisibleTermFields"):
1526
+ result["VisibleTermFields"] = child.attrib.copy()
1527
+ break
1528
+
1529
+ # Parse DimensionLink (multiple)
1530
+ dim_links = []
1531
+ for child in element:
1532
+ if child.tag.endswith("DimensionLink"):
1533
+ dim_links.append(child.attrib.get("DimensionID", ""))
1534
+ if dim_links:
1535
+ result["DimensionLink"] = dim_links
1536
+
1537
+ # Parse UserTypeLink (multiple) - parent type links
1538
+ user_type_links = []
1539
+ for child in element:
1540
+ if child.tag.endswith("UserTypeLink"):
1541
+ user_type_links.append(child.attrib.get("UserTypeID", ""))
1542
+ if user_type_links:
1543
+ result["UserTypeLink"] = user_type_links
1544
+
1545
+ # Parse AttributeLink (multiple) - valid attributes for this type
1546
+ attr_links = []
1547
+ for child in element:
1548
+ if child.tag.endswith("AttributeLink"):
1549
+ attr_links.append(child.attrib.get("AttributeID", ""))
1550
+ if attr_links:
1551
+ result["AttributeLink"] = attr_links
1552
+
1553
+ # Parse MIMEType (multiple)
1554
+ mime_types = []
1555
+ for child in element:
1556
+ if child.tag.endswith("MIMEType"):
1557
+ mime_type_dict = {"MIMEType": child.text or ""}
1558
+ mime_type_dict.update(child.attrib.copy())
1559
+ mime_types.append(mime_type_dict)
1560
+ if mime_types:
1561
+ result["MIMEType"] = mime_types
1562
+
1563
+ return result
1564
+
1565
+
1566
+ def _parse_context(element: ET.Element) -> dict:
1567
+ """Parse Context element (multi-language/multi-dimensional context).
1568
+
1569
+ Attributes (5 total):
1570
+ - ID (required): Context identifier (e.g., "en_US", "fr_FR")
1571
+ - Priority (optional, default "0") - Cross-context priority
1572
+ - Locale (optional string) - Locale code for language/region
1573
+ - Selected (optional boolean, default True)
1574
+ - Referenced (optional boolean, default False)
1575
+ - Changed (optional boolean, default False)
1576
+ - Deleted (optional boolean, default False)
1577
+
1578
+ Child Elements:
1579
+ - Name (text, multiple) - Context display name
1580
+ - DimensionPointLink (multiple) - Associated dimension points
1581
+ """
1582
+ result = _parse_element(
1583
+ element,
1584
+ attributes=("ID", "Priority", "Locale", "Selected", "Referenced",
1585
+ "Changed", "Deleted"),
1586
+ conversions={
1587
+ "Priority": (lambda x: x, "0"),
1588
+ "Selected": (_str_to_bool, True),
1589
+ "Referenced": (_str_to_bool, False),
1590
+ "Changed": (_str_to_bool, False),
1591
+ "Deleted": (_str_to_bool, False),
1592
+ }
1593
+ )
1594
+
1595
+ # Parse Name elements (text, multiple)
1596
+ names = []
1597
+ for child in element:
1598
+ if child.tag.endswith("Name"):
1599
+ if child.text:
1600
+ names.append(child.text)
1601
+ if names:
1602
+ result["Name"] = names
1603
+
1604
+ # Parse DimensionPointLink (multiple)
1605
+ dim_point_links = []
1606
+ for child in element:
1607
+ if child.tag.endswith("DimensionPointLink"):
1608
+ dim_point_links.append(child.attrib.get("DimensionPointID", ""))
1609
+ if dim_point_links:
1610
+ result["DimensionPointLink"] = dim_point_links
1611
+
1612
+ return result
1613
+
1614
+ def _parse_link_type(element: ET.Element) -> dict:
1615
+ """Parse LinkType element (cross-reference type definition).
1616
+
1617
+ Note: In STEP XML, LinkType elements appear within other structures
1618
+ to indicate which link types (CA=Classification-to-Attribute or
1619
+ PA=Product-to-Attribute) are valid for an attribute.
1620
+
1621
+ Attributes (1 total):
1622
+ - LinkTypeID (required): Either "CA" or "PA"
1623
+ - CA: Classification-to-Attribute link type
1624
+ - PA: Product-to-Attribute link type
1625
+ """
1626
+ return _parse_element(
1627
+ element,
1628
+ attributes=("LinkTypeID",)
1629
+ )
1630
+
1631
+ def _parse_business_rule(element: ET.Element) -> dict:
1632
+ """Parse BusinessRule element (business rule definition).
1633
+
1634
+ Business rules can be Actions, Conditions, Functions, or Libraries.
1635
+ They contain business logic that executes within STEP.
1636
+
1637
+ Attributes (4 total):
1638
+ - ID (required): Unique identifier for the business rule
1639
+ - Type (required): Rule type - Action, Condition, Function, or Library
1640
+ - Scope (optional): Local or Global (only for Actions/Conditions)
1641
+ - RunPrivileged (optional): Boolean, default False
1642
+
1643
+ Child elements:
1644
+ - Name (multiple): Display name in different languages
1645
+ - Description (optional): Rule description
1646
+ - Dependency (multiple): Dependencies on other rules
1647
+ - FunctionDependency (multiple): Dependencies on functions
1648
+ - OnApprove (optional): Execution on approval
1649
+ - Configuration (optional): Encoded configuration data
1650
+ - ValidObjectTypes (optional): Object types this rule applies to
1651
+ - SetupGroupLink (optional): Setup group link
1652
+ """
1653
+ result = _parse_element(
1654
+ element,
1655
+ attributes=("ID", "Type", "Scope", "RunPrivileged"),
1656
+ conversions={
1657
+ "RunPrivileged": (lambda x: x.lower() == "true", False)
1658
+ }
1659
+ )
1660
+
1661
+ # Parse Name elements (multiple)
1662
+ names = []
1663
+ for child in element:
1664
+ local_tag = _get_local_tag(child)
1665
+ if local_tag == "Name" and child.text:
1666
+ names.append(child.text)
1667
+ if names:
1668
+ result["Name"] = names
1669
+
1670
+ # Parse Description
1671
+ for child in element:
1672
+ local_tag = _get_local_tag(child)
1673
+ if local_tag == "Description" and child.text:
1674
+ result["Description"] = child.text
1675
+ break
1676
+
1677
+ return result
1678
+
1679
+ def _parse_event_queue(element: ET.Element) -> dict:
1680
+ """Parse EventQueue element (event queue configuration).
1681
+
1682
+ Event queues store events generated by STEP system operations.
1683
+ They can be read/processed or discarded.
1684
+
1685
+ Attributes (7 total):
1686
+ - ID (optional): Unique identifier for the event queue
1687
+ - QueueStatus (optional): Read or Discard, default Read
1688
+ - DaysRetained (optional): Number of days to retain events
1689
+ - Selected (optional): Boolean, default True
1690
+ - Referenced (optional): Boolean, default False
1691
+ - Changed (optional): Boolean, default False
1692
+ - Deleted (optional): Boolean, default False
1693
+
1694
+ Child elements:
1695
+ - Name (multiple): Display name in different languages
1696
+ - MetaData (optional): Metadata attributes and values
1697
+ - Configuration (required): Encoded configuration data
1698
+ """
1699
+ result = _parse_element(
1700
+ element,
1701
+ attributes=("ID", "QueueStatus", "DaysRetained", "Selected", "Referenced", "Changed", "Deleted"),
1702
+ conversions={
1703
+ "Selected": (lambda x: x.lower() == "true", True),
1704
+ "Referenced": (lambda x: x.lower() == "true", False),
1705
+ "Changed": (lambda x: x.lower() == "true", False),
1706
+ "Deleted": (lambda x: x.lower() == "true", False)
1707
+ }
1708
+ )
1709
+
1710
+ # Parse Name elements (multiple)
1711
+ names = []
1712
+ for child in element:
1713
+ local_tag = _get_local_tag(child)
1714
+ if local_tag == "Name" and child.text:
1715
+ names.append(child.text)
1716
+ if names:
1717
+ result["Name"] = names
1718
+
1719
+ # Parse MetaData
1720
+ for child in element:
1721
+ local_tag = _get_local_tag(child)
1722
+ if local_tag == "MetaData":
1723
+ result["MetaData"] = _parse_metadata(child)
1724
+ break
1725
+
1726
+ return result
1727
+
1728
+ def _parse_workflow(element: ET.Element) -> dict:
1729
+ """Parse STEPWorkflow element (workflow definition).
1730
+
1731
+ Workflows orchestrate business processes in STEP.
1732
+ They define states, transitions, and validations.
1733
+
1734
+ Attributes (3 total):
1735
+ - ID (required): Unique identifier for the workflow
1736
+ - Selected (optional): Boolean, default True
1737
+ - Referenced (optional): Boolean, default False
1738
+ - Changed (optional): Boolean, default False
1739
+
1740
+ Child elements:
1741
+ - Name (multiple): Display name in different languages
1742
+ - MetaData (optional): Metadata attributes and values
1743
+ - ValidUserTypeLink (multiple): Object types valid in workflow
1744
+ - StartCondition (optional): Condition for workflow start
1745
+ - Configuration (optional): Encoded configuration data
1746
+ - LocalBusinessRule (multiple): Embedded business rules
1747
+ - BusinessRuleLink (multiple): References to business rules
1748
+ - SetupGroupLink (optional): Setup group link
1749
+ """
1750
+ result = _parse_element(
1751
+ element,
1752
+ attributes=("ID", "Selected", "Referenced", "Changed"),
1753
+ conversions={
1754
+ "Selected": (lambda x: x.lower() == "true", True),
1755
+ "Referenced": (lambda x: x.lower() == "true", False),
1756
+ "Changed": (lambda x: x.lower() == "true", False)
1757
+ }
1758
+ )
1759
+
1760
+ # Parse Name elements (multiple)
1761
+ names = []
1762
+ for child in element:
1763
+ local_tag = _get_local_tag(child)
1764
+ if local_tag == "Name" and child.text:
1765
+ names.append(child.text)
1766
+ if names:
1767
+ result["Name"] = names
1768
+
1769
+ # Parse MetaData
1770
+ for child in element:
1771
+ local_tag = _get_local_tag(child)
1772
+ if local_tag == "MetaData":
1773
+ result["MetaData"] = _parse_metadata(child)
1774
+ break
1775
+
1776
+ # Parse ValidUserTypeLink elements (multiple)
1777
+ user_type_links = []
1778
+ for child in element:
1779
+ local_tag = _get_local_tag(child)
1780
+ if local_tag == "ValidUserTypeLink":
1781
+ user_type_links.append(child.attrib.get("UserTypeID", ""))
1782
+ if user_type_links:
1783
+ result["ValidUserTypeLink"] = user_type_links
1784
+
1785
+ return result
1786
+
1787
+ def _parse_export_configuration(element: ET.Element) -> dict:
1788
+ """Parse ExportConfiguration element (export/integration configuration).
1789
+
1790
+ Export configurations define how data is exported from STEP.
1791
+ They specify output format, target systems, and mapping rules.
1792
+
1793
+ Attributes (1 total):
1794
+ - ID (optional): Unique identifier for the configuration
1795
+
1796
+ Child elements:
1797
+ - Name (multiple): Display name in different languages
1798
+ - MetaData (optional): Metadata attributes and values
1799
+ - ClassificationReference (multiple): Classifications this applies to
1800
+ - Configuration (optional): Encoded configuration data
1801
+ """
1802
+ result = _parse_element(
1803
+ element,
1804
+ attributes=("ID",)
1805
+ )
1806
+
1807
+ # Parse Name elements (multiple)
1808
+ names = []
1809
+ for child in element:
1810
+ local_tag = _get_local_tag(child)
1811
+ if local_tag == "Name" and child.text:
1812
+ names.append(child.text)
1813
+ if names:
1814
+ result["Name"] = names
1815
+
1816
+ # Parse MetaData
1817
+ for child in element:
1818
+ local_tag = _get_local_tag(child)
1819
+ if local_tag == "MetaData":
1820
+ result["MetaData"] = _parse_metadata(child)
1821
+ break
1822
+
1823
+ # Parse ClassificationReference elements (multiple)
1824
+ class_refs = []
1825
+ for child in element:
1826
+ local_tag = _get_local_tag(child)
1827
+ if local_tag == "ClassificationReference":
1828
+ class_refs.append(child.attrib.get("ClassificationID", ""))
1829
+ if class_refs:
1830
+ result["ClassificationReference"] = class_refs
1831
+
1832
+ return result
1833
+
1834
+ def _parse_delete_classification(element: ET.Element) -> dict:
1835
+ """Parse DeleteClassification element (Phase 10: Organizational Structure).
1836
+
1837
+ Specifies deletion of a classification and optionally its values/subtypes.
1838
+ Attributes include the classification identifier and its type.
1839
+
1840
+ Attributes (2 total):
1841
+ - ID (required): Classification identifier to delete
1842
+ - Type (required): Type of deletion ('All', 'SubTypes', or 'Values')
1843
+
1844
+ Child elements:
1845
+ - KeyValue (multiple): Specific values/keys to delete
1846
+ """
1847
+ result = _parse_element(
1848
+ element,
1849
+ attributes=("ID", "Type")
1850
+ )
1851
+
1852
+ # Parse KeyValue elements (multiple)
1853
+ key_values = []
1854
+ for child in element:
1855
+ local_tag = _get_local_tag(child)
1856
+ if local_tag == "KeyValue":
1857
+ key_values.append(_parse_key_value(child))
1858
+ if key_values:
1859
+ result["KeyValue"] = key_values
1860
+
1861
+ return result
1862
+
1863
+ def _parse_delete_entity(element: ET.Element) -> dict:
1864
+ """Parse DeleteEntity element (Phase 10: Organizational Structure).
1865
+
1866
+ Specifies deletion of an entity and optionally its records/relationships.
1867
+ Attributes include the entity identifier.
1868
+
1869
+ Attributes (1 total):
1870
+ - ID (required): Entity identifier to delete
1871
+
1872
+ Child elements:
1873
+ - KeyValue (multiple): Entity records/keys to delete
1874
+ """
1875
+ result = _parse_element(
1876
+ element,
1877
+ attributes=("ID",)
1878
+ )
1879
+
1880
+ # Parse KeyValue elements (multiple)
1881
+ key_values = []
1882
+ for child in element:
1883
+ local_tag = _get_local_tag(child)
1884
+ if local_tag == "KeyValue":
1885
+ key_values.append(_parse_key_value(child))
1886
+ if key_values:
1887
+ result["KeyValue"] = key_values
1888
+
1889
+ return result
1890
+
1891
+ def _parse_cross_reference_type(element: ET.Element) -> dict:
1892
+ """Parse CrossReferenceType element (Phase 10: Organizational Structure).
1893
+
1894
+ Defines a type of cross-reference relationship between objects.
1895
+ Cross-reference types specify how different entities relate to each other.
1896
+
1897
+ Attributes (1 total):
1898
+ - ID (required): Unique identifier for the cross-reference type
1899
+
1900
+ Child elements:
1901
+ - Name (multiple): Display name in different languages
1902
+ - MetaData (optional): Metadata attributes and values
1903
+ """
1904
+ result = _parse_element(
1905
+ element,
1906
+ attributes=("ID",)
1907
+ )
1908
+
1909
+ # Parse Name elements (multiple)
1910
+ names = []
1911
+ for child in element:
1912
+ local_tag = _get_local_tag(child)
1913
+ if local_tag == "Name" and child.text:
1914
+ names.append(child.text)
1915
+ if names:
1916
+ result["Name"] = names
1917
+
1918
+ # Parse MetaData
1919
+ for child in element:
1920
+ local_tag = _get_local_tag(child)
1921
+ if local_tag == "MetaData":
1922
+ result["MetaData"] = _parse_metadata(child)
1923
+ break
1924
+
1925
+ return result
1926
+
1927
+ def _parse_qualifier(element: ET.Element) -> dict:
1928
+ """Parse Qualifier element (Phase 10: Organizational Structure).
1929
+
1930
+ Represents a dimension qualifier or multi-dimensional data point.
1931
+ Qualifiers enable multi-dimensional attribute management in STEP.
1932
+
1933
+ Attributes (1 total):
1934
+ - ID (required): Unique identifier for the qualifier
1935
+
1936
+ Child elements:
1937
+ - Name (multiple): Display name in different languages
1938
+ - MetaData (optional): Metadata attributes and values
1939
+ - DimensionPointLink (multiple): Dimension point references
1940
+ """
1941
+ result = _parse_element(
1942
+ element,
1943
+ attributes=("ID",)
1944
+ )
1945
+
1946
+ # Parse Name elements (multiple)
1947
+ names = []
1948
+ for child in element:
1949
+ local_tag = _get_local_tag(child)
1950
+ if local_tag == "Name" and child.text:
1951
+ names.append(child.text)
1952
+ if names:
1953
+ result["Name"] = names
1954
+
1955
+ # Parse MetaData
1956
+ for child in element:
1957
+ local_tag = _get_local_tag(child)
1958
+ if local_tag == "MetaData":
1959
+ result["MetaData"] = _parse_metadata(child)
1960
+ break
1961
+
1962
+ # Parse DimensionPointLink elements (multiple)
1963
+ dim_links = []
1964
+ for child in element:
1965
+ local_tag = _get_local_tag(child)
1966
+ if local_tag == "DimensionPointLink":
1967
+ dim_links.append({
1968
+ "DimensionID": child.attrib.get("DimensionID", ""),
1969
+ "DimensionPointID": child.attrib.get("DimensionPointID", "")
1970
+ })
1971
+ if dim_links:
1972
+ result["DimensionPointLink"] = dim_links
1973
+
1974
+ return result
1975
+
1976
+ def _parse_dimension(element: ET.Element) -> dict:
1977
+ """Parse Dimension element (Phase 11: Foundational Elements).
1978
+
1979
+ Represents a dimension for multi-dimensional data organization.
1980
+ Dimensions define axes along which data can be organized (e.g., Market, Color).
1981
+
1982
+ Attributes (2 total):
1983
+ - ID (required): Unique identifier for the dimension
1984
+ - AllowDuplicates (optional): Whether duplicate dimension points are allowed
1985
+
1986
+ Child elements:
1987
+ - Name (multiple): Display name in different languages
1988
+ - MetaData (optional): Metadata attributes and values
1989
+ - DimensionPoint (multiple): Points/values for this dimension
1990
+ """
1991
+ result = _parse_element(
1992
+ element,
1993
+ attributes=("ID", "AllowDuplicates")
1994
+ )
1995
+
1996
+ # Parse Name elements (multiple)
1997
+ names = []
1998
+ for child in element:
1999
+ local_tag = _get_local_tag(child)
2000
+ if local_tag == "Name" and child.text:
2001
+ names.append(child.text)
2002
+ if names:
2003
+ result["Name"] = names
2004
+
2005
+ # Parse MetaData
2006
+ for child in element:
2007
+ local_tag = _get_local_tag(child)
2008
+ if local_tag == "MetaData":
2009
+ result["MetaData"] = _parse_metadata(child)
2010
+ break
2011
+
2012
+ # Parse DimensionPoint elements (multiple)
2013
+ dim_points = []
2014
+ for child in element:
2015
+ local_tag = _get_local_tag(child)
2016
+ if local_tag == "DimensionPoint":
2017
+ dim_points.append(_parse_dimension_point(child))
2018
+ if dim_points:
2019
+ result["DimensionPoint"] = dim_points
2020
+
2021
+ return result
2022
+
2023
+ def _parse_dimension_point(element: ET.Element) -> dict:
2024
+ """Parse DimensionPoint element (Phase 11: Foundational Elements).
2025
+
2026
+ Represents a point/value within a dimension.
2027
+
2028
+ Attributes (1 total):
2029
+ - ID (required): Unique identifier for this dimension point
2030
+
2031
+ Child elements:
2032
+ - Name (multiple): Display name in different languages
2033
+ - MetaData (optional): Metadata attributes and values
2034
+ """
2035
+ result = _parse_element(
2036
+ element,
2037
+ attributes=("ID",)
2038
+ )
2039
+
2040
+ # Parse Name elements (multiple)
2041
+ names = []
2042
+ for child in element:
2043
+ local_tag = _get_local_tag(child)
2044
+ if local_tag == "Name" and child.text:
2045
+ names.append(child.text)
2046
+ if names:
2047
+ result["Name"] = names
2048
+
2049
+ # Parse MetaData
2050
+ for child in element:
2051
+ local_tag = _get_local_tag(child)
2052
+ if local_tag == "MetaData":
2053
+ result["MetaData"] = _parse_metadata(child)
2054
+ break
2055
+
2056
+ return result
2057
+
2058
+ def _parse_tag(element: ET.Element) -> dict:
2059
+ """Parse Tag element (Phase 11: Foundational Elements).
2060
+
2061
+ Represents a flexible tag for object categorization.
2062
+ Tags enable custom classification independent of the fixed hierarchy.
2063
+
2064
+ Attributes (1 total):
2065
+ - ID (required): Unique identifier for the tag
2066
+
2067
+ Child elements:
2068
+ - Name (multiple): Display name in different languages
2069
+ - MetaData (optional): Metadata attributes and values
2070
+ """
2071
+ result = _parse_element(
2072
+ element,
2073
+ attributes=("ID",)
2074
+ )
2075
+
2076
+ # Parse Name elements (multiple)
2077
+ names = []
2078
+ for child in element:
2079
+ local_tag = _get_local_tag(child)
2080
+ if local_tag == "Name" and child.text:
2081
+ names.append(child.text)
2082
+ if names:
2083
+ result["Name"] = names
2084
+
2085
+ # Parse MetaData
2086
+ for child in element:
2087
+ local_tag = _get_local_tag(child)
2088
+ if local_tag == "MetaData":
2089
+ result["MetaData"] = _parse_metadata(child)
2090
+ break
2091
+
2092
+ return result
2093
+
2094
+ def _parse_tag_group(element: ET.Element) -> dict:
2095
+ """Parse TagGroup element (Phase 11: Foundational Elements).
2096
+
2097
+ Represents a hierarchical grouping of tags.
2098
+ Tag groups organize tags into logical collections.
2099
+
2100
+ Attributes (1 total):
2101
+ - ID (required): Unique identifier for the tag group
2102
+
2103
+ Child elements:
2104
+ - Name (multiple): Display name in different languages
2105
+ - MetaData (optional): Metadata attributes and values
2106
+ - TagGroup (multiple): Nested tag groups (hierarchical)
2107
+ - Tag (multiple): Tags in this group
2108
+ """
2109
+ result = _parse_element(
2110
+ element,
2111
+ attributes=("ID",)
2112
+ )
2113
+
2114
+ # Parse Name elements (multiple)
2115
+ names = []
2116
+ for child in element:
2117
+ local_tag = _get_local_tag(child)
2118
+ if local_tag == "Name" and child.text:
2119
+ names.append(child.text)
2120
+ if names:
2121
+ result["Name"] = names
2122
+
2123
+ # Parse MetaData
2124
+ for child in element:
2125
+ local_tag = _get_local_tag(child)
2126
+ if local_tag == "MetaData":
2127
+ result["MetaData"] = _parse_metadata(child)
2128
+ break
2129
+
2130
+ # Parse nested TagGroup elements (multiple)
2131
+ tag_groups = []
2132
+ for child in element:
2133
+ local_tag = _get_local_tag(child)
2134
+ if local_tag == "TagGroup":
2135
+ tag_groups.append(_parse_tag_group(child))
2136
+ if tag_groups:
2137
+ result["TagGroup"] = tag_groups
2138
+
2139
+ # Parse Tag elements (multiple)
2140
+ tags = []
2141
+ for child in element:
2142
+ local_tag = _get_local_tag(child)
2143
+ if local_tag == "Tag":
2144
+ tags.append(_parse_tag(child))
2145
+ if tags:
2146
+ result["Tag"] = tags
2147
+
2148
+ return result
2149
+
2150
+ def _parse_unit(element: ET.Element) -> dict:
2151
+ """Parse Unit element (Phase 11: Foundational Elements).
2152
+
2153
+ Represents a measurement unit (e.g., meters, kilograms, liters).
2154
+ Units are used for quantitative attribute values.
2155
+
2156
+ Attributes (4 total):
2157
+ - ID (required): Unique identifier for the unit
2158
+ - Name (optional): Display name of the unit
2159
+ - Symbol (optional): Unit symbol (e.g., 'm', 'kg', 'l')
2160
+ - Formula (optional): Conversion formula to base unit
2161
+
2162
+ Child elements:
2163
+ - MetaData (optional): Metadata attributes and values
2164
+ """
2165
+ result = _parse_element(
2166
+ element,
2167
+ attributes=("ID", "Name", "Symbol", "Formula")
2168
+ )
2169
+
2170
+ # Parse MetaData
2171
+ for child in element:
2172
+ local_tag = _get_local_tag(child)
2173
+ if local_tag == "MetaData":
2174
+ result["MetaData"] = _parse_metadata(child)
2175
+ break
2176
+
2177
+ return result
2178
+
2179
+
2180
+ # Phase 12: Replacement Rules and Collections
2181
+
2182
+
2183
+
2184
+ def _parse_replace_attributes(element: ET.Element) -> dict:
2185
+ """Parse replacement rules for attributes."""
2186
+ return _parse_element(element, attributes=[], texts=[], specials={})
2187
+
2188
+
2189
+ def _parse_replace_products(element: ET.Element) -> dict:
2190
+ """Parse replacement rules for products."""
2191
+ result = {}
2192
+
2193
+ for child in element:
2194
+ local_tag = _get_local_tag(child)
2195
+
2196
+ if local_tag == "ReplaceValues":
2197
+ if "ReplaceValues" not in result:
2198
+ result["ReplaceValues"] = []
2199
+ result["ReplaceValues"].append(_parse_replace_values(child))
2200
+
2201
+ elif local_tag == "ReplaceCrossReferences":
2202
+ if "ReplaceCrossReferences" not in result:
2203
+ result["ReplaceCrossReferences"] = []
2204
+ result["ReplaceCrossReferences"].append(_parse_replace_cross_references(child))
2205
+
2206
+ elif local_tag == "ReplaceAttributeLinks":
2207
+ if "ReplaceAttributeLinks" not in result:
2208
+ result["ReplaceAttributeLinks"] = []
2209
+ result["ReplaceAttributeLinks"].append(_parse_replace_attribute_links(child))
2210
+
2211
+ elif local_tag == "ReplaceClassificationReferences":
2212
+ if "ReplaceClassificationReferences" not in result:
2213
+ result["ReplaceClassificationReferences"] = []
2214
+ result["ReplaceClassificationReferences"].append(_parse_replace_classification_references(child))
2215
+
2216
+ return result
2217
+
2218
+
2219
+ def _parse_replace_classifications(element: ET.Element) -> dict:
2220
+ """Parse replacement rules for classifications."""
2221
+ result = {}
2222
+
2223
+ for child in element:
2224
+ local_tag = _get_local_tag(child)
2225
+
2226
+ if local_tag == "ReplaceValues":
2227
+ if "ReplaceValues" not in result:
2228
+ result["ReplaceValues"] = []
2229
+ result["ReplaceValues"].append(_parse_replace_values(child))
2230
+
2231
+ elif local_tag == "ReplaceCrossReferences":
2232
+ if "ReplaceCrossReferences" not in result:
2233
+ result["ReplaceCrossReferences"] = []
2234
+ result["ReplaceCrossReferences"].append(_parse_replace_cross_references(child))
2235
+
2236
+ elif local_tag == "ReplaceAttributeLinks":
2237
+ if "ReplaceAttributeLinks" not in result:
2238
+ result["ReplaceAttributeLinks"] = []
2239
+ result["ReplaceAttributeLinks"].append(_parse_replace_attribute_links(child))
2240
+
2241
+ return result
2242
+
2243
+
2244
+ def _parse_replace_entities(element: ET.Element) -> dict:
2245
+ """Parse replacement rules for entities."""
2246
+ result = {}
2247
+
2248
+ for child in element:
2249
+ local_tag = _get_local_tag(child)
2250
+
2251
+ if local_tag == "ReplaceValues":
2252
+ if "ReplaceValues" not in result:
2253
+ result["ReplaceValues"] = []
2254
+ result["ReplaceValues"].append(_parse_replace_values(child))
2255
+
2256
+ elif local_tag == "ReplaceCrossReferences":
2257
+ if "ReplaceCrossReferences" not in result:
2258
+ result["ReplaceCrossReferences"] = []
2259
+ result["ReplaceCrossReferences"].append(_parse_replace_cross_references(child))
2260
+
2261
+ return result
2262
+
2263
+
2264
+ def _parse_replace_assets(element: ET.Element) -> dict:
2265
+ """Parse replacement rules for assets."""
2266
+ result = {}
2267
+
2268
+ for child in element:
2269
+ local_tag = _get_local_tag(child)
2270
+
2271
+ if local_tag == "ReplaceValues":
2272
+ if "ReplaceValues" not in result:
2273
+ result["ReplaceValues"] = []
2274
+ result["ReplaceValues"].append(_parse_replace_values(child))
2275
+
2276
+ elif local_tag == "ReplaceCrossReferences":
2277
+ if "ReplaceCrossReferences" not in result:
2278
+ result["ReplaceCrossReferences"] = []
2279
+ result["ReplaceCrossReferences"].append(_parse_replace_cross_references(child))
2280
+
2281
+ return result
2282
+
2283
+
2284
+ def _parse_replace_link_types(element: ET.Element) -> dict:
2285
+ """Parse replacement rules for link types."""
2286
+ return _parse_element(element, attributes=[], texts=[], specials={})
2287
+
2288
+
2289
+ def _parse_replace_user_types(element: ET.Element) -> dict:
2290
+ """Parse replacement rules for user types."""
2291
+ return _parse_element(element, attributes=[], texts=[], specials={})
2292
+
2293
+
2294
+ def _parse_replace_user_groups(element: ET.Element) -> dict:
2295
+ """Parse replacement rules for user groups."""
2296
+ return _parse_element(element, attributes=[], texts=[], specials={})
2297
+
2298
+
2299
+ def _parse_replace_values(element: ET.Element) -> dict:
2300
+ """Parse replacement values instruction for attributes."""
2301
+ return _parse_element(
2302
+ element,
2303
+ attributes=["AttributeID", "AttributeGroupID", "ContextID"],
2304
+ texts=[]
2305
+ )
2306
+
2307
+
2308
+ def _parse_replace_cross_references(element: ET.Element) -> dict:
2309
+ """Parse replacement cross references instruction."""
2310
+ return _parse_element(
2311
+ element,
2312
+ attributes=["CrossReferenceID", "ContextID", "MountLocation"],
2313
+ texts=[]
2314
+ )
2315
+
2316
+
2317
+ def _parse_replace_attribute_links(element: ET.Element) -> dict:
2318
+ """Parse replacement attribute links instruction."""
2319
+ return _parse_element(
2320
+ element,
2321
+ attributes=["AttributeID"],
2322
+ texts=[]
2323
+ )
2324
+
2325
+
2326
+ def _parse_replace_classification_references(element: ET.Element) -> dict:
2327
+ """Parse replacement classification references instruction."""
2328
+ return _parse_element(element, attributes=[], texts=[])
2329
+
2330
+
2331
+ def _parse_collection_item(element: ET.Element, is_group: bool = False) -> dict:
2332
+ """Parse collection or collection group element."""
2333
+ result = _parse_element(
2334
+ element,
2335
+ attributes=["ID", "ParentID"] + (["QueryURL"] if not is_group else []),
2336
+ texts=[]
2337
+ )
2338
+
2339
+ # Parse Name elements (multiple)
2340
+ names = []
2341
+ for child in element:
2342
+ local_tag = _get_local_tag(child)
2343
+ if local_tag == "Name":
2344
+ name_text = child.text
2345
+ if name_text:
2346
+ name_dict = {"value": name_text}
2347
+ # Get attributes (Locale, etc.)
2348
+ for attr_name, attr_value in child.attrib.items():
2349
+ local_attr = attr_name
2350
+ if '}' in local_attr:
2351
+ local_attr = local_attr.split('}', 1)[1]
2352
+ name_dict[local_attr] = attr_value
2353
+ names.append(name_dict)
2354
+
2355
+ if names:
2356
+ result["Name"] = names
2357
+
2358
+ # Parse MetaData
2359
+ for child in element:
2360
+ local_tag = _get_local_tag(child)
2361
+ if local_tag == "MetaData":
2362
+ result["MetaData"] = _parse_metadata(child)
2363
+ break
2364
+
2365
+ # For collection groups, parse nested groups and collections
2366
+ if is_group:
2367
+ collection_groups = []
2368
+ collections = []
2369
+ for child in element:
2370
+ local_tag = _get_local_tag(child)
2371
+ if local_tag == "CollectionGroup":
2372
+ collection_groups.append(_parse_collection_item(child, is_group=True))
2373
+ elif local_tag == "Collection":
2374
+ collections.append(_parse_collection_item(child, is_group=False))
2375
+
2376
+ if collection_groups:
2377
+ result["CollectionGroup"] = collection_groups
2378
+ if collections:
2379
+ result["Collection"] = collections
2380
+
2381
+ return result
2382
+
2383
+
2384
+ # Phase 13: Advanced Configuration Elements
2385
+
2386
+ def _parse_bulk_update_configuration(element: ET.Element) -> dict:
2387
+ """Parse bulk update configuration."""
2388
+ result = _parse_element(element, attributes=["ID"], texts=[])
2389
+
2390
+ # Parse Name elements (multiple)
2391
+ names = []
2392
+ for child in element:
2393
+ local_tag = _get_local_tag(child)
2394
+ if local_tag == "Name":
2395
+ name_text = child.text
2396
+ if name_text:
2397
+ names.append({"value": name_text})
2398
+
2399
+ if names:
2400
+ result["Name"] = names
2401
+
2402
+ # Parse MetaData
2403
+ for child in element:
2404
+ local_tag = _get_local_tag(child)
2405
+ if local_tag == "MetaData":
2406
+ result["MetaData"] = _parse_metadata(child)
2407
+ break
2408
+
2409
+ # Parse ClassificationReference elements (multiple)
2410
+ class_refs = []
2411
+ for child in element:
2412
+ local_tag = _get_local_tag(child)
2413
+ if local_tag == "ClassificationReference":
2414
+ class_refs.append(_parse_classification_reference(child))
2415
+
2416
+ if class_refs:
2417
+ result["ClassificationReference"] = class_refs
2418
+
2419
+ # Parse Configuration element
2420
+ for child in element:
2421
+ local_tag = _get_local_tag(child)
2422
+ if local_tag == "Configuration":
2423
+ result["Configuration"] = _parse_element(child, attributes=[], texts=[])
2424
+ break
2425
+
2426
+ return result
2427
+
2428
+
2429
+ def _parse_derived_event_type(element: ET.Element) -> dict:
2430
+ """Parse derived event type (simple text)."""
2431
+ event_text = element.text
2432
+ if event_text:
2433
+ return {"value": event_text}
2434
+ return {}
2435
+
2436
+
2437
+ def _parse_import_configuration(element: ET.Element) -> dict:
2438
+ """Parse import configuration."""
2439
+ result = _parse_element(element, attributes=["ID"], texts=[])
2440
+
2441
+ # Parse Name elements (multiple)
2442
+ names = []
2443
+ for child in element:
2444
+ local_tag = _get_local_tag(child)
2445
+ if local_tag == "Name":
2446
+ name_text = child.text
2447
+ if name_text:
2448
+ names.append({"value": name_text})
2449
+
2450
+ if names:
2451
+ result["Name"] = names
2452
+
2453
+ # Parse MetaData
2454
+ for child in element:
2455
+ local_tag = _get_local_tag(child)
2456
+ if local_tag == "MetaData":
2457
+ result["MetaData"] = _parse_metadata(child)
2458
+ break
2459
+
2460
+ # Parse ClassificationReference elements (multiple)
2461
+ class_refs = []
2462
+ for child in element:
2463
+ local_tag = _get_local_tag(child)
2464
+ if local_tag == "ClassificationReference":
2465
+ class_refs.append(_parse_classification_reference(child))
2466
+
2467
+ if class_refs:
2468
+ result["ClassificationReference"] = class_refs
2469
+
2470
+ # Parse Configuration element
2471
+ for child in element:
2472
+ local_tag = _get_local_tag(child)
2473
+ if local_tag == "Configuration":
2474
+ result["Configuration"] = _parse_element(child, attributes=[], texts=[])
2475
+ break
2476
+
2477
+ return result
2478
+
2479
+
2480
+ def _parse_portal_configuration(element: ET.Element) -> dict:
2481
+ """Parse portal/web UI configuration."""
2482
+ result = _parse_element(
2483
+ element,
2484
+ attributes=["ID", "Selected", "Referenced", "Changed"],
2485
+ texts=[],
2486
+ conversions={
2487
+ "Selected": (bool, True),
2488
+ "Referenced": (bool, False),
2489
+ "Changed": (bool, False)
2490
+ }
2491
+ )
2492
+
2493
+ # Parse SetupGroupLink
2494
+ for child in element:
2495
+ local_tag = _get_local_tag(child)
2496
+ if local_tag == "SetupGroupLink":
2497
+ result["SetupGroupLink"] = _parse_element(child, attributes=["ID"], texts=[])
2498
+ break
2499
+
2500
+ # Parse Name elements (multiple)
2501
+ names = []
2502
+ for child in element:
2503
+ local_tag = _get_local_tag(child)
2504
+ if local_tag == "Name":
2505
+ name_text = child.text
2506
+ if name_text:
2507
+ names.append({"value": name_text})
2508
+
2509
+ if names:
2510
+ result["Name"] = names
2511
+
2512
+ # Parse MetaData
2513
+ for child in element:
2514
+ local_tag = _get_local_tag(child)
2515
+ if local_tag == "MetaData":
2516
+ result["MetaData"] = _parse_metadata(child)
2517
+ break
2518
+
2519
+ # Parse Configuration element
2520
+ for child in element:
2521
+ local_tag = _get_local_tag(child)
2522
+ if local_tag == "Configuration":
2523
+ result["Configuration"] = _parse_element(child, attributes=[], texts=[])
2524
+ break
2525
+
2526
+ return result
2527
+
2528
+
2529
+ # ===== PHASE 14: Advanced Configuration & System Elements =====
2530
+
2531
+ def _parse_key(element: ET.Element) -> dict:
2532
+ """Parse key definition (unique identifier configuration)."""
2533
+ result = _parse_element(
2534
+ element,
2535
+ attributes=["ID", "State", "Selected", "Referenced", "Changed", "Deleted"],
2536
+ texts=[],
2537
+ conversions={
2538
+ "Selected": (_str_to_bool, True),
2539
+ "Referenced": (_str_to_bool, False),
2540
+ "Changed": (_str_to_bool, False),
2541
+ "Deleted": (_str_to_bool, False)
2542
+ }
2543
+ )
2544
+
2545
+ # Parse Name elements (multiple)
2546
+ names = []
2547
+ for child in element:
2548
+ local_tag = _get_local_tag(child)
2549
+ if local_tag == "Name":
2550
+ name_text = child.text
2551
+ if name_text:
2552
+ names.append({"value": name_text})
2553
+ if names:
2554
+ result["Name"] = names
2555
+
2556
+ # Parse KeyFormula
2557
+ for child in element:
2558
+ local_tag = _get_local_tag(child)
2559
+ if local_tag == "KeyFormula":
2560
+ result["KeyFormula"] = child.text or ""
2561
+ break
2562
+
2563
+ # Parse MetaData
2564
+ for child in element:
2565
+ local_tag = _get_local_tag(child)
2566
+ if local_tag == "MetaData":
2567
+ result["MetaData"] = _parse_element(child, attributes=[], texts=[])
2568
+ break
2569
+
2570
+ # Parse UserTypeLink (multiple)
2571
+ user_type_links = []
2572
+ for child in element:
2573
+ local_tag = _get_local_tag(child)
2574
+ if local_tag == "UserTypeLink":
2575
+ user_type_links.append(_parse_element(child, attributes=["ID"], texts=[]))
2576
+ if user_type_links:
2577
+ result["UserTypeLink"] = user_type_links
2578
+
2579
+ # Parse AttributeLink (multiple)
2580
+ attribute_links = []
2581
+ for child in element:
2582
+ local_tag = _get_local_tag(child)
2583
+ if local_tag == "AttributeLink":
2584
+ attribute_links.append(_parse_element(child, attributes=["ID"], texts=[]))
2585
+ if attribute_links:
2586
+ result["AttributeLink"] = attribute_links
2587
+
2588
+ return result
2589
+
2590
+
2591
+ def _parse_cross_context(element: ET.Element) -> dict:
2592
+ """Parse cross-context definition (context wrapper for cross-context exports)."""
2593
+ result = _parse_element(
2594
+ element,
2595
+ attributes=["ID", "Priority", "Locale", "Selected", "Referenced", "Changed", "Deleted"],
2596
+ texts=[],
2597
+ conversions={
2598
+ "Selected": (_str_to_bool, True),
2599
+ "Referenced": (_str_to_bool, False),
2600
+ "Changed": (_str_to_bool, False),
2601
+ "Deleted": (_str_to_bool, False)
2602
+ }
2603
+ )
2604
+
2605
+ # Parse Name elements (multiple)
2606
+ names = []
2607
+ for child in element:
2608
+ local_tag = _get_local_tag(child)
2609
+ if local_tag == "Name":
2610
+ name_text = child.text
2611
+ if name_text:
2612
+ names.append({"value": name_text})
2613
+ if names:
2614
+ result["Name"] = names
2615
+
2616
+ # Parse DimensionPointLink (multiple)
2617
+ dimension_point_links = []
2618
+ for child in element:
2619
+ local_tag = _get_local_tag(child)
2620
+ if local_tag == "DimensionPointLink":
2621
+ dimension_point_links.append(_parse_element(child, attributes=["ID"], texts=[]))
2622
+ if dimension_point_links:
2623
+ result["DimensionPointLink"] = dimension_point_links
2624
+
2625
+ return result
2626
+
2627
+
2628
+ def _parse_business_library(element: ET.Element) -> dict:
2629
+ """Parse business library (reusable business rule collection)."""
2630
+ result = _parse_element(
2631
+ element,
2632
+ attributes=["ID", "Type", "RunPrivileged", "Scope", "Selected", "Referenced", "Changed", "Deleted"],
2633
+ texts=[],
2634
+ conversions={
2635
+ "RunPrivileged": (_str_to_bool, False),
2636
+ "Selected": (_str_to_bool, True),
2637
+ "Referenced": (_str_to_bool, False),
2638
+ "Changed": (_str_to_bool, False),
2639
+ "Deleted": (_str_to_bool, False)
2640
+ }
2641
+ )
2642
+
2643
+ # Parse SetupGroupLink
2644
+ for child in element:
2645
+ local_tag = _get_local_tag(child)
2646
+ if local_tag == "SetupGroupLink":
2647
+ result["SetupGroupLink"] = _parse_element(child, attributes=["ID"], texts=[])
2648
+ break
2649
+
2650
+ # Parse Name elements (multiple)
2651
+ names = []
2652
+ for child in element:
2653
+ local_tag = _get_local_tag(child)
2654
+ if local_tag == "Name":
2655
+ name_text = child.text
2656
+ if name_text:
2657
+ names.append({"value": name_text})
2658
+ if names:
2659
+ result["Name"] = names
2660
+
2661
+ # Parse Description
2662
+ for child in element:
2663
+ local_tag = _get_local_tag(child)
2664
+ if local_tag == "Description":
2665
+ result["Description"] = child.text or ""
2666
+ break
2667
+
2668
+ # Parse Dependencies
2669
+ dependencies = []
2670
+ for child in element:
2671
+ local_tag = _get_local_tag(child)
2672
+ if local_tag == "Dependencies":
2673
+ dependencies.append(_parse_element(child, attributes=["LibraryID", "LibraryAlias"], texts=[]))
2674
+ if dependencies:
2675
+ result["Dependencies"] = dependencies
2676
+
2677
+ # Parse MetaData
2678
+ for child in element:
2679
+ local_tag = _get_local_tag(child)
2680
+ if local_tag == "MetaData":
2681
+ result["MetaData"] = _parse_element(child, attributes=[], texts=[])
2682
+ break
2683
+
2684
+ return result
2685
+
2686
+
2687
+ def _parse_setup_group(element: ET.Element) -> dict:
2688
+ """Parse setup group (hierarchical organization of setup components)."""
2689
+ result = _parse_element(
2690
+ element,
2691
+ attributes=["ID", "UserTypeID", "ParentID", "Referenced", "Selected", "Changed"],
2692
+ texts=[],
2693
+ conversions={
2694
+ "Referenced": (_str_to_bool, False),
2695
+ "Selected": (_str_to_bool, True),
2696
+ "Changed": (_str_to_bool, False)
2697
+ }
2698
+ )
2699
+
2700
+ # Parse Name elements (multiple)
2701
+ names = []
2702
+ for child in element:
2703
+ local_tag = _get_local_tag(child)
2704
+ if local_tag == "Name":
2705
+ name_text = child.text
2706
+ if name_text:
2707
+ names.append({"value": name_text})
2708
+ if names:
2709
+ result["Name"] = names
2710
+
2711
+ # Parse MetaData
2712
+ for child in element:
2713
+ local_tag = _get_local_tag(child)
2714
+ if local_tag == "MetaData":
2715
+ result["MetaData"] = _parse_element(child, attributes=[], texts=[])
2716
+ break
2717
+
2718
+ # Parse nested SetupGroup (hierarchical)
2719
+ nested_groups = []
2720
+ for child in element:
2721
+ local_tag = _get_local_tag(child)
2722
+ if local_tag == "SetupGroup":
2723
+ nested_groups.append(_parse_setup_group(child))
2724
+ if nested_groups:
2725
+ result["SetupGroup"] = nested_groups
2726
+
2727
+ return result
2728
+
2729
+
2730
+ def _parse_action_set(element: ET.Element) -> dict:
2731
+ """Parse action set (collection of actions for privilege control)."""
2732
+ result = _parse_element(
2733
+ element,
2734
+ attributes=["ID", "Type", "Selected", "Changed", "Referenced"],
2735
+ texts=[],
2736
+ conversions={
2737
+ "Selected": (_str_to_bool, True),
2738
+ "Changed": (_str_to_bool, False),
2739
+ "Referenced": (_str_to_bool, False)
2740
+ }
2741
+ )
2742
+
2743
+ # Parse Name elements (multiple)
2744
+ names = []
2745
+ for child in element:
2746
+ local_tag = _get_local_tag(child)
2747
+ if local_tag == "Name":
2748
+ name_text = child.text
2749
+ if name_text:
2750
+ names.append({"value": name_text})
2751
+ if names:
2752
+ result["Name"] = names
2753
+
2754
+ # Parse MetaData
2755
+ for child in element:
2756
+ local_tag = _get_local_tag(child)
2757
+ if local_tag == "MetaData":
2758
+ result["MetaData"] = _parse_element(child, attributes=[], texts=[])
2759
+ break
2760
+
2761
+ # Parse ActionLink (multiple)
2762
+ action_links = []
2763
+ for child in element:
2764
+ local_tag = _get_local_tag(child)
2765
+ if local_tag == "ActionLink":
2766
+ action_links.append(_parse_element(child, attributes=["ID"], texts=[]))
2767
+ if action_links:
2768
+ result["ActionLink"] = action_links
2769
+
2770
+ return result
2771
+
2772
+
2773
+ def _parse_status_flag(element: ET.Element) -> dict:
2774
+ """Parse status flag (flag definition for status tracking)."""
2775
+ result = _parse_element(
2776
+ element,
2777
+ attributes=["ID", "Selected", "Referenced", "Changed"],
2778
+ texts=[],
2779
+ conversions={
2780
+ "Selected": (_str_to_bool, True),
2781
+ "Referenced": (_str_to_bool, False),
2782
+ "Changed": (_str_to_bool, False)
2783
+ }
2784
+ )
2785
+
2786
+ # Parse SetupGroupLink
2787
+ for child in element:
2788
+ local_tag = _get_local_tag(child)
2789
+ if local_tag == "SetupGroupLink":
2790
+ result["SetupGroupLink"] = _parse_element(child, attributes=["ID"], texts=[])
2791
+ break
2792
+
2793
+ # Parse Name elements (multiple)
2794
+ names = []
2795
+ for child in element:
2796
+ local_tag = _get_local_tag(child)
2797
+ if local_tag == "Name":
2798
+ name_text = child.text
2799
+ if name_text:
2800
+ names.append({"value": name_text})
2801
+ if names:
2802
+ result["Name"] = names
2803
+
2804
+ return result
2805
+