crackerjack 0.33.0__py3-none-any.whl → 0.33.2__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.

Potentially problematic release.


This version of crackerjack might be problematic. Click here for more details.

Files changed (198) hide show
  1. crackerjack/__main__.py +1350 -34
  2. crackerjack/adapters/__init__.py +17 -0
  3. crackerjack/adapters/lsp_client.py +358 -0
  4. crackerjack/adapters/rust_tool_adapter.py +194 -0
  5. crackerjack/adapters/rust_tool_manager.py +193 -0
  6. crackerjack/adapters/skylos_adapter.py +231 -0
  7. crackerjack/adapters/zuban_adapter.py +560 -0
  8. crackerjack/agents/base.py +7 -3
  9. crackerjack/agents/coordinator.py +271 -33
  10. crackerjack/agents/documentation_agent.py +9 -15
  11. crackerjack/agents/dry_agent.py +3 -15
  12. crackerjack/agents/formatting_agent.py +1 -1
  13. crackerjack/agents/import_optimization_agent.py +36 -180
  14. crackerjack/agents/performance_agent.py +17 -98
  15. crackerjack/agents/performance_helpers.py +7 -31
  16. crackerjack/agents/proactive_agent.py +1 -3
  17. crackerjack/agents/refactoring_agent.py +16 -85
  18. crackerjack/agents/refactoring_helpers.py +7 -42
  19. crackerjack/agents/security_agent.py +9 -48
  20. crackerjack/agents/test_creation_agent.py +356 -513
  21. crackerjack/agents/test_specialist_agent.py +0 -4
  22. crackerjack/api.py +6 -25
  23. crackerjack/cli/cache_handlers.py +204 -0
  24. crackerjack/cli/cache_handlers_enhanced.py +683 -0
  25. crackerjack/cli/facade.py +100 -0
  26. crackerjack/cli/handlers.py +224 -9
  27. crackerjack/cli/interactive.py +6 -4
  28. crackerjack/cli/options.py +642 -55
  29. crackerjack/cli/utils.py +2 -1
  30. crackerjack/code_cleaner.py +58 -117
  31. crackerjack/config/global_lock_config.py +8 -48
  32. crackerjack/config/hooks.py +53 -62
  33. crackerjack/core/async_workflow_orchestrator.py +24 -34
  34. crackerjack/core/autofix_coordinator.py +3 -17
  35. crackerjack/core/enhanced_container.py +4 -13
  36. crackerjack/core/file_lifecycle.py +12 -89
  37. crackerjack/core/performance.py +2 -2
  38. crackerjack/core/performance_monitor.py +15 -55
  39. crackerjack/core/phase_coordinator.py +104 -204
  40. crackerjack/core/resource_manager.py +14 -90
  41. crackerjack/core/service_watchdog.py +62 -95
  42. crackerjack/core/session_coordinator.py +149 -0
  43. crackerjack/core/timeout_manager.py +14 -72
  44. crackerjack/core/websocket_lifecycle.py +13 -78
  45. crackerjack/core/workflow_orchestrator.py +171 -174
  46. crackerjack/docs/INDEX.md +11 -0
  47. crackerjack/docs/generated/api/API_REFERENCE.md +10895 -0
  48. crackerjack/docs/generated/api/CLI_REFERENCE.md +109 -0
  49. crackerjack/docs/generated/api/CROSS_REFERENCES.md +1755 -0
  50. crackerjack/docs/generated/api/PROTOCOLS.md +3 -0
  51. crackerjack/docs/generated/api/SERVICES.md +1252 -0
  52. crackerjack/documentation/__init__.py +31 -0
  53. crackerjack/documentation/ai_templates.py +756 -0
  54. crackerjack/documentation/dual_output_generator.py +765 -0
  55. crackerjack/documentation/mkdocs_integration.py +518 -0
  56. crackerjack/documentation/reference_generator.py +977 -0
  57. crackerjack/dynamic_config.py +55 -50
  58. crackerjack/executors/async_hook_executor.py +10 -15
  59. crackerjack/executors/cached_hook_executor.py +117 -43
  60. crackerjack/executors/hook_executor.py +8 -34
  61. crackerjack/executors/hook_lock_manager.py +26 -183
  62. crackerjack/executors/individual_hook_executor.py +13 -11
  63. crackerjack/executors/lsp_aware_hook_executor.py +270 -0
  64. crackerjack/executors/tool_proxy.py +417 -0
  65. crackerjack/hooks/lsp_hook.py +79 -0
  66. crackerjack/intelligence/adaptive_learning.py +25 -10
  67. crackerjack/intelligence/agent_orchestrator.py +2 -5
  68. crackerjack/intelligence/agent_registry.py +34 -24
  69. crackerjack/intelligence/agent_selector.py +5 -7
  70. crackerjack/interactive.py +17 -6
  71. crackerjack/managers/async_hook_manager.py +0 -1
  72. crackerjack/managers/hook_manager.py +79 -1
  73. crackerjack/managers/publish_manager.py +44 -8
  74. crackerjack/managers/test_command_builder.py +1 -15
  75. crackerjack/managers/test_executor.py +1 -3
  76. crackerjack/managers/test_manager.py +98 -7
  77. crackerjack/managers/test_manager_backup.py +10 -9
  78. crackerjack/mcp/cache.py +2 -2
  79. crackerjack/mcp/client_runner.py +1 -1
  80. crackerjack/mcp/context.py +191 -68
  81. crackerjack/mcp/dashboard.py +7 -5
  82. crackerjack/mcp/enhanced_progress_monitor.py +31 -28
  83. crackerjack/mcp/file_monitor.py +30 -23
  84. crackerjack/mcp/progress_components.py +31 -21
  85. crackerjack/mcp/progress_monitor.py +50 -53
  86. crackerjack/mcp/rate_limiter.py +6 -6
  87. crackerjack/mcp/server_core.py +17 -16
  88. crackerjack/mcp/service_watchdog.py +2 -1
  89. crackerjack/mcp/state.py +4 -7
  90. crackerjack/mcp/task_manager.py +11 -9
  91. crackerjack/mcp/tools/core_tools.py +173 -32
  92. crackerjack/mcp/tools/error_analyzer.py +3 -2
  93. crackerjack/mcp/tools/execution_tools.py +8 -10
  94. crackerjack/mcp/tools/execution_tools_backup.py +42 -30
  95. crackerjack/mcp/tools/intelligence_tool_registry.py +7 -5
  96. crackerjack/mcp/tools/intelligence_tools.py +5 -2
  97. crackerjack/mcp/tools/monitoring_tools.py +33 -70
  98. crackerjack/mcp/tools/proactive_tools.py +24 -11
  99. crackerjack/mcp/tools/progress_tools.py +5 -8
  100. crackerjack/mcp/tools/utility_tools.py +20 -14
  101. crackerjack/mcp/tools/workflow_executor.py +62 -40
  102. crackerjack/mcp/websocket/app.py +8 -0
  103. crackerjack/mcp/websocket/endpoints.py +352 -357
  104. crackerjack/mcp/websocket/jobs.py +40 -57
  105. crackerjack/mcp/websocket/monitoring_endpoints.py +2935 -0
  106. crackerjack/mcp/websocket/server.py +7 -25
  107. crackerjack/mcp/websocket/websocket_handler.py +6 -17
  108. crackerjack/mixins/__init__.py +0 -2
  109. crackerjack/mixins/error_handling.py +1 -70
  110. crackerjack/models/config.py +12 -1
  111. crackerjack/models/config_adapter.py +49 -1
  112. crackerjack/models/protocols.py +122 -122
  113. crackerjack/models/resource_protocols.py +55 -210
  114. crackerjack/monitoring/ai_agent_watchdog.py +13 -13
  115. crackerjack/monitoring/metrics_collector.py +426 -0
  116. crackerjack/monitoring/regression_prevention.py +8 -8
  117. crackerjack/monitoring/websocket_server.py +643 -0
  118. crackerjack/orchestration/advanced_orchestrator.py +11 -6
  119. crackerjack/orchestration/coverage_improvement.py +3 -3
  120. crackerjack/orchestration/execution_strategies.py +26 -6
  121. crackerjack/orchestration/test_progress_streamer.py +8 -5
  122. crackerjack/plugins/base.py +2 -2
  123. crackerjack/plugins/hooks.py +7 -0
  124. crackerjack/plugins/managers.py +11 -8
  125. crackerjack/security/__init__.py +0 -1
  126. crackerjack/security/audit.py +6 -35
  127. crackerjack/services/anomaly_detector.py +392 -0
  128. crackerjack/services/api_extractor.py +615 -0
  129. crackerjack/services/backup_service.py +2 -2
  130. crackerjack/services/bounded_status_operations.py +15 -152
  131. crackerjack/services/cache.py +127 -1
  132. crackerjack/services/changelog_automation.py +395 -0
  133. crackerjack/services/config.py +15 -9
  134. crackerjack/services/config_merge.py +19 -80
  135. crackerjack/services/config_template.py +506 -0
  136. crackerjack/services/contextual_ai_assistant.py +48 -22
  137. crackerjack/services/coverage_badge_service.py +171 -0
  138. crackerjack/services/coverage_ratchet.py +27 -25
  139. crackerjack/services/debug.py +3 -3
  140. crackerjack/services/dependency_analyzer.py +460 -0
  141. crackerjack/services/dependency_monitor.py +14 -11
  142. crackerjack/services/documentation_generator.py +491 -0
  143. crackerjack/services/documentation_service.py +675 -0
  144. crackerjack/services/enhanced_filesystem.py +6 -5
  145. crackerjack/services/enterprise_optimizer.py +865 -0
  146. crackerjack/services/error_pattern_analyzer.py +676 -0
  147. crackerjack/services/file_hasher.py +1 -1
  148. crackerjack/services/git.py +8 -25
  149. crackerjack/services/health_metrics.py +10 -8
  150. crackerjack/services/heatmap_generator.py +735 -0
  151. crackerjack/services/initialization.py +11 -30
  152. crackerjack/services/input_validator.py +5 -97
  153. crackerjack/services/intelligent_commit.py +327 -0
  154. crackerjack/services/log_manager.py +15 -12
  155. crackerjack/services/logging.py +4 -3
  156. crackerjack/services/lsp_client.py +628 -0
  157. crackerjack/services/memory_optimizer.py +19 -87
  158. crackerjack/services/metrics.py +42 -33
  159. crackerjack/services/parallel_executor.py +9 -67
  160. crackerjack/services/pattern_cache.py +1 -1
  161. crackerjack/services/pattern_detector.py +6 -6
  162. crackerjack/services/performance_benchmarks.py +18 -59
  163. crackerjack/services/performance_cache.py +20 -81
  164. crackerjack/services/performance_monitor.py +27 -95
  165. crackerjack/services/predictive_analytics.py +510 -0
  166. crackerjack/services/quality_baseline.py +234 -0
  167. crackerjack/services/quality_baseline_enhanced.py +646 -0
  168. crackerjack/services/quality_intelligence.py +785 -0
  169. crackerjack/services/regex_patterns.py +618 -524
  170. crackerjack/services/regex_utils.py +43 -123
  171. crackerjack/services/secure_path_utils.py +5 -164
  172. crackerjack/services/secure_status_formatter.py +30 -141
  173. crackerjack/services/secure_subprocess.py +11 -92
  174. crackerjack/services/security.py +9 -41
  175. crackerjack/services/security_logger.py +12 -24
  176. crackerjack/services/server_manager.py +124 -16
  177. crackerjack/services/status_authentication.py +16 -159
  178. crackerjack/services/status_security_manager.py +4 -131
  179. crackerjack/services/thread_safe_status_collector.py +19 -125
  180. crackerjack/services/unified_config.py +21 -13
  181. crackerjack/services/validation_rate_limiter.py +5 -54
  182. crackerjack/services/version_analyzer.py +459 -0
  183. crackerjack/services/version_checker.py +1 -1
  184. crackerjack/services/websocket_resource_limiter.py +10 -144
  185. crackerjack/services/zuban_lsp_service.py +390 -0
  186. crackerjack/slash_commands/__init__.py +2 -7
  187. crackerjack/slash_commands/run.md +2 -2
  188. crackerjack/tools/validate_input_validator_patterns.py +14 -40
  189. crackerjack/tools/validate_regex_patterns.py +19 -48
  190. {crackerjack-0.33.0.dist-info → crackerjack-0.33.2.dist-info}/METADATA +196 -25
  191. crackerjack-0.33.2.dist-info/RECORD +229 -0
  192. crackerjack/CLAUDE.md +0 -207
  193. crackerjack/RULES.md +0 -380
  194. crackerjack/py313.py +0 -234
  195. crackerjack-0.33.0.dist-info/RECORD +0 -187
  196. {crackerjack-0.33.0.dist-info → crackerjack-0.33.2.dist-info}/WHEEL +0 -0
  197. {crackerjack-0.33.0.dist-info → crackerjack-0.33.2.dist-info}/entry_points.txt +0 -0
  198. {crackerjack-0.33.0.dist-info → crackerjack-0.33.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,615 @@
1
+ """Service for extracting API documentation from Python source code."""
2
+
3
+ import ast
4
+ import inspect
5
+ import re
6
+ import typing as t
7
+ from pathlib import Path
8
+
9
+ from rich.console import Console
10
+
11
+ from ..models.protocols import APIExtractorProtocol
12
+ from .regex_patterns import SAFE_PATTERNS
13
+
14
+
15
+ class PythonDocstringParser:
16
+ """Parser for extracting structured information from Python docstrings."""
17
+
18
+ def __init__(self) -> None:
19
+ # Regex patterns for different docstring styles using safe patterns
20
+ self.google_param_pattern = SAFE_PATTERNS[
21
+ "extract_google_docstring_params"
22
+ ]._get_compiled_pattern()
23
+ self.sphinx_param_pattern = SAFE_PATTERNS[
24
+ "extract_sphinx_docstring_params"
25
+ ]._get_compiled_pattern()
26
+ self.returns_pattern = SAFE_PATTERNS[
27
+ "extract_docstring_returns"
28
+ ]._get_compiled_pattern()
29
+
30
+ def parse_docstring(self, docstring: str | None) -> dict[str, t.Any]:
31
+ """Parse a docstring and extract structured information."""
32
+ if not docstring:
33
+ return {"description": "", "parameters": {}, "returns": "", "raises": []}
34
+
35
+ docstring = inspect.cleandoc(docstring)
36
+
37
+ # Extract main description (first paragraph)
38
+ lines = docstring.split("\n")
39
+ description_lines = []
40
+ for line in lines:
41
+ if line.strip() and not self._is_section_header(line):
42
+ description_lines.append(line)
43
+ elif description_lines:
44
+ break
45
+
46
+ description = "\n".join(description_lines).strip()
47
+
48
+ # Extract parameters
49
+ parameters = self._extract_parameters(docstring)
50
+
51
+ # Extract returns
52
+ returns = self._extract_returns(docstring)
53
+
54
+ # Extract raises information
55
+ raises = self._extract_raises(docstring)
56
+
57
+ return {
58
+ "description": description,
59
+ "parameters": parameters,
60
+ "returns": returns,
61
+ "raises": raises,
62
+ }
63
+
64
+ def _is_section_header(self, line: str) -> bool:
65
+ """Check if a line is a docstring section header."""
66
+ line = line.strip().lower()
67
+ headers = [
68
+ "args:",
69
+ "arguments:",
70
+ "parameters:",
71
+ "param:",
72
+ "returns:",
73
+ "yields:",
74
+ "raises:",
75
+ "note:",
76
+ "example:",
77
+ ]
78
+ return any(line.startswith(header) for header in headers)
79
+
80
+ def _extract_parameters(self, docstring: str) -> dict[str, str]:
81
+ """Extract parameter documentation from docstring."""
82
+ parameters = {}
83
+
84
+ # Try Google style first
85
+ google_matches = self.google_param_pattern.findall(docstring)
86
+ for param_name, param_desc in google_matches:
87
+ parameters[param_name] = param_desc.strip()
88
+
89
+ # Try Sphinx style if no Google style found
90
+ if not parameters:
91
+ sphinx_matches = self.sphinx_param_pattern.findall(docstring)
92
+ for param_name, param_desc in sphinx_matches:
93
+ parameters[param_name] = param_desc.strip()
94
+
95
+ return parameters
96
+
97
+ def _extract_returns(self, docstring: str) -> str:
98
+ """Extract return value documentation from docstring."""
99
+ match = self.returns_pattern.search(docstring)
100
+ if match:
101
+ return match.group(1).strip()
102
+ return ""
103
+
104
+ def _extract_raises(self, docstring: str) -> list[str]:
105
+ """Extract exception documentation from docstring."""
106
+ raises_pattern = re.compile( # REGEX OK: exception extraction
107
+ r"(?:Raises?|Raise):\s*(.+?)(?=\n\n|\n\w+:|\Z)", re.MULTILINE | re.DOTALL
108
+ )
109
+ match = raises_pattern.search(docstring)
110
+ if match:
111
+ raises_text = match.group(1).strip()
112
+ # Split by lines and clean up
113
+ raises_list = [
114
+ line.strip() for line in raises_text.split("\n") if line.strip()
115
+ ]
116
+ return raises_list
117
+ return []
118
+
119
+
120
+ class APIExtractorImpl(APIExtractorProtocol):
121
+ """Implementation of API documentation extraction from source code."""
122
+
123
+ def __init__(self, console: Console) -> None:
124
+ self.console = console
125
+ self.docstring_parser = PythonDocstringParser()
126
+
127
+ def extract_from_python_files(self, files: list[Path]) -> dict[str, t.Any]:
128
+ """Extract API documentation from Python files."""
129
+ api_data: dict[str, t.Any] = {
130
+ "modules": {},
131
+ "classes": {},
132
+ "functions": {},
133
+ "protocols": {},
134
+ }
135
+
136
+ for file_path in files:
137
+ if not file_path.exists() or file_path.suffix != ".py":
138
+ continue
139
+
140
+ try:
141
+ source_code = Path(file_path).read_text(encoding="utf-8")
142
+
143
+ tree = ast.parse(source_code)
144
+ module_data = self._extract_module_info(tree, file_path, source_code)
145
+ api_data["modules"][str(file_path)] = module_data
146
+
147
+ except Exception as e:
148
+ self.console.print(
149
+ f"[yellow]Warning: Could not parse {file_path}: {e}[/yellow]"
150
+ )
151
+ continue
152
+
153
+ return api_data
154
+
155
+ def extract_protocol_definitions(self, protocol_file: Path) -> dict[str, t.Any]:
156
+ """Extract protocol definitions from protocols.py file."""
157
+ if not protocol_file.exists():
158
+ return {}
159
+
160
+ try:
161
+ source_code = Path(protocol_file).read_text(encoding="utf-8")
162
+
163
+ tree = ast.parse(source_code)
164
+ protocols = {}
165
+
166
+ for node in ast.walk(tree):
167
+ if isinstance(node, ast.ClassDef) and self._is_protocol_class(node):
168
+ protocol_info = self._extract_protocol_info(node, source_code)
169
+ protocols[node.name] = protocol_info
170
+
171
+ return {"protocols": protocols}
172
+
173
+ except Exception as e:
174
+ self.console.print(f"[red]Error extracting protocols: {e}[/red]")
175
+ return {}
176
+
177
+ def extract_service_interfaces(self, service_files: list[Path]) -> dict[str, t.Any]:
178
+ """Extract service interfaces and their methods."""
179
+ services = {}
180
+
181
+ for file_path in service_files:
182
+ if not file_path.exists() or file_path.suffix != ".py":
183
+ continue
184
+
185
+ try:
186
+ source_code = Path(file_path).read_text(encoding="utf-8")
187
+
188
+ tree = ast.parse(source_code)
189
+ service_info = self._extract_service_info(tree, file_path, source_code)
190
+ if service_info:
191
+ services[file_path.stem] = service_info
192
+
193
+ except Exception as e:
194
+ self.console.print(
195
+ f"[yellow]Warning: Could not parse service {file_path}: {e}[/yellow]"
196
+ )
197
+ continue
198
+
199
+ return {"services": services}
200
+
201
+ def extract_cli_commands(self, cli_files: list[Path]) -> dict[str, t.Any]:
202
+ """Extract CLI command definitions and options."""
203
+ cli_data: dict[str, t.Any] = {"commands": {}, "options": {}}
204
+
205
+ for file_path in cli_files:
206
+ if not file_path.exists() or file_path.suffix != ".py":
207
+ continue
208
+
209
+ try:
210
+ source_code = Path(file_path).read_text(encoding="utf-8")
211
+
212
+ tree = ast.parse(source_code)
213
+ cli_info = self._extract_cli_info(tree, source_code)
214
+ cli_data["commands"][file_path.stem] = cli_info
215
+
216
+ except Exception as e:
217
+ self.console.print(
218
+ f"[yellow]Warning: Could not parse CLI file {file_path}: {e}[/yellow]"
219
+ )
220
+ continue
221
+
222
+ return cli_data
223
+
224
+ def extract_mcp_tools(self, mcp_files: list[Path]) -> dict[str, t.Any]:
225
+ """Extract MCP tool definitions and their capabilities."""
226
+ mcp_tools = {}
227
+
228
+ for file_path in mcp_files:
229
+ if not file_path.exists():
230
+ continue
231
+
232
+ try:
233
+ if file_path.suffix == ".py":
234
+ source_code = Path(file_path).read_text(encoding="utf-8")
235
+ tree = ast.parse(source_code)
236
+ tool_info = self._extract_mcp_python_tools(tree, source_code)
237
+ elif file_path.suffix == ".md":
238
+ markdown_content = Path(file_path).read_text(encoding="utf-8")
239
+ tool_info = self._extract_mcp_markdown_docs(markdown_content)
240
+ else:
241
+ continue
242
+
243
+ if tool_info:
244
+ mcp_tools[file_path.stem] = tool_info
245
+
246
+ except Exception as e:
247
+ self.console.print(
248
+ f"[yellow]Warning: Could not parse MCP file {file_path}: {e}[/yellow]"
249
+ )
250
+ continue
251
+
252
+ return {"mcp_tools": mcp_tools}
253
+
254
+ def _extract_module_info(
255
+ self, tree: ast.AST, file_path: Path, source_code: str
256
+ ) -> dict[str, t.Any]:
257
+ """Extract information from a Python module."""
258
+ module_info = self._create_base_module_info(tree, file_path)
259
+ self._populate_module_components(module_info, tree, source_code)
260
+ return module_info
261
+
262
+ def _create_base_module_info(
263
+ self, tree: ast.AST, file_path: Path
264
+ ) -> dict[str, t.Any]:
265
+ """Create the base module information structure."""
266
+ # ast.get_docstring requires Module, ClassDef, FunctionDef, or AsyncFunctionDef
267
+ docstring = (
268
+ ast.get_docstring(tree)
269
+ if isinstance(
270
+ tree, ast.Module | ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef
271
+ )
272
+ else None
273
+ )
274
+ return {
275
+ "path": str(file_path),
276
+ "docstring": docstring,
277
+ "classes": [],
278
+ "functions": [],
279
+ "imports": [],
280
+ }
281
+
282
+ def _populate_module_components(
283
+ self, module_info: dict[str, t.Any], tree: ast.AST, source_code: str
284
+ ) -> None:
285
+ """Populate module components by walking the AST."""
286
+ for node in ast.walk(tree):
287
+ self._process_ast_node(module_info, node, source_code)
288
+
289
+ def _process_ast_node(
290
+ self, module_info: dict[str, t.Any], node: ast.AST, source_code: str
291
+ ) -> None:
292
+ """Process individual AST nodes and extract relevant information."""
293
+ if isinstance(node, ast.ClassDef):
294
+ class_info = self._extract_class_info(node, source_code)
295
+ module_info["classes"].append(class_info)
296
+ elif isinstance(node, ast.FunctionDef):
297
+ func_info = self._extract_function_info(node, source_code)
298
+ module_info["functions"].append(func_info)
299
+ elif isinstance(node, ast.Import | ast.ImportFrom):
300
+ import_info = self._extract_import_info(node)
301
+ module_info["imports"].append(import_info)
302
+
303
+ def _extract_class_info(
304
+ self, node: ast.ClassDef, source_code: str
305
+ ) -> dict[str, t.Any]:
306
+ """Extract information from a class definition."""
307
+ docstring = ast.get_docstring(node)
308
+ parsed_doc = self.docstring_parser.parse_docstring(docstring)
309
+
310
+ class_info = self._build_base_class_info(node, parsed_doc)
311
+ self._extract_class_methods(class_info, node, source_code)
312
+
313
+ return class_info
314
+
315
+ def _build_base_class_info(
316
+ self, node: ast.ClassDef, parsed_doc: dict[str, t.Any]
317
+ ) -> dict[str, t.Any]:
318
+ """Build the base class information structure."""
319
+ return {
320
+ "name": node.name,
321
+ "docstring": parsed_doc,
322
+ "base_classes": [self._get_node_name(base) for base in node.bases],
323
+ "methods": [],
324
+ "properties": [],
325
+ "is_protocol": self._is_protocol_class(node),
326
+ }
327
+
328
+ def _extract_class_methods(
329
+ self, class_info: dict[str, t.Any], node: ast.ClassDef, source_code: str
330
+ ) -> None:
331
+ """Extract method information from class body."""
332
+ for item in node.body:
333
+ if isinstance(item, ast.FunctionDef):
334
+ method_info = self._extract_function_info(item, source_code)
335
+ method_info["is_method"] = True
336
+ method_info["visibility"] = self._determine_method_visibility(item.name)
337
+ class_info["methods"].append(method_info)
338
+
339
+ def _determine_method_visibility(self, method_name: str) -> str:
340
+ """Determine method visibility based on naming convention."""
341
+ if method_name.startswith("_") and not method_name.startswith("__"):
342
+ return "protected"
343
+ elif method_name.startswith("__"):
344
+ return "private"
345
+ return "public"
346
+
347
+ def _extract_function_info(
348
+ self, node: ast.FunctionDef, source_code: str
349
+ ) -> dict[str, t.Any]:
350
+ """Extract information from a function definition."""
351
+ docstring = ast.get_docstring(node)
352
+ parsed_doc = self.docstring_parser.parse_docstring(docstring)
353
+
354
+ # Extract parameter information
355
+ parameters = []
356
+ for arg in node.args.args:
357
+ param_info = {
358
+ "name": arg.arg,
359
+ "annotation": self._get_annotation_string(arg.annotation),
360
+ "description": parsed_doc["parameters"].get(arg.arg, ""),
361
+ }
362
+ parameters.append(param_info)
363
+
364
+ # Extract return annotation
365
+ return_annotation = self._get_annotation_string(node.returns)
366
+
367
+ func_info = {
368
+ "name": node.name,
369
+ "docstring": parsed_doc,
370
+ "parameters": parameters,
371
+ "return_annotation": return_annotation,
372
+ "is_async": isinstance(node, ast.AsyncFunctionDef),
373
+ "decorators": [
374
+ self._get_node_name(decorator) for decorator in node.decorator_list
375
+ ],
376
+ }
377
+
378
+ return func_info
379
+
380
+ def _extract_protocol_info(
381
+ self, node: ast.ClassDef, source_code: str
382
+ ) -> dict[str, t.Any]:
383
+ """Extract detailed information from a protocol definition."""
384
+ docstring = ast.get_docstring(node)
385
+ parsed_doc = self.docstring_parser.parse_docstring(docstring)
386
+
387
+ protocol_info: dict[str, t.Any] = {
388
+ "name": node.name,
389
+ "docstring": parsed_doc,
390
+ "methods": [],
391
+ "runtime_checkable": any(
392
+ self._get_node_name(decorator) == "runtime_checkable"
393
+ for decorator in node.decorator_list
394
+ ),
395
+ }
396
+
397
+ for item in node.body:
398
+ if isinstance(item, ast.FunctionDef):
399
+ method_info = self._extract_function_info(item, source_code)
400
+ method_info["is_abstract"] = True # Protocol methods are abstract
401
+ if isinstance(protocol_info["methods"], list):
402
+ protocol_info["methods"].append(method_info)
403
+
404
+ return protocol_info
405
+
406
+ def _extract_service_info(
407
+ self, tree: ast.AST, file_path: Path, source_code: str
408
+ ) -> dict[str, t.Any] | None:
409
+ """Extract service implementation information."""
410
+ service_info = self._create_service_info_structure(file_path)
411
+ self._populate_service_classes(service_info, tree, source_code)
412
+ return service_info if service_info["classes"] else None
413
+
414
+ def _create_service_info_structure(self, file_path: Path) -> dict[str, t.Any]:
415
+ """Create the base service information structure."""
416
+ return {
417
+ "path": str(file_path),
418
+ "classes": [],
419
+ "functions": [],
420
+ "protocols_implemented": [],
421
+ }
422
+
423
+ def _populate_service_classes(
424
+ self, service_info: dict[str, t.Any], tree: ast.AST, source_code: str
425
+ ) -> None:
426
+ """Populate service classes by walking the AST."""
427
+ for node in ast.walk(tree):
428
+ if isinstance(node, ast.ClassDef):
429
+ self._process_service_class(service_info, node, source_code)
430
+
431
+ def _process_service_class(
432
+ self, service_info: dict[str, t.Any], node: ast.ClassDef, source_code: str
433
+ ) -> None:
434
+ """Process a single service class and extract its information."""
435
+ class_info = self._extract_class_info(node, source_code)
436
+ self._extract_implemented_protocols(service_info, node)
437
+ self._add_class_to_service_info(service_info, class_info)
438
+
439
+ def _extract_implemented_protocols(
440
+ self, service_info: dict[str, t.Any], node: ast.ClassDef
441
+ ) -> None:
442
+ """Extract protocol implementations from class bases."""
443
+ for base in node.bases:
444
+ base_name = self._get_node_name(base)
445
+ if "Protocol" in base_name and isinstance(
446
+ service_info["protocols_implemented"], list[t.Any]
447
+ ):
448
+ service_info["protocols_implemented"].append(base_name)
449
+
450
+ def _add_class_to_service_info(
451
+ self, service_info: dict[str, t.Any], class_info: dict[str, t.Any]
452
+ ) -> None:
453
+ """Add class information to the service info structure."""
454
+ if isinstance(service_info["classes"], list):
455
+ service_info["classes"].append(class_info)
456
+
457
+ def _extract_cli_info(self, tree: ast.AST, source_code: str) -> dict[str, t.Any]:
458
+ """Extract CLI command and option information."""
459
+ cli_info: dict[str, t.Any] = {"options": [], "commands": [], "arguments": []}
460
+
461
+ # Look for Pydantic model fields that represent CLI options
462
+ for node in ast.walk(tree):
463
+ if isinstance(node, ast.ClassDef):
464
+ self._extract_class_cli_options(node, cli_info)
465
+
466
+ return cli_info
467
+
468
+ def _extract_class_cli_options(
469
+ self, class_node: ast.ClassDef, cli_info: dict[str, t.Any]
470
+ ) -> None:
471
+ """Extract CLI options from a class definition."""
472
+ for item in class_node.body:
473
+ if self._is_cli_field(item) and isinstance(item, ast.AnnAssign):
474
+ field_info = self._create_cli_field_info(item)
475
+ if field_info and isinstance(cli_info["options"], list):
476
+ cli_info["options"].append(field_info)
477
+
478
+ def _is_cli_field(self, item: ast.stmt) -> bool:
479
+ """Check if an AST item represents a CLI field."""
480
+ return isinstance(item, ast.AnnAssign) and item.target is not None
481
+
482
+ def _create_cli_field_info(self, item: ast.AnnAssign) -> dict[str, t.Any] | None:
483
+ """Create field information from an annotated assignment."""
484
+ field_name = getattr(item.target, "id", None)
485
+ if not field_name:
486
+ return None
487
+
488
+ return {
489
+ "name": field_name,
490
+ "type": self._get_annotation_string(item.annotation),
491
+ "description": "",
492
+ }
493
+
494
+ def _extract_mcp_python_tools(
495
+ self, tree: ast.AST, source_code: str
496
+ ) -> dict[str, t.Any] | None:
497
+ """Extract MCP tool information from Python files."""
498
+ tools = []
499
+
500
+ for node in ast.walk(tree):
501
+ if isinstance(node, ast.FunctionDef):
502
+ # Look for functions that might be MCP tools
503
+ func_info = self._extract_function_info(node, source_code)
504
+ if "mcp" in func_info["name"].lower() or any(
505
+ "tool" in dec.lower() for dec in func_info["decorators"]
506
+ ):
507
+ tools.append(func_info)
508
+
509
+ return {"tools": tools} if tools else None
510
+
511
+ def _extract_mcp_markdown_docs(self, content: str) -> dict[str, t.Any]:
512
+ """Extract MCP tool documentation from markdown files."""
513
+ # Simple extraction of sections and command examples
514
+ sections: list[dict[str, t.Any]] = []
515
+ current_section: dict[str, t.Any] | None = None
516
+
517
+ for line in content.split("\n"):
518
+ if line.startswith("#"):
519
+ if current_section:
520
+ sections.append(current_section)
521
+ current_section = {"title": line.strip("#").strip(), "content": []}
522
+ elif current_section:
523
+ current_section["content"].append(line)
524
+
525
+ if current_section:
526
+ sections.append(current_section)
527
+
528
+ return {"sections": sections}
529
+
530
+ def _is_protocol_class(self, node: ast.ClassDef) -> bool:
531
+ """Check if a class is a Protocol definition."""
532
+ return "Protocol" in [self._get_node_name(base) for base in node.bases] or any(
533
+ self._get_node_name(decorator) == "runtime_checkable"
534
+ for decorator in node.decorator_list
535
+ )
536
+
537
+ def _get_node_name(self, node: ast.AST) -> str:
538
+ """Get the name from an AST node."""
539
+ if isinstance(node, ast.Name):
540
+ return node.id
541
+ elif isinstance(node, ast.Attribute):
542
+ return f"{self._get_node_name(node.value)}.{node.attr}"
543
+ elif isinstance(node, ast.Constant):
544
+ return str(node.value)
545
+ return ""
546
+
547
+ def _get_annotation_string(self, annotation: ast.AST | None) -> str:
548
+ """Convert an annotation AST node to a string representation."""
549
+ if annotation is None:
550
+ return ""
551
+
552
+ try:
553
+ return self._process_annotation_node(annotation)
554
+ except Exception:
555
+ return "Any"
556
+
557
+ def _process_annotation_node(self, annotation: ast.AST) -> str:
558
+ """Process different types of annotation nodes."""
559
+ if isinstance(annotation, ast.Name):
560
+ return annotation.id
561
+ elif isinstance(annotation, ast.Attribute):
562
+ return self._process_attribute_annotation(annotation)
563
+ elif isinstance(annotation, ast.Subscript):
564
+ return self._process_subscript_annotation(annotation)
565
+ elif isinstance(annotation, ast.BinOp) and isinstance(annotation.op, ast.BitOr):
566
+ return self._process_union_annotation(annotation)
567
+ elif isinstance(annotation, ast.Constant):
568
+ return str(annotation.value)
569
+ elif isinstance(annotation, ast.Tuple):
570
+ return self._process_tuple_annotation(annotation)
571
+ return self._get_fallback_annotation(annotation)
572
+
573
+ def _process_attribute_annotation(self, annotation: ast.Attribute) -> str:
574
+ """Process attribute-based annotation nodes."""
575
+ return f"{self._get_node_name(annotation.value)}.{annotation.attr}"
576
+
577
+ def _process_subscript_annotation(self, annotation: ast.Subscript) -> str:
578
+ """Process subscript-based annotation nodes (e.g., List[str])."""
579
+ value = self._get_node_name(annotation.value)
580
+ slice_val = self._get_annotation_string(annotation.slice)
581
+ return f"{value}[{slice_val}]"
582
+
583
+ def _process_union_annotation(self, annotation: ast.BinOp) -> str:
584
+ """Process Union types with | operator (Python 3.10+)."""
585
+ left = self._get_annotation_string(annotation.left)
586
+ right = self._get_annotation_string(annotation.right)
587
+ return f"{left} | {right}"
588
+
589
+ def _process_tuple_annotation(self, annotation: ast.Tuple) -> str:
590
+ """Process tuple-based annotation nodes."""
591
+ elements = [self._get_annotation_string(elt) for elt in annotation.elts]
592
+ return f"({', '.join(elements)})"
593
+
594
+ def _get_fallback_annotation(self, annotation: ast.AST) -> str:
595
+ """Get fallback annotation representation."""
596
+ return ast.unparse(annotation) if hasattr(ast, "unparse") else "Any"
597
+
598
+ def _extract_import_info(
599
+ self, node: ast.Import | ast.ImportFrom
600
+ ) -> dict[str, t.Any]:
601
+ """Extract import statement information."""
602
+ if isinstance(node, ast.Import):
603
+ return {
604
+ "type": "import",
605
+ "names": [alias.name for alias in node.names],
606
+ "from": None,
607
+ }
608
+
609
+ # ast.ImportFrom
610
+ return {
611
+ "type": "from_import",
612
+ "from": node.module,
613
+ "names": [alias.name for alias in node.names] if node.names else ["*"],
614
+ "level": node.level,
615
+ }
@@ -82,7 +82,7 @@ class PackageBackupService(BaseModel):
82
82
  )
83
83
 
84
84
  try:
85
- python_files = list(validated_pkg_dir.rglob("*.py"))
85
+ python_files = list[t.Any](validated_pkg_dir.rglob("*.py"))
86
86
 
87
87
  files_to_backup = self._filter_package_files(
88
88
  python_files, validated_pkg_dir
@@ -342,7 +342,7 @@ class PackageBackupService(BaseModel):
342
342
 
343
343
  def _calculate_backup_checksum(self, file_checksums: dict[str, str]) -> str:
344
344
  sorted_items = sorted(file_checksums.items())
345
- combined = "".join(f"{path}:{checksum}" for path, checksum in sorted_items)
345
+ combined = "".join(f"{path}: {checksum}" for path, checksum in sorted_items)
346
346
  return hashlib.sha256(combined.encode(), usedforsecurity=False).hexdigest()
347
347
 
348
348
  def _validate_backup(