Skip to content

Commit a4f1787

Browse files
committed
refactoring project and publish/send API
1 parent e6c537a commit a4f1787

File tree

8 files changed

+225
-220
lines changed

8 files changed

+225
-220
lines changed

demos/publish_and_reply.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,7 @@ async def handle_session_started(message):
4343
await client.wait_for('session.started')
4444

4545
# Send a message
46-
wait_for = await client.send('Hello, world!', {
47-
'messageType': 'text-message'
48-
})
46+
wait_for = await client.send('Hello, world!', message_type='text-message')
4947
await wait_for.wait_for_ack()
5048

5149
# Define a message handler
@@ -59,9 +57,7 @@ def handle_message(message, reply_fn):
5957
# Subscribe to chat.text-message events
6058
client.on('chat.text-message', handle_message)
6159

62-
wait_for = await client.publish('chat', 'Hello out there!', {
63-
'messageType': 'text-message'
64-
})
60+
wait_for = await client.publish('chat', 'Hello out there!', message_type='text-message')
6561
response = await wait_for.wait_for_reply()
6662
print('Reply:', response)
6763

demos/rpc/client.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,7 @@ async def get_url():
3535
# Define a message handler
3636
async def handle_session_started(message):
3737
client.logger.info('Requesting server time...')
38-
waiter = await client.send('', {
39-
'messageType': 'gettime'
40-
})
38+
waiter = await client.send('', message_type='gettime')
4139

4240
response, = await waiter.wait_for_reply(timeout=5)
4341
client.logger.info(f"Server time: {response['data']['time']}")

realtime_pubsub_client/client.py

Lines changed: 27 additions & 198 deletions
Original file line numberDiff line numberDiff line change
@@ -15,129 +15,11 @@
1515
import json # JSON encoding and decoding
1616
import secrets # Generate secure random numbers
1717
import logging # Logging library
18-
from typing import Callable # Type hinting support
19-
from websockets import connect, exceptions
20-
21-
22-
class EventEmitter:
23-
"""
24-
A simple event emitter class that allows registering and emitting events.
25-
Supports wildcard events using '*' and '**' with '.' as the separator.
26-
"""
27-
28-
def __init__(self):
29-
"""
30-
Initialize an `EventEmitter` instance with an empty events dictionary.
31-
"""
32-
self._events = {}
33-
34-
def on(self, event: str, listener: Callable):
35-
"""
36-
Register a listener for a specific event, with support for wildcards.
37-
38-
Args:
39-
event (str): The name of the event, can include wildcards ('*' or '**').
40-
listener (Callable): The function to call when the event is emitted.
41-
"""
42-
if event not in self._events:
43-
self._events[event] = []
44-
self._events[event].append(listener)
45-
46-
def off(self, event: str, listener: Callable):
47-
"""
48-
Remove a listener for a specific event.
49-
50-
Args:
51-
event (str): The name of the event.
52-
listener (Callable): The function to remove from the event listeners.
53-
"""
54-
if event in self._events:
55-
try:
56-
self._events[event].remove(listener)
57-
if not self._events[event]:
58-
del self._events[event]
59-
except ValueError:
60-
# Listener not found in the list
61-
pass
62-
63-
def emit(self, event: str, *args, **kwargs):
64-
"""
65-
Trigger all listeners associated with an event, supporting wildcards.
66-
67-
Args:
68-
event (str): The name of the event to emit.
69-
*args: Positional arguments to pass to the event listeners.
70-
**kwargs: Keyword arguments to pass to the event listeners.
71-
"""
72-
listeners = []
73-
for event_pattern, event_listeners in self._events.items():
74-
if self.event_matches(event_pattern, event):
75-
listeners.extend(event_listeners)
76-
for listener in listeners:
77-
if asyncio.iscoroutinefunction(listener):
78-
asyncio.create_task(listener(*args, **kwargs))
79-
else:
80-
listener(*args, **kwargs)
81-
82-
def once(self, event: str, listener: Callable):
83-
"""
84-
Register a listener for a specific event that will be called at most once.
85-
86-
Args:
87-
event (str): The name of the event.
88-
listener (Callable): The function to call when the event is emitted.
89-
"""
90-
91-
def _once_listener(*args, **kwargs):
92-
listener(*args, **kwargs)
93-
self.off(event, _once_listener)
94-
95-
self.on(event, _once_listener)
96-
97-
@staticmethod
98-
def event_matches(pattern: str, event_name: str) -> bool:
99-
"""
100-
Check if an event pattern matches an event name, supporting wildcards '*' and '**'.
101-
102-
Args:
103-
pattern (str): The event pattern, may include wildcards '*' and '**'.
104-
event_name (str): The event name to match against.
10518

106-
Returns:
107-
bool: True if the pattern matches the event name, False otherwise.
108-
"""
109-
110-
def match_segments(pattern_segments, event_segments):
111-
i = j = 0
112-
while i < len(pattern_segments) and j < len(event_segments):
113-
if pattern_segments[i] == '**':
114-
# '**' matches any number of segments, including zero
115-
if i == len(pattern_segments) - 1:
116-
# '**' at the end matches all remaining segments
117-
return True
118-
else:
119-
# Try to match remaining pattern with any position in event_segments
120-
for k in range(j, len(event_segments) + 1):
121-
if match_segments(pattern_segments[i + 1:], event_segments[k:]):
122-
return True
123-
return False
124-
elif pattern_segments[i] == '*':
125-
# '*' matches exactly one segment
126-
i += 1
127-
j += 1
128-
elif pattern_segments[i] == event_segments[j]:
129-
# Exact match
130-
i += 1
131-
j += 1
132-
else:
133-
return False
134-
while i < len(pattern_segments) and pattern_segments[i] == '**':
135-
i += 1
136-
return i == len(pattern_segments) and j == len(event_segments)
19+
from websockets import connect, exceptions
13720

