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.
- pygeai_orchestration/__init__.py +99 -0
- pygeai_orchestration/cli/__init__.py +7 -0
- pygeai_orchestration/cli/__main__.py +11 -0
- pygeai_orchestration/cli/commands/__init__.py +13 -0
- pygeai_orchestration/cli/commands/base.py +192 -0
- pygeai_orchestration/cli/error_handler.py +123 -0
- pygeai_orchestration/cli/formatters.py +419 -0
- pygeai_orchestration/cli/geai_orch.py +270 -0
- pygeai_orchestration/cli/interactive.py +265 -0
- pygeai_orchestration/cli/texts/help.py +169 -0
- pygeai_orchestration/core/__init__.py +130 -0
- pygeai_orchestration/core/base/__init__.py +23 -0
- pygeai_orchestration/core/base/agent.py +121 -0
- pygeai_orchestration/core/base/geai_agent.py +144 -0
- pygeai_orchestration/core/base/geai_orchestrator.py +77 -0
- pygeai_orchestration/core/base/orchestrator.py +142 -0
- pygeai_orchestration/core/base/pattern.py +161 -0
- pygeai_orchestration/core/base/tool.py +149 -0
- pygeai_orchestration/core/common/__init__.py +18 -0
- pygeai_orchestration/core/common/context.py +140 -0
- pygeai_orchestration/core/common/memory.py +176 -0
- pygeai_orchestration/core/common/message.py +50 -0
- pygeai_orchestration/core/common/state.py +181 -0
- pygeai_orchestration/core/composition.py +190 -0
- pygeai_orchestration/core/config.py +356 -0
- pygeai_orchestration/core/exceptions.py +400 -0
- pygeai_orchestration/core/handlers.py +380 -0
- pygeai_orchestration/core/utils/__init__.py +37 -0
- pygeai_orchestration/core/utils/cache.py +138 -0
- pygeai_orchestration/core/utils/config.py +94 -0
- pygeai_orchestration/core/utils/logging.py +57 -0
- pygeai_orchestration/core/utils/metrics.py +184 -0
- pygeai_orchestration/core/utils/validators.py +140 -0
- pygeai_orchestration/dev/__init__.py +15 -0
- pygeai_orchestration/dev/debug.py +288 -0
- pygeai_orchestration/dev/templates.py +321 -0
- pygeai_orchestration/dev/testing.py +301 -0
- pygeai_orchestration/patterns/__init__.py +15 -0
- pygeai_orchestration/patterns/multi_agent.py +237 -0
- pygeai_orchestration/patterns/planning.py +219 -0
- pygeai_orchestration/patterns/react.py +221 -0
- pygeai_orchestration/patterns/reflection.py +134 -0
- pygeai_orchestration/patterns/tool_use.py +170 -0
- pygeai_orchestration/tests/__init__.py +1 -0
- pygeai_orchestration/tests/test_base_classes.py +187 -0
- pygeai_orchestration/tests/test_cache.py +184 -0
- pygeai_orchestration/tests/test_cli_formatters.py +232 -0
- pygeai_orchestration/tests/test_common.py +214 -0
- pygeai_orchestration/tests/test_composition.py +265 -0
- pygeai_orchestration/tests/test_config.py +301 -0
- pygeai_orchestration/tests/test_dev_utils.py +337 -0
- pygeai_orchestration/tests/test_exceptions.py +327 -0
- pygeai_orchestration/tests/test_handlers.py +307 -0
- pygeai_orchestration/tests/test_metrics.py +171 -0
- pygeai_orchestration/tests/test_patterns.py +165 -0
- pygeai_orchestration-0.1.0b2.dist-info/METADATA +290 -0
- pygeai_orchestration-0.1.0b2.dist-info/RECORD +61 -0
- pygeai_orchestration-0.1.0b2.dist-info/WHEEL +5 -0
- pygeai_orchestration-0.1.0b2.dist-info/entry_points.txt +2 -0
- pygeai_orchestration-0.1.0b2.dist-info/licenses/LICENSE +8 -0
- 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)
|