rhapsody-cli 0.1.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 (92) hide show
  1. rhapsody_cli/__init__.py +21 -0
  2. rhapsody_cli/actions/__init__.py +17 -0
  3. rhapsody_cli/actions/abstract_action.py +259 -0
  4. rhapsody_cli/actions/attribute_action.py +595 -0
  5. rhapsody_cli/actions/class_action.py +708 -0
  6. rhapsody_cli/actions/operation_action.py +606 -0
  7. rhapsody_cli/actions/package_action.py +592 -0
  8. rhapsody_cli/actions/port_action.py +565 -0
  9. rhapsody_cli/actions/project_action.py +119 -0
  10. rhapsody_cli/application.py +390 -0
  11. rhapsody_cli/cli/__init__.py +5 -0
  12. rhapsody_cli/cli/cli.py +102 -0
  13. rhapsody_cli/cli/formatters.py +47 -0
  14. rhapsody_cli/cli/logging_config.py +39 -0
  15. rhapsody_cli/cli/main.py +6 -0
  16. rhapsody_cli/cli/path_resolver.py +117 -0
  17. rhapsody_cli/com_utils.py +54 -0
  18. rhapsody_cli/commands/__init__.py +9 -0
  19. rhapsody_cli/commands/abstract_command.py +92 -0
  20. rhapsody_cli/commands/attribute_command.py +36 -0
  21. rhapsody_cli/commands/class_command.py +38 -0
  22. rhapsody_cli/commands/operation_command.py +36 -0
  23. rhapsody_cli/commands/package_command.py +36 -0
  24. rhapsody_cli/commands/port_command.py +36 -0
  25. rhapsody_cli/commands/project_command.py +34 -0
  26. rhapsody_cli/exceptions/__init__.py +9 -0
  27. rhapsody_cli/exceptions/core.py +42 -0
  28. rhapsody_cli/models/__init__.py +16 -0
  29. rhapsody_cli/models/core.py +2263 -0
  30. rhapsody_cli/models/elements/__init__.py +15 -0
  31. rhapsody_cli/models/elements/activity/__init__.py +33 -0
  32. rhapsody_cli/models/elements/activity/model_actions.py +358 -0
  33. rhapsody_cli/models/elements/activity/model_activity.py +744 -0
  34. rhapsody_cli/models/elements/classifiers/__init__.py +27 -0
  35. rhapsody_cli/models/elements/classifiers/model_actor.py +76 -0
  36. rhapsody_cli/models/elements/classifiers/model_association_class.py +21 -0
  37. rhapsody_cli/models/elements/classifiers/model_class.py +461 -0
  38. rhapsody_cli/models/elements/classifiers/model_classifier.py +870 -0
  39. rhapsody_cli/models/elements/classifiers/model_interface_item.py +138 -0
  40. rhapsody_cli/models/elements/classifiers/model_operation.py +182 -0
  41. rhapsody_cli/models/elements/classifiers/model_statechart.py +108 -0
  42. rhapsody_cli/models/elements/classifiers/model_stereotype.py +23 -0
  43. rhapsody_cli/models/elements/classifiers/model_usecase.py +86 -0
  44. rhapsody_cli/models/elements/common/__init__.py +21 -0
  45. rhapsody_cli/models/elements/common/model_misc.py +45 -0
  46. rhapsody_cli/models/elements/common/model_other_model.py +822 -0
  47. rhapsody_cli/models/elements/containment/__init__.py +29 -0
  48. rhapsody_cli/models/elements/containment/model_collaboration.py +55 -0
  49. rhapsody_cli/models/elements/containment/model_component.py +59 -0
  50. rhapsody_cli/models/elements/containment/model_component_instance.py +20 -0
  51. rhapsody_cli/models/elements/containment/model_configuration.py +62 -0
  52. rhapsody_cli/models/elements/containment/model_module.py +17 -0
  53. rhapsody_cli/models/elements/containment/model_node.py +22 -0
  54. rhapsody_cli/models/elements/containment/model_package.py +285 -0
  55. rhapsody_cli/models/elements/containment/model_profile.py +17 -0
  56. rhapsody_cli/models/elements/containment/model_project.py +248 -0
  57. rhapsody_cli/models/elements/diagrams/__init__.py +31 -0
  58. rhapsody_cli/models/elements/diagrams/model_diagram_types.py +285 -0
  59. rhapsody_cli/models/elements/diagrams/model_diagrams.py +114 -0
  60. rhapsody_cli/models/elements/graphics/__init__.py +35 -0
  61. rhapsody_cli/models/elements/graphics/model_graphics.py +3053 -0
  62. rhapsody_cli/models/elements/interactions/__init__.py +29 -0
  63. rhapsody_cli/models/elements/interactions/model_interactions.py +1336 -0
  64. rhapsody_cli/models/elements/relations/__init__.py +23 -0
  65. rhapsody_cli/models/elements/relations/model_association_role.py +20 -0
  66. rhapsody_cli/models/elements/relations/model_dependency.py +23 -0
  67. rhapsody_cli/models/elements/relations/model_generalization.py +26 -0
  68. rhapsody_cli/models/elements/relations/model_hyperlink.py +25 -0
  69. rhapsody_cli/models/elements/relations/model_instance.py +224 -0
  70. rhapsody_cli/models/elements/relations/model_port.py +227 -0
  71. rhapsody_cli/models/elements/relations/model_relation.py +474 -0
  72. rhapsody_cli/models/elements/requirements/__init__.py +11 -0
  73. rhapsody_cli/models/elements/requirements/model_requirements.py +176 -0
  74. rhapsody_cli/models/elements/statemachine/__init__.py +11 -0
  75. rhapsody_cli/models/elements/statemachine/model_statemachine.py +793 -0
  76. rhapsody_cli/models/elements/templates/__init__.py +13 -0
  77. rhapsody_cli/models/elements/templates/model_templates.py +172 -0
  78. rhapsody_cli/models/elements/values/__init__.py +17 -0
  79. rhapsody_cli/models/elements/values/model_values.py +282 -0
  80. rhapsody_cli/models/elements/variables/__init__.py +15 -0
  81. rhapsody_cli/models/elements/variables/model_variables.py +301 -0
  82. rhapsody_cli/models/support/__init__.py +5 -0
  83. rhapsody_cli/models/support/model_codegen.py +1749 -0
  84. rhapsody_cli/models/support/model_files.py +438 -0
  85. rhapsody_cli/models/support/model_ide.py +782 -0
  86. rhapsody_cli/py.typed +0 -0
  87. rhapsody_cli-0.1.0.dist-info/METADATA +395 -0
  88. rhapsody_cli-0.1.0.dist-info/RECORD +92 -0
  89. rhapsody_cli-0.1.0.dist-info/WHEEL +5 -0
  90. rhapsody_cli-0.1.0.dist-info/entry_points.txt +2 -0
  91. rhapsody_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  92. rhapsody_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,595 @@
