ipc-framework 1.0.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.
@@ -0,0 +1,28 @@
1
+ """
2
+ IPC Framework - Efficient Inter-Process Communication Framework
3
+
4
+ A framework for handling client-server communication with hierarchical
5
+ application and channel management.
6
+ """
7
+
8
+ from .core import IPCServer, IPCClient, Application, Channel, Message, MessageType
9
+ from .server import FrameworkServer
10
+ from .client import FrameworkClient
11
+ from .exceptions import IPCError, ConnectionError, RoutingError
12
+
13
+ __version__ = "1.0.0"
14
+ __author__ = "IPC Framework"
15
+
16
+ __all__ = [
17
+ "IPCServer",
18
+ "IPCClient",
19
+ "Application",
20
+ "Channel",
21
+ "Message",
22
+ "MessageType",
23
+ "FrameworkServer",
24
+ "FrameworkClient",
25
+ "IPCError",
26
+ "ConnectionError",
27
+ "RoutingError"
28
+ ]
@@ -0,0 +1,274 @@
1
+ """
2
+ Socket-based client implementation for the IPC Framework
3
+ """
4
+
5
+ import socket
6
+ import threading
7
+ import time
8
+ import json
9
+ from typing import Optional, Callable, Dict, Any
10
+ from .core import IPCClient, Message, MessageType
11
+ from .exceptions import ConnectionError, SerializationError
12
+
13
+
14
+ class FrameworkClient(IPCClient):
15
+ """Socket-based IPC Client implementation"""
16
+
17
+ def __init__(self, app_id: str, host: str = "localhost", port: int = 8888, connection_timeout: float = 10.0):
18
+ super().__init__(app_id, host, port)
19
+ self.connection_timeout = connection_timeout
20
+ self.socket: Optional[socket.socket] = None
21
+ self.receive_thread: Optional[threading.Thread] = None
22
+ self.pending_responses: Dict[str, Any] = {} # message_id -> response data
23
+ self.response_handlers: Dict[str, Callable] = {} # message_id -> handler
24
+ self._socket_lock = threading.RLock()
25
+ self._response_lock = threading.RLock()
26
+
27
+ def connect(self) -> bool:
28
+ """Connect to the IPC server"""
29
+ if self.connected:
30
+ return True
31
+
32
+ try:
33
+ self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
34
+ self.socket.settimeout(self.connection_timeout)
35
+ self.socket.connect((self.host, self.port))
36
+
37
+ # Send connection message
38
+ connect_message = Message(
39
+ message_id="",
40
+ app_id=self.app_id,
41
+ channel_id="system",
42
+ message_type=MessageType.REQUEST,
43
+ payload={
44
+ 'action': 'connect',
45
+ 'connection_id': self.connection_id
46
+ },
47
+ timestamp=0
48
+ )
49
+
50
+ self._send_message(connect_message)
51
+
52
+ # Wait for acknowledgment
53
+ ack_data = self._receive_message()
54
+ if ack_data:
55
+ ack_message = Message.from_json(ack_data)
56
+ if (ack_message.message_type == MessageType.RESPONSE and
57
+ ack_message.payload.get('status') == 'connected'):
58
+ self.connected = True
59
+
60
+ # Start receive thread
61
+ self.receive_thread = threading.Thread(target=self._receive_loop, daemon=True)
62
+ self.receive_thread.start()
63
+
64
+ print(f"Connected to IPC server as {self.app_id}:{self.connection_id}")
65
+ return True
66
+
67
+ print("Failed to receive connection acknowledgment")
68
+ return False
69
+
70
+ except Exception as e:
71
+ print(f"Connection failed: {e}")
72
+ if self.socket:
73
+ try:
74
+ self.socket.close()
75
+ except:
76
+ pass
77
+ self.socket = None
78
+ return False
79
+
80
+ def disconnect(self):
81
+ """Disconnect from the IPC server"""
82
+ if not self.connected:
83
+ return
84
+
85
+ self.connected = False
86
+
87
+ # Close socket
88
+ if self.socket:
89
+ try:
90
+ self.socket.close()
91
+ except:
92
+ pass
93
+ self.socket = None
94
+
95
+ # Wait for receive thread to finish
96
+ if self.receive_thread and self.receive_thread.is_alive():
97
+ self.receive_thread.join(timeout=1.0)
98
+
99
+ print(f"Disconnected from IPC server")
100
+
101
+ def _send_message(self, message: Message) -> str:
102
+ """Internal method to send message"""
103
+ if not self.connected and message.message_type != MessageType.REQUEST:
104
+ raise ConnectionError("Not connected to server")
105
+
106
+ try:
107
+ with self._socket_lock:
108
+ if not self.socket:
109
+ raise ConnectionError("Socket is not available")
110
+
111
+ message_data = message.to_json().encode('utf-8')
112
+ message_length = len(message_data)
113
+
114
+ # Send message length first (4 bytes)
115
+ length_bytes = message_length.to_bytes(4, byteorder='big')
116
+ self.socket.sendall(length_bytes)
117
+
118
+ # Send message data
119
+ self.socket.sendall(message_data)
120
+
121
+ return message.message_id
122
+
123
+ except Exception as e:
124
+ raise ConnectionError(f"Failed to send message: {e}")
125
+
126
+ def _receive_message(self) -> Optional[str]:
127
+ """Receive a message from the server"""
128
+ try:
129
+ with self._socket_lock:
130
+ if not self.socket:
131
+ return None
132
+
133
+ # First, receive the message length (4 bytes)
134
+ length_data = b""
135
+ while len(length_data) < 4:
136
+ chunk = self.socket.recv(4 - len(length_data))
137
+ if not chunk:
138
+ return None
139
+ length_data += chunk
140
+
141
+ message_length = int.from_bytes(length_data, byteorder='big')
142
+
143
+ # Then receive the message data
144
+ message_data = b""
145
+ while len(message_data) < message_length:
146
+ chunk = self.socket.recv(message_length - len(message_data))
147
+ if not chunk:
148
+ return None
149
+ message_data += chunk
150
+
151
+ return message_data.decode('utf-8')
152
+
153
+ except Exception as e:
154
+ if self.connected:
155
+ print(f"Error receiving message: {e}")
156
+ return None
157
+
158
+ def _receive_loop(self):
159
+ """Background thread for receiving messages"""
160
+ while self.connected:
161
+ try:
162
+ data = self._receive_message()
163
+ if not data:
164
+ break
165
+
166
+ message = Message.from_json(data)
167
+
168
+ # Check if this is a response to a pending request
169
+ if message.reply_to:
170
+ with self._response_lock:
171
+ if message.reply_to in self.response_handlers:
172
+ handler = self.response_handlers.pop(message.reply_to)
173
+ try:
174
+ handler(message)
175
+ except Exception as e:
176
+ print(f"Response handler error: {e}")
177
+ else:
178
+ # Store response for sync requests
179
+ self.pending_responses[message.reply_to] = message
180
+ else:
181
+ # Handle regular messages
182
+ self.handle_message(message)
183
+
184
+ except Exception as e:
185
+ if self.connected:
186
+ print(f"Receive loop error: {e}")
187
+ break
188
+
189
+ # Connection lost
190
+ if self.connected:
191
+ print("Connection to server lost")
192
+ self.connected = False
193
+
194
+ def send_request(self, channel_id: str, data: Any, timeout: float = 5.0) -> Optional[Message]:
195
+ """Send a request and wait for response"""
196
+ message_id = self.request(channel_id, data)
197
+
198
+ # Wait for response
199
+ start_time = time.time()
200
+ while time.time() - start_time < timeout:
201
+ with self._response_lock:
202
+ if message_id in self.pending_responses:
203
+ return self.pending_responses.pop(message_id)
204
+ time.sleep(0.01)
205
+
206
+ return None # Timeout
207
+
208
+ def send_request_async(self, channel_id: str, data: Any, callback: Callable[[Message], None]) -> str:
209
+ """Send a request with async callback"""
210
+ message_id = self.request(channel_id, data)
211
+
212
+ with self._response_lock:
213
+ self.response_handlers[message_id] = callback
214
+
215
+ return message_id
216
+
217
+ def notify(self, channel_id: str, data: Any) -> str:
218
+ """Send a notification (no response expected)"""
219
+ return self.send_message(channel_id, MessageType.NOTIFICATION, data)
220
+
221
+ def create_channel_handler(self, channel_id: str, handler: Callable[[Message], None]):
222
+ """Create a handler for a specific channel"""
223
+ with self._lock:
224
+ self.message_handlers[channel_id] = handler
225
+
226
+ def remove_channel_handler(self, channel_id: str):
227
+ """Remove handler for a channel"""
228
+ with self._lock:
229
+ self.message_handlers.pop(channel_id, None)
230
+
231
+ def wait_for_message(self, channel_id: str, timeout: float = 5.0) -> Optional[Message]:
232
+ """Wait for a message on a specific channel"""
233
+ received_message = None
234
+ event = threading.Event()
235
+
236
+ def temp_handler(message: Message):
237
+ nonlocal received_message
238
+ received_message = message
239
+ event.set()
240
+
241
+ # Set temporary handler
242
+ original_handler = self.message_handlers.get(channel_id)
243
+ self.create_channel_handler(channel_id, temp_handler)
244
+
245
+ try:
246
+ # Wait for message
247
+ if event.wait(timeout):
248
+ return received_message
249
+ return None
250
+ finally:
251
+ # Restore original handler
252
+ if original_handler:
253
+ self.create_channel_handler(channel_id, original_handler)
254
+ else:
255
+ self.remove_channel_handler(channel_id)
256
+
257
+ def ping(self, timeout: float = 2.0) -> bool:
258
+ """Ping the server to check connection"""
259
+ try:
260
+ response = self.send_request("system", {"action": "ping"}, timeout)
261
+ return response is not None and response.payload.get("status") == "pong"
262
+ except:
263
+ return False
264
+
265
+ def get_connection_info(self) -> Dict[str, Any]:
266
+ """Get information about this connection"""
267
+ return {
268
+ 'app_id': self.app_id,
269
+ 'connection_id': self.connection_id,
270
+ 'connected': self.connected,
271
+ 'host': self.host,
272
+ 'port': self.port,
273
+ 'subscribed_channels': list(self.message_handlers.keys())
274
+ }
ipc_framework/core.py ADDED
@@ -0,0 +1,353 @@
1
+ """
2
+ Core classes for the IPC Framework
3
+ """
4
+
5
+ import json
6
+ import uuid
7
+ import time
8
+ import threading
9
+ from typing import Dict, List, Optional, Callable, Any
10
+ from dataclasses import dataclass, asdict
11
+ from enum import Enum
12
+
13
+
14
+ class MessageType(Enum):
15
+ """Types of messages in the IPC system"""
16
+ REQUEST = "request"
17
+ RESPONSE = "response"
18
+ NOTIFICATION = "notification"
19
+ SUBSCRIBE = "subscribe"
20
+ UNSUBSCRIBE = "unsubscribe"
21
+ PUBLISH = "publish"
22
+
23
+
24
+ @dataclass
25
+ class Message:
26
+ """Represents a message in the IPC system"""
27
+ message_id: str
28
+ app_id: str
29
+ channel_id: str
30
+ message_type: MessageType
31
+ payload: Any
32
+ timestamp: float
33
+ reply_to: Optional[str] = None
34
+
35
+ def __post_init__(self):
36
+ if not self.message_id:
37
+ self.message_id = str(uuid.uuid4())
38
+ if not self.timestamp:
39
+ self.timestamp = time.time()
40
+
41
+ def to_dict(self) -> Dict:
42
+ """Convert message to dictionary for serialization"""
43
+ data = asdict(self)
44
+ data['message_type'] = self.message_type.value
45
+ return data
46
+
47
+ @classmethod
48
+ def from_dict(cls, data: Dict) -> 'Message':
49
+ """Create message from dictionary"""
50
+ data['message_type'] = MessageType(data['message_type'])
51
+ return cls(**data)
52
+
53
+ def to_json(self) -> str:
54
+ """Serialize message to JSON"""
55
+ return json.dumps(self.to_dict())
56
+
57
+ @classmethod
58
+ def from_json(cls, json_str: str) -> 'Message':
59
+ """Deserialize message from JSON"""
60
+ return cls.from_dict(json.loads(json_str))
61
+
62
+
63
+ class Channel:
64
+ """Represents a communication channel within an application"""
65
+
66
+ def __init__(self, channel_id: str, app_id: str):
67
+ self.channel_id = channel_id
68
+ self.app_id = app_id
69
+ self.subscribers: List[str] = [] # Connection IDs
70
+ self.handlers: Dict[MessageType, Callable] = {}
71
+ self.created_at = time.time()
72
+ self._lock = threading.RLock()
73
+
74
+ def add_subscriber(self, connection_id: str):
75
+ """Add a subscriber to this channel"""
76
+ with self._lock:
77
+ if connection_id not in self.subscribers:
78
+ self.subscribers.append(connection_id)
79
+
80
+ def remove_subscriber(self, connection_id: str):
81
+ """Remove a subscriber from this channel"""
82
+ with self._lock:
83
+ if connection_id in self.subscribers:
84
+ self.subscribers.remove(connection_id)
85
+
86
+ def set_handler(self, message_type: MessageType, handler: Callable):
87
+ """Set a message handler for this channel"""
88
+ with self._lock:
89
+ self.handlers[message_type] = handler
90
+
91
+ def get_handler(self, message_type: MessageType) -> Optional[Callable]:
92
+ """Get the handler for a message type"""
93
+ with self._lock:
94
+ return self.handlers.get(message_type)
95
+
96
+ def get_subscribers(self) -> List[str]:
97
+ """Get list of current subscribers"""
98
+ with self._lock:
99
+ return self.subscribers.copy()
100
+
101
+
102
+ class Application:
103
+ """Represents an application with multiple channels"""
104
+
105
+ def __init__(self, app_id: str, name: str = None):
106
+ self.app_id = app_id
107
+ self.name = name or app_id
108
+ self.channels: Dict[str, Channel] = {}
109
+ self.created_at = time.time()
110
+ self.last_activity = time.time()
111
+ self._lock = threading.RLock()
112
+
113
+ def create_channel(self, channel_id: str) -> Channel:
114
+ """Create a new channel in this application"""
115
+ with self._lock:
116
+ if channel_id in self.channels:
117
+ return self.channels[channel_id]
118
+
119
+ channel = Channel(channel_id, self.app_id)
120
+ self.channels[channel_id] = channel
121
+ self.last_activity = time.time()
122
+ return channel
123
+
124
+ def get_channel(self, channel_id: str) -> Optional[Channel]:
125
+ """Get a channel by ID"""
126
+ with self._lock:
127
+ return self.channels.get(channel_id)
128
+
129
+ def remove_channel(self, channel_id: str) -> bool:
130
+ """Remove a channel"""
131
+ with self._lock:
132
+ if channel_id in self.channels:
133
+ del self.channels[channel_id]
134
+ self.last_activity = time.time()
135
+ return True
136
+ return False
137
+
138
+ def list_channels(self) -> List[str]:
139
+ """List all channel IDs in this application"""
140
+ with self._lock:
141
+ return list(self.channels.keys())
142
+
143
+ def update_activity(self):
144
+ """Update the last activity timestamp"""
145
+ self.last_activity = time.time()
146
+
147
+
148
+ class ConnectionManager:
149
+ """Manages client connections"""
150
+
151
+ def __init__(self):
152
+ self.connections: Dict[str, Any] = {} # connection_id -> connection object
153
+ self.connection_apps: Dict[str, str] = {} # connection_id -> app_id
154
+ self._lock = threading.RLock()
155
+
156
+ def add_connection(self, connection_id: str, connection_obj: Any, app_id: str):
157
+ """Add a new connection"""
158
+ with self._lock:
159
+ self.connections[connection_id] = connection_obj
160
+ self.connection_apps[connection_id] = app_id
161
+
162
+ def remove_connection(self, connection_id: str):
163
+ """Remove a connection"""
164
+ with self._lock:
165
+ self.connections.pop(connection_id, None)
166
+ self.connection_apps.pop(connection_id, None)
167
+
168
+ def get_connection(self, connection_id: str) -> Optional[Any]:
169
+ """Get a connection object"""
170
+ with self._lock:
171
+ return self.connections.get(connection_id)
172
+
173
+ def get_connections_for_app(self, app_id: str) -> List[str]:
174
+ """Get all connection IDs for an application"""
175
+ with self._lock:
176
+ return [conn_id for conn_id, app in self.connection_apps.items() if app == app_id]
177
+
178
+ def list_connections(self) -> List[str]:
179
+ """List all connection IDs"""
180
+ with self._lock:
181
+ return list(self.connections.keys())
182
+
183
+
184
+ class IPCServer:
185
+ """Base IPC Server class"""
186
+
187
+ def __init__(self, host: str = "localhost", port: int = 8888):
188
+ self.host = host
189
+ self.port = port
190
+ self.applications: Dict[str, Application] = {}
191
+ self.connection_manager = ConnectionManager()
192
+ self.running = False
193
+ self._lock = threading.RLock()
194
+
195
+ def create_application(self, app_id: str, name: str = None) -> Application:
196
+ """Create a new application"""
197
+ with self._lock:
198
+ if app_id in self.applications:
199
+ return self.applications[app_id]
200
+
201
+ app = Application(app_id, name)
202
+ self.applications[app_id] = app
203
+ return app
204
+
205
+ def get_application(self, app_id: str) -> Optional[Application]:
206
+ """Get an application by ID"""
207
+ with self._lock:
208
+ return self.applications.get(app_id)
209
+
210
+ def route_message(self, message: Message) -> bool:
211
+ """Route a message to the appropriate handlers"""
212
+ with self._lock:
213
+ app = self.get_application(message.app_id)
214
+ if not app:
215
+ return False
216
+
217
+ channel = app.get_channel(message.channel_id)
218
+ if not channel:
219
+ return False
220
+
221
+ app.update_activity()
222
+
223
+ # Handle different message types
224
+ if message.message_type == MessageType.PUBLISH:
225
+ return self._handle_publish(message, channel)
226
+ elif message.message_type == MessageType.SUBSCRIBE:
227
+ return self._handle_subscribe(message, channel)
228
+ elif message.message_type == MessageType.UNSUBSCRIBE:
229
+ return self._handle_unsubscribe(message, channel)
230
+ else:
231
+ handler = channel.get_handler(message.message_type)
232
+ if handler:
233
+ try:
234
+ handler(message)
235
+ return True
236
+ except Exception as e:
237
+ print(f"Handler error: {e}")
238
+ return False
239
+
240
+ return False
241
+
242
+ def _handle_publish(self, message: Message, channel: Channel) -> bool:
243
+ """Handle publish message by broadcasting to subscribers"""
244
+ subscribers = channel.get_subscribers()
245
+ for subscriber_id in subscribers:
246
+ connection = self.connection_manager.get_connection(subscriber_id)
247
+ if connection:
248
+ self.send_to_connection(connection, message)
249
+ return True
250
+
251
+ def _handle_subscribe(self, message: Message, channel: Channel) -> bool:
252
+ """Handle subscribe message"""
253
+ # Extract connection ID from message payload
254
+ connection_id = message.payload.get('connection_id')
255
+ if connection_id:
256
+ channel.add_subscriber(connection_id)
257
+ return True
258
+ return False
259
+
260
+ def _handle_unsubscribe(self, message: Message, channel: Channel) -> bool:
261
+ """Handle unsubscribe message"""
262
+ connection_id = message.payload.get('connection_id')
263
+ if connection_id:
264
+ channel.remove_subscriber(connection_id)
265
+ return True
266
+ return False
267
+
268
+ def send_to_connection(self, connection, message: Message):
269
+ """Send message to a specific connection - to be implemented by subclasses"""
270
+ raise NotImplementedError("Subclasses must implement send_to_connection")
271
+
272
+ def start(self):
273
+ """Start the server - to be implemented by subclasses"""
274
+ raise NotImplementedError("Subclasses must implement start")
275
+
276
+ def stop(self):
277
+ """Stop the server - to be implemented by subclasses"""
278
+ raise NotImplementedError("Subclasses must implement stop")
279
+
280
+
281
+ class IPCClient:
282
+ """Base IPC Client class"""
283
+
284
+ def __init__(self, app_id: str, host: str = "localhost", port: int = 8888):
285
+ self.app_id = app_id
286
+ self.host = host
287
+ self.port = port
288
+ self.connection_id = str(uuid.uuid4())
289
+ self.connected = False
290
+ self.message_handlers: Dict[str, Callable] = {} # channel_id -> handler
291
+ self._lock = threading.RLock()
292
+
293
+ def connect(self) -> bool:
294
+ """Connect to the server - to be implemented by subclasses"""
295
+ raise NotImplementedError("Subclasses must implement connect")
296
+
297
+ def disconnect(self):
298
+ """Disconnect from the server - to be implemented by subclasses"""
299
+ raise NotImplementedError("Subclasses must implement disconnect")
300
+
301
+ def send_message(self, channel_id: str, message_type: MessageType, payload: Any, reply_to: str = None) -> str:
302
+ """Send a message to a specific channel"""
303
+ message = Message(
304
+ message_id=str(uuid.uuid4()),
305
+ app_id=self.app_id,
306
+ channel_id=channel_id,
307
+ message_type=message_type,
308
+ payload=payload,
309
+ timestamp=time.time(),
310
+ reply_to=reply_to
311
+ )
312
+ return self._send_message(message)
313
+
314
+ def _send_message(self, message: Message) -> str:
315
+ """Internal method to send message - to be implemented by subclasses"""
316
+ raise NotImplementedError("Subclasses must implement _send_message")
317
+
318
+ def subscribe(self, channel_id: str, handler: Callable = None) -> bool:
319
+ """Subscribe to a channel"""
320
+ if handler:
321
+ with self._lock:
322
+ self.message_handlers[channel_id] = handler
323
+
324
+ payload = {'connection_id': self.connection_id}
325
+ self.send_message(channel_id, MessageType.SUBSCRIBE, payload)
326
+ return True
327
+
328
+ def unsubscribe(self, channel_id: str) -> bool:
329
+ """Unsubscribe from a channel"""
330
+ with self._lock:
331
+ self.message_handlers.pop(channel_id, None)
332
+
333
+ payload = {'connection_id': self.connection_id}
334
+ self.send_message(channel_id, MessageType.UNSUBSCRIBE, payload)
335
+ return True
336
+
337
+ def publish(self, channel_id: str, data: Any) -> str:
338
+ """Publish data to a channel"""
339
+ return self.send_message(channel_id, MessageType.PUBLISH, data)
340
+
341
+ def request(self, channel_id: str, data: Any) -> str:
342
+ """Send a request message"""
343
+ return self.send_message(channel_id, MessageType.REQUEST, data)
344
+
345
+ def handle_message(self, message: Message):
346
+ """Handle incoming message"""
347
+ with self._lock:
348
+ handler = self.message_handlers.get(message.channel_id)
349
+ if handler:
350
+ try:
351
+ handler(message)
352
+ except Exception as e:
353
+ print(f"Message handler error: {e}")