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 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
+ )