claude-mpm 4.4.3__py3-none-any.whl → 4.4.4__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 (118) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/agent_loader.py +3 -2
  3. claude_mpm/agents/agent_loader_integration.py +2 -1
  4. claude_mpm/agents/async_agent_loader.py +2 -2
  5. claude_mpm/agents/base_agent_loader.py +2 -2
  6. claude_mpm/agents/frontmatter_validator.py +1 -0
  7. claude_mpm/agents/system_agent_config.py +2 -1
  8. claude_mpm/cli/commands/doctor.py +44 -5
  9. claude_mpm/cli/commands/mpm_init.py +116 -62
  10. claude_mpm/cli/parsers/configure_parser.py +3 -1
  11. claude_mpm/cli/startup_logging.py +1 -3
  12. claude_mpm/config/agent_config.py +1 -1
  13. claude_mpm/config/paths.py +2 -1
  14. claude_mpm/core/agent_name_normalizer.py +1 -0
  15. claude_mpm/core/config.py +2 -1
  16. claude_mpm/core/config_aliases.py +2 -1
  17. claude_mpm/core/file_utils.py +0 -1
  18. claude_mpm/core/framework/__init__.py +6 -6
  19. claude_mpm/core/framework/formatters/__init__.py +2 -2
  20. claude_mpm/core/framework/formatters/capability_generator.py +19 -8
  21. claude_mpm/core/framework/formatters/content_formatter.py +8 -3
  22. claude_mpm/core/framework/formatters/context_generator.py +7 -3
  23. claude_mpm/core/framework/loaders/__init__.py +3 -3
  24. claude_mpm/core/framework/loaders/agent_loader.py +7 -3
  25. claude_mpm/core/framework/loaders/file_loader.py +16 -6
  26. claude_mpm/core/framework/loaders/instruction_loader.py +16 -6
  27. claude_mpm/core/framework/loaders/packaged_loader.py +36 -12
  28. claude_mpm/core/framework/processors/__init__.py +2 -2
  29. claude_mpm/core/framework/processors/memory_processor.py +14 -6
  30. claude_mpm/core/framework/processors/metadata_processor.py +5 -5
  31. claude_mpm/core/framework/processors/template_processor.py +12 -6
  32. claude_mpm/core/framework_loader.py +44 -20
  33. claude_mpm/core/log_manager.py +2 -1
  34. claude_mpm/core/tool_access_control.py +1 -0
  35. claude_mpm/core/unified_agent_registry.py +2 -1
  36. claude_mpm/core/unified_paths.py +1 -0
  37. claude_mpm/experimental/cli_enhancements.py +1 -0
  38. claude_mpm/hooks/base_hook.py +1 -0
  39. claude_mpm/hooks/instruction_reinforcement.py +1 -0
  40. claude_mpm/hooks/kuzu_memory_hook.py +20 -13
  41. claude_mpm/hooks/validation_hooks.py +1 -1
  42. claude_mpm/scripts/mpm_doctor.py +1 -0
  43. claude_mpm/services/agents/loading/agent_profile_loader.py +1 -1
  44. claude_mpm/services/agents/loading/base_agent_manager.py +1 -1
  45. claude_mpm/services/agents/loading/framework_agent_loader.py +1 -1
  46. claude_mpm/services/agents/management/agent_capabilities_generator.py +1 -0
  47. claude_mpm/services/agents/management/agent_management_service.py +1 -1
  48. claude_mpm/services/agents/memory/memory_categorization_service.py +0 -1
  49. claude_mpm/services/agents/memory/memory_file_service.py +6 -2
  50. claude_mpm/services/agents/memory/memory_format_service.py +0 -1
  51. claude_mpm/services/agents/registry/deployed_agent_discovery.py +1 -1
  52. claude_mpm/services/async_session_logger.py +1 -1
  53. claude_mpm/services/claude_session_logger.py +1 -0
  54. claude_mpm/services/core/path_resolver.py +1 -0
  55. claude_mpm/services/diagnostics/checks/__init__.py +2 -0
  56. claude_mpm/services/diagnostics/checks/installation_check.py +126 -25
  57. claude_mpm/services/diagnostics/checks/mcp_services_check.py +399 -0
  58. claude_mpm/services/diagnostics/diagnostic_runner.py +3 -0
  59. claude_mpm/services/diagnostics/doctor_reporter.py +259 -32
  60. claude_mpm/services/event_bus/direct_relay.py +2 -1
  61. claude_mpm/services/event_bus/event_bus.py +1 -0
  62. claude_mpm/services/event_bus/relay.py +3 -2
  63. claude_mpm/services/framework_claude_md_generator/content_assembler.py +1 -1
  64. claude_mpm/services/infrastructure/daemon_manager.py +1 -1
  65. claude_mpm/services/mcp_config_manager.py +10 -10
  66. claude_mpm/services/mcp_gateway/core/process_pool.py +62 -23
  67. claude_mpm/services/mcp_gateway/tools/__init__.py +6 -5
  68. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +3 -1
  69. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +16 -31
  70. claude_mpm/services/memory/cache/simple_cache.py +1 -1
  71. claude_mpm/services/project/archive_manager.py +159 -96
  72. claude_mpm/services/project/documentation_manager.py +64 -45
  73. claude_mpm/services/project/enhanced_analyzer.py +132 -89
  74. claude_mpm/services/project/project_organizer.py +225 -131
  75. claude_mpm/services/response_tracker.py +1 -1
  76. claude_mpm/services/socketio/server/eventbus_integration.py +1 -1
  77. claude_mpm/services/unified/__init__.py +1 -1
  78. claude_mpm/services/unified/analyzer_strategies/__init__.py +3 -3
  79. claude_mpm/services/unified/analyzer_strategies/code_analyzer.py +97 -53
  80. claude_mpm/services/unified/analyzer_strategies/dependency_analyzer.py +81 -40
  81. claude_mpm/services/unified/analyzer_strategies/performance_analyzer.py +277 -178
  82. claude_mpm/services/unified/analyzer_strategies/security_analyzer.py +196 -112
  83. claude_mpm/services/unified/analyzer_strategies/structure_analyzer.py +83 -49
  84. claude_mpm/services/unified/config_strategies/__init__.py +111 -126
  85. claude_mpm/services/unified/config_strategies/config_schema.py +157 -111
  86. claude_mpm/services/unified/config_strategies/context_strategy.py +91 -89
  87. claude_mpm/services/unified/config_strategies/error_handling_strategy.py +183 -173
  88. claude_mpm/services/unified/config_strategies/file_loader_strategy.py +160 -152
  89. claude_mpm/services/unified/config_strategies/unified_config_service.py +124 -112
  90. claude_mpm/services/unified/config_strategies/validation_strategy.py +298 -259
  91. claude_mpm/services/unified/deployment_strategies/__init__.py +7 -7
  92. claude_mpm/services/unified/deployment_strategies/base.py +24 -28
  93. claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +168 -88
  94. claude_mpm/services/unified/deployment_strategies/local.py +49 -34
  95. claude_mpm/services/unified/deployment_strategies/utils.py +39 -43
  96. claude_mpm/services/unified/deployment_strategies/vercel.py +30 -24
  97. claude_mpm/services/unified/interfaces.py +0 -26
  98. claude_mpm/services/unified/migration.py +17 -40
  99. claude_mpm/services/unified/strategies.py +9 -26
  100. claude_mpm/services/unified/unified_analyzer.py +48 -44
  101. claude_mpm/services/unified/unified_config.py +21 -19
  102. claude_mpm/services/unified/unified_deployment.py +21 -26
  103. claude_mpm/storage/state_storage.py +1 -0
  104. claude_mpm/utils/agent_dependency_loader.py +18 -6
  105. claude_mpm/utils/common.py +14 -12
  106. claude_mpm/utils/database_connector.py +15 -12
  107. claude_mpm/utils/error_handler.py +1 -0
  108. claude_mpm/utils/log_cleanup.py +1 -0
  109. claude_mpm/utils/path_operations.py +1 -0
  110. claude_mpm/utils/session_logging.py +1 -1
  111. claude_mpm/utils/subprocess_utils.py +1 -0
  112. claude_mpm/validation/agent_validator.py +1 -1
  113. {claude_mpm-4.4.3.dist-info → claude_mpm-4.4.4.dist-info}/METADATA +9 -3
  114. {claude_mpm-4.4.3.dist-info → claude_mpm-4.4.4.dist-info}/RECORD +118 -117
  115. {claude_mpm-4.4.3.dist-info → claude_mpm-4.4.4.dist-info}/WHEEL +0 -0
  116. {claude_mpm-4.4.3.dist-info → claude_mpm-4.4.4.dist-info}/entry_points.txt +0 -0
  117. {claude_mpm-4.4.3.dist-info → claude_mpm-4.4.4.dist-info}/licenses/LICENSE +0 -0
  118. {claude_mpm-4.4.3.dist-info → claude_mpm-4.4.4.dist-info}/top_level.txt +0 -0
