mcp-proxy-adapter 6.9.27__py3-none-any.whl → 6.9.29__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mcp-proxy-adapter might be problematic. Click here for more details.
- mcp_proxy_adapter/__init__.py +10 -0
- mcp_proxy_adapter/__main__.py +8 -21
- mcp_proxy_adapter/api/app.py +10 -913
- mcp_proxy_adapter/api/core/__init__.py +18 -0
- mcp_proxy_adapter/api/core/app_factory.py +243 -0
- mcp_proxy_adapter/api/core/lifespan_manager.py +55 -0
- mcp_proxy_adapter/api/core/registration_manager.py +166 -0
- mcp_proxy_adapter/api/core/ssl_context_factory.py +88 -0
- mcp_proxy_adapter/api/handlers.py +78 -199
- mcp_proxy_adapter/api/middleware/__init__.py +1 -44
- mcp_proxy_adapter/api/middleware/base.py +0 -42
- mcp_proxy_adapter/api/middleware/command_permission_middleware.py +0 -85
- mcp_proxy_adapter/api/middleware/error_handling.py +1 -127
- mcp_proxy_adapter/api/middleware/factory.py +0 -94
- mcp_proxy_adapter/api/middleware/logging.py +0 -112
- mcp_proxy_adapter/api/middleware/performance.py +0 -35
- mcp_proxy_adapter/api/middleware/protocol_middleware.py +2 -98
- mcp_proxy_adapter/api/middleware/transport_middleware.py +0 -37
- mcp_proxy_adapter/api/middleware/unified_security.py +10 -10
- mcp_proxy_adapter/api/middleware/user_info_middleware.py +0 -118
- mcp_proxy_adapter/api/openapi/__init__.py +21 -0
- mcp_proxy_adapter/api/openapi/command_integration.py +105 -0
- mcp_proxy_adapter/api/openapi/openapi_generator.py +40 -0
- mcp_proxy_adapter/api/openapi/openapi_registry.py +62 -0
- mcp_proxy_adapter/api/openapi/schema_loader.py +116 -0
- mcp_proxy_adapter/api/schemas.py +0 -61
- mcp_proxy_adapter/api/tool_integration.py +0 -117
- mcp_proxy_adapter/api/tools.py +0 -46
- mcp_proxy_adapter/cli/__init__.py +12 -0
- mcp_proxy_adapter/cli/commands/__init__.py +15 -0
- mcp_proxy_adapter/cli/commands/client.py +100 -0
- mcp_proxy_adapter/cli/commands/config_generate.py +21 -0
- mcp_proxy_adapter/cli/commands/config_validate.py +36 -0
- mcp_proxy_adapter/cli/commands/generate.py +259 -0
- mcp_proxy_adapter/cli/commands/server.py +174 -0
- mcp_proxy_adapter/cli/commands/sets.py +128 -0
- mcp_proxy_adapter/cli/commands/testconfig.py +177 -0
- mcp_proxy_adapter/cli/examples/__init__.py +8 -0
- mcp_proxy_adapter/cli/examples/http_basic.py +82 -0
- mcp_proxy_adapter/cli/examples/https_token.py +96 -0
- mcp_proxy_adapter/cli/examples/mtls_roles.py +103 -0
- mcp_proxy_adapter/cli/main.py +63 -0
- mcp_proxy_adapter/cli/parser.py +324 -0
- mcp_proxy_adapter/cli/validators.py +231 -0
- mcp_proxy_adapter/client/jsonrpc_client.py +406 -0
- mcp_proxy_adapter/client/proxy.py +45 -0
- mcp_proxy_adapter/commands/__init__.py +44 -28
- mcp_proxy_adapter/commands/auth_validation_command.py +7 -344
- mcp_proxy_adapter/commands/base.py +19 -43
- mcp_proxy_adapter/commands/builtin_commands.py +0 -75
- mcp_proxy_adapter/commands/catalog/__init__.py +20 -0
- mcp_proxy_adapter/commands/catalog/catalog_loader.py +34 -0
- mcp_proxy_adapter/commands/catalog/catalog_manager.py +122 -0
- mcp_proxy_adapter/commands/catalog/catalog_syncer.py +149 -0
- mcp_proxy_adapter/commands/catalog/command_catalog.py +43 -0
- mcp_proxy_adapter/commands/catalog/dependency_manager.py +37 -0
- mcp_proxy_adapter/commands/catalog_manager.py +58 -928
- mcp_proxy_adapter/commands/cert_monitor_command.py +0 -88
- mcp_proxy_adapter/commands/certificate_management_command.py +0 -45
- mcp_proxy_adapter/commands/command_registry.py +172 -904
- mcp_proxy_adapter/commands/config_command.py +0 -28
- mcp_proxy_adapter/commands/dependency_container.py +1 -70
- mcp_proxy_adapter/commands/dependency_manager.py +0 -128
- mcp_proxy_adapter/commands/echo_command.py +0 -34
- mcp_proxy_adapter/commands/health_command.py +0 -3
- mcp_proxy_adapter/commands/help_command.py +0 -159
- mcp_proxy_adapter/commands/hooks.py +0 -137
- mcp_proxy_adapter/commands/key_management_command.py +0 -25
- mcp_proxy_adapter/commands/load_command.py +7 -78
- mcp_proxy_adapter/commands/plugins_command.py +0 -16
- mcp_proxy_adapter/commands/protocol_management_command.py +0 -28
- mcp_proxy_adapter/commands/proxy_registration_command.py +0 -88
- mcp_proxy_adapter/commands/queue_commands.py +750 -0
- mcp_proxy_adapter/commands/registration_status_command.py +0 -43
- mcp_proxy_adapter/commands/registry/__init__.py +18 -0
- mcp_proxy_adapter/commands/registry/command_info.py +103 -0
- mcp_proxy_adapter/commands/registry/command_loader.py +207 -0
- mcp_proxy_adapter/commands/registry/command_manager.py +119 -0
- mcp_proxy_adapter/commands/registry/command_registry.py +217 -0
- mcp_proxy_adapter/commands/reload_command.py +0 -80
- mcp_proxy_adapter/commands/result.py +25 -77
- mcp_proxy_adapter/commands/role_test_command.py +0 -44
- mcp_proxy_adapter/commands/roles_management_command.py +0 -199
- mcp_proxy_adapter/commands/security_command.py +0 -30
- mcp_proxy_adapter/commands/settings_command.py +0 -68
- mcp_proxy_adapter/commands/ssl_setup_command.py +0 -42
- mcp_proxy_adapter/commands/token_management_command.py +0 -1
- mcp_proxy_adapter/commands/transport_management_command.py +0 -20
- mcp_proxy_adapter/commands/unload_command.py +0 -71
- mcp_proxy_adapter/config.py +15 -626
- mcp_proxy_adapter/core/__init__.py +5 -39
- mcp_proxy_adapter/core/app_factory.py +14 -36
- mcp_proxy_adapter/core/app_runner.py +0 -27
- mcp_proxy_adapter/core/auth_validator.py +1 -93
- mcp_proxy_adapter/core/certificate/__init__.py +20 -0
- mcp_proxy_adapter/core/certificate/certificate_creator.py +371 -0
- mcp_proxy_adapter/core/certificate/certificate_extractor.py +183 -0
- mcp_proxy_adapter/core/certificate/certificate_utils.py +249 -0
- mcp_proxy_adapter/core/certificate/certificate_validator.py +110 -0
- mcp_proxy_adapter/core/certificate/ssl_context_manager.py +70 -0
- mcp_proxy_adapter/core/certificate_utils.py +64 -903
- mcp_proxy_adapter/core/client.py +0 -6
- mcp_proxy_adapter/core/client_manager.py +0 -19
- mcp_proxy_adapter/core/client_security.py +0 -2
- mcp_proxy_adapter/core/config/__init__.py +18 -0
- mcp_proxy_adapter/core/config/config.py +195 -0
- mcp_proxy_adapter/core/config/config_factory.py +22 -0
- mcp_proxy_adapter/core/config/config_loader.py +66 -0
- mcp_proxy_adapter/core/config/feature_manager.py +31 -0
- mcp_proxy_adapter/core/config/simple_config.py +112 -0
- mcp_proxy_adapter/core/config/simple_config_generator.py +50 -0
- mcp_proxy_adapter/core/config/simple_config_validator.py +96 -0
- mcp_proxy_adapter/core/config_converter.py +0 -186
- mcp_proxy_adapter/core/config_validator.py +96 -1238
- mcp_proxy_adapter/core/errors.py +7 -42
- mcp_proxy_adapter/core/job_manager.py +54 -0
- mcp_proxy_adapter/core/logging.py +2 -22
- mcp_proxy_adapter/core/mtls_asgi.py +0 -20
- mcp_proxy_adapter/core/mtls_asgi_app.py +0 -12
- mcp_proxy_adapter/core/mtls_proxy.py +0 -80
- mcp_proxy_adapter/core/mtls_server.py +3 -173
- mcp_proxy_adapter/core/protocol_manager.py +1 -191
- mcp_proxy_adapter/core/proxy/__init__.py +22 -0
- mcp_proxy_adapter/core/proxy/auth_manager.py +27 -0
- mcp_proxy_adapter/core/proxy/proxy_registration_manager.py +137 -0
- mcp_proxy_adapter/core/proxy/registration_client.py +60 -0
- mcp_proxy_adapter/core/proxy/ssl_manager.py +101 -0
- mcp_proxy_adapter/core/proxy_client.py +0 -1
- mcp_proxy_adapter/core/proxy_registration.py +36 -912
- mcp_proxy_adapter/core/role_utils.py +0 -308
- mcp_proxy_adapter/core/security_adapter.py +1 -36
- mcp_proxy_adapter/core/security_factory.py +1 -150
- mcp_proxy_adapter/core/security_integration.py +0 -33
- mcp_proxy_adapter/core/server_adapter.py +1 -40
- mcp_proxy_adapter/core/server_engine.py +2 -173
- mcp_proxy_adapter/core/settings.py +0 -127
- mcp_proxy_adapter/core/signal_handler.py +0 -65
- mcp_proxy_adapter/core/ssl_utils.py +19 -137
- mcp_proxy_adapter/core/transport_manager.py +0 -151
- mcp_proxy_adapter/core/unified_config_adapter.py +1 -193
- mcp_proxy_adapter/core/utils.py +1 -182
- mcp_proxy_adapter/core/validation/__init__.py +21 -0
- mcp_proxy_adapter/core/validation/config_validator.py +211 -0
- mcp_proxy_adapter/core/validation/file_validator.py +73 -0
- mcp_proxy_adapter/core/validation/protocol_validator.py +191 -0
- mcp_proxy_adapter/core/validation/security_validator.py +58 -0
- mcp_proxy_adapter/core/validation/validation_result.py +27 -0
- mcp_proxy_adapter/custom_openapi.py +33 -652
- mcp_proxy_adapter/examples/bugfix_certificate_config.py +0 -23
- mcp_proxy_adapter/examples/check_config.py +0 -2
- mcp_proxy_adapter/examples/client_usage_example.py +164 -0
- mcp_proxy_adapter/examples/config_builder.py +13 -2
- mcp_proxy_adapter/examples/config_cli.py +0 -1
- mcp_proxy_adapter/examples/create_test_configs.py +0 -46
- mcp_proxy_adapter/examples/debug_request_state.py +0 -1
- mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +0 -47
- mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +0 -45
- mcp_proxy_adapter/examples/full_application/commands/echo_command.py +0 -12
- mcp_proxy_adapter/examples/full_application/commands/help_command.py +0 -12
- mcp_proxy_adapter/examples/full_application/commands/list_command.py +0 -7
- mcp_proxy_adapter/examples/full_application/hooks/__init__.py +0 -2
- mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +0 -59
- mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +0 -54
- mcp_proxy_adapter/examples/full_application/main.py +186 -150
- mcp_proxy_adapter/examples/full_application/proxy_endpoints.py +0 -107
- mcp_proxy_adapter/examples/full_application/test_minimal_server.py +0 -24
- mcp_proxy_adapter/examples/full_application/test_server.py +0 -58
- mcp_proxy_adapter/examples/generate_config.py +65 -11
- mcp_proxy_adapter/examples/queue_demo_simple.py +632 -0
- mcp_proxy_adapter/examples/queue_integration_example.py +578 -0
- mcp_proxy_adapter/examples/queue_server_demo.py +82 -0
- mcp_proxy_adapter/examples/queue_server_example.py +85 -0
- mcp_proxy_adapter/examples/queue_server_simple.py +173 -0
- mcp_proxy_adapter/examples/required_certificates.py +0 -2
- mcp_proxy_adapter/examples/run_full_test_suite.py +0 -29
- mcp_proxy_adapter/examples/run_proxy_server.py +31 -71
- mcp_proxy_adapter/examples/run_security_tests_fixed.py +0 -27
- mcp_proxy_adapter/examples/security_test/__init__.py +18 -0
- mcp_proxy_adapter/examples/security_test/auth_manager.py +14 -0
- mcp_proxy_adapter/examples/security_test/ssl_context_manager.py +28 -0
- mcp_proxy_adapter/examples/security_test/test_client.py +159 -0
- mcp_proxy_adapter/examples/security_test/test_result.py +22 -0
- mcp_proxy_adapter/examples/security_test_client.py +24 -1075
- mcp_proxy_adapter/examples/setup/__init__.py +24 -0
- mcp_proxy_adapter/examples/setup/certificate_manager.py +215 -0
- mcp_proxy_adapter/examples/setup/config_generator.py +12 -0
- mcp_proxy_adapter/examples/setup/config_validator.py +118 -0
- mcp_proxy_adapter/examples/setup/environment_setup.py +62 -0
- mcp_proxy_adapter/examples/setup/test_files_generator.py +10 -0
- mcp_proxy_adapter/examples/setup/test_runner.py +89 -0
- mcp_proxy_adapter/examples/setup_test_environment.py +133 -1425
- mcp_proxy_adapter/examples/test_config.py +0 -3
- mcp_proxy_adapter/examples/test_config_builder.py +25 -405
- mcp_proxy_adapter/examples/test_examples.py +0 -1
- mcp_proxy_adapter/examples/test_framework_complete.py +0 -2
- mcp_proxy_adapter/examples/test_mcp_server.py +0 -1
- mcp_proxy_adapter/examples/test_protocol_examples.py +0 -1
- mcp_proxy_adapter/examples/universal_client.py +0 -6
- mcp_proxy_adapter/examples/update_config_certificates.py +0 -1
- mcp_proxy_adapter/examples/validate_generator_compatibility.py +0 -1
- mcp_proxy_adapter/examples/validate_generator_compatibility_simple.py +0 -187
- mcp_proxy_adapter/integrations/__init__.py +25 -0
- mcp_proxy_adapter/integrations/queuemgr_integration.py +462 -0
- mcp_proxy_adapter/main.py +70 -62
- mcp_proxy_adapter/openapi.py +0 -22
- mcp_proxy_adapter/version.py +1 -1
- {mcp_proxy_adapter-6.9.27.dist-info → mcp_proxy_adapter-6.9.29.dist-info}/METADATA +2 -1
- mcp_proxy_adapter-6.9.29.dist-info/RECORD +235 -0
- {mcp_proxy_adapter-6.9.27.dist-info → mcp_proxy_adapter-6.9.29.dist-info}/entry_points.txt +1 -1
- mcp_proxy_adapter-6.9.27.dist-info/RECORD +0 -149
- {mcp_proxy_adapter-6.9.27.dist-info → mcp_proxy_adapter-6.9.29.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-6.9.27.dist-info → mcp_proxy_adapter-6.9.29.dist-info}/top_level.txt +0 -0
mcp_proxy_adapter/config.py
CHANGED
|
@@ -5,639 +5,28 @@ Author: Vasiliy Zdanovskiy
|
|
|
5
5
|
email: vasilyvz@gmail.com
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
import
|
|
9
|
-
import logging
|
|
10
|
-
import os
|
|
11
|
-
from typing import Any, Dict, Optional, List
|
|
8
|
+
from typing import Optional
|
|
12
9
|
|
|
13
|
-
|
|
10
|
+
from .core.config import Config
|
|
14
11
|
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
from .core.config_validator import ConfigValidator, ValidationResult, ValidationLevel
|
|
18
|
-
VALIDATION_AVAILABLE = True
|
|
19
|
-
except ImportError:
|
|
20
|
-
VALIDATION_AVAILABLE = False
|
|
21
|
-
logger.warning("Configuration validation not available. Install the package to enable validation.")
|
|
12
|
+
# Global configuration instance
|
|
13
|
+
_config: Optional[Config] = None
|
|
22
14
|
|
|
23
|
-
# Import configuration errors
|
|
24
|
-
from .core.errors import ConfigError, ValidationResult
|
|
25
15
|
|
|
26
|
-
|
|
27
|
-
class Config:
|
|
28
|
-
"""
|
|
29
|
-
Configuration management class for the microservice.
|
|
30
|
-
Allows loading settings from configuration file and environment variables.
|
|
31
|
-
Supports optional features that can be enabled/disabled.
|
|
16
|
+
def get_config() -> Config:
|
|
32
17
|
"""
|
|
18
|
+
Get the global configuration instance.
|
|
33
19
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
validate_on_load: Whether to validate configuration on load (default: True)
|
|
42
|
-
|
|
43
|
-
Raises:
|
|
44
|
-
ConfigError: If configuration validation fails
|
|
45
|
-
"""
|
|
46
|
-
self.config_path = config_path or "./config.json"
|
|
47
|
-
self.config_data: Dict[str, Any] = {}
|
|
48
|
-
self.validate_on_load = validate_on_load
|
|
49
|
-
self.validation_results: List[ValidationResult] = []
|
|
50
|
-
self.validator = None
|
|
51
|
-
|
|
52
|
-
if VALIDATION_AVAILABLE:
|
|
53
|
-
self.validator = ConfigValidator()
|
|
54
|
-
|
|
55
|
-
# Don't auto-load config - let user call load_from_file explicitly
|
|
56
|
-
|
|
57
|
-
def load_config(self) -> None:
|
|
58
|
-
"""
|
|
59
|
-
Load configuration from file and environment variables.
|
|
60
|
-
NO DEFAULT VALUES - configuration must be complete and valid.
|
|
61
|
-
"""
|
|
62
|
-
# Load configuration from file - NO DEFAULTS
|
|
63
|
-
if not os.path.exists(self.config_path):
|
|
64
|
-
raise ConfigError(f"Configuration file '{self.config_path}' does not exist. Use the configuration generator to create a valid configuration.")
|
|
65
|
-
|
|
66
|
-
try:
|
|
67
|
-
with open(self.config_path, "r", encoding="utf-8") as f:
|
|
68
|
-
self.config_data = json.load(f)
|
|
69
|
-
except json.JSONDecodeError as e:
|
|
70
|
-
raise ConfigError(f"Configuration file '{self.config_path}' contains invalid JSON: {e}")
|
|
71
|
-
except Exception as e:
|
|
72
|
-
raise ConfigError(f"Error loading configuration from '{self.config_path}': {e}")
|
|
73
|
-
|
|
74
|
-
# Validate configuration BEFORE applying any logic
|
|
75
|
-
if self.validate_on_load:
|
|
76
|
-
if VALIDATION_AVAILABLE and self.validator:
|
|
77
|
-
self.validator.config_data = self.config_data
|
|
78
|
-
validation_results = self.validator.validate_config()
|
|
79
|
-
|
|
80
|
-
# Check for critical errors
|
|
81
|
-
errors = [r for r in validation_results if r.level == "error"]
|
|
82
|
-
if errors:
|
|
83
|
-
error_summary = "\n".join([f"[{r.section}.{r.key if r.key else 'root'}] {r.message}" for r in errors])
|
|
84
|
-
raise ConfigError(f"Configuration validation failed with {len(errors)} error(s):\n{error_summary}", errors)
|
|
85
|
-
|
|
86
|
-
# Store validation results for later access
|
|
87
|
-
self.validation_results = validation_results
|
|
88
|
-
else:
|
|
89
|
-
raise ConfigError("Configuration validation is not available. Install required dependencies.")
|
|
90
|
-
|
|
91
|
-
# Load configuration from environment variables (overrides file values)
|
|
92
|
-
self._load_env_variables()
|
|
93
|
-
|
|
94
|
-
# Apply hostname check logic based on SSL configuration
|
|
95
|
-
self._validate_security_config()
|
|
96
|
-
self._apply_hostname_check_logic()
|
|
97
|
-
|
|
98
|
-
def load_from_file(self, config_path: str) -> None:
|
|
99
|
-
"""
|
|
100
|
-
Load configuration from the specified file.
|
|
101
|
-
|
|
102
|
-
Args:
|
|
103
|
-
config_path: Path to configuration file.
|
|
104
|
-
"""
|
|
105
|
-
self.config_path = config_path
|
|
106
|
-
self.load_config()
|
|
107
|
-
|
|
108
|
-
def _load_env_variables(self) -> None:
|
|
109
|
-
"""
|
|
110
|
-
Load configuration from environment variables.
|
|
111
|
-
Environment variables should be in format SERVICE_SECTION_KEY=value.
|
|
112
|
-
For example, SERVICE_SERVER_PORT=8080.
|
|
113
|
-
"""
|
|
114
|
-
prefix = "SERVICE_"
|
|
115
|
-
for key, value in os.environ.items():
|
|
116
|
-
if key.startswith(prefix):
|
|
117
|
-
parts = key[len(prefix) :].lower().split("_", 1)
|
|
118
|
-
if len(parts) == 2:
|
|
119
|
-
section, param = parts
|
|
120
|
-
if section not in self.config_data:
|
|
121
|
-
self.config_data[section] = {}
|
|
122
|
-
self.config_data[section][param] = self._convert_env_value(value)
|
|
123
|
-
|
|
124
|
-
def _convert_env_value(self, value: str) -> Any:
|
|
125
|
-
"""
|
|
126
|
-
Convert environment variable value to appropriate type.
|
|
127
|
-
|
|
128
|
-
Args:
|
|
129
|
-
value: Value as string
|
|
130
|
-
|
|
131
|
-
Returns:
|
|
132
|
-
Converted value
|
|
133
|
-
"""
|
|
134
|
-
# Try to convert to appropriate type
|
|
135
|
-
if value.lower() == "true":
|
|
136
|
-
return True
|
|
137
|
-
elif value.lower() == "false":
|
|
138
|
-
return False
|
|
139
|
-
elif value.isdigit():
|
|
140
|
-
return int(value)
|
|
141
|
-
else:
|
|
142
|
-
try:
|
|
143
|
-
return float(value)
|
|
144
|
-
except ValueError:
|
|
145
|
-
return value
|
|
146
|
-
|
|
147
|
-
def get(self, key: str, default: Any = None) -> Any:
|
|
148
|
-
"""
|
|
149
|
-
Get configuration value for key.
|
|
150
|
-
|
|
151
|
-
Args:
|
|
152
|
-
key: Configuration key in format "section.param"
|
|
153
|
-
default: Default value if key not found
|
|
154
|
-
|
|
155
|
-
Returns:
|
|
156
|
-
Configuration value
|
|
157
|
-
"""
|
|
158
|
-
parts = key.split(".")
|
|
159
|
-
|
|
160
|
-
# Get value from config
|
|
161
|
-
value = self.config_data
|
|
162
|
-
for part in parts:
|
|
163
|
-
if not isinstance(value, dict) or part not in value:
|
|
164
|
-
return default
|
|
165
|
-
value = value[part]
|
|
166
|
-
|
|
167
|
-
return value
|
|
168
|
-
|
|
169
|
-
def get_all(self) -> Dict[str, Any]:
|
|
170
|
-
"""
|
|
171
|
-
Get all configuration values.
|
|
172
|
-
|
|
173
|
-
Returns:
|
|
174
|
-
Dictionary with all configuration values
|
|
175
|
-
"""
|
|
176
|
-
return self.config_data.copy()
|
|
177
|
-
|
|
178
|
-
def set(self, key: str, value: Any) -> None:
|
|
179
|
-
"""
|
|
180
|
-
Set configuration value for key.
|
|
181
|
-
|
|
182
|
-
Args:
|
|
183
|
-
key: Configuration key in format "section.param"
|
|
184
|
-
value: Configuration value
|
|
185
|
-
"""
|
|
186
|
-
parts = key.split(".")
|
|
187
|
-
if len(parts) == 1:
|
|
188
|
-
self.config_data[key] = value
|
|
189
|
-
else:
|
|
190
|
-
section = parts[0]
|
|
191
|
-
param_key = ".".join(parts[1:])
|
|
192
|
-
|
|
193
|
-
if section not in self.config_data:
|
|
194
|
-
self.config_data[section] = {}
|
|
195
|
-
|
|
196
|
-
current = self.config_data[section]
|
|
197
|
-
for part in parts[1:-1]:
|
|
198
|
-
if part not in current:
|
|
199
|
-
current[part] = {}
|
|
200
|
-
current = current[part]
|
|
201
|
-
|
|
202
|
-
current[parts[-1]] = value
|
|
203
|
-
|
|
204
|
-
# Special handling for chk_hostname - mark it as user-set
|
|
205
|
-
if key == "transport.ssl.chk_hostname":
|
|
206
|
-
if "ssl" not in self.config_data.get("transport", {}):
|
|
207
|
-
self.config_data["transport"]["ssl"] = {}
|
|
208
|
-
self.config_data["transport"]["ssl"]["_chk_hostname_user_set"] = True
|
|
209
|
-
|
|
210
|
-
def save(self, path: Optional[str] = None) -> None:
|
|
211
|
-
"""
|
|
212
|
-
Save configuration to file.
|
|
213
|
-
|
|
214
|
-
Args:
|
|
215
|
-
path: Path to configuration file. If not specified,
|
|
216
|
-
self.config_path is used.
|
|
217
|
-
"""
|
|
218
|
-
save_path = path or self.config_path
|
|
219
|
-
with open(save_path, "w", encoding="utf-8") as f:
|
|
220
|
-
json.dump(self.config_data, f, indent=2)
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
def enable_feature(self, feature: str) -> None:
|
|
224
|
-
"""
|
|
225
|
-
Enable a specific feature in the configuration.
|
|
226
|
-
|
|
227
|
-
Args:
|
|
228
|
-
feature: Feature to enable (ssl, auth, roles, proxy_registration,
|
|
229
|
-
security)
|
|
230
|
-
"""
|
|
231
|
-
if feature == "ssl":
|
|
232
|
-
self.set("ssl.enabled", True)
|
|
233
|
-
self.set("security.ssl.enabled", True)
|
|
234
|
-
elif feature == "auth":
|
|
235
|
-
self.set("security.auth.enabled", True)
|
|
236
|
-
elif feature == "roles":
|
|
237
|
-
self.set("security.permissions.enabled", True)
|
|
238
|
-
self.set("roles.enabled", True)
|
|
239
|
-
elif feature == "proxy_registration":
|
|
240
|
-
self.set("proxy_registration.enabled", True)
|
|
241
|
-
elif feature == "security":
|
|
242
|
-
self.set("security.enabled", True)
|
|
243
|
-
elif feature == "rate_limit":
|
|
244
|
-
self.set("security.rate_limit.enabled", True)
|
|
245
|
-
elif feature == "certificates":
|
|
246
|
-
self.set("security.certificates.enabled", True)
|
|
247
|
-
else:
|
|
248
|
-
raise ValueError(f"Unknown feature: {feature}")
|
|
249
|
-
|
|
250
|
-
def disable_feature(self, feature: str) -> None:
|
|
251
|
-
"""
|
|
252
|
-
Disable a specific feature in the configuration.
|
|
253
|
-
|
|
254
|
-
Args:
|
|
255
|
-
feature: Feature to disable (ssl, auth, roles, proxy_registration,
|
|
256
|
-
security)
|
|
257
|
-
"""
|
|
258
|
-
if feature == "ssl":
|
|
259
|
-
self.set("ssl.enabled", False)
|
|
260
|
-
self.set("security.ssl.enabled", False)
|
|
261
|
-
elif feature == "auth":
|
|
262
|
-
self.set("security.auth.enabled", False)
|
|
263
|
-
elif feature == "roles":
|
|
264
|
-
self.set("security.permissions.enabled", False)
|
|
265
|
-
self.set("roles.enabled", False)
|
|
266
|
-
elif feature == "proxy_registration":
|
|
267
|
-
self.set("proxy_registration.enabled", False)
|
|
268
|
-
elif feature == "security":
|
|
269
|
-
self.set("security.enabled", False)
|
|
270
|
-
elif feature == "rate_limit":
|
|
271
|
-
self.set("security.rate_limit.enabled", False)
|
|
272
|
-
elif feature == "certificates":
|
|
273
|
-
self.set("security.certificates.enabled", False)
|
|
274
|
-
else:
|
|
275
|
-
raise ValueError(f"Unknown feature: {feature}")
|
|
276
|
-
|
|
277
|
-
def is_feature_enabled(self, feature: str) -> bool:
|
|
278
|
-
"""
|
|
279
|
-
Check if a specific feature is enabled.
|
|
280
|
-
|
|
281
|
-
Args:
|
|
282
|
-
feature: Feature to check (ssl, auth, roles, proxy_registration,
|
|
283
|
-
security)
|
|
284
|
-
|
|
285
|
-
Returns:
|
|
286
|
-
True if feature is enabled, False otherwise
|
|
287
|
-
"""
|
|
288
|
-
if feature == "ssl":
|
|
289
|
-
return self.get("ssl.enabled", False) or self.get(
|
|
290
|
-
"security.ssl.enabled", False
|
|
291
|
-
)
|
|
292
|
-
elif feature == "auth":
|
|
293
|
-
return self.get("security.auth.enabled", False)
|
|
294
|
-
elif feature == "roles":
|
|
295
|
-
return self.get("security.permissions.enabled", False) or self.get(
|
|
296
|
-
"roles.enabled", False
|
|
297
|
-
)
|
|
298
|
-
elif feature == "proxy_registration":
|
|
299
|
-
return self.get("proxy_registration.enabled", False)
|
|
300
|
-
elif feature == "security":
|
|
301
|
-
return self.get("security.enabled", False)
|
|
302
|
-
elif feature == "rate_limit":
|
|
303
|
-
return self.get("security.rate_limit.enabled", False)
|
|
304
|
-
elif feature == "certificates":
|
|
305
|
-
return self.get("security.certificates.enabled", False)
|
|
306
|
-
else:
|
|
307
|
-
raise ValueError(f"Unknown feature: {feature}")
|
|
308
|
-
|
|
309
|
-
def get_enabled_features(self) -> List[str]:
|
|
310
|
-
"""
|
|
311
|
-
Get list of all enabled features.
|
|
312
|
-
|
|
313
|
-
Returns:
|
|
314
|
-
List of enabled feature names
|
|
315
|
-
"""
|
|
316
|
-
features = []
|
|
317
|
-
if self.is_feature_enabled("ssl"):
|
|
318
|
-
features.append("ssl")
|
|
319
|
-
if self.is_feature_enabled("auth"):
|
|
320
|
-
features.append("auth")
|
|
321
|
-
if self.is_feature_enabled("roles"):
|
|
322
|
-
features.append("roles")
|
|
323
|
-
if self.is_feature_enabled("proxy_registration"):
|
|
324
|
-
features.append("proxy_registration")
|
|
325
|
-
if self.is_feature_enabled("security"):
|
|
326
|
-
features.append("security")
|
|
327
|
-
if self.is_feature_enabled("rate_limit"):
|
|
328
|
-
features.append("rate_limit")
|
|
329
|
-
if self.is_feature_enabled("certificates"):
|
|
330
|
-
features.append("certificates")
|
|
331
|
-
return features
|
|
332
|
-
|
|
333
|
-
def configure_auth_mode(self, mode: str, **kwargs) -> None:
|
|
334
|
-
"""
|
|
335
|
-
Configure authentication mode.
|
|
336
|
-
|
|
337
|
-
Args:
|
|
338
|
-
mode: Authentication mode (api_key, jwt, certificate, basic, oauth2)
|
|
339
|
-
**kwargs: Additional configuration parameters
|
|
340
|
-
"""
|
|
341
|
-
if mode == "api_key":
|
|
342
|
-
self.set("security.auth.methods", ["api_key"])
|
|
343
|
-
if "api_keys" in kwargs:
|
|
344
|
-
self.set("security.auth.api_keys", kwargs["api_keys"])
|
|
345
|
-
elif mode == "jwt":
|
|
346
|
-
self.set("security.auth.methods", ["jwt"])
|
|
347
|
-
if "jwt_secret" in kwargs:
|
|
348
|
-
self.set("security.auth.jwt_secret", kwargs["jwt_secret"])
|
|
349
|
-
elif mode == "certificate":
|
|
350
|
-
self.set("security.auth.methods", ["certificate"])
|
|
351
|
-
self.set("security.auth.certificate_auth", True)
|
|
352
|
-
elif mode == "basic":
|
|
353
|
-
self.set("security.auth.methods", ["basic"])
|
|
354
|
-
self.set("security.auth.basic_auth", True)
|
|
355
|
-
elif mode == "oauth2":
|
|
356
|
-
self.set("security.auth.methods", ["oauth2"])
|
|
357
|
-
if "oauth2_config" in kwargs:
|
|
358
|
-
self.set("security.auth.oauth2_config", kwargs["oauth2_config"])
|
|
359
|
-
else:
|
|
360
|
-
raise ValueError(f"Unknown authentication mode: {mode}")
|
|
361
|
-
|
|
362
|
-
def configure_proxy_registration_mode(self, mode: str, **kwargs) -> None:
|
|
363
|
-
"""
|
|
364
|
-
Configure proxy registration mode.
|
|
365
|
-
|
|
366
|
-
Args:
|
|
367
|
-
mode: Registration mode (token, certificate, api_key, none)
|
|
368
|
-
**kwargs: Additional configuration parameters
|
|
369
|
-
"""
|
|
370
|
-
if mode == "none":
|
|
371
|
-
self.set("proxy_registration.enabled", False)
|
|
372
|
-
else:
|
|
373
|
-
self.set("proxy_registration.enabled", True)
|
|
374
|
-
|
|
375
|
-
if mode == "token":
|
|
376
|
-
self.set("proxy_registration.auth_method", "token")
|
|
377
|
-
if "token" in kwargs:
|
|
378
|
-
self.set("proxy_registration.token.token", kwargs["token"])
|
|
379
|
-
elif mode == "certificate":
|
|
380
|
-
self.set("proxy_registration.auth_method", "certificate")
|
|
381
|
-
if "cert_file" in kwargs:
|
|
382
|
-
self.set(
|
|
383
|
-
"proxy_registration.certificate.cert_file", kwargs["cert_file"]
|
|
384
|
-
)
|
|
385
|
-
if "key_file" in kwargs:
|
|
386
|
-
self.set(
|
|
387
|
-
"proxy_registration.certificate.key_file", kwargs["key_file"]
|
|
388
|
-
)
|
|
389
|
-
elif mode == "api_key":
|
|
390
|
-
self.set("proxy_registration.auth_method", "api_key")
|
|
391
|
-
if "key" in kwargs:
|
|
392
|
-
self.set("proxy_registration.api_key.key", kwargs["key"])
|
|
393
|
-
|
|
394
|
-
def create_minimal_config(self) -> Dict[str, Any]:
|
|
395
|
-
"""
|
|
396
|
-
Create minimal configuration with only essential features.
|
|
397
|
-
|
|
398
|
-
Returns:
|
|
399
|
-
Minimal configuration dictionary
|
|
400
|
-
"""
|
|
401
|
-
minimal_config = self.config_data.copy()
|
|
402
|
-
|
|
403
|
-
# Disable all optional features
|
|
404
|
-
minimal_config["ssl"]["enabled"] = False
|
|
405
|
-
minimal_config["security"]["enabled"] = False
|
|
406
|
-
minimal_config["security"]["auth"]["enabled"] = False
|
|
407
|
-
minimal_config["security"]["permissions"]["enabled"] = False
|
|
408
|
-
minimal_config["security"]["rate_limit"]["enabled"] = False
|
|
409
|
-
minimal_config["security"]["certificates"]["enabled"] = False
|
|
410
|
-
minimal_config["proxy_registration"]["enabled"] = False
|
|
411
|
-
minimal_config["roles"]["enabled"] = False
|
|
412
|
-
|
|
413
|
-
return minimal_config
|
|
414
|
-
|
|
415
|
-
def create_secure_config(self) -> Dict[str, Any]:
|
|
416
|
-
"""
|
|
417
|
-
Create secure configuration with all security features enabled.
|
|
418
|
-
|
|
419
|
-
Returns:
|
|
420
|
-
Secure configuration dictionary
|
|
421
|
-
"""
|
|
422
|
-
secure_config = self.config_data.copy()
|
|
423
|
-
|
|
424
|
-
# Enable all security features
|
|
425
|
-
secure_config["ssl"]["enabled"] = True
|
|
426
|
-
secure_config["security"]["enabled"] = True
|
|
427
|
-
secure_config["security"]["auth"]["enabled"] = True
|
|
428
|
-
secure_config["security"]["permissions"]["enabled"] = True
|
|
429
|
-
secure_config["security"]["rate_limit"]["enabled"] = True
|
|
430
|
-
secure_config["security"]["certificates"]["enabled"] = True
|
|
431
|
-
secure_config["proxy_registration"]["enabled"] = True
|
|
432
|
-
secure_config["roles"]["enabled"] = True
|
|
433
|
-
|
|
434
|
-
return secure_config
|
|
435
|
-
|
|
436
|
-
def _validate_security_config(self) -> None:
|
|
437
|
-
"""
|
|
438
|
-
Validate security configuration and log warnings for incomplete setup.
|
|
439
|
-
"""
|
|
440
|
-
if not self.get("security.enabled", False):
|
|
441
|
-
return
|
|
442
|
-
|
|
443
|
-
# Check if security is enabled but no authentication methods are configured
|
|
444
|
-
tokens = self.get("security.tokens", {})
|
|
445
|
-
roles = self.get("security.roles", {})
|
|
446
|
-
roles_file = self.get("security.roles_file")
|
|
447
|
-
|
|
448
|
-
has_tokens = bool(tokens and any(tokens.values()))
|
|
449
|
-
has_roles = bool(roles and any(roles.values()))
|
|
450
|
-
has_roles_file = bool(roles_file and os.path.exists(roles_file))
|
|
451
|
-
|
|
452
|
-
if not (has_tokens or has_roles or has_roles_file):
|
|
453
|
-
logger.warning(
|
|
454
|
-
"Security is enabled but no authentication methods are configured. "
|
|
455
|
-
"Please configure tokens, roles, or roles_file in the security section."
|
|
456
|
-
)
|
|
457
|
-
|
|
458
|
-
def _apply_hostname_check_logic(self) -> None:
|
|
459
|
-
"""
|
|
460
|
-
Apply hostname check logic based on protocol configuration.
|
|
461
|
-
chk_hostname should be True for HTTPS/mTLS protocols, False for HTTP.
|
|
462
|
-
Only set default values if chk_hostname is not explicitly configured.
|
|
463
|
-
"""
|
|
464
|
-
protocol = self.get("server.protocol", "http")
|
|
465
|
-
ssl_enabled = self.get("transport.ssl.enabled", False)
|
|
466
|
-
|
|
467
|
-
# Check if chk_hostname is explicitly set by the user
|
|
468
|
-
# We check if it was set by looking for a special flag
|
|
469
|
-
transport_section = self.config_data.get("transport", {})
|
|
470
|
-
ssl_section = transport_section.get("ssl", {})
|
|
471
|
-
chk_hostname_explicitly_set = ssl_section.get("_chk_hostname_user_set", False)
|
|
472
|
-
|
|
473
|
-
# Set chk_hostname based on protocol only if not explicitly set
|
|
474
|
-
if not chk_hostname_explicitly_set:
|
|
475
|
-
if protocol in ["https", "mtls"]:
|
|
476
|
-
# For HTTPS/mTLS, enable hostname checking by default
|
|
477
|
-
self.set("transport.ssl.chk_hostname", True)
|
|
478
|
-
logger.debug(f"Set chk_hostname=True for protocol {protocol} (default)")
|
|
479
|
-
else:
|
|
480
|
-
# For HTTP, disable hostname checking
|
|
481
|
-
self.set("transport.ssl.chk_hostname", False)
|
|
482
|
-
logger.debug(f"Set chk_hostname=False for protocol {protocol} (default)")
|
|
483
|
-
else:
|
|
484
|
-
# Log the explicitly set value
|
|
485
|
-
chk_hostname_value = self.get("transport.ssl.chk_hostname")
|
|
486
|
-
logger.debug(f"Using explicitly set chk_hostname={chk_hostname_value} for protocol {protocol}")
|
|
20
|
+
Returns:
|
|
21
|
+
Configuration instance
|
|
22
|
+
"""
|
|
23
|
+
global _config
|
|
24
|
+
if _config is None:
|
|
25
|
+
_config = Config()
|
|
26
|
+
return _config
|
|
487
27
|
|
|
488
|
-
def validate(self) -> List[ValidationResult]:
|
|
489
|
-
"""
|
|
490
|
-
Validate current configuration.
|
|
491
|
-
|
|
492
|
-
Returns:
|
|
493
|
-
List of validation results
|
|
494
|
-
|
|
495
|
-
Raises:
|
|
496
|
-
ConfigError: If validation is not available or critical errors are found
|
|
497
|
-
"""
|
|
498
|
-
if not VALIDATION_AVAILABLE:
|
|
499
|
-
raise ConfigError("Configuration validation is not available. Please install the package properly.")
|
|
500
|
-
|
|
501
|
-
if not self.validator:
|
|
502
|
-
self.validator = ConfigValidator()
|
|
503
|
-
|
|
504
|
-
self.validator.config_data = self.config_data
|
|
505
|
-
self.validation_results = self.validator.validate_config()
|
|
506
|
-
|
|
507
|
-
# Log validation results
|
|
508
|
-
for result in self.validation_results:
|
|
509
|
-
if result.level == ValidationLevel.ERROR:
|
|
510
|
-
get_global_logger().error(f"Configuration error: {result.message}")
|
|
511
|
-
elif result.level == ValidationLevel.WARNING:
|
|
512
|
-
get_global_logger().warning(f"Configuration warning: {result.message}")
|
|
513
|
-
else:
|
|
514
|
-
get_global_logger().info(f"Configuration info: {result.message}")
|
|
515
|
-
|
|
516
|
-
# Raise ConfigError if there are critical errors
|
|
517
|
-
errors = [r for r in self.validation_results if r.level == ValidationLevel.ERROR]
|
|
518
|
-
if errors:
|
|
519
|
-
error_summary = "\n".join([f"[{r.section}.{r.key if r.key else 'root'}] {r.message}" for r in errors])
|
|
520
|
-
raise ConfigError(f"Configuration validation failed with {len(errors)} error(s):\n{error_summary}", errors)
|
|
521
|
-
|
|
522
|
-
return self.validation_results
|
|
523
|
-
|
|
524
|
-
def is_valid(self) -> bool:
|
|
525
|
-
"""Check if configuration is valid."""
|
|
526
|
-
if not VALIDATION_AVAILABLE:
|
|
527
|
-
return True
|
|
528
|
-
|
|
529
|
-
return all(result.level != ValidationLevel.ERROR for result in self.validation_results)
|
|
530
|
-
|
|
531
|
-
def get_validation_errors(self) -> List[ValidationResult]:
|
|
532
|
-
"""Get all validation errors."""
|
|
533
|
-
return [r for r in self.validation_results if r.level == ValidationLevel.ERROR]
|
|
534
|
-
|
|
535
|
-
def get_validation_warnings(self) -> List[ValidationResult]:
|
|
536
|
-
"""Get all validation warnings."""
|
|
537
|
-
return [r for r in self.validation_results if r.level == ValidationLevel.WARNING]
|
|
538
|
-
|
|
539
|
-
def get_validation_summary(self) -> Dict[str, Any]:
|
|
540
|
-
"""Get validation summary."""
|
|
541
|
-
if not VALIDATION_AVAILABLE or not self.validator:
|
|
542
|
-
return {"total_issues": 0, "errors": 0, "warnings": 0, "info": 0, "is_valid": True}
|
|
543
|
-
|
|
544
|
-
return self.validator.get_validation_summary()
|
|
545
|
-
|
|
546
|
-
def print_validation_report(self) -> None:
|
|
547
|
-
"""Print detailed validation report."""
|
|
548
|
-
if not VALIDATION_AVAILABLE or not self.validator:
|
|
549
|
-
print("Configuration validation not available")
|
|
550
|
-
return
|
|
551
|
-
|
|
552
|
-
self.validator.print_validation_report()
|
|
553
|
-
|
|
554
|
-
def check_feature_requirements(self, feature: str) -> List[ValidationResult]:
|
|
555
|
-
"""Check if all requirements for a feature are met."""
|
|
556
|
-
if not VALIDATION_AVAILABLE:
|
|
557
|
-
return []
|
|
558
|
-
|
|
559
|
-
results = []
|
|
560
|
-
|
|
561
|
-
if feature == "security":
|
|
562
|
-
if self.get("security.enabled", False):
|
|
563
|
-
tokens = self.get("security.tokens", {})
|
|
564
|
-
roles = self.get("security.roles", {})
|
|
565
|
-
roles_file = self.get("security.roles_file")
|
|
566
|
-
|
|
567
|
-
has_tokens = bool(tokens and any(tokens.values()))
|
|
568
|
-
has_roles = bool(roles and any(roles.values()))
|
|
569
|
-
has_roles_file = bool(roles_file and os.path.exists(roles_file))
|
|
570
|
-
|
|
571
|
-
if not (has_tokens or has_roles or has_roles_file):
|
|
572
|
-
results.append(ValidationResult(
|
|
573
|
-
level=ValidationLevel.WARNING,
|
|
574
|
-
message="Security is enabled but no authentication methods are configured",
|
|
575
|
-
section="security",
|
|
576
|
-
suggestion="Configure tokens, roles, or roles_file"
|
|
577
|
-
))
|
|
578
|
-
|
|
579
|
-
elif feature == "roles":
|
|
580
|
-
if self.get("roles.enabled", False):
|
|
581
|
-
config_file = self.get("roles.config_file")
|
|
582
|
-
if not config_file:
|
|
583
|
-
results.append(ValidationResult(
|
|
584
|
-
level=ValidationLevel.ERROR,
|
|
585
|
-
message="Roles are enabled but config_file is not specified",
|
|
586
|
-
section="roles",
|
|
587
|
-
key="config_file"
|
|
588
|
-
))
|
|
589
|
-
elif not os.path.exists(config_file):
|
|
590
|
-
results.append(ValidationResult(
|
|
591
|
-
level=ValidationLevel.ERROR,
|
|
592
|
-
message=f"Roles config file '{config_file}' does not exist",
|
|
593
|
-
section="roles",
|
|
594
|
-
key="config_file"
|
|
595
|
-
))
|
|
596
|
-
|
|
597
|
-
elif feature == "ssl":
|
|
598
|
-
if self.get("ssl.enabled", False):
|
|
599
|
-
cert_file = self.get("ssl.cert_file")
|
|
600
|
-
key_file = self.get("ssl.key_file")
|
|
601
|
-
|
|
602
|
-
if not cert_file:
|
|
603
|
-
results.append(ValidationResult(
|
|
604
|
-
level=ValidationLevel.ERROR,
|
|
605
|
-
message="SSL is enabled but cert_file is not specified",
|
|
606
|
-
section="ssl",
|
|
607
|
-
key="cert_file"
|
|
608
|
-
))
|
|
609
|
-
elif not os.path.exists(cert_file):
|
|
610
|
-
results.append(ValidationResult(
|
|
611
|
-
level=ValidationLevel.ERROR,
|
|
612
|
-
message=f"SSL certificate file '{cert_file}' does not exist",
|
|
613
|
-
section="ssl",
|
|
614
|
-
key="cert_file"
|
|
615
|
-
))
|
|
616
|
-
|
|
617
|
-
if not key_file:
|
|
618
|
-
results.append(ValidationResult(
|
|
619
|
-
level=ValidationLevel.ERROR,
|
|
620
|
-
message="SSL is enabled but key_file is not specified",
|
|
621
|
-
section="ssl",
|
|
622
|
-
key="key_file"
|
|
623
|
-
))
|
|
624
|
-
elif not os.path.exists(key_file):
|
|
625
|
-
results.append(ValidationResult(
|
|
626
|
-
level=ValidationLevel.ERROR,
|
|
627
|
-
message=f"SSL key file '{key_file}' does not exist",
|
|
628
|
-
section="ssl",
|
|
629
|
-
key="key_file"
|
|
630
|
-
))
|
|
631
|
-
|
|
632
|
-
return results
|
|
633
28
|
|
|
634
29
|
|
|
635
|
-
# Singleton instance - will be created when needed
|
|
636
|
-
config = None
|
|
637
30
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
global config
|
|
641
|
-
if config is None:
|
|
642
|
-
config = Config()
|
|
643
|
-
return config
|
|
31
|
+
# For backward compatibility
|
|
32
|
+
config = get_config()
|