claude-mpm 4.3.20__py3-none-any.whl → 4.4.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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/agent_loader.py +2 -2
- claude_mpm/agents/agent_loader_integration.py +2 -2
- claude_mpm/agents/async_agent_loader.py +2 -2
- claude_mpm/agents/base_agent_loader.py +2 -2
- claude_mpm/agents/frontmatter_validator.py +2 -2
- claude_mpm/agents/system_agent_config.py +2 -2
- claude_mpm/agents/templates/data_engineer.json +1 -2
- claude_mpm/cli/commands/doctor.py +2 -2
- claude_mpm/cli/commands/mpm_init.py +560 -47
- claude_mpm/cli/commands/mpm_init_handler.py +6 -0
- claude_mpm/cli/parsers/mpm_init_parser.py +39 -1
- claude_mpm/cli/startup_logging.py +11 -9
- claude_mpm/commands/mpm-init.md +76 -12
- claude_mpm/config/agent_config.py +2 -2
- claude_mpm/config/paths.py +2 -2
- claude_mpm/core/agent_name_normalizer.py +2 -2
- claude_mpm/core/config.py +2 -1
- claude_mpm/core/config_aliases.py +2 -2
- claude_mpm/core/file_utils.py +1 -0
- claude_mpm/core/log_manager.py +2 -2
- claude_mpm/core/tool_access_control.py +2 -2
- claude_mpm/core/unified_agent_registry.py +2 -2
- claude_mpm/core/unified_paths.py +2 -2
- claude_mpm/experimental/cli_enhancements.py +3 -2
- claude_mpm/hooks/base_hook.py +2 -2
- claude_mpm/hooks/instruction_reinforcement.py +2 -2
- claude_mpm/hooks/memory_integration_hook.py +1 -1
- claude_mpm/hooks/validation_hooks.py +2 -2
- claude_mpm/scripts/mpm_doctor.py +2 -2
- claude_mpm/services/agents/loading/agent_profile_loader.py +2 -2
- claude_mpm/services/agents/loading/base_agent_manager.py +2 -2
- claude_mpm/services/agents/loading/framework_agent_loader.py +2 -2
- claude_mpm/services/agents/management/agent_capabilities_generator.py +2 -2
- claude_mpm/services/agents/management/agent_management_service.py +2 -2
- claude_mpm/services/agents/memory/content_manager.py +5 -2
- claude_mpm/services/agents/memory/memory_categorization_service.py +5 -2
- claude_mpm/services/agents/memory/memory_file_service.py +28 -6
- claude_mpm/services/agents/memory/memory_format_service.py +5 -2
- claude_mpm/services/agents/memory/memory_limits_service.py +4 -2
- claude_mpm/services/agents/registry/deployed_agent_discovery.py +2 -2
- claude_mpm/services/agents/registry/modification_tracker.py +4 -4
- claude_mpm/services/async_session_logger.py +2 -1
- claude_mpm/services/claude_session_logger.py +2 -2
- claude_mpm/services/core/path_resolver.py +3 -2
- claude_mpm/services/diagnostics/diagnostic_runner.py +4 -3
- claude_mpm/services/event_bus/direct_relay.py +2 -1
- claude_mpm/services/event_bus/event_bus.py +2 -1
- claude_mpm/services/event_bus/relay.py +2 -2
- claude_mpm/services/framework_claude_md_generator/content_assembler.py +2 -2
- claude_mpm/services/infrastructure/daemon_manager.py +2 -2
- claude_mpm/services/memory/cache/simple_cache.py +2 -2
- claude_mpm/services/project/archive_manager.py +981 -0
- claude_mpm/services/project/documentation_manager.py +536 -0
- claude_mpm/services/project/enhanced_analyzer.py +491 -0
- claude_mpm/services/project/project_organizer.py +904 -0
- claude_mpm/services/response_tracker.py +2 -2
- claude_mpm/services/socketio/handlers/connection.py +14 -33
- claude_mpm/services/socketio/server/eventbus_integration.py +2 -2
- claude_mpm/services/unified/__init__.py +65 -0
- claude_mpm/services/unified/analyzer_strategies/__init__.py +44 -0
- claude_mpm/services/unified/analyzer_strategies/code_analyzer.py +473 -0
- claude_mpm/services/unified/analyzer_strategies/dependency_analyzer.py +643 -0
- claude_mpm/services/unified/analyzer_strategies/performance_analyzer.py +804 -0
- claude_mpm/services/unified/analyzer_strategies/security_analyzer.py +661 -0
- claude_mpm/services/unified/analyzer_strategies/structure_analyzer.py +696 -0
- claude_mpm/services/unified/deployment_strategies/__init__.py +97 -0
- claude_mpm/services/unified/deployment_strategies/base.py +557 -0
- claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +486 -0
- claude_mpm/services/unified/deployment_strategies/local.py +594 -0
- claude_mpm/services/unified/deployment_strategies/utils.py +672 -0
- claude_mpm/services/unified/deployment_strategies/vercel.py +471 -0
- claude_mpm/services/unified/interfaces.py +499 -0
- claude_mpm/services/unified/migration.py +532 -0
- claude_mpm/services/unified/strategies.py +551 -0
- claude_mpm/services/unified/unified_analyzer.py +534 -0
- claude_mpm/services/unified/unified_config.py +688 -0
- claude_mpm/services/unified/unified_deployment.py +470 -0
- claude_mpm/services/version_control/version_parser.py +5 -4
- claude_mpm/storage/state_storage.py +2 -2
- claude_mpm/utils/agent_dependency_loader.py +49 -0
- claude_mpm/utils/common.py +542 -0
- claude_mpm/utils/database_connector.py +298 -0
- claude_mpm/utils/error_handler.py +2 -1
- claude_mpm/utils/log_cleanup.py +2 -2
- claude_mpm/utils/path_operations.py +2 -2
- claude_mpm/utils/robust_installer.py +56 -0
- claude_mpm/utils/session_logging.py +2 -2
- claude_mpm/utils/subprocess_utils.py +2 -2
- claude_mpm/validation/agent_validator.py +2 -2
- {claude_mpm-4.3.20.dist-info → claude_mpm-4.4.0.dist-info}/METADATA +1 -1
- {claude_mpm-4.3.20.dist-info → claude_mpm-4.4.0.dist-info}/RECORD +96 -71
- {claude_mpm-4.3.20.dist-info → claude_mpm-4.4.0.dist-info}/WHEEL +0 -0
- {claude_mpm-4.3.20.dist-info → claude_mpm-4.4.0.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.3.20.dist-info → claude_mpm-4.4.0.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.3.20.dist-info → claude_mpm-4.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,542 @@
|
|
1
|
+
"""
|
2
|
+
Common utility functions to replace duplicate implementations across the codebase.
|
3
|
+
|
4
|
+
This module consolidates frequently duplicated utility functions found across
|
5
|
+
50+ files in the claude-mpm codebase.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import json
|
9
|
+
import os
|
10
|
+
import subprocess
|
11
|
+
import sys
|
12
|
+
from pathlib import Path
|
13
|
+
from typing import Any, Dict, List, Optional, Union
|
14
|
+
|
15
|
+
import yaml
|
16
|
+
|
17
|
+
# Import our centralized logger
|
18
|
+
from claude_mpm.core.logging_utils import get_logger
|
19
|
+
|
20
|
+
logger = get_logger(__name__)
|
21
|
+
|
22
|
+
|
23
|
+
# ==============================================================================
|
24
|
+
# JSON/YAML UTILITIES
|
25
|
+
# ==============================================================================
|
26
|
+
|
27
|
+
|
28
|
+
def load_json_safe(
|
29
|
+
file_path: Union[str, Path],
|
30
|
+
default: Optional[Any] = None,
|
31
|
+
encoding: str = "utf-8",
|
32
|
+
) -> Any:
|
33
|
+
"""
|
34
|
+
Safely load JSON from a file with error handling.
|
35
|
+
|
36
|
+
Replaces 20+ duplicate implementations across the codebase.
|
37
|
+
|
38
|
+
Args:
|
39
|
+
file_path: Path to JSON file
|
40
|
+
default: Default value if file doesn't exist or is invalid
|
41
|
+
encoding: File encoding
|
42
|
+
|
43
|
+
Returns:
|
44
|
+
Parsed JSON data or default value
|
45
|
+
"""
|
46
|
+
file_path = Path(file_path)
|
47
|
+
|
48
|
+
try:
|
49
|
+
with open(file_path, "r", encoding=encoding) as f:
|
50
|
+
return json.load(f)
|
51
|
+
except FileNotFoundError:
|
52
|
+
logger.debug(f"JSON file not found: {file_path}")
|
53
|
+
return default if default is not None else {}
|
54
|
+
except json.JSONDecodeError as e:
|
55
|
+
logger.warning(f"Invalid JSON in {file_path}: {e}")
|
56
|
+
return default if default is not None else {}
|
57
|
+
except Exception as e:
|
58
|
+
logger.error(f"Error loading JSON from {file_path}: {e}")
|
59
|
+
return default if default is not None else {}
|
60
|
+
|
61
|
+
|
62
|
+
def save_json_safe(
|
63
|
+
file_path: Union[str, Path],
|
64
|
+
data: Any,
|
65
|
+
indent: int = 2,
|
66
|
+
encoding: str = "utf-8",
|
67
|
+
create_parents: bool = True,
|
68
|
+
) -> bool:
|
69
|
+
"""
|
70
|
+
Safely save data to JSON file with error handling.
|
71
|
+
|
72
|
+
Args:
|
73
|
+
file_path: Path to save JSON file
|
74
|
+
data: Data to serialize
|
75
|
+
indent: JSON indentation level
|
76
|
+
encoding: File encoding
|
77
|
+
create_parents: Create parent directories if they don't exist
|
78
|
+
|
79
|
+
Returns:
|
80
|
+
True if successful, False otherwise
|
81
|
+
"""
|
82
|
+
file_path = Path(file_path)
|
83
|
+
|
84
|
+
try:
|
85
|
+
if create_parents:
|
86
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
87
|
+
|
88
|
+
with open(file_path, "w", encoding=encoding) as f:
|
89
|
+
json.dump(data, f, indent=indent, ensure_ascii=False)
|
90
|
+
return True
|
91
|
+
except Exception as e:
|
92
|
+
logger.error(f"Error saving JSON to {file_path}: {e}")
|
93
|
+
return False
|
94
|
+
|
95
|
+
|
96
|
+
def load_yaml_safe(
|
97
|
+
file_path: Union[str, Path],
|
98
|
+
default: Optional[Any] = None,
|
99
|
+
encoding: str = "utf-8",
|
100
|
+
) -> Any:
|
101
|
+
"""
|
102
|
+
Safely load YAML from a file with error handling.
|
103
|
+
|
104
|
+
Args:
|
105
|
+
file_path: Path to YAML file
|
106
|
+
default: Default value if file doesn't exist or is invalid
|
107
|
+
encoding: File encoding
|
108
|
+
|
109
|
+
Returns:
|
110
|
+
Parsed YAML data or default value
|
111
|
+
"""
|
112
|
+
file_path = Path(file_path)
|
113
|
+
|
114
|
+
try:
|
115
|
+
with open(file_path, "r", encoding=encoding) as f:
|
116
|
+
return yaml.safe_load(f) or default or {}
|
117
|
+
except FileNotFoundError:
|
118
|
+
logger.debug(f"YAML file not found: {file_path}")
|
119
|
+
return default if default is not None else {}
|
120
|
+
except yaml.YAMLError as e:
|
121
|
+
logger.warning(f"Invalid YAML in {file_path}: {e}")
|
122
|
+
return default if default is not None else {}
|
123
|
+
except Exception as e:
|
124
|
+
logger.error(f"Error loading YAML from {file_path}: {e}")
|
125
|
+
return default if default is not None else {}
|
126
|
+
|
127
|
+
|
128
|
+
def save_yaml_safe(
|
129
|
+
file_path: Union[str, Path],
|
130
|
+
data: Any,
|
131
|
+
encoding: str = "utf-8",
|
132
|
+
create_parents: bool = True,
|
133
|
+
) -> bool:
|
134
|
+
"""
|
135
|
+
Safely save data to YAML file with error handling.
|
136
|
+
|
137
|
+
Args:
|
138
|
+
file_path: Path to save YAML file
|
139
|
+
data: Data to serialize
|
140
|
+
encoding: File encoding
|
141
|
+
create_parents: Create parent directories if they don't exist
|
142
|
+
|
143
|
+
Returns:
|
144
|
+
True if successful, False otherwise
|
145
|
+
"""
|
146
|
+
file_path = Path(file_path)
|
147
|
+
|
148
|
+
try:
|
149
|
+
if create_parents:
|
150
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
151
|
+
|
152
|
+
with open(file_path, "w", encoding=encoding) as f:
|
153
|
+
yaml.safe_dump(data, f, default_flow_style=False, allow_unicode=True)
|
154
|
+
return True
|
155
|
+
except Exception as e:
|
156
|
+
logger.error(f"Error saving YAML to {file_path}: {e}")
|
157
|
+
return False
|
158
|
+
|
159
|
+
|
160
|
+
# ==============================================================================
|
161
|
+
# PATH/FILE UTILITIES
|
162
|
+
# ==============================================================================
|
163
|
+
|
164
|
+
|
165
|
+
def ensure_path_exists(
|
166
|
+
path: Union[str, Path],
|
167
|
+
create_parents: bool = True,
|
168
|
+
is_file: bool = False,
|
169
|
+
) -> bool:
|
170
|
+
"""
|
171
|
+
Ensure a path exists, optionally creating parent directories.
|
172
|
+
|
173
|
+
Replaces 50+ duplicate path existence checks.
|
174
|
+
|
175
|
+
Args:
|
176
|
+
path: Path to check/create
|
177
|
+
create_parents: Create parent directories if needed
|
178
|
+
is_file: If True, path is a file (create parent dir only)
|
179
|
+
|
180
|
+
Returns:
|
181
|
+
True if path exists or was created, False otherwise
|
182
|
+
"""
|
183
|
+
path = Path(path)
|
184
|
+
|
185
|
+
try:
|
186
|
+
if path.exists():
|
187
|
+
return True
|
188
|
+
|
189
|
+
if is_file:
|
190
|
+
if create_parents:
|
191
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
192
|
+
# Don't create the file itself, just ensure parent exists
|
193
|
+
return path.parent.exists()
|
194
|
+
else:
|
195
|
+
if create_parents:
|
196
|
+
path.mkdir(parents=True, exist_ok=True)
|
197
|
+
return True
|
198
|
+
return False
|
199
|
+
except Exception as e:
|
200
|
+
logger.error(f"Error ensuring path exists {path}: {e}")
|
201
|
+
return False
|
202
|
+
|
203
|
+
|
204
|
+
def read_file_if_exists(
|
205
|
+
file_path: Union[str, Path],
|
206
|
+
encoding: str = "utf-8",
|
207
|
+
default: str = "",
|
208
|
+
) -> Optional[str]:
|
209
|
+
"""
|
210
|
+
Read file contents if it exists, otherwise return default.
|
211
|
+
|
212
|
+
Args:
|
213
|
+
file_path: Path to file
|
214
|
+
encoding: File encoding
|
215
|
+
default: Default value if file doesn't exist
|
216
|
+
|
217
|
+
Returns:
|
218
|
+
File contents or default value
|
219
|
+
"""
|
220
|
+
file_path = Path(file_path)
|
221
|
+
|
222
|
+
try:
|
223
|
+
if file_path.exists() and file_path.is_file():
|
224
|
+
return file_path.read_text(encoding=encoding)
|
225
|
+
return default
|
226
|
+
except Exception as e:
|
227
|
+
logger.error(f"Error reading file {file_path}: {e}")
|
228
|
+
return default
|
229
|
+
|
230
|
+
|
231
|
+
def write_file_safe(
|
232
|
+
file_path: Union[str, Path],
|
233
|
+
content: str,
|
234
|
+
encoding: str = "utf-8",
|
235
|
+
create_parents: bool = True,
|
236
|
+
) -> bool:
|
237
|
+
"""
|
238
|
+
Safely write content to file with error handling.
|
239
|
+
|
240
|
+
Args:
|
241
|
+
file_path: Path to file
|
242
|
+
content: Content to write
|
243
|
+
encoding: File encoding
|
244
|
+
create_parents: Create parent directories if needed
|
245
|
+
|
246
|
+
Returns:
|
247
|
+
True if successful, False otherwise
|
248
|
+
"""
|
249
|
+
file_path = Path(file_path)
|
250
|
+
|
251
|
+
try:
|
252
|
+
if create_parents:
|
253
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
254
|
+
|
255
|
+
file_path.write_text(content, encoding=encoding)
|
256
|
+
return True
|
257
|
+
except Exception as e:
|
258
|
+
logger.error(f"Error writing file {file_path}: {e}")
|
259
|
+
return False
|
260
|
+
|
261
|
+
|
262
|
+
def get_file_size(file_path: Union[str, Path]) -> int:
|
263
|
+
"""
|
264
|
+
Get file size in bytes, returning 0 if file doesn't exist.
|
265
|
+
|
266
|
+
Args:
|
267
|
+
file_path: Path to file
|
268
|
+
|
269
|
+
Returns:
|
270
|
+
File size in bytes or 0
|
271
|
+
"""
|
272
|
+
file_path = Path(file_path)
|
273
|
+
|
274
|
+
try:
|
275
|
+
if file_path.exists() and file_path.is_file():
|
276
|
+
return file_path.stat().st_size
|
277
|
+
return 0
|
278
|
+
except Exception as e:
|
279
|
+
logger.error(f"Error getting file size {file_path}: {e}")
|
280
|
+
return 0
|
281
|
+
|
282
|
+
|
283
|
+
def find_files(
|
284
|
+
directory: Union[str, Path],
|
285
|
+
pattern: str = "*",
|
286
|
+
recursive: bool = True,
|
287
|
+
) -> List[Path]:
|
288
|
+
"""
|
289
|
+
Find files matching a pattern in a directory.
|
290
|
+
|
291
|
+
Args:
|
292
|
+
directory: Directory to search
|
293
|
+
pattern: Glob pattern
|
294
|
+
recursive: Search recursively
|
295
|
+
|
296
|
+
Returns:
|
297
|
+
List of matching file paths
|
298
|
+
"""
|
299
|
+
directory = Path(directory)
|
300
|
+
|
301
|
+
try:
|
302
|
+
if not directory.exists():
|
303
|
+
return []
|
304
|
+
|
305
|
+
if recursive:
|
306
|
+
return list(directory.rglob(pattern))
|
307
|
+
else:
|
308
|
+
return list(directory.glob(pattern))
|
309
|
+
except Exception as e:
|
310
|
+
logger.error(f"Error finding files in {directory}: {e}")
|
311
|
+
return []
|
312
|
+
|
313
|
+
|
314
|
+
# ==============================================================================
|
315
|
+
# SUBPROCESS UTILITIES
|
316
|
+
# ==============================================================================
|
317
|
+
|
318
|
+
|
319
|
+
def run_command_safe(
|
320
|
+
command: Union[str, List[str]],
|
321
|
+
cwd: Optional[Union[str, Path]] = None,
|
322
|
+
capture_output: bool = True,
|
323
|
+
check: bool = False,
|
324
|
+
timeout: Optional[int] = None,
|
325
|
+
env: Optional[Dict[str, str]] = None,
|
326
|
+
) -> subprocess.CompletedProcess:
|
327
|
+
"""
|
328
|
+
Safely run a subprocess command with error handling.
|
329
|
+
|
330
|
+
Replaces 15+ duplicate subprocess patterns.
|
331
|
+
|
332
|
+
Args:
|
333
|
+
command: Command to run (string or list)
|
334
|
+
cwd: Working directory
|
335
|
+
capture_output: Capture stdout/stderr
|
336
|
+
check: Raise exception on non-zero return code
|
337
|
+
timeout: Command timeout in seconds
|
338
|
+
env: Environment variables
|
339
|
+
|
340
|
+
Returns:
|
341
|
+
CompletedProcess result
|
342
|
+
"""
|
343
|
+
try:
|
344
|
+
if isinstance(command, str):
|
345
|
+
shell = True
|
346
|
+
else:
|
347
|
+
shell = False
|
348
|
+
|
349
|
+
result = subprocess.run(
|
350
|
+
command,
|
351
|
+
shell=shell,
|
352
|
+
cwd=cwd,
|
353
|
+
capture_output=capture_output,
|
354
|
+
text=True,
|
355
|
+
check=check,
|
356
|
+
timeout=timeout,
|
357
|
+
env=env,
|
358
|
+
)
|
359
|
+
return result
|
360
|
+
except subprocess.TimeoutExpired as e:
|
361
|
+
logger.error(f"Command timed out: {command}")
|
362
|
+
raise
|
363
|
+
except subprocess.CalledProcessError as e:
|
364
|
+
logger.error(f"Command failed: {command}, return code: {e.returncode}")
|
365
|
+
raise
|
366
|
+
except Exception as e:
|
367
|
+
logger.error(f"Error running command {command}: {e}")
|
368
|
+
raise
|
369
|
+
|
370
|
+
|
371
|
+
def check_command_exists(command: str) -> bool:
|
372
|
+
"""
|
373
|
+
Check if a command exists in the system PATH.
|
374
|
+
|
375
|
+
Args:
|
376
|
+
command: Command name to check
|
377
|
+
|
378
|
+
Returns:
|
379
|
+
True if command exists, False otherwise
|
380
|
+
"""
|
381
|
+
try:
|
382
|
+
result = run_command_safe(
|
383
|
+
["which", command] if sys.platform != "win32" else ["where", command],
|
384
|
+
capture_output=True,
|
385
|
+
check=False,
|
386
|
+
)
|
387
|
+
return result.returncode == 0
|
388
|
+
except Exception:
|
389
|
+
return False
|
390
|
+
|
391
|
+
|
392
|
+
# ==============================================================================
|
393
|
+
# ENVIRONMENT UTILITIES
|
394
|
+
# ==============================================================================
|
395
|
+
|
396
|
+
|
397
|
+
def get_env_bool(key: str, default: bool = False) -> bool:
|
398
|
+
"""
|
399
|
+
Get boolean value from environment variable.
|
400
|
+
|
401
|
+
Args:
|
402
|
+
key: Environment variable key
|
403
|
+
default: Default value if not set
|
404
|
+
|
405
|
+
Returns:
|
406
|
+
Boolean value
|
407
|
+
"""
|
408
|
+
value = os.environ.get(key, "").lower()
|
409
|
+
|
410
|
+
if not value:
|
411
|
+
return default
|
412
|
+
|
413
|
+
return value in ("1", "true", "yes", "on")
|
414
|
+
|
415
|
+
|
416
|
+
def get_env_int(key: str, default: int = 0) -> int:
|
417
|
+
"""
|
418
|
+
Get integer value from environment variable.
|
419
|
+
|
420
|
+
Args:
|
421
|
+
key: Environment variable key
|
422
|
+
default: Default value if not set or invalid
|
423
|
+
|
424
|
+
Returns:
|
425
|
+
Integer value
|
426
|
+
"""
|
427
|
+
value = os.environ.get(key, "")
|
428
|
+
|
429
|
+
if not value:
|
430
|
+
return default
|
431
|
+
|
432
|
+
try:
|
433
|
+
return int(value)
|
434
|
+
except ValueError:
|
435
|
+
logger.warning(f"Invalid integer value for {key}: {value}")
|
436
|
+
return default
|
437
|
+
|
438
|
+
|
439
|
+
def get_env_list(key: str, separator: str = ",", default: Optional[List[str]] = None) -> List[str]:
|
440
|
+
"""
|
441
|
+
Get list from environment variable.
|
442
|
+
|
443
|
+
Args:
|
444
|
+
key: Environment variable key
|
445
|
+
separator: List separator
|
446
|
+
default: Default list if not set
|
447
|
+
|
448
|
+
Returns:
|
449
|
+
List of values
|
450
|
+
"""
|
451
|
+
value = os.environ.get(key, "")
|
452
|
+
|
453
|
+
if not value:
|
454
|
+
return default or []
|
455
|
+
|
456
|
+
return [item.strip() for item in value.split(separator) if item.strip()]
|
457
|
+
|
458
|
+
|
459
|
+
# ==============================================================================
|
460
|
+
# IMPORT UTILITIES
|
461
|
+
# ==============================================================================
|
462
|
+
|
463
|
+
|
464
|
+
def safe_import(module_name: str, fallback: Any = None) -> Any:
|
465
|
+
"""
|
466
|
+
Safely import a module with fallback.
|
467
|
+
|
468
|
+
Replaces 40+ duplicate import error handling patterns.
|
469
|
+
|
470
|
+
Args:
|
471
|
+
module_name: Module to import
|
472
|
+
fallback: Fallback value if import fails
|
473
|
+
|
474
|
+
Returns:
|
475
|
+
Imported module or fallback
|
476
|
+
"""
|
477
|
+
try:
|
478
|
+
import importlib
|
479
|
+
return importlib.import_module(module_name)
|
480
|
+
except ImportError as e:
|
481
|
+
logger.debug(f"Could not import {module_name}: {e}")
|
482
|
+
return fallback
|
483
|
+
except Exception as e:
|
484
|
+
logger.error(f"Error importing {module_name}: {e}")
|
485
|
+
return fallback
|
486
|
+
|
487
|
+
|
488
|
+
def import_from_string(import_path: str, fallback: Any = None) -> Any:
|
489
|
+
"""
|
490
|
+
Import a class or function from a string path.
|
491
|
+
|
492
|
+
Args:
|
493
|
+
import_path: Full import path (e.g., "package.module.ClassName")
|
494
|
+
fallback: Fallback value if import fails
|
495
|
+
|
496
|
+
Returns:
|
497
|
+
Imported object or fallback
|
498
|
+
"""
|
499
|
+
try:
|
500
|
+
module_path, attr_name = import_path.rsplit(".", 1)
|
501
|
+
module = safe_import(module_path)
|
502
|
+
|
503
|
+
if module is None:
|
504
|
+
return fallback
|
505
|
+
|
506
|
+
return getattr(module, attr_name, fallback)
|
507
|
+
except (ValueError, AttributeError) as e:
|
508
|
+
logger.debug(f"Could not import {import_path}: {e}")
|
509
|
+
return fallback
|
510
|
+
|
511
|
+
|
512
|
+
# ==============================================================================
|
513
|
+
# DEPRECATION WARNINGS
|
514
|
+
# ==============================================================================
|
515
|
+
|
516
|
+
|
517
|
+
def deprecated(replacement: str = None):
|
518
|
+
"""
|
519
|
+
Decorator to mark functions as deprecated.
|
520
|
+
|
521
|
+
Args:
|
522
|
+
replacement: Suggested replacement function
|
523
|
+
|
524
|
+
Returns:
|
525
|
+
Decorated function
|
526
|
+
"""
|
527
|
+
def decorator(func):
|
528
|
+
def wrapper(*args, **kwargs):
|
529
|
+
import warnings
|
530
|
+
|
531
|
+
message = f"{func.__name__} is deprecated"
|
532
|
+
if replacement:
|
533
|
+
message += f", use {replacement} instead"
|
534
|
+
|
535
|
+
warnings.warn(message, DeprecationWarning, stacklevel=2)
|
536
|
+
return func(*args, **kwargs)
|
537
|
+
|
538
|
+
wrapper.__name__ = func.__name__
|
539
|
+
wrapper.__doc__ = func.__doc__
|
540
|
+
return wrapper
|
541
|
+
|
542
|
+
return decorator
|