Skip to content

Commit a480fbb

Browse files
committed
documented the example and checked functionality
1 parent 43691e3 commit a480fbb

File tree

5 files changed

+141
-47
lines changed

5 files changed

+141
-47
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,5 @@ pip3 install uvicorn
3333
and run using
3434

3535
```
36-
uvicorn demo:app --port=8080
36+
uvicorn demo_server:app --port=8080
3737
```

examples/README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# ObjectLink Core Python Protocol Example
2+
3+
To get a running client and server we need to hook up the protocol with a websocket connection and pass messages into the protocol and send messages emitted from the protocol.
4+
5+
The app example shows the client side of the protocol with an ObjectLink client.
6+
7+
The server example show the server side of the example with a ObjectLink service.
8+
9+
# App
10+
11+
The app is a websocket client which sends periodically an increment call to the remote endpoint.
12+
It uses the ObjectLink client to send the increment call to the remote endpoint.
13+
A websocket adapter adapts the client logic to the websocket protocol.
14+
The adapter uses the ObjectLink protocol to handle all the message parsing and flow.
15+
16+
- ClientWebSocketAdapter - connects to server and handles the websocket messages using the ObjectLink protocol.
17+
- CounterSink - is the client implementation as ObjectSink
18+
19+
TODO: might be better to split the sink logic from the client logic. or to define a clean client interface which can be exposed to the outside world
20+
21+
# Server
22+
23+
The server is a websocket async server which handles the websocket connection and the ObjectLink protocol.
24+
The service handles all the messages from the client and sends back the messages to the client using the ObjectLink protocol.
25+
A websocket adapter adapts the service logic to the websocket protocol.
26+
A remote endpoint handles the websocket connection and the communication with the adapter
27+
28+
```
29+
WebSocketServer -> WebSocketEndpoint -> WebSocketAdapter(ObjectLinkProtocol) -> Service
30+
```
31+
32+
- `WebSocketServer` handles all incoming connection. It creates a WebSocketEndpoint for each connection.
33+
- `WebSocketEndpoint` handles the websocket connection and the communication with the adapter.
34+
- `WebSocketAdapter` handles the ObjectLink protocol and wraps the service implementation.
35+
- `Service` is the service implementation.

examples/app.py

Lines changed: 57 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,92 +5,114 @@
55
import asyncio
66
import websockets
77

8-
class Counter(IObjectSink):
8+
9+
class CounterSink(IObjectSink):
10+
# this is the sink for the counter
911
count = 0
1012
client = None
13+
1114
def __init__(self):
15+
# register sink with client node
1216
self.client = ClientNode.register_sink(self)
1317
print('client', self.client)
1418

1519
def increment(self):
20+
# remote call the increment method
1621
if self.client:
1722
self.client.invoke_remote('demo.Counter/increment', [], None)
1823

1924
def olink_object_name(self):
25+
# return the name of the sink
2026
return 'demo.Counter'
2127

2228
def olink_on_signal(self, name: str, args: list[Any]):
29+
# handle the incoming signal from the remote source
2330
path = Name.path_from_name(name)
2431
print('on signal: %s: %s' % (path, args))
2532

2633
def olink_on_property_changed(self, name: str, value: Any) -> None:
34+
# handle the property change from the remote source
2735
path = Name.path_from_name(name)
2836
print('on property changed: %s: %s' % (path, value))
2937

3038
def olink_on_init(self, name: str, props: object, node: ClientNode):
39+
# handle the initialization of the sink,
40+
# called when the sink is linked to remote source
3141
print('on init: %s: %s' % (name, props))
3242
self.client = node
33-
43+
3444
def olink_on_release(self):
45+
# handle the release of the sink,
46+
# called when the sink is unlinked from remote source
3547
print('on release')
36-
37-
38-
async def sender(ws, q):
39-
while True:
40-
data = await q.get()
41-
await ws.send_text(data)
42-
q.task_done()
4348

4449

45-
async def connect(address: str):
46-
node = ClientNode()
47-
queue = Queue()
48-
async with websockets.connect(address) as ws:
49-
def writer(msg: str):
50-
queue.put_nowait(msg)
51-
node.on_write(writer)
52-
while True:
53-
data = await ws.recv()
54-
node.handle_message(data)
55-
56-
class Client:
50+
class ClientWebsocketAdapter:
51+
# adapts the websocket communication to the client node
5752
queue = Queue()
5853
node = None
54+
5955
def __init__(self, node):
6056
self.node = node
57+
# register a write function
6158
self.node.on_write(self.writer)
6259

6360
def writer(self, data):
64-
print('writer, data')
61+
# don't send directly, first write to queue
62+
print('write to queue')
6563
self.queue.put_nowait(data)
6664

6765
async def _reader(self, ws):
66+
# handle incoming ws messages
6867
while True:
69-
data = await ws.recv()
70-
self.node.handle_message(data)
71-
68+
msg = await ws.recv()
69+
self.node.handle_message(msg)
70+
7271
async def _sender(self, ws):
72+
# send messages from queue
7373
while True:
7474
data = await self.queue.get()
7575
await ws.send(data)
7676
self.queue.task_done()
7777

