Skip to content

Commit 4182427

Browse files
committed
Added documentation for websocket
1 parent 12d734e commit 4182427

File tree

4 files changed

+306
-12
lines changed

4 files changed

+306
-12
lines changed

README.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<p align="center">
2-
<a href="#" target="blank"><img src="docs/img/EllarLogoIconOnly.png" width="200" alt="Ellar Logo" /></a>
2+
<a href="#" target="blank"><img src="docs/img/EllarLogoB.png" width="200" alt="Ellar Logo" /></a>
33
</p>
44

55
<p align="center"> Ellar - Python ASGI web framework for building fast, efficient and scalable RESTAPIs and server-side application. </p>
@@ -16,8 +16,6 @@ Ellar is a lightweight ASGI framework for building efficient and scalable server
1616
It supports both OOP (Object-Oriented Programming) and FP (Functional Programming)
1717

1818
Ellar is based on [Starlette (ASGI toolkit)](https://www.starlette.io/), a lightweight ASGI framework/toolkit well-suited for developing asynchronous web services with Python.
19-
While Ellar provides a high level of abstraction on top of Starlette, it still incorporates some of its features, as well as those of FastAPI.
20-
If you are familiar with these frameworks, you will find it easy to understand and use Ellar.
2119

2220
## **Features Summary**
2321

docs/index.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,17 @@
1515
Ellar is a lightweight ASGI framework for building efficient and scalable server-side python applications.
1616
It supports both OOP (Object-Oriented Programming) and FP (Functional Programming)
1717

18-
Ellar is based on [Starlette (ASGI toolkit)](https://www.starlette.io/), a lightweight ASGI framework/toolkit well-suited for developing asynchronous web services in Python.
19-
And while Ellar provides a high level of abstraction on top of Starlette, it still incorporates some of its features, as well as those of FastAPI.
20-
If you are familiar with these frameworks, you will find it easy to understand and use Ellar.
18+
Ellar is also a higher level of abstraction of [Starlette (ASGI toolkit)](https://www.starlette.io/), a lightweight ASGI framework/toolkit well-suited for developing asynchronous web services in Python.
2119

2220
## **Inspiration**
23-
Ellar was deeply influenced by [NestJS](https://docs.nestjs.com/) for its ease of use and ability to handle complex project structures and applications.
24-
Additionally, it took some concepts from [FastAPI](https://fastapi.tiangolo.com/) in terms of request parameter handling and data serialization with Pydantic.
21+
Ellar was deeply influenced by [NestJS](https://docs.nestjs.com/) for its ease of use, project structures and patterns that aids in building small or complex project applications.
22+
Also, Ellar took some concepts from [FastAPI](https://fastapi.tiangolo.com/) in terms of request parameter handling and data serialization with Pydantic.
23+
24+
The objective of Ellar is to provide a high level of abstracted interface to the web, along with a well-structured project setup, give room for object-oriented approach to web application design,
25+
allow you chose your desired application architecture, and ultimately, deliver speedy handling to requests.
26+
27+
As developers, we never know how big a project can become or evolve over time but following some design patterns and architecture makes it easier to build a more testable and maintainable application.
2528

26-
With that said, the objective of Ellar is to offer a high level of abstraction in its framework APIs, along with a well-structured project setup, an object-oriented approach to web application design,
27-
the ability to adapt to any desired software architecture, and ultimately, speedy request handling.
2829

2930

3031
## **Features Summary**

docs/websockets/index.md

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
# Websocket
2+
3+
[WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) is a powerful communication protocol that allows for two-way communication between a client and a server over a single,
4+
long-lived connection, making it an ideal tool for building real-time applications.
5+
6+
## **Creating a WebSocket Route**
7+
In Ellar, you create websocket route using `ws_route` decorator.
8+
9+
```python
10+
# project_name/apps/car/controller.py
11+
12+
from ellar.core import ControllerBase
13+
from ellar.common import Controller, ws_route
14+
15+
16+
@Controller('/car')
17+
class CarController(ControllerBase):
18+
@ws_route('/live-support')
19+
async def live_support(self):
20+
pass
21+
```
22+
23+
Let's go deep with a more practical example. First we need to create a **html** with some javascript scripts that will connect to our websocket.
24+
25+
```html
26+
<!--project_name/apps/car/templates/ws-index.html --->
27+
28+
<!DOCTYPE html>
29+
<html>
30+
<head>
31+
<title>Ellar Chat Demo</title>
32+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css"
33+
rel="stylesheet" integrity="sha384-KK94CHFLLe+nY2dmCWGMq91rCGa5gtU4mk92HdvYe+M/SXH301p5ILy+dN9+nJOZ"
34+
crossorigin="anonymous">
35+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js"
36+
integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe"
37+
crossorigin="anonymous"></script>
38+
</head>
39+
<body>
40+
<div class="container" style="max-width: 40rem">
41+
<div class="mt-3 text-center">
42+
<h2>Support Live Support</h2>
43+
</div>
44+
<hr>
45+
<div class="row">
46+
<form action="" onsubmit="sendMessage(event)">
47+
<textarea type="text" class="form-control" id="messageText" rows="4"></textarea>
48+
<button class="btn btn-primary mt-2">Send Message</button>
49+
</form>
50+
</div>
51+
<hr>
52+
<ul id='messages' class="mx-auto list-unstyled">
53+
</ul>
54+
</div>
55+
<script>
56+
let ws = new WebSocket("ws://localhost:8000/car/live-support");
57+
ws.onmessage = function(event) {
58+
addMessage(event.data)
59+
};
60+
function sendMessage(event) {
61+
let input = document.getElementById("messageText")
62+
addMessage(input.value, true)
63+
ws.send(input.value)
64+
input.value = ''
65+
event.preventDefault()
66+
}
67+
68+
function addMessage(data, inBound = false) {
69+
let messages = document.getElementById('messages')
70+
let container_message = document.createElement('li')
71+
let message = document.createElement('p')
72+
if (inBound) {
73+
container_message.classList.add('d-flex', 'justify-content-end')
74+
message.classList.add(...['p-2','rounded-2','bg-danger-subtle', 'my-1'])
75+
}else {
76+
container_message.classList.add('d-flex')
77+
message.classList.add(...[ 'p-2', 'rounded-2', 'bg-primary-subtle', 'my-1'])
78+
}
79+
message.innerHTML = data
80+
container_message.appendChild(message)
81+
messages.appendChild(container_message)
82+
}
83+
</script>
84+
</body>
85+
</html>
86+
```
87+
88+
Next, we add some code to the `live-feed` websocket route we created initially to accept connection and send messages to the client once there is a successful handshake.
89+
90+
```python
91+
@Controller('/car')
92+
class CarController(ControllerBase):
93+
@get('/ws-index')
94+
@render(template_name='ws-index.html')
95+
async def ws_index(self):
96+
return {}
97+
98+
@ws_route('/live-support')
99+
async def live_support(self):
100+
ws = self.context.switch_to_websocket().get_client()
101+
await ws.accept()
102+
await ws.send_text('Welcome to our live support room!\nHow can we help you?')
103+
104+
while True:
105+
try:
106+
data = await ws.receive_text()
107+
await ws.send_text(f'We have received you complain:<br><br><strong>"{data}"</strong><br><br>We shall get back to you.')
108+
except Exception as ex:
109+
assert ws.close()
110+
break
111+
```
112+
In example, we added `/ws-index`, to fetch the html file that has some javascript websocket connection to `/live-support` websocket route.
113+
114+
So, when we visit the route below [http://127.0.0.1:8000/car/ws-index](http://127.0.0.1:8000/car/ws-index), you will have an interacting screen as shown below
115+
116+
![live_screen](../img/live_support_websocket.gif)
117+
118+
In above example, `ws.receive_text()` was used to receive messages sent from the websocket client.
119+
Also messages can be received in json(`ws.receive_json()`), text(`ws.receive_text()`) and in bytes(`ws.receive_bytes()`)
120+
121+
In the same other, messages can be sent back in the same forms; text(`ws.send_text()`), json(`ws.send_json()`) and bytes(`ws.send_bytes()`)
122+
123+
## **Guards on websockets**
124+
Guards works exactly as described earlier for a normal HTTP request. In the case of websocket, Guards are only active when a client is about to
125+
connect to the server. After a successful handshake between the server and the client, the guards actions are no longer involved in server to client communication and vice versa.
126+
127+
```python
128+
from ellar.di import injectable
129+
from ellar.common import Query, Guards
130+
from ellar.core import GuardCanActivate
131+
132+
...
133+
@injectable
134+
class MyGuard(GuardCanActivate):
135+
async def can_activate(self, context: IExecutionContext) -> bool:
136+
print('MyGuard was called.')
137+
return False
138+
139+
140+
@ws_route('/live-support')
141+
@Guards(MyGuard)
142+
async def live_support(self, name: str = Query('John')):
143+
ws = self.context.switch_to_websocket().get_client()
144+
await ws.accept()
145+
await ws.send_text(f'Welcome {name} to our live support room!\nHow can we help you?')
146+
147+
while True:
148+
try:
149+
data = await ws.receive_text()
150+
await ws.send_text(f'We have received you complain:<br><br><strong>"{data}"</strong><br><br>We shall get back to you.')
151+
except Exception as ex:
152+
assert ws.close()
153+
break
154+
```
155+
In the construction above, we applied `MyGuard` to `/live-suport` route function.
156+
And for a connection to `/live-suport` to be successful, `MyGuard` can_activate must return `True`.
157+
158+
## **Websocket handler Dependencies**
159+
Websocket handler supports all route [handler parameters](../parsing-inputs#tutorial) except `Body` and `Forms`.
160+
161+
Let's use a `Query` parameter on the `/live-feed` WebSocket route.
162+
```python
163+
from ellar.common import Query
164+
...
165+
@ws_route('/live-support')
166+
async def live_support(self, name: str = Query('John')):
167+
ws = self.context.switch_to_websocket().get_client()
168+
await ws.accept()
169+
await ws.send_text(f'Welcome {name} to our live support room!\nHow can we help you?')
170+
171+
while True:
172+
try:
173+
data = await ws.receive_text()
174+
await ws.send_text(f'We have received you complain:<br><br><strong>"{data}"</strong><br><br>We shall get back to you.')
175+
except Exception as ex:
176+
assert ws.close()
177+
break
178+
```
179+
Now, when you visit this endpoint [http://127.0.0.1:8000/car/ws-index](http://127.0.0.1:8000/car/ws-index?name=ellar) again, you will see a
180+
name query parameter attached to the welcome message.
181+
182+
183+
## **Advance websocket usage**
184+
The `ws_route` offers more than just defining a websocket route. It can also be used to define handlers for different sessions of a websocket route.
185+
By setting `use_extra_handler=True` in `ws_route` decorator, we activate an in-built handler that gives the ability to
186+
manage different sessions of websocket differently like `on_connect`, `on_message` and `on_disconnect`
187+
188+
- `on_connect(websocket, **kwargs)`: handler to handles client connection with the server.
189+
- `on_message(websocket, data)`: handler for messages received from the client
190+
- `on_disconnect(websocket, close_code)`: handler that handles client disconnecting from websocket server
191+
192+
This approach also enables message data type validation using `WsBody`.
193+
`WsBody` is similar to [`Body`](../parsing-inputs/body) but for websockets.
194+
195+
Let's rewrite the previous example, `/live-support` websocket route.
196+
```python
197+
# project_name/apps/car/controller.py
198+
199+
from ellar.core import ControllerBase
200+
from ellar.common import Controller, ws_route, get, render, WsBody, Guards
201+
from starlette.websockets import WebSocket
202+
203+
204+
@Controller('/car')
205+
class CarController(ControllerBase):
206+
@get('/ws-index')
207+
@render(template_name='ws-index.html')
208+
async def ws_index(self):
209+
return {}
210+
211+
@ws_route('/live-support', use_extra_handler=True, encoding='text')
212+
@Guards(MyGuard)
213+
async def live_support(self, data: str = WsBody()):
214+
ws = self.context.switch_to_websocket().get_client()
215+
await ws.send_text(f'We have received you complain:<br><br><strong>"{data}"</strong><br><br>We shall get back to you.')
216+
217+
@ws_route.connect(live_support)
218+
async def live_support_connect(self, websocket: WebSocket):
219+
await websocket.accept()
220+
await websocket.send_text('Welcome to our live support room!\nHow can we help you?')
221+
222+
@ws_route.disconnect(live_support)
223+
async def live_support_disconnect(self, websocket: WebSocket, code: int):
224+
await websocket.close(code)
225+
```
226+
In the construct above, we created `def live_support_connect` to handle connection to the `'/live-support'` websocket route and
227+
`def live_support_disconnect` to handle disconnection from it. `def live_support_connect` and `def live_support_disconnect` takes `websocket` instance as only parameter and must be an asynchronous function.
228+
229+
On the other hand,`def live_support` function is now a **message receiver handler** and so, there is need to define a parameter with `WsBody`, in this case `data:str = WsBody()`. And message sent from client will be passed to `data` parameter and validated as `str` data type.
230+
If validation fails, an error will be sent to the client and connection will be destroyed.
231+
232+
The **`encoding` = 'text'** states the **message** data structure expected from client to the server.
233+
There are 3 different **`encoding`** types.
234+
235+
- `text`: allows only simple text messages as in the case above, e.g. `@ws_route('/path', use_extra_handler=True, encoding='text')`
236+
- `json`: allows json messages e.g. `@ws_route('/path', use_extra_handler=True, encoding='json')`
237+
- `bytes`: allows byte messages e.g. `@ws_route('/path', use_extra_handler=True, encoding='bytes')`
238+
239+
**Simplifying the example above**
240+
241+
We can further simplify the example above by getting rid of the `live_support_connect` and `live_support_disconnect`
242+
and let the inbuilt handler manage that for us as shown below:
243+
244+
```python
245+
# project_name/apps/car/controller.py
246+
247+
from ellar.core import ControllerBase
248+
from ellar.common import Controller, ws_route, get, render, WsBody
249+
250+
251+
@Controller('/car')
252+
class CarController(ControllerBase):
253+
@get('/ws-index')
254+
@render(template_name='ws-index.html')
255+
async def ws_index(self):
256+
return {}
257+
258+
@ws_route('/live-support', use_extra_handler=True, encoding='text')
259+
async def live_support(self, data: str = WsBody()):
260+
ws = self.context.switch_to_websocket().get_client()
261+
await ws.send_text(f'We have received you complain:<br><br><strong>"{data}"</strong><br><br>We shall get back to you.')
262+
```
263+
264+
## **Testing a Websocket Route**
265+
You can use the same [TestClient](../basics/testing#testclient) to test WebSockets.
266+
267+
For this, you use the TestClient in a `with` statement, connecting to the WebSocket:
268+
269+
```python
270+
# project_name/car/tests/test_controllers.py
271+
from project_name.apps.car.controllers import CarController
272+
from ellar.testing import Test, TestClient
273+
274+
275+
class TestCarController:
276+
def setup(self):
277+
test_module = Test.create_test_module(
278+
controllers=[CarController,],
279+
config_module=dict(
280+
REDIRECT_SLASHES=True
281+
)
282+
)
283+
self.client: TestClient = test_module.get_test_client()
284+
285+
def test_live_support_works(self):
286+
with self.client.websocket_connect('/car/live-support') as websocket_client:
287+
data = websocket_client.receive_text()
288+
assert data == 'Welcome to our live support room!\nHow can we help you?'
289+
290+
websocket_client.send_text('Message from client')
291+
data = websocket_client.receive_text()
292+
assert data == 'We have received you complain:<br><br><strong>"Message from client"</strong><br><br>We shall get back to you.'
293+
```

mkdocs.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ theme:
4040
icon: material/lightbulb-outline
4141
name: Switch to light mode
4242
font:
43-
text: Noto Sans
43+
text: Source Sans Pro
4444
code: Fira Code
4545
language: en
4646
logo: img/Icon.svg
@@ -101,7 +101,9 @@ nav:
101101
- Command Grouping: commands/command-grouping.md
102102
- Versioning: basics/versioning.md
103103
- Testing: basics/testing.md
104-
- WebSockets: websockets.md
104+
- WebSockets:
105+
- index: websockets/index.md
106+
- socketio: websockets/socketio.md
105107
- Mount: mount.md
106108
- Background Tasks: background-task.md
107109
- Release Notes: release-notes.md

0 commit comments

Comments
 (0)