1
+ """Attribute-related CLI actions.
2
+
3
+ SWR_ATTR_00001: Attribute Create Command
4
+ SWR_ATTR_00002: Attribute Delete Command
5
+ SWR_ATTR_00003: Attribute View Command
6
+ SWR_ATTR_00004: Attribute List Command
7
+ SWR_ATTR_00005: Attribute Update Command
8
+ SWR_ATTR_00006: Path and Name Validation
9
+ SWR_ATTR_00007: External JSON File Support
10
+ SWR_ATTR_00008: Multi-Format Output
11
+ SWR_ATTR_00009: Error Handling and Logging
12
+ SWR_ATTR_00010: GUID Lookup Support
13
+ SWR_ATTR_00011: Type Resolution
14
+ SWR_ATTR_00012: IsStatic Flag Support
15
+ SWR_ATTR_00013: Bulk Creation Support
16
+ """
17
+
18
+ import argparse
19
+ import json
20
+ import logging
21
+ from pathlib import Path
22
+ from typing import Any, Dict, List
23
+
24
+ from rhapsody_cli.actions.abstract_action import ElementManagementAction
25
+ from rhapsody_cli.cli.formatters import OutputFormatter
26
+ from rhapsody_cli.exceptions import CliExecutionError
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ class AbstractAttributeAction(ElementManagementAction):
32
+ """Base class for attribute actions with common path, name and GUID validation.
33
+
34
+ SWR_ATTR_00006: Path and Name Validation
35
+ SWR_ATTR_00009: Error Handling and Logging
36
+ SWR_ATTR_00010: GUID Lookup Support
37
+ """
38
+
39
+ def _resolve_classifier(self, path: str) -> Any:
40
+ """Resolve a parent classifier path.
41
+
42
+ Args:
43
+ path: Classifier path to resolve.
44
+
45
+ Returns:
46
+ Classifier COM object.
47
+
48
+ Raises:
49
+ CliExecutionError: If path not found.
50
+ """
51
+ root = self._get_active_root()
52
+ return self._resolve_container_or_element(root, path, resolve_element=True, operation=f"resolve classifier path '{path}'")
53
+
54
+ def _resolve_attribute(self, classifier: Any, name: str) -> Any:
55
+ """Find an attribute by name within a classifier.
56
+
57
+ Args:
58
+ classifier: The parent classifier COM object.
59
+ name: The attribute name to find.
60
+
61
+ Returns:
62
+ Attribute COM object.
63
+
64
+ Raises:
65
+ CliExecutionError: If attribute not found.
66
+ """
67
+ attribute = classifier.find_attribute(name)
68
+ if attribute is None:
69
+ raise CliExecutionError(f"Attribute '{name}' not found in classifier")
70
+ return attribute
71
+
72
+ def _resolve_attribute_by_guid(self, guid: str) -> Any:
73
+ """Locate an attribute by GUID and validate it's an Attribute element.
74
+
75
+ SWR_ATTR_00010: GUID Lookup Support
76
+
77
+ Args:
78
+ guid: GUID string in format 12345678-1234-1234-1234-123456789abc.
79
+
80
+ Returns:
81
+ Attribute COM object.
82
+
83
+ Raises:
84
+ CliExecutionError: If GUID not found or element is not an Attribute.
85
+ """
86
+ project = self._get_active_project()
87
+ try:
88
+ element = project.find_element_by_guid(guid)
89
+ except Exception as e:
90
+ self._handle_execution_error(e, f"Failed to locate attribute by GUID '{guid}'")
91
+
92
+ if element is None:
93
+ raise CliExecutionError(f"No element found with GUID '{guid}'")
94
+
95
+ meta_class = element.get_meta_class()
96
+ if meta_class != "Attribute":
97
+ raise CliExecutionError(f"GUID '{guid}' does not resolve to an Attribute (found {meta_class})")
98
+
99
+ return element
100
+
101
+
102
+ class AttributeCreateAction(AbstractAttributeAction):
103
+ """Create one or multiple attributes.
104
+
105
+ SWR_ATTR_00001: Attribute Create Command
106
+ SWR_ATTR_00007: External JSON File Support
107
+ SWR_ATTR_00011: Type Resolution
108
+ SWR_ATTR_00012: IsStatic Flag Support
109
+ SWR_ATTR_00013: Bulk Creation Support
110
+ """
111
+
112
+ VALID_ATTRIBUTES = {
113
+ "name",
114
+ "type",
115
+ "defaultValue",
116
+ "multiplicity",
117
+ "isStatic",
118
+ "visibility",
119
+ "declaration",
120
+ "description",
121
+ }
122
+
123
+ def __init__(self) -> None:
124
+ """Initialize the 'create' action."""
125
+ super().__init__(command_id="create")
126
+
127
+ def init_arguments(self, sub_parser: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
128
+ """Register the 'create' subcommand and its arguments."""
129
+ parser = sub_parser.add_parser("create", help="Create an attribute")
130
+ self.add_path_argument(parser, required=True, help_text="Parent classifier path")
131
+ parser.add_argument("--input", default=None, help="JSON file with attribute attributes")
132
+ parser.add_argument("attributes", nargs="?", default=None, help="Inline JSON or JSON file path")
133
+ self.add_verbose_argument(parser)
134
+
135
+ def execute(self, args: argparse.Namespace) -> None:
136
+ """Execute attribute creation."""
137
+ input_data = args.input if args.input else args.attributes
138
+ if not input_data:
139
+ raise CliExecutionError("Either --input or attributes argument must be provided")
140
+
141
+ data = self._load_json_data(input_data)
142
+ attrs_data = data if isinstance(data, list) else [data]
143
+
144
+ classifier = self._resolve_classifier(args.path)
145
+
146
+ created: List[str] = []
147
+ errors: List[str] = []
148
+ for attr_attrs in attrs_data:
149
+ try:
150
+ name = self._create_single_attribute(classifier, attr_attrs, args.path)
151
+ created.append(name)
152
+ except CliExecutionError:
153
+ raise
154
+ except Exception as e:
155
+ attr_name = attr_attrs.get("name", "unknown")
156
+ self.logger.error("Failed to create attribute '%s': %s", attr_name, e)
157
+ errors.append(attr_name)
158
+
159
+ self._report_results(created, errors, len(attrs_data))
160
+
161
+ def _create_single_attribute(self, classifier: Any, attr_attrs: Dict[str, Any], parent_path: str) -> str:
162
+ """Create a single attribute and set its attributes. Returns the attribute name."""
163
+ name = str(attr_attrs.get("name", ""))
164
+ if not name:
165
+ raise CliExecutionError("'name' is required in attributes")
166
+
167
+ unknown = set(attr_attrs.keys()) - self.VALID_ATTRIBUTES
168
+ if unknown:
169
+ self.logger.warning("Skipping unknown attributes: %s", unknown)
170
+
171
+ attribute = classifier.add_attribute(name)
172
+ self._set_attributes(classifier, attribute, attr_attrs)
173
+
174
+ full_path = f"{parent_path}/{name}"
175
+ self.logger.info("Created attribute: %s", full_path)
176
+ return name
177
+
178
+ def _report_results(self, created: List[str], errors: List[str], total: int) -> None:
179
+ """Log summary of creation results."""
180
+ if errors and not created:
181
+ raise CliExecutionError(f"Created 0/{total} attributes; all failed")
182
+ if errors:
183
+ self.logger.info("Created %d/%d attributes with %d error(s)", len(created), total, len(errors))
184
+
185
+ def _load_json_data(self, attributes_input: str) -> Any:
186
+ """Load JSON data from inline string or external file.
187
+
188
+ SWR_ATTR_00007: External JSON File Support
189
+ """
190
+ if attributes_input.startswith("{") or attributes_input.startswith("["):
191
+ try:
192
+ return json.loads(attributes_input)
193
+ except json.JSONDecodeError as e:
194
+ raise CliExecutionError(f"Invalid JSON: {e}") from e
195
+
196
+ if not Path(attributes_input).exists():
197
+ raise CliExecutionError(f"File not found: {attributes_input}")
198
+
199
+ try:
200
+ with open(attributes_input, encoding="utf-8") as f:
201
+ return json.load(f)
202
+ except json.JSONDecodeError as e:
203
+ raise CliExecutionError(f"Invalid JSON in file: {e}") from e
204
+ except OSError as e:
205
+ raise CliExecutionError(f"Failed to read file: {e}") from e
206
+
207
+ def _set_attributes(self, classifier: Any, attribute: Any, attrs: Dict[str, Any]) -> None:
208
+ """Set validated attributes on attribute."""
209
+ if "name" in attrs:
210
+ attribute.set_name(attrs["name"])
211
+ if "defaultValue" in attrs:
212
+ attribute.set_default_value(attrs["defaultValue"])
213
+ if "multiplicity" in attrs:
214
+ attribute.set_multiplicity(attrs["multiplicity"])
215
+ if "visibility" in attrs:
216
+ attribute.set_visibility(attrs["visibility"])
217
+ if "declaration" in attrs:
218
+ attribute.set_declaration(attrs["declaration"])
219
+ if "description" in attrs:
220
+ attribute.set_description(attrs["description"])
221
+ self._set_boolean_flags(attribute, attrs)
222
+ self._set_type(classifier, attribute, attrs)
223
+
224
+ def _set_boolean_flags(self, attribute: Any, attrs: Dict[str, Any]) -> None:
225
+ """Set boolean flag isStatic.
226
+
227
+ SWR_ATTR_00012: IsStatic Flag Support
228
+ """
229
+ if "isStatic" in attrs:
230
+ attribute.set_is_static(1 if attrs["isStatic"] else 0)
231
+
232
+ def _set_type(self, classifier: Any, attribute: Any, attrs: Dict[str, Any]) -> None:
233
+ """Resolve and set the attribute type.
234
+
235
+ SWR_ATTR_00011: Type Resolution
236
+ """
237
+ if "type" in attrs:
238
+ type_name = attrs["type"]
239
+ owner = classifier.get_owner()
240
+ target = owner.find_nested_classifier_recursive(type_name)
241
+ if target is None:
242
+ raise CliExecutionError(f"Type '{type_name}' not found")
243
+ attribute.set_type(target)
244
+
245
+
246
+ class AttributeDeleteAction(AbstractAttributeAction):
247
+ """Delete an attribute.
248
+
249
+ SWR_ATTR_00002: Attribute Delete Command
250
+ SWR_ATTR_00010: GUID Lookup Support
251
+ """
252
+
253
+ def __init__(self) -> None:
254
+ """Initialize the 'delete' action."""
255
+ super().__init__(command_id="delete")
256
+
257
+ def init_arguments(self, sub_parser: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
258
+ """Register the 'delete' subcommand and its arguments."""
259
+ parser = sub_parser.add_parser("delete", help="Delete an attribute")
260
+ self.add_path_argument(parser, required=False, help_text="Parent classifier path")
261
+ parser.add_argument("--guid", default=None, help="Attribute GUID to delete")
262
+ parser.add_argument("--name", default=None, help="Attribute name within classifier")
263
+ self.add_verbose_argument(parser)
264
+
265
+ def execute(self, args: argparse.Namespace) -> None:
266
+ """Execute attribute deletion."""
267
+ has_path_name = args.path and args.name
268
+ has_guid = args.guid is not None
269
+
270
+ if has_path_name and has_guid:
271
+ raise CliExecutionError("Only one of --path + --name or --guid may be specified")
272
+ if not has_path_name and not has_guid:
273
+ raise CliExecutionError("Either --path + --name or --guid must be specified")
274
+
275
+ if has_guid:
276
+ attribute = self._resolve_attribute_by_guid(args.guid)
277
+ classifier = attribute.get_owner()
278
+ label = f"GUID '{args.guid}'"
279
+ else:
280
+ classifier = self._resolve_classifier(args.path)
281
+ attribute = self._resolve_attribute(classifier, args.name)
282
+ label = args.name
283
+
284
+ try:
285
+ classifier.delete_attribute(attribute)
286
+ self.logger.info("Deleted attribute: %s", label)
287
+ except Exception as e:
288
+ self._handle_execution_error(e, f"Failed to delete attribute '{label}'")
289
+
290
+
291
+ class AttributeListAction(AbstractAttributeAction):
292
+ """List attributes on a classifier.
293
+
294
+ SWR_ATTR_00004: Attribute List Command
295
+ SWR_ATTR_00008: Multi-Format Output
296
+ """
297
+
298
+ def __init__(self) -> None:
299
+ """Initialize the 'list' action."""
300
+ super().__init__(command_id="list")
301
+
302
+ def init_arguments(self, sub_parser: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
303
+ """Register the 'list' subcommand and its arguments."""
304
+ parser = sub_parser.add_parser("list", help="List attributes on a classifier")
305
+ self.add_path_argument(parser, required=True, help_text="Classifier path")
306
+ parser.add_argument("--format", choices=["table", "json", "csv"], default="table", help="Output format")
307
+ parser.add_argument("--output", default=None, help="Write output to file")
308
+ self.add_verbose_argument(parser)
309
+
310
+ def execute(self, args: argparse.Namespace) -> None:
311
+ """Execute attribute list."""
312
+ classifier = self._resolve_classifier(args.path)
313
+
314
+ try:
315
+ attr_names = self._collect_attribute_names(classifier)
316
+ output = self._format_output(attr_names, args.format)
317
+
318
+ if args.output:
319
+ self._write_to_file(args.output, output)
320
+ self.logger.info("Wrote %d attributes to: %s", len(attr_names), args.output)
321
+ else:
322
+ print(output)
323
+ except CliExecutionError:
324
+ raise
325
+ except Exception as e:
326
+ self._handle_execution_error(e, f"Failed to list attributes in '{args.path}'")
327
+
328
+ def _collect_attribute_names(self, classifier: Any) -> List[str]:
329
+ """Collect names of attributes on a classifier."""
330
+ attributes = classifier.get_attributes()
331
+ return [attr.get_name() for attr in attributes]
332
+
333
+ def _format_output(self, attr_names: List[str], format_type: str) -> str:
334
+ """Format output based on format parameter."""
335
+ if format_type == "json":
336
+ return OutputFormatter.json_format(attr_names)
337
+ elif format_type == "csv":
338
+ table_rows = [[name] for name in attr_names]
339
+ return OutputFormatter.csv_format(["Name"], table_rows)
340
+ else:
341
+ table_rows = [[name] for name in attr_names]
342
+ return OutputFormatter.table(["Name"], table_rows)
343
+
344
+ def _write_to_file(self, file_path: str, content: str) -> None:
345
+ """Write content to file."""
346
+ try:
347
+ with open(file_path, "w", encoding="utf-8") as f:
348
+ f.write(content)
349
+ except OSError as e:
350
+ raise CliExecutionError(f"Failed to write file '{file_path}': {e}") from e
351
+
352
+
353
+ class AttributeViewAction(AbstractAttributeAction):
354
+ """View attribute details.
355
+
356
+ SWR_ATTR_00003: Attribute View Command
357
+ SWR_ATTR_00008: Multi-Format Output
358
+ SWR_ATTR_00010: GUID Lookup Support
359
+ """
360
+
361
+ _VIEW_HEADERS = [
362
+ "Name",
363
+ "GUID",
364
+ "Description",
365
+ "Type",
366
+ "DefaultValue",
367
+ "Multiplicity",
368
+ "IsStatic",
369
+ "Visibility",
370
+ "Declaration",
371
+ "MetaClass",
372
+ "FullPath",
373
+ ]
374
+ _VIEW_KEYS = [
375
+ "name",
376
+ "guid",
377
+ "description",
378
+ "type",
379
+ "defaultValue",
380
+ "multiplicity",
381
+ "isStatic",
382
+ "visibility",
383
+ "declaration",
384
+ "metaClass",
385
+ "fullPath",
386
+ ]
387
+
388
+ def __init__(self) -> None:
389
+ """Initialize the 'view' action."""
390
+ super().__init__(command_id="view")
391
+
392
+ def init_arguments(self, sub_parser: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
393
+ """Register the 'view' subcommand and its arguments."""
394
+ parser = sub_parser.add_parser("view", help="View attribute details")
395
+ self.add_path_argument(parser, required=False, help_text="Parent classifier path")
396
+ parser.add_argument("--guid", default=None, help="Attribute GUID to view")
397
+ parser.add_argument("--name", default=None, help="Attribute name within classifier")
398
+ parser.add_argument("--format", choices=["table", "json", "csv"], default="table", help="Output format")
399
+ parser.add_argument("--output", default=None, help="Write output to file")
400
+ self.add_verbose_argument(parser)
401
+
402
+ def execute(self, args: argparse.Namespace) -> None:
403
+ """Execute attribute view."""
404
+ has_path_name = args.path and args.name
405
+ has_guid = args.guid is not None
406
+
407
+ if has_path_name and has_guid:
408
+ raise CliExecutionError("Only one of --path + --name or --guid may be specified")
409
+ if not has_path_name and not has_guid:
410
+ raise CliExecutionError("Either --path + --name or --guid must be specified")
411
+
412
+ if has_guid:
413
+ attribute = self._resolve_attribute_by_guid(args.guid)
414
+ else:
415
+ classifier = self._resolve_classifier(args.path)
416
+ attribute = self._resolve_attribute(classifier, args.name)
417
+
418
+ try:
419
+ data = self._collect_attribute_data(attribute)
420
+ output = self._format_output(data, args.format)
421
+
422
+ if args.output:
423
+ self._write_to_file(args.output, output)
424
+ self.logger.info("Wrote attribute details to: %s", args.output)
425
+ else:
426
+ print(output)
427
+ except CliExecutionError:
428
+ raise
429
+ except Exception as e:
430
+ self._handle_execution_error(e, f"Failed to view attribute '{args.name or args.guid}'")
431
+
432
+ def _collect_attribute_data(self, attribute: Any) -> Dict[str, Any]:
433
+ """Collect attribute details into a data dictionary.
434
+
435
+ Normalizes boolean flags to int for clean JSON round-trip.
436
+ """
437
+ attr_type = attribute.get_type()
438
+ type_name = attr_type.get_name() if attr_type is not None else ""
439
+ return {
440
+ "name": attribute.get_name(),
441
+ "guid": attribute.get_guid(),
442
+ "description": attribute.get_description(),
443
+ "type": type_name,
444
+ "defaultValue": attribute.get_default_value(),
445
+ "multiplicity": attribute.get_multiplicity(),
446
+ "isStatic": int(attribute.get_is_static()),
447
+ "visibility": attribute.get_visibility(),
448
+ "declaration": attribute.get_declaration(),
449
+ "metaClass": attribute.get_meta_class(),
450
+ "fullPath": attribute.get_full_path_name(),
451
+ }
452
+
453
+ def _format_output(self, data: Dict[str, Any], format_type: str) -> str:
454
+ """Format output based on format parameter."""
455
+ if format_type == "json":
456
+ return OutputFormatter.json_format(data)
457
+ elif format_type == "csv":
458
+ data_row = [data[key] for key in self._VIEW_KEYS]
459
+ return OutputFormatter.csv_format(self._VIEW_HEADERS, [data_row])
460
+ else:
461
+ table_rows = [
462
+ ["Name", data["name"]],
463
+ ["GUID", data["guid"]],
464
+ ["Description", data["description"]],
465
+ ["Type", data["type"]],
466
+ ["DefaultValue", data["defaultValue"]],
467
+ ["Multiplicity", data["multiplicity"]],
468
+ ["IsStatic", data["isStatic"]],
469
+ ["Visibility", data["visibility"]],
470
+ ["Declaration", data["declaration"]],
471
+ ["MetaClass", data["metaClass"]],
472
+ ["FullPath", data["fullPath"]],
473
+ ]
474
+ return OutputFormatter.table(["Property", "Value"], table_rows)
475
+
476
+ def _write_to_file(self, file_path: str, content: str) -> None:
477
+ """Write content to file."""
478
+ try:
479
+ with open(file_path, "w", encoding="utf-8") as f:
480
+ f.write(content)
481
+ except OSError as e:
482
+ raise CliExecutionError(f"Failed to write file '{file_path}': {e}") from e
483
+
484
+
485
+ class AttributeUpdateAction(AbstractAttributeAction):
486
+ """Update attributes of an existing attribute.
487
+
488
+ SWR_ATTR_00005: Attribute Update Command
489
+ SWR_ATTR_00007: External JSON File Support
490
+ SWR_ATTR_00010: GUID Lookup Support
491
+ SWR_ATTR_00011: Type Resolution
492
+ SWR_ATTR_00012: IsStatic Flag Support
493
+ """
494
+
495
+ VALID_ATTRIBUTES = {
496
+ "name",
497
+ "type",
498
+ "defaultValue",
499
+ "multiplicity",
500
+ "isStatic",
501
+ "visibility",
502
+ "declaration",
503
+ "description",
504
+ }
505
+
506
+ def __init__(self) -> None:
507
+ """Initialize the 'update' action."""
508
+ super().__init__(command_id="update")
509
+
510
+ def init_arguments(self, sub_parser: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
511
+ """Register the 'update' subcommand and its arguments."""
512
+ parser = sub_parser.add_parser("update", help="Update attributes of an existing attribute")
513
+ self.add_path_argument(parser, required=False, help_text="Parent classifier path")
514
+ parser.add_argument("--guid", default=None, help="Attribute GUID to update")
515
+ parser.add_argument("--name", default=None, help="Attribute name within classifier")
516
+ parser.add_argument("--input", default=None, help="JSON file with attribute attributes")
517
+ parser.add_argument("attributes", nargs="?", default=None, help="Inline JSON or JSON file path")
518
+ self.add_verbose_argument(parser)
519
+
520
+ def execute(self, args: argparse.Namespace) -> None:
521
+ """Execute attribute update."""
522
+ has_path_name = args.path and args.name
523
+ has_guid = args.guid is not None
524
+
525
+ if has_path_name and has_guid:
526
+ raise CliExecutionError("Only one of --path + --name or --guid may be specified")
527
+ if not has_path_name and not has_guid:
528
+ raise CliExecutionError("Either --path + --name or --guid must be specified")
529
+
530
+ if has_guid:
531
+ attribute = self._resolve_attribute_by_guid(args.guid)
532
+ classifier = attribute.get_owner()
533
+ else:
534
+ classifier = self._resolve_classifier(args.path)
535
+ attribute = self._resolve_attribute(classifier, args.name)
536
+
537
+ input_data = args.input if args.input else args.attributes
538
+ if not input_data:
539
+ raise CliExecutionError("Either --input or attributes argument must be provided")
540
+
541
+ data = self._load_json_data(input_data)
542
+
543
+ unknown = set(data.keys()) - self.VALID_ATTRIBUTES
544
+ if unknown:
545
+ self.logger.warning("Skipping unknown attributes: %s", unknown)
546
+
547
+ self._set_attributes(classifier, attribute, data)
548
+
549
+ self.logger.info("Successfully updated attribute: %s", attribute.get_name())
550
+
551
+ def _load_json_data(self, attributes_input: str) -> Any:
552
+ """Load JSON data from inline string or external file.
553
+
554
+ SWR_ATTR_00007: External JSON File Support
555
+ """
556
+ if attributes_input.startswith("{") or attributes_input.startswith("["):
557
+ try:
558
+ return json.loads(attributes_input)
559
+ except json.JSONDecodeError as e:
560
+ raise CliExecutionError(f"Invalid JSON: {e}") from e
561
+
562
+ if not Path(attributes_input).exists():
563
+ raise CliExecutionError(f"File not found: {attributes_input}")
564
+
565
+ try:
566
+ with open(attributes_input, encoding="utf-8") as f:
567
+ return json.load(f)
568
+ except json.JSONDecodeError as e:
569
+ raise CliExecutionError(f"Invalid JSON in file: {e}") from e
570
+ except OSError as e:
571
+ raise CliExecutionError(f"Failed to read file: {e}") from e
572
+
573
+ def _set_attributes(self, classifier: Any, attribute: Any, attrs: Dict[str, Any]) -> None:
574
+ """Set validated attributes on attribute (partial update)."""
575
+ if "name" in attrs:
576
+ attribute.set_name(attrs["name"])
577
+ if "defaultValue" in attrs:
578
+ attribute.set_default_value(attrs["defaultValue"])
579
+ if "multiplicity" in attrs:
580
+ attribute.set_multiplicity(attrs["multiplicity"])
581
+ if "visibility" in attrs:
582
+ attribute.set_visibility(attrs["visibility"])
583
+ if "declaration" in attrs:
584
+ attribute.set_declaration(attrs["declaration"])
585
+ if "description" in attrs:
586
+ attribute.set_description(attrs["description"])
587
+ if "isStatic" in attrs:
588
+ attribute.set_is_static(1 if attrs["isStatic"] else 0)
589
+ if "type" in attrs:
590
+ type_name = attrs["type"]
591
+ owner = classifier.get_owner()
592
+ target = owner.find_nested_classifier_recursive(type_name)
593
+ if target is None:
594
+ raise CliExecutionError(f"Type '{type_name}' not found")
595
+ attribute.set_type(target)