toolplane-python-client 0.1.0__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.
- toolplane/__init__.py +106 -0
- toolplane/common/__init__.py +93 -0
- toolplane/common/base_config.py +129 -0
- toolplane/common/base_connection_manager.py +171 -0
- toolplane/common/base_session_manager.py +321 -0
- toolplane/common/base_tool_manager.py +347 -0
- toolplane/common/constants.py +47 -0
- toolplane/common/utils.py +310 -0
- toolplane/core/__init__.py +67 -0
- toolplane/core/config.py +107 -0
- toolplane/core/connection.py +285 -0
- toolplane/core/errors.py +298 -0
- toolplane/core/machine.py +480 -0
- toolplane/core/request.py +775 -0
- toolplane/core/session.py +332 -0
- toolplane/core/session_context.py +514 -0
- toolplane/core/task.py +130 -0
- toolplane/core/tool.py +329 -0
- toolplane/http_core/__init__.py +37 -0
- toolplane/http_core/http_config.py +97 -0
- toolplane/http_core/http_connection.py +409 -0
- toolplane/http_core/http_machine.py +298 -0
- toolplane/http_core/http_request.py +748 -0
- toolplane/http_core/http_session.py +348 -0
- toolplane/http_core/http_session_context.py +491 -0
- toolplane/http_core/http_task.py +101 -0
- toolplane/http_core/http_tool.py +400 -0
- toolplane/interfaces/__init__.py +27 -0
- toolplane/interfaces/client_interface.py +122 -0
- toolplane/interfaces/connection_interface.py +193 -0
- toolplane/interfaces/event_interface.py +290 -0
- toolplane/interfaces/request_interface.py +439 -0
- toolplane/interfaces/session_interface.py +288 -0
- toolplane/interfaces/tool_interface.py +441 -0
- toolplane/proto/__init__.py +0 -0
- toolplane/proto/service_pb2.py +315 -0
- toolplane/proto/service_pb2_grpc.py +2240 -0
- toolplane/provider_cli.py +268 -0
- toolplane/provider_registry.py +77 -0
- toolplane/provider_runtime.py +302 -0
- toolplane/toolkits/__init__.py +0 -0
- toolplane/toolkits/standalone_tools/__init__.py +0 -0
- toolplane/toolkits/standalone_tools/create_directory.py +94 -0
- toolplane/toolkits/standalone_tools/create_file.py +124 -0
- toolplane/toolkits/standalone_tools/file_search.py +229 -0
- toolplane/toolkits/standalone_tools/grep_search.py +372 -0
- toolplane/toolkits/standalone_tools/launcher.py +146 -0
- toolplane/toolkits/standalone_tools/list_dir.py +395 -0
- toolplane/toolkits/standalone_tools/read_file.py +346 -0
- toolplane/toolkits/standalone_tools/replace_string_in_file.py +407 -0
- toolplane/toolkits/standalone_tools/run_tests.py +66 -0
- toolplane/toolkits/standalone_tools/semantic_search.py +485 -0
- toolplane/toolkits/standalone_tools/standalone_toolkit.py +979 -0
- toolplane/toolkits/standalone_tools/test_failure_analysis.py +618 -0
- toolplane/toolkits/standalone_tools/test_standalone_toolkit.py +517 -0
- toolplane/toolkits/swe/__init__.py +35 -0
- toolplane/toolkits/swe/create_directory.py +15 -0
- toolplane/toolkits/swe/create_file.py +15 -0
- toolplane/toolkits/swe/descriptions.py +273 -0
- toolplane/toolkits/swe/execute_bash.py +93 -0
- toolplane/toolkits/swe/file_editor.py +775 -0
- toolplane/toolkits/swe/file_search.py +16 -0
- toolplane/toolkits/swe/finish.py +50 -0
- toolplane/toolkits/swe/grep_search.py +19 -0
- toolplane/toolkits/swe/list_dir.py +407 -0
- toolplane/toolkits/swe/read_file.py +18 -0
- toolplane/toolkits/swe/replace_string_in_file.py +17 -0
- toolplane/toolkits/swe/search.py +260 -0
- toolplane/toolkits/swe/semantic_search.py +20 -0
- toolplane/toolkits/swe/str_replace_editor.py +647 -0
- toolplane/toolkits/swe/submit.py +29 -0
- toolplane/toolkits/swe/swe_toolkit.py +1296 -0
- toolplane/toolplane_client.py +686 -0
- toolplane/toolplane_http_client.py +681 -0
- toolplane/utils/__init__.py +3 -0
- toolplane/utils/schema.py +146 -0
- toolplane_python_client-0.1.0.dist-info/METADATA +543 -0
- toolplane_python_client-0.1.0.dist-info/RECORD +81 -0
- toolplane_python_client-0.1.0.dist-info/WHEEL +5 -0
- toolplane_python_client-0.1.0.dist-info/entry_points.txt +2 -0
- toolplane_python_client-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Connection management interface definitions."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any, Dict, Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ConnectionState(Enum):
|
|
9
|
+
"""Connection states."""
|
|
10
|
+
|
|
11
|
+
DISCONNECTED = "disconnected"
|
|
12
|
+
CONNECTING = "connecting"
|
|
13
|
+
CONNECTED = "connected"
|
|
14
|
+
RECONNECTING = "reconnecting"
|
|
15
|
+
ERROR = "error"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ConnectionStrategy(Enum):
|
|
19
|
+
"""Connection strategies."""
|
|
20
|
+
|
|
21
|
+
DIRECT = "direct"
|
|
22
|
+
POOLED = "pooled"
|
|
23
|
+
LOAD_BALANCED = "load_balanced"
|
|
24
|
+
CIRCUIT_BREAKER = "circuit_breaker"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@runtime_checkable
|
|
28
|
+
class IConnectionManager(Protocol):
|
|
29
|
+
"""Protocol interface for connection management."""
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def connected(self) -> bool:
|
|
33
|
+
"""Check if connected to server."""
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def state(self) -> ConnectionState:
|
|
38
|
+
"""Get current connection state."""
|
|
39
|
+
...
|
|
40
|
+
|
|
41
|
+
def connect(self) -> bool:
|
|
42
|
+
"""Connect to server."""
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
def disconnect(self) -> None:
|
|
46
|
+
"""Disconnect from server."""
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
def health_check(self) -> bool:
|
|
50
|
+
"""Perform health check."""
|
|
51
|
+
...
|
|
52
|
+
|
|
53
|
+
def get_connection_info(self) -> Dict[str, Any]:
|
|
54
|
+
"""Get connection information."""
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class IConnectionStrategy(ABC):
|
|
59
|
+
"""Abstract strategy for connection management."""
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
def connect(self, config: Dict[str, Any]) -> bool:
|
|
63
|
+
"""Execute connection strategy."""
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
@abstractmethod
|
|
67
|
+
def disconnect(self) -> None:
|
|
68
|
+
"""Execute disconnection strategy."""
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
@abstractmethod
|
|
72
|
+
def health_check(self) -> bool:
|
|
73
|
+
"""Execute health check strategy."""
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
@abstractmethod
|
|
77
|
+
def get_metrics(self) -> Dict[str, Any]:
|
|
78
|
+
"""Get connection metrics."""
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class DirectConnectionStrategy(IConnectionStrategy):
|
|
83
|
+
"""Direct connection strategy implementation."""
|
|
84
|
+
|
|
85
|
+
def __init__(self):
|
|
86
|
+
self._connected = False
|
|
87
|
+
self._connection_info = {}
|
|
88
|
+
|
|
89
|
+
def connect(self, config: Dict[str, Any]) -> bool:
|
|
90
|
+
"""Establish direct connection."""
|
|
91
|
+
try:
|
|
92
|
+
# Implementation would depend on protocol
|
|
93
|
+
# This is a template for the strategy pattern
|
|
94
|
+
self._connection_info = {
|
|
95
|
+
"host": config.get("server_host", "localhost"),
|
|
96
|
+
"port": config.get("server_port", 8080),
|
|
97
|
+
"protocol": config.get("protocol", "http"),
|
|
98
|
+
"connected_at": __import__("time").time(),
|
|
99
|
+
}
|
|
100
|
+
self._connected = True
|
|
101
|
+
return True
|
|
102
|
+
except Exception:
|
|
103
|
+
self._connected = False
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
def disconnect(self) -> None:
|
|
107
|
+
"""Close direct connection."""
|
|
108
|
+
self._connected = False
|
|
109
|
+
self._connection_info.clear()
|
|
110
|
+
|
|
111
|
+
def health_check(self) -> bool:
|
|
112
|
+
"""Check direct connection health."""
|
|
113
|
+
return self._connected
|
|
114
|
+
|
|
115
|
+
def get_metrics(self) -> Dict[str, Any]:
|
|
116
|
+
"""Get direct connection metrics."""
|
|
117
|
+
return {
|
|
118
|
+
"connected": self._connected,
|
|
119
|
+
"connection_info": self._connection_info.copy(),
|
|
120
|
+
"strategy": "direct",
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class PooledConnectionStrategy(IConnectionStrategy):
|
|
125
|
+
"""Pooled connection strategy implementation."""
|
|
126
|
+
|
|
127
|
+
def __init__(self, pool_size: int = 5):
|
|
128
|
+
self.pool_size = pool_size
|
|
129
|
+
self._pool = []
|
|
130
|
+
self._active_connections = 0
|
|
131
|
+
|
|
132
|
+
def connect(self, config: Dict[str, Any]) -> bool:
|
|
133
|
+
"""Establish pooled connections."""
|
|
134
|
+
try:
|
|
135
|
+
# Initialize connection pool
|
|
136
|
+
for _ in range(self.pool_size):
|
|
137
|
+
conn_info = {
|
|
138
|
+
"id": len(self._pool),
|
|
139
|
+
"host": config.get("server_host", "localhost"),
|
|
140
|
+
"port": config.get("server_port", 8080),
|
|
141
|
+
"active": False,
|
|
142
|
+
"created_at": __import__("time").time(),
|
|
143
|
+
}
|
|
144
|
+
self._pool.append(conn_info)
|
|
145
|
+
|
|
146
|
+
self._active_connections = self.pool_size
|
|
147
|
+
return True
|
|
148
|
+
except Exception:
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
def disconnect(self) -> None:
|
|
152
|
+
"""Close all pooled connections."""
|
|
153
|
+
self._pool.clear()
|
|
154
|
+
self._active_connections = 0
|
|
155
|
+
|
|
156
|
+
def health_check(self) -> bool:
|
|
157
|
+
"""Check pool health."""
|
|
158
|
+
return self._active_connections > 0
|
|
159
|
+
|
|
160
|
+
def get_metrics(self) -> Dict[str, Any]:
|
|
161
|
+
"""Get pooled connection metrics."""
|
|
162
|
+
return {
|
|
163
|
+
"pool_size": len(self._pool),
|
|
164
|
+
"active_connections": self._active_connections,
|
|
165
|
+
"strategy": "pooled",
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class ConnectionStrategyFactory:
|
|
170
|
+
"""Factory for creating connection strategies."""
|
|
171
|
+
|
|
172
|
+
_strategies = {
|
|
173
|
+
ConnectionStrategy.DIRECT: DirectConnectionStrategy,
|
|
174
|
+
ConnectionStrategy.POOLED: PooledConnectionStrategy,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
@classmethod
|
|
178
|
+
def create_strategy(
|
|
179
|
+
self, strategy_type: ConnectionStrategy, **kwargs
|
|
180
|
+
) -> IConnectionStrategy:
|
|
181
|
+
"""Create a connection strategy instance."""
|
|
182
|
+
if strategy_type not in self._strategies:
|
|
183
|
+
raise ValueError(f"Unsupported connection strategy: {strategy_type}")
|
|
184
|
+
|
|
185
|
+
strategy_class = self._strategies[strategy_type]
|
|
186
|
+
return strategy_class(**kwargs)
|
|
187
|
+
|
|
188
|
+
@classmethod
|
|
189
|
+
def register_strategy(
|
|
190
|
+
cls, strategy_type: ConnectionStrategy, strategy_class: type
|
|
191
|
+
) -> None:
|
|
192
|
+
"""Register a new connection strategy."""
|
|
193
|
+
cls._strategies[strategy_type] = strategy_class
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""Event system interfaces for decoupled component communication."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import weakref
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Any, Callable, Dict, List, Optional, Protocol, runtime_checkable
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EventType(Enum):
|
|
15
|
+
"""Standard event types for Toolplane system."""
|
|
16
|
+
|
|
17
|
+
# Connection events
|
|
18
|
+
CONNECTION_ESTABLISHED = "connection.established"
|
|
19
|
+
CONNECTION_LOST = "connection.lost"
|
|
20
|
+
CONNECTION_ERROR = "connection.error"
|
|
21
|
+
|
|
22
|
+
# Session events
|
|
23
|
+
SESSION_CREATED = "session.created"
|
|
24
|
+
SESSION_DESTROYED = "session.destroyed"
|
|
25
|
+
SESSION_ERROR = "session.error"
|
|
26
|
+
|
|
27
|
+
# Tool events
|
|
28
|
+
TOOL_REGISTERED = "tool.registered"
|
|
29
|
+
TOOL_UNREGISTERED = "tool.unregistered"
|
|
30
|
+
TOOL_EXECUTED = "tool.executed"
|
|
31
|
+
TOOL_ERROR = "tool.error"
|
|
32
|
+
|
|
33
|
+
# Machine events
|
|
34
|
+
MACHINE_REGISTERED = "machine.registered"
|
|
35
|
+
MACHINE_UNREGISTERED = "machine.unregistered"
|
|
36
|
+
MACHINE_HEARTBEAT_FAILED = "machine.heartbeat_failed"
|
|
37
|
+
MACHINE_RECOVERY_STARTED = "machine.recovery_started"
|
|
38
|
+
MACHINE_RECOVERY_SUCCEEDED = "machine.recovery_succeeded"
|
|
39
|
+
MACHINE_RECOVERY_FAILED = "machine.recovery_failed"
|
|
40
|
+
|
|
41
|
+
# Request events
|
|
42
|
+
REQUEST_STARTED = "request.started"
|
|
43
|
+
REQUEST_COMPLETED = "request.completed"
|
|
44
|
+
REQUEST_FAILED = "request.failed"
|
|
45
|
+
REQUEST_TIMEOUT = "request.timeout"
|
|
46
|
+
|
|
47
|
+
# Client events
|
|
48
|
+
CLIENT_STARTED = "client.started"
|
|
49
|
+
CLIENT_STOPPED = "client.stopped"
|
|
50
|
+
CLIENT_ERROR = "client.error"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class Event:
|
|
55
|
+
"""Represents an event in the system."""
|
|
56
|
+
|
|
57
|
+
type: EventType
|
|
58
|
+
source: str
|
|
59
|
+
data: Dict[str, Any] = field(default_factory=dict)
|
|
60
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
61
|
+
correlation_id: Optional[str] = None
|
|
62
|
+
|
|
63
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
64
|
+
"""Convert event to dictionary."""
|
|
65
|
+
return {
|
|
66
|
+
"type": self.type.value,
|
|
67
|
+
"source": self.source,
|
|
68
|
+
"data": self.data,
|
|
69
|
+
"timestamp": self.timestamp.isoformat(),
|
|
70
|
+
"correlation_id": self.correlation_id,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@runtime_checkable
|
|
75
|
+
class IEventHandler(Protocol):
|
|
76
|
+
"""Protocol for event handlers."""
|
|
77
|
+
|
|
78
|
+
def handle_event(self, event: Event) -> None:
|
|
79
|
+
"""Handle an event."""
|
|
80
|
+
...
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class IEventEmitter(ABC):
|
|
84
|
+
"""Abstract interface for event emission."""
|
|
85
|
+
|
|
86
|
+
@abstractmethod
|
|
87
|
+
def emit(self, event: Event) -> None:
|
|
88
|
+
"""Emit an event."""
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
@abstractmethod
|
|
92
|
+
def subscribe(
|
|
93
|
+
self,
|
|
94
|
+
event_type: EventType,
|
|
95
|
+
handler: IEventHandler,
|
|
96
|
+
filter_func: Optional[Callable[[Event], bool]] = None,
|
|
97
|
+
) -> str:
|
|
98
|
+
"""Subscribe to events of a specific type."""
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
@abstractmethod
|
|
102
|
+
def unsubscribe(self, subscription_id: str) -> bool:
|
|
103
|
+
"""Unsubscribe from events."""
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
@abstractmethod
|
|
107
|
+
def get_subscribers(self, event_type: EventType) -> List[IEventHandler]:
|
|
108
|
+
"""Get subscribers for an event type."""
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class EventBus(IEventEmitter):
|
|
113
|
+
"""Central event bus implementation with weak references."""
|
|
114
|
+
|
|
115
|
+
def __init__(self):
|
|
116
|
+
self._subscribers: Dict[EventType, Dict[str, weakref.ReferenceType]] = {}
|
|
117
|
+
self._filters: Dict[str, Callable[[Event], bool]] = {}
|
|
118
|
+
self._subscription_counter = 0
|
|
119
|
+
|
|
120
|
+
def emit(self, event: Event) -> None:
|
|
121
|
+
"""Emit an event to all subscribers."""
|
|
122
|
+
if event.type not in self._subscribers:
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
# Clean up dead references and call live handlers
|
|
126
|
+
dead_refs = []
|
|
127
|
+
for sub_id, handler_ref in self._subscribers[event.type].items():
|
|
128
|
+
handler = handler_ref()
|
|
129
|
+
if handler is None:
|
|
130
|
+
dead_refs.append(sub_id)
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
# Apply filter if present
|
|
134
|
+
if sub_id in self._filters:
|
|
135
|
+
if not self._filters[sub_id](event):
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
handler.handle_event(event)
|
|
140
|
+
except Exception as e:
|
|
141
|
+
# Log error but don't stop other handlers
|
|
142
|
+
logger.warning("Error in event handler %s: %s", sub_id, e)
|
|
143
|
+
|
|
144
|
+
# Clean up dead references
|
|
145
|
+
for sub_id in dead_refs:
|
|
146
|
+
self._remove_subscription(event.type, sub_id)
|
|
147
|
+
|
|
148
|
+
def subscribe(
|
|
149
|
+
self,
|
|
150
|
+
event_type: EventType,
|
|
151
|
+
handler: IEventHandler,
|
|
152
|
+
filter_func: Optional[Callable[[Event], bool]] = None,
|
|
153
|
+
) -> str:
|
|
154
|
+
"""Subscribe to events of a specific type."""
|
|
155
|
+
if event_type not in self._subscribers:
|
|
156
|
+
self._subscribers[event_type] = {}
|
|
157
|
+
|
|
158
|
+
# Generate unique subscription ID
|
|
159
|
+
self._subscription_counter += 1
|
|
160
|
+
sub_id = f"sub_{self._subscription_counter}"
|
|
161
|
+
|
|
162
|
+
# Store weak reference to handler
|
|
163
|
+
self._subscribers[event_type][sub_id] = weakref.ref(handler)
|
|
164
|
+
|
|
165
|
+
# Store filter if provided
|
|
166
|
+
if filter_func:
|
|
167
|
+
self._filters[sub_id] = filter_func
|
|
168
|
+
|
|
169
|
+
return sub_id
|
|
170
|
+
|
|
171
|
+
def unsubscribe(self, subscription_id: str) -> bool:
|
|
172
|
+
"""Unsubscribe from events."""
|
|
173
|
+
for event_type in self._subscribers:
|
|
174
|
+
if subscription_id in self._subscribers[event_type]:
|
|
175
|
+
self._remove_subscription(event_type, subscription_id)
|
|
176
|
+
return True
|
|
177
|
+
return False
|
|
178
|
+
|
|
179
|
+
def get_subscribers(self, event_type: EventType) -> List[IEventHandler]:
|
|
180
|
+
"""Get live subscribers for an event type."""
|
|
181
|
+
if event_type not in self._subscribers:
|
|
182
|
+
return []
|
|
183
|
+
|
|
184
|
+
subscribers = []
|
|
185
|
+
dead_refs = []
|
|
186
|
+
|
|
187
|
+
for sub_id, handler_ref in self._subscribers[event_type].items():
|
|
188
|
+
handler = handler_ref()
|
|
189
|
+
if handler is None:
|
|
190
|
+
dead_refs.append(sub_id)
|
|
191
|
+
else:
|
|
192
|
+
subscribers.append(handler)
|
|
193
|
+
|
|
194
|
+
# Clean up dead references
|
|
195
|
+
for sub_id in dead_refs:
|
|
196
|
+
self._remove_subscription(event_type, sub_id)
|
|
197
|
+
|
|
198
|
+
return subscribers
|
|
199
|
+
|
|
200
|
+
def _remove_subscription(self, event_type: EventType, sub_id: str) -> None:
|
|
201
|
+
"""Remove a subscription."""
|
|
202
|
+
if event_type in self._subscribers and sub_id in self._subscribers[event_type]:
|
|
203
|
+
del self._subscribers[event_type][sub_id]
|
|
204
|
+
|
|
205
|
+
if sub_id in self._filters:
|
|
206
|
+
del self._filters[sub_id]
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class EventHandlerMixin:
|
|
210
|
+
"""Mixin to add event handling capabilities to classes."""
|
|
211
|
+
|
|
212
|
+
def __init__(self, *args, **kwargs):
|
|
213
|
+
super().__init__(*args, **kwargs)
|
|
214
|
+
self._event_subscriptions: List[str] = []
|
|
215
|
+
|
|
216
|
+
def subscribe_to_events(
|
|
217
|
+
self,
|
|
218
|
+
event_bus: IEventEmitter,
|
|
219
|
+
event_types: List[EventType],
|
|
220
|
+
filter_func: Optional[Callable[[Event], bool]] = None,
|
|
221
|
+
) -> None:
|
|
222
|
+
"""Subscribe to multiple event types."""
|
|
223
|
+
for event_type in event_types:
|
|
224
|
+
sub_id = event_bus.subscribe(event_type, self, filter_func)
|
|
225
|
+
self._event_subscriptions.append(sub_id)
|
|
226
|
+
|
|
227
|
+
def unsubscribe_from_events(self, event_bus: IEventEmitter) -> None:
|
|
228
|
+
"""Unsubscribe from all events."""
|
|
229
|
+
for sub_id in self._event_subscriptions:
|
|
230
|
+
event_bus.unsubscribe(sub_id)
|
|
231
|
+
self._event_subscriptions.clear()
|
|
232
|
+
|
|
233
|
+
def handle_event(self, event: Event) -> None:
|
|
234
|
+
"""Default event handler - override in subclasses."""
|
|
235
|
+
pass
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# Global event bus instance (singleton pattern)
|
|
239
|
+
_global_event_bus: Optional[EventBus] = None
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def get_global_event_bus() -> EventBus:
|
|
243
|
+
"""Get the global event bus instance."""
|
|
244
|
+
global _global_event_bus
|
|
245
|
+
if _global_event_bus is None:
|
|
246
|
+
_global_event_bus = EventBus()
|
|
247
|
+
return _global_event_bus
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class EventLogger(IEventHandler):
|
|
251
|
+
"""Event handler that logs events."""
|
|
252
|
+
|
|
253
|
+
def __init__(self, log_func: Optional[Callable[[str], None]] = None):
|
|
254
|
+
self.log_func = log_func or print
|
|
255
|
+
|
|
256
|
+
def handle_event(self, event: Event) -> None:
|
|
257
|
+
"""Log the event."""
|
|
258
|
+
self.log_func(
|
|
259
|
+
f"Event: {event.type.value} from {event.source} "
|
|
260
|
+
f"at {event.timestamp.isoformat()}"
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class EventMetrics(IEventHandler):
|
|
265
|
+
"""Event handler that collects metrics."""
|
|
266
|
+
|
|
267
|
+
def __init__(self):
|
|
268
|
+
self.event_counts: Dict[EventType, int] = {}
|
|
269
|
+
self.error_counts: Dict[str, int] = {}
|
|
270
|
+
|
|
271
|
+
def handle_event(self, event: Event) -> None:
|
|
272
|
+
"""Collect metrics from the event."""
|
|
273
|
+
# Count events by type
|
|
274
|
+
self.event_counts[event.type] = self.event_counts.get(event.type, 0) + 1
|
|
275
|
+
|
|
276
|
+
# Count errors by source
|
|
277
|
+
if "error" in event.type.value:
|
|
278
|
+
source = event.source
|
|
279
|
+
self.error_counts[source] = self.error_counts.get(source, 0) + 1
|
|
280
|
+
|
|
281
|
+
def get_metrics(self) -> Dict[str, Any]:
|
|
282
|
+
"""Get collected metrics."""
|
|
283
|
+
return {
|
|
284
|
+
"event_counts": {
|
|
285
|
+
et.value: count for et, count in self.event_counts.items()
|
|
286
|
+
},
|
|
287
|
+
"error_counts": self.error_counts.copy(),
|
|
288
|
+
"total_events": sum(self.event_counts.values()),
|
|
289
|
+
"total_errors": sum(self.error_counts.values()),
|
|
290
|
+
}
|