domd 2.2.7__tar.gz → 2.2.8__tar.gz
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.
- {domd-2.2.7 → domd-2.2.8}/PKG-INFO +1 -1
- {domd-2.2.7 → domd-2.2.8}/pyproject.toml +1 -1
- domd-2.2.8/src/domd/core/command_detection/handlers/command_handler.py +207 -0
- domd-2.2.8/src/domd/core/command_detection/handlers/config_handler.py +131 -0
- domd-2.2.8/src/domd/core/command_detection/parsers/__init__.py +6 -0
- domd-2.2.8/src/domd/core/command_detection/parsers/base_parser.py +85 -0
- domd-2.2.8/src/domd/core/command_detection/parsers/parser_registry.py +155 -0
- domd-2.2.8/src/domd/core/command_detection/project_detector.py +312 -0
- domd-2.2.8/src/domd/core/command_detection/utils/__init__.py +11 -0
- domd-2.2.8/src/domd/core/command_detection/utils/file_utils.py +107 -0
- domd-2.2.8/src/domd/core/command_detection/utils/virtualenv.py +103 -0
- {domd-2.2.7 → domd-2.2.8}/LICENSE +0 -0
- {domd-2.2.7 → domd-2.2.8}/README.md +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/__init__.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/adapters/__init__.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/adapters/api/__init__.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/adapters/api/flask_api.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/adapters/cli/__init__.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/adapters/cli/command_presenter.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/adapters/formatters/__init__.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/adapters/formatters/markdown_formatter.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/adapters/persistence/__init__.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/adapters/persistence/in_memory_command_repository.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/adapters/persistence/shell_command_executor.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/api.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/application/__init__.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/application/factory.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/cli.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/command_execution/__init__.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/__init__.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/command_detection/__init__.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/command_detection/base_detector.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/command_detection/handlers/__init__.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/command_detection/models.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/command_execution/__init__.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/command_execution/command_executor.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/command_execution/command_recorder.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/command_execution/command_runner.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/command_execution/environment_detector.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/command_execution/executor.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/commands/__init__.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/commands/command.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/commands/executor.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/detector.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/domain/__init__.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/domain/command.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsers/__init__.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsers/ansible_galaxy.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsers/ansible_inventory.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsers/ansible_playbook.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsers/ansible_role.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsers/ansible_vault.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsers/base.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsers/cargo_toml.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsers/composer_json.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsers/go_mod.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsers/makefile.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsers/package_json.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsers/pyproject_toml.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsers/tox_ini.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsing/__init__.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsing/base.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsing/file_processor.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsing/parser_registry.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsing/pattern_matcher.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/ports/__init__.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/ports/command_executor.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/ports/command_repository.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/ports/report_formatter.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/project_detection/__init__.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/project_detection/command_handling.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/project_detection/config_files.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/project_detection/detector.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/project_detection/virtualenv.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/reporters/__init__.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/reporters/base.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/reporters/done_md.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/reporters/todo_md.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/reporting/__init__.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/reporting/formatters.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/reporting/reporter.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/services/__init__.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/services/command_service.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/services/report_service.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/utils/__init__.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/utils/command_utils.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/utils/environment.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/utils/file_utils.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/utils/logging_utils.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/core/utils/virtualenv.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/parsers/__init__.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/parsers/ansible.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/parsers/base.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/parsers/build_systems.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/parsers/ci_cd.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/parsers/docker/__init__.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/parsers/docker/docker_compose.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/parsers/docker/dockerfile.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/parsers/docker.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/parsers/javascript/__init__.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/parsers/javascript.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/parsers/package_json.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/parsers/python.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/parsers/test_build_systems.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/parsers/test_ci_cd.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/parsers/test_docker.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/parsers/test_javascript.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/parsers/test_python.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/parsing/__init__.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/parsing/base.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/reporters/__init__.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/reporters/console.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/reporters/json_reporter.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/reporters/test_console.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/reporters/test_json_reporter.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/reporters/test_todo_md.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/reporters/todo_md.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/reporting/__init__.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/utils/__init__.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/utils/command_runner.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/utils/file_utils.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/utils/test_command_runner.py +0 -0
- {domd-2.2.7 → domd-2.2.8}/src/domd/utils/test_file_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "domd"
|
|
3
|
-
version = "2.2.
|
|
3
|
+
version = "2.2.8"
|
|
4
4
|
description = "Project Command Detector - Automatically detects and tests project commands, generates TODO.md for failed commands"
|
|
5
5
|
authors = ["Tom Sapletta <info@softreck.dev>"]
|
|
6
6
|
maintainers = ["WRONAI Team"]
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Handler for executing and managing project commands."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, List, Optional, Union
|
|
7
|
+
|
|
8
|
+
from ...services.command_runner import CommandRunner
|
|
9
|
+
from ..models import Command, CommandResult
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CommandHandler:
|
|
15
|
+
"""Handler for executing and managing project commands."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
project_path: Path,
|
|
20
|
+
command_runner: CommandRunner,
|
|
21
|
+
timeout: int = 60,
|
|
22
|
+
ignore_patterns: Optional[List[str]] = None,
|
|
23
|
+
):
|
|
24
|
+
"""Initialize the CommandHandler.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
project_path: Path to the project root
|
|
28
|
+
command_runner: CommandRunner instance for executing commands
|
|
29
|
+
timeout: Default command execution timeout in seconds
|
|
30
|
+
ignore_patterns: List of command patterns to ignore
|
|
31
|
+
"""
|
|
32
|
+
self.project_path = project_path
|
|
33
|
+
self.command_runner = command_runner
|
|
34
|
+
self.timeout = timeout
|
|
35
|
+
self.ignore_patterns = ignore_patterns or []
|
|
36
|
+
|
|
37
|
+
# Command storage - can contain both Command objects and dictionaries
|
|
38
|
+
self.failed_commands: List[Union[Command, Dict[str, Any]]] = []
|
|
39
|
+
self.successful_commands: List[Union[Command, Dict[str, Any]]] = []
|
|
40
|
+
self.ignored_commands: List[Union[Command, Dict[str, Any]]] = []
|
|
41
|
+
|
|
42
|
+
def test_commands(self, commands: List[Union[Command, Dict]]) -> None:
|
|
43
|
+
"""Test a list of commands and update internal state.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
commands: List of Command objects or command dictionaries to test
|
|
47
|
+
"""
|
|
48
|
+
self.failed_commands = []
|
|
49
|
+
self.successful_commands = []
|
|
50
|
+
self.ignored_commands = []
|
|
51
|
+
|
|
52
|
+
for cmd in commands:
|
|
53
|
+
try:
|
|
54
|
+
# Handle both Command objects and dictionaries
|
|
55
|
+
if self.should_ignore_command(cmd):
|
|
56
|
+
self._handle_ignored_command(cmd)
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
# Execute the command
|
|
60
|
+
result = self.execute_single_command(cmd)
|
|
61
|
+
|
|
62
|
+
# Update command state based on result
|
|
63
|
+
if result.get("success"):
|
|
64
|
+
self._handle_successful_command(cmd, result)
|
|
65
|
+
else:
|
|
66
|
+
self._handle_failed_command(cmd, result)
|
|
67
|
+
|
|
68
|
+
except Exception as e:
|
|
69
|
+
logger.error(f"Error testing command: {e}", exc_info=True)
|
|
70
|
+
self._handle_error(cmd, str(e))
|
|
71
|
+
|
|
72
|
+
def execute_single_command(self, cmd_info: Union[Command, Dict]) -> Dict[str, Any]:
|
|
73
|
+
"""Execute a single command and return the result.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
cmd_info: Either a Command object or a dictionary containing command info
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Dictionary with command execution results
|
|
80
|
+
"""
|
|
81
|
+
if isinstance(cmd_info, Command):
|
|
82
|
+
command = cmd_info.command
|
|
83
|
+
cwd = cmd_info.metadata.get("cwd", self.project_path)
|
|
84
|
+
env = cmd_info.metadata.get("env", {})
|
|
85
|
+
timeout = cmd_info.metadata.get("timeout", self.timeout)
|
|
86
|
+
else:
|
|
87
|
+
command = cmd_info.get("command", "")
|
|
88
|
+
cwd = cmd_info.get("cwd", self.project_path)
|
|
89
|
+
env = cmd_info.get("env", {})
|
|
90
|
+
timeout = cmd_info.get("timeout", self.timeout)
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
start_time = time.time()
|
|
94
|
+
result = self.command_runner.run(
|
|
95
|
+
command=command,
|
|
96
|
+
cwd=cwd,
|
|
97
|
+
env=env,
|
|
98
|
+
timeout=timeout,
|
|
99
|
+
)
|
|
100
|
+
execution_time = time.time() - start_time
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
"success": result.return_code == 0,
|
|
104
|
+
"return_code": result.return_code,
|
|
105
|
+
"stdout": result.stdout,
|
|
106
|
+
"stderr": result.stderr,
|
|
107
|
+
"execution_time": execution_time,
|
|
108
|
+
}
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.error(f"Error executing command '{command}': {e}", exc_info=True)
|
|
111
|
+
return {
|
|
112
|
+
"success": False,
|
|
113
|
+
"return_code": -1,
|
|
114
|
+
"error": str(e),
|
|
115
|
+
"stdout": "",
|
|
116
|
+
"stderr": str(e),
|
|
117
|
+
"execution_time": 0,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
def should_ignore_command(self, command: Union[str, Dict, Command]) -> bool:
|
|
121
|
+
"""Check if a command should be ignored based on ignore patterns.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
command: Command to check (string, dict, or Command object)
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
True if the command should be ignored, False otherwise
|
|
128
|
+
"""
|
|
129
|
+
if not self.ignore_patterns:
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
cmd_str = self._extract_command_string(command)
|
|
133
|
+
if not cmd_str:
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
return any(pattern in cmd_str for pattern in self.ignore_patterns)
|
|
137
|
+
|
|
138
|
+
def _extract_command_string(self, command: Union[str, Dict, Command]) -> str:
|
|
139
|
+
"""Extract the command string from various input types."""
|
|
140
|
+
if isinstance(command, str):
|
|
141
|
+
return command
|
|
142
|
+
if isinstance(command, dict):
|
|
143
|
+
return command.get("command", "")
|
|
144
|
+
if hasattr(command, "command"):
|
|
145
|
+
return command.command
|
|
146
|
+
return str(command)
|
|
147
|
+
|
|
148
|
+
def _handle_ignored_command(self, command: Union[Command, Dict]) -> None:
|
|
149
|
+
"""Handle a command that should be ignored."""
|
|
150
|
+
if isinstance(command, dict):
|
|
151
|
+
command["ignored"] = True
|
|
152
|
+
else:
|
|
153
|
+
setattr(command, "ignored", True)
|
|
154
|
+
self.ignored_commands.append(command)
|
|
155
|
+
logger.debug(f"Ignored command: {self._extract_command_string(command)}")
|
|
156
|
+
|
|
157
|
+
def _handle_successful_command(
|
|
158
|
+
self, command: Union[Command, Dict], result: Dict[str, Any]
|
|
159
|
+
) -> None:
|
|
160
|
+
"""Handle a successfully executed command."""
|
|
161
|
+
self._update_command_result(command, result, success=True)
|
|
162
|
+
self.successful_commands.append(command)
|
|
163
|
+
logger.debug(f"Command succeeded: {self._extract_command_string(command)}")
|
|
164
|
+
|
|
165
|
+
def _handle_failed_command(
|
|
166
|
+
self, command: Union[Command, Dict], result: Dict[str, Any]
|
|
167
|
+
) -> None:
|
|
168
|
+
"""Handle a failed command execution."""
|
|
169
|
+
self._update_command_result(command, result, success=False)
|
|
170
|
+
self.failed_commands.append(command)
|
|
171
|
+
logger.warning(f"Command failed: {self._extract_command_string(command)}")
|
|
172
|
+
|
|
173
|
+
def _handle_error(self, command: Union[Command, Dict], error: str) -> None:
|
|
174
|
+
"""Handle an error during command execution."""
|
|
175
|
+
result = {
|
|
176
|
+
"success": False,
|
|
177
|
+
"error": error,
|
|
178
|
+
"return_code": -1,
|
|
179
|
+
"stdout": "",
|
|
180
|
+
"stderr": error,
|
|
181
|
+
}
|
|
182
|
+
self._update_command_result(command, result, success=False)
|
|
183
|
+
self.failed_commands.append(command)
|
|
184
|
+
logger.error(f"Error executing command: {error}")
|
|
185
|
+
|
|
186
|
+
def _update_command_result(
|
|
187
|
+
self, command: Union[Command, Dict], result: Dict[str, Any], success: bool
|
|
188
|
+
) -> None:
|
|
189
|
+
"""Update a command object with execution results."""
|
|
190
|
+
if isinstance(command, dict):
|
|
191
|
+
command.update(
|
|
192
|
+
{
|
|
193
|
+
"success": success,
|
|
194
|
+
"return_code": result.get("return_code", -1),
|
|
195
|
+
"stdout": result.get("stdout", ""),
|
|
196
|
+
"stderr": result.get("stderr", ""),
|
|
197
|
+
"error": result.get("error"),
|
|
198
|
+
"execution_time": result.get("execution_time", 0),
|
|
199
|
+
}
|
|
200
|
+
)
|
|
201
|
+
else:
|
|
202
|
+
command.success = success
|
|
203
|
+
command.return_code = result.get("return_code", -1)
|
|
204
|
+
command.stdout = result.get("stdout", "")
|
|
205
|
+
command.stderr = result.get("stderr", "")
|
|
206
|
+
command.error = result.get("error")
|
|
207
|
+
command.execution_time = result.get("execution_time", 0)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Handler for finding and processing configuration files."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict, List, Optional, Set, Union
|
|
6
|
+
|
|
7
|
+
from ...services.file_processor import FileProcessor
|
|
8
|
+
from ...services.pattern_matcher import PatternMatcher
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ConfigFileHandler:
|
|
14
|
+
"""Handler for finding and processing configuration files."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
project_path: Path,
|
|
19
|
+
exclude_patterns: Optional[List[str]] = None,
|
|
20
|
+
include_patterns: Optional[List[str]] = None,
|
|
21
|
+
ignore_file: Optional[Path] = None,
|
|
22
|
+
):
|
|
23
|
+
"""Initialize the ConfigFileHandler.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
project_path: Path to the project root
|
|
27
|
+
exclude_patterns: List of file patterns to exclude
|
|
28
|
+
include_patterns: List of file patterns to include
|
|
29
|
+
ignore_file: Path to the ignore file
|
|
30
|
+
"""
|
|
31
|
+
self.project_path = project_path
|
|
32
|
+
self.exclude_patterns = set(exclude_patterns or [])
|
|
33
|
+
self.include_patterns = set(include_patterns or [])
|
|
34
|
+
self.ignore_file = ignore_file
|
|
35
|
+
self.file_processor = FileProcessor(project_root=project_path)
|
|
36
|
+
self.pattern_matcher = PatternMatcher()
|
|
37
|
+
|
|
38
|
+
# Load ignore patterns from file if provided
|
|
39
|
+
if self.ignore_file and self.ignore_file.exists():
|
|
40
|
+
self._load_ignore_patterns()
|
|
41
|
+
|
|
42
|
+
def find_config_files(self, parsers: List[Any]) -> List[Path]:
|
|
43
|
+
"""Find all configuration files that can be parsed by the given parsers.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
parsers: List of parser instances that can parse configuration files
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
List of Path objects to configuration files
|
|
50
|
+
"""
|
|
51
|
+
if not parsers:
|
|
52
|
+
logger.warning("No parsers provided, cannot find config files")
|
|
53
|
+
return []
|
|
54
|
+
|
|
55
|
+
# Get all supported file patterns from parsers
|
|
56
|
+
supported_patterns: Set[str] = set()
|
|
57
|
+
for parser in parsers:
|
|
58
|
+
if hasattr(parser, "get_supported_files"):
|
|
59
|
+
patterns = parser.get_supported_files()
|
|
60
|
+
if patterns:
|
|
61
|
+
supported_patterns.update(patterns)
|
|
62
|
+
|
|
63
|
+
if not supported_patterns:
|
|
64
|
+
logger.warning("No supported file patterns found in parsers")
|
|
65
|
+
return []
|
|
66
|
+
|
|
67
|
+
logger.debug(f"Looking for files matching patterns: {supported_patterns}")
|
|
68
|
+
|
|
69
|
+
# Find all files matching the patterns
|
|
70
|
+
matched_files = self.file_processor.find_files(
|
|
71
|
+
patterns=supported_patterns,
|
|
72
|
+
exclude_patterns=self.exclude_patterns,
|
|
73
|
+
include_patterns=self.include_patterns,
|
|
74
|
+
max_depth=3, # Limit depth for performance
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
logger.info(f"Found {len(matched_files)} configuration files")
|
|
78
|
+
return matched_files
|
|
79
|
+
|
|
80
|
+
def _load_ignore_patterns(self) -> None:
|
|
81
|
+
"""Load ignore patterns from the ignore file."""
|
|
82
|
+
if not self.ignore_file or not self.ignore_file.exists():
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
with open(self.ignore_file, "r", encoding="utf-8") as f:
|
|
87
|
+
patterns = [
|
|
88
|
+
line.strip()
|
|
89
|
+
for line in f
|
|
90
|
+
if line.strip() and not line.startswith("#")
|
|
91
|
+
]
|
|
92
|
+
self.exclude_patterns.update(patterns)
|
|
93
|
+
logger.info(
|
|
94
|
+
f"Loaded {len(patterns)} ignore patterns from {self.ignore_file}"
|
|
95
|
+
)
|
|
96
|
+
except Exception as e:
|
|
97
|
+
logger.error(f"Error loading ignore patterns from {self.ignore_file}: {e}")
|
|
98
|
+
|
|
99
|
+
def should_process_file(self, file_path: Union[str, Path]) -> bool:
|
|
100
|
+
"""Check if a file should be processed based on include/exclude patterns.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
file_path: Path to the file to check
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
True if the file should be processed, False otherwise
|
|
107
|
+
"""
|
|
108
|
+
if not isinstance(file_path, Path):
|
|
109
|
+
file_path = Path(file_path)
|
|
110
|
+
|
|
111
|
+
if not file_path.exists() or not file_path.is_file():
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
# Convert to relative path for pattern matching
|
|
115
|
+
try:
|
|
116
|
+
rel_path = file_path.relative_to(self.project_path)
|
|
117
|
+
except ValueError:
|
|
118
|
+
# File is outside project directory
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
# Check exclude patterns first
|
|
122
|
+
if self.pattern_matcher.match_any_pattern(str(rel_path), self.exclude_patterns):
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
# If include patterns are specified, file must match at least one
|
|
126
|
+
if self.include_patterns and not self.pattern_matcher.match_any_pattern(
|
|
127
|
+
str(rel_path), self.include_patterns
|
|
128
|
+
):
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
return True
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Base parser class for detecting commands in configuration files."""
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, List, Optional, Set, Union
|
|
7
|
+
|
|
8
|
+
from ...models import Command
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BaseParser(abc.ABC):
|
|
14
|
+
"""Abstract base class for command parsers."""
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
@abc.abstractmethod
|
|
18
|
+
def get_supported_files(cls) -> Set[str]:
|
|
19
|
+
"""Get the file patterns this parser can handle.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Set of file patterns (e.g., {"*.py", "*.sh"})
|
|
23
|
+
"""
|
|
24
|
+
raise NotImplementedError("Subclasses must implement get_supported_files()")
|
|
25
|
+
|
|
26
|
+
@abc.abstractmethod
|
|
27
|
+
def parse(self, content: str, file_path: Optional[Path] = None) -> List[Command]:
|
|
28
|
+
"""Parse commands from file content.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
content: File content to parse
|
|
32
|
+
file_path: Optional path to the file being parsed
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
List of Command objects
|
|
36
|
+
"""
|
|
37
|
+
raise NotImplementedError("Subclasses must implement parse()")
|
|
38
|
+
|
|
39
|
+
def parse_file(self, file_path: Union[str, Path]) -> List[Command]:
|
|
40
|
+
"""Parse commands from a file.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
file_path: Path to the file to parse
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
List of Command objects
|
|
47
|
+
"""
|
|
48
|
+
file_path = Path(file_path)
|
|
49
|
+
if not file_path.exists():
|
|
50
|
+
logger.warning(f"File not found: {file_path}")
|
|
51
|
+
return []
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
content = file_path.read_text(encoding="utf-8")
|
|
55
|
+
return self.parse(content, file_path=file_path)
|
|
56
|
+
except Exception as e:
|
|
57
|
+
logger.error(f"Error parsing file {file_path}: {e}", exc_info=True)
|
|
58
|
+
return []
|
|
59
|
+
|
|
60
|
+
def can_parse(self, file_path: Union[str, Path]) -> bool:
|
|
61
|
+
"""Check if this parser can handle the given file.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
file_path: Path to the file to check
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
True if this parser can handle the file, False otherwise
|
|
68
|
+
"""
|
|
69
|
+
file_path = Path(file_path)
|
|
70
|
+
supported_patterns = self.get_supported_files()
|
|
71
|
+
|
|
72
|
+
# If no patterns are specified, assume the parser can handle any file
|
|
73
|
+
if not supported_patterns:
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
# Check if the file matches any of the supported patterns
|
|
77
|
+
file_name = file_path.name
|
|
78
|
+
for pattern in supported_patterns:
|
|
79
|
+
if pattern.startswith("*"):
|
|
80
|
+
if file_name.endswith(pattern[1:]):
|
|
81
|
+
return True
|
|
82
|
+
elif file_name == pattern:
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
return False
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Registry for managing command parsers."""
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import inspect
|
|
5
|
+
import logging
|
|
6
|
+
import pkgutil
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, List, Optional, Set, Type, TypeVar, Union
|
|
9
|
+
|
|
10
|
+
from .base_parser import BaseParser
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
T = TypeVar("T", bound=BaseParser)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ParserRegistry:
|
|
18
|
+
"""Registry for managing and discovering command parsers."""
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
"""Initialize the parser registry."""
|
|
22
|
+
self._parsers: Dict[str, Type[BaseParser]] = {}
|
|
23
|
+
self._discovered = False
|
|
24
|
+
|
|
25
|
+
def register(self, parser_class: Type[T]) -> Type[T]:
|
|
26
|
+
"""Register a parser class.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
parser_class: Parser class to register
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
The registered parser class (for use as a decorator)
|
|
33
|
+
"""
|
|
34
|
+
if not inspect.isclass(parser_class) or not issubclass(
|
|
35
|
+
parser_class, BaseParser
|
|
36
|
+
):
|
|
37
|
+
raise ValueError(
|
|
38
|
+
f"Parser must be a subclass of BaseParser, got {parser_class}"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
parser_name = parser_class.__name__
|
|
42
|
+
self._parsers[parser_name] = parser_class
|
|
43
|
+
logger.debug(f"Registered parser: {parser_name}")
|
|
44
|
+
return parser_class
|
|
45
|
+
|
|
46
|
+
def get_parser(self, name: str) -> Optional[Type[BaseParser]]:
|
|
47
|
+
"""Get a parser class by name.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
name: Name of the parser class
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
The parser class, or None if not found
|
|
54
|
+
"""
|
|
55
|
+
return self._parsers.get(name)
|
|
56
|
+
|
|
57
|
+
def get_parsers(self) -> List[Type[BaseParser]]:
|
|
58
|
+
"""Get all registered parser classes.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
List of parser classes
|
|
62
|
+
"""
|
|
63
|
+
return list(self._parsers.values())
|
|
64
|
+
|
|
65
|
+
def create_parser_instances(self) -> List[BaseParser]:
|
|
66
|
+
"""Create instances of all registered parsers.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
List of parser instances
|
|
70
|
+
"""
|
|
71
|
+
return [parser() for parser in self._parsers.values()]
|
|
72
|
+
|
|
73
|
+
def discover_parsers(self, package_path: Optional[Union[str, Path]] = None) -> None:
|
|
74
|
+
"""Discover and register parser classes in the given package.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
package_path: Path to the package containing parser modules.
|
|
78
|
+
If None, uses the default parsers directory.
|
|
79
|
+
"""
|
|
80
|
+
if self._discovered:
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
if package_path is None:
|
|
84
|
+
# Default to the parsers directory in the same package
|
|
85
|
+
package_path = Path(__file__).parent / "parsers"
|
|
86
|
+
package = f"{__package__}.parsers"
|
|
87
|
+
else:
|
|
88
|
+
package_path = Path(package_path)
|
|
89
|
+
package = package_path.name
|
|
90
|
+
|
|
91
|
+
if not package_path.exists():
|
|
92
|
+
logger.warning(f"Parser directory not found: {package_path}")
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
logger.debug(f"Discovering parsers in {package_path}")
|
|
96
|
+
|
|
97
|
+
# Import all modules in the parsers directory
|
|
98
|
+
for finder, name, _ in pkgutil.iter_modules(
|
|
99
|
+
[str(package_path)], prefix=f"{package}."
|
|
100
|
+
):
|
|
101
|
+
try:
|
|
102
|
+
# Skip __pycache__ and other non-module directories
|
|
103
|
+
if name.endswith("__pycache__"):
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
module = importlib.import_module(name)
|
|
107
|
+
self._register_parsers_from_module(module)
|
|
108
|
+
except ImportError as e:
|
|
109
|
+
logger.error(f"Failed to import parser module {name}: {e}")
|
|
110
|
+
|
|
111
|
+
self._discovered = True
|
|
112
|
+
|
|
113
|
+
def _register_parsers_from_module(self, module) -> None:
|
|
114
|
+
"""Register all parser classes found in a module.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
module: Python module to search for parser classes
|
|
118
|
+
"""
|
|
119
|
+
for name, obj in inspect.getmembers(module):
|
|
120
|
+
if (
|
|
121
|
+
inspect.isclass(obj)
|
|
122
|
+
and issubclass(obj, BaseParser)
|
|
123
|
+
and obj is not BaseParser
|
|
124
|
+
):
|
|
125
|
+
self.register(obj)
|
|
126
|
+
logger.debug(f"Discovered parser: {obj.__name__}")
|
|
127
|
+
|
|
128
|
+
def get_parser_for_file(self, file_path: Union[str, Path]) -> Optional[BaseParser]:
|
|
129
|
+
"""Get an appropriate parser for the given file.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
file_path: Path to the file to find a parser for
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Parser instance that can handle the file, or None if none found
|
|
136
|
+
"""
|
|
137
|
+
file_path = Path(file_path)
|
|
138
|
+
|
|
139
|
+
# Try to find a parser that explicitly supports this file
|
|
140
|
+
for parser_class in self._parsers.values():
|
|
141
|
+
parser = parser_class()
|
|
142
|
+
if hasattr(parser, "can_parse") and parser.can_parse(file_path):
|
|
143
|
+
return parser
|
|
144
|
+
|
|
145
|
+
# Fall back to checking file extensions
|
|
146
|
+
for parser_class in self._parsers.values():
|
|
147
|
+
parser = parser_class()
|
|
148
|
+
if hasattr(parser, "get_supported_files"):
|
|
149
|
+
supported_files = parser.get_supported_files()
|
|
150
|
+
if supported_files and any(
|
|
151
|
+
file_path.match(pat) for pat in supported_files
|
|
152
|
+
):
|
|
153
|
+
return parser
|
|
154
|
+
|
|
155
|
+
return None
|