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
@@ -0,0 +1,467 @@
1
+ import hashlib
2
+ import shutil
3
+ import time
4
+ import typing as t
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+
8
+ from pydantic import BaseModel, ConfigDict
9
+
10
+ from ..errors import ErrorCode, ExecutionError
11
+ from .secure_path_utils import AtomicFileOperations, SecurePathValidator
12
+ from .security_logger import SecurityEventLevel, SecurityEventType, get_security_logger
13
+
14
+
15
+ class BackupMetadata(BaseModel):
16
+ model_config = ConfigDict(arbitrary_types_allowed=True)
17
+
18
+ backup_id: str
19
+ timestamp: datetime
20
+ package_directory: Path
21
+ backup_directory: Path
22
+ total_files: int
23
+ total_size: int
24
+ checksum: str
25
+ file_checksums: dict[str, str]
26
+
27
+
28
+ class BackupValidationResult(BaseModel):
29
+ is_valid: bool
30
+ missing_files: list[Path]
31
+ corrupted_files: list[Path]
32
+ total_validated: int
33
+ validation_errors: list[str]
34
+
35
+
36
+ class PackageBackupService(BaseModel):
37
+ model_config = ConfigDict(arbitrary_types_allowed=True)
38
+
39
+ logger: t.Any = None
40
+ security_logger: t.Any = None
41
+ backup_root: Path | None = None
42
+
43
+ def model_post_init(self, _: t.Any) -> None:
44
+ if self.logger is None:
45
+ import logging
46
+
47
+ self.logger = logging.getLogger("crackerjack.backup_service")
48
+
49
+ if self.security_logger is None:
50
+ self.security_logger = get_security_logger()
51
+
52
+ if self.backup_root is None:
53
+ import tempfile
54
+
55
+ self.backup_root = Path(tempfile.gettempdir()) / "crackerjack_backups"
56
+
57
+ def create_package_backup(
58
+ self,
59
+ package_directory: Path,
60
+ base_directory: Path | None = None,
61
+ ) -> BackupMetadata:
62
+ validated_pkg_dir = SecurePathValidator.validate_file_path(
63
+ package_directory, base_directory
64
+ )
65
+
66
+ if not validated_pkg_dir.is_dir():
67
+ raise ExecutionError(
68
+ message=f"Package directory does not exist: {validated_pkg_dir}",
69
+ error_code=ErrorCode.VALIDATION_ERROR,
70
+ )
71
+
72
+ backup_id = self._generate_backup_id()
73
+ backup_dir = self._create_backup_directory(backup_id)
74
+
75
+ self.logger.info(f"Creating package backup: {backup_id}")
76
+ self.security_logger.log_security_event(
77
+ SecurityEventType.BACKUP_CREATED,
78
+ SecurityEventLevel.MEDIUM,
79
+ f"Starting package backup: {backup_id}",
80
+ file_path=validated_pkg_dir,
81
+ backup_id=backup_id,
82
+ )
83
+
84
+ try:
85
+ python_files = list(validated_pkg_dir.rglob("*.py"))
86
+
87
+ files_to_backup = self._filter_package_files(
88
+ python_files, validated_pkg_dir
89
+ )
90
+
91
+ if not files_to_backup:
92
+ raise ExecutionError(
93
+ message="No package files found to backup",
94
+ error_code=ErrorCode.VALIDATION_ERROR,
95
+ )
96
+
97
+ backup_metadata = self._perform_backup(
98
+ files_to_backup,
99
+ validated_pkg_dir,
100
+ backup_dir,
101
+ backup_id,
102
+ )
103
+
104
+ validation_result = self._validate_backup(backup_metadata)
105
+
106
+ if not validation_result.is_valid:
107
+ self._cleanup_backup_directory(backup_dir)
108
+ raise ExecutionError(
109
+ message=f"Backup validation failed: {validation_result.validation_errors}",
110
+ error_code=ErrorCode.FILE_WRITE_ERROR,
111
+ )
112
+
113
+ self.logger.info(
114
+ f"Package backup completed successfully: {backup_id} "
115
+ f"({backup_metadata.total_files} files, {backup_metadata.total_size} bytes)"
116
+ )
117
+
118
+ self.security_logger.log_backup_created(
119
+ validated_pkg_dir,
120
+ backup_dir,
121
+ backup_id=backup_id,
122
+ file_count=backup_metadata.total_files,
123
+ )
124
+
125
+ return backup_metadata
126
+
127
+ except Exception as e:
128
+ self._cleanup_backup_directory(backup_dir)
129
+
130
+ if isinstance(e, ExecutionError):
131
+ raise
132
+
133
+ raise ExecutionError(
134
+ message=f"Failed to create package backup: {e}",
135
+ error_code=ErrorCode.FILE_WRITE_ERROR,
136
+ ) from e
137
+
138
+ def restore_from_backup(
139
+ self,
140
+ backup_metadata: BackupMetadata,
141
+ base_directory: Path | None = None,
142
+ ) -> None:
143
+ backup_dir = backup_metadata.backup_directory
144
+
145
+ if not backup_dir.exists():
146
+ raise ExecutionError(
147
+ message=f"Backup directory not found: {backup_dir}",
148
+ error_code=ErrorCode.FILE_READ_ERROR,
149
+ )
150
+
151
+ self.logger.info(f"Restoring from backup: {backup_metadata.backup_id}")
152
+ self.security_logger.log_security_event(
153
+ SecurityEventType.BACKUP_RESTORED,
154
+ SecurityEventLevel.HIGH,
155
+ f"Starting backup restoration: {backup_metadata.backup_id}",
156
+ file_path=backup_dir,
157
+ backup_id=backup_metadata.backup_id,
158
+ )
159
+
160
+ validation_result = self._validate_backup(backup_metadata)
161
+ if not validation_result.is_valid:
162
+ raise ExecutionError(
163
+ message=f"Cannot restore corrupted backup: {validation_result.validation_errors}",
164
+ error_code=ErrorCode.FILE_READ_ERROR,
165
+ )
166
+
167
+ temp_restore_dir = None
168
+ try:
169
+ temp_restore_dir = self._create_temp_restore_directory(
170
+ backup_metadata.backup_id
171
+ )
172
+
173
+ self._stage_backup_files(backup_metadata, temp_restore_dir)
174
+
175
+ self._commit_restoration(backup_metadata, temp_restore_dir, base_directory)
176
+
177
+ self.logger.info(
178
+ f"Backup restoration completed successfully: {backup_metadata.backup_id}"
179
+ )
180
+
181
+ self.security_logger.log_security_event(
182
+ SecurityEventType.BACKUP_RESTORED,
183
+ SecurityEventLevel.HIGH,
184
+ f"Backup restoration completed: {backup_metadata.backup_id}",
185
+ file_path=backup_metadata.package_directory,
186
+ backup_id=backup_metadata.backup_id,
187
+ )
188
+
189
+ except Exception as e:
190
+ if isinstance(e, ExecutionError):
191
+ raise
192
+
193
+ raise ExecutionError(
194
+ message=f"Failed to restore from backup {backup_metadata.backup_id}: {e}",
195
+ error_code=ErrorCode.FILE_WRITE_ERROR,
196
+ ) from e
197
+
198
+ finally:
199
+ if temp_restore_dir and temp_restore_dir.exists():
200
+ self._cleanup_backup_directory(temp_restore_dir)
201
+
202
+ def cleanup_backup(self, backup_metadata: BackupMetadata) -> None:
203
+ backup_dir = backup_metadata.backup_directory
204
+
205
+ if backup_dir.exists():
206
+ self._cleanup_backup_directory(backup_dir)
207
+
208
+ self.logger.info(f"Backup cleaned up: {backup_metadata.backup_id}")
209
+ self.security_logger.log_security_event(
210
+ SecurityEventType.BACKUP_DELETED,
211
+ SecurityEventLevel.LOW,
212
+ f"Backup cleanup completed: {backup_metadata.backup_id}",
213
+ file_path=backup_dir,
214
+ backup_id=backup_metadata.backup_id,
215
+ )
216
+
217
+ def _generate_backup_id(self) -> str:
218
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
219
+ random_hash = hashlib.md5(
220
+ f"{timestamp}_{time.time()}".encode(),
221
+ usedforsecurity=False,
222
+ ).hexdigest()[:8]
223
+ return f"backup_{timestamp}_{random_hash}"
224
+
225
+ def _create_backup_directory(self, backup_id: str) -> Path:
226
+ if not self.backup_root:
227
+ raise ExecutionError(
228
+ message="Backup root directory not configured",
229
+ error_code=ErrorCode.VALIDATION_ERROR,
230
+ )
231
+
232
+ self.backup_root.mkdir(parents=True, exist_ok=True)
233
+
234
+ backup_dir = self.backup_root / backup_id
235
+ backup_dir.mkdir(parents=True, exist_ok=True)
236
+
237
+ import stat
238
+
239
+ backup_dir.chmod(stat.S_IRWXU)
240
+
241
+ return backup_dir
242
+
243
+ def _create_temp_restore_directory(self, backup_id: str) -> Path:
244
+ import tempfile
245
+
246
+ temp_dir = Path(tempfile.mkdtemp(prefix=f"restore_{backup_id}_"))
247
+
248
+ import stat
249
+
250
+ temp_dir.chmod(stat.S_IRWXU)
251
+
252
+ return temp_dir
253
+
254
+ def _filter_package_files(
255
+ self,
256
+ python_files: list[Path],
257
+ package_directory: Path,
258
+ ) -> list[Path]:
259
+ ignore_patterns = {
260
+ "__pycache__",
261
+ ".git",
262
+ ".venv",
263
+ "site-packages",
264
+ ".pytest_cache",
265
+ "build",
266
+ "dist",
267
+ "tests",
268
+ "test",
269
+ "examples",
270
+ "example",
271
+ ".tox",
272
+ "htmlcov",
273
+ }
274
+
275
+ files_to_backup = []
276
+
277
+ for file_path in python_files:
278
+ skip_file = False
279
+ for parent in file_path.parents:
280
+ if parent.name in ignore_patterns:
281
+ skip_file = True
282
+ break
283
+
284
+ if skip_file:
285
+ continue
286
+
287
+ if file_path.name.startswith("."):
288
+ continue
289
+
290
+ if file_path.suffix != ".py":
291
+ continue
292
+
293
+ files_to_backup.append(file_path)
294
+
295
+ return files_to_backup
296
+
297
+ def _perform_backup(
298
+ self,
299
+ files_to_backup: list[Path],
300
+ package_directory: Path,
301
+ backup_dir: Path,
302
+ backup_id: str,
303
+ ) -> BackupMetadata:
304
+ file_checksums: dict[str, str] = {}
305
+ total_size = 0
306
+
307
+ for file_path in files_to_backup:
308
+ try:
309
+ relative_path = file_path.relative_to(package_directory)
310
+ backup_file_path = backup_dir / relative_path
311
+
312
+ backup_file_path.parent.mkdir(parents=True, exist_ok=True)
313
+
314
+ content = file_path.read_bytes()
315
+ total_size += len(content)
316
+
317
+ checksum = hashlib.sha256(content, usedforsecurity=False).hexdigest()
318
+ file_checksums[str(relative_path)] = checksum
319
+
320
+ AtomicFileOperations.atomic_write(backup_file_path, content)
321
+
322
+ self.logger.debug(f"Backed up file: {relative_path}")
323
+
324
+ except Exception as e:
325
+ raise ExecutionError(
326
+ message=f"Failed to backup file {file_path}: {e}",
327
+ error_code=ErrorCode.FILE_WRITE_ERROR,
328
+ ) from e
329
+
330
+ overall_checksum = self._calculate_backup_checksum(file_checksums)
331
+
332
+ return BackupMetadata(
333
+ backup_id=backup_id,
334
+ timestamp=datetime.now(),
335
+ package_directory=package_directory,
336
+ backup_directory=backup_dir,
337
+ total_files=len(files_to_backup),
338
+ total_size=total_size,
339
+ checksum=overall_checksum,
340
+ file_checksums=file_checksums,
341
+ )
342
+
343
+ def _calculate_backup_checksum(self, file_checksums: dict[str, str]) -> str:
344
+ sorted_items = sorted(file_checksums.items())
345
+ combined = "".join(f"{path}:{checksum}" for path, checksum in sorted_items)
346
+ return hashlib.sha256(combined.encode(), usedforsecurity=False).hexdigest()
347
+
348
+ def _validate_backup(
349
+ self, backup_metadata: BackupMetadata
350
+ ) -> BackupValidationResult:
351
+ missing_files: list[Path] = []
352
+ corrupted_files: list[Path] = []
353
+ validation_errors: list[str] = []
354
+ total_validated = 0
355
+
356
+ backup_dir = backup_metadata.backup_directory
357
+
358
+ if not backup_dir.exists():
359
+ validation_errors.append(f"Backup directory missing: {backup_dir}")
360
+ return BackupValidationResult(
361
+ is_valid=False,
362
+ missing_files=missing_files,
363
+ corrupted_files=corrupted_files,
364
+ total_validated=0,
365
+ validation_errors=validation_errors,
366
+ )
367
+
368
+ for (
369
+ relative_path_str,
370
+ expected_checksum,
371
+ ) in backup_metadata.file_checksums.items():
372
+ backup_file_path = backup_dir / relative_path_str
373
+
374
+ if not backup_file_path.exists():
375
+ missing_files.append(backup_file_path)
376
+ validation_errors.append(f"Missing backup file: {relative_path_str}")
377
+ continue
378
+
379
+ try:
380
+ content = backup_file_path.read_bytes()
381
+ actual_checksum = hashlib.sha256(
382
+ content, usedforsecurity=False
383
+ ).hexdigest()
384
+
385
+ if actual_checksum != expected_checksum:
386
+ corrupted_files.append(backup_file_path)
387
+ validation_errors.append(
388
+ f"Corrupted backup file: {relative_path_str} "
389
+ f"(expected: {expected_checksum}, actual: {actual_checksum})"
390
+ )
391
+ continue
392
+
393
+ total_validated += 1
394
+
395
+ except Exception as e:
396
+ validation_errors.append(f"Error validating {relative_path_str}: {e}")
397
+
398
+ if not validation_errors:
399
+ recalculated_checksum = self._calculate_backup_checksum(
400
+ backup_metadata.file_checksums
401
+ )
402
+ if recalculated_checksum != backup_metadata.checksum:
403
+ validation_errors.append("Overall backup checksum mismatch")
404
+
405
+ is_valid = len(validation_errors) == 0
406
+
407
+ if is_valid:
408
+ self.logger.debug(f"Backup validation passed: {backup_metadata.backup_id}")
409
+ else:
410
+ self.logger.warning(
411
+ f"Backup validation failed: {backup_metadata.backup_id}, "
412
+ f"errors: {len(validation_errors)}"
413
+ )
414
+
415
+ return BackupValidationResult(
416
+ is_valid=is_valid,
417
+ missing_files=missing_files,
418
+ corrupted_files=corrupted_files,
419
+ total_validated=total_validated,
420
+ validation_errors=validation_errors,
421
+ )
422
+
423
+ def _stage_backup_files(
424
+ self,
425
+ backup_metadata: BackupMetadata,
426
+ temp_restore_dir: Path,
427
+ ) -> None:
428
+ backup_dir = backup_metadata.backup_directory
429
+
430
+ for relative_path_str in backup_metadata.file_checksums.keys():
431
+ backup_file_path = backup_dir / relative_path_str
432
+ staging_file_path = temp_restore_dir / relative_path_str
433
+
434
+ staging_file_path.parent.mkdir(parents=True, exist_ok=True)
435
+
436
+ shutil.copy2(backup_file_path, staging_file_path)
437
+
438
+ def _commit_restoration(
439
+ self,
440
+ backup_metadata: BackupMetadata,
441
+ temp_restore_dir: Path,
442
+ base_directory: Path | None,
443
+ ) -> None:
444
+ package_dir = backup_metadata.package_directory
445
+
446
+ for relative_path_str in backup_metadata.file_checksums.keys():
447
+ staging_file_path = temp_restore_dir / relative_path_str
448
+ final_file_path = package_dir / relative_path_str
449
+
450
+ final_file_path.parent.mkdir(parents=True, exist_ok=True)
451
+
452
+ content = staging_file_path.read_bytes()
453
+
454
+ AtomicFileOperations.atomic_write(
455
+ final_file_path,
456
+ content,
457
+ base_directory,
458
+ )
459
+
460
+ self.logger.debug(f"Restored file: {relative_path_str}")
461
+
462
+ def _cleanup_backup_directory(self, directory: Path) -> None:
463
+ if directory.exists() and directory.is_dir():
464
+ try:
465
+ shutil.rmtree(directory)
466
+ except Exception as e:
467
+ self.logger.warning(f"Failed to cleanup directory {directory}: {e}")