Skip to content

Commit 2591db6

Browse files
authored
Merge pull request #78 from rsocket/tutorial_final_app
Tutorial application and bug fixes
2 parents a80e094 + a3cdf0d commit 2591db6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+2283
-67
lines changed

CHANGELOG.rst

+9-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,15 @@ Changelog
33

44
v0.4.4
55
======
6-
- Fragmentation fix - empty payload (either in request or response) with fragmentation enabled failed to send.
6+
- Fragmentation fix - empty payload (either in request or response) with fragmentation enabled failed to send
7+
- Breaking change: *on_connection_lost* was renamed to *on_close*. An *on_connection_error* method was added to handle initial connection errors
8+
- Routing request handler:
9+
- Throws an RSocketUnknownRoute exception which results in an error frame on the requester side
10+
- Added error logging for response/stream/channel requests
11+
- Added *create_response* helper method as shorthand for creating a future with a Payload
12+
- Added *utf8_decode* helper. Decodes bytes to utf-8. If data is None, returns None.
13+
- Refactoring client reconnect flow
14+
- Added example code for tutorial on rsocket.io
715

816
v0.4.3
917
======

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ or install any of the extras:
2222
Example:
2323

2424
```shell
25-
pip install --pre rsocket[reactivex]
25+
pip install rsocket[reactivex]
2626
```
2727

2828
Alternatively, download the source code, build a package:

examples/client_reconnect.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import asyncio
22
import logging
33
import sys
4+
from typing import Optional
45

56
from rsocket.extensions.helpers import route, composite, authenticate_simple
67
from rsocket.extensions.mimetypes import WellKnownMimeTypes
@@ -21,7 +22,7 @@ async def request_response(client: RSocketClient) -> Payload:
2122

2223
class Handler(BaseRequestHandler):
2324

24-
async def on_connection_lost(self, rsocket: RSocketClient, exception: Exception):
25+
async def on_close(self, rsocket, exception: Optional[Exception] = None):
2526
await asyncio.sleep(5)
2627
await rsocket.reconnect()
2728

examples/tutorial/__init__.py

Whitespace-only changes.

examples/tutorial/reactivex/__init__.py