@@ -3,38 +3,41 @@ File Loader Strategy - Consolidates 215 file loading instances into 5 strategic
3
3
  Part of Phase 3 Configuration Consolidation
4
4
  """
5
5
 
6
- from abc import ABC, abstractmethod
7
- from typing import Any, Dict, Optional, List, Union, Callable
8
- from pathlib import Path
6
+ import configparser
7
+ import importlib.util
9
8
  import json
10
- import yaml
11
9
  import os
12
10
  import re
11
+ from abc import ABC, abstractmethod
13
12
  from dataclasses import dataclass
14
13
  from enum import Enum
15
- import configparser
16
- import importlib.util
17
- from contextlib import contextmanager
14
+ from pathlib import Path
15
+ from typing import Any, Callable, Dict, List, Optional, Union
16
+
17
+ import yaml
18
18
 
19
19
  from claude_mpm.core.logging_utils import get_logger
20
- from .unified_config_service import IConfigStrategy, ConfigFormat
20
+
21
+ from .unified_config_service import ConfigFormat, IConfigStrategy
21
22
 
22
23
 
23
24
  class LoaderType(Enum):
24
25
  """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
26
+
27
+ STRUCTURED = "structured" # JSON, YAML, TOML - 85 instances
28
+ ENVIRONMENT = "environment" # ENV files and variables - 45 instances
29
+ PROGRAMMATIC = "programmatic" # Python modules - 35 instances
30
+ LEGACY = "legacy" # INI, properties - 30 instances
31
+ COMPOSITE = "composite" # Multi-source loading - 20 instances
30
32
 
