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.
- rhapsody_cli/__init__.py +21 -0
- rhapsody_cli/actions/__init__.py +17 -0
- rhapsody_cli/actions/abstract_action.py +259 -0
- rhapsody_cli/actions/attribute_action.py +595 -0
- rhapsody_cli/actions/class_action.py +708 -0
- rhapsody_cli/actions/operation_action.py +606 -0
- rhapsody_cli/actions/package_action.py +592 -0
- rhapsody_cli/actions/port_action.py +565 -0
- rhapsody_cli/actions/project_action.py +119 -0
- rhapsody_cli/application.py +390 -0
- rhapsody_cli/cli/__init__.py +5 -0
- rhapsody_cli/cli/cli.py +102 -0
- rhapsody_cli/cli/formatters.py +47 -0
- rhapsody_cli/cli/logging_config.py +39 -0
- rhapsody_cli/cli/main.py +6 -0
- rhapsody_cli/cli/path_resolver.py +117 -0
- rhapsody_cli/com_utils.py +54 -0
- rhapsody_cli/commands/__init__.py +9 -0
- rhapsody_cli/commands/abstract_command.py +92 -0
- rhapsody_cli/commands/attribute_command.py +36 -0
- rhapsody_cli/commands/class_command.py +38 -0
- rhapsody_cli/commands/operation_command.py +36 -0
- rhapsody_cli/commands/package_command.py +36 -0
- rhapsody_cli/commands/port_command.py +36 -0
- rhapsody_cli/commands/project_command.py +34 -0
- rhapsody_cli/exceptions/__init__.py +9 -0
- rhapsody_cli/exceptions/core.py +42 -0
- rhapsody_cli/models/__init__.py +16 -0
- rhapsody_cli/models/core.py +2263 -0
- rhapsody_cli/models/elements/__init__.py +15 -0
- rhapsody_cli/models/elements/activity/__init__.py +33 -0
- rhapsody_cli/models/elements/activity/model_actions.py +358 -0
- rhapsody_cli/models/elements/activity/model_activity.py +744 -0
- rhapsody_cli/models/elements/classifiers/__init__.py +27 -0
- rhapsody_cli/models/elements/classifiers/model_actor.py +76 -0
- rhapsody_cli/models/elements/classifiers/model_association_class.py +21 -0
- rhapsody_cli/models/elements/classifiers/model_class.py +461 -0
- rhapsody_cli/models/elements/classifiers/model_classifier.py +870 -0
- rhapsody_cli/models/elements/classifiers/model_interface_item.py +138 -0
- rhapsody_cli/models/elements/classifiers/model_operation.py +182 -0
- rhapsody_cli/models/elements/classifiers/model_statechart.py +108 -0
- rhapsody_cli/models/elements/classifiers/model_stereotype.py +23 -0
- rhapsody_cli/models/elements/classifiers/model_usecase.py +86 -0
- rhapsody_cli/models/elements/common/__init__.py +21 -0
- rhapsody_cli/models/elements/common/model_misc.py +45 -0
- rhapsody_cli/models/elements/common/model_other_model.py +822 -0
- rhapsody_cli/models/elements/containment/__init__.py +29 -0
- rhapsody_cli/models/elements/containment/model_collaboration.py +55 -0
- rhapsody_cli/models/elements/containment/model_component.py +59 -0
- rhapsody_cli/models/elements/containment/model_component_instance.py +20 -0
- rhapsody_cli/models/elements/containment/model_configuration.py +62 -0
- rhapsody_cli/models/elements/containment/model_module.py +17 -0
- rhapsody_cli/models/elements/containment/model_node.py +22 -0
- rhapsody_cli/models/elements/containment/model_package.py +285 -0
- rhapsody_cli/models/elements/containment/model_profile.py +17 -0
- rhapsody_cli/models/elements/containment/model_project.py +248 -0
- rhapsody_cli/models/elements/diagrams/__init__.py +31 -0
- rhapsody_cli/models/elements/diagrams/model_diagram_types.py +285 -0
- rhapsody_cli/models/elements/diagrams/model_diagrams.py +114 -0
- rhapsody_cli/models/elements/graphics/__init__.py +35 -0
- rhapsody_cli/models/elements/graphics/model_graphics.py +3053 -0
- rhapsody_cli/models/elements/interactions/__init__.py +29 -0
- rhapsody_cli/models/elements/interactions/model_interactions.py +1336 -0
- rhapsody_cli/models/elements/relations/__init__.py +23 -0
- rhapsody_cli/models/elements/relations/model_association_role.py +20 -0
- rhapsody_cli/models/elements/relations/model_dependency.py +23 -0
- rhapsody_cli/models/elements/relations/model_generalization.py +26 -0
- rhapsody_cli/models/elements/relations/model_hyperlink.py +25 -0
- rhapsody_cli/models/elements/relations/model_instance.py +224 -0
- rhapsody_cli/models/elements/relations/model_port.py +227 -0
- rhapsody_cli/models/elements/relations/model_relation.py +474 -0
- rhapsody_cli/models/elements/requirements/__init__.py +11 -0
- rhapsody_cli/models/elements/requirements/model_requirements.py +176 -0
- rhapsody_cli/models/elements/statemachine/__init__.py +11 -0
- rhapsody_cli/models/elements/statemachine/model_statemachine.py +793 -0
- rhapsody_cli/models/elements/templates/__init__.py +13 -0
- rhapsody_cli/models/elements/templates/model_templates.py +172 -0
- rhapsody_cli/models/elements/values/__init__.py +17 -0
- rhapsody_cli/models/elements/values/model_values.py +282 -0
- rhapsody_cli/models/elements/variables/__init__.py +15 -0
- rhapsody_cli/models/elements/variables/model_variables.py +301 -0
- rhapsody_cli/models/support/__init__.py +5 -0
- rhapsody_cli/models/support/model_codegen.py +1749 -0
- rhapsody_cli/models/support/model_files.py +438 -0
- rhapsody_cli/models/support/model_ide.py +782 -0
- rhapsody_cli/py.typed +0 -0
- rhapsody_cli-0.1.0.dist-info/METADATA +395 -0
- rhapsody_cli-0.1.0.dist-info/RECORD +92 -0
- rhapsody_cli-0.1.0.dist-info/WHEEL +5 -0
- rhapsody_cli-0.1.0.dist-info/entry_points.txt +2 -0
- rhapsody_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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)
|