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.
Files changed (129) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/WORKFLOW.md +2 -14
  3. claude_mpm/agents/agent_loader.py +3 -2
  4. claude_mpm/agents/agent_loader_integration.py +2 -1
  5. claude_mpm/agents/async_agent_loader.py +2 -2
  6. claude_mpm/agents/base_agent_loader.py +2 -2
  7. claude_mpm/agents/frontmatter_validator.py +1 -0
  8. claude_mpm/agents/system_agent_config.py +2 -1
  9. claude_mpm/cli/commands/configure.py +2 -29
  10. claude_mpm/cli/commands/doctor.py +44 -5
  11. claude_mpm/cli/commands/mpm_init.py +117 -63
  12. claude_mpm/cli/parsers/configure_parser.py +6 -15
  13. claude_mpm/cli/startup_logging.py +1 -3
  14. claude_mpm/config/agent_config.py +1 -1
  15. claude_mpm/config/paths.py +2 -1
  16. claude_mpm/core/agent_name_normalizer.py +1 -0
  17. claude_mpm/core/config.py +2 -1
  18. claude_mpm/core/config_aliases.py +2 -1
  19. claude_mpm/core/file_utils.py +0 -1
  20. claude_mpm/core/framework/__init__.py +38 -0
  21. claude_mpm/core/framework/formatters/__init__.py +11 -0
  22. claude_mpm/core/framework/formatters/capability_generator.py +367 -0
  23. claude_mpm/core/framework/formatters/content_formatter.py +288 -0
  24. claude_mpm/core/framework/formatters/context_generator.py +184 -0
  25. claude_mpm/core/framework/loaders/__init__.py +13 -0
  26. claude_mpm/core/framework/loaders/agent_loader.py +206 -0
  27. claude_mpm/core/framework/loaders/file_loader.py +223 -0
  28. claude_mpm/core/framework/loaders/instruction_loader.py +161 -0
  29. claude_mpm/core/framework/loaders/packaged_loader.py +232 -0
  30. claude_mpm/core/framework/processors/__init__.py +11 -0
  31. claude_mpm/core/framework/processors/memory_processor.py +230 -0
  32. claude_mpm/core/framework/processors/metadata_processor.py +146 -0
  33. claude_mpm/core/framework/processors/template_processor.py +244 -0
  34. claude_mpm/core/framework_loader.py +298 -1795
  35. claude_mpm/core/log_manager.py +2 -1
  36. claude_mpm/core/tool_access_control.py +1 -0
  37. claude_mpm/core/unified_agent_registry.py +2 -1
  38. claude_mpm/core/unified_paths.py +1 -0
  39. claude_mpm/experimental/cli_enhancements.py +1 -0
  40. claude_mpm/hooks/__init__.py +9 -1
  41. claude_mpm/hooks/base_hook.py +1 -0
  42. claude_mpm/hooks/instruction_reinforcement.py +1 -0
  43. claude_mpm/hooks/kuzu_memory_hook.py +359 -0
  44. claude_mpm/hooks/validation_hooks.py +1 -1
  45. claude_mpm/scripts/mpm_doctor.py +1 -0
  46. claude_mpm/services/agents/loading/agent_profile_loader.py +1 -1
  47. claude_mpm/services/agents/loading/base_agent_manager.py +1 -1
  48. claude_mpm/services/agents/loading/framework_agent_loader.py +1 -1
  49. claude_mpm/services/agents/management/agent_capabilities_generator.py +1 -0
  50. claude_mpm/services/agents/management/agent_management_service.py +1 -1
  51. claude_mpm/services/agents/memory/memory_categorization_service.py +0 -1
  52. claude_mpm/services/agents/memory/memory_file_service.py +6 -2
  53. claude_mpm/services/agents/memory/memory_format_service.py +0 -1
  54. claude_mpm/services/agents/registry/deployed_agent_discovery.py +1 -1
  55. claude_mpm/services/async_session_logger.py +1 -1
  56. claude_mpm/services/claude_session_logger.py +1 -0
  57. claude_mpm/services/core/path_resolver.py +2 -0
  58. claude_mpm/services/diagnostics/checks/__init__.py +2 -0
  59. claude_mpm/services/diagnostics/checks/installation_check.py +126 -25
  60. claude_mpm/services/diagnostics/checks/mcp_services_check.py +399 -0
  61. claude_mpm/services/diagnostics/diagnostic_runner.py +4 -0
  62. claude_mpm/services/diagnostics/doctor_reporter.py +259 -32
  63. claude_mpm/services/event_bus/direct_relay.py +2 -1
  64. claude_mpm/services/event_bus/event_bus.py +1 -0
  65. claude_mpm/services/event_bus/relay.py +3 -2
  66. claude_mpm/services/framework_claude_md_generator/content_assembler.py +1 -1
  67. claude_mpm/services/infrastructure/daemon_manager.py +1 -1
  68. claude_mpm/services/mcp_config_manager.py +67 -4
  69. claude_mpm/services/mcp_gateway/core/process_pool.py +320 -0
  70. claude_mpm/services/mcp_gateway/core/startup_verification.py +2 -2
  71. claude_mpm/services/mcp_gateway/main.py +3 -13
  72. claude_mpm/services/mcp_gateway/server/stdio_server.py +4 -10
  73. claude_mpm/services/mcp_gateway/tools/__init__.py +14 -2
  74. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +38 -6
  75. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +527 -0
  76. claude_mpm/services/memory/cache/simple_cache.py +1 -1
  77. claude_mpm/services/project/archive_manager.py +159 -96
  78. claude_mpm/services/project/documentation_manager.py +64 -45
  79. claude_mpm/services/project/enhanced_analyzer.py +132 -89
  80. claude_mpm/services/project/project_organizer.py +225 -131
  81. claude_mpm/services/response_tracker.py +1 -1
  82. claude_mpm/services/shared/__init__.py +2 -1
  83. claude_mpm/services/shared/service_factory.py +8 -5
  84. claude_mpm/services/socketio/server/eventbus_integration.py +1 -1
  85. claude_mpm/services/unified/__init__.py +1 -1
  86. claude_mpm/services/unified/analyzer_strategies/__init__.py +3 -3
  87. claude_mpm/services/unified/analyzer_strategies/code_analyzer.py +97 -53
  88. claude_mpm/services/unified/analyzer_strategies/dependency_analyzer.py +81 -40
  89. claude_mpm/services/unified/analyzer_strategies/performance_analyzer.py +277 -178
  90. claude_mpm/services/unified/analyzer_strategies/security_analyzer.py +196 -112
  91. claude_mpm/services/unified/analyzer_strategies/structure_analyzer.py +83 -49
  92. claude_mpm/services/unified/config_strategies/__init__.py +175 -0
  93. claude_mpm/services/unified/config_strategies/config_schema.py +735 -0
  94. claude_mpm/services/unified/config_strategies/context_strategy.py +750 -0
  95. claude_mpm/services/unified/config_strategies/error_handling_strategy.py +1009 -0
  96. claude_mpm/services/unified/config_strategies/file_loader_strategy.py +879 -0
  97. claude_mpm/services/unified/config_strategies/unified_config_service.py +814 -0
  98. claude_mpm/services/unified/config_strategies/validation_strategy.py +1144 -0
  99. claude_mpm/services/unified/deployment_strategies/__init__.py +7 -7
  100. claude_mpm/services/unified/deployment_strategies/base.py +24 -28
  101. claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +168 -88
  102. claude_mpm/services/unified/deployment_strategies/local.py +49 -34
  103. claude_mpm/services/unified/deployment_strategies/utils.py +39 -43
  104. claude_mpm/services/unified/deployment_strategies/vercel.py +30 -24
  105. claude_mpm/services/unified/interfaces.py +0 -26
  106. claude_mpm/services/unified/migration.py +17 -40
  107. claude_mpm/services/unified/strategies.py +9 -26
  108. claude_mpm/services/unified/unified_analyzer.py +48 -44
  109. claude_mpm/services/unified/unified_config.py +21 -19
  110. claude_mpm/services/unified/unified_deployment.py +21 -26
  111. claude_mpm/storage/state_storage.py +1 -0
  112. claude_mpm/utils/agent_dependency_loader.py +18 -6
  113. claude_mpm/utils/common.py +14 -12
  114. claude_mpm/utils/database_connector.py +15 -12
  115. claude_mpm/utils/error_handler.py +1 -0
  116. claude_mpm/utils/log_cleanup.py +1 -0
  117. claude_mpm/utils/path_operations.py +1 -0
  118. claude_mpm/utils/session_logging.py +1 -1
  119. claude_mpm/utils/subprocess_utils.py +1 -0
  120. claude_mpm/validation/agent_validator.py +1 -1
  121. {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.4.dist-info}/METADATA +23 -17
  122. {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.4.dist-info}/RECORD +126 -105
  123. claude_mpm/cli/commands/configure_tui.py +0 -1927
  124. claude_mpm/services/mcp_gateway/tools/ticket_tools.py +0 -645
  125. claude_mpm/services/mcp_gateway/tools/unified_ticket_tool.py +0 -602
  126. {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.4.dist-info}/WHEEL +0 -0
  127. {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.4.dist-info}/entry_points.txt +0 -0
  128. {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.4.dist-info}/licenses/LICENSE +0 -0
  129. {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
+ ]