31
33
 
32
34
  @dataclass
33
35
  class FileLoadContext:
34
36
  """Context for file loading operations"""
37
+
35
38
  path: Path
36
39
  format: ConfigFormat
37
- encoding: str = 'utf-8'
40
+ encoding: str = "utf-8"
38
41
  strict: bool = True
39
42
  interpolate: bool = False
40
43
  includes: List[str] = None
@@ -53,33 +56,31 @@ class BaseFileLoader(ABC):
53
56
  @abstractmethod
54
57
  def load(self, context: FileLoadContext) -> Dict[str, Any]:
55
58
  """Load configuration from file"""
56
- pass
57
59
 
58
60
  @abstractmethod
59
61
  def supports(self, format: ConfigFormat) -> bool:
60
62
  """Check if loader supports the format"""
61
- pass
62
63
 
63
- def _read_file(self, path: Path, encoding: str = 'utf-8') -> str:
64
+ def _read_file(self, path: Path, encoding: str = "utf-8") -> str:
64
65
  """Read file with proper error handling"""
65
66
  try:
66
- with open(path, 'r', encoding=encoding) as f:
67
+ with open(path, encoding=encoding) as f:
67
68
  return f.read()
68
69
  except UnicodeDecodeError:
69
70
  # Try with different encodings
70
- for enc in ['latin-1', 'cp1252', 'utf-16']:
71
+ for enc in ["latin-1", "cp1252", "utf-16"]:
71
72
  try:
72
- with open(path, 'r', encoding=enc) as f:
73
- self.logger.warning(f"Read {path} with fallback encoding: {enc}")
73
+ with open(path, encoding=enc) as f:
74
+ self.logger.warning(
75
+ f"Read {path} with fallback encoding: {enc}"
76
+ )
74
77
  return f.read()
75
78
  except:
76
79
  continue
77
80
  raise
78
81
 
79
82
  def _apply_transformations(
80
- self,
81
- config: Dict[str, Any],
82
- transformations: List[Callable]
83
+ self, config: Dict[str, Any], transformations: List[Callable]
83
84
  ) -> Dict[str, Any]:
84
85
  """Apply transformation pipeline"""
85
86
  if not transformations:
@@ -140,7 +141,7 @@ class StructuredFileLoader(BaseFileLoader):
140
141
  def _load_json(self, content: str, context: FileLoadContext) -> Dict[str, Any]:
141
142
  """Load JSON with comments support"""
142
143
  # Remove comments if present
143
- if '//' in content or '/*' in content:
144
+ if "//" in content or "/*" in content:
144
145
  content = self._strip_json_comments(content)