Whitespace-only changes.
+192
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import asyncio
2+
import json
3+
import logging
4+
import resource
5+
from asyncio import Event, Task
6+
from typing import List, Optional
7+
8+
from reactivex import operators
9+
10+
from examples.tutorial.step5.models import Message, chat_filename_mimetype, ServerStatistics, ClientStatistics
11+
from reactivestreams.publisher import DefaultPublisher
12+
from reactivestreams.subscriber import DefaultSubscriber
13+
from rsocket.extensions.helpers import composite, route, metadata_item
14+
from rsocket.extensions.mimetypes import WellKnownMimeTypes
15+
from rsocket.frame_helpers import ensure_bytes
16+
from rsocket.helpers import single_transport_provider, utf8_decode
17+
from rsocket.payload import Payload
18+
from rsocket.reactivex.reactivex_client import ReactiveXClient
19+
from rsocket.rsocket_client import RSocketClient
20+
from rsocket.transports.tcp import TransportTCP
21+
22+
23+
def encode_dataclass(obj):
24+
return ensure_bytes(json.dumps(obj.__dict__))
25+
26+
27+
class ChatClient:
28+
def __init__(self, rsocket: RSocketClient):
29+
self._rsocket = rsocket
30+
self._listen_task: Optional[Task] = None
31+
self._statistics_task: Optional[Task] = None
32+
self._session_id: Optional[str] = None
33+
34+
async def login(self, username: str):
35+
payload = Payload(ensure_bytes(username), composite(route('login')))
36+
self._session_id = (await self._rsocket.request_response(payload)).data
37+
return self
38+
39+
async def join(self, channel_name: str):
40+
request = Payload(ensure_bytes(channel_name), composite(route('channel.join')))
41+
await self._rsocket.request_response(request)
42+
return self
43+
44+
async def leave(self, channel_name: str):
45+
request = Payload(ensure_bytes(channel_name), composite(route('channel.leave')))
46+
await self._rsocket.request_response(request)
47+
return self
48+
49+
def listen_for_messages(self):
50+
def print_message(data: bytes):
51+
message = Message(**json.loads(data))
52+
print(f'{message.user} ({message.channel}): {message.content}')
53+
54+
async def listen_for_messages():
55+
await ReactiveXClient(self._rsocket).request_stream(Payload(metadata=composite(
56+
route('messages.incoming')
57+
))).pipe(
58+
operators.do_action(on_next=lambda value: print_message(value.data),
59+
on_error=lambda exception: print(exception)))
60+
61+
self._listen_task = asyncio.create_task(listen_for_messages())
62+
63+
async def wait_for_messages(self):
64+
messages_done = asyncio.Event()
65+
self._listen_task.add_done_callback(lambda _: messages_done.set())
66+
await messages_done.wait()
67+
68+
def stop_listening_for_messages(self):
69+
self._listen_task.cancel()
70+
71+
async def send_statistics(self):
72+
memory_usage = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
73+
payload = Payload(encode_dataclass(ClientStatistics(memory_usage=memory_usage)),
74+
metadata=composite(route('statistics')))
75+
await self._rsocket.fire_and_forget(payload)
76+
77+
def listen_for_statistics(self):
78+
class StatisticsHandler(DefaultPublisher, DefaultSubscriber):
79+
80+
def __init__(self):
81+
super().__init__()
82+
self.done = Event()
83+
84+
def on_next(self, value: Payload, is_complete=False):
85+
statistics = ServerStatistics(**json.loads(utf8_decode(value.data)))
86+
print(statistics)
87+
88+
if is_complete:
89+
self.done.set()
90+
91+
async def listen_for_statistics(client: RSocketClient, subscriber):
92+
client.request_channel(Payload(metadata=composite(
93+
route('statistics')
94+
))).subscribe(subscriber)
95+
96+
await subscriber.done.wait()
97+
98+
statistics_handler = StatisticsHandler()
99+
self._statistics_task = asyncio.create_task(
100+
listen_for_statistics(self._rsocket, statistics_handler))
101+
102+
async def private_message(self, username: str, content: str):
103+
print(f'Sending {content} to user {username}')
104+
await self._rsocket.request_response(Payload(encode_dataclass(Message(username, content)),
105+
composite(route('message'))))
106+
107+
async def channel_message(self, channel: str, content: str):
108+
print(f'Sending {content} to channel {channel}')
109+
await self._rsocket.request_response(Payload(encode_dataclass(Message(channel=channel, content=content)),
110+
composite(route('message'))))
111+
112+
async def upload(self, file_name, content):
113+
await self._rsocket.request_response(Payload(content, composite(
114+
route('file.upload'),
115+
metadata_item(ensure_bytes(file_name), chat_filename_mimetype)
116+
)))
117+
118+
async def download(self, file_name):
119+
return await self._rsocket.request_response(Payload(
120+
metadata=composite(route('file.download'), metadata_item(ensure_bytes(file_name), chat_filename_mimetype))))
121+
122+
async def list_files(self) -> List[str]:
123+
request = Payload(metadata=composite(route('files')))
124+
return await ReactiveXClient(self._rsocket).request_stream(
125+
request
126+
).pipe(operators.map(lambda x: utf8_decode(x.data)),
127+
operators.to_list())
128+
129+
async def list_channels(self) -> List[str]:
130+
request = Payload(metadata=composite(route('channels')))
131+
return await ReactiveXClient(self._rsocket).request_stream(
132+
request
133+
).pipe(operators.map(lambda _: utf8_decode(_.data)),
134+
operators.to_list())
135+
136+
137+
async def main():
138+
connection1 = await asyncio.open_connection('localhost', 6565)
139+
140+
async with RSocketClient(single_transport_provider(TransportTCP(*connection1)),
141+
metadata_encoding=WellKnownMimeTypes.MESSAGE_RSOCKET_COMPOSITE_METADATA,
142+
fragment_size_bytes=1_000_000) as client1:
143+
connection2 = await asyncio.open_connection('localhost', 6565)
144+
145+
async with RSocketClient(single_transport_provider(TransportTCP(*connection2)),
146+
metadata_encoding=WellKnownMimeTypes.MESSAGE_RSOCKET_COMPOSITE_METADATA,
147+
fragment_size_bytes=1_000_000) as client2:
148+
149+
user1 = ChatClient(client1)
150+
user2 = ChatClient(client2)
151+
152+
await user1.login('user1')
153+
await user2.login('user2')
154+
155+
user1.listen_for_messages()
156+
user2.listen_for_messages()
157+
158+
await user1.join('channel1')
159+
await user2.join('channel1')
160+
161+
await user1.send_statistics()
162+
user1.listen_for_statistics()
163+
164+
print(f'Files: {await user1.list_files()}')
165+
print(f'Channels: {await user1.list_channels()}')
166+
167+
await user1.private_message('user2', 'private message from user1')
168+
await user1.channel_message('channel1', 'channel message from user1')
169+
170+
file_contents = b'abcdefg1234567'
171+
file_name = 'file_name_1.txt'
172+
await user1.upload(file_name, file_contents)
173+
174+
download = await user2.download(file_name)
175+
176+
if download.data != file_contents:
177+
raise Exception('File download failed')
178+
else:
179+
print(f'Downloaded file: {len(download.data)} bytes')
180+
181+
try:
182+
await asyncio.wait_for(user2.wait_for_messages(), 3)
183+
except asyncio.TimeoutError:
184+
pass
185+
186+
user1.stop_listening_for_messages()
187+
user2.stop_listening_for_messages()
188+
189+
190+
if __name__ == '__main__':
191+
logging.basicConfig(level=logging.INFO)
192+
asyncio.run(main())

0 commit comments

Comments
 (0)