FastAPI
This commit is contained in:
5
README.md
Normal file
5
README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
https://fastapi.tiangolo.com/advanced/websockets/#await-for-messages-and-send-messages
|
||||||
|
|
||||||
|
```python
|
||||||
|
fastapi dev server.py
|
||||||
|
```
|
||||||
66
app.py
66
app.py
@@ -1,66 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import itertools
|
|
||||||
import json
|
|
||||||
|
|
||||||
from websockets.asyncio.server import serve
|
|
||||||
|
|
||||||
from connect4 import PLAYER1, PLAYER2, Connect4
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def handler(websocket):
|
|
||||||
# Initialize a Connect Four game.
|
|
||||||
game = Connect4()
|
|
||||||
|
|
||||||
# Players take alternate turns, using the same browser.
|
|
||||||
turns = itertools.cycle([PLAYER1, PLAYER2])
|
|
||||||
player = next(turns)
|
|
||||||
|
|
||||||
async for message in websocket:
|
|
||||||
# Parse a "play" event from the UI.
|
|
||||||
event = json.loads(message)
|
|
||||||
assert event["type"] == "play"
|
|
||||||
column = event["column"]
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Play the move.
|
|
||||||
row = game.play(player, column)
|
|
||||||
except ValueError as exc:
|
|
||||||
# Send an "error" event if the move was illegal.
|
|
||||||
event = {
|
|
||||||
"type": "error",
|
|
||||||
"message": str(exc),
|
|
||||||
}
|
|
||||||
await websocket.send(json.dumps(event))
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Send a "play" event to update the UI.
|
|
||||||
event = {
|
|
||||||
"type": "play",
|
|
||||||
"player": player,
|
|
||||||
"column": column,
|
|
||||||
"row": row,
|
|
||||||
}
|
|
||||||
await websocket.send(json.dumps(event))
|
|
||||||
|
|
||||||
# If move is winning, send a "win" event.
|
|
||||||
if game.winner is not None:
|
|
||||||
event = {
|
|
||||||
"type": "win",
|
|
||||||
"player": game.winner,
|
|
||||||
}
|
|
||||||
await websocket.send(json.dumps(event))
|
|
||||||
|
|
||||||
# Alternate turns.
|
|
||||||
player = next(turns)
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
async with serve(handler, "", 8001) as server:
|
|
||||||
await server.serve_forever()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(main())
|
|
||||||
105
connect4.css
105
connect4.css
@@ -1,105 +0,0 @@
|
|||||||
/* General layout */
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: white;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column-reverse;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
margin: 0;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Action buttons */
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-evenly;
|
|
||||||
align-items: flex-end;
|
|
||||||
width: 720px;
|
|
||||||
height: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action {
|
|
||||||
color: darkgray;
|
|
||||||
font-family: "Helvetica Neue", sans-serif;
|
|
||||||
font-size: 20px;
|
|
||||||
line-height: 20px;
|
|
||||||
font-weight: 300;
|
|
||||||
text-align: center;
|
|
||||||
text-decoration: none;
|
|
||||||
text-transform: uppercase;
|
|
||||||
padding: 20px;
|
|
||||||
width: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action:hover {
|
|
||||||
background-color: darkgray;
|
|
||||||
color: white;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action[href=""] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Connect Four board */
|
|
||||||
|
|
||||||
.board {
|
|
||||||
background-color: blue;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
padding: 0 10px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.board::before,
|
|
||||||
.board::after {
|
|
||||||
background-color: blue;
|
|
||||||
content: "";
|
|
||||||
height: 720px;
|
|
||||||
width: 20px;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
.board::before {
|
|
||||||
left: -20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.board::after {
|
|
||||||
right: -20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.column {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column-reverse;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cell {
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 80px;
|
|
||||||
height: 80px;
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty {
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.column:hover .empty {
|
|
||||||
background-color: lightgray;
|
|
||||||
}
|
|
||||||
|
|
||||||
.column:hover .empty ~ .empty {
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.red {
|
|
||||||
background-color: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yellow {
|
|
||||||
background-color: yellow;
|
|
||||||
}
|
|
||||||
45
connect4.js
45
connect4.js
@@ -1,45 +0,0 @@
|
|||||||
const PLAYER1 = "red";
|
|
||||||
|
|
||||||
const PLAYER2 = "yellow";
|
|
||||||
|
|
||||||
function createBoard(board) {
|
|
||||||
// Inject stylesheet.
|
|
||||||
const linkElement = document.createElement("link");
|
|
||||||
linkElement.href = import.meta.url.replace(".js", ".css");
|
|
||||||
linkElement.rel = "stylesheet";
|
|
||||||
document.head.append(linkElement);
|
|
||||||
// Generate board.
|
|
||||||
for (let column = 0; column < 7; column++) {
|
|
||||||
const columnElement = document.createElement("div");
|
|
||||||
columnElement.className = "column";
|
|
||||||
columnElement.dataset.column = column;
|
|
||||||
for (let row = 0; row < 6; row++) {
|
|
||||||
const cellElement = document.createElement("div");
|
|
||||||
cellElement.className = "cell empty";
|
|
||||||
cellElement.dataset.column = column;
|
|
||||||
columnElement.append(cellElement);
|
|
||||||
}
|
|
||||||
board.append(columnElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function playMove(board, player, column, row) {
|
|
||||||
// Check values of arguments.
|
|
||||||
if (player !== PLAYER1 && player !== PLAYER2) {
|
|
||||||
throw new Error(`player must be ${PLAYER1} or ${PLAYER2}.`);
|
|
||||||
}
|
|
||||||
const columnElement = board.querySelectorAll(".column")[column];
|
|
||||||
if (columnElement === undefined) {
|
|
||||||
throw new RangeError("column must be between 0 and 6.");
|
|
||||||
}
|
|
||||||
const cellElement = columnElement.querySelectorAll(".cell")[row];
|
|
||||||
if (cellElement === undefined) {
|
|
||||||
throw new RangeError("row must be between 0 and 5.");
|
|
||||||
}
|
|
||||||
// Place checker in cell.
|
|
||||||
if (!cellElement.classList.replace("empty", player)) {
|
|
||||||
throw new Error("cell must be empty.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { PLAYER1, PLAYER2, createBoard, playMove };
|
|
||||||
62
connect4.py
62
connect4.py
@@ -1,62 +0,0 @@
|
|||||||
__all__ = ["PLAYER1", "PLAYER2", "Connect4"]
|
|
||||||
|
|
||||||
PLAYER1, PLAYER2 = "red", "yellow"
|
|
||||||
|
|
||||||
|
|
||||||
class Connect4:
|
|
||||||
"""
|
|
||||||
A Connect Four game.
|
|
||||||
|
|
||||||
Play moves with :meth:`play`.
|
|
||||||
|
|
||||||
Get past moves with :attr:`moves`.
|
|
||||||
|
|
||||||
Check for a victory with :attr:`winner`.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.moves = []
|
|
||||||
self.top = [0 for _ in range(7)]
|
|
||||||
self.winner = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def last_player(self):
|
|
||||||
"""
|
|
||||||
Player who played the last move.
|
|
||||||
|
|
||||||
"""
|
|
||||||
return PLAYER1 if len(self.moves) % 2 else PLAYER2
|
|
||||||
|
|
||||||
@property
|
|
||||||
def last_player_won(self):
|
|
||||||
"""
|
|
||||||
Whether the last move is winning.
|
|
||||||
|
|
||||||
"""
|
|
||||||
b = sum(1 << (8 * column + row) for _, column, row in self.moves[::-2])
|
|
||||||
return any(b & b >> v & b >> 2 * v & b >> 3 * v for v in [1, 7, 8, 9])
|
|
||||||
|
|
||||||
def play(self, player, column):
|
|
||||||
"""
|
|
||||||
Play a move in a column.
|
|
||||||
|
|
||||||
Returns the row where the checker lands.
|
|
||||||
|
|
||||||
Raises :exc:`ValueError` if the move is illegal.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if player == self.last_player:
|
|
||||||
raise ValueError("It isn't your turn.")
|
|
||||||
|
|
||||||
row = self.top[column]
|
|
||||||
if row == 6:
|
|
||||||
raise ValueError("This slot is full.")
|
|
||||||
|
|
||||||
self.moves.append((player, column, row))
|
|
||||||
self.top[column] += 1
|
|
||||||
|
|
||||||
if self.winner is None and self.last_player_won:
|
|
||||||
self.winner = self.last_player
|
|
||||||
|
|
||||||
return row
|
|
||||||
41
html/index.html
Normal file
41
html/index.html
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Chat</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>WebSocket Chat</h1>
|
||||||
|
<form action="" onsubmit="sendMessage(event)">
|
||||||
|
<label>Item ID: <input type="text" id="itemId" autocomplete="off" value="foo"/></label>
|
||||||
|
<label>Token: <input type="text" id="token" autocomplete="off" value="some-key-token"/></label>
|
||||||
|
<button onclick="connect(event)">Connect</button>
|
||||||
|
<hr>
|
||||||
|
<label>Message: <input type="text" id="messageText" autocomplete="off"/></label>
|
||||||
|
<button>Send</button>
|
||||||
|
</form>
|
||||||
|
<ul id='messages'>
|
||||||
|
</ul>
|
||||||
|
<script>
|
||||||
|
var ws = null;
|
||||||
|
function connect(event) {
|
||||||
|
var itemId = document.getElementById("itemId")
|
||||||
|
var token = document.getElementById("token")
|
||||||
|
ws = new WebSocket("ws://localhost:8000/items/" + itemId.value + "/ws?token=" + token.value);
|
||||||
|
ws.onmessage = function(event) {
|
||||||
|
var messages = document.getElementById('messages')
|
||||||
|
var message = document.createElement('li')
|
||||||
|
var content = document.createTextNode(event.data)
|
||||||
|
message.appendChild(content)
|
||||||
|
messages.appendChild(message)
|
||||||
|
};
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
function sendMessage(event) {
|
||||||
|
var input = document.getElementById("messageText")
|
||||||
|
ws.send(input.value)
|
||||||
|
input.value = ''
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
10
index.html
10
index.html
@@ -1,10 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<title>Connect Four</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="board"></div>
|
|
||||||
<script src="main.js" type="module"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
53
main.js
53
main.js
@@ -1,53 +0,0 @@
|
|||||||
import { createBoard, playMove } from "./connect4.js";
|
|
||||||
|
|
||||||
function showMessage(message) {
|
|
||||||
window.setTimeout(() => window.alert(message), 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
function receiveMoves(board, websocket) {
|
|
||||||
websocket.addEventListener("message", ({ data }) => {
|
|
||||||
const event = JSON.parse(data);
|
|
||||||
switch (event.type) {
|
|
||||||
case "play":
|
|
||||||
// Update the UI with the move.
|
|
||||||
playMove(board, event.player, event.column, event.row);
|
|
||||||
break;
|
|
||||||
case "win":
|
|
||||||
showMessage(`Player ${event.player} wins!`);
|
|
||||||
// No further messages are expected; close the WebSocket connection.
|
|
||||||
websocket.close(1000);
|
|
||||||
break;
|
|
||||||
case "error":
|
|
||||||
showMessage(event.message);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error(`Unsupported event type: ${event.type}.`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendMoves(board, websocket) {
|
|
||||||
// When clicking a column, send a "play" event for a move in that column.
|
|
||||||
board.addEventListener("click", ({ target }) => {
|
|
||||||
const column = target.dataset.column;
|
|
||||||
// Ignore clicks outside a column.
|
|
||||||
if (column === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const event = {
|
|
||||||
type: "play",
|
|
||||||
column: parseInt(column, 10),
|
|
||||||
};
|
|
||||||
websocket.send(JSON.stringify(event));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("DOMContentLoaded", () => {
|
|
||||||
// Initialize the UI.
|
|
||||||
const board = document.querySelector(".board");
|
|
||||||
createBoard(board);
|
|
||||||
// Open the WebSocket connection and register event handlers.
|
|
||||||
const websocket = new WebSocket("ws://localhost:8001/");
|
|
||||||
receiveMoves(board, websocket);
|
|
||||||
sendMoves(board, websocket);
|
|
||||||
});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
websockets==15.0.1
|
|
||||||
81
server.py
Normal file
81
server.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
html = """
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Chat</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>WebSocket Chat</h1>
|
||||||
|
<h2>Your ID: <span id="ws-id"></span></h2>
|
||||||
|
<form action="" onsubmit="sendMessage(event)">
|
||||||
|
<input type="text" id="messageText" autocomplete="off"/>
|
||||||
|
<button>Send</button>
|
||||||
|
</form>
|
||||||
|
<ul id='messages'>
|
||||||
|
</ul>
|
||||||
|
<script>
|
||||||
|
var client_id = Date.now()
|
||||||
|
document.querySelector("#ws-id").textContent = client_id;
|
||||||
|
var ws = new WebSocket(`ws://localhost:8000/ws/${client_id}`);
|
||||||
|
ws.onmessage = function(event) {
|
||||||
|
var messages = document.getElementById('messages')
|
||||||
|
var message = document.createElement('li')
|
||||||
|
var content = document.createTextNode(event.data)
|
||||||
|
message.appendChild(content)
|
||||||
|
messages.appendChild(message)
|
||||||
|
};
|
||||||
|
function sendMessage(event) {
|
||||||
|
var input = document.getElementById("messageText")
|
||||||
|
ws.send(input.value)
|
||||||
|
input.value = ''
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionManager:
|
||||||
|
def __init__(self):
|
||||||
|
self.active_connections: list[WebSocket] = []
|
||||||
|
|
||||||
|
async def connect(self, websocket: WebSocket):
|
||||||
|
await websocket.accept()
|
||||||
|
self.active_connections.append(websocket)
|
||||||
|
|
||||||
|
def disconnect(self, websocket: WebSocket):
|
||||||
|
self.active_connections.remove(websocket)
|
||||||
|
|
||||||
|
async def send_personal_message(self, message: str, websocket: WebSocket):
|
||||||
|
await websocket.send_text(message)
|
||||||
|
|
||||||
|
async def broadcast(self, message: str):
|
||||||
|
for connection in self.active_connections:
|
||||||
|
await connection.send_text(message)
|
||||||
|
|
||||||
|
|
||||||
|
manager = ConnectionManager()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def get():
|
||||||
|
return HTMLResponse(html)
|
||||||
|
|
||||||
|
|
||||||
|
@app.websocket("/ws/{client_id}")
|
||||||
|
async def websocket_endpoint(websocket: WebSocket, client_id: int):
|
||||||
|
await manager.connect(websocket)
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
data = await websocket.receive_text()
|
||||||
|
await manager.send_personal_message(f"You wrote: {data}", websocket)
|
||||||
|
await manager.broadcast(f"Client #{client_id} says: {data}")
|
||||||
|
except WebSocketDisconnect:
|
||||||
|
manager.disconnect(websocket)
|
||||||
|
await manager.broadcast(f"Client #{client_id} left the chat")
|
||||||
Reference in New Issue
Block a user