145
146
 
146
147
  try:
@@ -159,13 +160,12 @@ class StructuredFileLoader(BaseFileLoader):
159
160
 
160
161
  if len(docs) == 1:
161
162
  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
163
+ # Merge multiple documents
164
+ result = {}
165
+ for doc in docs:
166
+ if doc:
167
+ result.update(doc)
168
+ return result
169
169
 
170
170
  except yaml.YAMLError as e:
171
171
  if context.strict:
@@ -177,11 +177,13 @@ class StructuredFileLoader(BaseFileLoader):
177
177
  """Load TOML configuration"""
178
178
  try:
179
179
  import toml
180
+
180
181
  return toml.loads(content)
181
182
  except ImportError:
182
183
  self.logger.error("toml package not installed")
183
184
  try:
184
185
  import tomli
186
+
185
187
  return tomli.loads(content)
186
188
  except ImportError:
187
189
  raise ImportError("Neither toml nor tomli package is installed")
@@ -194,24 +196,26 @@ class StructuredFileLoader(BaseFileLoader):
194
196
  def _strip_json_comments(self, content: str) -> str:
195
197
  """Remove comments from JSON content"""
196
198
  # Remove single-line comments
197
- content = re.sub(r'//.*?$', '', content, flags=re.MULTILINE)
199
+ content = re.sub(r"//.*?$", "", content, flags=re.MULTILINE)
198
200
  # Remove multi-line comments
199
- content = re.sub(r'/\*.*?\*/', '', content, flags=re.DOTALL)
201
+ content = re.sub(r"/\*.*?\*/", "", content, flags=re.DOTALL)
200
202
  return content
201
203
 
202
204
  def _recover_json(self, content: str) -> Dict[str, Any]:
203
205
  """Attempt to recover from malformed JSON"""
204
206
  # Try to fix common issues
205
207
  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
+ content = re.sub(r",\s*}", "}", content) # Trailing commas in objects
209
+ content = re.sub(r",\s*]", "]", content) # Trailing commas in arrays
208
210
 
209
211
  try:
210
212
  return json.loads(content)
211
213
  except:
212
214
  return {}
213
215
 
214
- def _process_includes(self, config: Dict[str, Any], context: FileLoadContext) -> Dict[str, Any]:
216
+ def _process_includes(
217
+ self, config: Dict[str, Any], context: FileLoadContext
218
+ ) -> Dict[str, Any]:
215
219
  """Process include directives"""
216
220
  for include_key in context.includes:
217
221
  if include_key in config:
@@ -224,7 +228,7 @@ class StructuredFileLoader(BaseFileLoader):
224
228
  path=include_path,
225
229
  format=self._detect_format(include_path),
226
230
  encoding=context.encoding,
227
- strict=context.strict
231
+ strict=context.strict,
228
232
  )
229
233
  included_config = self.load(include_context)
230
234
 
@@ -236,7 +240,9 @@ class StructuredFileLoader(BaseFileLoader):
236
240
 
237
241
  return config
238
242
 
239
- def _process_excludes(self, config: Dict[str, Any], context: FileLoadContext) -> Dict[str, Any]:
243
+ def _process_excludes(
244
+ self, config: Dict[str, Any], context: FileLoadContext
245
+ ) -> Dict[str, Any]:
240
246
  """Process exclude patterns"""
241
247
  for pattern in context.excludes:
242
248
  config = self._exclude_keys(config, pattern)
@@ -244,21 +250,27 @@ class StructuredFileLoader(BaseFileLoader):
244
250
 
245
251
  def _exclude_keys(self, config: Dict[str, Any], pattern: str) -> Dict[str, Any]:
246
252
  """Exclude keys matching pattern"""
247
- if '*' in pattern or '?' in pattern:
253
+ if "*" in pattern or "?" in pattern:
248
254
  # Glob pattern
249
255
  import fnmatch
256
+
250
257
  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
258
+ # Exact match
259
+ config.pop(pattern, None)
260
+ return config
255
261
 
