claude-mpm 4.4.3__py3-none-any.whl → 4.4.5__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 +3 -2
- claude_mpm/agents/agent_loader_integration.py +2 -1
- claude_mpm/agents/async_agent_loader.py +2 -2
- claude_mpm/agents/base_agent_loader.py +2 -2
- claude_mpm/agents/frontmatter_validator.py +1 -0
- claude_mpm/agents/system_agent_config.py +2 -1
- claude_mpm/cli/commands/doctor.py +44 -5
- claude_mpm/cli/commands/mpm_init.py +116 -62
- claude_mpm/cli/parsers/configure_parser.py +3 -1
- claude_mpm/cli/startup_logging.py +1 -3
- claude_mpm/config/agent_config.py +1 -1
- claude_mpm/config/paths.py +2 -1
- claude_mpm/core/agent_name_normalizer.py +1 -0
- claude_mpm/core/config.py +2 -1
- claude_mpm/core/config_aliases.py +2 -1
- claude_mpm/core/file_utils.py +0 -1
- claude_mpm/core/framework/__init__.py +6 -6
- claude_mpm/core/framework/formatters/__init__.py +2 -2
- claude_mpm/core/framework/formatters/capability_generator.py +19 -8
- claude_mpm/core/framework/formatters/content_formatter.py +8 -3
- claude_mpm/core/framework/formatters/context_generator.py +7 -3
- claude_mpm/core/framework/loaders/__init__.py +3 -3
- claude_mpm/core/framework/loaders/agent_loader.py +7 -3
- claude_mpm/core/framework/loaders/file_loader.py +16 -6
- claude_mpm/core/framework/loaders/instruction_loader.py +16 -6
- claude_mpm/core/framework/loaders/packaged_loader.py +36 -12
- claude_mpm/core/framework/processors/__init__.py +2 -2
- claude_mpm/core/framework/processors/memory_processor.py +14 -6
- claude_mpm/core/framework/processors/metadata_processor.py +5 -5
- claude_mpm/core/framework/processors/template_processor.py +12 -6
- claude_mpm/core/framework_loader.py +44 -20
- claude_mpm/core/log_manager.py +2 -1
- claude_mpm/core/tool_access_control.py +1 -0
- claude_mpm/core/unified_agent_registry.py +2 -1
- claude_mpm/core/unified_paths.py +1 -0
- claude_mpm/experimental/cli_enhancements.py +1 -0
- claude_mpm/hooks/base_hook.py +1 -0
- claude_mpm/hooks/instruction_reinforcement.py +1 -0
- claude_mpm/hooks/kuzu_memory_hook.py +20 -13
- claude_mpm/hooks/validation_hooks.py +1 -1
- claude_mpm/scripts/mpm_doctor.py +1 -0
- claude_mpm/services/agents/loading/agent_profile_loader.py +1 -1
- claude_mpm/services/agents/loading/base_agent_manager.py +1 -1
- claude_mpm/services/agents/loading/framework_agent_loader.py +1 -1
- claude_mpm/services/agents/management/agent_capabilities_generator.py +1 -0
- claude_mpm/services/agents/management/agent_management_service.py +1 -1
- claude_mpm/services/agents/memory/memory_categorization_service.py +0 -1
- claude_mpm/services/agents/memory/memory_file_service.py +6 -2
- claude_mpm/services/agents/memory/memory_format_service.py +0 -1
- claude_mpm/services/agents/registry/deployed_agent_discovery.py +1 -1
- claude_mpm/services/async_session_logger.py +1 -1
- claude_mpm/services/claude_session_logger.py +1 -0
- claude_mpm/services/core/path_resolver.py +1 -0
- claude_mpm/services/diagnostics/checks/__init__.py +2 -0
- claude_mpm/services/diagnostics/checks/installation_check.py +126 -25
- claude_mpm/services/diagnostics/checks/mcp_services_check.py +451 -0
- claude_mpm/services/diagnostics/diagnostic_runner.py +3 -0
- claude_mpm/services/diagnostics/doctor_reporter.py +259 -32
- claude_mpm/services/event_bus/direct_relay.py +2 -1
- claude_mpm/services/event_bus/event_bus.py +1 -0
- claude_mpm/services/event_bus/relay.py +3 -2
- claude_mpm/services/framework_claude_md_generator/content_assembler.py +1 -1
- claude_mpm/services/infrastructure/daemon_manager.py +1 -1
- claude_mpm/services/mcp_config_manager.py +301 -54
- claude_mpm/services/mcp_gateway/core/process_pool.py +62 -23
- claude_mpm/services/mcp_gateway/tools/__init__.py +6 -5
- claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +3 -1
- claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +16 -31
- claude_mpm/services/memory/cache/simple_cache.py +1 -1
- claude_mpm/services/project/archive_manager.py +159 -96
- claude_mpm/services/project/documentation_manager.py +64 -45
- claude_mpm/services/project/enhanced_analyzer.py +132 -89
- claude_mpm/services/project/project_organizer.py +225 -131
- claude_mpm/services/response_tracker.py +1 -1
- claude_mpm/services/socketio/server/eventbus_integration.py +1 -1
- claude_mpm/services/unified/__init__.py +1 -1
- claude_mpm/services/unified/analyzer_strategies/__init__.py +3 -3
- claude_mpm/services/unified/analyzer_strategies/code_analyzer.py +97 -53
- claude_mpm/services/unified/analyzer_strategies/dependency_analyzer.py +81 -40
- claude_mpm/services/unified/analyzer_strategies/performance_analyzer.py +277 -178
- claude_mpm/services/unified/analyzer_strategies/security_analyzer.py +196 -112
- claude_mpm/services/unified/analyzer_strategies/structure_analyzer.py +83 -49
- claude_mpm/services/unified/config_strategies/__init__.py +111 -126
- claude_mpm/services/unified/config_strategies/config_schema.py +157 -111
- claude_mpm/services/unified/config_strategies/context_strategy.py +91 -89
- claude_mpm/services/unified/config_strategies/error_handling_strategy.py +183 -173
- claude_mpm/services/unified/config_strategies/file_loader_strategy.py +160 -152
- claude_mpm/services/unified/config_strategies/unified_config_service.py +124 -112
- claude_mpm/services/unified/config_strategies/validation_strategy.py +298 -259
- claude_mpm/services/unified/deployment_strategies/__init__.py +7 -7
- claude_mpm/services/unified/deployment_strategies/base.py +24 -28
- claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +168 -88
- claude_mpm/services/unified/deployment_strategies/local.py +49 -34
- claude_mpm/services/unified/deployment_strategies/utils.py +39 -43
- claude_mpm/services/unified/deployment_strategies/vercel.py +30 -24
- claude_mpm/services/unified/interfaces.py +0 -26
- claude_mpm/services/unified/migration.py +17 -40
- claude_mpm/services/unified/strategies.py +9 -26
- claude_mpm/services/unified/unified_analyzer.py +48 -44
- claude_mpm/services/unified/unified_config.py +21 -19
- claude_mpm/services/unified/unified_deployment.py +21 -26
- claude_mpm/storage/state_storage.py +1 -0
- claude_mpm/utils/agent_dependency_loader.py +18 -6
- claude_mpm/utils/common.py +14 -12
- claude_mpm/utils/database_connector.py +15 -12
- claude_mpm/utils/error_handler.py +1 -0
- claude_mpm/utils/log_cleanup.py +1 -0
- claude_mpm/utils/path_operations.py +1 -0
- claude_mpm/utils/session_logging.py +1 -1
- claude_mpm/utils/subprocess_utils.py +1 -0
- claude_mpm/validation/agent_validator.py +1 -1
- {claude_mpm-4.4.3.dist-info → claude_mpm-4.4.5.dist-info}/METADATA +35 -15
- {claude_mpm-4.4.3.dist-info → claude_mpm-4.4.5.dist-info}/RECORD +118 -117
- {claude_mpm-4.4.3.dist-info → claude_mpm-4.4.5.dist-info}/WHEEL +0 -0
- {claude_mpm-4.4.3.dist-info → claude_mpm-4.4.5.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.4.3.dist-info → claude_mpm-4.4.5.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.4.3.dist-info → claude_mpm-4.4.5.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
|
-
|
7
|
-
|
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
|
16
|
-
import
|
17
|
-
|
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
|
-
|
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
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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 =
|
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 =
|
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,
|
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 [
|
71
|
+
for enc in ["latin-1", "cp1252", "utf-16"]:
|
71
72
|
try:
|
72
|
-
with open(path,
|
73
|
-
self.logger.warning(
|
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
|
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
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
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
|
199
|
+
content = re.sub(r"//.*?$", "", content, flags=re.MULTILINE)
|
198
200
|
# Remove multi-line comments
|
199
|
-
content = re.sub(r
|
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
|
207
|
-
content = re.sub(r
|
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(
|
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(
|
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
|
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
|
-
|
252
|
-
|
253
|
-
|
254
|
-
return config
|
258
|
+
# Exact match
|
259
|
+
config.pop(pattern, None)
|
260
|
+
return config
|
255
261
|
|
256
|
-
def _merge_configs(
|
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
|
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 ==
|
284
|
+
if suffix == ".json":
|
273
285
|
return ConfigFormat.JSON
|
274
|
-
|
286
|
+
if suffix in [".yaml", ".yml"]:
|
275
287
|
return ConfigFormat.YAML
|
276
|
-
|
288
|
+
if suffix == ".toml":
|
277
289
|
return ConfigFormat.TOML
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
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
|
333
|
-
key, value = line.split(
|
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(
|
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
|
-
|
375
|
+
if context.excludes:
|
366
376
|
return not any(key.startswith(exc) for exc in context.excludes)
|
367
|
-
|
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(
|
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 [
|
385
|
-
return value.lower() ==
|
394
|
+
if value.lower() in ["true", "false"]:
|
395
|
+
return value.lower() == "true"
|
386
396
|
|
387
397
|
# None
|
388
|
-
if value.lower() in [
|
398
|
+
if value.lower() in ["none", "null"]:
|
389
399
|
return None
|
390
400
|
|
391
401
|
# Number
|
392
402
|
try:
|
393
|
-
if
|
403
|
+
if "." in value:
|
394
404
|
return float(value)
|
395
|
-
|
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(
|
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
|
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
|
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
|
-
|
435
|
+
if var_name in os.environ:
|
426
436
|
return os.environ[var_name]
|
427
|
-
|
428
|
-
return match.group(0)
|
437
|
+
return match.group(0)
|
429
438
|
|
430
439
|
return re.sub(pattern, replacer, value)
|
431
440
|
|
432
|
-
|
441
|
+
if isinstance(value, dict):
|
433
442
|
return {k: interpolate_value(v) for k, v in value.items()}
|
434
443
|
|
435
|
-
|
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,
|
488
|
+
if hasattr(module, "CONFIG"):
|
480
489
|
# Direct CONFIG dict
|
481
490
|
config = module.CONFIG
|
482
|
-
elif hasattr(module,
|
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,
|
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(
|
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=
|
543
|
-
|
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[
|
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
|
569
|
-
if len(sections) == 1 and not config.get(
|
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(
|
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(
|
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
|
593
|
-
key, value = line.split(
|
594
|
-
elif
|
595
|
-
key, value = line.split(
|
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 [
|
631
|
+
if value.lower() in ["true", "yes", "on", "1"]:
|
619
632
|
return True
|
620
|
-
|
633
|
+
if value.lower() in ["false", "no", "off", "0"]:
|
621
634
|
return False
|
622
635
|
|
623
636
|
# Number
|
624
637
|
try:
|
625
|
-
if
|
638
|
+
if "." in value:
|
626
639
|
return float(value)
|
627
|
-
|
628
|
-
return int(value)
|
640
|
+
return int(value)
|
629
641
|
except ValueError:
|
630
642
|
pass
|
631
643
|
|
632
644
|
# List (comma-separated)
|
633
|
-
if
|
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
|
-
|
642
|
-
|
643
|
-
|
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
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
|
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(
|
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(
|
829
|
-
encoding=kwargs.get(
|
830
|
-
strict=kwargs.get(
|
831
|
-
interpolate=kwargs.get(
|
832
|
-
includes=kwargs.get(
|
833
|
-
excludes=kwargs.get(
|
834
|
-
transformations=kwargs.get(
|
835
|
-
fallback_paths=[Path(p) for p in kwargs.get(
|
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__ = [
|
879
|
+
__all__ = ["FileLoadContext", "FileLoaderStrategy", "LoaderType"]
|