Skip to content

Commit 33af210

Browse files
stackjayjaygaha
authored andcommitted
day #48 fastapi #12 ws
1 parent 990d46d commit 33af210

10 files changed

Lines changed: 283 additions & 0 deletions

File tree

workspace/7_framework/fastapi/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ uvicorn main:app --reload
109109
Learn to build a production-ready authentication system with JWT, including project structure, configuration management, password hashing, and dependency injection.
110110
_Includes: `pydantic-settings`, `passlib`, `python-jose`, SQLAlchemy, and comprehensive testing._
111111

112+
- [Day 12: Real-Time Chat with WebSockets](day12/README.md)
113+
Learn how to build a real-time chat application with WebSockets, manage connections, and broadcast messages.
114+
_Includes: `WebSocket`, connection management, client-side integration, and `pytest` for WebSockets._
115+
112116
---
113117

114118
## Recommended Project Structure
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# FastAPI Day 12: Real-Time Chat with WebSockets
2+
3+
Welcome to **Day 12** of the FastAPI tutorial series! Today, we're diving into the world of real-time communication with WebSockets. You'll learn how to build a simple chat application where multiple clients can connect and exchange messages in real-time.
4+
5+
---
6+
7+
## What You'll Learn
8+
9+
- **WebSocket Endpoints**: Implement a WebSocket endpoint in FastAPI to handle persistent connections.
10+
- **Connection Management**: Create a `ConnectionManager` to keep track of active WebSocket connections.
11+
- **Broadcasting Messages**: Broadcast messages to all connected clients to enable real-time chat functionality.
12+
- **Client-Side Integration**: Build a simple HTML and JavaScript client to interact with your WebSocket server.
13+
- **Testing WebSockets**: Write tests for your WebSocket endpoint using `pytest` and FastAPI's `TestClient`.
14+
15+
---
16+
17+
## Key Concepts
18+
19+
For this tutorial, we've built a simple chat server with a corresponding client.
20+
21+
- `src/main.py`: Contains the main FastAPI application, the `ConnectionManager`, and the WebSocket endpoint.
22+
- `client/index.html`: A simple HTML file with JavaScript to connect to the WebSocket and handle messaging.
23+
- `tests/test_main.py`: Contains tests for the WebSocket endpoint.
24+
25+
### 1. The Connection Manager
26+
27+
To manage all active WebSocket connections, we use a `ConnectionManager` class. This class is responsible for adding new connections, removing disconnected ones, and broadcasting messages to all clients.
28+
29+
```python-beginner/workspace/7_framework/fastapi/day12/server/src/main.py#L4-L18
30+
class ConnectionManager:
31+
def __init__(self):
32+
self.active_connections: List[WebSocket] = []
33+
34+
async def connect(self, websocket: WebSocket):
35+
await websocket.accept()
36+
self.active_connections.append(websocket)
37+
38+
def disconnect(self, websocket: WebSocket):
39+
self.active_connections.remove(websocket)
40+
41+
async def broadcast(self, message: str):
42+
for connection in self.active_connections:
43+
await connection.send_text(message)
44+
```
45+
46+
### 2. The WebSocket Endpoint
47+
48+
The core of our application is the WebSocket endpoint, defined with the `@app.websocket()` decorator. This endpoint handles incoming connections, listens for messages, and broadcasts them to all other clients.
49+
50+
```python-beginner/workspace/7_framework/fastapi/day12/server/src/main.py#L26-L38
51+
@app.websocket("/ws/{client_id}")
52+
async def websocket_endpoint(websocket: WebSocket, client_id: int):
53+
"""
54+
This endpoint handles WebSocket connections for the chat room.
55+
"""
56+
await manager.connect(websocket)
57+
await manager.broadcast(f"Client #{client_id} has entered the chat")
58+
try:
59+
while True:
60+
data = await websocket.receive_text()
61+
await manager.broadcast(f"Client #{client_id}: {data}")
62+
except WebSocketDisconnect:
63+
manager.disconnect(websocket)
64+
await manager.broadcast(f"Client #{client_id} has left the chat")
65+
```
66+
67+
### 3. Client-Side Implementation
68+
69+
The `client/index.html` file provides a simple user interface for the chat. It uses JavaScript's `WebSocket` API to connect to the server, send messages, and display received messages.
70+
71+
```python-beginner/workspace/7_framework/fastapi/day12/client/index.html#L48-L69
72+
<script>
73+
var clientId = Date.now();
74+
document.querySelector("#ws-id").textContent = clientId;
75+
var ws = new WebSocket(`ws://localhost:8000/ws/${clientId}`);
76+
ws.onmessage = function (event) {
77+
var messages = document.getElementById("messages");
78+
var message = document.createElement("li");
79+
var content = document.createTextNode(event.data);
80+
message.appendChild(content);
81+
messages.appendChild(message);
82+
messages.scrollTop = messages.scrollHeight;
83+
};
84+
function sendMessage(event) {
85+
var input = document.getElementById("messageText");
86+
ws.send(input.value);
87+
input.value = "";
88+
event.preventDefault();
89+
}
90+
</script>
91+
```
92+
93+
### 4. Testing WebSockets
94+
95+
Testing WebSockets is straightforward with FastAPI's `TestClient`. We can use `client.websocket_connect()` to simulate a client connection and test the message flow.
96+
97+
```python-beginner/workspace/7_framework/fastapi/day12/server/tests/test_main.py#L14-L32
98+
def test_websocket_broadcast(client):
99+
"""
100+
Test the WebSocket broadcasting functionality.
101+
- It connects two clients to the /ws/{client_id} endpoint.
102+
- It checks the initial connection messages in the correct order.
103+
- Has one client send a message.
104+
- Asserts that both clients receive the broadcasted message.
105+
"""
106+
with client.websocket_connect("/ws/1") as websocket1:
107+
# Client 1 connects and receives its own connection message
108+
assert websocket1.receive_text() == "Client #1 has entered the chat"
109+
110+
with client.websocket_connect("/ws/2") as websocket2:
111+
# Client 2 connects, and both clients receive the announcement
112+
assert websocket1.receive_text() == "Client #2 has entered the chat"
113+
assert websocket2.receive_text() == "Client #2 has entered the chat"
114+
115+
# Test broadcasting a message from client 1
116+
websocket1.send_text("Hello from client 1")
117+
assert websocket1.receive_text() == "Client #1: Hello from client 1"
118+
assert websocket2.receive_text() == "Client #1: Hello from client 1"
119+
```
120+
121+
---
122+
123+
## Next Steps
124+
125+
- Navigate to the server directory and install the dependencies: `cd server && pip install -r requirements.txt`.
126+
- Run the application with `uvicorn src.main:app --reload`.
127+
- Open `client/index.html` in your web browser. You can open multiple tabs to simulate multiple clients.
128+
- Send messages and see them appear in all open tabs.
129+
- Run the automated tests with `python -m pytest`.
130+
131+
---
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<title>FastAPI WebSocket Chat</title>
5+
<style>
6+
body {
7+
font-family: Arial, sans-serif;
8+
margin: 2em;
9+
}
10+
#messages {
11+
list-style-type: none;
12+
margin: 0;
13+
padding: 0;
14+
border: 1px solid #ccc;
15+
height: 300px;
16+
overflow-y: scroll;
17+
margin-bottom: 1em;
18+
}
19+
#messages li {
20+
padding: 8px 12px;
21+
}
22+
#messages li:nth-child(odd) {
23+
background: #f9f9f9;
24+
}
25+
#form {
26+
display: flex;
27+
}
28+
#messageText {
29+
flex-grow: 1;
30+
padding: 8px;
31+
}
32+
button {
33+
padding: 8px 15px;
34+
}
35+
</style>
36+
</head>
37+
<body>
38+
<h1>FastAPI WebSocket Chat</h1>
39+
<p>Your ID: <span id="ws-id"></span></p>
40+
<ul id="messages"></ul>
41+
<form id="form" onsubmit="sendMessage(event)">
42+
<input type="text" id="messageText" autocomplete="off" />
43+
<button>Send</button>
44+
</form>
45+
<script>
46+
var clientId = Date.now();
47+
document.querySelector("#ws-id").textContent = clientId;
48+
var ws = new WebSocket(`ws://localhost:8000/ws/${clientId}`);
49+
ws.onmessage = function (event) {
50+
var messages = document.getElementById("messages");
51+
var message = document.createElement("li");
52+
var content = document.createTextNode(event.data);
53+
message.appendChild(content);
54+
messages.appendChild(message);
55+
messages.scrollTop = messages.scrollHeight;
56+
};
57+
function sendMessage(event) {
58+
var input = document.getElementById("messageText");
59+
ws.send(input.value);
60+
input.value = "";
61+
event.preventDefault();
62+
}
63+
</script>
64+
</body>
65+
</html>

