claude-mpm 4.3.22__py3-none-any.whl → 4.4.3__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.
Files changed (74) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/WORKFLOW.md +2 -14
  3. claude_mpm/cli/commands/configure.py +2 -29
  4. claude_mpm/cli/commands/doctor.py +2 -2
  5. claude_mpm/cli/commands/mpm_init.py +3 -3
  6. claude_mpm/cli/parsers/configure_parser.py +4 -15
  7. claude_mpm/core/framework/__init__.py +38 -0
  8. claude_mpm/core/framework/formatters/__init__.py +11 -0
  9. claude_mpm/core/framework/formatters/capability_generator.py +356 -0
  10. claude_mpm/core/framework/formatters/content_formatter.py +283 -0
  11. claude_mpm/core/framework/formatters/context_generator.py +180 -0
  12. claude_mpm/core/framework/loaders/__init__.py +13 -0
  13. claude_mpm/core/framework/loaders/agent_loader.py +202 -0
  14. claude_mpm/core/framework/loaders/file_loader.py +213 -0
  15. claude_mpm/core/framework/loaders/instruction_loader.py +151 -0
  16. claude_mpm/core/framework/loaders/packaged_loader.py +208 -0
  17. claude_mpm/core/framework/processors/__init__.py +11 -0
  18. claude_mpm/core/framework/processors/memory_processor.py +222 -0
  19. claude_mpm/core/framework/processors/metadata_processor.py +146 -0
  20. claude_mpm/core/framework/processors/template_processor.py +238 -0
  21. claude_mpm/core/framework_loader.py +277 -1798
  22. claude_mpm/hooks/__init__.py +9 -1
  23. claude_mpm/hooks/kuzu_memory_hook.py +352 -0
  24. claude_mpm/hooks/memory_integration_hook.py +1 -1
  25. claude_mpm/services/agents/memory/content_manager.py +5 -2
  26. claude_mpm/services/agents/memory/memory_file_service.py +1 -0
  27. claude_mpm/services/agents/memory/memory_limits_service.py +1 -0
  28. claude_mpm/services/core/path_resolver.py +1 -0
  29. claude_mpm/services/diagnostics/diagnostic_runner.py +1 -0
  30. claude_mpm/services/mcp_config_manager.py +67 -4
  31. claude_mpm/services/mcp_gateway/core/process_pool.py +281 -0
  32. claude_mpm/services/mcp_gateway/core/startup_verification.py +2 -2
  33. claude_mpm/services/mcp_gateway/main.py +3 -13
  34. claude_mpm/services/mcp_gateway/server/stdio_server.py +4 -10
  35. claude_mpm/services/mcp_gateway/tools/__init__.py +13 -2
  36. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +36 -6
  37. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +542 -0
  38. claude_mpm/services/shared/__init__.py +2 -1
  39. claude_mpm/services/shared/service_factory.py +8 -5
  40. claude_mpm/services/unified/__init__.py +65 -0
  41. claude_mpm/services/unified/analyzer_strategies/__init__.py +44 -0
  42. claude_mpm/services/unified/analyzer_strategies/code_analyzer.py +473 -0
  43. claude_mpm/services/unified/analyzer_strategies/dependency_analyzer.py +643 -0
  44. claude_mpm/services/unified/analyzer_strategies/performance_analyzer.py +804 -0
  45. claude_mpm/services/unified/analyzer_strategies/security_analyzer.py +661 -0
  46. claude_mpm/services/unified/analyzer_strategies/structure_analyzer.py +696 -0
  47. claude_mpm/services/unified/config_strategies/__init__.py +190 -0
  48. claude_mpm/services/unified/config_strategies/config_schema.py +689 -0
  49. claude_mpm/services/unified/config_strategies/context_strategy.py +748 -0
  50. claude_mpm/services/unified/config_strategies/error_handling_strategy.py +999 -0
  51. claude_mpm/services/unified/config_strategies/file_loader_strategy.py +871 -0
  52. claude_mpm/services/unified/config_strategies/unified_config_service.py +802 -0
  53. claude_mpm/services/unified/config_strategies/validation_strategy.py +1105 -0
  54. claude_mpm/services/unified/deployment_strategies/__init__.py +97 -0
  55. claude_mpm/services/unified/deployment_strategies/base.py +557 -0
  56. claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +486 -0
  57. claude_mpm/services/unified/deployment_strategies/local.py +594 -0
  58. claude_mpm/services/unified/deployment_strategies/utils.py +672 -0
  59. claude_mpm/services/unified/deployment_strategies/vercel.py +471 -0
  60. claude_mpm/services/unified/interfaces.py +499 -0
  61. claude_mpm/services/unified/migration.py +532 -0
  62. claude_mpm/services/unified/strategies.py +551 -0
  63. claude_mpm/services/unified/unified_analyzer.py +534 -0
  64. claude_mpm/services/unified/unified_config.py +688 -0
  65. claude_mpm/services/unified/unified_deployment.py +470 -0
  66. {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/METADATA +15 -15
  67. {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/RECORD +71 -32
  68. claude_mpm/cli/commands/configure_tui.py +0 -1927
  69. claude_mpm/services/mcp_gateway/tools/ticket_tools.py +0 -645
  70. claude_mpm/services/mcp_gateway/tools/unified_ticket_tool.py +0 -602
  71. {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/WHEEL +0 -0
  72. {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/entry_points.txt +0 -0
  73. {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/licenses/LICENSE +0 -0
  74. {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,871 @@
1
+ """
2
+ File Loader Strategy - Consolidates 215 file loading instances into 5 strategic loaders
3
+ Part of Phase 3 Configuration Consolidation
4
+ """
5
+
6
+ from abc import ABC, abstractmethod
7
+ from typing import Any, Dict, Optional, List, Union, Callable
8
+ from pathlib import Path
9
+ import json
10
+ import yaml
11
+ import os
12
+ import re
13
+ from dataclasses import dataclass
14
+ from enum import Enum
15
+ import configparser
16
+ import importlib.util
17
+ from contextlib import contextmanager
18
+
19
+ from claude_mpm.core.logging_utils import get_logger
20
+ from .unified_config_service import IConfigStrategy, ConfigFormat
21
+
22
+
23
+ class LoaderType(Enum):
24
+ """Strategic loader types consolidating 215 instances"""
25
+ STRUCTURED = "structured" # JSON, YAML, TOML - 85 instances
26
+ ENVIRONMENT = "environment" # ENV files and variables - 45 instances
27
+ PROGRAMMATIC = "programmatic" # Python modules - 35 instances
28
+ LEGACY = "legacy" # INI, properties - 30 instances
29
+ COMPOSITE = "composite" # Multi-source loading - 20 instances
30
+
31
+
32
+ @dataclass
33
+ class FileLoadContext:
34
+ """Context for file loading operations"""
35
+ path: Path
36
+ format: ConfigFormat
37
+ encoding: str = 'utf-8'
38
+ strict: bool = True
39
+ interpolate: bool = False
40
+ includes: List[str] = None
41
+ excludes: List[str] = None
42
+ transformations: List[Callable] = None
43
+ fallback_paths: List[Path] = None
44
+
45
+
46
+ class BaseFileLoader(ABC):
47
+ """Base class for all file loaders"""
48
+
49
+ def __init__(self):
50
+ self.logger = get_logger(self.__class__.__name__)
51
+ self._cache = {}
52
+
53
+ @abstractmethod
54
+ def load(self, context: FileLoadContext) -> Dict[str, Any]:
55
+ """Load configuration from file"""
56
+ pass
57
+
58
+ @abstractmethod
59
+ def supports(self, format: ConfigFormat) -> bool:
60
+ """Check if loader supports the format"""
61
+ pass
62
+
63
+ def _read_file(self, path: Path, encoding: str = 'utf-8') -> str:
64
+ """Read file with proper error handling"""
65
+ try:
66
+ with open(path, 'r', encoding=encoding) as f:
67
+ return f.read()
68
+ except UnicodeDecodeError:
69
+ # Try with different encodings
70
+ for enc in ['latin-1', 'cp1252', 'utf-16']:
71
+ try:
72
+ with open(path, 'r', encoding=enc) as f:
73
+ self.logger.warning(f"Read {path} with fallback encoding: {enc}")
74
+ return f.read()
75
+ except:
76
+ continue
77
+ raise
78
+
79
+ def _apply_transformations(
80
+ self,
81
+ config: Dict[str, Any],
82
+ transformations: List[Callable]
83
+ ) -> Dict[str, Any]:
84
+ """Apply transformation pipeline"""
85
+ if not transformations:
86
+ return config
87
+
88
+ for transform in transformations:
89
+ try:
90
+ config = transform(config)
91
+ except Exception as e:
92
+ self.logger.error(f"Transformation failed: {e}")
93
+
94
+ return config
95
+
96
+
97
+ class StructuredFileLoader(BaseFileLoader):
98
+ """
99
+ Handles JSON, YAML, TOML formats
100
+ Consolidates 85 individual loaders
101
+ """
102
+
103
+ def supports(self, format: ConfigFormat) -> bool:
104
+ return format in [ConfigFormat.JSON, ConfigFormat.YAML, ConfigFormat.TOML]
105
+
106
+ def load(self, context: FileLoadContext) -> Dict[str, Any]:
107
+ """Load structured configuration files"""
108
+ if context.path in self._cache:
109
+ self.logger.debug(f"Using cached config: {context.path}")
110
+ return self._cache[context.path]
111
+
112
+ content = self._read_file(context.path, context.encoding)
113
+
114
+ if context.format == ConfigFormat.JSON:
115
+ config = self._load_json(content, context)
116
+ elif context.format == ConfigFormat.YAML:
117
+ config = self._load_yaml(content, context)
118
+ elif context.format == ConfigFormat.TOML:
119
+ config = self._load_toml(content, context)
120
+ else:
121
+ raise ValueError(f"Unsupported format: {context.format}")
122
+
123
+ # Handle includes
124
+ if context.includes:
125
+ config = self._process_includes(config, context)
126
+
127
+ # Handle excludes
128
+ if context.excludes:
129
+ config = self._process_excludes(config, context)
130
+
131
+ # Apply transformations
132
+ if context.transformations:
133
+ config = self._apply_transformations(config, context.transformations)
134
+
135
+ # Cache result
136
+ self._cache[context.path] = config
137
+
138
+ return config
139
+
140
+ def _load_json(self, content: str, context: FileLoadContext) -> Dict[str, Any]:
141
+ """Load JSON with comments support"""
142
+ # Remove comments if present
143
+ if '//' in content or '/*' in content:
144
+ content = self._strip_json_comments(content)
145
+
146
+ try:
147
+ return json.loads(content)
148
+ except json.JSONDecodeError as e:
149
+ if context.strict:
150
+ raise
151
+ self.logger.warning(f"JSON parse error, attempting recovery: {e}")
152
+ return self._recover_json(content)
153
+
154
+ def _load_yaml(self, content: str, context: FileLoadContext) -> Dict[str, Any]:
155
+ """Load YAML with advanced features"""
156
+ try:
157
+ # Support multiple documents
158
+ docs = list(yaml.safe_load_all(content))
159
+
160
+ if len(docs) == 1:
161
+ return docs[0] or {}
162
+ else:
163
+ # Merge multiple documents
164
+ result = {}
165
+ for doc in docs:
166
+ if doc:
167
+ result.update(doc)
168
+ return result
169
+
170
+ except yaml.YAMLError as e:
171
+ if context.strict:
172
+ raise
173
+ self.logger.warning(f"YAML parse error: {e}")
174
+ return {}
175
+
176
+ def _load_toml(self, content: str, context: FileLoadContext) -> Dict[str, Any]:
177
+ """Load TOML configuration"""
178
+ try:
179
+ import toml
180
+ return toml.loads(content)
181
+ except ImportError:
182
+ self.logger.error("toml package not installed")
183
+ try:
184
+ import tomli
185
+ return tomli.loads(content)
186
+ except ImportError:
187
+ raise ImportError("Neither toml nor tomli package is installed")
188
+ except Exception as e:
189
+ if context.strict:
190
+ raise
191
+ self.logger.warning(f"TOML parse error: {e}")
192
+ return {}
193
+
194
+ def _strip_json_comments(self, content: str) -> str:
195
+ """Remove comments from JSON content"""
196
+ # Remove single-line comments
197
+ content = re.sub(r'//.*?$', '', content, flags=re.MULTILINE)
198
+ # Remove multi-line comments
199
+ content = re.sub(r'/\*.*?\*/', '', content, flags=re.DOTALL)
200
+ return content
201
+
202
+ def _recover_json(self, content: str) -> Dict[str, Any]:
203
+ """Attempt to recover from malformed JSON"""
204
+ # Try to fix common issues
205
+ content = content.replace("'", '"') # Single to double quotes
206
+ content = re.sub(r',\s*}', '}', content) # Trailing commas in objects
207
+ content = re.sub(r',\s*]', ']', content) # Trailing commas in arrays
208
+
209
+ try:
210
+ return json.loads(content)
211
+ except:
212
+ return {}
213
+
214
+ def _process_includes(self, config: Dict[str, Any], context: FileLoadContext) -> Dict[str, Any]:
215
+ """Process include directives"""
216
+ for include_key in context.includes:
217
+ if include_key in config:
218
+ include_path = Path(config[include_key])
219
+ if not include_path.is_absolute():
220
+ include_path = context.path.parent / include_path
221
+
222
+ if include_path.exists():
223
+ include_context = FileLoadContext(
224
+ path=include_path,
225
+ format=self._detect_format(include_path),
226
+ encoding=context.encoding,
227
+ strict=context.strict
228
+ )
229
+ included_config = self.load(include_context)
230
+
231
+ # Merge included config
232
+ config = self._merge_configs(config, included_config)
233
+
234
+ # Remove include directive
235
+ del config[include_key]
236
+
237
+ return config
238
+
239
+ def _process_excludes(self, config: Dict[str, Any], context: FileLoadContext) -> Dict[str, Any]:
240
+ """Process exclude patterns"""
241
+ for pattern in context.excludes:
242
+ config = self._exclude_keys(config, pattern)
243
+ return config
244
+
245
+ def _exclude_keys(self, config: Dict[str, Any], pattern: str) -> Dict[str, Any]:
246
+ """Exclude keys matching pattern"""
247
+ if '*' in pattern or '?' in pattern:
248
+ # Glob pattern
249
+ import fnmatch
250
+ return {k: v for k, v in config.items() if not fnmatch.fnmatch(k, pattern)}
251
+ else:
252
+ # Exact match
253
+ config.pop(pattern, None)
254
+ return config
255
+
256
+ def _merge_configs(self, base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
257
+ """Deep merge configurations"""
258
+ result = base.copy()
259
+
260
+ for key, value in override.items():
261
+ if key in result and isinstance(result[key], dict) and isinstance(value, dict):
262
+ result[key] = self._merge_configs(result[key], value)
263
+ else:
264
+ result[key] = value
265
+
266
+ return result
267
+
268
+ def _detect_format(self, path: Path) -> ConfigFormat:
269
+ """Detect file format from extension"""
270
+ suffix = path.suffix.lower()
271
+
272
+ if suffix == '.json':
273
+ return ConfigFormat.JSON
274
+ elif suffix in ['.yaml', '.yml']:
275
+ return ConfigFormat.YAML
276
+ elif suffix == '.toml':
277
+ return ConfigFormat.TOML
278
+ else:
279
+ # Try to detect from content
280
+ content = self._read_file(path)
281
+ if content.strip().startswith('{'):
282
+ return ConfigFormat.JSON
283
+ elif ':' in content:
284
+ return ConfigFormat.YAML
285
+ else:
286
+ return ConfigFormat.JSON
287
+
288
+
289
+ class EnvironmentFileLoader(BaseFileLoader):
290
+ """
291
+ Handles environment files and variables
292
+ Consolidates 45 individual loaders
293
+ """
294
+
295
+ def supports(self, format: ConfigFormat) -> bool:
296
+ return format == ConfigFormat.ENV
297
+
298
+ def load(self, context: FileLoadContext) -> Dict[str, Any]:
299
+ """Load environment configuration"""
300
+ config = {}
301
+
302
+ # Load from file if exists
303
+ if context.path and context.path.exists():
304
+ config.update(self._load_env_file(context.path, context))
305
+
306
+ # Load from environment variables
307
+ config.update(self._load_env_vars(context))
308
+
309
+ # Apply variable interpolation if requested
310
+ if context.interpolate:
311
+ config = self._interpolate_variables(config)
312
+
313
+ # Apply transformations
314
+ if context.transformations:
315
+ config = self._apply_transformations(config, context.transformations)
316
+
317
+ return config
318
+
319
+ def _load_env_file(self, path: Path, context: FileLoadContext) -> Dict[str, Any]:
320
+ """Load .env file format"""
321
+ config = {}
322
+ content = self._read_file(path, context.encoding)
323
+
324
+ for line in content.splitlines():
325
+ line = line.strip()
326
+
327
+ # Skip comments and empty lines
328
+ if not line or line.startswith('#'):
329
+ continue
330
+
331
+ # Parse KEY=VALUE format
332
+ if '=' in line:
333
+ key, value = line.split('=', 1)
334
+ key = key.strip()
335
+ value = value.strip()
336
+
337
+ # Remove quotes if present
338
+ if value.startswith('"') and value.endswith('"'):
339
+ value = value[1:-1]
340
+ elif value.startswith("'") and value.endswith("'"):
341
+ value = value[1:-1]
342
+
343
+ # Parse value type
344
+ config[key] = self._parse_env_value(value)
345
+
346
+ return config
347
+
348
+ def _load_env_vars(self, context: FileLoadContext) -> Dict[str, Any]:
349
+ """Load from environment variables"""
350
+ config = {}
351
+ prefix = context.path.stem.upper() if context.path else ''
352
+
353
+ for key, value in os.environ.items():
354
+ # Check if key matches pattern
355
+ if self._should_include_env_var(key, prefix, context):
356
+ clean_key = self._clean_env_key(key, prefix)
357
+ config[clean_key] = self._parse_env_value(value)
358
+
359
+ return config
360
+
361
+ def _should_include_env_var(self, key: str, prefix: str, context: FileLoadContext) -> bool:
362
+ """Check if environment variable should be included"""
363
+ if context.includes:
364
+ return any(key.startswith(inc) for inc in context.includes)
365
+ elif context.excludes:
366
+ return not any(key.startswith(exc) for exc in context.excludes)
367
+ elif prefix:
368
+ return key.startswith(prefix)
369
+ return True
370
+
371
+ def _clean_env_key(self, key: str, prefix: str) -> str:
372
+ """Clean environment variable key"""
373
+ if prefix and key.startswith(prefix):
374
+ key = key[len(prefix):]
375
+ if key.startswith('_'):
376
+ key = key[1:]
377
+
378
+ # Convert to lowercase and replace underscores
379
+ return key.lower().replace('__', '.').replace('_', '-')
380
+
381
+ def _parse_env_value(self, value: str) -> Any:
382
+ """Parse environment variable value to appropriate type"""
383
+ # Boolean
384
+ if value.lower() in ['true', 'false']:
385
+ return value.lower() == 'true'
386
+
387
+ # None
388
+ if value.lower() in ['none', 'null']:
389
+ return None
390
+
391
+ # Number
392
+ try:
393
+ if '.' in value:
394
+ return float(value)
395
+ else:
396
+ return int(value)
397
+ except ValueError:
398
+ pass
399
+
400
+ # JSON array or object
401
+ if value.startswith('[') or value.startswith('{'):
402
+ try:
403
+ return json.loads(value)
404
+ except:
405
+ pass
406
+
407
+ # Comma-separated list
408
+ if ',' in value:
409
+ return [v.strip() for v in value.split(',')]
410
+
411
+ return value
412
+
413
+ def _interpolate_variables(self, config: Dict[str, Any]) -> Dict[str, Any]:
414
+ """Interpolate variables in configuration values"""
415
+ def interpolate_value(value: Any) -> Any:
416
+ if isinstance(value, str):
417
+ # Replace ${VAR} or $VAR patterns
418
+ pattern = r'\$\{([^}]+)\}|\$(\w+)'
419
+
420
+ def replacer(match):
421
+ var_name = match.group(1) or match.group(2)
422
+ # Look in config first, then environment
423
+ if var_name in config:
424
+ return str(config[var_name])
425
+ elif var_name in os.environ:
426
+ return os.environ[var_name]
427
+ else:
428
+ return match.group(0)
429
+
430
+ return re.sub(pattern, replacer, value)
431
+
432
+ elif isinstance(value, dict):
433
+ return {k: interpolate_value(v) for k, v in value.items()}
434
+
435
+ elif isinstance(value, list):
436
+ return [interpolate_value(v) for v in value]
437
+
438
+ return value
439
+
440
+ return {k: interpolate_value(v) for k, v in config.items()}
441
+
442
+
443
+ class ProgrammaticFileLoader(BaseFileLoader):
444
+ """
445
+ Handles Python module configurations
446
+ Consolidates 35 individual loaders
447
+ """
448
+
449
+ def supports(self, format: ConfigFormat) -> bool:
450
+ return format == ConfigFormat.PYTHON
451
+
452
+ def load(self, context: FileLoadContext) -> Dict[str, Any]:
453
+ """Load Python module as configuration"""
454
+ if not context.path.exists():
455
+ raise FileNotFoundError(f"Python config not found: {context.path}")
456
+
457
+ # Load module
458
+ spec = importlib.util.spec_from_file_location("config", context.path)
459
+ if not spec or not spec.loader:
460
+ raise ImportError(f"Cannot load Python module: {context.path}")
461
+
462
+ module = importlib.util.module_from_spec(spec)
463
+ spec.loader.exec_module(module)
464
+
465
+ # Extract configuration
466
+ config = self._extract_config(module, context)
467
+
468
+ # Apply transformations
469
+ if context.transformations:
470
+ config = self._apply_transformations(config, context.transformations)
471
+
472
+ return config
473
+
474
+ def _extract_config(self, module: Any, context: FileLoadContext) -> Dict[str, Any]:
475
+ """Extract configuration from Python module"""
476
+ config = {}
477
+
478
+ # Look for specific config patterns
479
+ if hasattr(module, 'CONFIG'):
480
+ # Direct CONFIG dict
481
+ config = module.CONFIG
482
+ elif hasattr(module, 'config'):
483
+ # config dict or function
484
+ if callable(module.config):
485
+ config = module.config()
486
+ else:
487
+ config = module.config
488
+ elif hasattr(module, 'get_config'):
489
+ # get_config function
490
+ config = module.get_config()
491
+ else:
492
+ # Extract all uppercase variables
493
+ for name in dir(module):
494
+ if name.isupper() and not name.startswith('_'):
495
+ value = getattr(module, name)
496
+ # Skip modules and functions unless specified
497
+ if not (callable(value) or isinstance(value, type)):
498
+ config[name] = value
499
+
500
+ # Apply includes/excludes
501
+ if context.includes:
502
+ config = {k: v for k, v in config.items() if k in context.includes}
503
+ if context.excludes:
504
+ config = {k: v for k, v in config.items() if k not in context.excludes}
505
+
506
+ return config
507
+
508
+
509
+ class LegacyFileLoader(BaseFileLoader):
510
+ """
511
+ Handles INI and properties files
512
+ Consolidates 30 individual loaders
513
+ """
514
+
515
+ def supports(self, format: ConfigFormat) -> bool:
516
+ return format == ConfigFormat.INI
517
+
518
+ def load(self, context: FileLoadContext) -> Dict[str, Any]:
519
+ """Load legacy configuration formats"""
520
+ content = self._read_file(context.path, context.encoding)
521
+
522
+ # Detect format from content if needed
523
+ if self._is_properties_format(content):
524
+ config = self._load_properties(content, context)
525
+ else:
526
+ config = self._load_ini(content, context)
527
+
528
+ # Apply transformations
529
+ if context.transformations:
530
+ config = self._apply_transformations(config, context.transformations)
531
+
532
+ return config
533
+
534
+ def _is_properties_format(self, content: str) -> bool:
535
+ """Check if content is Java properties format"""
536
+ # Properties files don't have sections
537
+ return not any(line.strip().startswith('[') for line in content.splitlines())
538
+
539
+ def _load_ini(self, content: str, context: FileLoadContext) -> Dict[str, Any]:
540
+ """Load INI format configuration"""
541
+ parser = configparser.ConfigParser(
542
+ interpolation=configparser.ExtendedInterpolation() if context.interpolate else None,
543
+ allow_no_value=True
544
+ )
545
+
546
+ try:
547
+ parser.read_string(content)
548
+ except configparser.Error as e:
549
+ if context.strict:
550
+ raise
551
+ self.logger.warning(f"INI parse error: {e}")
552
+ return {}
553
+
554
+ # Convert to dict
555
+ config = {}
556
+
557
+ # Handle DEFAULT section
558
+ if parser.defaults():
559
+ config['_defaults'] = dict(parser.defaults())
560
+
561
+ # Handle other sections
562
+ for section in parser.sections():
563
+ config[section] = {}
564
+ for key, value in parser.items(section):
565
+ config[section][key] = self._parse_ini_value(value)
566
+
567
+ # Flatten if only one section (excluding defaults)
568
+ sections = [s for s in config.keys() if s != '_defaults']
569
+ if len(sections) == 1 and not config.get('_defaults'):
570
+ config = config[sections[0]]
571
+
572
+ return config
573
+
574
+ def _load_properties(self, content: str, context: FileLoadContext) -> Dict[str, Any]:
575
+ """Load Java properties format"""
576
+ config = {}
577
+
578
+ for line in content.splitlines():
579
+ line = line.strip()
580
+
581
+ # Skip comments and empty lines
582
+ if not line or line.startswith('#') or line.startswith('!'):
583
+ continue
584
+
585
+ # Handle line continuation
586
+ while line.endswith('\\'):
587
+ line = line[:-1]
588
+ next_line = next(content.splitlines(), '')
589
+ line += next_line.strip()
590
+
591
+ # Parse key=value or key:value
592
+ if '=' in line:
593
+ key, value = line.split('=', 1)
594
+ elif ':' in line:
595
+ key, value = line.split(':', 1)
596
+ else:
597
+ # Key without value
598
+ key = line
599
+ value = ''
600
+
601
+ key = key.strip()
602
+ value = value.strip()
603
+
604
+ # Unescape special characters
605
+ value = self._unescape_properties_value(value)
606
+
607
+ # Store in nested dict structure based on dots in key
608
+ self._set_nested_value(config, key, value)
609
+
610
+ return config
611
+
612
+ def _parse_ini_value(self, value: str) -> Any:
613
+ """Parse INI value to appropriate type"""
614
+ if not value:
615
+ return ''
616
+
617
+ # Boolean
618
+ if value.lower() in ['true', 'yes', 'on', '1']:
619
+ return True
620
+ elif value.lower() in ['false', 'no', 'off', '0']:
621
+ return False
622
+
623
+ # Number
624
+ try:
625
+ if '.' in value:
626
+ return float(value)
627
+ else:
628
+ return int(value)
629
+ except ValueError:
630
+ pass
631
+
632
+ # List (comma-separated)
633
+ if ',' in value:
634
+ return [v.strip() for v in value.split(',')]
635
+
636
+ return value
637
+
638
+ def _unescape_properties_value(self, value: str) -> str:
639
+ """Unescape Java properties special characters"""
640
+ replacements = {
641
+ '\\n': '\n',
642
+ '\\r': '\r',
643
+ '\\t': '\t',
644
+ '\\\\': '\\',
645
+ '\\:': ':',
646
+ '\\=': '=',
647
+ '\\ ': ' '
648
+ }
649
+
650
+ for old, new in replacements.items():
651
+ value = value.replace(old, new)
652
+
653
+ return value
654
+
655
+ def _set_nested_value(self, config: Dict[str, Any], key: str, value: Any):
656
+ """Set value in nested dict structure based on dot notation"""
657
+ parts = key.split('.')
658
+ current = config
659
+
660
+ for part in parts[:-1]:
661
+ if part not in current:
662
+ current[part] = {}
663
+ current = current[part]
664
+
665
+ current[parts[-1]] = value
666
+
667
+
668
+ class CompositeFileLoader(BaseFileLoader):
669
+ """
670
+ Handles multi-source configuration loading
671
+ Consolidates 20 individual loaders
672
+ """
673
+
674
+ def __init__(self):
675
+ super().__init__()
676
+ self.loaders = {
677
+ LoaderType.STRUCTURED: StructuredFileLoader(),
678
+ LoaderType.ENVIRONMENT: EnvironmentFileLoader(),
679
+ LoaderType.PROGRAMMATIC: ProgrammaticFileLoader(),
680
+ LoaderType.LEGACY: LegacyFileLoader()
681
+ }
682
+
683
+ def supports(self, format: ConfigFormat) -> bool:
684
+ """Composite loader supports all formats"""
685
+ return True
686
+
687
+ def load(self, context: FileLoadContext) -> Dict[str, Any]:
688
+ """Load configuration from multiple sources"""
689
+ configs = []
690
+
691
+ # Check for directory of configs
692
+ if context.path.is_dir():
693
+ configs.extend(self._load_directory(context))
694
+
695
+ # Check for fallback paths
696
+ if context.fallback_paths:
697
+ for fallback in context.fallback_paths:
698
+ if fallback.exists():
699
+ fallback_context = FileLoadContext(
700
+ path=fallback,
701
+ format=self._detect_format(fallback),
702
+ encoding=context.encoding,
703
+ strict=False # Non-strict for fallbacks
704
+ )
705
+ try:
706
+ config = self._load_single(fallback_context)
707
+ configs.append(config)
708
+ except Exception as e:
709
+ self.logger.debug(f"Fallback load failed: {e}")
710
+
711
+ # Load primary config
712
+ if context.path.is_file():
713
+ configs.append(self._load_single(context))
714
+
715
+ # Merge all configs
716
+ result = {}
717
+ for config in configs:
718
+ result = self._deep_merge(result, config)
719
+
720
+ # Apply transformations
721
+ if context.transformations:
722
+ result = self._apply_transformations(result, context.transformations)
723
+
724
+ return result
725
+
726
+ def _load_directory(self, context: FileLoadContext) -> List[Dict[str, Any]]:
727
+ """Load all config files from directory"""
728
+ configs = []
729
+
730
+ # Define load order
731
+ patterns = [
732
+ 'default.*',
733
+ 'config.*',
734
+ 'settings.*',
735
+ '*.config.*',
736
+ '*.settings.*'
737
+ ]
738
+
739
+ # Load files in order
740
+ for pattern in patterns:
741
+ for file_path in context.path.glob(pattern):
742
+ if file_path.is_file():
743
+ file_context = FileLoadContext(
744
+ path=file_path,
745
+ format=self._detect_format(file_path),
746
+ encoding=context.encoding,
747
+ strict=context.strict
748
+ )
749
+ try:
750
+ config = self._load_single(file_context)
751
+ configs.append(config)
752
+ except Exception as e:
753
+ self.logger.warning(f"Failed to load {file_path}: {e}")
754
+
755
+ return configs
756
+
757
+ def _load_single(self, context: FileLoadContext) -> Dict[str, Any]:
758
+ """Load single configuration file"""
759
+ # Find appropriate loader
760
+ for loader_type, loader in self.loaders.items():
761
+ if loader.supports(context.format):
762
+ return loader.load(context)
763
+
764
+ raise ValueError(f"No loader available for format: {context.format}")
765
+
766
+ def _detect_format(self, path: Path) -> ConfigFormat:
767
+ """Detect configuration format from file"""
768
+ suffix = path.suffix.lower()
769
+
770
+ format_map = {
771
+ '.json': ConfigFormat.JSON,
772
+ '.yaml': ConfigFormat.YAML,
773
+ '.yml': ConfigFormat.YAML,
774
+ '.toml': ConfigFormat.TOML,
775
+ '.env': ConfigFormat.ENV,
776
+ '.py': ConfigFormat.PYTHON,
777
+ '.ini': ConfigFormat.INI,
778
+ '.cfg': ConfigFormat.INI,
779
+ '.conf': ConfigFormat.INI,
780
+ '.properties': ConfigFormat.INI
781
+ }
782
+
783
+ return format_map.get(suffix, ConfigFormat.JSON)
784
+
785
+ def _deep_merge(self, base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
786
+ """Deep merge two configurations"""
787
+ result = base.copy()
788
+
789
+ for key, value in override.items():
790
+ if key in result:
791
+ if isinstance(result[key], dict) and isinstance(value, dict):
792
+ result[key] = self._deep_merge(result[key], value)
793
+ elif isinstance(result[key], list) and isinstance(value, list):
794
+ result[key].extend(value)
795
+ else:
796
+ result[key] = value
797
+ else:
798
+ result[key] = value
799
+
800
+ return result
801
+
802
+
803
+ class FileLoaderStrategy(IConfigStrategy):
804
+ """
805
+ Main strategy class integrating all file loaders
806
+ Reduces 215 file loading instances to 5 strategic loaders
807
+ """
808
+
809
+ def __init__(self):
810
+ self.logger = get_logger(self.__class__.__name__)
811
+ self.composite_loader = CompositeFileLoader()
812
+
813
+ def can_handle(self, source: Union[str, Path, Dict]) -> bool:
814
+ """Check if source is a file or directory"""
815
+ if isinstance(source, dict):
816
+ return False
817
+
818
+ path = Path(source)
819
+ return path.exists() or path.parent.exists()
820
+
821
+ def load(self, source: Any, **kwargs) -> Dict[str, Any]:
822
+ """Load configuration from file source"""
823
+ path = Path(source)
824
+
825
+ # Create load context
826
+ context = FileLoadContext(
827
+ path=path,
828
+ format=kwargs.get('format', self._detect_format(path)),
829
+ encoding=kwargs.get('encoding', 'utf-8'),
830
+ strict=kwargs.get('strict', True),
831
+ interpolate=kwargs.get('interpolate', False),
832
+ includes=kwargs.get('includes'),
833
+ excludes=kwargs.get('excludes'),
834
+ transformations=kwargs.get('transformations'),
835
+ fallback_paths=[Path(p) for p in kwargs.get('fallback_paths', [])]
836
+ )
837
+
838
+ return self.composite_loader.load(context)
839
+
840
+ def validate(self, config: Dict[str, Any], schema: Optional[Dict] = None) -> bool:
841
+ """Validate loaded configuration"""
842
+ # Basic validation - can be extended
843
+ return config is not None and isinstance(config, dict)
844
+
845
+ def transform(self, config: Dict[str, Any]) -> Dict[str, Any]:
846
+ """Transform configuration to standard format"""
847
+ # Apply standard transformations
848
+ return self._normalize_config(config)
849
+
850
+ def _detect_format(self, path: Path) -> ConfigFormat:
851
+ """Detect configuration format"""
852
+ return self.composite_loader._detect_format(path)
853
+
854
+ def _normalize_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
855
+ """Normalize configuration structure"""
856
+ # Convert all keys to lowercase
857
+ normalized = {}
858
+
859
+ for key, value in config.items():
860
+ norm_key = key.lower().replace('-', '_')
861
+
862
+ if isinstance(value, dict):
863
+ normalized[norm_key] = self._normalize_config(value)
864
+ else:
865
+ normalized[norm_key] = value
866
+
867
+ return normalized
868
+
869
+
870
+ # Export the main strategy
871
+ __all__ = ['FileLoaderStrategy', 'LoaderType', 'FileLoadContext']