crackerjack 0.31.10__py3-none-any.whl → 0.31.12__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 (155) hide show
  1. crackerjack/CLAUDE.md +288 -705
  2. crackerjack/__main__.py +22 -8
  3. crackerjack/agents/__init__.py +0 -3
  4. crackerjack/agents/architect_agent.py +0 -43
  5. crackerjack/agents/base.py +1 -9
  6. crackerjack/agents/coordinator.py +2 -148
  7. crackerjack/agents/documentation_agent.py +109 -81
  8. crackerjack/agents/dry_agent.py +122 -97
  9. crackerjack/agents/formatting_agent.py +3 -16
  10. crackerjack/agents/import_optimization_agent.py +1174 -130
  11. crackerjack/agents/performance_agent.py +956 -188
  12. crackerjack/agents/performance_helpers.py +229 -0
  13. crackerjack/agents/proactive_agent.py +1 -48
  14. crackerjack/agents/refactoring_agent.py +516 -246
  15. crackerjack/agents/refactoring_helpers.py +282 -0
  16. crackerjack/agents/security_agent.py +393 -90
  17. crackerjack/agents/test_creation_agent.py +1776 -120
  18. crackerjack/agents/test_specialist_agent.py +59 -15
  19. crackerjack/agents/tracker.py +0 -102
  20. crackerjack/api.py +145 -37
  21. crackerjack/cli/handlers.py +48 -30
  22. crackerjack/cli/interactive.py +11 -11
  23. crackerjack/cli/options.py +66 -4
  24. crackerjack/code_cleaner.py +808 -148
  25. crackerjack/config/global_lock_config.py +110 -0
  26. crackerjack/config/hooks.py +43 -64
  27. crackerjack/core/async_workflow_orchestrator.py +247 -97
  28. crackerjack/core/autofix_coordinator.py +192 -109
  29. crackerjack/core/enhanced_container.py +46 -63
  30. crackerjack/core/file_lifecycle.py +549 -0
  31. crackerjack/core/performance.py +9 -8
  32. crackerjack/core/performance_monitor.py +395 -0
  33. crackerjack/core/phase_coordinator.py +281 -94
  34. crackerjack/core/proactive_workflow.py +9 -58
  35. crackerjack/core/resource_manager.py +501 -0
  36. crackerjack/core/service_watchdog.py +490 -0
  37. crackerjack/core/session_coordinator.py +4 -8
  38. crackerjack/core/timeout_manager.py +504 -0
  39. crackerjack/core/websocket_lifecycle.py +475 -0
  40. crackerjack/core/workflow_orchestrator.py +343 -209
  41. crackerjack/dynamic_config.py +47 -6
  42. crackerjack/errors.py +3 -4
  43. crackerjack/executors/async_hook_executor.py +63 -13
  44. crackerjack/executors/cached_hook_executor.py +14 -14
  45. crackerjack/executors/hook_executor.py +100 -37
  46. crackerjack/executors/hook_lock_manager.py +856 -0
  47. crackerjack/executors/individual_hook_executor.py +120 -86
  48. crackerjack/intelligence/__init__.py +0 -7
  49. crackerjack/intelligence/adaptive_learning.py +13 -86
  50. crackerjack/intelligence/agent_orchestrator.py +15 -78
  51. crackerjack/intelligence/agent_registry.py +12 -59
  52. crackerjack/intelligence/agent_selector.py +31 -92
  53. crackerjack/intelligence/integration.py +1 -41
  54. crackerjack/interactive.py +9 -9
  55. crackerjack/managers/async_hook_manager.py +25 -8
  56. crackerjack/managers/hook_manager.py +9 -9
  57. crackerjack/managers/publish_manager.py +57 -59
  58. crackerjack/managers/test_command_builder.py +6 -36
  59. crackerjack/managers/test_executor.py +9 -61
  60. crackerjack/managers/test_manager.py +17 -63
  61. crackerjack/managers/test_manager_backup.py +77 -127
  62. crackerjack/managers/test_progress.py +4 -23
  63. crackerjack/mcp/cache.py +5 -12
  64. crackerjack/mcp/client_runner.py +10 -10
  65. crackerjack/mcp/context.py +64 -6
  66. crackerjack/mcp/dashboard.py +14 -11
  67. crackerjack/mcp/enhanced_progress_monitor.py +55 -55
  68. crackerjack/mcp/file_monitor.py +72 -42
  69. crackerjack/mcp/progress_components.py +103 -84
  70. crackerjack/mcp/progress_monitor.py +122 -49
  71. crackerjack/mcp/rate_limiter.py +12 -12
  72. crackerjack/mcp/server_core.py +16 -22
  73. crackerjack/mcp/service_watchdog.py +26 -26
  74. crackerjack/mcp/state.py +15 -0
  75. crackerjack/mcp/tools/core_tools.py +95 -39
  76. crackerjack/mcp/tools/error_analyzer.py +6 -32
  77. crackerjack/mcp/tools/execution_tools.py +1 -56
  78. crackerjack/mcp/tools/execution_tools_backup.py +35 -131
  79. crackerjack/mcp/tools/intelligence_tool_registry.py +0 -36
  80. crackerjack/mcp/tools/intelligence_tools.py +2 -55
  81. crackerjack/mcp/tools/monitoring_tools.py +308 -145
  82. crackerjack/mcp/tools/proactive_tools.py +12 -42
  83. crackerjack/mcp/tools/progress_tools.py +23 -15
  84. crackerjack/mcp/tools/utility_tools.py +3 -40
  85. crackerjack/mcp/tools/workflow_executor.py +40 -60
  86. crackerjack/mcp/websocket/app.py +0 -3
  87. crackerjack/mcp/websocket/endpoints.py +206 -268
  88. crackerjack/mcp/websocket/jobs.py +213 -66
  89. crackerjack/mcp/websocket/server.py +84 -6
  90. crackerjack/mcp/websocket/websocket_handler.py +137 -29
  91. crackerjack/models/config_adapter.py +3 -16
  92. crackerjack/models/protocols.py +162 -3
  93. crackerjack/models/resource_protocols.py +454 -0
  94. crackerjack/models/task.py +3 -3
  95. crackerjack/monitoring/__init__.py +0 -0
  96. crackerjack/monitoring/ai_agent_watchdog.py +25 -71
  97. crackerjack/monitoring/regression_prevention.py +28 -87
  98. crackerjack/orchestration/advanced_orchestrator.py +44 -78
  99. crackerjack/orchestration/coverage_improvement.py +10 -60
  100. crackerjack/orchestration/execution_strategies.py +16 -16
  101. crackerjack/orchestration/test_progress_streamer.py +61 -53
  102. crackerjack/plugins/base.py +1 -1
  103. crackerjack/plugins/managers.py +22 -20
  104. crackerjack/py313.py +65 -21
  105. crackerjack/services/backup_service.py +467 -0
  106. crackerjack/services/bounded_status_operations.py +627 -0
  107. crackerjack/services/cache.py +7 -9
  108. crackerjack/services/config.py +35 -52
  109. crackerjack/services/config_integrity.py +5 -16
  110. crackerjack/services/config_merge.py +542 -0
  111. crackerjack/services/contextual_ai_assistant.py +17 -19
  112. crackerjack/services/coverage_ratchet.py +44 -73
  113. crackerjack/services/debug.py +25 -39
  114. crackerjack/services/dependency_monitor.py +52 -50
  115. crackerjack/services/enhanced_filesystem.py +14 -11
  116. crackerjack/services/file_hasher.py +1 -1
  117. crackerjack/services/filesystem.py +1 -12
  118. crackerjack/services/git.py +71 -47
  119. crackerjack/services/health_metrics.py +31 -27
  120. crackerjack/services/initialization.py +276 -428
  121. crackerjack/services/input_validator.py +760 -0
  122. crackerjack/services/log_manager.py +16 -16
  123. crackerjack/services/logging.py +7 -6
  124. crackerjack/services/metrics.py +43 -43
  125. crackerjack/services/pattern_cache.py +2 -31
  126. crackerjack/services/pattern_detector.py +26 -63
  127. crackerjack/services/performance_benchmarks.py +20 -45
  128. crackerjack/services/regex_patterns.py +2887 -0
  129. crackerjack/services/regex_utils.py +537 -0
  130. crackerjack/services/secure_path_utils.py +683 -0
  131. crackerjack/services/secure_status_formatter.py +534 -0
  132. crackerjack/services/secure_subprocess.py +605 -0
  133. crackerjack/services/security.py +47 -10
  134. crackerjack/services/security_logger.py +492 -0
  135. crackerjack/services/server_manager.py +109 -50
  136. crackerjack/services/smart_scheduling.py +8 -25
  137. crackerjack/services/status_authentication.py +603 -0
  138. crackerjack/services/status_security_manager.py +442 -0
  139. crackerjack/services/thread_safe_status_collector.py +546 -0
  140. crackerjack/services/tool_version_service.py +1 -23
  141. crackerjack/services/unified_config.py +36 -58
  142. crackerjack/services/validation_rate_limiter.py +269 -0
  143. crackerjack/services/version_checker.py +9 -40
  144. crackerjack/services/websocket_resource_limiter.py +572 -0
  145. crackerjack/slash_commands/__init__.py +52 -2
  146. crackerjack/tools/__init__.py +0 -0
  147. crackerjack/tools/validate_input_validator_patterns.py +262 -0
  148. crackerjack/tools/validate_regex_patterns.py +198 -0
  149. {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/METADATA +197 -12
  150. crackerjack-0.31.12.dist-info/RECORD +178 -0
  151. crackerjack/cli/facade.py +0 -104
  152. crackerjack-0.31.10.dist-info/RECORD +0 -149
  153. {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/WHEEL +0 -0
  154. {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/entry_points.txt +0 -0
  155. {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/licenses/LICENSE +0 -0
@@ -1,11 +1,9 @@
1
- import json
2
1
  import os
3
2
  import typing as t
4
3
  from contextlib import suppress
5
4
  from pathlib import Path
6
5
  from typing import Any
7
6
 
8
- import yaml
9
7
  from pydantic import BaseModel, Field, field_validator
10
8
  from rich.console import Console
11
9
 
@@ -27,7 +25,7 @@ class CrackerjackConfig(BaseModel):
27
25
 
28
26
  test_timeout: int = 300
29
27
  test_workers: int = Field(default_factory=lambda: os.cpu_count() or 1)
30
- min_coverage: float = 10.11 # Baseline from coverage ratchet system
28
+ min_coverage: float = 10.0
31
29
 
32
30
  log_level: str = "INFO"
33
31
  log_json: bool = False
@@ -38,11 +36,7 @@ class CrackerjackConfig(BaseModel):
38
36
  skip_hooks: bool = False
39
37
  experimental_hooks: bool = False
40
38
 
41
- performance_tracking: bool = True
42
- benchmark_mode: bool = False
43
-
44
- publish_enabled: bool = False
45
- keyring_provider: str = "subprocess"
39
+ # Removed unused configuration fields: performance_tracking, benchmark_mode, publish_enabled, keyring_provider
46
40
 
47
41
  batch_file_operations: bool = True
48
42
  file_operation_batch_size: int = 10
@@ -95,7 +89,7 @@ class ConfigSource:
95
89
  self.priority = priority
96
90
  self.logger = get_logger("crackerjack.config.source")
97
91
 
98
- def load(self) -> dict[str, Any]:
92
+ def load(self) -> dict[str, t.Any]:
99
93
  raise NotImplementedError
100
94
 
101
95
  def is_available(self) -> bool:
@@ -108,8 +102,8 @@ class EnvironmentConfigSource(ConfigSource):
108
102
  def __init__(self, priority: int = 100) -> None:
109
103
  super().__init__(priority)
110
104
 
111
- def load(self) -> dict[str, Any]:
112
- config: dict[str, Any] = {}
105
+ def load(self) -> dict[str, t.Any]:
106
+ config: dict[str, t.Any] = {}
113
107
 
114
108
  for key, value in os.environ.items():
115
109
  if key.startswith(self.ENV_PREFIX):
@@ -117,71 +111,58 @@ class EnvironmentConfigSource(ConfigSource):
117
111
 
118
112
  config[config_key] = self._convert_value(value)
119
113
 
120
- self.logger.debug("Loaded environment config", keys=list(config.keys()))
121
114
  return config
122
115
 
123
- def _convert_value(self, value: str) -> Any:
116
+ def _convert_value(self, value: str) -> t.Any:
124
117
  if value.lower() in ("true", "1", "yes", "on"):
125
118
  return True
126
119
  if value.lower() in ("false", "0", "no", "off"):
127
120
  return False
128
121
 
122
+ # Handle negative numbers with spaces (e.g., "- 10")
123
+ cleaned_value = value.replace(" ", "")
124
+
129
125
  with suppress(ValueError):
130
- return int(value)
126
+ return int(cleaned_value)
131
127
 
132
128
  with suppress(ValueError):
133
- return float(value)
129
+ return float(cleaned_value)
134
130
 
135
131
  return value
136
132
 
137
133
 
138
134
  class FileConfigSource(ConfigSource):
139
- def __init__(self, file_path: Path, priority: int = 50) -> None:
135
+ def __init__(self, config_path: Path, priority: int = 50) -> None:
140
136
  super().__init__(priority)
141
- self.file_path = file_path
137
+ self.config_path = config_path
142
138
 
143
139
  def is_available(self) -> bool:
144
- return self.file_path.exists() and self.file_path.is_file()
140
+ return self.config_path.exists() and self.config_path.is_file()
145
141
 
146
- def load(self) -> dict[str, Any]:
142
+ def load(self) -> dict[str, t.Any]:
147
143
  if not self.is_available():
148
144
  return {}
149
145
 
150
146
  try:
151
- content = self.file_path.read_text()
152
-
153
- if self.file_path.suffix.lower() in (".yml", ".yaml"):
154
- yaml_result: t.Any = yaml.safe_load(content)
155
- config = (
156
- t.cast("dict[str, Any]", yaml_result)
157
- if yaml_result is not None
158
- else {}
159
- )
160
- elif self.file_path.suffix.lower() == ".json":
161
- json_result = json.loads(content)
162
- config = (
163
- t.cast("dict[str, Any]", json_result)
164
- if json_result is not None
165
- else {}
166
- )
167
- else:
168
- self.logger.warning(
169
- "Unknown config file format",
170
- path=str(self.file_path),
171
- )
172
- return {}
173
-
174
- self.logger.debug(
175
- "Loaded file config",
176
- path=str(self.file_path),
177
- keys=list(config.keys()),
178
- )
179
- return config
180
-
147
+ with self.config_path.open("r") as f:
148
+ if self.config_path.suffix in (".yaml", ".yml"):
149
+ import yaml
150
+
151
+ result = yaml.safe_load(f)
152
+ return result if isinstance(result, dict) else {}
153
+ elif self.config_path.suffix == ".json":
154
+ import json
155
+
156
+ result = json.load(f)
157
+ return result if isinstance(result, dict) else {}
158
+ else:
159
+ self.logger.warning(
160
+ f"Unsupported config file format: {self.config_path.suffix}"
161
+ )
162
+ return {}
181
163
  except Exception as e:
182
164
  self.logger.exception(
183
- "Failed to load config file",
184
- path=str(self.file_path),
165
+ f"Failed to load config from file: {self.config_path}",
185
166
  error=str(e),
186
167
  )
187
168
  return {}
@@ -195,7 +176,7 @@ class PyprojectConfigSource(ConfigSource):
195
176
  def is_available(self) -> bool:
196
177
  return self.pyproject_path.exists() and self.pyproject_path.is_file()
197
178
 
198
- def load(self) -> dict[str, Any]:
179
+ def load(self) -> dict[str, t.Any]:
199
180
  if not self.is_available():
200
181
  return {}
201
182
 
@@ -234,8 +215,8 @@ class OptionsConfigSource(ConfigSource):
234
215
  super().__init__(priority)
235
216
  self.options = options
236
217
 
237
- def load(self) -> dict[str, Any]:
238
- config: dict[str, Any] = {}
218
+ def load(self) -> dict[str, t.Any]:
219
+ config: dict[str, t.Any] = {}
239
220
 
240
221
  option_mappings = {
241
222
  "testing": "test_mode",
@@ -280,9 +261,6 @@ class UnifiedConfigurationService:
280
261
  ),
281
262
  )
282
263
 
283
- # .crackerjack.* config files are no longer supported
284
- # Configuration should be done through pyproject.toml
285
-
286
264
  self.sources.append(EnvironmentConfigSource())
287
265
 
288
266
  if options:
@@ -294,7 +272,7 @@ class UnifiedConfigurationService:
294
272
  pkg_path = self.pkg_path
295
273
 
296
274
  class DefaultConfigSource(ConfigSource):
297
- def load(self) -> dict[str, Any]:
275
+ def load(self) -> dict[str, t.Any]:
298
276
  return {
299
277
  "package_path": pkg_path,
300
278
  "cache_enabled": True,
@@ -0,0 +1,269 @@
1
+ """
2
+ Rate limiting for input validation to prevent abuse and DoS attacks.
3
+
4
+ This module provides rate limiting functionality to:
5
+ - Prevent excessive validation failures
6
+ - Protect against DoS attacks via malformed input
7
+ - Track validation patterns for security monitoring
8
+ """
9
+
10
+ import time
11
+ import typing as t
12
+ from collections import defaultdict, deque
13
+ from threading import Lock
14
+
15
+ from .security_logger import SecurityEventLevel, get_security_logger
16
+
17
+
18
+ class ValidationRateLimit:
19
+ """Rate limiting configuration for different validation types."""
20
+
21
+ def __init__(
22
+ self,
23
+ max_failures: int = 10,
24
+ window_seconds: int = 60,
25
+ block_duration: int = 300, # 5 minutes
26
+ ):
27
+ self.max_failures = max_failures
28
+ self.window_seconds = window_seconds
29
+ self.block_duration = block_duration
30
+
31
+
32
+ class ValidationRateLimiter:
33
+ """
34
+ Rate limiter for validation failures to prevent abuse.
35
+
36
+ Tracks validation failures by client/source and blocks excessive failures.
37
+ Uses sliding window approach for accurate rate limiting.
38
+ """
39
+
40
+ def __init__(self):
41
+ self._failure_windows: dict[str, deque[float]] = defaultdict(deque)
42
+ self._blocked_until: dict[str, float] = {}
43
+ self._lock = Lock()
44
+ self._logger = get_security_logger()
45
+
46
+ # Default rate limits by validation type
47
+ self._limits = {
48
+ "default": ValidationRateLimit(),
49
+ "command_injection": ValidationRateLimit(
50
+ max_failures=3, block_duration=600
51
+ ),
52
+ "path_traversal": ValidationRateLimit(max_failures=3, block_duration=600),
53
+ "sql_injection": ValidationRateLimit(max_failures=2, block_duration=900),
54
+ "code_injection": ValidationRateLimit(max_failures=2, block_duration=900),
55
+ "json_payload": ValidationRateLimit(max_failures=20),
56
+ "job_id": ValidationRateLimit(max_failures=15),
57
+ "project_name": ValidationRateLimit(max_failures=15),
58
+ }
59
+
60
+ def is_blocked(self, client_id: str) -> bool:
61
+ """Check if a client is currently blocked."""
62
+ with self._lock:
63
+ if client_id in self._blocked_until:
64
+ if time.time() < self._blocked_until[client_id]:
65
+ return True
66
+ else:
67
+ # Block expired, remove it
68
+ del self._blocked_until[client_id]
69
+ return False
70
+
71
+ def record_failure(
72
+ self,
73
+ client_id: str,
74
+ validation_type: str,
75
+ severity: SecurityEventLevel = SecurityEventLevel.MEDIUM,
76
+ ) -> bool:
77
+ """
78
+ Record a validation failure and check if client should be blocked.
79
+
80
+ Returns True if the client should be blocked, False otherwise.
81
+ """
82
+ with self._lock:
83
+ current_time = time.time()
84
+
85
+ # Get rate limit for this validation type
86
+ limit = self._limits.get(validation_type, self._limits["default"])
87
+
88
+ # Initialize failure window if needed
89
+ if client_id not in self._failure_windows:
90
+ self._failure_windows[client_id] = deque()
91
+
92
+ # Clean old failures outside the window
93
+ window = self._failure_windows[client_id]
94
+ while window and current_time - window[0] > limit.window_seconds:
95
+ window.popleft()
96
+
97
+ # Record this failure
98
+ window.append(current_time)
99
+
100
+ # Check if limit exceeded
101
+ if len(window) >= limit.max_failures:
102
+ # Block this client
103
+ self._blocked_until[client_id] = current_time + limit.block_duration
104
+
105
+ # Log the rate limit exceeded event
106
+ self._logger.log_rate_limit_exceeded(
107
+ limit_type=validation_type,
108
+ current_count=len(window),
109
+ max_allowed=limit.max_failures,
110
+ client_id=client_id,
111
+ block_duration=limit.block_duration,
112
+ )
113
+
114
+ # Clear the failure window since client is now blocked
115
+ self._failure_windows[client_id].clear()
116
+
117
+ return True
118
+
119
+ return False
120
+
121
+ def get_remaining_attempts(self, client_id: str, validation_type: str) -> int:
122
+ """Get remaining validation attempts before rate limit."""
123
+ with self._lock:
124
+ if self.is_blocked(client_id):
125
+ return 0
126
+
127
+ limit = self._limits.get(validation_type, self._limits["default"])
128
+ current_failures = len(self._failure_windows.get(client_id, []))
129
+ return max(0, limit.max_failures - current_failures)
130
+
131
+ def get_block_time_remaining(self, client_id: str) -> int:
132
+ """Get seconds remaining until client is unblocked."""
133
+ with self._lock:
134
+ if client_id in self._blocked_until:
135
+ remaining = max(0, int(self._blocked_until[client_id] - time.time()))
136
+ return remaining
137
+ return 0
138
+
139
+ def get_client_stats(self, client_id: str) -> dict[str, t.Any]:
140
+ """Get rate limiting statistics for a client."""
141
+ with self._lock:
142
+ current_time = time.time()
143
+
144
+ stats = {
145
+ "client_id": client_id,
146
+ "is_blocked": self.is_blocked(client_id),
147
+ "block_time_remaining": self.get_block_time_remaining(client_id),
148
+ "recent_failures": 0,
149
+ "total_failures": 0,
150
+ }
151
+
152
+ if client_id in self._failure_windows:
153
+ window = self._failure_windows[client_id]
154
+ stats["total_failures"] = len(window)
155
+
156
+ # Count recent failures (last 5 minutes)
157
+ recent_count = sum(
158
+ 1 for failure_time in window if current_time - failure_time <= 300
159
+ )
160
+ stats["recent_failures"] = recent_count
161
+
162
+ return stats
163
+
164
+ def cleanup_expired_data(self) -> int:
165
+ """Clean up expired data and return number of items removed."""
166
+ with self._lock:
167
+ current_time = time.time()
168
+ removed_count = 0
169
+
170
+ # Clean expired blocks
171
+ expired_blocks = [
172
+ client_id
173
+ for client_id, block_until in self._blocked_until.items()
174
+ if current_time >= block_until
175
+ ]
176
+
177
+ for client_id in expired_blocks:
178
+ del self._blocked_until[client_id]
179
+ removed_count += 1
180
+
181
+ # Clean old failure windows (keep only last 24 hours)
182
+ for client_id, window in list(self._failure_windows.items()):
183
+ # Remove failures older than 24 hours
184
+ while window and current_time - window[0] > 86400: # 24 hours
185
+ window.popleft()
186
+ removed_count += 1
187
+
188
+ # Remove empty windows
189
+ if not window:
190
+ del self._failure_windows[client_id]
191
+
192
+ return removed_count
193
+
194
+ def update_rate_limits(
195
+ self,
196
+ validation_type: str,
197
+ max_failures: int,
198
+ window_seconds: int,
199
+ block_duration: int,
200
+ ) -> None:
201
+ """Update rate limits for a specific validation type."""
202
+ with self._lock:
203
+ self._limits[validation_type] = ValidationRateLimit(
204
+ max_failures=max_failures,
205
+ window_seconds=window_seconds,
206
+ block_duration=block_duration,
207
+ )
208
+
209
+ def get_all_stats(self) -> dict[str, t.Any]:
210
+ """Get comprehensive rate limiting statistics."""
211
+ with self._lock:
212
+ current_time = time.time()
213
+
214
+ stats: dict[str, t.Any] = {
215
+ "total_clients_tracked": len(self._failure_windows),
216
+ "currently_blocked": len(self._blocked_until),
217
+ "rate_limits": {
218
+ vtype: {
219
+ "max_failures": limit.max_failures,
220
+ "window_seconds": limit.window_seconds,
221
+ "block_duration": limit.block_duration,
222
+ }
223
+ for vtype, limit in self._limits.items()
224
+ },
225
+ "blocked_clients": [],
226
+ "active_clients": [],
227
+ }
228
+
229
+ # Get blocked client info
230
+ for client_id, block_until in self._blocked_until.items():
231
+ remaining = max(0, int(block_until - current_time))
232
+ stats["blocked_clients"].append(
233
+ {
234
+ "client_id": client_id,
235
+ "blocked_until": block_until,
236
+ "time_remaining": remaining,
237
+ }
238
+ )
239
+
240
+ # Get active client info (with recent failures)
241
+ for client_id, window in self._failure_windows.items():
242
+ if client_id not in self._blocked_until and window:
243
+ recent_failures = sum(
244
+ 1
245
+ for failure_time in window
246
+ if current_time - failure_time <= 300 # Last 5 minutes
247
+ )
248
+ if recent_failures > 0:
249
+ stats["active_clients"].append(
250
+ {
251
+ "client_id": client_id,
252
+ "recent_failures": recent_failures,
253
+ "total_failures": len(window),
254
+ }
255
+ )
256
+
257
+ return stats
258
+
259
+
260
+ # Global rate limiter instance
261
+ _rate_limiter: ValidationRateLimiter | None = None
262
+
263
+
264
+ def get_validation_rate_limiter() -> ValidationRateLimiter:
265
+ """Get the global validation rate limiter instance."""
266
+ global _rate_limiter
267
+ if _rate_limiter is None:
268
+ _rate_limiter = ValidationRateLimiter()
269
+ return _rate_limiter
@@ -1,9 +1,3 @@
1
- """Core version checking and comparison functionality.
2
-
3
- This module handles tool version detection, comparison, and update notifications.
4
- Split from tool_version_service.py to follow single responsibility principle.
5
- """
6
-
7
1
  import subprocess
8
2
  import typing as t
9
3
  from dataclasses import dataclass
@@ -14,8 +8,6 @@ from rich.console import Console
14
8
 
15
9
  @dataclass
16
10
  class VersionInfo:
17
- """Information about a tool's version and update status."""
18
-
19
11
  tool_name: str
20
12
  current_version: str
21
13
  latest_version: str | None = None
@@ -24,8 +16,6 @@ class VersionInfo:
24
16
 
25
17
 
26
18
  class VersionChecker:
27
- """Service for checking tool versions and updates."""
28
-
29
19
  def __init__(self, console: Console) -> None:
30
20
  self.console = console
31
21
  self.tools_to_check = {
@@ -36,7 +26,6 @@ class VersionChecker:
36
26
  }
37
27
 
38
28
  async def check_tool_updates(self) -> dict[str, VersionInfo]:
39
- """Check updates for all registered tools."""
40
29
  results = {}
41
30
  for tool_name, version_getter in self.tools_to_check.items():
42
31
  results[tool_name] = await self._check_single_tool(
@@ -47,7 +36,6 @@ class VersionChecker:
47
36
  async def _check_single_tool(
48
37
  self, tool_name: str, version_getter: t.Callable[[], str | None]
49
38
  ) -> VersionInfo:
50
- """Check updates for a single tool."""
51
39
  try:
52
40
  current_version = version_getter()
53
41
  if current_version:
@@ -63,7 +51,6 @@ class VersionChecker:
63
51
  def _create_installed_version_info(
64
52
  self, tool_name: str, current_version: str, latest_version: str | None
65
53
  ) -> VersionInfo:
66
- """Create version info for installed tool."""
67
54
  update_available = (
68
55
  latest_version is not None
69
56
  and self._version_compare(current_version, latest_version) < 0
@@ -72,7 +59,7 @@ class VersionChecker:
72
59
  if update_available:
73
60
  self.console.print(
74
61
  f"[yellow]🔄 {tool_name} update available: "
75
- f"{current_version} → {latest_version}[/yellow]"
62
+ f"{current_version} → {latest_version}[/ yellow]"
76
63
  )
77
64
 
78
65
  return VersionInfo(
@@ -83,8 +70,7 @@ class VersionChecker:
83
70
  )
84
71
 
85
72
  def _create_missing_tool_info(self, tool_name: str) -> VersionInfo:
86
- """Create version info for missing tool."""
87
- self.console.print(f"[red]⚠️ {tool_name} not installed[/red]")
73
+ self.console.print(f"[red]⚠️ {tool_name} not installed[/ red]")
88
74
  return VersionInfo(
89
75
  tool_name=tool_name,
90
76
  current_version="not installed",
@@ -94,8 +80,7 @@ class VersionChecker:
94
80
  def _create_error_version_info(
95
81
  self, tool_name: str, error: Exception
96
82
  ) -> VersionInfo:
97
- """Create version info for tool with error."""
98
- self.console.print(f"[red]❌ Error checking {tool_name}: {error}[/red]")
83
+ self.console.print(f"[red]❌ Error checking {tool_name}: {error}[/ red]")
99
84
  return VersionInfo(
100
85
  tool_name=tool_name,
101
86
  current_version="unknown",
@@ -103,23 +88,18 @@ class VersionChecker:
103
88
  )
104
89
 
105
90
  def _get_ruff_version(self) -> str | None:
106
- """Get currently installed Ruff version."""
107
91
  return self._get_tool_version("ruff")
108
92
 
109
93
  def _get_pyright_version(self) -> str | None:
110
- """Get currently installed Pyright version."""
111
94
  return self._get_tool_version("pyright")
112
95
 
113
96
  def _get_precommit_version(self) -> str | None:
114
- """Get currently installed pre-commit version."""
115
97
  return self._get_tool_version("pre-commit")
116
98
 
117
99
  def _get_uv_version(self) -> str | None:
118
- """Get currently installed UV version."""
119
100
  return self._get_tool_version("uv")
120
101
 
121
102
  def _get_tool_version(self, tool_name: str) -> str | None:
122
- """Generic method to get tool version via subprocess."""
123
103
  try:
124
104
  result = subprocess.run(
125
105
  [tool_name, "--version"],
@@ -136,13 +116,12 @@ class VersionChecker:
136
116
  return None
137
117
 
138
118
  async def _fetch_latest_version(self, tool_name: str) -> str | None:
139
- """Fetch latest version from PyPI."""
140
119
  try:
141
120
  pypi_urls = {
142
- "ruff": "https://pypi.org/pypi/ruff/json",
143
- "pyright": "https://pypi.org/pypi/pyright/json",
144
- "pre-commit": "https://pypi.org/pypi/pre-commit/json",
145
- "uv": "https://pypi.org/pypi/uv/json",
121
+ "ruff": "https: / / pypi.org / pypi / ruff / json",
122
+ "pyright": "https: / / pypi.org / pypi / pyright / json",
123
+ "pre-commit": "https: / / pypi.org / pypi / pre-commit / json",
124
+ "uv": "https: / / pypi.org / pypi / uv / json",
146
125
  }
147
126
 
148
127
  url = pypi_urls.get(tool_name)
@@ -160,24 +139,20 @@ class VersionChecker:
160
139
  return None
161
140
 
162
141
  def _version_compare(self, current: str, latest: str) -> int:
163
- """Compare two version strings. Returns -1 if current < latest, 0 if equal, 1 if current > latest."""
164
142
  try:
165
143
  current_parts, current_len = self._parse_version_parts(current)
166
144
  latest_parts, latest_len = self._parse_version_parts(latest)
167
145
 
168
- # Normalize lengths
169
146
  normalized_current, normalized_latest = self._normalize_version_parts(
170
147
  current_parts, latest_parts
171
148
  )
172
149
 
173
- # Compare numeric values
174
150
  numeric_result = self._compare_numeric_parts(
175
151
  normalized_current, normalized_latest
176
152
  )
177
153
  if numeric_result != 0:
178
154
  return numeric_result
179
155
 
180
- # Handle length differences when numeric values are equal
181
156
  return self._handle_length_differences(
182
157
  current_len, latest_len, normalized_current, normalized_latest
183
158
  )
@@ -186,14 +161,12 @@ class VersionChecker:
186
161
  return 0
187
162
 
188
163
  def _parse_version_parts(self, version: str) -> tuple[list[int], int]:
189
- """Parse version string into integer parts and return original length."""
190
164
  parts = [int(x) for x in version.split(".")]
191
165
  return parts, len(parts)
192
166
 
193
167
  def _normalize_version_parts(
194
168
  self, current_parts: list[int], latest_parts: list[int]
195
169
  ) -> tuple[list[int], list[int]]:
196
- """Extend version parts to same length with zeros."""
197
170
  max_len = max(len(current_parts), len(latest_parts))
198
171
  current_normalized = current_parts + [0] * (max_len - len(current_parts))
199
172
  latest_normalized = latest_parts + [0] * (max_len - len(latest_parts))
@@ -202,7 +175,6 @@ class VersionChecker:
202
175
  def _compare_numeric_parts(
203
176
  self, current_parts: list[int], latest_parts: list[int]
204
177
  ) -> int:
205
- """Compare version parts numerically."""
206
178
  for current_part, latest_part in zip(current_parts, latest_parts):
207
179
  if current_part < latest_part:
208
180
  return -1
@@ -217,7 +189,6 @@ class VersionChecker:
217
189
  current_parts: list[int],
218
190
  latest_parts: list[int],
219
191
  ) -> int:
220
- """Handle version comparison when lengths differ but numeric values are equal."""
221
192
  if current_len == latest_len:
222
193
  return 0
223
194
 
@@ -230,19 +201,17 @@ class VersionChecker:
230
201
  def _compare_when_current_shorter(
231
202
  self, current_len: int, latest_len: int, latest_parts: list[int]
232
203
  ) -> int:
233
- """Compare when current version has fewer parts than latest."""
234
204
  extra_parts = latest_parts[current_len:]
235
205
  if any(part != 0 for part in extra_parts):
236
206
  return -1
237
- # "1.0" vs "1.0.0" should return -1, but "1" vs "1.0" should return 0
207
+
238
208
  return -1 if current_len > 1 else 0
239
209
 
240
210
  def _compare_when_latest_shorter(
241
211
  self, latest_len: int, current_len: int, current_parts: list[int]
242
212
  ) -> int:
243
- """Compare when latest version has fewer parts than current."""
244
213
  extra_parts = current_parts[latest_len:]
245
214
  if any(part != 0 for part in extra_parts):
246
215
  return 1
247
- # "1.0.0" vs "1.0" should return 1, but "1.0" vs "1" should return 0
216
+
248
217
  return 1 if latest_len > 1 else 0