claude-mpm 4.0.23__py3-none-any.whl → 4.0.25__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/BUILD_NUMBER +1 -1
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/BASE_AGENT_TEMPLATE.md +4 -1
- claude_mpm/agents/BASE_PM.md +3 -0
- claude_mpm/agents/templates/code_analyzer.json +2 -2
- claude_mpm/cli/commands/agents.py +453 -113
- claude_mpm/cli/commands/aggregate.py +107 -15
- claude_mpm/cli/commands/cleanup.py +142 -10
- claude_mpm/cli/commands/config.py +358 -224
- claude_mpm/cli/commands/info.py +184 -75
- claude_mpm/cli/commands/mcp_command_router.py +5 -76
- claude_mpm/cli/commands/mcp_install_commands.py +68 -36
- claude_mpm/cli/commands/mcp_server_commands.py +30 -37
- claude_mpm/cli/commands/memory.py +331 -61
- claude_mpm/cli/commands/monitor.py +101 -7
- claude_mpm/cli/commands/run.py +368 -8
- claude_mpm/cli/commands/tickets.py +206 -24
- claude_mpm/cli/parsers/mcp_parser.py +3 -0
- claude_mpm/cli/shared/__init__.py +40 -0
- claude_mpm/cli/shared/argument_patterns.py +212 -0
- claude_mpm/cli/shared/command_base.py +234 -0
- claude_mpm/cli/shared/error_handling.py +238 -0
- claude_mpm/cli/shared/output_formatters.py +231 -0
- claude_mpm/config/agent_config.py +29 -8
- claude_mpm/core/container.py +6 -4
- claude_mpm/core/service_registry.py +4 -2
- claude_mpm/core/shared/__init__.py +17 -0
- claude_mpm/core/shared/config_loader.py +320 -0
- claude_mpm/core/shared/path_resolver.py +277 -0
- claude_mpm/core/shared/singleton_manager.py +208 -0
- claude_mpm/hooks/claude_hooks/memory_integration.py +4 -2
- claude_mpm/hooks/claude_hooks/response_tracking.py +14 -3
- claude_mpm/hooks/memory_integration_hook.py +11 -2
- claude_mpm/services/agents/deployment/agent_deployment.py +43 -23
- claude_mpm/services/agents/deployment/deployment_wrapper.py +71 -0
- claude_mpm/services/agents/deployment/pipeline/pipeline_context.py +1 -0
- claude_mpm/services/agents/deployment/pipeline/steps/agent_processing_step.py +43 -0
- claude_mpm/services/agents/deployment/processors/agent_deployment_context.py +4 -0
- claude_mpm/services/agents/deployment/processors/agent_processor.py +1 -1
- claude_mpm/services/agents/loading/base_agent_manager.py +11 -3
- claude_mpm/services/agents/registry/deployed_agent_discovery.py +14 -5
- claude_mpm/services/event_aggregator.py +4 -2
- claude_mpm/services/mcp_gateway/config/config_loader.py +89 -28
- claude_mpm/services/mcp_gateway/config/configuration.py +29 -0
- claude_mpm/services/mcp_gateway/registry/service_registry.py +22 -5
- claude_mpm/services/memory/builder.py +6 -1
- claude_mpm/services/response_tracker.py +3 -1
- claude_mpm/services/runner_configuration_service.py +15 -6
- claude_mpm/services/shared/__init__.py +20 -0
- claude_mpm/services/shared/async_service_base.py +219 -0
- claude_mpm/services/shared/config_service_base.py +292 -0
- claude_mpm/services/shared/lifecycle_service_base.py +317 -0
- claude_mpm/services/shared/manager_base.py +303 -0
- claude_mpm/services/shared/service_factory.py +308 -0
- {claude_mpm-4.0.23.dist-info → claude_mpm-4.0.25.dist-info}/METADATA +19 -13
- {claude_mpm-4.0.23.dist-info → claude_mpm-4.0.25.dist-info}/RECORD +60 -44
- {claude_mpm-4.0.23.dist-info → claude_mpm-4.0.25.dist-info}/WHEEL +0 -0
- {claude_mpm-4.0.23.dist-info → claude_mpm-4.0.25.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.0.23.dist-info → claude_mpm-4.0.25.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.0.23.dist-info → claude_mpm-4.0.25.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base class for services with complex lifecycle management.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
from ...core.mixins import LoggerMixin
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ServiceState(Enum):
|
|
14
|
+
"""Standard service states."""
|
|
15
|
+
UNINITIALIZED = "uninitialized"
|
|
16
|
+
INITIALIZING = "initializing"
|
|
17
|
+
INITIALIZED = "initialized"
|
|
18
|
+
STARTING = "starting"
|
|
19
|
+
RUNNING = "running"
|
|
20
|
+
STOPPING = "stopping"
|
|
21
|
+
STOPPED = "stopped"
|
|
22
|
+
ERROR = "error"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class LifecycleServiceBase(LoggerMixin, ABC):
|
|
26
|
+
"""
|
|
27
|
+
Base class for services with complex lifecycle management.
|
|
28
|
+
|
|
29
|
+
Provides common patterns:
|
|
30
|
+
- State machine management
|
|
31
|
+
- Lifecycle hooks
|
|
32
|
+
- Health monitoring
|
|
33
|
+
- Dependency management
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, service_name: str, config: Optional[Dict[str, Any]] = None):
|
|
37
|
+
"""
|
|
38
|
+
Initialize lifecycle service.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
service_name: Name of the service
|
|
42
|
+
config: Optional configuration
|
|
43
|
+
"""
|
|
44
|
+
self.service_name = service_name
|
|
45
|
+
self._logger_name = f"service.{service_name}"
|
|
46
|
+
self.config = config or {}
|
|
47
|
+
|
|
48
|
+
# State management
|
|
49
|
+
self._state = ServiceState.UNINITIALIZED
|
|
50
|
+
self._previous_state = None
|
|
51
|
+
self._state_history: List[tuple] = []
|
|
52
|
+
|
|
53
|
+
# Lifecycle tracking
|
|
54
|
+
self._start_time: Optional[float] = None
|
|
55
|
+
self._last_health_check: Optional[float] = None
|
|
56
|
+
self._health_status: Dict[str, Any] = {}
|
|
57
|
+
|
|
58
|
+
# Dependencies
|
|
59
|
+
self._dependencies: List[str] = []
|
|
60
|
+
self._dependents: List[str] = []
|
|
61
|
+
|
|
62
|
+
# Error tracking
|
|
63
|
+
self._errors: List[Dict[str, Any]] = []
|
|
64
|
+
self._max_errors = 10
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def state(self) -> ServiceState:
|
|
68
|
+
"""Get current service state."""
|
|
69
|
+
return self._state
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def is_running(self) -> bool:
|
|
73
|
+
"""Check if service is running."""
|
|
74
|
+
return self._state == ServiceState.RUNNING
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def is_healthy(self) -> bool:
|
|
78
|
+
"""Check if service is healthy."""
|
|
79
|
+
return (
|
|
80
|
+
self._state == ServiceState.RUNNING and
|
|
81
|
+
len(self._errors) == 0 and
|
|
82
|
+
self._health_status.get('healthy', True)
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def uptime(self) -> Optional[float]:
|
|
87
|
+
"""Get service uptime in seconds."""
|
|
88
|
+
if self._start_time and self.is_running:
|
|
89
|
+
return time.time() - self._start_time
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
def add_dependency(self, service_name: str) -> None:
|
|
93
|
+
"""Add a service dependency."""
|
|
94
|
+
if service_name not in self._dependencies:
|
|
95
|
+
self._dependencies.append(service_name)
|
|
96
|
+
self.logger.debug(f"Added dependency: {service_name}")
|
|
97
|
+
|
|
98
|
+
def add_dependent(self, service_name: str) -> None:
|
|
99
|
+
"""Add a dependent service."""
|
|
100
|
+
if service_name not in self._dependents:
|
|
101
|
+
self._dependents.append(service_name)
|
|
102
|
+
self.logger.debug(f"Added dependent: {service_name}")
|
|
103
|
+
|
|
104
|
+
def initialize(self) -> bool:
|
|
105
|
+
"""
|
|
106
|
+
Initialize the service.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
True if initialization successful
|
|
110
|
+
"""
|
|
111
|
+
if self._state != ServiceState.UNINITIALIZED:
|
|
112
|
+
self.logger.warning(f"Service {self.service_name} already initialized")
|
|
113
|
+
return self._state in (ServiceState.INITIALIZED, ServiceState.RUNNING)
|
|
114
|
+
|
|
115
|
+
self._transition_state(ServiceState.INITIALIZING)
|
|
116
|
+
self.logger.info(f"Initializing service: {self.service_name}")
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
# Check dependencies
|
|
120
|
+
if not self._check_dependencies():
|
|
121
|
+
self._transition_state(ServiceState.ERROR)
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
# Service-specific initialization
|
|
125
|
+
success = self._do_initialize()
|
|
126
|
+
|
|
127
|
+
if success:
|
|
128
|
+
self._transition_state(ServiceState.INITIALIZED)
|
|
129
|
+
self.logger.info(f"Service {self.service_name} initialized successfully")
|
|
130
|
+
else:
|
|
131
|
+
self._transition_state(ServiceState.ERROR)
|
|
132
|
+
self.logger.error(f"Service {self.service_name} initialization failed")
|
|
133
|
+
|
|
134
|
+
return success
|
|
135
|
+
|
|
136
|
+
except Exception as e:
|
|
137
|
+
self._record_error("initialization", e)
|
|
138
|
+
self._transition_state(ServiceState.ERROR)
|
|
139
|
+
self.logger.error(f"Service {self.service_name} initialization error: {e}", exc_info=True)
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
def start(self) -> bool:
|
|
143
|
+
"""
|
|
144
|
+
Start the service.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
True if start successful
|
|
148
|
+
"""
|
|
149
|
+
if self._state == ServiceState.RUNNING:
|
|
150
|
+
self.logger.warning(f"Service {self.service_name} already running")
|
|
151
|
+
return True
|
|
152
|
+
|
|
153
|
+
if self._state != ServiceState.INITIALIZED:
|
|
154
|
+
if not self.initialize():
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
self._transition_state(ServiceState.STARTING)
|
|
158
|
+
self.logger.info(f"Starting service: {self.service_name}")
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
success = self._do_start()
|
|
162
|
+
|
|
163
|
+
if success:
|
|
164
|
+
self._start_time = time.time()
|
|
165
|
+
self._transition_state(ServiceState.RUNNING)
|
|
166
|
+
self.logger.info(f"Service {self.service_name} started successfully")
|
|
167
|
+
else:
|
|
168
|
+
self._transition_state(ServiceState.ERROR)
|
|
169
|
+
self.logger.error(f"Service {self.service_name} start failed")
|
|
170
|
+
|
|
171
|
+
return success
|
|
172
|
+
|
|
173
|
+
except Exception as e:
|
|
174
|
+
self._record_error("start", e)
|
|
175
|
+
self._transition_state(ServiceState.ERROR)
|
|
176
|
+
self.logger.error(f"Service {self.service_name} start error: {e}", exc_info=True)
|
|
177
|
+
return False
|
|
178
|
+
|
|
179
|
+
def stop(self) -> bool:
|
|
180
|
+
"""
|
|
181
|
+
Stop the service.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
True if stop successful
|
|
185
|
+
"""
|
|
186
|
+
if self._state == ServiceState.STOPPED:
|
|
187
|
+
self.logger.warning(f"Service {self.service_name} already stopped")
|
|
188
|
+
return True
|
|
189
|
+
|
|
190
|
+
self._transition_state(ServiceState.STOPPING)
|
|
191
|
+
self.logger.info(f"Stopping service: {self.service_name}")
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
success = self._do_stop()
|
|
195
|
+
|
|
196
|
+
if success:
|
|
197
|
+
self._start_time = None
|
|
198
|
+
self._transition_state(ServiceState.STOPPED)
|
|
199
|
+
self.logger.info(f"Service {self.service_name} stopped successfully")
|
|
200
|
+
else:
|
|
201
|
+
self._transition_state(ServiceState.ERROR)
|
|
202
|
+
self.logger.error(f"Service {self.service_name} stop failed")
|
|
203
|
+
|
|
204
|
+
return success
|
|
205
|
+
|
|
206
|
+
except Exception as e:
|
|
207
|
+
self._record_error("stop", e)
|
|
208
|
+
self._transition_state(ServiceState.ERROR)
|
|
209
|
+
self.logger.error(f"Service {self.service_name} stop error: {e}", exc_info=True)
|
|
210
|
+
return False
|
|
211
|
+
|
|
212
|
+
def restart(self) -> bool:
|
|
213
|
+
"""Restart the service."""
|
|
214
|
+
self.logger.info(f"Restarting service: {self.service_name}")
|
|
215
|
+
|
|
216
|
+
if self.is_running:
|
|
217
|
+
if not self.stop():
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
return self.start()
|
|
221
|
+
|
|
222
|
+
def health_check(self) -> Dict[str, Any]:
|
|
223
|
+
"""
|
|
224
|
+
Perform health check.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
Health status dictionary
|
|
228
|
+
"""
|
|
229
|
+
self._last_health_check = time.time()
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
# Service-specific health check
|
|
233
|
+
service_health = self._do_health_check()
|
|
234
|
+
|
|
235
|
+
# Combine with base health info
|
|
236
|
+
health_status = {
|
|
237
|
+
"service": self.service_name,
|
|
238
|
+
"state": self._state.value,
|
|
239
|
+
"healthy": self.is_healthy,
|
|
240
|
+
"uptime": self.uptime,
|
|
241
|
+
"error_count": len(self._errors),
|
|
242
|
+
"dependencies": self._dependencies,
|
|
243
|
+
"dependents": self._dependents,
|
|
244
|
+
"last_check": self._last_health_check,
|
|
245
|
+
**service_health
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
self._health_status = health_status
|
|
249
|
+
return health_status
|
|
250
|
+
|
|
251
|
+
except Exception as e:
|
|
252
|
+
self._record_error("health_check", e)
|
|
253
|
+
return {
|
|
254
|
+
"service": self.service_name,
|
|
255
|
+
"state": self._state.value,
|
|
256
|
+
"healthy": False,
|
|
257
|
+
"error": str(e)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
def _transition_state(self, new_state: ServiceState) -> None:
|
|
261
|
+
"""Transition to a new state."""
|
|
262
|
+
old_state = self._state
|
|
263
|
+
self._previous_state = old_state
|
|
264
|
+
self._state = new_state
|
|
265
|
+
|
|
266
|
+
# Record state transition
|
|
267
|
+
self._state_history.append((time.time(), old_state.value, new_state.value))
|
|
268
|
+
|
|
269
|
+
# Keep history bounded
|
|
270
|
+
if len(self._state_history) > 50:
|
|
271
|
+
self._state_history = self._state_history[-50:]
|
|
272
|
+
|
|
273
|
+
self.logger.debug(f"State transition: {old_state.value} -> {new_state.value}")
|
|
274
|
+
|
|
275
|
+
def _record_error(self, operation: str, error: Exception) -> None:
|
|
276
|
+
"""Record an error."""
|
|
277
|
+
error_record = {
|
|
278
|
+
"timestamp": time.time(),
|
|
279
|
+
"operation": operation,
|
|
280
|
+
"error": str(error),
|
|
281
|
+
"type": type(error).__name__
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
self._errors.append(error_record)
|
|
285
|
+
|
|
286
|
+
# Keep error list bounded
|
|
287
|
+
if len(self._errors) > self._max_errors:
|
|
288
|
+
self._errors = self._errors[-self._max_errors:]
|
|
289
|
+
|
|
290
|
+
def _check_dependencies(self) -> bool:
|
|
291
|
+
"""Check if all dependencies are satisfied."""
|
|
292
|
+
# This is a placeholder - in a real implementation,
|
|
293
|
+
# you would check with a service registry
|
|
294
|
+
return True
|
|
295
|
+
|
|
296
|
+
@abstractmethod
|
|
297
|
+
def _do_initialize(self) -> bool:
|
|
298
|
+
"""Service-specific initialization logic."""
|
|
299
|
+
pass
|
|
300
|
+
|
|
301
|
+
@abstractmethod
|
|
302
|
+
def _do_start(self) -> bool:
|
|
303
|
+
"""Service-specific start logic."""
|
|
304
|
+
pass
|
|
305
|
+
|
|
306
|
+
@abstractmethod
|
|
307
|
+
def _do_stop(self) -> bool:
|
|
308
|
+
"""Service-specific stop logic."""
|
|
309
|
+
pass
|
|
310
|
+
|
|
311
|
+
def _do_health_check(self) -> Dict[str, Any]:
|
|
312
|
+
"""Service-specific health check logic."""
|
|
313
|
+
return {"healthy": True}
|
|
314
|
+
|
|
315
|
+
def __repr__(self) -> str:
|
|
316
|
+
"""String representation."""
|
|
317
|
+
return f"{self.__class__.__name__}(name={self.service_name}, state={self._state.value})"
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base class for manager-style services to reduce duplication.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Any, Dict, List, Optional, Set, TypeVar
|
|
7
|
+
|
|
8
|
+
from ...core.mixins import LoggerMixin
|
|
9
|
+
from .config_service_base import ConfigServiceBase
|
|
10
|
+
|
|
11
|
+
T = TypeVar('T')
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ManagerBase(ConfigServiceBase, ABC):
|
|
15
|
+
"""
|
|
16
|
+
Base class for manager-style services.
|
|
17
|
+
|
|
18
|
+
Provides common patterns for services that manage collections of items:
|
|
19
|
+
- Item registration and discovery
|
|
20
|
+
- Lifecycle management
|
|
21
|
+
- Caching and indexing
|
|
22
|
+
- Validation and filtering
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self,
|
|
26
|
+
manager_name: str,
|
|
27
|
+
config: Optional[Dict[str, Any]] = None,
|
|
28
|
+
config_section: Optional[str] = None):
|
|
29
|
+
"""
|
|
30
|
+
Initialize manager.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
manager_name: Name of the manager
|
|
34
|
+
config: Configuration dictionary
|
|
35
|
+
config_section: Configuration section name
|
|
36
|
+
"""
|
|
37
|
+
super().__init__(manager_name, config, config_section)
|
|
38
|
+
|
|
39
|
+
# Item storage
|
|
40
|
+
self._items: Dict[str, T] = {}
|
|
41
|
+
self._item_metadata: Dict[str, Dict[str, Any]] = {}
|
|
42
|
+
|
|
43
|
+
# Indexing
|
|
44
|
+
self._indexes: Dict[str, Dict[Any, Set[str]]] = {}
|
|
45
|
+
|
|
46
|
+
# State tracking
|
|
47
|
+
self._initialized = False
|
|
48
|
+
self._last_scan_time: Optional[float] = None
|
|
49
|
+
|
|
50
|
+
# Configuration
|
|
51
|
+
self._auto_scan = self.get_config_value("auto_scan", True, config_type=bool)
|
|
52
|
+
self._cache_enabled = self.get_config_value("cache_enabled", True, config_type=bool)
|
|
53
|
+
self._max_items = self.get_config_value("max_items", 1000, config_type=int)
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def item_count(self) -> int:
|
|
57
|
+
"""Get number of managed items."""
|
|
58
|
+
return len(self._items)
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def is_initialized(self) -> bool:
|
|
62
|
+
"""Check if manager is initialized."""
|
|
63
|
+
return self._initialized
|
|
64
|
+
|
|
65
|
+
def initialize(self) -> bool:
|
|
66
|
+
"""
|
|
67
|
+
Initialize the manager.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
True if initialization successful
|
|
71
|
+
"""
|
|
72
|
+
if self._initialized:
|
|
73
|
+
self.logger.warning(f"Manager {self.service_name} already initialized")
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
self.logger.info(f"Initializing manager: {self.service_name}")
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
# Manager-specific initialization
|
|
80
|
+
success = self._do_initialize()
|
|
81
|
+
|
|
82
|
+
if success:
|
|
83
|
+
# Perform initial scan if auto-scan enabled
|
|
84
|
+
if self._auto_scan:
|
|
85
|
+
self.scan_items()
|
|
86
|
+
|
|
87
|
+
self._initialized = True
|
|
88
|
+
self.logger.info(f"Manager {self.service_name} initialized successfully")
|
|
89
|
+
else:
|
|
90
|
+
self.logger.error(f"Manager {self.service_name} initialization failed")
|
|
91
|
+
|
|
92
|
+
return success
|
|
93
|
+
|
|
94
|
+
except Exception as e:
|
|
95
|
+
self.logger.error(f"Manager {self.service_name} initialization error: {e}", exc_info=True)
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
def register_item(self, item_id: str, item: T, metadata: Dict[str, Any] = None) -> bool:
|
|
99
|
+
"""
|
|
100
|
+
Register an item.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
item_id: Unique identifier for the item
|
|
104
|
+
item: The item to register
|
|
105
|
+
metadata: Optional metadata for the item
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
True if registration successful
|
|
109
|
+
"""
|
|
110
|
+
if not self._initialized:
|
|
111
|
+
self.logger.warning("Manager not initialized")
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
# Check item limit
|
|
115
|
+
if len(self._items) >= self._max_items:
|
|
116
|
+
self.logger.warning(f"Maximum items ({self._max_items}) reached")
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
# Validate item
|
|
120
|
+
if not self._validate_item(item_id, item):
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
# Store item
|
|
124
|
+
self._items[item_id] = item
|
|
125
|
+
self._item_metadata[item_id] = metadata or {}
|
|
126
|
+
|
|
127
|
+
# Update indexes
|
|
128
|
+
self._update_indexes(item_id, item, metadata)
|
|
129
|
+
|
|
130
|
+
self.logger.debug(f"Registered item: {item_id}")
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
def unregister_item(self, item_id: str) -> bool:
|
|
134
|
+
"""
|
|
135
|
+
Unregister an item.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
item_id: Identifier of item to unregister
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
True if unregistration successful
|
|
142
|
+
"""
|
|
143
|
+
if item_id not in self._items:
|
|
144
|
+
self.logger.warning(f"Item not found: {item_id}")
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
# Remove from storage
|
|
148
|
+
item = self._items.pop(item_id)
|
|
149
|
+
metadata = self._item_metadata.pop(item_id, {})
|
|
150
|
+
|
|
151
|
+
# Update indexes
|
|
152
|
+
self._remove_from_indexes(item_id, item, metadata)
|
|
153
|
+
|
|
154
|
+
self.logger.debug(f"Unregistered item: {item_id}")
|
|
155
|
+
return True
|
|
156
|
+
|
|
157
|
+
def get_item(self, item_id: str) -> Optional[T]:
|
|
158
|
+
"""Get item by ID."""
|
|
159
|
+
return self._items.get(item_id)
|
|
160
|
+
|
|
161
|
+
def get_item_metadata(self, item_id: str) -> Dict[str, Any]:
|
|
162
|
+
"""Get item metadata."""
|
|
163
|
+
return self._item_metadata.get(item_id, {})
|
|
164
|
+
|
|
165
|
+
def list_items(self, filter_func=None) -> List[str]:
|
|
166
|
+
"""
|
|
167
|
+
List item IDs.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
filter_func: Optional filter function
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
List of item IDs
|
|
174
|
+
"""
|
|
175
|
+
if filter_func is None:
|
|
176
|
+
return list(self._items.keys())
|
|
177
|
+
|
|
178
|
+
return [
|
|
179
|
+
item_id for item_id, item in self._items.items()
|
|
180
|
+
if filter_func(item_id, item, self._item_metadata.get(item_id, {}))
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
def find_items(self, **criteria) -> List[str]:
|
|
184
|
+
"""
|
|
185
|
+
Find items by criteria using indexes.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
**criteria: Search criteria
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
List of matching item IDs
|
|
192
|
+
"""
|
|
193
|
+
if not criteria:
|
|
194
|
+
return self.list_items()
|
|
195
|
+
|
|
196
|
+
# Use indexes if available
|
|
197
|
+
result_sets = []
|
|
198
|
+
for key, value in criteria.items():
|
|
199
|
+
if key in self._indexes and value in self._indexes[key]:
|
|
200
|
+
result_sets.append(self._indexes[key][value])
|
|
201
|
+
|
|
202
|
+
if result_sets:
|
|
203
|
+
# Intersection of all criteria
|
|
204
|
+
result = result_sets[0]
|
|
205
|
+
for result_set in result_sets[1:]:
|
|
206
|
+
result = result.intersection(result_set)
|
|
207
|
+
return list(result)
|
|
208
|
+
|
|
209
|
+
# Fallback to linear search
|
|
210
|
+
return self.list_items(
|
|
211
|
+
lambda item_id, item, metadata: all(
|
|
212
|
+
metadata.get(key) == value for key, value in criteria.items()
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
def scan_items(self) -> int:
|
|
217
|
+
"""
|
|
218
|
+
Scan for items and update registry.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
Number of items found
|
|
222
|
+
"""
|
|
223
|
+
import time
|
|
224
|
+
|
|
225
|
+
self.logger.info(f"Scanning for items: {self.service_name}")
|
|
226
|
+
start_time = time.time()
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
# Manager-specific scanning
|
|
230
|
+
found_items = self._do_scan_items()
|
|
231
|
+
|
|
232
|
+
# Update scan time
|
|
233
|
+
self._last_scan_time = time.time()
|
|
234
|
+
|
|
235
|
+
scan_duration = self._last_scan_time - start_time
|
|
236
|
+
self.logger.info(f"Scan completed: found {found_items} items in {scan_duration:.2f}s")
|
|
237
|
+
|
|
238
|
+
return found_items
|
|
239
|
+
|
|
240
|
+
except Exception as e:
|
|
241
|
+
self.logger.error(f"Scan failed: {e}", exc_info=True)
|
|
242
|
+
return 0
|
|
243
|
+
|
|
244
|
+
def clear_items(self) -> None:
|
|
245
|
+
"""Clear all items."""
|
|
246
|
+
self.logger.info(f"Clearing all items: {self.service_name}")
|
|
247
|
+
self._items.clear()
|
|
248
|
+
self._item_metadata.clear()
|
|
249
|
+
self._indexes.clear()
|
|
250
|
+
|
|
251
|
+
def get_status(self) -> Dict[str, Any]:
|
|
252
|
+
"""
|
|
253
|
+
Get manager status.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Status dictionary
|
|
257
|
+
"""
|
|
258
|
+
return {
|
|
259
|
+
"manager": self.service_name,
|
|
260
|
+
"initialized": self._initialized,
|
|
261
|
+
"item_count": self.item_count,
|
|
262
|
+
"max_items": self._max_items,
|
|
263
|
+
"auto_scan": self._auto_scan,
|
|
264
|
+
"cache_enabled": self._cache_enabled,
|
|
265
|
+
"last_scan_time": self._last_scan_time,
|
|
266
|
+
"indexes": list(self._indexes.keys())
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
def _create_index(self, index_name: str) -> None:
|
|
270
|
+
"""Create an index for fast lookups."""
|
|
271
|
+
if index_name not in self._indexes:
|
|
272
|
+
self._indexes[index_name] = {}
|
|
273
|
+
self.logger.debug(f"Created index: {index_name}")
|
|
274
|
+
|
|
275
|
+
def _update_indexes(self, item_id: str, item: T, metadata: Dict[str, Any]) -> None:
|
|
276
|
+
"""Update indexes for an item."""
|
|
277
|
+
# Manager-specific index updates
|
|
278
|
+
self._do_update_indexes(item_id, item, metadata)
|
|
279
|
+
|
|
280
|
+
def _remove_from_indexes(self, item_id: str, item: T, metadata: Dict[str, Any]) -> None:
|
|
281
|
+
"""Remove item from indexes."""
|
|
282
|
+
for index_name, index in self._indexes.items():
|
|
283
|
+
for value, item_set in index.items():
|
|
284
|
+
item_set.discard(item_id)
|
|
285
|
+
|
|
286
|
+
@abstractmethod
|
|
287
|
+
def _do_initialize(self) -> bool:
|
|
288
|
+
"""Manager-specific initialization logic."""
|
|
289
|
+
pass
|
|
290
|
+
|
|
291
|
+
@abstractmethod
|
|
292
|
+
def _validate_item(self, item_id: str, item: T) -> bool:
|
|
293
|
+
"""Validate an item before registration."""
|
|
294
|
+
pass
|
|
295
|
+
|
|
296
|
+
@abstractmethod
|
|
297
|
+
def _do_scan_items(self) -> int:
|
|
298
|
+
"""Manager-specific item scanning logic."""
|
|
299
|
+
pass
|
|
300
|
+
|
|
301
|
+
def _do_update_indexes(self, item_id: str, item: T, metadata: Dict[str, Any]) -> None:
|
|
302
|
+
"""Manager-specific index update logic."""
|
|
303
|
+
pass
|