256
- def _merge_configs(self, base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
262
+ def _merge_configs(
263
+ self, base: Dict[str, Any], override: Dict[str, Any]
264
+ ) -> Dict[str, Any]:
257
265
  """Deep merge configurations"""
258
266
  result = base.copy()
259
267
 
260
268
  for key, value in override.items():
261
- if key in result and isinstance(result[key], dict) and isinstance(value, dict):
269
+ if (
270
+ key in result
271
+ and isinstance(result[key], dict)
272
+ and isinstance(value, dict)
273
+ ):
262
274
  result[key] = self._merge_configs(result[key], value)
263
275
  else:
264
276
  result[key] = value
@@ -269,21 +281,19 @@ class StructuredFileLoader(BaseFileLoader):
269
281
  """Detect file format from extension"""
270
282
  suffix = path.suffix.lower()
271
283
 
272
- if suffix == '.json':
284
+ if suffix == ".json":
273
285
  return ConfigFormat.JSON
274
- elif suffix in ['.yaml', '.yml']:
286
+ if suffix in [".yaml", ".yml"]:
275
287
  return ConfigFormat.YAML
276
- elif suffix == '.toml':
288
+ if suffix == ".toml":
277
289
  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
290
+ # Try to detect from content
291
+ content = self._read_file(path)
292
+ if content.strip().startswith("{"):
293
+ return ConfigFormat.JSON
294
+ if ":" in content:
295
+ return ConfigFormat.YAML
296
+ return ConfigFormat.JSON
287
297
 
288
298
 
289
299
  class EnvironmentFileLoader(BaseFileLoader):
@@ -325,19 +335,17 @@ class EnvironmentFileLoader(BaseFileLoader):
325
335
  line = line.strip()
326
336
 
327
337
  # Skip comments and empty lines
328
- if not line or line.startswith('#'):
338
+ if not line or line.startswith("#"):
329
339
  continue
330
340
 
331
341
  # Parse KEY=VALUE format
332
- if '=' in line:
333
- key, value = line.split('=', 1)
342
+ if "=" in line:
343
+ key, value = line.split("=", 1)
334
344
  key = key.strip()
335
345
  value = value.strip()
336
346
 
337
347
  # Remove quotes if present
338
- if value.startswith('"') and value.endswith('"'):
339
- value = value[1:-1]
340
- elif value.startswith("'") and value.endswith("'"):
348
+ if (value.startswith('"') and value.endswith('"')) or (value.startswith("'") and value.endswith("'")):
341
349
  value = value[1:-1]
342
350
 
343
351
  # Parse value type
@@ -348,7 +356,7 @@ class EnvironmentFileLoader(BaseFileLoader):
348
356
  def _load_env_vars(self, context: FileLoadContext) -> Dict[str, Any]:
349
357
  """Load from environment variables"""
350
358
  config = {}
351
- prefix = context.path.stem.upper() if context.path else ''
359
+ prefix = context.path.stem.upper() if context.path else ""
352
360
 
353
361
  for key, value in os.environ.items():
354
362
  # Check if key matches pattern
@@ -358,81 +366,82 @@ class EnvironmentFileLoader(BaseFileLoader):
358
366
 
359
367
  return config
360
368
 
361
- def _should_include_env_var(self, key: str, prefix: str, context: FileLoadContext) -> bool:
369
+ def _should_include_env_var(
370
+ self, key: str, prefix: str, context: FileLoadContext
371
+ ) -> bool:
362
372
  """Check if environment variable should be included"""
363
373
  if context.includes:
364
374
  return any(key.startswith(inc) for inc in context.includes)
365
- elif context.excludes:
375
+ if context.excludes:
366
376
  return not any(key.startswith(exc) for exc in context.excludes)
367
- elif prefix:
377
+ if prefix:
368
378
  return key.startswith(prefix)
369
379
  return True
370
380
 
371
381
  def _clean_env_key(self, key: str, prefix: str) -> str:
372
382
  """Clean environment variable key"""
373
383
  if prefix and key.startswith(prefix):
374
- key = key[len(prefix):]
375
- if key.startswith('_'):
384
+ key = key[len(prefix) :]
385
+ if key.startswith("_"):
376
386
  key = key[1:]
377
387
 
378
388
  # Convert to lowercase and replace underscores
379
- return key.lower().replace('__', '.').replace('_', '-')
389
+ return key.lower().replace("__", ".").replace("_", "-")
380
390
 
381
391
  def _parse_env_value(self, value: str) -> Any:
382
392
  """Parse environment variable value to appropriate type"""
383
393
  # Boolean
384
- if value.lower() in ['true', 'false']:
385
- return value.lower() == 'true'
394
+ if value.lower() in ["true", "false"]:
395
+ return value.lower() == "true"
386
396
 
387
397
  # None
388
- if value.lower() in ['none', 'null']:
398
+ if value.lower() in ["none", "null"]:
389
399
  return None
390
400
 
391
401
  # Number
392
402
  try:
393
- if '.' in value:
403
+ if "." in value:
394
404
  return float(value)
395
- else:
396
- return int(value)
405
+ return int(value)
397
406
  except ValueError:
398
407
  pass
399
408
 
400
409
  # JSON array or object
401
- if value.startswith('[') or value.startswith('{'):
410
+ if value.startswith("[") or value.startswith("{"):
402
411
  try:
403
412
  return json.loads(value)
404
413
  except:
405
414
  pass
406
415
 
407
416
  # Comma-separated list
408
- if ',' in value:
409
- return [v.strip() for v in value.split(',')]
417
+ if "," in value:
418
+ return [v.strip() for v in value.split(",")]
410
419
 
411
420
  return value
412
421
 
413
422
  def _interpolate_variables(self, config: Dict[str, Any]) -> Dict[str, Any]:
414
423
  """Interpolate variables in configuration values"""
424
+
415
425
  def interpolate_value(value: Any) -> Any:
416
426
  if isinstance(value, str):
417
427
  # Replace ${VAR} or $VAR patterns
418
- pattern = r'\$\{([^}]+)\}|\$(\w+)'
428
+ pattern = r"\$\{([^}]+)\}|\$(\w+)"
419
429
 
420
430
  def replacer(match):
421
431
  var_name = match.group(1) or match.group(2)
422
432
  # Look in config first, then environment
423
433
  if var_name in config:
424
434
  return str(config[var_name])
425
- elif var_name in os.environ:
435
+ if var_name in os.environ:
426
436
  return os.environ[var_name]
427
- else:
428
- return match.group(0)
437
+ return match.group(0)
429
438
 
430
439
  return re.sub(pattern, replacer, value)
431
440
 
432
- elif isinstance(value, dict):
441
+ if isinstance(value, dict):
433
442
  return {k: interpolate_value(v) for k, v in value.items()}
434
443
 
435
- elif isinstance(value, list):
444
+ if isinstance(value, list):
436
445
  return [interpolate_value(v) for v in value]
437
446
 
438
447
  return value
@@ -476,22 +485,22 @@ class ProgrammaticFileLoader(BaseFileLoader):
476
485
  config = {}
477
486
 
478
487
  # Look for specific config patterns
479
- if hasattr(module, 'CONFIG'):
488
+ if hasattr(module, "CONFIG"):
480
489
  # Direct CONFIG dict
481
490
  config = module.CONFIG
482
- elif hasattr(module, 'config'):
491
+ elif hasattr(module, "config"):
483
492
  # config dict or function
484
493
  if callable(module.config):
485
494
  config = module.config()
486
495
  else:
487
496
  config = module.config
488
- elif hasattr(module, 'get_config'):
497
+ elif hasattr(module, "get_config"):
489
498
  # get_config function
490
499
  config = module.get_config()
491
500
  else:
492
501
  # Extract all uppercase variables
493
502
  for name in dir(module):
494
- if name.isupper() and not name.startswith('_'):
503
+ if name.isupper() and not name.startswith("_"):
495
504
  value = getattr(module, name)
496
505
  # Skip modules and functions unless specified
497
506
  if not (callable(value) or isinstance(value, type)):
@@ -534,13 +543,15 @@ class LegacyFileLoader(BaseFileLoader):
534
543
  def _is_properties_format(self, content: str) -> bool:
535
544
  """Check if content is Java properties format"""
536
545
  # Properties files don't have sections
537
- return not any(line.strip().startswith('[') for line in content.splitlines())
546
+ return not any(line.strip().startswith("[") for line in content.splitlines())
538
547
 
539
548
  def _load_ini(self, content: str, context: FileLoadContext) -> Dict[str, Any]:
540
549
  """Load INI format configuration"""
541
550
  parser = configparser.ConfigParser(
542
- interpolation=configparser.ExtendedInterpolation() if context.interpolate else None,
543
- allow_no_value=True
551
+ interpolation=(
552
+ configparser.ExtendedInterpolation() if context.interpolate else None
553
+ ),
554
+ allow_no_value=True,
544
555
  )
545
556
 
546
557
  try:
@@ -556,7 +567,7 @@ class LegacyFileLoader(BaseFileLoader):
556
567
 
557
568
  # Handle DEFAULT section
558
569
  if parser.defaults():
559
- config['_defaults'] = dict(parser.defaults())
570
+ config["_defaults"] = dict(parser.defaults())
560
571
 
561
572
  # Handle other sections
562
573
  for section in parser.sections():
@@ -565,13 +576,15 @@ class LegacyFileLoader(BaseFileLoader):
565
576
  config[section][key] = self._parse_ini_value(value)
566
577
 
567
578
  # 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'):
579
+ sections = [s for s in config if s != "_defaults"]
580
+ if len(sections) == 1 and not config.get("_defaults"):
570
581
  config = config[sections[0]]
571
582
 
572
583
  return config
573
584
 
574
- def _load_properties(self, content: str, context: FileLoadContext) -> Dict[str, Any]:
585
+ def _load_properties(
586
+ self, content: str, context: FileLoadContext
587
+ ) -> Dict[str, Any]:
575
588
  """Load Java properties format"""
576
589
  config = {}
577
590
 
@@ -579,24 +592,24 @@ class LegacyFileLoader(BaseFileLoader):
579
592
  line = line.strip()
580
593
 
581
594
  # Skip comments and empty lines
582
- if not line or line.startswith('#') or line.startswith('!'):
595
+ if not line or line.startswith("#") or line.startswith("!"):
583
596
  continue
584
597
 
585
598
  # Handle line continuation
586
- while line.endswith('\\'):
599
+ while line.endswith("\\"):
587
600
  line = line[:-1]
588
- next_line = next(content.splitlines(), '')
601
+ next_line = next(content.splitlines(), "")
589
602
  line += next_line.strip()
590
603
 
591
604
  # 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)
605
+ if "=" in line:
606
+ key, value = line.split("=", 1)
607
+ elif ":" in line:
608
+ key, value = line.split(":", 1)
596
609
  else:
597
610
  # Key without value
598
611
  key = line
599
- value = ''
612
+ value = ""
600
613
 
601
614
  key = key.strip()
602
615
  value = value.strip()
@@ -612,39 +625,38 @@ class LegacyFileLoader(BaseFileLoader):
612
625
  def _parse_ini_value(self, value: str) -> Any:
613
626
  """Parse INI value to appropriate type"""
614
627
  if not value:
615
- return ''
628
+ return ""
616
629
 
617
630
  # Boolean
618
- if value.lower() in ['true', 'yes', 'on', '1']:
631
+ if value.lower() in ["true", "yes", "on", "1"]:
619
632
  return True
620
- elif value.lower() in ['false', 'no', 'off', '0']:
633
+ if value.lower() in ["false", "no", "off", "0"]:
621
634
  return False
622
635
 
623
636
  # Number
624
637
  try:
625
- if '.' in value:
638
+ if "." in value:
626
639
  return float(value)
627
- else:
628
- return int(value)
640
+ return int(value)
629
641
  except ValueError:
630
642
  pass
631
643
 
632
644
  # List (comma-separated)
633
- if ',' in value:
634
- return [v.strip() for v in value.split(',')]
645
+ if "," in value:
646
+ return [v.strip() for v in value.split(",")]
635
647
 
636
648
  return value
637
649
 
638
650
  def _unescape_properties_value(self, value: str) -> str:
639
651
  """Unescape Java properties special characters"""
640
652
  replacements = {
641
- '\\n': '\n',
642
- '\\r': '\r',
643
- '\\t': '\t',
644
- '\\\\': '\\',
645
- '\\:': ':',
646
- '\\=': '=',
647
- '\\ ': ' '
653
+ "\\n": "\n",
654
+ "\\r": "\r",
655
+ "\\t": "\t",
656
+ "\\\\": "\\",
657
+ "\\:": ":",
658
+ "\\=": "=",
659
+ "\\ ": " ",
648
660
  }
649
661
 
650
662
  for old, new in replacements.items():
@@ -654,7 +666,7 @@ class LegacyFileLoader(BaseFileLoader):
654
666
 
655
667
  def _set_nested_value(self, config: Dict[str, Any], key: str, value: Any):
656
668
  """Set value in nested dict structure based on dot notation"""
657
- parts = key.split('.')
669
+ parts = key.split(".")
658
670
  current = config
659
671
 
660
672
  for part in parts[:-1]:
@@ -677,7 +689,7 @@ class CompositeFileLoader(BaseFileLoader):
677
689
  LoaderType.STRUCTURED: StructuredFileLoader(),
678
690
  LoaderType.ENVIRONMENT: EnvironmentFileLoader(),
679
691
  LoaderType.PROGRAMMATIC: ProgrammaticFileLoader(),
680
- LoaderType.LEGACY: LegacyFileLoader()
692
+ LoaderType.LEGACY: LegacyFileLoader(),
681
693
  }
