pygeai-orchestration 0.1.0b2__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 (61) hide show
  1. pygeai_orchestration/__init__.py +99 -0
  2. pygeai_orchestration/cli/__init__.py +7 -0
  3. pygeai_orchestration/cli/__main__.py +11 -0
  4. pygeai_orchestration/cli/commands/__init__.py +13 -0
  5. pygeai_orchestration/cli/commands/base.py +192 -0
  6. pygeai_orchestration/cli/error_handler.py +123 -0
  7. pygeai_orchestration/cli/formatters.py +419 -0
  8. pygeai_orchestration/cli/geai_orch.py +270 -0
  9. pygeai_orchestration/cli/interactive.py +265 -0
  10. pygeai_orchestration/cli/texts/help.py +169 -0
  11. pygeai_orchestration/core/__init__.py +130 -0
  12. pygeai_orchestration/core/base/__init__.py +23 -0
  13. pygeai_orchestration/core/base/agent.py +121 -0
  14. pygeai_orchestration/core/base/geai_agent.py +144 -0
  15. pygeai_orchestration/core/base/geai_orchestrator.py +77 -0
  16. pygeai_orchestration/core/base/orchestrator.py +142 -0
  17. pygeai_orchestration/core/base/pattern.py +161 -0
  18. pygeai_orchestration/core/base/tool.py +149 -0
  19. pygeai_orchestration/core/common/__init__.py +18 -0
  20. pygeai_orchestration/core/common/context.py +140 -0
  21. pygeai_orchestration/core/common/memory.py +176 -0
  22. pygeai_orchestration/core/common/message.py +50 -0
  23. pygeai_orchestration/core/common/state.py +181 -0
  24. pygeai_orchestration/core/composition.py +190 -0
  25. pygeai_orchestration/core/config.py +356 -0
  26. pygeai_orchestration/core/exceptions.py +400 -0
  27. pygeai_orchestration/core/handlers.py +380 -0
  28. pygeai_orchestration/core/utils/__init__.py +37 -0
  29. pygeai_orchestration/core/utils/cache.py +138 -0
  30. pygeai_orchestration/core/utils/config.py +94 -0
  31. pygeai_orchestration/core/utils/logging.py +57 -0
  32. pygeai_orchestration/core/utils/metrics.py +184 -0
  33. pygeai_orchestration/core/utils/validators.py +140 -0
  34. pygeai_orchestration/dev/__init__.py +15 -0
  35. pygeai_orchestration/dev/debug.py +288 -0
  36. pygeai_orchestration/dev/templates.py +321 -0
  37. pygeai_orchestration/dev/testing.py +301 -0
  38. pygeai_orchestration/patterns/__init__.py +15 -0
  39. pygeai_orchestration/patterns/multi_agent.py +237 -0
  40. pygeai_orchestration/patterns/planning.py +219 -0
  41. pygeai_orchestration/patterns/react.py +221 -0
  42. pygeai_orchestration/patterns/reflection.py +134 -0
  43. pygeai_orchestration/patterns/tool_use.py +170 -0
  44. pygeai_orchestration/tests/__init__.py +1 -0
  45. pygeai_orchestration/tests/test_base_classes.py +187 -0
  46. pygeai_orchestration/tests/test_cache.py +184 -0
  47. pygeai_orchestration/tests/test_cli_formatters.py +232 -0
  48. pygeai_orchestration/tests/test_common.py +214 -0
  49. pygeai_orchestration/tests/test_composition.py +265 -0
  50. pygeai_orchestration/tests/test_config.py +301 -0
  51. pygeai_orchestration/tests/test_dev_utils.py +337 -0
  52. pygeai_orchestration/tests/test_exceptions.py +327 -0
  53. pygeai_orchestration/tests/test_handlers.py +307 -0
  54. pygeai_orchestration/tests/test_metrics.py +171 -0
  55. pygeai_orchestration/tests/test_patterns.py +165 -0
  56. pygeai_orchestration-0.1.0b2.dist-info/METADATA +290 -0
  57. pygeai_orchestration-0.1.0b2.dist-info/RECORD +61 -0
  58. pygeai_orchestration-0.1.0b2.dist-info/WHEEL +5 -0
  59. pygeai_orchestration-0.1.0b2.dist-info/entry_points.txt +2 -0
  60. pygeai_orchestration-0.1.0b2.dist-info/licenses/LICENSE +8 -0
  61. pygeai_orchestration-0.1.0b2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,419 @@