7878
async def connect(self, address: str):
79-
async with websockets.connect(address) as ws:
80-
print('connected')
81-
sender_task = asyncio.create_task(self._sender(ws))
82-
reader_task = asyncio.create_task(self._reader(ws))
83-
await asyncio.gather(sender_task, reader_task)
84-
await self.queue.join()
85-
79+
# connect to websocket server
80+
async for ws in websockets.connect(address):
81+
# connect might fail so loop continuously, for a retry-connection
82+
# see https://websockets.readthedocs.io/en/stable/reference/client.html#opening-a-connection
83+
try:
84+
sender_task = asyncio.create_task(self._sender(ws))
85+
reader_task = asyncio.create_task(self._reader(ws))
86+
await asyncio.gather(sender_task, reader_task)
87+
await self.queue.join()
88+
except Exception as e:
89+
print('exception while connecting: ', e)
8690

8791

8892
address = 'ws://localhost:8282/ws'
93+
# create a client node for ObjectLink registry and protocol
8994
node = ClientNode()
95+
# link the node to the service name
9096
node.link_node('demo.Counter')
91-
client = Client(node)
9297

93-
counter = Counter()
98+
# create a ws client which handles the ws adapter
99+
client = ClientWebsocketAdapter(node)
100+
101+
counter = CounterSink()
94102
node.link_remote('demo.Counter')
95103
counter.increment()
96-
asyncio.get_event_loop().run_until_complete(client.connect(address))
104+
105+
106+
async def countForever():
107+
# every send increment the counter
108+
while True:
109+
await asyncio.sleep(1)
110+
counter.increment()
111+
112+
113+
# await both tasks to complete
114+
future = asyncio.gather(client.connect(address), countForever())
115+
# get the event loop
116+
loop = asyncio.get_event_loop()
117+
# run the event loop until future completes
118+
loop.run_until_complete(future)

examples/requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
websockets
2+
starlette
3+
uvicorn

examples/server.py

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,81 +10,115 @@
1010
from olink.remotenode import IObjectSource, RemoteNode
1111

1212

13-
class Counter:
13+
class CounterService:
1414
count = 0
1515
_node: RemoteNode
16+
1617
def increment(self):
1718
self.count += 1
1819
self._node.notify_property_change('demo.Counter/count', self.count)
1920

20-
class CounterAdapter(IObjectSource):
21+
22+
class CounterWebsocketAdapter(IObjectSource):
23+
# adapts the websocket communication to the remote source
2124
node: RemoteNode = None
25+
2226
def __init__(self, impl):
2327
self.impl = impl
28+
# register the source with the node registry
2429
RemoteNode.register_source(self)
2530

2631
def olink_object_name(self):
32+
# return service name
2733
return 'demo.Counter'
2834

2935
def olink_invoke(self, name: str, args: list[Any]) -> Any:
36+
# handle the remote call from client node
3037
path = Name.path_from_name(name)
38+
# get the function from the implementation
3139
func = getattr(self.impl, path)
32-
func()
40+
try:
41+
# call function with arguments from the implementation
42+
result = func(**args)
43+
except Exception as e:
44+
# need to have proper exception handling here
45+
print('error: %s' % e)
46+
result = None
47+
# results will be send back to calling client node
48+
return result
3349

3450
def olink_set_property(self, name: str, value: Any):
51+
# set property value on implementation
3552
path = Name.path_from_name(name)
3653
setattr(self, self.impl, value)
3754

3855
def olink_linked(self, name: str, node: "RemoteNode"):
56+
# called when the source is linked to a client node
3957
self.impl._node = node
4058

4159
def olink_collect_properties(self) -> object:
60+
# collect properties from implementation to send back to client node initially
4261
return {k: getattr(self.impl, k) for k in ['count']}
4362

44-
counter = Counter()
45-
adapter = CounterAdapter(counter)
4663

64+
# create the service implementation
65+
counter = CounterService()
4766

67+
# create the adapter for the implementation
68+
adapter = CounterWebsocketAdapter(counter)
4869

4970

5071
class RemoteEndpoint(WebSocketEndpoint):
72+
# endpoint to handle a client connection
5173
encoding = "text"
5274
node = RemoteNode()
75+
# message queue
5376
queue = Queue()
5477

5578
async def sender(self, ws):
79+
# sender coroutine, messages from queue are send to client
5680
print('start sender')
5781
while True:
58-
print('001')
5982
msg = await self.queue.get()
6083
print('send', msg)
6184
await ws.send_text(msg)
62-
self.queue.task_done()
85+
self.queue.task_done()
6386

6487
async def on_connect(self, ws: WebSocket):
88+
# handle a socket connection
6589
print('on_connect')
90+
# register a sender to the connection
6691
asyncio.create_task(self.sender(ws))
6792

93+
# a writer function to queue messages
6894
def writer(msg: str):
69-
print('writer', msg)
95+
print('write to queue:', msg)
7096
self.queue.put_nowait(msg)
97+
# register the writer function to the node
7198
self.node.on_write(writer)
99+
# call the super connection handler
72100
await super().on_connect(ws)
73101

74-
75102
async def on_receive(self, ws: WebSocket, data: Any) -> None:
103+
# handle a message from a client socket
76104
print('on_receive', data)
77105
self.node.handle_message(data)
78106

79107
async def on_disconnect(self, websocket: WebSocket, close_code: int) -> None:
108+
# handle a socket disconnect
80109
await super().on_disconnect(websocket, close_code)
110+
# remove the writer from the node
81111
self.node.on_write(None)
112+
# wait for all messages to be send, before closing the connection
82113
await self.queue.join()
83114

84115

85-
116+
# see https://www.starlette.io/routing/
86117
routes = [
87118
WebSocketRoute("/ws", RemoteEndpoint)
88119
]
89120

90-
app = Starlette(routes=routes)
121+
122+
# call with `uvicorn server:app --port 8282`
123+
# see https://www.starlette.io
124+
app = Starlette(routes=routes)

0 commit comments

Comments
 (0)