682
694
 
683
695
  def supports(self, format: ConfigFormat) -> bool:
@@ -700,7 +712,7 @@ class CompositeFileLoader(BaseFileLoader):
700
712
  path=fallback,
701
713
  format=self._detect_format(fallback),
702
714
  encoding=context.encoding,
703
- strict=False # Non-strict for fallbacks
715
+ strict=False, # Non-strict for fallbacks
704
716
  )
705
717
  try:
706
718
  config = self._load_single(fallback_context)
@@ -728,13 +740,7 @@ class CompositeFileLoader(BaseFileLoader):
728
740
  configs = []
729
741
 
730
742
  # Define load order
731
- patterns = [
732
- 'default.*',
733
- 'config.*',
734
- 'settings.*',
735
- '*.config.*',
736
- '*.settings.*'
737
- ]
743
+ patterns = ["default.*", "config.*", "settings.*", "*.config.*", "*.settings.*"]
738
744
 
739
745
  # Load files in order
740
746
  for pattern in patterns:
@@ -744,7 +750,7 @@ class CompositeFileLoader(BaseFileLoader):
744
750
  path=file_path,
745
751
  format=self._detect_format(file_path),
746
752
  encoding=context.encoding,
747
- strict=context.strict
753
+ strict=context.strict,
748
754
  )