1
+ """CLI output formatters and styling utilities."""
2
+
3
+ import sys
4
+ from enum import Enum
5
+ from typing import Any, Dict, List, Optional
6
+
7
+
8
+ class Color(str, Enum):
9
+ """ANSI color codes."""
10
+
11
+ RESET = "\033[0m"
12
+ BOLD = "\033[1m"
13
+ DIM = "\033[2m"
14
+ UNDERLINE = "\033[4m"
15
+
16
+ BLACK = "\033[30m"
17
+ RED = "\033[31m"
18
+ GREEN = "\033[32m"
19
+ YELLOW = "\033[33m"
20
+ BLUE = "\033[34m"
21
+ MAGENTA = "\033[35m"
22
+ CYAN = "\033[36m"
23
+ WHITE = "\033[37m"
24
+
25
+ BRIGHT_BLACK = "\033[90m"
26
+ BRIGHT_RED = "\033[91m"
27
+ BRIGHT_GREEN = "\033[92m"
28
+ BRIGHT_YELLOW = "\033[93m"
29
+ BRIGHT_BLUE = "\033[94m"
30
+ BRIGHT_MAGENTA = "\033[95m"
31
+ BRIGHT_CYAN = "\033[96m"
32
+ BRIGHT_WHITE = "\033[97m"
33
+
34
+
35
+ class Symbol(str, Enum):
36
+ """Unicode symbols for output."""
37
+
38
+ SUCCESS = "✓"
39
+ ERROR = "✗"
40
+ WARNING = "⚠"
41
+ INFO = "ℹ"
42
+ ARROW = "→"
43
+ BULLET = "•"
44
+ SPINNER = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
45
+
46
+
47
+ class OutputFormatter:
48
+ """Format CLI output with colors and styling."""
49
+
50
+ def __init__(self, use_color: bool = True):
51
+ """Initialize formatter.
52
+
53
+ Args:
54
+ use_color: Enable color output
55
+ """
56
+ self.use_color = use_color and sys.stdout.isatty()
57
+
58
+ def colorize(self, text: str, color: Color, bold: bool = False) -> str:
59
+ """Apply color to text.
60
+
61
+ Args:
62
+ text: Text to colorize
63
+ color: Color to apply
64
+ bold: Make text bold
65
+
66
+ Returns:
67
+ Colorized text
68
+ """
69
+ if not self.use_color:
70
+ return text
71
+
72
+ result = f"{color}{text}{Color.RESET}"
73
+ if bold:
74
+ result = f"{Color.BOLD}{result}"
75
+ return result
76
+
77
+ def success(self, message: str) -> str:
78
+ """Format success message.
79
+
80
+ Args:
81
+ message: Message text
82
+
83
+ Returns:
84
+ Formatted message
85
+ """
86
+ symbol = self.colorize(Symbol.SUCCESS.value, Color.GREEN, bold=True)
87
+ return f"{symbol} {message}"
88
+
89
+ def error(self, message: str) -> str:
90
+ """Format error message.
91
+
92
+ Args:
93
+ message: Message text
94
+
95
+ Returns:
96
+ Formatted message
97
+ """
98
+ symbol = self.colorize(Symbol.ERROR.value, Color.RED, bold=True)
99
+ return f"{symbol} {message}"
100
+
101
+ def warning(self, message: str) -> str:
102
+ """Format warning message.
103
+
104
+ Args:
105
+ message: Message text
106
+
107
+ Returns:
108
+ Formatted message
109
+ """
110
+ symbol = self.colorize(Symbol.WARNING.value, Color.YELLOW, bold=True)
111
+ return f"{symbol} {message}"
112
+
113
+ def info(self, message: str) -> str:
114
+ """Format info message.
115
+
116
+ Args:
117
+ message: Message text
118
+
119
+ Returns:
120
+ Formatted message
121
+ """
122
+ symbol = self.colorize(Symbol.INFO.value, Color.BLUE, bold=True)
123
+ return f"{symbol} {message}"
124
+
125
+ def heading(self, text: str, level: int = 1) -> str:
126
+ """Format heading.
127
+
128
+ Args:
129
+ text: Heading text
130
+ level: Heading level (1-3)
131
+
132
+ Returns:
133
+ Formatted heading
134
+ """
135
+ if level == 1:
136
+ return self.colorize(text, Color.CYAN, bold=True)
137
+ elif level == 2:
138
+ return self.colorize(text, Color.BLUE, bold=True)
139
+ else:
140
+ return self.colorize(text, Color.WHITE, bold=True)
141
+
142
+ def dim(self, text: str) -> str:
143
+ """Format dim text.
144
+
145
+ Args:
146
+ text: Text to dim
147
+
148
+ Returns:
149
+ Formatted text
150
+ """
151
+ if not self.use_color:
152
+ return text
153
+ return f"{Color.DIM}{text}{Color.RESET}"
154
+
155
+ def bold(self, text: str) -> str:
156
+ """Format bold text.
157
+
158
+ Args:
159
+ text: Text to make bold
160
+
161
+ Returns:
162
+ Formatted text
163
+ """
164
+ if not self.use_color:
165
+ return text
166
+ return f"{Color.BOLD}{text}{Color.RESET}"
167
+
168
+ def key_value(self, key: str, value: Any, indent: int = 0) -> str:
169
+ """Format key-value pair.
170
+
171
+ Args:
172
+ key: Key text
173
+ value: Value
174
+ indent: Indentation level
175
+
176
+ Returns:
177
+ Formatted pair
178
+ """
179
+ spaces = " " * indent
180
+ key_formatted = self.colorize(key, Color.CYAN)
181
+ return f"{spaces}{key_formatted}: {value}"
182
+
183
+ def bullet_list(self, items: List[str], indent: int = 0) -> str:
184
+ """Format bullet list.
185
+
186
+ Args:
187
+ items: List items
188
+ indent: Indentation level
189
+
190
+ Returns:
191
+ Formatted list
192
+ """
193
+ spaces = " " * indent
194
+ bullet = self.colorize(Symbol.BULLET.value, Color.BLUE)
195
+ lines = [f"{spaces}{bullet} {item}" for item in items]
196
+ return "\n".join(lines)
197
+
198
+ def section(self, title: str, content: str) -> str:
199
+ """Format section with title.
200
+
201
+ Args:
202
+ title: Section title
203
+ content: Section content
204
+
205
+ Returns:
206
+ Formatted section
207
+ """
208
+ separator = self.dim("─" * 50)
209
+ title_formatted = self.heading(title, level=2)
210
+ return f"\n{title_formatted}\n{separator}\n{content}\n"
211
+
212
+ def table(self, headers: List[str], rows: List[List[str]]) -> str:
213
+ """Format simple table.
214
+
215
+ Args:
216
+ headers: Table headers
217
+ rows: Table rows
218
+
219
+ Returns:
220
+ Formatted table
221
+ """
222
+ if not rows:
223
+ return ""
224
+
225
+ col_widths = [len(h) for h in headers]
226
+ for row in rows:
227
+ for i, cell in enumerate(row):
228
+ col_widths[i] = max(col_widths[i], len(str(cell)))
229
+
230
+ header_line = " ".join(
231
+ self.bold(h.ljust(w)) for h, w in zip(headers, col_widths)
232
+ )
233
+ separator = self.dim("─" * (sum(col_widths) + 2 * (len(headers) - 1)))
234
+
235
+ row_lines = []
236
+ for row in rows:
237
+ row_line = " ".join(
238
+ str(cell).ljust(w) for cell, w in zip(row, col_widths)
239
+ )
240
+ row_lines.append(row_line)
241
+
242
+ return f"{header_line}\n{separator}\n" + "\n".join(row_lines)
243
+
244
+ def json_tree(self, data: Dict[str, Any], indent: int = 0) -> str:
245
+ """Format JSON-like tree structure.
246
+
247
+ Args:
248
+ data: Dictionary data
249
+ indent: Current indentation level
250
+
251
+ Returns:
252
+ Formatted tree
253
+ """
254
+ lines = []
255
+ spaces = " " * indent
256
+
257
+ for key, value in data.items():
258
+ key_formatted = self.colorize(key, Color.CYAN)
259
+
260
+ if isinstance(value, dict):
261
+ lines.append(f"{spaces}{key_formatted}:")
262
+ lines.append(self.json_tree(value, indent + 1))
263
+ elif isinstance(value, list):
264
+ lines.append(f"{spaces}{key_formatted}:")
265
+ for item in value:
266
+ if isinstance(item, dict):
267
+ lines.append(self.json_tree(item, indent + 1))
268
+ else:
269
+ lines.append(f"{spaces} {Symbol.BULLET.value} {item}")
270
+ else:
271
+ value_str = self.colorize(str(value), Color.GREEN)
272
+ lines.append(f"{spaces}{key_formatted}: {value_str}")
273
+
274
+ return "\n".join(lines)
275
+
276
+
277
+ class ProgressBar:
278
+ """Simple progress bar for CLI."""
279
+
280
+ def __init__(
281
+ self,
282
+ total: int,
283
+ width: int = 40,
284
+ formatter: Optional[OutputFormatter] = None
285
+ ):
286
+ """Initialize progress bar.
287
+
288
+ Args:
289
+ total: Total number of items
290
+ width: Width of progress bar
291
+ formatter: Output formatter
292
+ """
293
+ self.total = total
294
+ self.width = width
295
+ self.current = 0
296
+ self.formatter = formatter or OutputFormatter()
297
+
298
+ def update(self, n: int = 1) -> None:
299
+ """Update progress.
300
+
301
+ Args:
302
+ n: Number of items completed
303
+ """
304
+ self.current += n
305
+ self._render()
306
+
307
+ def _render(self) -> None:
308
+ """Render progress bar."""
309
+ if not sys.stdout.isatty():
310
+ return
311
+
312
+ percent = self.current / self.total if self.total > 0 else 0
313
+ filled = int(self.width * percent)
314
+ bar = "█" * filled + "░" * (self.width - filled)
315
+
316
+ bar_colored = self.formatter.colorize(bar, Color.GREEN)
317
+ percent_str = f"{percent * 100:.0f}%"
318
+
319
+ line = f"\r[{bar_colored}] {percent_str} ({self.current}/{self.total})"
320
+ sys.stdout.write(line)
321
+ sys.stdout.flush()
322
+
323
+ if self.current >= self.total:
324
+ sys.stdout.write("\n")
325
+ sys.stdout.flush()
326
+
327
+ def finish(self) -> None:
328
+ """Complete progress bar."""
329
+ self.current = self.total
330
+ self._render()
331
+
332
+
333
+ class Spinner:
334
+ """Animated spinner for CLI."""
335
+
336
+ def __init__(self, message: str = "", formatter: Optional[OutputFormatter] = None):
337
+ """Initialize spinner.
338
+
339
+ Args:
340
+ message: Message to display
341
+ formatter: Output formatter
342
+ """
343
+ self.message = message
344
+ self.formatter = formatter or OutputFormatter()
345
+ self.frames = list(Symbol.SPINNER)
346
+ self.current_frame = 0
347
+ self.running = False
348
+
349
+ def start(self) -> None:
350
+ """Start spinner."""
351
+ self.running = True
352
+ self._render()
353
+
354
+ def update(self, message: str) -> None:
355
+ """Update spinner message.
356
+
357
+ Args:
358
+ message: New message
359
+ """
360
+ self.message = message
361
+ self._render()
362
+
363
+ def _render(self) -> None:
364
+ """Render spinner frame."""
365
+ if not sys.stdout.isatty():
366
+ return
367
+
368
+ frame = self.frames[self.current_frame]
369
+ frame_colored = self.formatter.colorize(frame, Color.CYAN)
370
+
371
+ line = f"\r{frame_colored} {self.message}"
372
+ sys.stdout.write(line)
373
+ sys.stdout.flush()
374
+
375
+ self.current_frame = (self.current_frame + 1) % len(self.frames)
376
+
377
+ def stop(self, final_message: Optional[str] = None) -> None:
378
+ """Stop spinner.
379
+
380
+ Args:
381
+ final_message: Final message to display
382
+ """
383
+ self.running = False
384
+
385
+ if final_message:
386
+ sys.stdout.write(f"\r{final_message}\n")
387
+ else:
388
+ sys.stdout.write("\r" + " " * (len(self.message) + 2) + "\r")
389
+
390
+ sys.stdout.flush()
391
+
392
+
393
+ def format_error_details(
394
+ error: Exception,
395
+ formatter: Optional[OutputFormatter] = None
396
+ ) -> str:
397
+ """Format error with details.
398
+
399
+ Args:
400
+ error: Exception to format
401
+ formatter: Output formatter
402
+
403
+ Returns:
404
+ Formatted error message
405
+ """
406
+ fmt = formatter or OutputFormatter()
407
+
408
+ error_type = fmt.colorize(type(error).__name__, Color.RED, bold=True)
409
+ error_msg = str(error)
410
+
411
+ result = f"{error_type}: {error_msg}"
412
+
413
+ if hasattr(error, "__dict__"):
414
+ details = {k: v for k, v in error.__dict__.items() if not k.startswith("_")}
415
+ if details:
416
+ result += "\n\n" + fmt.heading("Details:", level=3)
417
+ result += "\n" + fmt.json_tree(details, indent=1)
418
+
419
+ return result
@@ -0,0 +1,270 @@
1
+ import sys
2
+ import logging
3
+ from typing import List, Optional
4
+
5
+ from pygeai_orchestration.core.utils import get_logger
6
+ from pygeai_orchestration.cli.commands.base import base_commands, base_options
7
+ from pygeai_orchestration.cli.commands import ArgumentsEnum, Command
8
+ from pygeai.cli.parsers import CommandParser
9
+ from pygeai_orchestration.cli.texts.help import CLI_USAGE
10
+ from pygeai_orchestration.cli.error_handler import ErrorHandler, ExitCode
11
+
12
+ logger = get_logger()
13
+ from pygeai.core.base.session import get_session
14
+ from pygeai.core.common.exceptions import (
15
+ UnknownArgumentError,
16
+ MissingRequirementException,
17
+ WrongArgumentError,
18
+ )
19
+ from pygeai.core.utils.console import Console
20
+
21
+
22
+ def setup_verbose_logging() -> None:
23
+ """
24
+ Configure verbose logging for the CLI.
25
+
26
+ Sets up a console handler with DEBUG level logging and a formatted output
27
+ that includes timestamp, logger name, level, and message.
28
+ """
29
+ if logger.handlers:
30
+ for handler in logger.handlers:
31
+ if not isinstance(handler, logging.NullHandler):
32
+ return
33
+
34
+ logger.setLevel(logging.DEBUG)
35
+ console_handler = logging.StreamHandler(sys.stderr)
36
+ console_handler.setLevel(logging.DEBUG)
37
+ formatter = logging.Formatter(
38
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
39
+ )
40
+ console_handler.setFormatter(formatter)
41
+ logger.addHandler(console_handler)
42
+ logger.propagate = False
43
+ logger.debug("Verbose mode enabled")
44
+
45
+
46
+ def main() -> int:
47
+ """
48
+ Main entry point for the geai-orch CLI application.
49
+
50
+ :return: int - Exit code indicating success or error.
51
+ """
52
+ try:
53
+ driver = CLIDriver()
54
+ return driver.main()
55
+ except MissingRequirementException as e:
56
+ error_msg = ErrorHandler.handle_missing_requirement(str(e))
57
+ Console.write_stderr(error_msg)
58
+ return ExitCode.MISSING_REQUIREMENT
59
+
60
+
61
+ class CLIDriver:
62
+ """
63
+ Main CLI driver for the geai-orch command-line interface.
64
+
65
+ The CLIDriver orchestrates command parsing, execution, and error handling
66
+ for all geai-orch CLI operations. It supports multi-profile session management
67
+ via the --alias flag and provides comprehensive error handling with
68
+ user-friendly messages.
69
+ """
70
+
71
+ def __init__(self, session=None, credentials_file=None) -> None:
72
+ """
73
+ Initialize the CLI driver with optional session and credentials file.
74
+
75
+ Sets up the session to be used while running commands, either with a
76
+ specified alias, environment variables, or function parameters.
77
+ Once the session is defined, it won't change during the execution.
78
+
79
+ :param session: Optional session object. If None, uses 'default' or
80
+ alias-specified session from command-line arguments.
81
+ :param credentials_file: Optional path to custom credentials file.
82
+ """
83
+ from pygeai.core.common.config import get_settings
84
+
85
+ arguments = sys.argv
86
+
87
+ if credentials_file or "--credentials" in arguments or "--creds" in arguments:
88
+ if not credentials_file:
89
+ credentials_file = self._get_credentials_file(arguments)
90
+ get_settings(credentials_file=credentials_file)
91
+ logger.debug(f"Using custom credentials file: {credentials_file}")
92
+
93
+ if "-a" in arguments or "--alias" in arguments:
94
+ alias = self._get_alias(arguments)
95
+ session = get_session(alias)
96
+
97
+ self.session = get_session("default") if session is None else session
98
+
99
+ def _get_alias(self, arguments: List[str]) -> str:
100
+ """
101
+ Retrieves and removes alias and alias flag from argument list.
102
+
103
+ :param arguments: List[str] - Command line arguments.
104
+ :return: str - The alias value.
105
+ :raises MissingRequirementException: If alias flag is present but no value provided.
106
+ """
107
+ alias_index = None
108
+
109
+ if "-a" in arguments:
110
+ alias_index = arguments.index("-a")
111
+ elif "--alias" in arguments:
112
+ alias_index = arguments.index("--alias")
113
+
114
+ try:
115
+ _ = arguments.pop(alias_index)
116
+ alias = arguments.pop(alias_index)
117
+ return alias
118
+ except IndexError:
119
+ Console.write_stderr(
120
+ "-a/--alias option requires an alias. Please provide a valid alias after the option"
121
+ )
122
+ raise MissingRequirementException("Couldn't find a valid alias in parameter list.")
123
+
124
+ def _get_credentials_file(self, arguments: List[str]) -> str:
125
+ """
126
+ Retrieves and removes credentials file path and flag from argument list.
127
+
128
+ :param arguments: List[str] - Command line arguments.
129
+ :return: str - The credentials file path.
130
+ :raises MissingRequirementException: If credentials flag is present but no value provided.
131
+ """
132
+ creds_index = None
133
+
134
+ if "--credentials" in arguments:
135
+ creds_index = arguments.index("--credentials")
136
+ elif "--creds" in arguments:
137
+ creds_index = arguments.index("--creds")
138
+
139
+ try:
140
+ _ = arguments.pop(creds_index)
141
+ credentials_file = arguments.pop(creds_index)
142
+ return credentials_file
143
+ except IndexError:
144
+ Console.write_stderr(
145
+ "--creds/--credentials option requires a file path. Please provide a valid path after the option."
146
+ )
147
+ raise MissingRequirementException("Couldn't find a valid path in parameter list.")
148
+
149
+ def main(self, args: Optional[List[str]] = None) -> int:
150
+ """
151
+ Execute the CLI command based on provided arguments.
152
+
153
+ If no argument is received, it defaults to help (first command in base_command list).
154
+ Otherwise, it parses the arguments received to identify the appropriate command and either
155
+ execute it or parse it again to detect subcommands.
156
+
157
+ :param args: Optional[List[str]] - Command line arguments. If None, uses sys.argv.
158
+ :return: int - Exit code (0 for success, non-zero for errors).
159
+ """
160
+ try:
161
+ argv = sys.argv if args is None else args
162
+
163
+ if "--verbose" in argv or "-v" in argv:
164
+ setup_verbose_logging()
165
+ argv_copy = [a for a in argv if a not in ("--verbose", "-v")]
166
+ if args is None:
167
+ sys.argv = argv_copy
168
+ else:
169
+ args = argv_copy
170
+ argv = argv_copy
171
+
172
+ logger.debug(f"Running geai-orch with: {' '.join(a for a in argv)}")
173
+ logger.debug(
174
+ f"Session: {self.session.alias if hasattr(self.session, 'alias') else 'default'}"
175
+ )
176
+
177
+ if len(argv) > 1:
178
+ arg = argv[1] if args is None else args[1]
179
+ arguments = argv[2:] if args is None else args[2:]
180
+
181
+ logger.debug(f"Identifying command for argument: {arg}")
182
+ command = CommandParser(base_commands, base_options).identify_command(arg)
183
+ logger.debug(f"Command identified: {command.name}")
184
+ else:
185
+ logger.debug("No arguments provided, defaulting to help command")
186
+ command = base_commands[0]
187
+ arguments = []
188
+
189
+ self.process_command(command, arguments)
190
+ logger.debug("Command completed successfully")
191
+ return ExitCode.SUCCESS
192
+ except UnknownArgumentError as e:
193
+ if hasattr(e, "available_commands") and e.available_commands:
194
+ error_msg = ErrorHandler.handle_unknown_command(e.arg, e.available_commands)
195
+ elif hasattr(e, "available_options") and e.available_options:
196
+ error_msg = ErrorHandler.handle_unknown_option(e.arg, e.available_options)
197
+ else:
198
+ error_msg = ErrorHandler.format_error("Unknown Argument", str(e))
199
+
200
+ Console.write_stderr(error_msg)
201
+ return ExitCode.USER_INPUT_ERROR
202
+ except WrongArgumentError as e:
203
+ error_msg = ErrorHandler.handle_wrong_argument(str(e), CLI_USAGE)
204
+ Console.write_stderr(error_msg)
205
+ return ExitCode.USER_INPUT_ERROR
206
+ except MissingRequirementException as e:
207
+ error_msg = ErrorHandler.handle_missing_requirement(str(e))
208
+ Console.write_stderr(error_msg)
209
+ return ExitCode.MISSING_REQUIREMENT
210
+ except KeyboardInterrupt:
211
+ message = ErrorHandler.handle_keyboard_interrupt()
212
+ Console.write_stdout(message)
213
+ return ExitCode.KEYBOARD_INTERRUPT
214
+ except Exception as e:
215
+ error_msg = ErrorHandler.handle_unexpected_error(e)
216
+ Console.write_stderr(error_msg)
217
+ return ExitCode.UNEXPECTED_ERROR
218
+
219
+ def process_command(self, command: Command, arguments: list[str]):
220
+ """
221
+ Process a command by either executing it or identifying subcommands.
222
+
223
+ If the command has no action associated with it, it means it has subcommands,
224
+ so it must be parsed again to identify it.
225
+
226
+ :param command: Command - The command to process
227
+ :param arguments: list[str] - Additional arguments for the command
228
+ """
229
+ logger.debug(f"Processing command: {command.name}, arguments: {arguments}")
230
+
231
+ if command.action:
232
+ if command.additional_args == ArgumentsEnum.NOT_AVAILABLE:
233
+ logger.debug(f"Executing command {command.name} without arguments")
234
+ command.action()
235
+ else:
236
+ logger.debug(f"Extracting options for command {command.name}")
237
+ option_list = CommandParser(base_commands, command.options).extract_option_list(
238
+ arguments
239
+ )
240
+ logger.debug(f"Options extracted: {len(option_list)} items")
241
+ command.action(option_list)
242
+ elif command.subcommands:
243
+ subcommand_arg = arguments[0] if len(arguments) > 0 else None
244
+ subcommand_arguments = arguments[1:] if len(arguments) > 1 else []
245
+
246
+ logger.debug(f"Command has subcommands, identifying: {subcommand_arg}")
247
+
248
+ available_commands = command.subcommands
249
+ available_options = command.options
250
+ parser = CommandParser(available_commands, available_options)
251
+
252
+ if not subcommand_arg:
253
+ logger.debug(
254
+ f"No subcommand specified, using default: {command.subcommands[0].name}"
255
+ )
256
+ subcommand = command.subcommands[0]
257
+ else:
258
+ subcommand = parser.identify_command(subcommand_arg)
259
+ logger.debug(f"Subcommand identified: {subcommand.name}")
260
+
261
+ if subcommand.additional_args == ArgumentsEnum.NOT_AVAILABLE:
262
+ logger.debug(f"Executing subcommand {subcommand.name} without arguments")
263
+ subcommand.action()
264
+ else:
265
+ logger.debug(f"Extracting options for subcommand {subcommand.name}")
266
+ option_list = CommandParser(None, subcommand.options).extract_option_list(
267
+ subcommand_arguments
268
+ )
269
+ logger.debug(f"Options extracted: {len(option_list)} items")
270
+ subcommand.action(option_list)