workspace/7_framework/fastapi/day12/server/__init__.py

Whitespace-only changes.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[pytest]
2+
asyncio_default_fixture_loop_scope = function
3+
pythonpath = . src
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
fastapi
2+
uvicorn[standard]
3+
pytest
4+
httpx

workspace/7_framework/fastapi/day12/server/src/__init__.py

Whitespace-only changes.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from typing import List
2+
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
3+
4+
class ConnectionManager:
5+
def __init__(self):
6+
self.active_connections: List[WebSocket] = []
7+
8+
async def connect(self, websocket: WebSocket):
9+
await websocket.accept()
10+
self.active_connections.append(websocket)
11+
12+
def disconnect(self, websocket: WebSocket):
13+
self.active_connections.remove(websocket)
14+
15+
async def broadcast(self, message: str):
16+
for connection in self.active_connections:
17+
await connection.send_text(message)
18+
19+
manager = ConnectionManager()
20+
21+
# Create the FastAPI app instance
22+
app = FastAPI()
23+
24+
@app.get("/")
25+
def read_root():
26+
"""
27+
A simple root endpoint to confirm the API is running.
28+
"""
29+
return {"status": "API is running"}
30+
31+
@app.websocket("/ws/{client_id}")
32+
async def websocket_endpoint(websocket: WebSocket, client_id: int):
33+
"""
34+
This endpoint handles WebSocket connections for the chat room.
35+
"""
36+
await manager.connect(websocket)
37+
await manager.broadcast(f"Client #{client_id} has entered the chat")
38+
try:
39+
while True:
40+
data = await websocket.receive_text()
41+
await manager.broadcast(f"Client #{client_id}: {data}")
42+
except WebSocketDisconnect:
43+
manager.disconnect(websocket)
44+
await manager.broadcast(f"Client #{client_id} has left the chat")

