oduit 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.
- oduit/__init__.py +75 -0
- oduit/_version.py +34 -0
- oduit/base_process_manager.py +308 -0
- oduit/builders.py +870 -0
- oduit/cli_typer.py +693 -0
- oduit/cli_types.py +65 -0
- oduit/config_loader.py +521 -0
- oduit/config_provider.py +183 -0
- oduit/demo_process_manager.py +986 -0
- oduit/exceptions.py +49 -0
- oduit/manifest.py +230 -0
- oduit/module_manager.py +372 -0
- oduit/odoo_code_executor.py +436 -0
- oduit/odoo_embedded_manager.py +541 -0
- oduit/odoo_operations.py +935 -0
- oduit/operation_result.py +489 -0
- oduit/output.py +302 -0
- oduit/process_manager.py +894 -0
- oduit/utils.py +76 -0
- oduit-0.1.0.dist-info/METADATA +1047 -0
- oduit-0.1.0.dist-info/RECORD +26 -0
- oduit-0.1.0.dist-info/WHEEL +5 -0
- oduit-0.1.0.dist-info/entry_points.txt +2 -0
- oduit-0.1.0.dist-info/licenses/AUTHORS +1 -0
- oduit-0.1.0.dist-info/licenses/LICENSE +373 -0
- oduit-0.1.0.dist-info/top_level.txt +1 -0
oduit/__init__.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Copyright (C) 2025 The ODUIT Authors.
|
|
2
|
+
#
|
|
3
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
4
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
5
|
+
# You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
"""Oduit - Odoo development utilities."""
|
|
9
|
+
|
|
10
|
+
from ._version import __version__, version_tuple
|
|
11
|
+
from .config_loader import ConfigLoader
|
|
12
|
+
from .demo_process_manager import DemoProcessManager
|
|
13
|
+
from .exceptions import (
|
|
14
|
+
ConfigError,
|
|
15
|
+
OdooOperationError,
|
|
16
|
+
ModuleOperationError,
|
|
17
|
+
ModuleUpdateError,
|
|
18
|
+
ModuleInstallError,
|
|
19
|
+
ModuleNotFoundError,
|
|
20
|
+
DatabaseOperationError,
|
|
21
|
+
)
|
|
22
|
+
from .manifest import (
|
|
23
|
+
Manifest,
|
|
24
|
+
ManifestError,
|
|
25
|
+
InvalidManifestError,
|
|
26
|
+
ManifestNotFoundError,
|
|
27
|
+
)
|
|
28
|
+
from .module_manager import ModuleManager
|
|
29
|
+
from .odoo_operations import OdooOperations
|
|
30
|
+
from .odoo_embedded_manager import OdooEmbeddedManager
|
|
31
|
+
from .odoo_code_executor import OdooCodeExecutor
|
|
32
|
+
from .output import (
|
|
33
|
+
OutputFormatter,
|
|
34
|
+
configure_output,
|
|
35
|
+
print_error,
|
|
36
|
+
print_error_result,
|
|
37
|
+
print_info,
|
|
38
|
+
print_result,
|
|
39
|
+
print_success,
|
|
40
|
+
print_warning,
|
|
41
|
+
)
|
|
42
|
+
from .process_manager import ProcessManager
|
|
43
|
+
from .operation_result import OperationResult
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"__version__",
|
|
47
|
+
"version_tuple",
|
|
48
|
+
"ConfigLoader",
|
|
49
|
+
"Manifest",
|
|
50
|
+
"ManifestError",
|
|
51
|
+
"InvalidManifestError",
|
|
52
|
+
"ManifestNotFoundError",
|
|
53
|
+
"ModuleManager",
|
|
54
|
+
"ConfigError",
|
|
55
|
+
"OdooOperationError",
|
|
56
|
+
"ModuleOperationError",
|
|
57
|
+
"ModuleUpdateError",
|
|
58
|
+
"ModuleInstallError",
|
|
59
|
+
"ModuleNotFoundError",
|
|
60
|
+
"DatabaseOperationError",
|
|
61
|
+
"DemoProcessManager",
|
|
62
|
+
"OdooOperations",
|
|
63
|
+
"OdooEmbeddedManager",
|
|
64
|
+
"OdooCodeExecutor",
|
|
65
|
+
"ProcessManager",
|
|
66
|
+
"OperationResult",
|
|
67
|
+
"OutputFormatter",
|
|
68
|
+
"configure_output",
|
|
69
|
+
"print_info",
|
|
70
|
+
"print_success",
|
|
71
|
+
"print_warning",
|
|
72
|
+
"print_error",
|
|
73
|
+
"print_result",
|
|
74
|
+
"print_error_result",
|
|
75
|
+
]
|
oduit/_version.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '0.1.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 0)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# Copyright (C) 2025 The ODUIT Authors.
|
|
2
|
+
#
|
|
3
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
4
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
5
|
+
# You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from collections.abc import Generator
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
from .output import print_error, print_info
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from .builders import CommandOperation
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BaseProcessManager(ABC):
|
|
19
|
+
"""Abstract base class for all process managers.
|
|
20
|
+
|
|
21
|
+
This class defines the common interface and shared functionality for
|
|
22
|
+
process managers that execute commands in different ways (subprocess,
|
|
23
|
+
embedded, mock/demo, etc.).
|
|
24
|
+
|
|
25
|
+
All concrete implementations must provide the core execution methods
|
|
26
|
+
while optionally overriding shared utility methods for customization.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def run_command(
|
|
31
|
+
self,
|
|
32
|
+
cmd: list[str],
|
|
33
|
+
stop_on_error: bool = False,
|
|
34
|
+
compact: bool = False,
|
|
35
|
+
verbose: bool = False,
|
|
36
|
+
suppress_output: bool = False,
|
|
37
|
+
) -> dict[str, Any]:
|
|
38
|
+
"""Execute a command and return structured results.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
cmd: Command to execute as list of strings (e.g., ['ls', '-la'])
|
|
42
|
+
stop_on_error: If True, terminate on first error encountered
|
|
43
|
+
compact: If True, use compact output formatting
|
|
44
|
+
verbose: If True, print the command being executed
|
|
45
|
+
suppress_output: If True, suppress all output to console
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Dict containing execution results with keys:
|
|
49
|
+
- success (bool): True if command executed successfully
|
|
50
|
+
- return_code (int): Process exit code (0 for success)
|
|
51
|
+
- output (str): Combined stdout/stderr output
|
|
52
|
+
- command (str): The executed command as string
|
|
53
|
+
- error (str): Error message if execution failed (optional)
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def run_command_yielding(
|
|
57
|
+
self,
|
|
58
|
+
cmd: list[str],
|
|
59
|
+
stop_on_error: bool = False,
|
|
60
|
+
compact: bool = False,
|
|
61
|
+
verbose: bool = False,
|
|
62
|
+
suppress_output: bool = False,
|
|
63
|
+
) -> Generator[dict[str, Any], None, None]:
|
|
64
|
+
"""Generator version of run_command that yields lines as they arrive.
|
|
65
|
+
|
|
66
|
+
Default implementation calls run_command and yields the final result.
|
|
67
|
+
Concrete implementations may override for true line-by-line streaming.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
cmd: Command to execute
|
|
71
|
+
stop_on_error: Stop execution on first error pattern
|
|
72
|
+
compact: Only show relevant lines (dots, errors, warnings)
|
|
73
|
+
verbose: Print command before execution
|
|
74
|
+
suppress_output: Don't print lines to console (only yield them)
|
|
75
|
+
|
|
76
|
+
Yields:
|
|
77
|
+
dict: For each line: {
|
|
78
|
+
'line': str, # Raw line content
|
|
79
|
+
'formatted': str, # Colorized/formatted line
|
|
80
|
+
'should_show': bool, # Whether line should be shown in compact mode
|
|
81
|
+
'is_error': bool, # Whether line matches error patterns
|
|
82
|
+
'process_running': bool # Whether process is still active
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
Final yield: {
|
|
86
|
+
'result': dict, # Final command result (same as run_command)
|
|
87
|
+
'process_running': False
|
|
88
|
+
}
|
|
89
|
+
"""
|
|
90
|
+
result = self.run_command(cmd, stop_on_error, compact, verbose, suppress_output)
|
|
91
|
+
yield {"result": result, "process_running": False}
|
|
92
|
+
|
|
93
|
+
@abstractmethod
|
|
94
|
+
def run_operation(
|
|
95
|
+
self,
|
|
96
|
+
command_operation: "CommandOperation",
|
|
97
|
+
verbose: bool = False,
|
|
98
|
+
suppress_output: bool = False,
|
|
99
|
+
) -> dict[str, Any]:
|
|
100
|
+
"""Execute a CommandOperation directly.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
command_operation: Structured command operation with metadata
|
|
104
|
+
verbose: Enable verbose output
|
|
105
|
+
suppress_output: Suppress output to console
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Dict containing execution results
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
@abstractmethod
|
|
112
|
+
def run_shell_command(
|
|
113
|
+
self, cmd, verbose: bool = False, capture_output: bool = False
|
|
114
|
+
) -> dict[str, Any]:
|
|
115
|
+
"""Execute a shell command that may receive piped input.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
cmd: Either a list of command arguments or a string to be evaluated by shell
|
|
119
|
+
verbose: Print command before execution
|
|
120
|
+
capture_output: Capture stdout/stderr instead of inheriting
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Dict with execution results (format similar to run_command)
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
@abstractmethod
|
|
128
|
+
def run_interactive_shell(cmd: list[str]) -> int:
|
|
129
|
+
"""Run an interactive shell session.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
cmd: Command to execute interactively
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
int: Exit code of the interactive session
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
# Shared utility methods that can be overridden
|
|
139
|
+
|
|
140
|
+
def _create_result(
|
|
141
|
+
self,
|
|
142
|
+
success: bool,
|
|
143
|
+
return_code: int,
|
|
144
|
+
output: str,
|
|
145
|
+
command: str,
|
|
146
|
+
error: str | None = None,
|
|
147
|
+
) -> dict[str, Any]:
|
|
148
|
+
"""Create standardized result dictionary.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
success: Whether the command succeeded
|
|
152
|
+
return_code: Process exit code
|
|
153
|
+
output: Command output
|
|
154
|
+
command: The executed command as string
|
|
155
|
+
error: Optional error message
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Standardized result dictionary
|
|
159
|
+
"""
|
|
160
|
+
result = {
|
|
161
|
+
"success": success,
|
|
162
|
+
"return_code": return_code,
|
|
163
|
+
"output": output,
|
|
164
|
+
"command": command,
|
|
165
|
+
}
|
|
166
|
+
if error is not None:
|
|
167
|
+
result["error"] = error
|
|
168
|
+
return result
|
|
169
|
+
|
|
170
|
+
def _validate_command(self, cmd: list[str]) -> bool:
|
|
171
|
+
"""Validate command format and content.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
cmd: Command list to validate
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
bool: True if command is valid
|
|
178
|
+
|
|
179
|
+
Raises:
|
|
180
|
+
ValueError: If command format is invalid
|
|
181
|
+
"""
|
|
182
|
+
if not cmd:
|
|
183
|
+
raise ValueError("Command cannot be empty")
|
|
184
|
+
if not isinstance(cmd, list):
|
|
185
|
+
raise ValueError("Command must be a list of strings")
|
|
186
|
+
if not all(isinstance(arg, str) for arg in cmd):
|
|
187
|
+
raise ValueError("All command arguments must be strings")
|
|
188
|
+
return True
|
|
189
|
+
|
|
190
|
+
def _colorize_log_line(self, line: str) -> str:
|
|
191
|
+
"""Add ANSI colors to common log levels (INFO/WARNING/ERROR).
|
|
192
|
+
|
|
193
|
+
Can be overridden by concrete implementations for custom colorization.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
line: Log line to colorize
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
str: Colorized line with ANSI escape codes
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
# Use cautious, single-substitution replacements to avoid over-coloring
|
|
203
|
+
# Colors: INFO=blue(34), WARNING=yellow(33), ERROR=red(31), DOTS=green(32)
|
|
204
|
+
def repl_once(pattern: re.Pattern, color_code: str, text: str) -> str:
|
|
205
|
+
m = pattern.search(text)
|
|
206
|
+
if not m:
|
|
207
|
+
return text
|
|
208
|
+
start, end = m.span(1)
|
|
209
|
+
return (
|
|
210
|
+
text[:start]
|
|
211
|
+
+ f"\033[{color_code}m"
|
|
212
|
+
+ m.group(1)
|
|
213
|
+
+ "\033[0m"
|
|
214
|
+
+ text[end:]
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Match levels as standalone tokens to reduce false hits
|
|
218
|
+
patterns = [
|
|
219
|
+
(re.compile(r"(\bINFO\b)"), "34"),
|
|
220
|
+
(re.compile(r"(\bWARNING\b)"), "33"),
|
|
221
|
+
(re.compile(r"(\bERROR\b)"), "31"),
|
|
222
|
+
(re.compile(r"( \.\.\.+)"), "32"), # Space + 3+ dots
|
|
223
|
+
]
|
|
224
|
+
|
|
225
|
+
colored = line
|
|
226
|
+
for pat, code in patterns:
|
|
227
|
+
colored = repl_once(pat, code, colored)
|
|
228
|
+
return colored
|
|
229
|
+
|
|
230
|
+
def _should_show_line_in_compact(self, line: str) -> bool:
|
|
231
|
+
"""Determine if line should be shown in compact mode.
|
|
232
|
+
|
|
233
|
+
Can be overridden by concrete implementations for custom filtering.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
line: Log line to evaluate
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
bool: True if line should be shown in compact mode
|
|
240
|
+
"""
|
|
241
|
+
failure_patterns = [
|
|
242
|
+
re.compile(r"\bFAIL:\s", re.IGNORECASE),
|
|
243
|
+
re.compile(r"\bERROR:\s", re.IGNORECASE),
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
" ..." in line
|
|
248
|
+
or any(p.search(line) for p in failure_patterns)
|
|
249
|
+
or "odoo.tests.stats:" in line
|
|
250
|
+
or "odoo.tests.result:" in line
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
def _handle_verbose_output(
|
|
254
|
+
self, cmd: list[str], suppress_output: bool = False
|
|
255
|
+
) -> None:
|
|
256
|
+
"""Handle verbose command output.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
cmd: Command being executed
|
|
260
|
+
suppress_output: Whether to suppress output
|
|
261
|
+
"""
|
|
262
|
+
if not suppress_output:
|
|
263
|
+
print_info(f"Running command: {' '.join(cmd)}")
|
|
264
|
+
|
|
265
|
+
def _handle_error_output(
|
|
266
|
+
self, error_msg: str, suppress_output: bool = False
|
|
267
|
+
) -> None:
|
|
268
|
+
"""Handle error output.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
error_msg: Error message to output
|
|
272
|
+
suppress_output: Whether to suppress output
|
|
273
|
+
"""
|
|
274
|
+
if not suppress_output:
|
|
275
|
+
print_error(error_msg)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class ProcessManagerFactory:
|
|
279
|
+
"""Factory for creating appropriate process manager instances."""
|
|
280
|
+
|
|
281
|
+
@staticmethod
|
|
282
|
+
def create_manager(manager_type: str = "system", **kwargs) -> "BaseProcessManager":
|
|
283
|
+
"""Create a process manager instance.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
manager_type: Type of manager to create ("system", "demo")
|
|
287
|
+
**kwargs: Additional arguments passed to manager constructor
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
BaseProcessManager: Appropriate manager instance
|
|
291
|
+
|
|
292
|
+
Raises:
|
|
293
|
+
ValueError: If manager_type is not recognized
|
|
294
|
+
"""
|
|
295
|
+
# Import classes locally to avoid circular imports
|
|
296
|
+
if manager_type == "demo":
|
|
297
|
+
from .demo_process_manager import DemoProcessManager
|
|
298
|
+
|
|
299
|
+
return DemoProcessManager(**kwargs)
|
|
300
|
+
elif manager_type == "system":
|
|
301
|
+
from .process_manager import ProcessManager
|
|
302
|
+
|
|
303
|
+
return ProcessManager(**kwargs)
|
|
304
|
+
else:
|
|
305
|
+
raise ValueError(
|
|
306
|
+
f"Unknown manager type: {manager_type}. "
|
|
307
|
+
f"Valid types: 'system', 'demo', 'embedded'"
|
|
308
|
+
)
|