749
755
  try:
750
756
  config = self._load_single(file_context)
@@ -768,21 +774,23 @@ class CompositeFileLoader(BaseFileLoader):
768
774
  suffix = path.suffix.lower()
769
775
 
770
776
  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
777
+ ".json": ConfigFormat.JSON,
778
+ ".yaml": ConfigFormat.YAML,
779
+ ".yml": ConfigFormat.YAML,
780
+ ".toml": ConfigFormat.TOML,
781
+ ".env": ConfigFormat.ENV,
782
+ ".py": ConfigFormat.PYTHON,
783
+ ".ini": ConfigFormat.INI,
784
+ ".cfg": ConfigFormat.INI,
785
+ ".conf": ConfigFormat.INI,
786
+ ".properties": ConfigFormat.INI,
781
787
  }
782
788
 
783
789
  return format_map.get(suffix, ConfigFormat.JSON)
784
790
 
785
- def _deep_merge(self, base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
791
+ def _deep_merge(
792
+ self, base: Dict[str, Any], override: Dict[str, Any]
793
+ ) -> Dict[str, Any]:
786
794
  """Deep merge two configurations"""
787
795
  result = base.copy()
788
796
 
@@ -825,14 +833,14 @@ class FileLoaderStrategy(IConfigStrategy):
825
833
  # Create load context
826
834
  context = FileLoadContext(
827
835
  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
+ format=kwargs.get("format", self._detect_format(path)),
837
+ encoding=kwargs.get("encoding", "utf-8"),
838
+ strict=kwargs.get("strict", True),
839
+ interpolate=kwargs.get("interpolate", False),
840
+ includes=kwargs.get("includes"),
841
+ excludes=kwargs.get("excludes"),
842
+ transformations=kwargs.get("transformations"),
843
+ fallback_paths=[Path(p) for p in kwargs.get("fallback_paths", [])],
836
844
  )
837
845
 
838
846
  return self.composite_loader.load(context)
@@ -857,7 +865,7 @@ class FileLoaderStrategy(IConfigStrategy):
857
865
  normalized = {}
858
866
 
859
867
  for key, value in config.items():
860
- norm_key = key.lower().replace('-', '_')
868
+ norm_key = key.lower().replace("-", "_")
861
869
 
862
870
  if isinstance(value, dict):
863
871
  normalized[norm_key] = self._normalize_config(value)
@@ -868,4 +876,4 @@ class FileLoaderStrategy(IConfigStrategy):
868
876
 
869
877
 
870
878
  # Export the main strategy
871
- __all__ = ['FileLoaderStrategy', 'LoaderType', 'FileLoadContext']
879
+ __all__ = ["FileLoadContext", "FileLoaderStrategy", "LoaderType"]