claude-mpm 0.3.0__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 claude-mpm might be problematic. Click here for more details.
- claude_mpm/__init__.py +17 -0
- claude_mpm/__main__.py +14 -0
- claude_mpm/_version.py +32 -0
- claude_mpm/agents/BASE_AGENT_TEMPLATE.md +88 -0
- claude_mpm/agents/INSTRUCTIONS.md +375 -0
- claude_mpm/agents/__init__.py +118 -0
- claude_mpm/agents/agent_loader.py +621 -0
- claude_mpm/agents/agent_loader_integration.py +229 -0
- claude_mpm/agents/agents_metadata.py +204 -0
- claude_mpm/agents/base_agent.json +27 -0
- claude_mpm/agents/base_agent_loader.py +519 -0
- claude_mpm/agents/schema/agent_schema.json +160 -0
- claude_mpm/agents/system_agent_config.py +587 -0
- claude_mpm/agents/templates/__init__.py +101 -0
- claude_mpm/agents/templates/data_engineer_agent.json +46 -0
- claude_mpm/agents/templates/documentation_agent.json +45 -0
- claude_mpm/agents/templates/engineer_agent.json +49 -0
- claude_mpm/agents/templates/ops_agent.json +46 -0
- claude_mpm/agents/templates/qa_agent.json +45 -0
- claude_mpm/agents/templates/research_agent.json +49 -0
- claude_mpm/agents/templates/security_agent.json +46 -0
- claude_mpm/agents/templates/update-optimized-specialized-agents.json +374 -0
- claude_mpm/agents/templates/version_control_agent.json +46 -0
- claude_mpm/agents/test_fix_deployment/.claude-pm/config/project.json +6 -0
- claude_mpm/cli.py +655 -0
- claude_mpm/cli_main.py +13 -0
- claude_mpm/cli_module/__init__.py +15 -0
- claude_mpm/cli_module/args.py +222 -0
- claude_mpm/cli_module/commands.py +203 -0
- claude_mpm/cli_module/migration_example.py +183 -0
- claude_mpm/cli_module/refactoring_guide.md +253 -0
- claude_mpm/cli_old/__init__.py +1 -0
- claude_mpm/cli_old/ticket_cli.py +102 -0
- claude_mpm/config/__init__.py +5 -0
- claude_mpm/config/hook_config.py +42 -0
- claude_mpm/constants.py +150 -0
- claude_mpm/core/__init__.py +45 -0
- claude_mpm/core/agent_name_normalizer.py +248 -0
- claude_mpm/core/agent_registry.py +627 -0
- claude_mpm/core/agent_registry.py.bak +312 -0
- claude_mpm/core/agent_session_manager.py +273 -0
- claude_mpm/core/base_service.py +747 -0
- claude_mpm/core/base_service.py.bak +406 -0
- claude_mpm/core/config.py +334 -0
- claude_mpm/core/config_aliases.py +292 -0
- claude_mpm/core/container.py +347 -0
- claude_mpm/core/factories.py +281 -0
- claude_mpm/core/framework_loader.py +472 -0
- claude_mpm/core/injectable_service.py +206 -0
- claude_mpm/core/interfaces.py +539 -0
- claude_mpm/core/logger.py +468 -0
- claude_mpm/core/minimal_framework_loader.py +107 -0
- claude_mpm/core/mixins.py +150 -0
- claude_mpm/core/service_registry.py +299 -0
- claude_mpm/core/session_manager.py +190 -0
- claude_mpm/core/simple_runner.py +511 -0
- claude_mpm/core/tool_access_control.py +173 -0
- claude_mpm/hooks/README.md +243 -0
- claude_mpm/hooks/__init__.py +5 -0
- claude_mpm/hooks/base_hook.py +154 -0
- claude_mpm/hooks/builtin/__init__.py +1 -0
- claude_mpm/hooks/builtin/logging_hook_example.py +165 -0
- claude_mpm/hooks/builtin/post_delegation_hook_example.py +124 -0
- claude_mpm/hooks/builtin/pre_delegation_hook_example.py +125 -0
- claude_mpm/hooks/builtin/submit_hook_example.py +100 -0
- claude_mpm/hooks/builtin/ticket_extraction_hook_example.py +237 -0
- claude_mpm/hooks/builtin/todo_agent_prefix_hook.py +239 -0
- claude_mpm/hooks/builtin/workflow_start_hook.py +181 -0
- claude_mpm/hooks/hook_client.py +264 -0
- claude_mpm/hooks/hook_runner.py +370 -0
- claude_mpm/hooks/json_rpc_executor.py +259 -0
- claude_mpm/hooks/json_rpc_hook_client.py +319 -0
- claude_mpm/hooks/tool_call_interceptor.py +204 -0
- claude_mpm/init.py +246 -0
- claude_mpm/orchestration/SUBPROCESS_DESIGN.md +66 -0
- claude_mpm/orchestration/__init__.py +6 -0
- claude_mpm/orchestration/archive/direct_orchestrator.py +195 -0
- claude_mpm/orchestration/archive/factory.py +215 -0
- claude_mpm/orchestration/archive/hook_enabled_orchestrator.py +188 -0
- claude_mpm/orchestration/archive/hook_integration_example.py +178 -0
- claude_mpm/orchestration/archive/interactive_subprocess_orchestrator.py +826 -0
- claude_mpm/orchestration/archive/orchestrator.py +501 -0
- claude_mpm/orchestration/archive/pexpect_orchestrator.py +252 -0
- claude_mpm/orchestration/archive/pty_orchestrator.py +270 -0
- claude_mpm/orchestration/archive/simple_orchestrator.py +82 -0
- claude_mpm/orchestration/archive/subprocess_orchestrator.py +801 -0
- claude_mpm/orchestration/archive/system_prompt_orchestrator.py +278 -0
- claude_mpm/orchestration/archive/wrapper_orchestrator.py +187 -0
- claude_mpm/scripts/__init__.py +1 -0
- claude_mpm/scripts/ticket.py +269 -0
- claude_mpm/services/__init__.py +10 -0
- claude_mpm/services/agent_deployment.py +955 -0
- claude_mpm/services/agent_lifecycle_manager.py +948 -0
- claude_mpm/services/agent_management_service.py +596 -0
- claude_mpm/services/agent_modification_tracker.py +841 -0
- claude_mpm/services/agent_profile_loader.py +606 -0
- claude_mpm/services/agent_registry.py +677 -0
- claude_mpm/services/base_agent_manager.py +380 -0
- claude_mpm/services/framework_agent_loader.py +337 -0
- claude_mpm/services/framework_claude_md_generator/README.md +92 -0
- claude_mpm/services/framework_claude_md_generator/__init__.py +206 -0
- claude_mpm/services/framework_claude_md_generator/content_assembler.py +151 -0
- claude_mpm/services/framework_claude_md_generator/content_validator.py +126 -0
- claude_mpm/services/framework_claude_md_generator/deployment_manager.py +137 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/__init__.py +106 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +582 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/claude_pm_init.py +97 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/core_responsibilities.py +27 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/delegation_constraints.py +23 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/environment_config.py +23 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/footer.py +20 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/header.py +26 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/orchestration_principles.py +30 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/role_designation.py +37 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/subprocess_validation.py +111 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +89 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/troubleshooting.py +39 -0
- claude_mpm/services/framework_claude_md_generator/section_manager.py +106 -0
- claude_mpm/services/framework_claude_md_generator/version_manager.py +121 -0
- claude_mpm/services/framework_claude_md_generator.py +621 -0
- claude_mpm/services/hook_service.py +388 -0
- claude_mpm/services/hook_service_manager.py +223 -0
- claude_mpm/services/json_rpc_hook_manager.py +92 -0
- claude_mpm/services/parent_directory_manager/README.md +83 -0
- claude_mpm/services/parent_directory_manager/__init__.py +577 -0
- claude_mpm/services/parent_directory_manager/backup_manager.py +258 -0
- claude_mpm/services/parent_directory_manager/config_manager.py +210 -0
- claude_mpm/services/parent_directory_manager/deduplication_manager.py +279 -0
- claude_mpm/services/parent_directory_manager/framework_protector.py +143 -0
- claude_mpm/services/parent_directory_manager/operations.py +186 -0
- claude_mpm/services/parent_directory_manager/state_manager.py +624 -0
- claude_mpm/services/parent_directory_manager/template_deployer.py +579 -0
- claude_mpm/services/parent_directory_manager/validation_manager.py +378 -0
- claude_mpm/services/parent_directory_manager/version_control_helper.py +339 -0
- claude_mpm/services/parent_directory_manager/version_manager.py +222 -0
- claude_mpm/services/shared_prompt_cache.py +819 -0
- claude_mpm/services/ticket_manager.py +213 -0
- claude_mpm/services/ticket_manager_di.py +318 -0
- claude_mpm/services/ticketing_service_original.py +508 -0
- claude_mpm/services/version_control/VERSION +1 -0
- claude_mpm/services/version_control/__init__.py +70 -0
- claude_mpm/services/version_control/branch_strategy.py +670 -0
- claude_mpm/services/version_control/conflict_resolution.py +744 -0
- claude_mpm/services/version_control/git_operations.py +784 -0
- claude_mpm/services/version_control/semantic_versioning.py +703 -0
- claude_mpm/ui/__init__.py +1 -0
- claude_mpm/ui/rich_terminal_ui.py +295 -0
- claude_mpm/ui/terminal_ui.py +328 -0
- claude_mpm/utils/__init__.py +16 -0
- claude_mpm/utils/config_manager.py +468 -0
- claude_mpm/utils/import_migration_example.py +80 -0
- claude_mpm/utils/imports.py +182 -0
- claude_mpm/utils/path_operations.py +357 -0
- claude_mpm/utils/paths.py +289 -0
- claude_mpm-0.3.0.dist-info/METADATA +290 -0
- claude_mpm-0.3.0.dist-info/RECORD +159 -0
- claude_mpm-0.3.0.dist-info/WHEEL +5 -0
- claude_mpm-0.3.0.dist-info/entry_points.txt +4 -0
- claude_mpm-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Import utilities for handling relative and absolute imports.
|
|
2
|
+
|
|
3
|
+
This module provides utilities to handle the common pattern of trying
|
|
4
|
+
relative imports first and falling back to absolute imports, which is
|
|
5
|
+
used throughout the claude_mpm codebase.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import importlib
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Optional, Any, List, Union
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def safe_import(
|
|
14
|
+
module_name: str,
|
|
15
|
+
fallback_name: Optional[str] = None,
|
|
16
|
+
from_list: Optional[List[str]] = None,
|
|
17
|
+
logger: Optional[logging.Logger] = None
|
|
18
|
+
) -> Optional[Any]:
|
|
19
|
+
"""
|
|
20
|
+
Safely import a module with fallback support.
|
|
21
|
+
|
|
22
|
+
Attempts to import a module using the primary module name first,
|
|
23
|
+
then falls back to the fallback name if provided. This is useful
|
|
24
|
+
for handling both relative and absolute imports.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
module_name: Primary module name to import (e.g., '..utils.logger')
|
|
28
|
+
fallback_name: Fallback module name if primary fails (e.g., 'utils.logger')
|
|
29
|
+
from_list: List of names to import from the module (for 'from X import Y' style)
|
|
30
|
+
logger: Optional logger for debugging import attempts
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
The imported module or specific attribute(s) if from_list is provided.
|
|
34
|
+
Returns None if all import attempts fail.
|
|
35
|
+
|
|
36
|
+
Examples:
|
|
37
|
+
# Import entire module
|
|
38
|
+
logger_module = safe_import('..utils.logger', 'utils.logger')
|
|
39
|
+
|
|
40
|
+
# Import specific function (returns the function directly)
|
|
41
|
+
get_logger = safe_import('..utils.logger', 'utils.logger', ['get_logger'])
|
|
42
|
+
|
|
43
|
+
# Import multiple items (returns tuple)
|
|
44
|
+
logger, setup = safe_import('..utils.logger', 'utils.logger',
|
|
45
|
+
['get_logger', 'setup_logging'])
|
|
46
|
+
"""
|
|
47
|
+
# Try primary import
|
|
48
|
+
try:
|
|
49
|
+
if logger:
|
|
50
|
+
logger.debug(f"Attempting import: {module_name}")
|
|
51
|
+
|
|
52
|
+
module = importlib.import_module(module_name)
|
|
53
|
+
|
|
54
|
+
if from_list:
|
|
55
|
+
# Handle 'from X import Y' style imports
|
|
56
|
+
results = []
|
|
57
|
+
for name in from_list:
|
|
58
|
+
if hasattr(module, name):
|
|
59
|
+
results.append(getattr(module, name))
|
|
60
|
+
else:
|
|
61
|
+
if logger:
|
|
62
|
+
logger.warning(f"Module {module_name} has no attribute {name}")
|
|
63
|
+
results.append(None)
|
|
64
|
+
|
|
65
|
+
# Return single item if only one requested, otherwise tuple
|
|
66
|
+
if len(results) == 1:
|
|
67
|
+
return results[0]
|
|
68
|
+
return tuple(results)
|
|
69
|
+
|
|
70
|
+
return module
|
|
71
|
+
|
|
72
|
+
except ImportError as e:
|
|
73
|
+
if logger:
|
|
74
|
+
logger.debug(f"Primary import failed: {e}")
|
|
75
|
+
|
|
76
|
+
# Try fallback if provided
|
|
77
|
+
if fallback_name:
|
|
78
|
+
try:
|
|
79
|
+
if logger:
|
|
80
|
+
logger.debug(f"Attempting fallback import: {fallback_name}")
|
|
81
|
+
|
|
82
|
+
module = importlib.import_module(fallback_name)
|
|
83
|
+
|
|
84
|
+
if from_list:
|
|
85
|
+
# Handle 'from X import Y' style imports
|
|
86
|
+
results = []
|
|
87
|
+
for name in from_list:
|
|
88
|
+
if hasattr(module, name):
|
|
89
|
+
results.append(getattr(module, name))
|
|
90
|
+
else:
|
|
91
|
+
if logger:
|
|
92
|
+
logger.warning(f"Module {fallback_name} has no attribute {name}")
|
|
93
|
+
results.append(None)
|
|
94
|
+
|
|
95
|
+
# Return single item if only one requested, otherwise tuple
|
|
96
|
+
if len(results) == 1:
|
|
97
|
+
return results[0]
|
|
98
|
+
return tuple(results)
|
|
99
|
+
|
|
100
|
+
return module
|
|
101
|
+
|
|
102
|
+
except ImportError as e2:
|
|
103
|
+
if logger:
|
|
104
|
+
logger.debug(f"Fallback import also failed: {e2}")
|
|
105
|
+
|
|
106
|
+
# All imports failed
|
|
107
|
+
if logger:
|
|
108
|
+
logger.error(f"Failed to import {module_name}" +
|
|
109
|
+
(f" or {fallback_name}" if fallback_name else ""))
|
|
110
|
+
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def safe_import_multiple(
|
|
115
|
+
imports: List[Union[tuple, dict]],
|
|
116
|
+
logger: Optional[logging.Logger] = None
|
|
117
|
+
) -> dict:
|
|
118
|
+
"""
|
|
119
|
+
Import multiple modules with fallback support.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
imports: List of import specifications. Each can be:
|
|
123
|
+
- tuple: (primary_name, fallback_name, from_list)
|
|
124
|
+
- dict: {'primary': '...', 'fallback': '...', 'from_list': [...], 'as': 'alias'}
|
|
125
|
+
logger: Optional logger for debugging
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Dictionary mapping module/attribute names to imported objects
|
|
129
|
+
|
|
130
|
+
Example:
|
|
131
|
+
imports = [
|
|
132
|
+
('..utils.logger', 'utils.logger', ['get_logger']),
|
|
133
|
+
{'primary': '..core.agent_registry', 'fallback': 'core.agent_registry',
|
|
134
|
+
'from_list': ['AgentRegistry'], 'as': 'registry'}
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
imported = safe_import_multiple(imports)
|
|
138
|
+
# Result: {'get_logger': <function>, 'registry': <class>}
|
|
139
|
+
"""
|
|
140
|
+
results = {}
|
|
141
|
+
|
|
142
|
+
for spec in imports:
|
|
143
|
+
if isinstance(spec, tuple):
|
|
144
|
+
primary, fallback, from_list = spec
|
|
145
|
+
imported = safe_import(primary, fallback, from_list, logger)
|
|
146
|
+
|
|
147
|
+
if imported is not None:
|
|
148
|
+
if from_list and len(from_list) == 1:
|
|
149
|
+
# Single import gets stored by its name
|
|
150
|
+
results[from_list[0]] = imported
|
|
151
|
+
elif from_list and len(from_list) > 1:
|
|
152
|
+
# Multiple imports get unpacked
|
|
153
|
+
for i, name in enumerate(from_list):
|
|
154
|
+
if imported[i] is not None:
|
|
155
|
+
results[name] = imported[i]
|
|
156
|
+
else:
|
|
157
|
+
# Module import gets stored by last part of module name
|
|
158
|
+
module_alias = primary.split('.')[-1]
|
|
159
|
+
results[module_alias] = imported
|
|
160
|
+
|
|
161
|
+
elif isinstance(spec, dict):
|
|
162
|
+
primary = spec.get('primary')
|
|
163
|
+
fallback = spec.get('fallback')
|
|
164
|
+
from_list = spec.get('from_list')
|
|
165
|
+
alias = spec.get('as')
|
|
166
|
+
|
|
167
|
+
imported = safe_import(primary, fallback, from_list, logger)
|
|
168
|
+
|
|
169
|
+
if imported is not None:
|
|
170
|
+
if alias:
|
|
171
|
+
results[alias] = imported
|
|
172
|
+
elif from_list and len(from_list) == 1:
|
|
173
|
+
results[from_list[0]] = imported
|
|
174
|
+
elif from_list and len(from_list) > 1:
|
|
175
|
+
for i, name in enumerate(from_list):
|
|
176
|
+
if imported[i] is not None:
|
|
177
|
+
results[name] = imported[i]
|
|
178
|
+
else:
|
|
179
|
+
module_alias = primary.split('.')[-1]
|
|
180
|
+
results[module_alias] = imported
|
|
181
|
+
|
|
182
|
+
return results
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"""Path operations and validation utilities.
|
|
2
|
+
|
|
3
|
+
This module provides a centralized PathOperations class for common path validation
|
|
4
|
+
and file operations, reducing code duplication across the codebase.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
import tempfile
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional, List, Union, Callable
|
|
12
|
+
import logging
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PathOperations:
|
|
18
|
+
"""Utility class for path validation and safe file operations."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, default_encoding: str = 'utf-8'):
|
|
21
|
+
"""Initialize PathOperations with default encoding.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
default_encoding: Default encoding for file operations
|
|
25
|
+
"""
|
|
26
|
+
self.default_encoding = default_encoding
|
|
27
|
+
|
|
28
|
+
# Path Validation Methods
|
|
29
|
+
|
|
30
|
+
def validate_exists(self, path: Union[str, Path]) -> bool:
|
|
31
|
+
"""Check if path exists.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
path: Path to validate
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
True if path exists, False otherwise
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
return Path(path).exists()
|
|
41
|
+
except Exception as e:
|
|
42
|
+
logger.error(f"Error checking path existence: {e}")
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
def validate_is_file(self, path: Union[str, Path]) -> bool:
|
|
46
|
+
"""Check if path is a file.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
path: Path to validate
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
True if path is a file, False otherwise
|
|
53
|
+
"""
|
|
54
|
+
try:
|
|
55
|
+
return Path(path).is_file()
|
|
56
|
+
except Exception as e:
|
|
57
|
+
logger.error(f"Error checking if path is file: {e}")
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
def validate_is_dir(self, path: Union[str, Path]) -> bool:
|
|
61
|
+
"""Check if path is a directory.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
path: Path to validate
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
True if path is a directory, False otherwise
|
|
68
|
+
"""
|
|
69
|
+
try:
|
|
70
|
+
return Path(path).is_dir()
|
|
71
|
+
except Exception as e:
|
|
72
|
+
logger.error(f"Error checking if path is directory: {e}")
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
def validate_readable(self, path: Union[str, Path]) -> bool:
|
|
76
|
+
"""Check if path has read permissions.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
path: Path to validate
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
True if path is readable, False otherwise
|
|
83
|
+
"""
|
|
84
|
+
try:
|
|
85
|
+
path_obj = Path(path)
|
|
86
|
+
if not path_obj.exists():
|
|
87
|
+
return False
|
|
88
|
+
return os.access(str(path_obj), os.R_OK)
|
|
89
|
+
except Exception as e:
|
|
90
|
+
logger.error(f"Error checking read permissions: {e}")
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
def validate_writable(self, path: Union[str, Path]) -> bool:
|
|
94
|
+
"""Check if path has write permissions.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
path: Path to validate
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
True if path is writable, False otherwise
|
|
101
|
+
"""
|
|
102
|
+
try:
|
|
103
|
+
path_obj = Path(path)
|
|
104
|
+
if path_obj.exists():
|
|
105
|
+
return os.access(str(path_obj), os.W_OK)
|
|
106
|
+
# Check parent directory for new files
|
|
107
|
+
parent = path_obj.parent
|
|
108
|
+
return parent.exists() and os.access(str(parent), os.W_OK)
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.error(f"Error checking write permissions: {e}")
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
# Safe File Operations
|
|
114
|
+
|
|
115
|
+
def safe_read(self, path: Union[str, Path],
|
|
116
|
+
encoding: Optional[str] = None,
|
|
117
|
+
default: Optional[str] = None) -> Optional[str]:
|
|
118
|
+
"""Read file with error handling.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
path: Path to read
|
|
122
|
+
encoding: File encoding (uses default if None)
|
|
123
|
+
default: Default value if read fails
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
File contents or default value
|
|
127
|
+
"""
|
|
128
|
+
encoding = encoding or self.default_encoding
|
|
129
|
+
try:
|
|
130
|
+
path_obj = Path(path)
|
|
131
|
+
if not self.validate_readable(path_obj):
|
|
132
|
+
logger.warning(f"File not readable: {path}")
|
|
133
|
+
return default
|
|
134
|
+
|
|
135
|
+
with open(path_obj, 'r', encoding=encoding) as f:
|
|
136
|
+
return f.read()
|
|
137
|
+
except Exception as e:
|
|
138
|
+
logger.error(f"Error reading file {path}: {e}")
|
|
139
|
+
return default
|
|
140
|
+
|
|
141
|
+
def safe_write(self, path: Union[str, Path],
|
|
142
|
+
content: str,
|
|
143
|
+
encoding: Optional[str] = None,
|
|
144
|
+
backup: bool = False,
|
|
145
|
+
atomic: bool = False) -> bool:
|
|
146
|
+
"""Write file with error handling and optional backup.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
path: Path to write
|
|
150
|
+
content: Content to write
|
|
151
|
+
encoding: File encoding (uses default if None)
|
|
152
|
+
backup: Create backup before writing
|
|
153
|
+
atomic: Use atomic write (write to temp file and move)
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
True if write successful, False otherwise
|
|
157
|
+
"""
|
|
158
|
+
encoding = encoding or self.default_encoding
|
|
159
|
+
path_obj = Path(path)
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
# Create backup if requested
|
|
163
|
+
if backup and path_obj.exists():
|
|
164
|
+
backup_path = path_obj.with_suffix(path_obj.suffix + '.bak')
|
|
165
|
+
shutil.copy2(str(path_obj), str(backup_path))
|
|
166
|
+
logger.info(f"Created backup: {backup_path}")
|
|
167
|
+
|
|
168
|
+
# Ensure parent directory exists
|
|
169
|
+
path_obj.parent.mkdir(parents=True, exist_ok=True)
|
|
170
|
+
|
|
171
|
+
if atomic:
|
|
172
|
+
# Atomic write: write to temp file then move
|
|
173
|
+
with tempfile.NamedTemporaryFile(
|
|
174
|
+
mode='w',
|
|
175
|
+
encoding=encoding,
|
|
176
|
+
dir=path_obj.parent,
|
|
177
|
+
delete=False
|
|
178
|
+
) as tmp_file:
|
|
179
|
+
tmp_file.write(content)
|
|
180
|
+
tmp_path = tmp_file.name
|
|
181
|
+
|
|
182
|
+
# Move temp file to target
|
|
183
|
+
shutil.move(tmp_path, str(path_obj))
|
|
184
|
+
else:
|
|
185
|
+
# Direct write
|
|
186
|
+
with open(path_obj, 'w', encoding=encoding) as f:
|
|
187
|
+
f.write(content)
|
|
188
|
+
|
|
189
|
+
logger.info(f"Successfully wrote to {path}")
|
|
190
|
+
return True
|
|
191
|
+
|
|
192
|
+
except Exception as e:
|
|
193
|
+
logger.error(f"Error writing file {path}: {e}")
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
def safe_delete(self, path: Union[str, Path],
|
|
197
|
+
confirm: Optional[Callable[[], bool]] = None) -> bool:
|
|
198
|
+
"""Delete file/directory with optional confirmation.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
path: Path to delete
|
|
202
|
+
confirm: Optional confirmation callback
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
True if delete successful, False otherwise
|
|
206
|
+
"""
|
|
207
|
+
try:
|
|
208
|
+
path_obj = Path(path)
|
|
209
|
+
if not path_obj.exists():
|
|
210
|
+
logger.warning(f"Path does not exist: {path}")
|
|
211
|
+
return True
|
|
212
|
+
|
|
213
|
+
# Confirm if callback provided
|
|
214
|
+
if confirm and not confirm():
|
|
215
|
+
logger.info(f"Delete cancelled by user: {path}")
|
|
216
|
+
return False
|
|
217
|
+
|
|
218
|
+
if path_obj.is_file():
|
|
219
|
+
path_obj.unlink()
|
|
220
|
+
else:
|
|
221
|
+
shutil.rmtree(str(path_obj))
|
|
222
|
+
|
|
223
|
+
logger.info(f"Successfully deleted: {path}")
|
|
224
|
+
return True
|
|
225
|
+
|
|
226
|
+
except Exception as e:
|
|
227
|
+
logger.error(f"Error deleting {path}: {e}")
|
|
228
|
+
return False
|
|
229
|
+
|
|
230
|
+
def safe_copy(self, src: Union[str, Path],
|
|
231
|
+
dst: Union[str, Path],
|
|
232
|
+
overwrite: bool = False) -> bool:
|
|
233
|
+
"""Copy file/directory with overwrite protection.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
src: Source path
|
|
237
|
+
dst: Destination path
|
|
238
|
+
overwrite: Allow overwriting existing files
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
True if copy successful, False otherwise
|
|
242
|
+
"""
|
|
243
|
+
try:
|
|
244
|
+
src_path = Path(src)
|
|
245
|
+
dst_path = Path(dst)
|
|
246
|
+
|
|
247
|
+
if not src_path.exists():
|
|
248
|
+
logger.error(f"Source does not exist: {src}")
|
|
249
|
+
return False
|
|
250
|
+
|
|
251
|
+
if dst_path.exists() and not overwrite:
|
|
252
|
+
logger.error(f"Destination exists and overwrite=False: {dst}")
|
|
253
|
+
return False
|
|
254
|
+
|
|
255
|
+
# Ensure parent directory exists
|
|
256
|
+
dst_path.parent.mkdir(parents=True, exist_ok=True)
|
|
257
|
+
|
|
258
|
+
if src_path.is_file():
|
|
259
|
+
shutil.copy2(str(src_path), str(dst_path))
|
|
260
|
+
else:
|
|
261
|
+
if dst_path.exists():
|
|
262
|
+
shutil.rmtree(str(dst_path))
|
|
263
|
+
shutil.copytree(str(src_path), str(dst_path))
|
|
264
|
+
|
|
265
|
+
logger.info(f"Successfully copied {src} to {dst}")
|
|
266
|
+
return True
|
|
267
|
+
|
|
268
|
+
except Exception as e:
|
|
269
|
+
logger.error(f"Error copying {src} to {dst}: {e}")
|
|
270
|
+
return False
|
|
271
|
+
|
|
272
|
+
# Common Patterns
|
|
273
|
+
|
|
274
|
+
def ensure_dir(self, path: Union[str, Path]) -> bool:
|
|
275
|
+
"""Create directory if it doesn't exist.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
path: Directory path to ensure
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
True if directory exists or was created, False otherwise
|
|
282
|
+
"""
|
|
283
|
+
try:
|
|
284
|
+
Path(path).mkdir(parents=True, exist_ok=True)
|
|
285
|
+
return True
|
|
286
|
+
except Exception as e:
|
|
287
|
+
logger.error(f"Error creating directory {path}: {e}")
|
|
288
|
+
return False
|
|
289
|
+
|
|
290
|
+
def get_size(self, path: Union[str, Path]) -> int:
|
|
291
|
+
"""Get size of file or directory in bytes.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
path: Path to measure
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
Size in bytes, or -1 on error
|
|
298
|
+
"""
|
|
299
|
+
try:
|
|
300
|
+
path_obj = Path(path)
|
|
301
|
+
if not path_obj.exists():
|
|
302
|
+
return -1
|
|
303
|
+
|
|
304
|
+
if path_obj.is_file():
|
|
305
|
+
return path_obj.stat().st_size
|
|
306
|
+
else:
|
|
307
|
+
# Calculate total size for directory
|
|
308
|
+
total = 0
|
|
309
|
+
for item in path_obj.rglob('*'):
|
|
310
|
+
if item.is_file():
|
|
311
|
+
total += item.stat().st_size
|
|
312
|
+
return total
|
|
313
|
+
|
|
314
|
+
except Exception as e:
|
|
315
|
+
logger.error(f"Error getting size of {path}: {e}")
|
|
316
|
+
return -1
|
|
317
|
+
|
|
318
|
+
def list_files(self, path: Union[str, Path],
|
|
319
|
+
pattern: str = '*',
|
|
320
|
+
recursive: bool = False,
|
|
321
|
+
include_dirs: bool = False) -> List[Path]:
|
|
322
|
+
"""List files in directory with filtering.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
path: Directory path
|
|
326
|
+
pattern: Glob pattern for filtering
|
|
327
|
+
recursive: Search recursively
|
|
328
|
+
include_dirs: Include directories in results
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
List of matching paths
|
|
332
|
+
"""
|
|
333
|
+
try:
|
|
334
|
+
path_obj = Path(path)
|
|
335
|
+
if not path_obj.is_dir():
|
|
336
|
+
logger.error(f"Not a directory: {path}")
|
|
337
|
+
return []
|
|
338
|
+
|
|
339
|
+
if recursive:
|
|
340
|
+
items = path_obj.rglob(pattern)
|
|
341
|
+
else:
|
|
342
|
+
items = path_obj.glob(pattern)
|
|
343
|
+
|
|
344
|
+
results = []
|
|
345
|
+
for item in items:
|
|
346
|
+
if item.is_file() or (include_dirs and item.is_dir()):
|
|
347
|
+
results.append(item)
|
|
348
|
+
|
|
349
|
+
return sorted(results)
|
|
350
|
+
|
|
351
|
+
except Exception as e:
|
|
352
|
+
logger.error(f"Error listing files in {path}: {e}")
|
|
353
|
+
return []
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
# Convenience instance for direct imports
|
|
357
|
+
path_ops = PathOperations()
|