claude-mpm 4.4.0__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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/WORKFLOW.md +2 -14
- 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/configure.py +2 -29
- claude_mpm/cli/commands/doctor.py +44 -5
- claude_mpm/cli/commands/mpm_init.py +117 -63
- claude_mpm/cli/parsers/configure_parser.py +6 -15
- 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 +38 -0
- claude_mpm/core/framework/formatters/__init__.py +11 -0
- claude_mpm/core/framework/formatters/capability_generator.py +367 -0
- claude_mpm/core/framework/formatters/content_formatter.py +288 -0
- claude_mpm/core/framework/formatters/context_generator.py +184 -0
- claude_mpm/core/framework/loaders/__init__.py +13 -0
- claude_mpm/core/framework/loaders/agent_loader.py +206 -0
- claude_mpm/core/framework/loaders/file_loader.py +223 -0
- claude_mpm/core/framework/loaders/instruction_loader.py +161 -0
- claude_mpm/core/framework/loaders/packaged_loader.py +232 -0
- claude_mpm/core/framework/processors/__init__.py +11 -0
- claude_mpm/core/framework/processors/memory_processor.py +230 -0
- claude_mpm/core/framework/processors/metadata_processor.py +146 -0
- claude_mpm/core/framework/processors/template_processor.py +244 -0
- claude_mpm/core/framework_loader.py +298 -1795
- 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/__init__.py +9 -1
- claude_mpm/hooks/base_hook.py +1 -0
- claude_mpm/hooks/instruction_reinforcement.py +1 -0
- claude_mpm/hooks/kuzu_memory_hook.py +359 -0
- 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 +2 -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 +399 -0
- claude_mpm/services/diagnostics/diagnostic_runner.py +4 -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 +67 -4
- claude_mpm/services/mcp_gateway/core/process_pool.py +320 -0
- claude_mpm/services/mcp_gateway/core/startup_verification.py +2 -2
- claude_mpm/services/mcp_gateway/main.py +3 -13
- claude_mpm/services/mcp_gateway/server/stdio_server.py +4 -10
- claude_mpm/services/mcp_gateway/tools/__init__.py +14 -2
- claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +38 -6
- claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +527 -0
- 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/shared/__init__.py +2 -1
- claude_mpm/services/shared/service_factory.py +8 -5
- 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 +175 -0
- claude_mpm/services/unified/config_strategies/config_schema.py +735 -0
- claude_mpm/services/unified/config_strategies/context_strategy.py +750 -0
- claude_mpm/services/unified/config_strategies/error_handling_strategy.py +1009 -0
- claude_mpm/services/unified/config_strategies/file_loader_strategy.py +879 -0
- claude_mpm/services/unified/config_strategies/unified_config_service.py +814 -0
- claude_mpm/services/unified/config_strategies/validation_strategy.py +1144 -0
- 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.0.dist-info → claude_mpm-4.4.4.dist-info}/METADATA +23 -17
- {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.4.dist-info}/RECORD +126 -105
- claude_mpm/cli/commands/configure_tui.py +0 -1927
- claude_mpm/services/mcp_gateway/tools/ticket_tools.py +0 -645
- claude_mpm/services/mcp_gateway/tools/unified_ticket_tool.py +0 -602
- {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.4.dist-info}/WHEEL +0 -0
- {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.4.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.4.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,750 @@
|
|
1
|
+
"""
|
2
|
+
Context Strategy - Context-based lifecycle management for configurations
|
3
|
+
Part of Phase 3 Configuration Consolidation
|
4
|
+
"""
|
5
|
+
|
6
|
+
import threading
|
7
|
+
import weakref
|
8
|
+
from abc import ABC, abstractmethod
|
9
|
+
from collections import OrderedDict
|
10
|
+
from contextlib import contextmanager
|
11
|
+
from dataclasses import dataclass, field
|
12
|
+
from datetime import datetime, timedelta
|
13
|
+
from enum import Enum
|
14
|
+
from pathlib import Path
|
15
|
+
from typing import Any, Callable, Dict, List, Optional, Set, Union
|
16
|
+
|
17
|
+
from claude_mpm.core.logging_utils import get_logger
|
18
|
+
|
19
|
+
from .unified_config_service import IConfigStrategy
|
20
|
+
|
21
|
+
|
22
|
+
class ContextScope(Enum):
|
23
|
+
"""Configuration context scopes"""
|
24
|
+
|
25
|
+
GLOBAL = "global" # Application-wide
|
26
|
+
SESSION = "session" # User session
|
27
|
+
PROJECT = "project" # Project-specific
|
28
|
+
AGENT = "agent" # Agent-specific
|
29
|
+
SERVICE = "service" # Service-specific
|
30
|
+
TRANSACTION = "transaction" # Transaction-specific
|
31
|
+
REQUEST = "request" # Request-specific
|
32
|
+
THREAD = "thread" # Thread-local
|
33
|
+
TEMPORARY = "temporary" # Temporary context
|
34
|
+
|
35
|
+
|
36
|
+
class ContextLifecycle(Enum):
|
37
|
+
"""Context lifecycle states"""
|
38
|
+
|
39
|
+
CREATED = "created"
|
40
|
+
INITIALIZING = "initializing"
|
41
|
+
ACTIVE = "active"
|
42
|
+
SUSPENDED = "suspended"
|
43
|
+
CLOSING = "closing"
|
44
|
+
CLOSED = "closed"
|
45
|
+
|
46
|
+
|
47
|
+
@dataclass
|
48
|
+
class ContextMetadata:
|
49
|
+
"""Metadata for context tracking"""
|
50
|
+
|
51
|
+
id: str
|
52
|
+
scope: ContextScope
|
53
|
+
lifecycle: ContextLifecycle
|
54
|
+
created_at: datetime
|
55
|
+
updated_at: datetime
|
56
|
+
parent_id: Optional[str] = None
|
57
|
+
children: List[str] = field(default_factory=list)
|
58
|
+
attributes: Dict[str, Any] = field(default_factory=dict)
|
59
|
+
ttl: Optional[timedelta] = None
|
60
|
+
expires_at: Optional[datetime] = None
|
61
|
+
|
62
|
+
|
63
|
+
@dataclass
|
64
|
+
class ContextConfig:
|
65
|
+
"""Configuration within a context"""
|
66
|
+
|
67
|
+
context_id: str
|
68
|
+
data: Dict[str, Any]
|
69
|
+
overrides: Dict[str, Any] = field(default_factory=dict)
|
70
|
+
defaults: Dict[str, Any] = field(default_factory=dict)
|
71
|
+
locked_keys: Set[str] = field(default_factory=set)
|
72
|
+
watchers: Dict[str, List[Callable]] = field(default_factory=dict)
|
73
|
+
|
74
|
+
|
75
|
+
class BaseContextManager(ABC):
|
76
|
+
"""Base class for context managers"""
|
77
|
+
|
78
|
+
def __init__(self):
|
79
|
+
self.logger = get_logger(self.__class__.__name__)
|
80
|
+
self.contexts: Dict[str, ContextMetadata] = {}
|
81
|
+
self.configs: Dict[str, ContextConfig] = {}
|
82
|
+
self._lock = threading.RLock()
|
83
|
+
|
84
|
+
@abstractmethod
|
85
|
+
def create_context(self, scope: ContextScope, **kwargs) -> str:
|
86
|
+
"""Create a new context"""
|
87
|
+
|
88
|
+
@abstractmethod
|
89
|
+
def get_context(self, context_id: str) -> Optional[ContextMetadata]:
|
90
|
+
"""Get context metadata"""
|
91
|
+
|
92
|
+
@abstractmethod
|
93
|
+
def close_context(self, context_id: str):
|
94
|
+
"""Close a context"""
|
95
|
+
|
96
|
+
|
97
|
+
class HierarchicalContextManager(BaseContextManager):
|
98
|
+
"""Manages hierarchical context relationships"""
|
99
|
+
|
100
|
+
def __init__(self):
|
101
|
+
super().__init__()
|
102
|
+
self.context_stack = threading.local()
|
103
|
+
self.context_hierarchy: Dict[str, List[str]] = {} # parent -> children
|
104
|
+
|
105
|
+
def create_context(
|
106
|
+
self,
|
107
|
+
scope: ContextScope,
|
108
|
+
parent_id: Optional[str] = None,
|
109
|
+
ttl: Optional[timedelta] = None,
|
110
|
+
**kwargs,
|
111
|
+
) -> str:
|
112
|
+
"""Create a new hierarchical context"""
|
113
|
+
with self._lock:
|
114
|
+
# Generate context ID
|
115
|
+
context_id = self._generate_context_id(scope)
|
116
|
+
|
117
|
+
# Use current context as parent if not specified
|
118
|
+
if parent_id is None and hasattr(self.context_stack, "stack"):
|
119
|
+
if self.context_stack.stack:
|
120
|
+
parent_id = self.context_stack.stack[-1]
|
121
|
+
|
122
|
+
# Create metadata
|
123
|
+
metadata = ContextMetadata(
|
124
|
+
id=context_id,
|
125
|
+
scope=scope,
|
126
|
+
lifecycle=ContextLifecycle.CREATED,
|
127
|
+
created_at=datetime.now(),
|
128
|
+
updated_at=datetime.now(),
|
129
|
+
parent_id=parent_id,
|
130
|
+
ttl=ttl,
|
131
|
+
expires_at=datetime.now() + ttl if ttl else None,
|
132
|
+
attributes=kwargs,
|
133
|
+
)
|
134
|
+
|
135
|
+
# Store context
|
136
|
+
self.contexts[context_id] = metadata
|
137
|
+
|
138
|
+
# Update hierarchy
|
139
|
+
if parent_id:
|
140
|
+
if parent_id in self.contexts:
|
141
|
+
self.contexts[parent_id].children.append(context_id)
|
142
|
+
if parent_id not in self.context_hierarchy:
|
143
|
+
self.context_hierarchy[parent_id] = []
|
144
|
+
self.context_hierarchy[parent_id].append(context_id)
|
145
|
+
|
146
|
+
# Initialize context config
|
147
|
+
self.configs[context_id] = ContextConfig(context_id=context_id, data={})
|
148
|
+
|
149
|
+
# Set lifecycle to active
|
150
|
+
metadata.lifecycle = ContextLifecycle.ACTIVE
|
151
|
+
|
152
|
+
self.logger.debug(f"Created context: {context_id} (scope={scope.value})")
|
153
|
+
return context_id
|
154
|
+
|
155
|
+
def get_context(self, context_id: str) -> Optional[ContextMetadata]:
|
156
|
+
"""Get context metadata"""
|
157
|
+
with self._lock:
|
158
|
+
context = self.contexts.get(context_id)
|
159
|
+
|
160
|
+
# Check expiration
|
161
|
+
if context and context.expires_at:
|
162
|
+
if datetime.now() > context.expires_at:
|
163
|
+
self.close_context(context_id)
|
164
|
+
return None
|
165
|
+
|
166
|
+
return context
|
167
|
+
|
168
|
+
def close_context(self, context_id: str):
|
169
|
+
"""Close a context and its children"""
|
170
|
+
with self._lock:
|
171
|
+
if context_id not in self.contexts:
|
172
|
+
return
|
173
|
+
|
174
|
+
context = self.contexts[context_id]
|
175
|
+
context.lifecycle = ContextLifecycle.CLOSING
|
176
|
+
|
177
|
+
# Close all children first
|
178
|
+
for child_id in context.children[:]:
|
179
|
+
self.close_context(child_id)
|
180
|
+
|
181
|
+
# Remove from parent's children
|
182
|
+
if context.parent_id and context.parent_id in self.contexts:
|
183
|
+
parent = self.contexts[context.parent_id]
|
184
|
+
if context_id in parent.children:
|
185
|
+
parent.children.remove(context_id)
|
186
|
+
|
187
|
+
# Clean up hierarchy
|
188
|
+
if context_id in self.context_hierarchy:
|
189
|
+
del self.context_hierarchy[context_id]
|
190
|
+
|
191
|
+
# Remove configs
|
192
|
+
if context_id in self.configs:
|
193
|
+
del self.configs[context_id]
|
194
|
+
|
195
|
+
# Remove context
|
196
|
+
del self.contexts[context_id]
|
197
|
+
|
198
|
+
context.lifecycle = ContextLifecycle.CLOSED
|
199
|
+
self.logger.debug(f"Closed context: {context_id}")
|
200
|
+
|
201
|
+
def get_context_chain(self, context_id: str) -> List[str]:
|
202
|
+
"""Get the chain of contexts from root to specified context"""
|
203
|
+
chain = []
|
204
|
+
current_id = context_id
|
205
|
+
|
206
|
+
while current_id:
|
207
|
+
if current_id in self.contexts:
|
208
|
+
chain.insert(0, current_id)
|
209
|
+
current_id = self.contexts[current_id].parent_id
|
210
|
+
else:
|
211
|
+
break
|
212
|
+
|
213
|
+
return chain
|
214
|
+
|
215
|
+
def _generate_context_id(self, scope: ContextScope) -> str:
|
216
|
+
"""Generate unique context ID"""
|
217
|
+
import uuid
|
218
|
+
|
219
|
+
return f"{scope.value}_{uuid.uuid4().hex[:8]}"
|
220
|
+
|
221
|
+
@contextmanager
|
222
|
+
def context(self, scope: ContextScope, **kwargs):
|
223
|
+
"""Context manager for automatic context lifecycle"""
|
224
|
+
context_id = self.create_context(scope, **kwargs)
|
225
|
+
|
226
|
+
# Push to thread-local stack
|
227
|
+
if not hasattr(self.context_stack, "stack"):
|
228
|
+
self.context_stack.stack = []
|
229
|
+
self.context_stack.stack.append(context_id)
|
230
|
+
|
231
|
+
try:
|
232
|
+
yield context_id
|
233
|
+
finally:
|
234
|
+
# Pop from stack
|
235
|
+
if self.context_stack.stack and self.context_stack.stack[-1] == context_id:
|
236
|
+
self.context_stack.stack.pop()
|
237
|
+
|
238
|
+
# Close context
|
239
|
+
self.close_context(context_id)
|
240
|
+
|
241
|
+
|
242
|
+
class ScopedConfigManager:
|
243
|
+
"""Manages configuration within scoped contexts"""
|
244
|
+
|
245
|
+
def __init__(self, context_manager: HierarchicalContextManager):
|
246
|
+
self.logger = get_logger(self.__class__.__name__)
|
247
|
+
self.context_manager = context_manager
|
248
|
+
self._lock = threading.RLock()
|
249
|
+
|
250
|
+
def get_config(
|
251
|
+
self, context_id: str, key: Optional[str] = None, inherit: bool = True
|
252
|
+
) -> Any:
|
253
|
+
"""Get configuration value from context"""
|
254
|
+
with self._lock:
|
255
|
+
if context_id not in self.context_manager.configs:
|
256
|
+
return None
|
257
|
+
|
258
|
+
if inherit:
|
259
|
+
# Get merged config from context chain
|
260
|
+
config = self._get_inherited_config(context_id)
|
261
|
+
else:
|
262
|
+
# Get config from this context only
|
263
|
+
config = self.context_manager.configs[context_id].data
|
264
|
+
|
265
|
+
if key:
|
266
|
+
return self._get_nested_value(config, key)
|
267
|
+
return config
|
268
|
+
|
269
|
+
def set_config(
|
270
|
+
self,
|
271
|
+
context_id: str,
|
272
|
+
key: str,
|
273
|
+
value: Any,
|
274
|
+
override: bool = False,
|
275
|
+
lock: bool = False,
|
276
|
+
):
|
277
|
+
"""Set configuration value in context"""
|
278
|
+
with self._lock:
|
279
|
+
if context_id not in self.context_manager.configs:
|
280
|
+
raise ValueError(f"Context not found: {context_id}")
|
281
|
+
|
282
|
+
config = self.context_manager.configs[context_id]
|
283
|
+
|
284
|
+
# Check if key is locked
|
285
|
+
if key in config.locked_keys and not override:
|
286
|
+
raise ValueError(f"Configuration key is locked: {key}")
|
287
|
+
|
288
|
+
# Set value
|
289
|
+
if override:
|
290
|
+
config.overrides[key] = value
|
291
|
+
else:
|
292
|
+
self._set_nested_value(config.data, key, value)
|
293
|
+
|
294
|
+
# Lock key if requested
|
295
|
+
if lock:
|
296
|
+
config.locked_keys.add(key)
|
297
|
+
|
298
|
+
# Trigger watchers
|
299
|
+
self._trigger_watchers(context_id, key, value)
|
300
|
+
|
301
|
+
# Update context metadata
|
302
|
+
if context_id in self.context_manager.contexts:
|
303
|
+
self.context_manager.contexts[context_id].updated_at = datetime.now()
|
304
|
+
|
305
|
+
def _get_inherited_config(self, context_id: str) -> Dict[str, Any]:
|
306
|
+
"""Get merged configuration from context hierarchy"""
|
307
|
+
chain = self.context_manager.get_context_chain(context_id)
|
308
|
+
merged = {}
|
309
|
+
|
310
|
+
for ctx_id in chain:
|
311
|
+
if ctx_id in self.context_manager.configs:
|
312
|
+
config = self.context_manager.configs[ctx_id]
|
313
|
+
|
314
|
+
# Apply defaults first
|
315
|
+
merged = self._deep_merge(config.defaults, merged)
|
316
|
+
|
317
|
+
# Apply data
|
318
|
+
merged = self._deep_merge(merged, config.data)
|
319
|
+
|
320
|
+
# Apply overrides last
|
321
|
+
merged = self._deep_merge(merged, config.overrides)
|
322
|
+
|
323
|
+
return merged
|
324
|
+
|
325
|
+
def _deep_merge(self, base: Dict, override: Dict) -> Dict:
|
326
|
+
"""Deep merge two dictionaries"""
|
327
|
+
result = base.copy()
|
328
|
+
|
329
|
+
for key, value in override.items():
|
330
|
+
if (
|
331
|
+
key in result
|
332
|
+
and isinstance(result[key], dict)
|
333
|
+
and isinstance(value, dict)
|
334
|
+
):
|
335
|
+
result[key] = self._deep_merge(result[key], value)
|
336
|
+
else:
|
337
|
+
result[key] = value
|
338
|
+
|
339
|
+
return result
|
340
|
+
|
341
|
+
def _get_nested_value(self, config: Dict, key: str) -> Any:
|
342
|
+
"""Get nested value using dot notation"""
|
343
|
+
parts = key.split(".")
|
344
|
+
current = config
|
345
|
+
|
346
|
+
for part in parts:
|
347
|
+
if isinstance(current, dict) and part in current:
|
348
|
+
current = current[part]
|
349
|
+
else:
|
350
|
+
return None
|
351
|
+
|
352
|
+
return current
|
353
|
+
|
354
|
+
def _set_nested_value(self, config: Dict, key: str, value: Any):
|
355
|
+
"""Set nested value using dot notation"""
|
356
|
+
parts = key.split(".")
|
357
|
+
current = config
|
358
|
+
|
359
|
+
for part in parts[:-1]:
|
360
|
+
if part not in current:
|
361
|
+
current[part] = {}
|
362
|
+
current = current[part]
|
363
|
+
|
364
|
+
current[parts[-1]] = value
|
365
|
+
|
366
|
+
def _trigger_watchers(self, context_id: str, key: str, value: Any):
|
367
|
+
"""Trigger configuration watchers"""
|
368
|
+
if context_id in self.context_manager.configs:
|
369
|
+
config = self.context_manager.configs[context_id]
|
370
|
+
|
371
|
+
# Trigger exact match watchers
|
372
|
+
if key in config.watchers:
|
373
|
+
for watcher in config.watchers[key]:
|
374
|
+
try:
|
375
|
+
watcher(key, value, context_id)
|
376
|
+
except Exception as e:
|
377
|
+
self.logger.error(f"Watcher failed: {e}")
|
378
|
+
|
379
|
+
# Trigger pattern watchers
|
380
|
+
for pattern, watchers in config.watchers.items():
|
381
|
+
if "*" in pattern or "?" in pattern:
|
382
|
+
import fnmatch
|
383
|
+
|
384
|
+
if fnmatch.fnmatch(key, pattern):
|
385
|
+
for watcher in watchers:
|
386
|
+
try:
|
387
|
+
watcher(key, value, context_id)
|
388
|
+
except Exception as e:
|
389
|
+
self.logger.error(f"Pattern watcher failed: {e}")
|
390
|
+
|
391
|
+
|
392
|
+
class IsolatedContextManager:
|
393
|
+
"""Manages isolated configuration contexts"""
|
394
|
+
|
395
|
+
def __init__(self):
|
396
|
+
self.logger = get_logger(self.__class__.__name__)
|
397
|
+
self.isolated_contexts: Dict[str, Dict[str, Any]] = {}
|
398
|
+
self._lock = threading.RLock()
|
399
|
+
|
400
|
+
def create_isolated_context(
|
401
|
+
self, base_config: Optional[Dict[str, Any]] = None
|
402
|
+
) -> str:
|
403
|
+
"""Create an isolated context with no inheritance"""
|
404
|
+
import uuid
|
405
|
+
|
406
|
+
context_id = f"isolated_{uuid.uuid4().hex[:8]}"
|
407
|
+
|
408
|
+
with self._lock:
|
409
|
+
self.isolated_contexts[context_id] = base_config or {}
|
410
|
+
self.logger.debug(f"Created isolated context: {context_id}")
|
411
|
+
|
412
|
+
return context_id
|
413
|
+
|
414
|
+
def get_isolated_config(self, context_id: str) -> Dict[str, Any]:
|
415
|
+
"""Get configuration from isolated context"""
|
416
|
+
with self._lock:
|
417
|
+
return self.isolated_contexts.get(context_id, {}).copy()
|
418
|
+
|
419
|
+
def update_isolated_config(self, context_id: str, updates: Dict[str, Any]):
|
420
|
+
"""Update isolated context configuration"""
|
421
|
+
with self._lock:
|
422
|
+
if context_id in self.isolated_contexts:
|
423
|
+
self.isolated_contexts[context_id].update(updates)
|
424
|
+
|
425
|
+
def close_isolated_context(self, context_id: str):
|
426
|
+
"""Close isolated context"""
|
427
|
+
with self._lock:
|
428
|
+
if context_id in self.isolated_contexts:
|
429
|
+
del self.isolated_contexts[context_id]
|
430
|
+
self.logger.debug(f"Closed isolated context: {context_id}")
|
431
|
+
|
432
|
+
|
433
|
+
class ThreadLocalContextManager:
|
434
|
+
"""Manages thread-local configuration contexts"""
|
435
|
+
|
436
|
+
def __init__(self):
|
437
|
+
self.logger = get_logger(self.__class__.__name__)
|
438
|
+
self.thread_contexts = threading.local()
|
439
|
+
self._global_registry: weakref.WeakValueDictionary = (
|
440
|
+
weakref.WeakValueDictionary()
|
441
|
+
)
|
442
|
+
|
443
|
+
def get_thread_context(self) -> Optional[Dict[str, Any]]:
|
444
|
+
"""Get current thread's context"""
|
445
|
+
if hasattr(self.thread_contexts, "config"):
|
446
|
+
return self.thread_contexts.config
|
447
|
+
return None
|
448
|
+
|
449
|
+
def set_thread_context(self, config: Dict[str, Any]):
|
450
|
+
"""Set current thread's context"""
|
451
|
+
self.thread_contexts.config = config
|
452
|
+
self._global_registry[threading.current_thread().ident] = config
|
453
|
+
|
454
|
+
def clear_thread_context(self):
|
455
|
+
"""Clear current thread's context"""
|
456
|
+
if hasattr(self.thread_contexts, "config"):
|
457
|
+
delattr(self.thread_contexts, "config")
|
458
|
+
|
459
|
+
@contextmanager
|
460
|
+
def thread_context(self, config: Dict[str, Any]):
|
461
|
+
"""Context manager for thread-local configuration"""
|
462
|
+
old_config = self.get_thread_context()
|
463
|
+
self.set_thread_context(config)
|
464
|
+
|
465
|
+
try:
|
466
|
+
yield config
|
467
|
+
finally:
|
468
|
+
if old_config is not None:
|
469
|
+
self.set_thread_context(old_config)
|
470
|
+
else:
|
471
|
+
self.clear_thread_context()
|
472
|
+
|
473
|
+
|
474
|
+
class CachingContextManager:
|
475
|
+
"""Manages context-aware configuration caching"""
|
476
|
+
|
477
|
+
def __init__(self, max_size: int = 1000):
|
478
|
+
self.logger = get_logger(self.__class__.__name__)
|
479
|
+
self.cache: OrderedDict = OrderedDict()
|
480
|
+
self.max_size = max_size
|
481
|
+
self._lock = threading.RLock()
|
482
|
+
self.hit_count = 0
|
483
|
+
self.miss_count = 0
|
484
|
+
|
485
|
+
def get_cached(self, context_id: str, key: str) -> Optional[Any]:
|
486
|
+
"""Get cached value for context"""
|
487
|
+
cache_key = f"{context_id}:{key}"
|
488
|
+
|
489
|
+
with self._lock:
|
490
|
+
if cache_key in self.cache:
|
491
|
+
# Move to end (LRU)
|
492
|
+
self.cache.move_to_end(cache_key)
|
493
|
+
self.hit_count += 1
|
494
|
+
return self.cache[cache_key]
|
495
|
+
|
496
|
+
self.miss_count += 1
|
497
|
+
return None
|
498
|
+
|
499
|
+
def set_cached(self, context_id: str, key: str, value: Any):
|
500
|
+
"""Cache value for context"""
|
501
|
+
cache_key = f"{context_id}:{key}"
|
502
|
+
|
503
|
+
with self._lock:
|
504
|
+
# Remove oldest if at capacity
|
505
|
+
if len(self.cache) >= self.max_size:
|
506
|
+
self.cache.popitem(last=False)
|
507
|
+
|
508
|
+
self.cache[cache_key] = value
|
509
|
+
|
510
|
+
def invalidate_context(self, context_id: str):
|
511
|
+
"""Invalidate all cached values for context"""
|
512
|
+
with self._lock:
|
513
|
+
keys_to_remove = [
|
514
|
+
k for k in self.cache.keys() if k.startswith(f"{context_id}:")
|
515
|
+
]
|
516
|
+
|
517
|
+
for key in keys_to_remove:
|
518
|
+
del self.cache[key]
|
519
|
+
|
520
|
+
def get_statistics(self) -> Dict[str, Any]:
|
521
|
+
"""Get cache statistics"""
|
522
|
+
total_requests = self.hit_count + self.miss_count
|
523
|
+
|
524
|
+
return {
|
525
|
+
"size": len(self.cache),
|
526
|
+
"max_size": self.max_size,
|
527
|
+
"hit_count": self.hit_count,
|
528
|
+
"miss_count": self.miss_count,
|
529
|
+
"hit_rate": (
|
530
|
+
(self.hit_count / total_requests * 100) if total_requests > 0 else 0
|
531
|
+
),
|
532
|
+
"utilization": (
|
533
|
+
(len(self.cache) / self.max_size * 100) if self.max_size > 0 else 0
|
534
|
+
),
|
535
|
+
}
|
536
|
+
|
537
|
+
|
538
|
+
class ContextStrategy(IConfigStrategy):
|
539
|
+
"""
|
540
|
+
Main context strategy for configuration lifecycle management
|
541
|
+
Provides context-based configuration with hierarchy, isolation, and caching
|
542
|
+
"""
|
543
|
+
|
544
|
+
def __init__(self):
|
545
|
+
self.logger = get_logger(self.__class__.__name__)
|
546
|
+
self.hierarchy_manager = HierarchicalContextManager()
|
547
|
+
self.scoped_manager = ScopedConfigManager(self.hierarchy_manager)
|
548
|
+
self.isolated_manager = IsolatedContextManager()
|
549
|
+
self.thread_manager = ThreadLocalContextManager()
|
550
|
+
self.cache_manager = CachingContextManager()
|
551
|
+
|
552
|
+
def can_handle(self, source: Union[str, Path, Dict]) -> bool:
|
553
|
+
"""Context strategy handles dictionary configurations"""
|
554
|
+
return isinstance(source, dict)
|
555
|
+
|
556
|
+
def load(self, source: Any, **kwargs) -> Dict[str, Any]:
|
557
|
+
"""Load configuration into context"""
|
558
|
+
context_scope = kwargs.get("context_scope", ContextScope.TEMPORARY)
|
559
|
+
context_id = kwargs.get("context_id")
|
560
|
+
|
561
|
+
if not context_id:
|
562
|
+
# Create new context
|
563
|
+
context_id = self.hierarchy_manager.create_context(
|
564
|
+
context_scope, ttl=kwargs.get("ttl")
|
565
|
+
)
|
566
|
+
|
567
|
+
# Load config into context
|
568
|
+
if isinstance(source, dict):
|
569
|
+
for key, value in source.items():
|
570
|
+
self.scoped_manager.set_config(context_id, key, value)
|
571
|
+
|
572
|
+
return self.scoped_manager.get_config(context_id)
|
573
|
+
|
574
|
+
def validate(self, config: Dict[str, Any], schema: Optional[Dict] = None) -> bool:
|
575
|
+
"""Validate configuration in context"""
|
576
|
+
return True
|
577
|
+
|
578
|
+
def transform(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
579
|
+
"""Transform configuration based on context"""
|
580
|
+
# Apply context-specific transformations
|
581
|
+
context = self.get_current_context()
|
582
|
+
|
583
|
+
if context:
|
584
|
+
# Apply context-specific overrides
|
585
|
+
overrides = self.scoped_manager.get_config(
|
586
|
+
context, "transformations.overrides", inherit=True
|
587
|
+
)
|
588
|
+
|
589
|
+
if overrides:
|
590
|
+
config = self._apply_overrides(config, overrides)
|
591
|
+
|
592
|
+
return config
|
593
|
+
|
594
|
+
def create_context(
|
595
|
+
self,
|
596
|
+
scope: ContextScope = ContextScope.TEMPORARY,
|
597
|
+
parent: Optional[str] = None,
|
598
|
+
isolated: bool = False,
|
599
|
+
**kwargs,
|
600
|
+
) -> str:
|
601
|
+
"""Create a new configuration context"""
|
602
|
+
if isolated:
|
603
|
+
return self.isolated_manager.create_isolated_context(
|
604
|
+
kwargs.get("base_config")
|
605
|
+
)
|
606
|
+
return self.hierarchy_manager.create_context(
|
607
|
+
scope, parent_id=parent, **kwargs
|
608
|
+
)
|
609
|
+
|
610
|
+
def get_current_context(self) -> Optional[str]:
|
611
|
+
"""Get current active context"""
|
612
|
+
# Check thread-local stack
|
613
|
+
if hasattr(self.hierarchy_manager.context_stack, "stack"):
|
614
|
+
stack = self.hierarchy_manager.context_stack.stack
|
615
|
+
if stack:
|
616
|
+
return stack[-1]
|
617
|
+
|
618
|
+
# Check for global context
|
619
|
+
for ctx_id, metadata in self.hierarchy_manager.contexts.items():
|
620
|
+
if (
|
621
|
+
metadata.scope == ContextScope.GLOBAL
|
622
|
+
and metadata.lifecycle == ContextLifecycle.ACTIVE
|
623
|
+
):
|
624
|
+
return ctx_id
|
625
|
+
|
626
|
+
return None
|
627
|
+
|
628
|
+
def with_context(self, context_id: str, operation: Callable) -> Any:
|
629
|
+
"""Execute operation within specified context"""
|
630
|
+
# Push context
|
631
|
+
if not hasattr(self.hierarchy_manager.context_stack, "stack"):
|
632
|
+
self.hierarchy_manager.context_stack.stack = []
|
633
|
+
|
634
|
+
self.hierarchy_manager.context_stack.stack.append(context_id)
|
635
|
+
|
636
|
+
try:
|
637
|
+
return operation()
|
638
|
+
finally:
|
639
|
+
# Pop context
|
640
|
+
if self.hierarchy_manager.context_stack.stack:
|
641
|
+
self.hierarchy_manager.context_stack.stack.pop()
|
642
|
+
|
643
|
+
@contextmanager
|
644
|
+
def context(self, scope: ContextScope = ContextScope.TEMPORARY, **kwargs):
|
645
|
+
"""Context manager for configuration contexts"""
|
646
|
+
with self.hierarchy_manager.context(scope, **kwargs) as context_id:
|
647
|
+
yield context_id
|
648
|
+
|
649
|
+
def get_config(
|
650
|
+
self,
|
651
|
+
key: Optional[str] = None,
|
652
|
+
context: Optional[str] = None,
|
653
|
+
default: Any = None,
|
654
|
+
) -> Any:
|
655
|
+
"""Get configuration value from context"""
|
656
|
+
context_id = context or self.get_current_context()
|
657
|
+
|
658
|
+
if not context_id:
|
659
|
+
return default
|
660
|
+
|
661
|
+
# Check cache first
|
662
|
+
if key:
|
663
|
+
cached = self.cache_manager.get_cached(context_id, key)
|
664
|
+
if cached is not None:
|
665
|
+
return cached
|
666
|
+
|
667
|
+
# Get from context
|
668
|
+
value = self.scoped_manager.get_config(context_id, key)
|
669
|
+
|
670
|
+
# Cache result
|
671
|
+
if key and value is not None:
|
672
|
+
self.cache_manager.set_cached(context_id, key, value)
|
673
|
+
|
674
|
+
return value if value is not None else default
|
675
|
+
|
676
|
+
def set_config(self, key: str, value: Any, context: Optional[str] = None, **kwargs):
|
677
|
+
"""Set configuration value in context"""
|
678
|
+
context_id = context or self.get_current_context()
|
679
|
+
|
680
|
+
if not context_id:
|
681
|
+
# Create temporary context
|
682
|
+
context_id = self.create_context(ContextScope.TEMPORARY)
|
683
|
+
|
684
|
+
self.scoped_manager.set_config(context_id, key, value, **kwargs)
|
685
|
+
|
686
|
+
# Invalidate cache
|
687
|
+
self.cache_manager.invalidate_context(context_id)
|
688
|
+
|
689
|
+
def close_context(self, context_id: str):
|
690
|
+
"""Close a configuration context"""
|
691
|
+
# Invalidate cache
|
692
|
+
self.cache_manager.invalidate_context(context_id)
|
693
|
+
|
694
|
+
# Close context
|
695
|
+
if context_id.startswith("isolated_"):
|
696
|
+
self.isolated_manager.close_isolated_context(context_id)
|
697
|
+
else:
|
698
|
+
self.hierarchy_manager.close_context(context_id)
|
699
|
+
|
700
|
+
def _apply_overrides(self, config: Dict, overrides: Dict) -> Dict:
|
701
|
+
"""Apply context overrides to configuration"""
|
702
|
+
result = config.copy()
|
703
|
+
|
704
|
+
for key, value in overrides.items():
|
705
|
+
if (
|
706
|
+
key in result
|
707
|
+
and isinstance(result[key], dict)
|
708
|
+
and isinstance(value, dict)
|
709
|
+
):
|
710
|
+
result[key] = self._apply_overrides(result[key], value)
|
711
|
+
else:
|
712
|
+
result[key] = value
|
713
|
+
|
714
|
+
return result
|
715
|
+
|
716
|
+
def get_statistics(self) -> Dict[str, Any]:
|
717
|
+
"""Get context strategy statistics"""
|
718
|
+
return {
|
719
|
+
"active_contexts": len(self.hierarchy_manager.contexts),
|
720
|
+
"isolated_contexts": len(self.isolated_manager.isolated_contexts),
|
721
|
+
"cache_stats": self.cache_manager.get_statistics(),
|
722
|
+
"contexts_by_scope": self._count_by_scope(),
|
723
|
+
"contexts_by_lifecycle": self._count_by_lifecycle(),
|
724
|
+
}
|
725
|
+
|
726
|
+
def _count_by_scope(self) -> Dict[str, int]:
|
727
|
+
"""Count contexts by scope"""
|
728
|
+
counts = {}
|
729
|
+
for metadata in self.hierarchy_manager.contexts.values():
|
730
|
+
scope = metadata.scope.value
|
731
|
+
counts[scope] = counts.get(scope, 0) + 1
|
732
|
+
return counts
|
733
|
+
|
734
|
+
def _count_by_lifecycle(self) -> Dict[str, int]:
|
735
|
+
"""Count contexts by lifecycle state"""
|
736
|
+
counts = {}
|
737
|
+
for metadata in self.hierarchy_manager.contexts.values():
|
738
|
+
state = metadata.lifecycle.value
|
739
|
+
counts[state] = counts.get(state, 0) + 1
|
740
|
+
return counts
|
741
|
+
|
742
|
+
|
743
|
+
# Export main components
|
744
|
+
__all__ = [
|
745
|
+
"ContextLifecycle",
|
746
|
+
"ContextScope",
|
747
|
+
"ContextStrategy",
|
748
|
+
"HierarchicalContextManager",
|
749
|
+
"ScopedConfigManager",
|
750
|
+
]
|