workspace/7_framework/fastapi/day12/server/tests/__init__.py

Whitespace-only changes.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import pytest
2+
from fastapi.testclient import TestClient
3+
from src.main import app
4+
5+
@pytest.fixture
6+
def client():
7+
"""
8+
Pytest fixture to create a TestClient for our app.
9+
"""
10+
return TestClient(app)
11+
12+
def test_websocket_broadcast(client):
13+
"""
14+
Test the WebSocket broadcasting functionality.
15+
- It connects two clients to the /ws/{client_id} endpoint.
16+
- It checks the initial connection messages in the correct order.
17+
- Has one client send a message.
18+
- Asserts that both clients receive the broadcasted message.
19+
"""
20+
with client.websocket_connect("/ws/1") as websocket1:
21+
# Client 1 connects and receives its own connection message
22+
assert websocket1.receive_text() == "Client #1 has entered the chat"
23+
24+
with client.websocket_connect("/ws/2") as websocket2:
25+
# Client 2 connects, and both clients receive the announcement
26+
assert websocket1.receive_text() == "Client #2 has entered the chat"
27+
assert websocket2.receive_text() == "Client #2 has entered the chat"
28+
29+
# Test broadcasting a message from client 1
30+
websocket1.send_text("Hello from client 1")
31+
assert websocket1.receive_text() == "Client #1: Hello from client 1"
32+
assert websocket2.receive_text() == "Client #1: Hello from client 1"

0 commit comments

Comments
 (0)