138-
pattern_segments = pattern.split('.')
139-
event_segments = event_name.split('.')
140-
return match_segments(pattern_segments, event_segments)
21+
from realtime_pubsub_client.event_emitter import EventEmitter
22+
from realtime_pubsub_client.wait_for import WaitFor
14123

14224

14325
def reply(client, message):
@@ -158,14 +40,14 @@ def reply(client, message):
15840
ValueError: If the connection ID is not available in the incoming message.
15941
"""
16042

161-
def reply_function(data, status='ok', options=None):
43+
def reply_function(data, status='ok', compress=False):
16244
"""
16345
Sends a reply message back to the sender of the original message.
16446
16547
Args:
16648
data: The payload data to send in the reply.
16749
status (str, optional): The status of the reply. Defaults to 'ok'.
168-
options (dict, optional): Additional message options. Defaults to None.
50+
compress (bool, optional): Whether to compress the reply payload. Defaults to False.
16951
17052
Returns:
17153
WaitFor: An instance to wait for acknowledgments or replies.
@@ -181,69 +63,13 @@ def reply_function(data, status='ok', options=None):
18163
'data': data,
18264
'status': status,
18365
'id': message['data'].get('id'),
184-
},
185-
{
186-
'messageType': 'response',
187-
'compress': options.get('compress', False) if options else False,
188-
},
189-
))
66+
}, message_type='response', compress=compress, ))
19067
else:
19168
raise ValueError('Connection ID is not available in the message')
19269

19370
return reply_function
19471

19572

