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.
Files changed (123) hide show
  1. {domd-2.2.7 → domd-2.2.8}/PKG-INFO +1 -1
  2. {domd-2.2.7 → domd-2.2.8}/pyproject.toml +1 -1
  3. domd-2.2.8/src/domd/core/command_detection/handlers/command_handler.py +207 -0
  4. domd-2.2.8/src/domd/core/command_detection/handlers/config_handler.py +131 -0
  5. domd-2.2.8/src/domd/core/command_detection/parsers/__init__.py +6 -0
  6. domd-2.2.8/src/domd/core/command_detection/parsers/base_parser.py +85 -0
  7. domd-2.2.8/src/domd/core/command_detection/parsers/parser_registry.py +155 -0
  8. domd-2.2.8/src/domd/core/command_detection/project_detector.py +312 -0
  9. domd-2.2.8/src/domd/core/command_detection/utils/__init__.py +11 -0
  10. domd-2.2.8/src/domd/core/command_detection/utils/file_utils.py +107 -0
  11. domd-2.2.8/src/domd/core/command_detection/utils/virtualenv.py +103 -0
  12. {domd-2.2.7 → domd-2.2.8}/LICENSE +0 -0
  13. {domd-2.2.7 → domd-2.2.8}/README.md +0 -0
  14. {domd-2.2.7 → domd-2.2.8}/src/domd/__init__.py +0 -0
  15. {domd-2.2.7 → domd-2.2.8}/src/domd/adapters/__init__.py +0 -0
  16. {domd-2.2.7 → domd-2.2.8}/src/domd/adapters/api/__init__.py +0 -0
  17. {domd-2.2.7 → domd-2.2.8}/src/domd/adapters/api/flask_api.py +0 -0
  18. {domd-2.2.7 → domd-2.2.8}/src/domd/adapters/cli/__init__.py +0 -0
  19. {domd-2.2.7 → domd-2.2.8}/src/domd/adapters/cli/command_presenter.py +0 -0
  20. {domd-2.2.7 → domd-2.2.8}/src/domd/adapters/formatters/__init__.py +0 -0
  21. {domd-2.2.7 → domd-2.2.8}/src/domd/adapters/formatters/markdown_formatter.py +0 -0
  22. {domd-2.2.7 → domd-2.2.8}/src/domd/adapters/persistence/__init__.py +0 -0
  23. {domd-2.2.7 → domd-2.2.8}/src/domd/adapters/persistence/in_memory_command_repository.py +0 -0
  24. {domd-2.2.7 → domd-2.2.8}/src/domd/adapters/persistence/shell_command_executor.py +0 -0
  25. {domd-2.2.7 → domd-2.2.8}/src/domd/api.py +0 -0
  26. {domd-2.2.7 → domd-2.2.8}/src/domd/application/__init__.py +0 -0
  27. {domd-2.2.7 → domd-2.2.8}/src/domd/application/factory.py +0 -0
  28. {domd-2.2.7 → domd-2.2.8}/src/domd/cli.py +0 -0
  29. {domd-2.2.7 → domd-2.2.8}/src/domd/command_execution/__init__.py +0 -0
  30. {domd-2.2.7 → domd-2.2.8}/src/domd/core/__init__.py +0 -0
  31. {domd-2.2.7 → domd-2.2.8}/src/domd/core/command_detection/__init__.py +0 -0
  32. {domd-2.2.7 → domd-2.2.8}/src/domd/core/command_detection/base_detector.py +0 -0
  33. {domd-2.2.7 → domd-2.2.8}/src/domd/core/command_detection/handlers/__init__.py +0 -0
  34. {domd-2.2.7 → domd-2.2.8}/src/domd/core/command_detection/models.py +0 -0
  35. {domd-2.2.7 → domd-2.2.8}/src/domd/core/command_execution/__init__.py +0 -0
  36. {domd-2.2.7 → domd-2.2.8}/src/domd/core/command_execution/command_executor.py +0 -0
  37. {domd-2.2.7 → domd-2.2.8}/src/domd/core/command_execution/command_recorder.py +0 -0
  38. {domd-2.2.7 → domd-2.2.8}/src/domd/core/command_execution/command_runner.py +0 -0
  39. {domd-2.2.7 → domd-2.2.8}/src/domd/core/command_execution/environment_detector.py +0 -0
  40. {domd-2.2.7 → domd-2.2.8}/src/domd/core/command_execution/executor.py +0 -0
  41. {domd-2.2.7 → domd-2.2.8}/src/domd/core/commands/__init__.py +0 -0
  42. {domd-2.2.7 → domd-2.2.8}/src/domd/core/commands/command.py +0 -0
  43. {domd-2.2.7 → domd-2.2.8}/src/domd/core/commands/executor.py +0 -0
  44. {domd-2.2.7 → domd-2.2.8}/src/domd/core/detector.py +0 -0
  45. {domd-2.2.7 → domd-2.2.8}/src/domd/core/domain/__init__.py +0 -0
  46. {domd-2.2.7 → domd-2.2.8}/src/domd/core/domain/command.py +0 -0
  47. {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsers/__init__.py +0 -0
  48. {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsers/ansible_galaxy.py +0 -0
  49. {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsers/ansible_inventory.py +0 -0
  50. {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsers/ansible_playbook.py +0 -0
  51. {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsers/ansible_role.py +0 -0
  52. {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsers/ansible_vault.py +0 -0
  53. {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsers/base.py +0 -0
  54. {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsers/cargo_toml.py +0 -0
  55. {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsers/composer_json.py +0 -0
  56. {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsers/go_mod.py +0 -0
  57. {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsers/makefile.py +0 -0
  58. {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsers/package_json.py +0 -0
  59. {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsers/pyproject_toml.py +0 -0
  60. {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsers/tox_ini.py +0 -0
  61. {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsing/__init__.py +0 -0
  62. {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsing/base.py +0 -0
  63. {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsing/file_processor.py +0 -0
  64. {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsing/parser_registry.py +0 -0
  65. {domd-2.2.7 → domd-2.2.8}/src/domd/core/parsing/pattern_matcher.py +0 -0
  66. {domd-2.2.7 → domd-2.2.8}/src/domd/core/ports/__init__.py +0 -0
  67. {domd-2.2.7 → domd-2.2.8}/src/domd/core/ports/command_executor.py +0 -0
  68. {domd-2.2.7 → domd-2.2.8}/src/domd/core/ports/command_repository.py +0 -0
  69. {domd-2.2.7 → domd-2.2.8}/src/domd/core/ports/report_formatter.py +0 -0
  70. {domd-2.2.7 → domd-2.2.8}/src/domd/core/project_detection/__init__.py +0 -0
  71. {domd-2.2.7 → domd-2.2.8}/src/domd/core/project_detection/command_handling.py +0 -0
  72. {domd-2.2.7 → domd-2.2.8}/src/domd/core/project_detection/config_files.py +0 -0
  73. {domd-2.2.7 → domd-2.2.8}/src/domd/core/project_detection/detector.py +0 -0
  74. {domd-2.2.7 → domd-2.2.8}/src/domd/core/project_detection/virtualenv.py +0 -0
  75. {domd-2.2.7 → domd-2.2.8}/src/domd/core/reporters/__init__.py +0 -0
  76. {domd-2.2.7 → domd-2.2.8}/src/domd/core/reporters/base.py +0 -0
  77. {domd-2.2.7 → domd-2.2.8}/src/domd/core/reporters/done_md.py +0 -0
  78. {domd-2.2.7 → domd-2.2.8}/src/domd/core/reporters/todo_md.py +0 -0
  79. {domd-2.2.7 → domd-2.2.8}/src/domd/core/reporting/__init__.py +0 -0
  80. {domd-2.2.7 → domd-2.2.8}/src/domd/core/reporting/formatters.py +0 -0
  81. {domd-2.2.7 → domd-2.2.8}/src/domd/core/reporting/reporter.py +0 -0
  82. {domd-2.2.7 → domd-2.2.8}/src/domd/core/services/__init__.py +0 -0
  83. {domd-2.2.7 → domd-2.2.8}/src/domd/core/services/command_service.py +0 -0
  84. {domd-2.2.7 → domd-2.2.8}/src/domd/core/services/report_service.py +0 -0
  85. {domd-2.2.7 → domd-2.2.8}/src/domd/core/utils/__init__.py +0 -0
  86. {domd-2.2.7 → domd-2.2.8}/src/domd/core/utils/command_utils.py +0 -0
  87. {domd-2.2.7 → domd-2.2.8}/src/domd/core/utils/environment.py +0 -0
  88. {domd-2.2.7 → domd-2.2.8}/src/domd/core/utils/file_utils.py +0 -0
  89. {domd-2.2.7 → domd-2.2.8}/src/domd/core/utils/logging_utils.py +0 -0
  90. {domd-2.2.7 → domd-2.2.8}/src/domd/core/utils/virtualenv.py +0 -0
  91. {domd-2.2.7 → domd-2.2.8}/src/domd/parsers/__init__.py +0 -0
  92. {domd-2.2.7 → domd-2.2.8}/src/domd/parsers/ansible.py +0 -0
  93. {domd-2.2.7 → domd-2.2.8}/src/domd/parsers/base.py +0 -0
  94. {domd-2.2.7 → domd-2.2.8}/src/domd/parsers/build_systems.py +0 -0
  95. {domd-2.2.7 → domd-2.2.8}/src/domd/parsers/ci_cd.py +0 -0
  96. {domd-2.2.7 → domd-2.2.8}/src/domd/parsers/docker/__init__.py +0 -0
  97. {domd-2.2.7 → domd-2.2.8}/src/domd/parsers/docker/docker_compose.py +0 -0
  98. {domd-2.2.7 → domd-2.2.8}/src/domd/parsers/docker/dockerfile.py +0 -0
  99. {domd-2.2.7 → domd-2.2.8}/src/domd/parsers/docker.py +0 -0
  100. {domd-2.2.7 → domd-2.2.8}/src/domd/parsers/javascript/__init__.py +0 -0
  101. {domd-2.2.7 → domd-2.2.8}/src/domd/parsers/javascript.py +0 -0
  102. {domd-2.2.7 → domd-2.2.8}/src/domd/parsers/package_json.py +0 -0
  103. {domd-2.2.7 → domd-2.2.8}/src/domd/parsers/python.py +0 -0
  104. {domd-2.2.7 → domd-2.2.8}/src/domd/parsers/test_build_systems.py +0 -0
  105. {domd-2.2.7 → domd-2.2.8}/src/domd/parsers/test_ci_cd.py +0 -0
  106. {domd-2.2.7 → domd-2.2.8}/src/domd/parsers/test_docker.py +0 -0
  107. {domd-2.2.7 → domd-2.2.8}/src/domd/parsers/test_javascript.py +0 -0
  108. {domd-2.2.7 → domd-2.2.8}/src/domd/parsers/test_python.py +0 -0
  109. {domd-2.2.7 → domd-2.2.8}/src/domd/parsing/__init__.py +0 -0
  110. {domd-2.2.7 → domd-2.2.8}/src/domd/parsing/base.py +0 -0
  111. {domd-2.2.7 → domd-2.2.8}/src/domd/reporters/__init__.py +0 -0
  112. {domd-2.2.7 → domd-2.2.8}/src/domd/reporters/console.py +0 -0
  113. {domd-2.2.7 → domd-2.2.8}/src/domd/reporters/json_reporter.py +0 -0
  114. {domd-2.2.7 → domd-2.2.8}/src/domd/reporters/test_console.py +0 -0
  115. {domd-2.2.7 → domd-2.2.8}/src/domd/reporters/test_json_reporter.py +0 -0
  116. {domd-2.2.7 → domd-2.2.8}/src/domd/reporters/test_todo_md.py +0 -0
  117. {domd-2.2.7 → domd-2.2.8}/src/domd/reporters/todo_md.py +0 -0
  118. {domd-2.2.7 → domd-2.2.8}/src/domd/reporting/__init__.py +0 -0
  119. {domd-2.2.7 → domd-2.2.8}/src/domd/utils/__init__.py +0 -0
  120. {domd-2.2.7 → domd-2.2.8}/src/domd/utils/command_runner.py +0 -0
  121. {domd-2.2.7 → domd-2.2.8}/src/domd/utils/file_utils.py +0 -0
  122. {domd-2.2.7 → domd-2.2.8}/src/domd/utils/test_command_runner.py +0 -0
  123. {domd-2.2.7 → domd-2.2.8}/src/domd/utils/test_file_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: domd
3
- Version: 2.2.7
3
+ Version: 2.2.8
4
4
  Summary: Project Command Detector - Automatically detects and tests project commands, generates TODO.md for failed commands
5
5
  License: Apache-2.0
6
6
  Author: Tom Sapletta
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "domd"
3
- version = "2.2.7"
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,6 @@
1
+ """Parsers for detecting commands in various file types."""
2
+
3
+ from .base_parser import BaseParser
4
+ from .parser_registry import ParserRegistry
5
+
6
+ __all__ = ["ParserRegistry", "BaseParser"]
@@ -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