orionis 0.448.0__py3-none-any.whl → 0.450.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.
- orionis/console/args/argument.py +174 -43
- orionis/console/args/parser.py +37 -3
- orionis/console/base/command.py +103 -48
- orionis/console/base/contracts/command.py +97 -40
- orionis/console/core/reactor.py +408 -14
- orionis/console/output/contracts/executor.py +93 -0
- orionis/console/output/executor.py +153 -0
- orionis/container/container.py +46 -22
- orionis/container/context/scope.py +1 -1
- orionis/foundation/application.py +6 -2
- orionis/foundation/providers/console_provider.py +35 -10
- orionis/foundation/providers/dumper_provider.py +42 -14
- orionis/foundation/providers/executor_provider.py +80 -0
- orionis/foundation/providers/inspirational_provider.py +43 -23
- orionis/foundation/providers/logger_provider.py +47 -8
- orionis/foundation/providers/progress_bar_provider.py +55 -10
- orionis/foundation/providers/testing_provider.py +75 -31
- orionis/foundation/providers/workers_provider.py +69 -11
- orionis/metadata/framework.py +1 -1
- orionis/support/facades/console.py +11 -3
- orionis/support/facades/dumper.py +10 -3
- orionis/support/facades/executor.py +24 -0
- orionis/support/facades/inspire.py +9 -6
- orionis/support/facades/logger.py +11 -4
- orionis/support/facades/progress_bar.py +9 -3
- orionis/support/facades/testing.py +10 -4
- orionis/support/facades/workers.py +8 -4
- orionis/test/kernel.py +2 -2
- {orionis-0.448.0.dist-info → orionis-0.450.0.dist-info}/METADATA +1 -1
- {orionis-0.448.0.dist-info → orionis-0.450.0.dist-info}/RECORD +34 -30
- {orionis-0.448.0.dist-info → orionis-0.450.0.dist-info}/WHEEL +0 -0
- {orionis-0.448.0.dist-info → orionis-0.450.0.dist-info}/licenses/LICENCE +0 -0
- {orionis-0.448.0.dist-info → orionis-0.450.0.dist-info}/top_level.txt +0 -0
- {orionis-0.448.0.dist-info → orionis-0.450.0.dist-info}/zip-safe +0 -0
|
@@ -1,68 +1,125 @@
|
|
|
1
1
|
|
|
2
2
|
from abc import ABC, abstractmethod
|
|
3
|
-
from typing import Any, Dict
|
|
3
|
+
from typing import Any, Dict, List
|
|
4
|
+
|
|
5
|
+
from orionis.console.args.argument import CLIArgument
|
|
4
6
|
|
|
5
7
|
class IBaseCommand(ABC):
|
|
6
8
|
"""
|
|
7
|
-
Abstract base contract for console commands in Orionis.
|
|
9
|
+
Abstract base contract for console commands in Orionis framework.
|
|
10
|
+
|
|
11
|
+
This abstract base class defines the standardized interface that all console
|
|
12
|
+
commands must implement within the Orionis framework. It provides a consistent
|
|
13
|
+
contract for command execution, argument handling, metadata storage, and console
|
|
14
|
+
output management.
|
|
8
15
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
16
|
+
The class establishes the foundation for command-line interface functionality,
|
|
17
|
+
ensuring all commands follow a uniform pattern for registration, execution,
|
|
18
|
+
and user interaction while maintaining flexibility for specific command logic
|
|
19
|
+
implementation.
|
|
12
20
|
|
|
13
21
|
Attributes
|
|
14
22
|
----------
|
|
15
|
-
|
|
16
|
-
|
|
23
|
+
timestamps : bool, default=True
|
|
24
|
+
Controls whether timestamps are displayed in console output. When enabled,
|
|
25
|
+
all console messages will include timestamp prefixes for better debugging
|
|
26
|
+
and logging capabilities.
|
|
27
|
+
signature : str
|
|
28
|
+
The command signature string that defines the command name and expected
|
|
29
|
+
arguments format. Used for command registration in the console system
|
|
30
|
+
and automatic help text generation. Must follow the framework's signature
|
|
31
|
+
format conventions.
|
|
32
|
+
description : str
|
|
33
|
+
Human-readable description explaining the command's purpose and functionality.
|
|
34
|
+
This text is displayed in help documentation, command listings, and usage
|
|
35
|
+
instructions to assist users in understanding the command's capabilities.
|
|
36
|
+
_args : Dict[str, Any]
|
|
37
|
+
Dictionary containing parsed command-line arguments and options passed to
|
|
38
|
+
the command during execution. Populated automatically by the command parser
|
|
39
|
+
before the handle() method is called, providing structured access to all
|
|
40
|
+
user-provided input parameters.
|
|
41
|
+
arguments : List[CLIArgument]
|
|
42
|
+
List of CLIArgument instances defining the command's accepted arguments
|
|
43
|
+
and options. Used for argument parsing, validation, and help text generation.
|
|
44
|
+
|
|
45
|
+
Methods
|
|
46
|
+
-------
|
|
47
|
+
handle() -> None
|
|
48
|
+
Abstract method that must be implemented by all concrete command subclasses.
|
|
49
|
+
Contains the main execution logic specific to each command type and handles
|
|
50
|
+
argument processing, business logic execution, and output generation.
|
|
51
|
+
|
|
52
|
+
Notes
|
|
53
|
+
-----
|
|
54
|
+
- All concrete implementations must override the handle() method
|
|
55
|
+
- Command signatures should follow framework naming conventions
|
|
56
|
+
- Use self._args dictionary to access parsed command-line arguments
|
|
57
|
+
- Implement proper error handling and validation within command logic
|
|
58
|
+
- Follow single responsibility principle for maintainable command structure
|
|
59
|
+
- Utilize framework's console output methods for consistent user experience
|
|
60
|
+
|
|
61
|
+
See Also
|
|
62
|
+
--------
|
|
63
|
+
abc.ABC : Abstract base class functionality
|
|
64
|
+
typing.Dict : Type hints for argument dictionary structure
|
|
17
65
|
"""
|
|
18
66
|
|
|
19
|
-
|
|
67
|
+
# Enable timestamps in console output by default
|
|
68
|
+
timestamps: bool = True
|
|
20
69
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"""
|
|
24
|
-
Main entry point for command execution.
|
|
70
|
+
# Command signature string for registration and help text generation
|
|
71
|
+
signature: str
|
|
25
72
|
|
|
26
|
-
|
|
27
|
-
|
|
73
|
+
# Human-readable description for documentation and help display
|
|
74
|
+
description: str
|
|
28
75
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
pass
|
|
76
|
+
# Dictionary to store parsed command-line arguments and options
|
|
77
|
+
_args: Dict[str, Any] = {}
|
|
78
|
+
|
|
79
|
+
# List of CLIArgument instances defining command arguments
|
|
80
|
+
arguments: List[CLIArgument] = []
|
|
35
81
|
|
|
36
82
|
@abstractmethod
|
|
37
|
-
def
|
|
83
|
+
def handle(self) -> None:
|
|
38
84
|
"""
|
|
39
|
-
|
|
85
|
+
Execute the main logic of the console command.
|
|
86
|
+
|
|
87
|
+
This abstract method serves as the primary entry point for command execution
|
|
88
|
+
and must be implemented by all concrete command subclasses. The method contains
|
|
89
|
+
the core business logic specific to each command type and is responsible for
|
|
90
|
+
processing the parsed arguments stored in self.args and producing the desired
|
|
91
|
+
output or side effects.
|
|
40
92
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
93
|
+
The implementation should access parsed command-line arguments through the
|
|
94
|
+
self.args dictionary and utilize appropriate console output methods for
|
|
95
|
+
user feedback and result presentation. Error handling and resource cleanup
|
|
96
|
+
should also be managed within this method to ensure robust command execution.
|
|
45
97
|
|
|
46
98
|
Returns
|
|
47
99
|
-------
|
|
48
|
-
|
|
49
|
-
|
|
100
|
+
None
|
|
101
|
+
This method does not return any value. All command output, results,
|
|
102
|
+
error messages, and user feedback should be handled through console
|
|
103
|
+
output methods, file operations, database transactions, or other
|
|
104
|
+
side effects rather than return values.
|
|
50
105
|
|
|
51
106
|
Raises
|
|
52
107
|
------
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
108
|
+
NotImplementedError
|
|
109
|
+
Automatically raised when this method is called on the abstract base
|
|
110
|
+
class without a concrete implementation. All subclasses must override
|
|
111
|
+
this method with their specific command logic to avoid this exception.
|
|
57
112
|
|
|
58
|
-
|
|
59
|
-
|
|
113
|
+
Notes
|
|
114
|
+
-----
|
|
115
|
+
- Access command arguments and options via the self.args dictionary
|
|
116
|
+
- Use framework's console output methods for consistent user interaction
|
|
117
|
+
- Implement comprehensive error handling and input validation
|
|
118
|
+
- Ensure proper cleanup of resources (files, connections, etc.) if needed
|
|
119
|
+
- Follow the single responsibility principle for maintainable command logic
|
|
120
|
+
- Handle both success and failure scenarios appropriately
|
|
60
121
|
"""
|
|
61
|
-
Returns all parsed arguments as a dictionary.
|
|
62
122
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
dict
|
|
66
|
-
Dictionary containing all arguments received by the command. If no arguments, returns an empty dictionary.
|
|
67
|
-
"""
|
|
123
|
+
# Abstract method placeholder - concrete implementations must override this method
|
|
124
|
+
# Each subclass should replace this pass statement with specific command logic
|
|
68
125
|
pass
|
orionis/console/core/reactor.py
CHANGED
|
@@ -1,28 +1,422 @@
|
|
|
1
|
+
import argparse
|
|
1
2
|
import os
|
|
2
3
|
from pathlib import Path
|
|
4
|
+
import time
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
from orionis.app import Orionis
|
|
7
|
+
from orionis.console.args.argument import CLIArgument
|
|
8
|
+
from orionis.console.base.command import BaseCommand
|
|
9
|
+
from orionis.console.base.contracts.command import IBaseCommand
|
|
10
|
+
from orionis.console.output.contracts.executor import IExecutor
|
|
11
|
+
from orionis.console.output.executor import Executor
|
|
12
|
+
from orionis.foundation.contracts.application import IApplication
|
|
3
13
|
from orionis.services.introspection.modules.reflection import ReflectionModule
|
|
4
|
-
|
|
14
|
+
import re
|
|
5
15
|
|
|
6
16
|
class Reactor:
|
|
7
17
|
|
|
8
18
|
def __init__(
|
|
9
|
-
self
|
|
19
|
+
self,
|
|
20
|
+
app: Optional[IApplication] = None
|
|
10
21
|
):
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
22
|
+
"""
|
|
23
|
+
Initializes a new Reactor instance for command discovery and management.
|
|
24
|
+
|
|
25
|
+
The Reactor constructor sets up the command processing environment by establishing
|
|
26
|
+
the application context, determining the project root directory, and automatically
|
|
27
|
+
discovering and loading command classes from the console commands directory.
|
|
28
|
+
It maintains an internal registry of discovered commands for efficient lookup
|
|
29
|
+
and execution.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
app : Optional[IApplication], default None
|
|
34
|
+
The application instance to use for command processing. If None is provided,
|
|
35
|
+
a new Orionis application instance will be created automatically. The
|
|
36
|
+
application instance provides access to configuration, paths, and other
|
|
37
|
+
framework services required for command execution.
|
|
38
|
+
|
|
39
|
+
Returns
|
|
40
|
+
-------
|
|
41
|
+
None
|
|
42
|
+
This is a constructor method and does not return any value. The instance
|
|
43
|
+
is configured with the provided or default application and populated with
|
|
44
|
+
discovered commands.
|
|
45
|
+
|
|
46
|
+
Notes
|
|
47
|
+
-----
|
|
48
|
+
- Command discovery is performed automatically during initialization
|
|
49
|
+
- The current working directory is used as the project root for module resolution
|
|
50
|
+
- Commands are loaded from the path specified by app.path('console_commands')
|
|
51
|
+
- The internal command registry is initialized as an empty dictionary before loading
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
# Initialize the application instance, using provided app or creating new Orionis instance
|
|
55
|
+
self.__app = app or Orionis()
|
|
56
|
+
|
|
57
|
+
# Set the project root directory to current working directory for module path resolution
|
|
58
|
+
self.__root: str = str(Path.cwd())
|
|
59
|
+
|
|
60
|
+
# Initialize the internal command registry as an empty dictionary
|
|
61
|
+
self.__commands: dict = {}
|
|
62
|
+
|
|
63
|
+
# Automatically discover and load command classes from the console commands directory
|
|
64
|
+
self.__loadCommands(str(self.__app.path('console_commands')), self.__root)
|
|
65
|
+
|
|
66
|
+
# Initialize the executor for command output management
|
|
67
|
+
self.__executer: IExecutor = self.__app.make('x-orionis.console.output.executor')
|
|
68
|
+
|
|
69
|
+
def __loadCommands(self, commands_path: str, root_path: str) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Loads command classes from Python files in the specified commands directory.
|
|
14
72
|
|
|
15
|
-
|
|
73
|
+
This method recursively walks through the commands directory, imports Python modules,
|
|
74
|
+
and registers command classes that inherit from BaseCommand. It performs module path
|
|
75
|
+
sanitization to handle virtual environment paths and validates command structure
|
|
76
|
+
before registration.
|
|
16
77
|
|
|
17
|
-
|
|
18
|
-
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
commands_path : str
|
|
81
|
+
The absolute path to the directory containing command modules to load.
|
|
82
|
+
root_path : str
|
|
83
|
+
The root path of the project, used for module path normalization.
|
|
84
|
+
|
|
85
|
+
Returns
|
|
86
|
+
-------
|
|
87
|
+
None
|
|
88
|
+
This method does not return any value. Command classes are registered
|
|
89
|
+
internally in the reactor's command registry.
|
|
90
|
+
|
|
91
|
+
Notes
|
|
92
|
+
-----
|
|
93
|
+
- Only Python files (.py extension) are processed
|
|
94
|
+
- Virtual environment paths are automatically filtered out during module resolution
|
|
95
|
+
- Command classes must inherit from BaseCommand to be registered
|
|
96
|
+
- Each discovered command class undergoes structure validation via __ensureStructure
|
|
97
|
+
"""
|
|
19
98
|
|
|
20
99
|
# Iterate through the command path and load command modules
|
|
21
|
-
for current_directory, _, files in os.walk(
|
|
100
|
+
for current_directory, _, files in os.walk(commands_path):
|
|
22
101
|
for file in files:
|
|
102
|
+
|
|
103
|
+
# Only process Python files
|
|
23
104
|
if file.endswith('.py'):
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
105
|
+
|
|
106
|
+
# Sanitize the module path by converting filesystem path to Python module notation
|
|
107
|
+
pre_module = current_directory.replace(root_path, '')\
|
|
108
|
+
.replace(os.sep, '.')\
|
|
109
|
+
.lstrip('.')
|
|
110
|
+
|
|
111
|
+
# Remove virtual environment paths using regex (Windows, Linux, macOS)
|
|
112
|
+
# Windows: venv\Lib\site-packages or venv\lib\site-packages
|
|
113
|
+
# Linux/macOS: venv/lib/python*/site-packages
|
|
114
|
+
pre_module = re.sub(r'[^.]*\.(?:Lib|lib)\.(?:python[^.]*\.)?site-packages\.?', '', pre_module)
|
|
115
|
+
|
|
116
|
+
# Remove any remaining .venv or venv patterns from the module path
|
|
117
|
+
pre_module = re.sub(r'\.?v?env\.?', '', pre_module)
|
|
118
|
+
|
|
119
|
+
# Clean up any double dots or leading/trailing dots that may have been created
|
|
120
|
+
pre_module = re.sub(r'\.+', '.', pre_module).strip('.')
|
|
121
|
+
|
|
122
|
+
# Skip if module name is empty after cleaning (invalid module path)
|
|
123
|
+
if not pre_module:
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
# Create the reflection module path by combining sanitized path with filename
|
|
127
|
+
rf_module = ReflectionModule(f"{pre_module}.{file[:-3]}")
|
|
128
|
+
|
|
129
|
+
# Iterate through all classes found in the current module
|
|
130
|
+
for name, obj in rf_module.getClasses().items():
|
|
131
|
+
|
|
132
|
+
# Check if the class is a valid command class (inherits from BaseCommand but is not BaseCommand itself)
|
|
133
|
+
if issubclass(obj, BaseCommand) and obj is not BaseCommand and obj is not IBaseCommand:
|
|
134
|
+
|
|
135
|
+
# Validate the command class structure and register it
|
|
136
|
+
timestamp = self.__ensureTimestamps(obj)
|
|
137
|
+
signature = self.__ensureSignature(obj)
|
|
138
|
+
description = self.__ensureDescription(obj)
|
|
139
|
+
args = self.__ensureArguments(obj)
|
|
140
|
+
|
|
141
|
+
# Add the command to the internal registry
|
|
142
|
+
self.__commands[signature] = {
|
|
143
|
+
"class": obj,
|
|
144
|
+
"timestamps": timestamp,
|
|
145
|
+
"signature": signature,
|
|
146
|
+
"description": description,
|
|
147
|
+
"args": args
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
def __ensureTimestamps(self, obj: IBaseCommand) -> bool:
|
|
151
|
+
"""
|
|
152
|
+
Validates that a command class has a properly formatted timestamps attribute.
|
|
153
|
+
|
|
154
|
+
This method ensures that the command class contains a 'timestamps' attribute
|
|
155
|
+
that is a boolean value, indicating whether timestamps should be included in
|
|
156
|
+
console output messages.
|
|
157
|
+
|
|
158
|
+
Parameters
|
|
159
|
+
----------
|
|
160
|
+
obj : IBaseCommand
|
|
161
|
+
The command class instance to validate.
|
|
162
|
+
|
|
163
|
+
Returns
|
|
164
|
+
-------
|
|
165
|
+
Reactor
|
|
166
|
+
Returns self to enable method chaining for additional validation calls.
|
|
167
|
+
|
|
168
|
+
Raises
|
|
169
|
+
------
|
|
170
|
+
ValueError
|
|
171
|
+
If the command class lacks a 'timestamps' attribute or if the attribute is not a boolean.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
# Check if the command class has a timestamps attribute
|
|
175
|
+
if not hasattr(obj, 'timestamps'):
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
# Ensure the timestamps attribute is a boolean type
|
|
179
|
+
if not isinstance(obj.timestamps, bool):
|
|
180
|
+
raise TypeError(f"Command class {obj.__name__} 'timestamps' must be a boolean.")
|
|
181
|
+
|
|
182
|
+
# Return timestamps value
|
|
183
|
+
return obj.timestamps
|
|
184
|
+
|
|
185
|
+
def __ensureSignature(self, obj: IBaseCommand) -> str:
|
|
186
|
+
"""
|
|
187
|
+
Validates that a command class has a properly formatted signature attribute.
|
|
188
|
+
|
|
189
|
+
This method ensures that the command class contains a 'signature' attribute
|
|
190
|
+
that follows the required naming conventions for command identification.
|
|
191
|
+
The signature must be a non-empty string containing only alphanumeric
|
|
192
|
+
characters, underscores, and colons, with specific placement rules.
|
|
193
|
+
|
|
194
|
+
Parameters
|
|
195
|
+
----------
|
|
196
|
+
obj : IBaseCommand
|
|
197
|
+
The command class instance to validate.
|
|
198
|
+
|
|
199
|
+
Returns
|
|
200
|
+
-------
|
|
201
|
+
Reactor
|
|
202
|
+
Returns self to enable method chaining.
|
|
203
|
+
|
|
204
|
+
Raises
|
|
205
|
+
------
|
|
206
|
+
ValueError
|
|
207
|
+
If the command class lacks a 'signature' attribute, if the signature
|
|
208
|
+
is an empty string, or if the signature doesn't match the required pattern.
|
|
209
|
+
TypeError
|
|
210
|
+
If the 'signature' attribute is not a string.
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
# Check if the command class has a signature attribute
|
|
214
|
+
if not hasattr(obj, 'signature'):
|
|
215
|
+
raise ValueError(f"Command class {obj.__name__} must have a 'signature' attribute.")
|
|
216
|
+
|
|
217
|
+
# Ensure the signature attribute is a string type
|
|
218
|
+
if not isinstance(obj.signature, str):
|
|
219
|
+
raise TypeError(f"Command class {obj.__name__} 'signature' must be a string.")
|
|
220
|
+
|
|
221
|
+
# Validate that the signature is not empty after stripping whitespace
|
|
222
|
+
if obj.signature.strip() == '':
|
|
223
|
+
raise ValueError(f"Command class {obj.__name__} 'signature' cannot be an empty string.")
|
|
224
|
+
|
|
225
|
+
# Define the regex pattern for valid signature format
|
|
226
|
+
# Pattern allows: alphanumeric chars, underscores, colons
|
|
227
|
+
# Cannot start/end with underscore or colon, cannot start with number
|
|
228
|
+
pattern = r'^[a-zA-Z][a-zA-Z0-9_:]*[a-zA-Z0-9]$|^[a-zA-Z]$'
|
|
229
|
+
|
|
230
|
+
# Validate the signature against the required pattern
|
|
231
|
+
if not re.match(pattern, obj.signature):
|
|
232
|
+
raise ValueError(f"Command class {obj.__name__} 'signature' must contain only alphanumeric characters, underscores (_) and colons (:), cannot start or end with underscore or colon, and cannot start with a number.")
|
|
233
|
+
|
|
234
|
+
# Return signature
|
|
235
|
+
return obj.signature.strip()
|
|
236
|
+
|
|
237
|
+
def __ensureDescription(self, obj: IBaseCommand) -> str:
|
|
238
|
+
"""
|
|
239
|
+
Validates that a command class has a properly formatted description attribute.
|
|
240
|
+
|
|
241
|
+
This method ensures that the command class contains a 'description' attribute
|
|
242
|
+
that provides meaningful documentation for the command. The description must
|
|
243
|
+
be a non-empty string that can be used for help documentation and command
|
|
244
|
+
listing purposes.
|
|
245
|
+
|
|
246
|
+
Parameters
|
|
247
|
+
----------
|
|
248
|
+
obj : IBaseCommand
|
|
249
|
+
The command class instance to validate.
|
|
250
|
+
|
|
251
|
+
Returns
|
|
252
|
+
-------
|
|
253
|
+
Reactor
|
|
254
|
+
Returns self to enable method chaining for additional validation calls.
|
|
255
|
+
|
|
256
|
+
Raises
|
|
257
|
+
------
|
|
258
|
+
ValueError
|
|
259
|
+
If the command class lacks a 'description' attribute or if the description
|
|
260
|
+
is an empty string after stripping whitespace.
|
|
261
|
+
TypeError
|
|
262
|
+
If the 'description' attribute is not a string type.
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
# Check if the command class has a description attribute
|
|
266
|
+
if not hasattr(obj, 'description'):
|
|
267
|
+
raise ValueError(f"Command class {obj.__name__} must have a 'description' attribute.")
|
|
268
|
+
|
|
269
|
+
# Ensure the description attribute is a string type
|
|
270
|
+
if not isinstance(obj.description, str):
|
|
271
|
+
raise TypeError(f"Command class {obj.__name__} 'description' must be a string.")
|
|
272
|
+
|
|
273
|
+
# Validate that the description is not empty after stripping whitespace
|
|
274
|
+
if obj.description.strip() == '':
|
|
275
|
+
raise ValueError(f"Command class {obj.__name__} 'description' cannot be an empty string.")
|
|
276
|
+
|
|
277
|
+
# Return description
|
|
278
|
+
return obj.description.strip()
|
|
279
|
+
|
|
280
|
+
def __ensureArguments(self, obj: IBaseCommand) -> Optional[argparse.ArgumentParser]:
|
|
281
|
+
"""
|
|
282
|
+
Validates and processes command arguments for a command class.
|
|
283
|
+
|
|
284
|
+
This method ensures that the command class has properly formatted arguments
|
|
285
|
+
and creates an ArgumentParser instance configured with those arguments.
|
|
286
|
+
|
|
287
|
+
Parameters
|
|
288
|
+
----------
|
|
289
|
+
obj : IBaseCommand
|
|
290
|
+
The command class instance to validate.
|
|
291
|
+
|
|
292
|
+
Returns
|
|
293
|
+
-------
|
|
294
|
+
Optional[argparse.ArgumentParser]
|
|
295
|
+
An ArgumentParser instance configured with the command's arguments,
|
|
296
|
+
or None if the command has no arguments.
|
|
297
|
+
|
|
298
|
+
Raises
|
|
299
|
+
------
|
|
300
|
+
TypeError
|
|
301
|
+
If the 'arguments' attribute is not a list or contains non-CLIArgument instances.
|
|
302
|
+
"""
|
|
303
|
+
|
|
304
|
+
# Check if the command class has an arguments attribute
|
|
305
|
+
if not hasattr(obj, 'arguments'):
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
# Ensure the arguments attribute is a list type
|
|
309
|
+
if not isinstance(obj.arguments, list):
|
|
310
|
+
raise TypeError(f"Command class {obj.__name__} 'arguments' must be a list.")
|
|
311
|
+
|
|
312
|
+
# If arguments is empty, return None
|
|
313
|
+
if len(obj.arguments) == 0:
|
|
314
|
+
return None
|
|
315
|
+
|
|
316
|
+
# Validate that all items in the arguments list are CLIArgument instances
|
|
317
|
+
for index, value in enumerate(obj.arguments):
|
|
318
|
+
if not isinstance(value, CLIArgument):
|
|
319
|
+
raise TypeError(f"Command class {obj.__name__} 'arguments' must contain only CLIArgument instances, found '{type(value).__name__}' at index {index}.")
|
|
320
|
+
|
|
321
|
+
# Build the arguments dictionary from the CLIArgument instances
|
|
322
|
+
required_args: List[CLIArgument] = obj.arguments
|
|
323
|
+
|
|
324
|
+
# Create an ArgumentParser instance to handle the command arguments
|
|
325
|
+
arg_parser = argparse.ArgumentParser(
|
|
326
|
+
description=f"{obj.signature} - {obj.description}",
|
|
327
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
328
|
+
add_help=True,
|
|
329
|
+
allow_abbrev=False
|
|
330
|
+
)
|
|
331
|
+
for arg in required_args:
|
|
332
|
+
arg.addToParser(arg_parser)
|
|
333
|
+
|
|
334
|
+
# Return the configured ArgumentParser
|
|
335
|
+
return arg_parser
|
|
336
|
+
|
|
337
|
+
def call(
|
|
338
|
+
self,
|
|
339
|
+
signature: str,
|
|
340
|
+
args: Optional[List[str]] = None
|
|
341
|
+
):
|
|
342
|
+
"""
|
|
343
|
+
Executes a command by its signature with optional command-line arguments.
|
|
344
|
+
|
|
345
|
+
This method retrieves a registered command by its signature, validates any provided
|
|
346
|
+
arguments against the command's argument parser, and executes the command's handle
|
|
347
|
+
method with the parsed arguments and application context.
|
|
348
|
+
|
|
349
|
+
Parameters
|
|
350
|
+
----------
|
|
351
|
+
signature : str
|
|
352
|
+
The unique signature identifier of the command to execute.
|
|
353
|
+
args : Optional[List[str]], default None
|
|
354
|
+
Command-line arguments to pass to the command. If None, no arguments are provided.
|
|
355
|
+
|
|
356
|
+
Returns
|
|
357
|
+
-------
|
|
358
|
+
None
|
|
359
|
+
This method does not return any value. The command's handle method is called
|
|
360
|
+
directly for execution.
|
|
361
|
+
|
|
362
|
+
Raises
|
|
363
|
+
------
|
|
364
|
+
ValueError
|
|
365
|
+
If the command with the specified signature is not found in the registry.
|
|
366
|
+
SystemExit
|
|
367
|
+
If argument parsing fails due to invalid arguments provided.
|
|
368
|
+
"""
|
|
369
|
+
|
|
370
|
+
# Retrieve the command from the registry
|
|
371
|
+
command: dict = self.__commands.get(signature)
|
|
372
|
+
if command is None:
|
|
373
|
+
raise ValueError(f"Command '{signature}' not found.")
|
|
374
|
+
|
|
375
|
+
# Start execution timer
|
|
376
|
+
start_time = time.perf_counter()
|
|
377
|
+
|
|
378
|
+
# Get command details
|
|
379
|
+
command_class = command.get("class")
|
|
380
|
+
arg_parser: Optional[argparse.ArgumentParser] = command.get("args")
|
|
381
|
+
timestamps: bool = command.get("timestamps")
|
|
382
|
+
|
|
383
|
+
# Initialize parsed arguments
|
|
384
|
+
parsed_args = None
|
|
385
|
+
|
|
386
|
+
# If the command has arguments and args were provided, validate them
|
|
387
|
+
if arg_parser is not None and isinstance(arg_parser, argparse.ArgumentParser):
|
|
388
|
+
if args is None:
|
|
389
|
+
args = []
|
|
390
|
+
try:
|
|
391
|
+
# Parse the provided arguments using the command's ArgumentParser
|
|
392
|
+
parsed_args = arg_parser.parse_args(args)
|
|
393
|
+
except SystemExit:
|
|
394
|
+
# Re-raise SystemExit to allow argparse to handle help/error messages
|
|
395
|
+
raise
|
|
396
|
+
|
|
397
|
+
# Log the command execution start with RUNNING state
|
|
398
|
+
if timestamps:
|
|
399
|
+
self.__executer.running(program=signature)
|
|
400
|
+
|
|
401
|
+
try:
|
|
402
|
+
|
|
403
|
+
# Create an instance of the command class and execute it
|
|
404
|
+
command_instance: IBaseCommand = command_class()
|
|
405
|
+
command_instance._args = vars(parsed_args) if parsed_args else {}
|
|
406
|
+
output = command_instance.handle(self.__app.make('x-orionis.services.inspirational.inspire'))
|
|
407
|
+
|
|
408
|
+
# Log the command execution completion with DONE state
|
|
409
|
+
if timestamps:
|
|
410
|
+
elapsed_time = round(time.perf_counter() - start_time, 2)
|
|
411
|
+
self.__executer.done(program=signature, time=f"{elapsed_time}s")
|
|
412
|
+
|
|
413
|
+
# If the command has a return value or output, return it
|
|
414
|
+
if output is not None:
|
|
415
|
+
return output
|
|
416
|
+
|
|
417
|
+
except Exception as e:
|
|
418
|
+
|
|
419
|
+
# Log the command execution failure with ERROR state
|
|
420
|
+
if timestamps:
|
|
421
|
+
elapsed_time = round(time.perf_counter() - start_time, 2)
|
|
422
|
+
self.__executer.fail(program=signature, time=f"{elapsed_time}s")
|