diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 2bcf066..941a762 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -23,5 +23,4 @@ jobs: - run: | python -m pip install --upgrade pip pip install flake8 - pip install isort - make lint + flake8 --config flake8 diff --git a/Makefile b/Makefile index 4fdc65f..6e2dc0c 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,8 @@ migrations: lint: isort * + flake8 --config flake8 + start: docker-compose up diff --git a/README.md b/README.md index ff6e0c7..68ecc6b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ How to play: 1. run `make start` -2. go to http://localhost:5173/ +2. go to http://localhost:5173/ 3. click on `Create user / Login` 4. enter username and password and press `Create user` 5. log in with created user diff --git a/alembic/versions/68cb451e6f2c_add_unique_contsraint.py b/alembic/versions/68cb451e6f2c_add_unique_contsraint.py index 1690a62..ced5cc3 100644 --- a/alembic/versions/68cb451e6f2c_add_unique_contsraint.py +++ b/alembic/versions/68cb451e6f2c_add_unique_contsraint.py @@ -7,9 +7,9 @@ """ from typing import Sequence, Union -from alembic import op import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. revision: str = '68cb451e6f2c' diff --git a/app.py b/app.py index 836c2d1..4882d7d 100644 --- a/app.py +++ b/app.py @@ -1,20 +1,16 @@ import json +import os from fastapi import FastAPI, WebSocket, WebSocketDisconnect from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse -from sqlalchemy import MetaData, create_engine -from sqlalchemy.orm import (Session, sessionmaker) - -from src.db.orm_models import Base, UserORM -from src.game import (ConnectionManager, Game, Player, - PlayerOptions, Room, RoomManager, GameStatus) +from sqlalchemy import create_engine from src.db.db import DB +from src.errors import UserAlreadyExists, UserNotFoundError, WrongPassword +from src.game import (ConnectionManager, Game, GameStatus, Player, + PlayerOptions, Room, RoomManager) from src.models import CreateUserRequest -from src.errors import UserNotFoundError, UserAlreadyExists, WrongPassword -from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine -import os app = FastAPI() @@ -41,7 +37,7 @@ room_manager = RoomManager() engine = create_engine( - f'postgresql+psycopg://postgres:qwerty@{db_host}/postgres', + f'postgresql+psycopg://postgres:qwerty@{db_host}/postgres', echo=True, pool_size=5, max_overflow=10, @@ -57,8 +53,7 @@ async def websocket_endpoint(websocket: WebSocket, username: str): # Create player player = Player(ws=websocket, id=username) playing_room = None - - + try: while True: # If user already in a room @@ -100,7 +95,6 @@ async def websocket_endpoint(websocket: WebSocket, username: str): playing_room.game.p2_option = playing_room.player2.choice result = playing_room.game.find_winner() - # перенести в room manager? await playing_room.announce_result(result) db.save_result( playing_room.player1.id, @@ -113,9 +107,6 @@ async def websocket_endpoint(websocket: WebSocket, username: str): await playing_room.send_message_to_another_player(player, GameStatus.WAITING.name) # remove user room_manager.remove_player(player.id) - - - @app.get("/rating") @@ -124,6 +115,7 @@ async def rating(): return JSONResponse(json.dumps(users_rated)) + @app.post("/users/create") async def create_user(request: CreateUserRequest): try: @@ -140,6 +132,5 @@ async def login(request: CreateUserRequest): return JSONResponse('{}') except UserNotFoundError as e: return JSONResponse(json.dumps(e.message), status_code=400) - except WrongPassword as e: - return JSONResponse(json.dumps(e.message), status_code=401) - + except WrongPassword as e: + return JSONResponse(json.dumps(e.message), status_code=401) \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 2b0b495..2533896 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,6 @@ services: app: build: ./ command: bash -c "./wait-for-postgres.sh db && uvicorn app:app --proxy-headers --host 0.0.0.0 --port 8000" - # - alembic upgrade head ports: - 8000:8000 depends_on: diff --git a/flake8 b/flake8 new file mode 100644 index 0000000..d65a493 --- /dev/null +++ b/flake8 @@ -0,0 +1,3 @@ +[flake8] +ignore = D203,E501,W293,W292,W291 +exclude = .git,__pycache__,docs/source/conf.py,old,build,dist,./.venv,./alembic \ No newline at end of file diff --git a/game-client/src/components/AuthForm.vue b/game-client/src/components/AuthForm.vue index b942d95..668a22d 100644 --- a/game-client/src/components/AuthForm.vue +++ b/game-client/src/components/AuthForm.vue @@ -4,32 +4,9 @@ import { ref } from 'vue' import { useUsersStore } from '../../store/users' const usersStore = useUsersStore() - const message=ref('') -// const username=ref('') const password=ref('') -// function createUser(event) { -// if (username.value && password.value){ -// axios({ -// method: 'post', -// url: 'http://127.0.0.1:8000/users/create', -// data: { -// username: username.value.toLowerCase(), -// password: password.value, -// }, -// }) -// .then(function (response) { -// console.log(response.data) -// message.value = `User "${username.value}" created` -// }) -// .catch(function (error, response) { -// console.log(error); -// message.value = error.response.data; -// }) -// } -// } - function createUser(event) { if (usersStore.userName && password.value){ axios({ @@ -41,11 +18,9 @@ function createUser(event) { }, }) .then(function (response) { - // console.log(response.data) message.value = `User "${usersStore.userName}" created` }) .catch(function (error, response) { - // console.log(error); message.value = error.response.data; }) } @@ -62,38 +37,15 @@ function login(event) { }, }) .then(function (response) { - // console.log(response.data) message.value = `You logged in as "${usersStore.userName}"` usersStore.setIsLoggedIn(true) }) .catch(function (error, response) { - // console.log(error); message.value = error.response.data; }) } } -// function login(event) { -// if (username.value && password.value){ -// axios({ -// method: 'post', -// url: 'http://127.0.0.1:8000/auth/login', -// data: { -// username: username.value.toLowerCase(), -// password: password.value, -// }, -// }) -// .then(function (response) { -// console.log(response.data) -// message.value = `You logged in as "${username.value}"` -// }) -// .catch(function (error, response) { -// console.log(error); -// message.value = error.response.data; -// }) -// } -// } - const handleChange = (event) => { usersStore.setUserName(event.target.value); }; @@ -108,7 +60,6 @@ const handleChange = (event) => {
-
@@ -124,24 +75,6 @@ const handleChange = (event) => {
- diff --git a/game-client/src/router/index.js b/game-client/src/router/index.js index 8950204..05ebcf3 100644 --- a/game-client/src/router/index.js +++ b/game-client/src/router/index.js @@ -6,7 +6,6 @@ import AuthFormView from '../views/AuthFormView.vue' const router = createRouter({ history: createWebHistory(), - // history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/', diff --git a/game-client/vite.config.js b/game-client/vite.config.js index 61af1ed..3c1c8ee 100644 --- a/game-client/vite.config.js +++ b/game-client/vite.config.js @@ -3,7 +3,6 @@ import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' -// https://vitejs.dev/config/ export default defineConfig({ plugins: [ vue(), diff --git a/requirements.txt b/requirements.txt index a4a4684..4d99b0f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,6 +29,7 @@ fastapi-users-db-sqlalchemy==6.0.1 fastapi_auth_middleware==1.0.2 fastjsonschema==2.19.1 filelock==3.13.4 +flake8==7.0.0 Flask==3.0.3 Flask-Caching==2.1.0 Flask-Cors==4.0.0 @@ -61,9 +62,11 @@ psycopg2-binary==2.9.9 ptyprocess==0.7.0 pwdlib==0.2.0 pyasn1==0.6.0 +pycodestyle==2.11.1 pycparser==2.22 pydantic==2.7.0 pydantic_core==2.18.1 +pyflakes==3.2.0 PyJWT==2.8.0 pylint==3.1.0 pyproject_hooks==1.0.0 @@ -93,4 +96,4 @@ websocket-client==1.7.0 websockets==12.0 Werkzeug==3.0.2 wheel==0.43.0 -xattr==1.1.0 +xattr==1.1.0 \ No newline at end of file diff --git a/src/db/db.py b/src/db/db.py index a229135..195310a 100644 --- a/src/db/db.py +++ b/src/db/db.py @@ -1,12 +1,13 @@ -from sqlalchemy import MetaData, create_engine -from sqlalchemy.orm import (Session, sessionmaker) -from src.db.orm_models import Base, UserORM -from src.models import User -import json import uuid as pyuuid -from src.errors import UserNotFoundError, UserAlreadyExists, WrongPassword +from sqlalchemy import MetaData +from sqlalchemy.orm import sessionmaker + +from src.db.orm_models import Base, UserORM +from src.errors import UserAlreadyExists, UserNotFoundError, WrongPassword from src.game import Result +from src.models import User + class DB: def __init__(self, engine): @@ -15,11 +16,9 @@ def __init__(self, engine): Session = sessionmaker(engine) self.session = Session() - def get_rating(self) -> list[User]: users = self.session.query(UserORM).order_by(UserORM.wins.desc()).all() usersDTO: list[User] = [User.model_validate(user).model_dump_json() for user in users] - return usersDTO def create_user(self, username: str, password: str): @@ -46,21 +45,19 @@ def login(self, username: str, password: str): raise UserNotFoundError(f"User `{username}` not found") elif user.password != password: raise WrongPassword(f"Wrong password for user `{username}`") - def save_result(self, username1: str, username2: str, result: Result): # updated_at player1 = self.find_by_username(username1) player2 = self.find_by_username(username2) - player1.total +=1 - player2.total +=1 + player1.total += 1 + player2.total += 1 if result is Result.PLAYER1_WON: - player1.wins +=1 + player1.wins += 1 elif result is Result.PLAYER2_WON: - player2.wins +=1 + player2.wins += 1 self.session.add_all([player1, player2]) self.session.commit() - diff --git a/src/db/orm_models.py b/src/db/orm_models.py index 512b06e..2dc7ebe 100644 --- a/src/db/orm_models.py +++ b/src/db/orm_models.py @@ -1,12 +1,12 @@ import uuid as pyuuid -from sqlalchemy.orm import (DeclarativeBase, Mapped, joinedload, mapped_column, - relationship, scoped_session, sessionmaker) +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column class Base(DeclarativeBase): pass + class UserORM(Base): __tablename__ = 'users' @@ -15,13 +15,3 @@ class UserORM(Base): password: Mapped[str] total: Mapped[int] wins: Mapped[int] - -# users_table = Table( -# 'users', -# metadata, -# Column("uuid", UUID, primary_key=True), -# Column("username", String(50)), -# Column("password", String(50)), -# Column("total", Integer), -# Column("wins", Integer), -# ) diff --git a/src/errors.py b/src/errors.py index c3a927e..24acc26 100644 --- a/src/errors.py +++ b/src/errors.py @@ -3,11 +3,13 @@ def __init__(self, message): self.message = message super().__init__(self.message) + class UserNotFoundError(Exception): def __init__(self, message): self.message = message super().__init__(self.message) + class WrongPassword(Exception): def __init__(self, message): self.message = message diff --git a/src/game.py b/src/game.py index 49dec9c..d349142 100644 --- a/src/game.py +++ b/src/game.py @@ -3,7 +3,6 @@ from enum import Enum from fastapi import WebSocket -from fastapi.responses import JSONResponse from loguru import logger @@ -12,17 +11,20 @@ class PlayerOptions(Enum): SCISSORS = 2 PAPER = 3 + class GameStatus(Enum): WAITING = 0 CREATED = 1 IN_PROGRESS = 2 FINISHED = 3 + class Result(Enum): PLAYER1_WON = 1 PLAYER2_WON = 2 TIE = 3 + BEAT_RULES = { # choices: winner option frozenset({PlayerOptions.ROCK, PlayerOptions.PAPER}): PlayerOptions.PAPER, @@ -49,6 +51,7 @@ def find_winner(self) -> Result: return Result.PLAYER2_WON + class ConnectionManager: def __init__(self): self.active_connections: list[WebSocket] = [] @@ -70,6 +73,7 @@ async def broadcast(self, message: str): class Player: choice: PlayerOptions + def __init__(self, ws: WebSocket, id: str): self.websocket = ws self.id = id @@ -81,7 +85,7 @@ def __init__(self, ws: WebSocket, id: str): class Room: def __init__(self): - self.id: pyuuid.UUID = pyuuid.uuid4() + self.id: pyuuid.UUID = pyuuid.uuid4() self.player1: Player = None self.player2: Player = None self.game: Game = None @@ -89,17 +93,16 @@ def __init__(self): logger.debug(f"Room #{self.id} created.") def is_empty(self) -> bool: - return self.player1 == None and self.player2 == None + return self.player1 is None and self.player2 is None def is_waiting(self) -> bool: - return self.player1 == None or self.player2 == None and not self.is_empty() + return self.player1 is None or self.player2 is None and not self.is_empty() def add_player(self, player: Player): if self.is_empty(): self.player1 = player else: self.player2 = player - logger.debug(f"Player #{player.id} added to the room #{self.id}.") def is_player_here(self, player_id: str) -> bool: @@ -114,7 +117,6 @@ def remove_player(self, player_id: str): logger.debug(f"Player #{player_id} removed from the room #{self.id}.") return - async def announce_result(self, result: Result): if result is Result.PLAYER1_WON: @@ -126,7 +128,7 @@ async def announce_result(self, result: Result): else: await self.broadcast("It's a tie!") - self.set_default() + self.set_default() def set_default(self): self.player1.choice = None @@ -145,7 +147,6 @@ async def countdown(self, seconds: int): await self.broadcast("Time off!") async def send_message_to_another_player(self, sender: Player, message: str): - # переписать условия if sender is self.player1 and self.player1 is not None and self.player2 is not None: await self.player2.websocket.send_text(message) @@ -154,7 +155,7 @@ async def send_message_to_another_player(self, sender: Player, message: str): async def broadcast(self, message: str): await self.player1.websocket.send_text(message) - await self.player2.websocket.send_text(message) + await self.player2.websocket.send_text(message) class RoomManager: @@ -171,25 +172,15 @@ def get_waiting_room(self) -> Room: def add_room(self, room: Room) -> None: self.rooms.append(room) - def remove_player(self, player_id:int): - # TODO: перегруппировать пользователей, если есть по одному игроку в комнате - + def remove_player(self, player_id: int): for room in self.rooms: if room.is_player_here(player_id): room.remove_player(player_id) if room.is_empty(): self.remove_room(room) - return + return def remove_room(self, room: Room): - # обработать ошибку self.rooms.remove(room) logger.debug(f"Room #{room.id} removed.") - - - - - - - - + diff --git a/src/models.py b/src/models.py index eb73cad..79cd553 100644 --- a/src/models.py +++ b/src/models.py @@ -1,7 +1,7 @@ import uuid as pyuuid -from pydantic import BaseModel, ConfigDict, StringConstraints, TypeAdapter -from typing_extensions import Annotated, List +from pydantic import BaseModel, ConfigDict, StringConstraints +from typing_extensions import Annotated class User(BaseModel): @@ -13,7 +13,7 @@ class User(BaseModel): total: int wins: int + class CreateUserRequest(BaseModel): username: Annotated[str, StringConstraints(max_length=50)] password: Annotated[str, StringConstraints(max_length=50)] - \ No newline at end of file