Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions hw2/hw/client_websockets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import asyncio
import websockets


DEFAULT_HOST = "localhost"
DEFAULT_PORT = 8000


async def chat_client(room_name: str, host: str = DEFAULT_HOST, port: int = DEFAULT_PORT):
uri = f"ws://{host}:{port}/chat/{room_name}"
print(f"Подключение к комнате '{room_name}' по адресу {uri}...")

try:
async with websockets.connect(uri) as websocket:

welcome = await websocket.recv()
print(f"{welcome}\n")
print("Чат запущен! Введите сообщение и нажмите Enter.")
print("Чтобы выйти — введите 'quit' или нажмите Ctrl+C.\n")

async def receive_messages():
try:
while True:
message = await websocket.recv()
print(f"\r{message}")
print("Вы: ", end="", flush=True)
except websockets.exceptions.ConnectionClosed:
print("\nСоединение закрыто сервером.")
return

async def send_messages():
loop = asyncio.get_event_loop()
while True:

msg = await loop.run_in_executor(None, input, "Вы: ")
if msg.strip().lower() == 'quit':
break
if msg.strip():
await websocket.send(msg.strip())

await asyncio.gather(receive_messages(), send_messages())

except KeyboardInterrupt:
print("\nВыход по запросу пользователя.")
except Exception as e:
print(f"\nОшибка подключения: {e}")
print("Убедитесь, что сервер запущен: uvicorn main:app")


def main():
import argparse

parser = argparse.ArgumentParser(description="WebSocket чат-клиент")
parser.add_argument("room", help="Имя комнаты для подключения")
parser.add_argument("--host", default=DEFAULT_HOST, help="Хост сервера (по умолчанию: localhost)")
parser.add_argument("--port", type=int, default=DEFAULT_PORT, help="Порт сервера (по умолчанию: 8000)")

args = parser.parse_args()

try:
asyncio.run(chat_client(args.room, args.host, args.port))
except KeyboardInterrupt:

print("\nДо встречи!")


if __name__ == "__main__":
main()
3 changes: 2 additions & 1 deletion hw2/hw/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# Основные зависимости для ASGI приложения
fastapi>=0.117.1
uvicorn>=0.24.0
websockets>=1.8.0

# Зависимости для тестирования
pytest>=7.4.0
pytest-asyncio>=0.21.0
httpx>=0.27.2
Faker>=37.8.0
Faker>=37.8.0
1 change: 1 addition & 0 deletions hw2/hw/shop_api/chat_manager/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from shop_api.chat_manager.chat_manager import chat_manager
73 changes: 73 additions & 0 deletions hw2/hw/shop_api/chat_manager/chat_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import uuid
from typing import List, Dict
from fastapi import APIRouter, WebSocket, WebSocketDisconnect



class ChatManager:
def __init__(self):

self.rooms: Dict[str, List[WebSocket]] = {}
self.user_data: Dict[WebSocket, tuple[str, str]] = {}

def generate_username(self) -> str:
return f"user-{uuid.uuid4().hex}"

async def connect(self, websocket: WebSocket, chat_name: str) -> str:

await websocket.accept()
username = self.generate_username()

if chat_name not in self.rooms:
self.rooms[chat_name] = []

self.rooms[chat_name].append(websocket)
self.user_data[websocket] = (username, chat_name)

await websocket.send_text(f"Вы подключены как: {username}")
return username


async def disconnect(self, websocket: WebSocket):

if websocket not in self.user_data:
return

username, chat_name = self.user_data[websocket]
del self.user_data[websocket]

if chat_name in self.rooms:
if websocket in self.rooms[chat_name]:
self.rooms[chat_name].remove(websocket)
# Удаляем пустую комнату
if not self.rooms[chat_name]:
del self.rooms[chat_name]


async def publish(self, websocket: WebSocket, message: str):

if websocket not in self.user_data:
return

username, chat_name = self.user_data[websocket]
full_message = f"{username} :: {message}"

if chat_name not in self.rooms:
return

disconnected = []
for client in self.rooms[chat_name]:
try:
await client.send_text(full_message)
except Exception:
disconnected.append(client)

for client in disconnected:
if client in self.rooms[chat_name]:
self.rooms[chat_name].remove(client)
if client in self.user_data:
del self.user_data[client]



chat_manager = ChatManager()
5 changes: 3 additions & 2 deletions hw2/hw/shop_api/main.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from fastapi import FastAPI
from shop_api.routers import cart_router, item_router
from shop_api.routers import cart_router, item_router, chat_router


app = FastAPI(title="Shop API")
app.include_router(cart_router)
app.include_router(item_router)
app.include_router(item_router)
app.include_router(chat_router)
3 changes: 2 additions & 1 deletion hw2/hw/shop_api/routers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from shop_api.routers.cart import router as cart_router
from shop_api.routers.item import router as item_router
from shop_api.routers.item import router as item_router
from shop_api.routers.chat import router as chat_router
22 changes: 22 additions & 0 deletions hw2/hw/shop_api/routers/chat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
from shop_api.chat_manager import chat_manager



router = APIRouter(
prefix="/chat",
tags=["Chat"]
)


@router.websocket("/{chat_name}")
async def websocket_endpoint(websocket: WebSocket, chat_name: str):
try:
await chat_manager.connect(websocket, chat_name)
while True:
data = await websocket.receive_text()
await chat_manager.publish(websocket, data)
except WebSocketDisconnect:
pass
finally:
await chat_manager.disconnect(websocket)
36 changes: 36 additions & 0 deletions hw2/hw/test_websockets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import pytest
from fastapi.testclient import TestClient
from shop_api.main import app


client = TestClient(app)


def test_websocket_connection():

with client.websocket_connect("/chat/testroom") as websocket:

data = websocket.receive_text()
assert data.startswith("Вы подключены как: user-")

websocket.send_text("Привет из теста!")

response = websocket.receive_text()
assert " :: Привет из теста!" in response
assert response.startswith("user-")


def test_message_format_with_extracted_username():
with client.websocket_connect("/chat/test") as ws:

welcome = ws.receive_text()
assert welcome.startswith("Вы подключены как: ")
username = welcome.replace("Вы подключены как: ", "").strip()

test_msg = "Привет, это тест!"
ws.send_text(test_msg)

response = ws.receive_text()

expected = f"{username} :: {test_msg}"
assert response == expected, f"Ожидалось '{expected}', получено '{response}'"