196-
class WaitFor:
197-
"""
198-
Class representing a factory for waiting on acknowledgments or replies.
199-
200-
The `WaitFor` class provides methods to wait for acknowledgments from the Messaging Gateway
201-
or replies from other subscribers or backend services. It is used in conjunction with
202-
message publishing and sending methods to ensure reliable communication.
203-
"""
204-
205-
def __init__(self, client, options):
206-
"""
207-
Initialize a new instance of the `WaitFor` class.
208-
209-
Args:
210-
client (RealtimeClient): The `RealtimeClient` instance associated with this factory.
211-
options (dict): The message options used for publishing or sending messages.
212-
"""
213-
self.client = client
214-
self.options = options
215-
216-
async def wait_for_ack(self, timeout=5):
217-
"""
218-
Wait for an acknowledgment event with a timeout.
219-
220-
Args:
221-
timeout (int, optional): The maximum time to wait for the acknowledgment in seconds. Defaults to 5.
222-
223-
Returns:
224-
Any: The acknowledgment data received.
225-
226-
Raises:
227-
TimeoutError: If the acknowledgment is not received within the timeout period.
228-
"""
229-
return await self.client.wait_for(f"ack.{self.options['id']}", timeout)
230-
231-
async def wait_for_reply(self, timeout=5):
232-
"""
233-
Wait for a reply event with a timeout.
234-
235-
Args:
236-
timeout (int, optional): The maximum time to wait for the reply in seconds. Defaults to 5.
237-
238-
Returns:
239-
Any: The reply data received.
240-
241-
Raises:
242-
TimeoutError: If the reply is not received within the timeout period.
243-
"""
244-
return await self.client.wait_for(f"response.{self.options['id']}", timeout)
245-
246-
24773
async def wait(ms):
24874
"""
24975
Wait for a specified duration before proceeding.
@@ -367,8 +193,7 @@ async def connect(self):
367193
raise ValueError('WebSocket URL is not provided')
368194

369195
try:
370-
max_float = float('inf')
371-
self.ws = await connect(ws_url, max_size=None, ping_interval=None, ping_timeout=None)
196+
self.ws = await connect(ws_url, max_size=None, ping_interval=None, ping_timeout=None)
372197
self.logger.info(f'Connected to WebSocket URL: {ws_url[:80]}...') # Masking the URL for security
373198
asyncio.ensure_future(self._receive_messages())
374199

@@ -393,7 +218,7 @@ async def disconnect(self):
393218
self.ws = None
394219
self.logger.info('WebSocket connection closed.')
395220

396-
async def publish(self, topic, payload, options=None):
221+
async def publish(self, topic, payload, message_type="broadcast", compress=False, message_id=None):
397222
"""
398223
Publish a message to a specified topic.
399224
@@ -403,7 +228,9 @@ async def publish(self, topic, payload, options=None):
403228
Args:
404229
topic (str): The topic to publish the message to.
405230
payload (str or dict): The message payload.
406-
options (dict, optional): Optional message options, including `id`, `messageType`, and `compress`.
231+
message_type (str, optional): The type of message being published. Defaults to "broadcast".
232+
compress (bool, optional): Whether to compress the message payload. Defaults to False.
233+
message_id (str, optional): The unique identifier for the message. Defaults to auto-generated value.
407234
408235
Returns:
409236
WaitFor: An instance to wait for acknowledgments or replies.
@@ -415,26 +242,26 @@ async def publish(self, topic, payload, options=None):
415242
self.logger.error('Attempted to publish without an active WebSocket connection.')
416243
raise Exception('WebSocket connection is not established')
417244

418-
options = options or {}
419-
options['id'] = options.get('id', get_random_id())
245+
if message_id is None:
246+
message_id = get_random_id()
420247

421248
message = json.dumps({
422249
'type': 'publish',
423250
'data': {
424251
'topic': topic,
425-
'messageType': options.get('messageType'),
426-
'compress': options.get('compress', False),
252+
'messageType': message_type,
253+
'compress': bool(compress),
427254
'payload': payload,
428-
'id': options['id'],
255+
'id': message_id,
429256
},
430257
})
431258

432259
self.logger.debug(f'Publishing message to topic {topic}: {payload}')
433260
await self.ws.send(message)
434261

435-
return WaitFor(self, options)
262+
return WaitFor(self, message_id)
436263

437-
async def send(self, payload, options=None):
264+
async def send(self, payload, message_type="broadcast", compress=False, message_id=None):
438265
"""
439266
Send a message directly to the server.
440267
@@ -443,7 +270,9 @@ async def send(self, payload, options=None):
443270
444271
Args:
445272
payload (str or dict): The message payload.
446-
options (dict, optional): Optional message options, including `id`, `messageType`, and `compress`.
273+
message_type (str, optional): The type of message being sent. Defaults to "broadcast".
274+
compress (bool, optional): Whether to compress the message payload. Defaults to False.
275+
message_id (str, optional): The unique identifier for the message. Defaults to auto-generated value.
447276
448277
Returns:
449278
WaitFor: An instance to wait for acknowledgments or replies.
@@ -455,23 +284,23 @@ async def send(self, payload, options=None):
455284
self.logger.error('Attempted to send without an active WebSocket connection.')
456285
raise Exception('WebSocket connection is not established')
457286

458-
options = options or {}
459-
options['id'] = options.get('id', get_random_id())
287+
if message_id is None:
288+
message_id = get_random_id()
460289

461290
message = json.dumps({
462291
'type': 'message',
463292
'data': {
464-
'messageType': options.get('messageType'),
465-
'compress': options.get('compress', False),
293+
'messageType': message_type,
294+
'compress': bool(compress),
466295
'payload': payload,
467-
'id': options['id'],
296+
'id': message_id,
468297
},
469298
})
470299

471300
self.logger.debug(f'Sending message: {payload}')
472301
await self.ws.send(message)
473302

474-
return WaitFor(self, options)
303+
return WaitFor(self, message_id)
475304

476305
async def subscribe_remote_topic(self, topic):
477306
"""

0 commit comments

Comments
 (0)