From 0041b7d43ed166c3d03b7a6b7216939049f7257f Mon Sep 17 00:00:00 2001 From: Eden Kirin Date: Sat, 25 Mar 2023 13:21:07 +0100 Subject: [PATCH] Project rename and restructure --- .gitignore | 6 + Makefile | 16 ++ README.md | 316 +++++++++++++++++++++++++++++++++ hopper/__init__.py | 0 hopper/api/__init__.py | 0 hopper/api/dependencies.py | 13 ++ hopper/api/dto.py | 73 ++++++++ hopper/api/views.py | 132 ++++++++++++++ hopper/api_tests/requests.http | 34 ++++ hopper/engine.py | 153 ++++++++++++++++ hopper/enums.py | 20 +++ hopper/errors.py | 10 ++ hopper/models/__init__.py | 0 hopper/models/board.py | 111 ++++++++++++ hopper/models/config.py | 29 +++ hopper/models/player.py | 34 ++++ hopper/watchdog.py | 47 +++++ main.py | 16 ++ poetry.lock | 293 ++++++++++++++++++++++++++++++ pyproject.toml | 18 ++ settings_template.py | 7 + 21 files changed, 1328 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 hopper/__init__.py create mode 100644 hopper/api/__init__.py create mode 100644 hopper/api/dependencies.py create mode 100644 hopper/api/dto.py create mode 100644 hopper/api/views.py create mode 100644 hopper/api_tests/requests.http create mode 100644 hopper/engine.py create mode 100644 hopper/enums.py create mode 100644 hopper/errors.py create mode 100644 hopper/models/__init__.py create mode 100644 hopper/models/board.py create mode 100644 hopper/models/config.py create mode 100644 hopper/models/player.py create mode 100644 hopper/watchdog.py create mode 100644 main.py create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 settings_template.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..468ae32 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.vscode +.idea +__pycache__ +/env +/.venv +/settings.py diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4e9d471 --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +run: + @poetry run \ + uvicorn \ + main:app \ + --host 0.0.0.0 \ + --port 8010 \ + --workers=1 + +run-dev: + @poetry run \ + uvicorn \ + main:app \ + --host 0.0.0.0 \ + --port 8010 \ + --workers=1 \ + --reload diff --git a/README.md b/README.md new file mode 100644 index 0000000..1e96ffa --- /dev/null +++ b/README.md @@ -0,0 +1,316 @@ +# FairHopper + +## Game + +### Overview + +- Rectangle board W × H +- Destination: center of a board (W / 2, H / 2) +- Initial player position: Random on board border +- Available moves: + - left + - right + - up + - down +- Optional on-board obstacles + +### Rules +- Goal: Reach the goal destination +- Player can't move out of board +- Player can't move if destination position contains obstacle +- Move timeout: 10s. Game is finished if timeout ocurrs. + + +## FairHopper Game Server + +Requirements: +- Python 3.10+ + +### Install virtual envirnonment + +Project uses [Poetry](https://python-poetry.org), ultimate dependency management software for Python. + +Install Poetry: +```sh +pip install poetry +``` + +Install virtual environment: +```sh +poetry install +``` + +### Setting up + +Copy `settings_template.py` to `settings.py`. + +Edit `settings.py` and customize application. + + +### Starting FairHopper Game Server + +```sh +make run +``` + +By default, JFK runs on port **8010**. To run on other port, start `uvicorn` directly: +```sh +poetry run uvicorn main:app --host 0.0.0.0 --port 8010 --workers=1 +``` + +To activate virtual environment: +```sh +poetry shell +``` + + +## System overview + +### Architecture + +```plantuml +actor "Player 1" as P1 +actor "Player 2" as P2 +actor "Player 3" as P3 + +package Masterpiece { + usecase JFK as "JFK Game Server" + usecase WS as "WS Server" + usecase Vis as "Visualisation\nService" +} + +P1 -left-> JFK: REST API +P2 -left-> JFK: REST API +P3 -left-> JFK: REST API +JFK --> WS: WebSockets +WS --> Vis: WebSockets +``` + +### WebSockets + +```plantuml + participant JFK as "JFK Game Server" + participant WS as "WS Server" + participant Client1 as "Visualisation\nClient 1" + participant Client2 as "Visualisation\nClient 2" + + JFK ->o WS: Server Connect + activate WS #coral + WS -> JFK: Get game state + activate JFK #yellow + JFK -> WS: Game state + deactivate + deactivate + + Client1 ->o WS: Client Connect + activate WS #coral + WS -> Client1: Game state + deactivate + + Client2 ->o WS: Client Connect + activate WS #coral + WS -> Client2: Game state + deactivate + + loop #lightyellow On game state change + JFK ->o WS: Game state + activate WS #coral + WS o-> Client1: Game state + WS o-> Client2: Game state + deactivate +end +``` + + +## REST API + +- Start game +- Move left +- Move right +- Move up +- Move down +- Get current position +- Get board info + +Check REST API interface on [FastAPI docs](http://localhost:8010/docs). + +### Start game + +**Endpoint**: POST `/game` + +Request body: +```json +{ + "player_name": "Pero" +} +``` + +Response body: +```json +{ + "board": { + "width": 101, + "height": 101 + }, + "destination": { + "position": { + "x": 50, + "y": 50 + }, + }, + "player": { + "uuid": "75bba7cd-a4c1-4b50-b0b5-6382c2822a25", + "position": { + "x": 0, + "y": 10 + }, + "move_count": 0, + "move_attempt_count": 0 + } +} +``` + +### Player Move + +POST `/player/{uuid}/move/left` +POST `/player/{uuid}/move/right` +POST `/player/{uuid}/move/up` +POST `/player/{uuid}/move/down` + +Request body: None + +Response code: +- 200 OK: Destination reached +- 201 Created: Player moved successfully +- 403 Forbidden: Player uuid not valid, probably timeout +- 409 Conflict: Invalid move, obstacle or position out of board +- 422 Unprocessable Content: Validation error + +Response body: +```json +{ + "player": { + "uuid": "string", + "position": { + "x": 50, + "y": 50 + }, + "move_count": 10, + "move_attempt_count": 12 + } +} +``` + +### Get Player Info + +GET `/player/{{uuid}}` + +Request body: None + +Response body: +```json +{ + "player": { + "uuid": "string", + "position": { + "x": 50, + "y": 50 + }, + "move_count": 10, + "move_attempt_count": 12 + } +} +``` + +### Get Game Info + +GET `/game` + +Response body: +```json +{ + "playerId": "75bba7cd-a4c1-4b50-b0b5-6382c2822a25", + "board": { + "width": 101, + "height": 101 + }, + "destinationPosition": { + "x": 50, + "y": 50 + }, + "playerPosition": { + "x": 0, + "y": 10 + } +} +``` + +## WebSockets + +### WS Data format +- json + +General data format: +```json +{ + "command": "command", + "data": {} +} +``` + +### Game info structure + +Command: `gameInfo` + +Data: +```json +{ + "board": { + "width": 101, + "height": 101 + }, + "destinationPosition": { + "x": 50, + "y": 50 + }, + "players": [ + { + "id": "75bba7cd-a4c1-4b50-b0b5-6382c2822a25", + "name": "Pero", + "position": { + "x": 0, + "y": 10 + } + }, + { + "id": "04793b36-0785-4bf3-9396-3585c358cbac", + "name": "Mirko", + "position": { + "x": 11, + "y": 12 + } + } + ], + "layers": [ + { + "name": "obstacles", + "objects": [ + { + "type": "obstacle", + "position": { + "x": 15, + "y": 25 + } + }, + { + "type": "obstacle", + "position": { + "x": 33, + "y": 44 + } + } + ] + } + ] +} +``` diff --git a/hopper/__init__.py b/hopper/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hopper/api/__init__.py b/hopper/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hopper/api/dependencies.py b/hopper/api/dependencies.py new file mode 100644 index 0000000..cac2a49 --- /dev/null +++ b/hopper/api/dependencies.py @@ -0,0 +1,13 @@ +from hopper.engine import GameEngine, GameEngineFactory + +game_engine: GameEngine + + +def create_game_engine() -> GameEngine: + global game_engine + game_engine = GameEngineFactory.create_default() + return game_engine + + +def get_game_engine() -> GameEngine: + return game_engine diff --git a/hopper/api/dto.py b/hopper/api/dto.py new file mode 100644 index 0000000..109c86f --- /dev/null +++ b/hopper/api/dto.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +from pydantic import BaseModel as PydanticBaseModel + +from hopper.models.board import GameBoard +from hopper.models.player import Player, Position + + +class BaseModel(PydanticBaseModel): + class Config: + orm_mode = True + + +class PingResponse(BaseModel): + message: str + + +class BoardDto(BaseModel): + width: int + height: int + + @staticmethod + def from_model(board: GameBoard) -> BoardDto: + return BoardDto.from_orm(board) + + +class PositionDto(BaseModel): + x: int + y: int + + @staticmethod + def from_model(position: Position) -> PositionDto: + return PositionDto.from_orm(position) + + +class PlayerDto(BaseModel): + uuid: str + position: PositionDto + move_count: int + move_attempt_count: int + + @staticmethod + def from_model(player: Player) -> PlayerDto: + return PlayerDto.from_orm(player) + + +class DestinationDto(BaseModel): + position: PositionDto + + +class StartGameRequestDto(BaseModel): + player_name: str + + +class GameInfoDto(BaseModel): + board: BoardDto + destination: DestinationDto + + +class StartGameResponseDto(GameInfoDto): + player: PlayerDto + + +class MovePlayerResponseDto(BaseModel): + player: PlayerDto + + +class PlayerInfoResponseDto(MovePlayerResponseDto): + ... + + +class ErrorResponseDto(BaseModel): + detail: str diff --git a/hopper/api/views.py b/hopper/api/views.py new file mode 100644 index 0000000..27e6086 --- /dev/null +++ b/hopper/api/views.py @@ -0,0 +1,132 @@ +from fastapi import APIRouter, Depends, HTTPException, Response +from starlette import status + +from hopper.api.dependencies import get_game_engine +from hopper.api.dto import ( + BoardDto, + DestinationDto, + ErrorResponseDto, + GameInfoDto, + MovePlayerResponseDto, + PingResponse, + PlayerDto, + PlayerInfoResponseDto, + PositionDto, + StartGameRequestDto, + StartGameResponseDto, +) +from hopper.engine import GameEngine +from hopper.enums import Direction, PlayerMoveResult +from hopper.errors import Collision, PositionOutOfBounds + +router = APIRouter() + + +@router.get("/ping", response_model=PingResponse) +async def ping() -> PingResponse: + return PingResponse( + message="Pong!", + ) + + +@router.get("/game", response_model=GameInfoDto) +async def get_game_info( + engine: GameEngine = Depends(get_game_engine), +) -> GameInfoDto: + return GameInfoDto( + board=BoardDto.from_model(engine.board), + destination=DestinationDto( + position=PositionDto.from_model(engine.board.destination.position) + ), + ) + + +@router.post("/game", response_model=StartGameResponseDto) +async def start_game( + body: StartGameRequestDto, + engine: GameEngine = Depends(get_game_engine), +) -> StartGameResponseDto: + new_player = engine.start_game(player_name=body.player_name) + + return StartGameResponseDto( + board=BoardDto.from_model(engine.board), + player=PlayerDto.from_model(new_player), + destination=DestinationDto( + position=PositionDto.from_model(engine.board.destination.position) + ), + ) + + +@router.get( + "/player/{uuid}", + response_model=PlayerInfoResponseDto, + status_code=status.HTTP_201_CREATED, +) +async def get_player_info( + uuid: str, + engine: GameEngine = Depends(get_game_engine), +) -> MovePlayerResponseDto: + player = engine.players.find(uuid) + if player is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Player not found" + ) + if not player.active: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Player kicked out due to inactivity", + ) + return PlayerInfoResponseDto(player=PlayerDto.from_model(player)) + + +@router.post( + "/player/{uuid}/move/{direction}", + response_model=MovePlayerResponseDto, + status_code=status.HTTP_201_CREATED, + responses={ + status.HTTP_200_OK: { + "model": MovePlayerResponseDto, + "description": "Destination reached!", + }, + status.HTTP_403_FORBIDDEN: { + "model": ErrorResponseDto, + "description": " Player uuid not valid, probably due to inactivity", + }, + status.HTTP_409_CONFLICT: { + "model": ErrorResponseDto, + "description": " Position out of bounds or collision with an object", + }, + }, +) +async def move_player( + uuid: str, + direction: Direction, + response: Response, + engine: GameEngine = Depends(get_game_engine), +) -> MovePlayerResponseDto: + player = engine.players.find(uuid) + if player is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Player not found" + ) + if not player.active: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Player kicked out due to inactivity", + ) + + try: + move_result = engine.move_player(player, direction) + except PositionOutOfBounds: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="Position out of bounds" + ) + except Collision: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="Collision with an object" + ) + + if move_result == PlayerMoveResult.DESTINATION_REACHED: + response.status_code = status.HTTP_200_OK + + return MovePlayerResponseDto(player=PlayerDto.from_model(player)) diff --git a/hopper/api_tests/requests.http b/hopper/api_tests/requests.http new file mode 100644 index 0000000..2657fb7 --- /dev/null +++ b/hopper/api_tests/requests.http @@ -0,0 +1,34 @@ +GET http://localhost:8010/ping +### + +# create new game +POST http://localhost:8010/game + +{ + "player_name": "Mirko" +} +### + +# get game info +GET http://localhost:8010/game +### + +# get player info +GET http://localhost:8010/player/test-player-id +### + +# move player left +POST http://localhost:8010/player/test-player-id/move/left +### + +# move player right +POST http://localhost:8010/player/test-player-id/move/right +### + +# move player up +POST http://localhost:8010/player/test-player-id/move/up +### + +# move player down +POST http://localhost:8010/player/test-player-id/move/down +### diff --git a/hopper/engine.py b/hopper/engine.py new file mode 100644 index 0000000..ea450c1 --- /dev/null +++ b/hopper/engine.py @@ -0,0 +1,153 @@ +import logging + +from hopper.enums import Direction, PlayerMoveResult +from hopper.errors import Collision, PositionOutOfBounds +from hopper.models.board import ( + BOARD_DUMP_CHARS, + Destination, + GameBoard, + Layer, + LayerObject, + ObjectType, + create_random_position, +) +from hopper.models.player import Player, PlayerList, Position +from hopper.watchdog import InactivityWatchdog +from settings import settings + + +class GameEngine: + def __init__(self, board: GameBoard) -> None: + self.board = board + self.players = PlayerList() + self._inacivity_watchdog = None + self.__debug_print_board() + + def dump_board(self) -> list[list[str]]: + dump = self.board.dump() + + for player in self.players: + dump[player.position.y][player.position.x] = BOARD_DUMP_CHARS[ + ObjectType.PLAYER + ] + + return dump + + def __debug_print_board(self): + if not (settings.debug and settings.debug.PRINT_BOARD): + return + for line in self.dump_board(): + print(" ".join(line)) + + def _start_inactivity_watchdog(self) -> None: + if not self._inacivity_watchdog: + self._inacivity_watchdog = InactivityWatchdog( + players=self.players, daemon=True + ) + self._inacivity_watchdog.start() + + def start_game(self, player_name: str) -> Player: + self._start_inactivity_watchdog() + player = Player( + name=player_name, + position=Position(0, 0), + ) + self.players.append(player) + + logging.info(f"Starting new game for player: {player}") + self.__debug_print_board() + + return player + + def move_player(self, player: Player, direction: Direction) -> PlayerMoveResult: + player.reset_timeout() + + new_position = Position(player.position.x, player.position.y) + logging.info(f"Player {player} move to {direction}") + + player.move_attempt_count += 1 + + if direction == Direction.LEFT: + new_position.x -= 1 + elif direction == Direction.RIGHT: + new_position.x += 1 + elif direction == Direction.UP: + new_position.y -= 1 + elif direction == Direction.DOWN: + new_position.y += 1 + else: + raise ValueError(f"Unhandled direction: {direction}") + + if not self.position_in_board_bounds(new_position): + raise PositionOutOfBounds() + + if self.colided_with_obstacle(new_position): + raise Collision() + + player.position = new_position + player.move_count += 1 + + if self.is_player_on_destination(player): + logging.info(f"Player {player} reached destination!") + return PlayerMoveResult.DESTINATION_REACHED + + self.__debug_print_board() + return PlayerMoveResult.OK + + def is_player_on_destination(self, player: Player) -> bool: + return player.position == self.board.destination.position + + def position_in_board_bounds(self, position: Position) -> bool: + return ( + 0 <= position.x < self.board.width and 0 <= position.y < self.board.height + ) + + def colided_with_obstacle(self, position: Position) -> bool: + return self.board.get_object_at_position(position) is not None + + +class GameEngineFactory: + @staticmethod + def create( + board_width: int, + board_height: int, + obstacle_count: int = 0, + ) -> GameEngine: + board = GameBoard( + width=board_width, + height=board_height, + destination=Destination(Position(board_height // 2, board_height // 2)), + ) + obstacle_layer = Layer(name="obstacles") + for _ in range(obstacle_count): + obstacle_layer.objects.append( + LayerObject( + type_=ObjectType.OBSTACLE, + position=create_random_position(board_width, board_height), + ), + ) + board.layers.append(obstacle_layer) + + game = GameEngine(board=board) + GameEngineFactory.__add_test_player(game.players) + return game + + @staticmethod + def create_default() -> GameEngine: + return GameEngineFactory.create( + board_width=settings.board.WIDTH, + board_height=settings.board.HEIGHT, + obstacle_count=settings.board.OBSTACLE_COUNT, + ) + + @staticmethod + def __add_test_player(players: PlayerList) -> None: + if not (settings.debug and settings.debug.CREATE_TEST_PLAYER): + return + player = Player( + name="Pero", + uuid="test-player-id", + position=Position(2, 2), + ) + players.append(player) + logging.info(f"Test player created: {player}") diff --git a/hopper/enums.py b/hopper/enums.py new file mode 100644 index 0000000..3d406f2 --- /dev/null +++ b/hopper/enums.py @@ -0,0 +1,20 @@ +from enum import Enum, auto + + +class Direction(Enum): + LEFT = "left" + RIGHT = "right" + UP = "up" + DOWN = "down" + + +class ObjectType(str, Enum): + NONE = auto() + OBSTACLE = auto() + PLAYER = auto() + DESTINATION = auto() + + +class PlayerMoveResult(Enum): + OK = auto() + DESTINATION_REACHED = auto() diff --git a/hopper/errors.py b/hopper/errors.py new file mode 100644 index 0000000..6c623d3 --- /dev/null +++ b/hopper/errors.py @@ -0,0 +1,10 @@ +class BaseError(Exception): + ... + + +class PositionOutOfBounds(BaseError): + ... + + +class Collision(BaseError): + ... diff --git a/hopper/models/__init__.py b/hopper/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hopper/models/board.py b/hopper/models/board.py new file mode 100644 index 0000000..34933e3 --- /dev/null +++ b/hopper/models/board.py @@ -0,0 +1,111 @@ +import random +from copy import copy +from dataclasses import dataclass, field +from typing import Optional + +from hopper.enums import ObjectType +from hopper.models.player import Player, Position + +BOARD_DUMP_CHARS: dict[ObjectType, str] = { + ObjectType.NONE: "·", + ObjectType.OBSTACLE: "✘", + ObjectType.PLAYER: "⯧", + ObjectType.DESTINATION: "⬤", +} + + +@dataclass +class LayerObject: + type_: ObjectType + position: Position + + +@dataclass +class Layer: + name: Optional[str] = None + objects: list[LayerObject] = field(default_factory=list) + + def get_object_at_position(self, position: Position) -> Optional[LayerObject]: + for obj in self.objects: + if obj.position == position: + return obj + return None + + +@dataclass +class Destination: + position: Position + + +@dataclass +class GameBoard: + width: int + height: int + destination: Destination + layers: list[Layer] = field(default_factory=list) + + def dump(self) -> list[list[str]]: + board = [ + [BOARD_DUMP_CHARS[ObjectType.NONE] for _ in range(self.width)] + for _ in range(self.height) + ] + + for layer in self.layers: + for obj in layer.objects: + board[obj.position.y][obj.position.x] = BOARD_DUMP_CHARS[ + ObjectType.OBSTACLE + ] + + board[self.destination.position.y][ + self.destination.position.x + ] = BOARD_DUMP_CHARS[ObjectType.DESTINATION] + + return board + + def get_object_at_position(self, position: Position) -> Optional[LayerObject]: + for layer in self.layers: + obj = layer.get_object_at_position(position) + if obj is not None: + return obj + return None + + +class BoardLayout: + def __init__(self, board: GameBoard, players: list[Player]) -> None: + self.board = board + self.players = players + self.layers = self.__create_layers() + + def __create_layers(self) -> list[Layer]: + layers = copy(self.board.layers) + layers.append( + Layer( + name="destination", + objects=[ + LayerObject( + type_=ObjectType.DESTINATION, + position=self.board.destination.position, + ), + ], + ) + ) + layers.append( + Layer( + name="players", + objects=[ + LayerObject( + type_=ObjectType.PLAYER, + position=player.position, + ) + for player in self.players + ], + ) + ) + return layers + + +def create_random_position(board_width: int, board_height: int) -> Position: + return Position( + x=random.randint(0, board_width - 1), + y=random.randint(0, board_height - 1), + ) diff --git a/hopper/models/config.py b/hopper/models/config.py new file mode 100644 index 0000000..2e10d00 --- /dev/null +++ b/hopper/models/config.py @@ -0,0 +1,29 @@ +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class BoardSettings: + WIDTH: int = 21 + HEIGHT: int = 21 + OBSTACLE_COUNT: int = 10 + + +@dataclass +class InactivityWatchdogSettings: + INACIVITY_TIMEOUT: int = 10 # seconds + KICK_TIMEOUT: int = 60 * 10 # seconds + TICK_INTERVAL: int = 1 # seconds + + +@dataclass +class DebugSettings: + PRINT_BOARD: bool = False + CREATE_TEST_PLAYER: bool = False + + +@dataclass +class Settings: + board: BoardSettings + inacivity_watchdog: InactivityWatchdogSettings + debug: Optional[DebugSettings] = None diff --git a/hopper/models/player.py b/hopper/models/player.py new file mode 100644 index 0000000..cc8900a --- /dev/null +++ b/hopper/models/player.py @@ -0,0 +1,34 @@ +import datetime +import uuid +from dataclasses import dataclass, field +from typing import Optional + + +@dataclass +class Position: + x: int + y: int + + +@dataclass +class Player: + name: str + uuid: str = field(default_factory=lambda: str(uuid.uuid4())) + position: Position = field(default_factory=lambda: Position(0, 0)) + move_count: int = 0 + move_attempt_count: int = 0 + last_seen: datetime.datetime = field( + default_factory=lambda: datetime.datetime.now() + ) + active: bool = True + + def reset_timeout(self) -> None: + self.last_seen = datetime.datetime.now() + + +class PlayerList(list[Player]): + def find(self, uuid: str) -> Optional[Player]: + for player in self: + if player.uuid == uuid: + return player + return None diff --git a/hopper/watchdog.py b/hopper/watchdog.py new file mode 100644 index 0000000..3d57f62 --- /dev/null +++ b/hopper/watchdog.py @@ -0,0 +1,47 @@ +import datetime +import logging +import time +from threading import Thread + +from hopper.models.player import PlayerList +from settings import settings + + +class InactivityWatchdog(Thread): + def __init__(self, players: PlayerList, *args, **kwargs) -> None: + self.players = players + self.stopped = False + super().__init__(*args, **kwargs) + + def run(self) -> None: + logging.info("Starting inactivity watchdog") + while not self.stopped: + self.cleanup_players() + time.sleep(settings.inacivity_watchdog.TICK_INTERVAL) + + def cleanup_players(self) -> None: + now = datetime.datetime.now() + inactivity_threshold = now - datetime.timedelta( + seconds=settings.inacivity_watchdog.INACIVITY_TIMEOUT + ) + kick_threshold = now - datetime.timedelta( + seconds=settings.inacivity_watchdog.KICK_TIMEOUT + ) + + for player in self.players: + if player.active and player.last_seen < inactivity_threshold: + player.active = False + logging.info(f"Player {player} set as inactive") + + # safe remove from list + n = 0 + while n < len(self.players): + player = self.players[n] + if player.last_seen < kick_threshold: + self.players.pop(n) + logging.info(f"Player {player} kicked out") + else: + n += 1 + + def stop(self) -> None: + self.stopped = True diff --git a/main.py b/main.py new file mode 100644 index 0000000..f18c2a8 --- /dev/null +++ b/main.py @@ -0,0 +1,16 @@ +import logging + +from fastapi import FastAPI + +from hopper.api.dependencies import create_game_engine +from hopper.api.views import router + +logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s %(levelname)s - %(message)s", +) +logging.info("JFK Game server started.") + +app = FastAPI() +app.include_router(router, tags=["Game API"]) +create_game_engine() diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..17c0a04 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,293 @@ +# This file is automatically @generated by Poetry 1.4.1 and should not be changed by hand. + +[[package]] +name = "anyio" +version = "3.6.2" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +category = "main" +optional = false +python-versions = ">=3.6.2" +files = [ + {file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"}, + {file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"}, +] + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"] +trio = ["trio (>=0.16,<0.22)"] + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "fastapi" +version = "0.95.0" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "fastapi-0.95.0-py3-none-any.whl", hash = "sha256:daf73bbe844180200be7966f68e8ec9fd8be57079dff1bacb366db32729e6eb5"}, + {file = "fastapi-0.95.0.tar.gz", hash = "sha256:99d4fdb10e9dd9a24027ac1d0bd4b56702652056ca17a6c8721eec4ad2f14e18"}, +] + +[package.dependencies] +pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" +starlette = ">=0.26.1,<0.27.0" + +[package.extras] +all = ["email-validator (>=1.1.1)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +dev = ["pre-commit (>=2.17.0,<3.0.0)", "ruff (==0.0.138)", "uvicorn[standard] (>=0.12.0,<0.21.0)"] +doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer-cli (>=0.0.13,<0.0.14)", "typer[all] (>=0.6.1,<0.8.0)"] +test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==23.1.0)", "coverage[toml] (>=6.5.0,<8.0)", "databases[sqlite] (>=0.3.2,<0.7.0)", "email-validator (>=1.1.1,<2.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.23.0,<0.24.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.982)", "orjson (>=3.2.1,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=7.1.3,<8.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.7)", "pyyaml (>=5.3.1,<7.0.0)", "ruff (==0.0.138)", "sqlalchemy (>=1.3.18,<1.4.43)", "types-orjson (==3.6.2)", "types-ujson (==5.7.0.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"] + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "pydantic" +version = "1.10.7" +description = "Data validation and settings management using python type hints" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic-1.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e79e999e539872e903767c417c897e729e015872040e56b96e67968c3b918b2d"}, + {file = "pydantic-1.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:01aea3a42c13f2602b7ecbbea484a98169fb568ebd9e247593ea05f01b884b2e"}, + {file = "pydantic-1.10.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:516f1ed9bc2406a0467dd777afc636c7091d71f214d5e413d64fef45174cfc7a"}, + {file = "pydantic-1.10.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae150a63564929c675d7f2303008d88426a0add46efd76c3fc797cd71cb1b46f"}, + {file = "pydantic-1.10.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ecbbc51391248116c0a055899e6c3e7ffbb11fb5e2a4cd6f2d0b93272118a209"}, + {file = "pydantic-1.10.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f4a2b50e2b03d5776e7f21af73e2070e1b5c0d0df255a827e7c632962f8315af"}, + {file = "pydantic-1.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:a7cd2251439988b413cb0a985c4ed82b6c6aac382dbaff53ae03c4b23a70e80a"}, + {file = "pydantic-1.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:68792151e174a4aa9e9fc1b4e653e65a354a2fa0fed169f7b3d09902ad2cb6f1"}, + {file = "pydantic-1.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe2507b8ef209da71b6fb5f4e597b50c5a34b78d7e857c4f8f3115effaef5fe"}, + {file = "pydantic-1.10.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10a86d8c8db68086f1e30a530f7d5f83eb0685e632e411dbbcf2d5c0150e8dcd"}, + {file = "pydantic-1.10.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75ae19d2a3dbb146b6f324031c24f8a3f52ff5d6a9f22f0683694b3afcb16fb"}, + {file = "pydantic-1.10.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:464855a7ff7f2cc2cf537ecc421291b9132aa9c79aef44e917ad711b4a93163b"}, + {file = "pydantic-1.10.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:193924c563fae6ddcb71d3f06fa153866423ac1b793a47936656e806b64e24ca"}, + {file = "pydantic-1.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:b4a849d10f211389502059c33332e91327bc154acc1845f375a99eca3afa802d"}, + {file = "pydantic-1.10.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cc1dde4e50a5fc1336ee0581c1612215bc64ed6d28d2c7c6f25d2fe3e7c3e918"}, + {file = "pydantic-1.10.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0cfe895a504c060e5d36b287ee696e2fdad02d89e0d895f83037245218a87fe"}, + {file = "pydantic-1.10.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:670bb4683ad1e48b0ecb06f0cfe2178dcf74ff27921cdf1606e527d2617a81ee"}, + {file = "pydantic-1.10.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:950ce33857841f9a337ce07ddf46bc84e1c4946d2a3bba18f8280297157a3fd1"}, + {file = "pydantic-1.10.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c15582f9055fbc1bfe50266a19771bbbef33dd28c45e78afbe1996fd70966c2a"}, + {file = "pydantic-1.10.7-cp37-cp37m-win_amd64.whl", hash = "sha256:82dffb306dd20bd5268fd6379bc4bfe75242a9c2b79fec58e1041fbbdb1f7914"}, + {file = "pydantic-1.10.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c7f51861d73e8b9ddcb9916ae7ac39fb52761d9ea0df41128e81e2ba42886cd"}, + {file = "pydantic-1.10.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6434b49c0b03a51021ade5c4daa7d70c98f7a79e95b551201fff682fc1661245"}, + {file = "pydantic-1.10.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d34ab766fa056df49013bb6e79921a0265204c071984e75a09cbceacbbdd5d"}, + {file = "pydantic-1.10.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:701daea9ffe9d26f97b52f1d157e0d4121644f0fcf80b443248434958fd03dc3"}, + {file = "pydantic-1.10.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cf135c46099ff3f919d2150a948ce94b9ce545598ef2c6c7bf55dca98a304b52"}, + {file = "pydantic-1.10.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0f85904f73161817b80781cc150f8b906d521fa11e3cdabae19a581c3606209"}, + {file = "pydantic-1.10.7-cp38-cp38-win_amd64.whl", hash = "sha256:9f6f0fd68d73257ad6685419478c5aece46432f4bdd8d32c7345f1986496171e"}, + {file = "pydantic-1.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c230c0d8a322276d6e7b88c3f7ce885f9ed16e0910354510e0bae84d54991143"}, + {file = "pydantic-1.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:976cae77ba6a49d80f461fd8bba183ff7ba79f44aa5cfa82f1346b5626542f8e"}, + {file = "pydantic-1.10.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d45fc99d64af9aaf7e308054a0067fdcd87ffe974f2442312372dfa66e1001d"}, + {file = "pydantic-1.10.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d2a5ebb48958754d386195fe9e9c5106f11275867051bf017a8059410e9abf1f"}, + {file = "pydantic-1.10.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:abfb7d4a7cd5cc4e1d1887c43503a7c5dd608eadf8bc615413fc498d3e4645cd"}, + {file = "pydantic-1.10.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:80b1fab4deb08a8292d15e43a6edccdffa5377a36a4597bb545b93e79c5ff0a5"}, + {file = "pydantic-1.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:d71e69699498b020ea198468e2480a2f1e7433e32a3a99760058c6520e2bea7e"}, + {file = "pydantic-1.10.7-py3-none-any.whl", hash = "sha256:0cd181f1d0b1d00e2b705f1bf1ac7799a2d938cce3376b8007df62b29be3c2c6"}, + {file = "pydantic-1.10.7.tar.gz", hash = "sha256:cfc83c0678b6ba51b0532bea66860617c4cd4251ecf76e9846fa5a9f3454e97e"}, +] + +[package.dependencies] +typing-extensions = ">=4.2.0" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + +[[package]] +name = "sniffio" +version = "1.3.0" +description = "Sniff out which async library your code is running under" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, + {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, +] + +[[package]] +name = "starlette" +version = "0.26.1" +description = "The little ASGI library that shines." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "starlette-0.26.1-py3-none-any.whl", hash = "sha256:e87fce5d7cbdde34b76f0ac69013fd9d190d581d80681493016666e6f96c6d5e"}, + {file = "starlette-0.26.1.tar.gz", hash = "sha256:41da799057ea8620e4667a3e69a5b1923ebd32b1819c8fa75634bbe8d8bea9bd"}, +] + +[package.dependencies] +anyio = ">=3.4.0,<5" + +[package.extras] +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] + +[[package]] +name = "typing-extensions" +version = "4.5.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, + {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, +] + +[[package]] +name = "uvicorn" +version = "0.21.1" +description = "The lightning-fast ASGI server." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "uvicorn-0.21.1-py3-none-any.whl", hash = "sha256:e47cac98a6da10cd41e6fd036d472c6f58ede6c5dbee3dbee3ef7a100ed97742"}, + {file = "uvicorn-0.21.1.tar.gz", hash = "sha256:0fac9cb342ba099e0d582966005f3fdba5b0290579fed4a6266dc702ca7bb032"}, +] + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" + +[package.extras] +standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] + +[[package]] +name = "websockets" +version = "10.4" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "websockets-10.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d58804e996d7d2307173d56c297cf7bc132c52df27a3efaac5e8d43e36c21c48"}, + {file = "websockets-10.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc0b82d728fe21a0d03e65f81980abbbcb13b5387f733a1a870672c5be26edab"}, + {file = "websockets-10.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ba089c499e1f4155d2a3c2a05d2878a3428cf321c848f2b5a45ce55f0d7d310c"}, + {file = "websockets-10.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33d69ca7612f0ddff3316b0c7b33ca180d464ecac2d115805c044bf0a3b0d032"}, + {file = "websockets-10.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62e627f6b6d4aed919a2052efc408da7a545c606268d5ab5bfab4432734b82b4"}, + {file = "websockets-10.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ea7b82bfcae927eeffc55d2ffa31665dc7fec7b8dc654506b8e5a518eb4d50"}, + {file = "websockets-10.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e0cb5cc6ece6ffa75baccfd5c02cffe776f3f5c8bf486811f9d3ea3453676ce8"}, + {file = "websockets-10.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ae5e95cfb53ab1da62185e23b3130e11d64431179debac6dc3c6acf08760e9b1"}, + {file = "websockets-10.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7c584f366f46ba667cfa66020344886cf47088e79c9b9d39c84ce9ea98aaa331"}, + {file = "websockets-10.4-cp310-cp310-win32.whl", hash = "sha256:b029fb2032ae4724d8ae8d4f6b363f2cc39e4c7b12454df8df7f0f563ed3e61a"}, + {file = "websockets-10.4-cp310-cp310-win_amd64.whl", hash = "sha256:8dc96f64ae43dde92530775e9cb169979f414dcf5cff670455d81a6823b42089"}, + {file = "websockets-10.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:47a2964021f2110116cc1125b3e6d87ab5ad16dea161949e7244ec583b905bb4"}, + {file = "websockets-10.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e789376b52c295c4946403bd0efecf27ab98f05319df4583d3c48e43c7342c2f"}, + {file = "websockets-10.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7d3f0b61c45c3fa9a349cf484962c559a8a1d80dae6977276df8fd1fa5e3cb8c"}, + {file = "websockets-10.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f55b5905705725af31ccef50e55391621532cd64fbf0bc6f4bac935f0fccec46"}, + {file = "websockets-10.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00c870522cdb69cd625b93f002961ffb0c095394f06ba8c48f17eef7c1541f96"}, + {file = "websockets-10.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f38706e0b15d3c20ef6259fd4bc1700cd133b06c3c1bb108ffe3f8947be15fa"}, + {file = "websockets-10.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f2c38d588887a609191d30e902df2a32711f708abfd85d318ca9b367258cfd0c"}, + {file = "websockets-10.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:fe10ddc59b304cb19a1bdf5bd0a7719cbbc9fbdd57ac80ed436b709fcf889106"}, + {file = "websockets-10.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:90fcf8929836d4a0e964d799a58823547df5a5e9afa83081761630553be731f9"}, + {file = "websockets-10.4-cp311-cp311-win32.whl", hash = "sha256:b9968694c5f467bf67ef97ae7ad4d56d14be2751000c1207d31bf3bb8860bae8"}, + {file = "websockets-10.4-cp311-cp311-win_amd64.whl", hash = "sha256:a7a240d7a74bf8d5cb3bfe6be7f21697a28ec4b1a437607bae08ac7acf5b4882"}, + {file = "websockets-10.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:74de2b894b47f1d21cbd0b37a5e2b2392ad95d17ae983e64727e18eb281fe7cb"}, + {file = "websockets-10.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3a686ecb4aa0d64ae60c9c9f1a7d5d46cab9bfb5d91a2d303d00e2cd4c4c5cc"}, + {file = "websockets-10.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d15c968ea7a65211e084f523151dbf8ae44634de03c801b8bd070b74e85033"}, + {file = "websockets-10.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00213676a2e46b6ebf6045bc11d0f529d9120baa6f58d122b4021ad92adabd41"}, + {file = "websockets-10.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:e23173580d740bf8822fd0379e4bf30aa1d5a92a4f252d34e893070c081050df"}, + {file = "websockets-10.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:dd500e0a5e11969cdd3320935ca2ff1e936f2358f9c2e61f100a1660933320ea"}, + {file = "websockets-10.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4239b6027e3d66a89446908ff3027d2737afc1a375f8fd3eea630a4842ec9a0c"}, + {file = "websockets-10.4-cp37-cp37m-win32.whl", hash = "sha256:8a5cc00546e0a701da4639aa0bbcb0ae2bb678c87f46da01ac2d789e1f2d2038"}, + {file = "websockets-10.4-cp37-cp37m-win_amd64.whl", hash = "sha256:a9f9a735deaf9a0cadc2d8c50d1a5bcdbae8b6e539c6e08237bc4082d7c13f28"}, + {file = "websockets-10.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5c1289596042fad2cdceb05e1ebf7aadf9995c928e0da2b7a4e99494953b1b94"}, + {file = "websockets-10.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0cff816f51fb33c26d6e2b16b5c7d48eaa31dae5488ace6aae468b361f422b63"}, + {file = "websockets-10.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:dd9becd5fe29773d140d68d607d66a38f60e31b86df75332703757ee645b6faf"}, + {file = "websockets-10.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45ec8e75b7dbc9539cbfafa570742fe4f676eb8b0d3694b67dabe2f2ceed8aa6"}, + {file = "websockets-10.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f72e5cd0f18f262f5da20efa9e241699e0cf3a766317a17392550c9ad7b37d8"}, + {file = "websockets-10.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:185929b4808b36a79c65b7865783b87b6841e852ef5407a2fb0c03381092fa3b"}, + {file = "websockets-10.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7d27a7e34c313b3a7f91adcd05134315002aaf8540d7b4f90336beafaea6217c"}, + {file = "websockets-10.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:884be66c76a444c59f801ac13f40c76f176f1bfa815ef5b8ed44321e74f1600b"}, + {file = "websockets-10.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:931c039af54fc195fe6ad536fde4b0de04da9d5916e78e55405436348cfb0e56"}, + {file = "websockets-10.4-cp38-cp38-win32.whl", hash = "sha256:db3c336f9eda2532ec0fd8ea49fef7a8df8f6c804cdf4f39e5c5c0d4a4ad9a7a"}, + {file = "websockets-10.4-cp38-cp38-win_amd64.whl", hash = "sha256:48c08473563323f9c9debac781ecf66f94ad5a3680a38fe84dee5388cf5acaf6"}, + {file = "websockets-10.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:40e826de3085721dabc7cf9bfd41682dadc02286d8cf149b3ad05bff89311e4f"}, + {file = "websockets-10.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:56029457f219ade1f2fc12a6504ea61e14ee227a815531f9738e41203a429112"}, + {file = "websockets-10.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f5fc088b7a32f244c519a048c170f14cf2251b849ef0e20cbbb0fdf0fdaf556f"}, + {file = "websockets-10.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fc8709c00704194213d45e455adc106ff9e87658297f72d544220e32029cd3d"}, + {file = "websockets-10.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0154f7691e4fe6c2b2bc275b5701e8b158dae92a1ab229e2b940efe11905dff4"}, + {file = "websockets-10.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c6d2264f485f0b53adf22697ac11e261ce84805c232ed5dbe6b1bcb84b00ff0"}, + {file = "websockets-10.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9bc42e8402dc5e9905fb8b9649f57efcb2056693b7e88faa8fb029256ba9c68c"}, + {file = "websockets-10.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:edc344de4dac1d89300a053ac973299e82d3db56330f3494905643bb68801269"}, + {file = "websockets-10.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:84bc2a7d075f32f6ed98652db3a680a17a4edb21ca7f80fe42e38753a58ee02b"}, + {file = "websockets-10.4-cp39-cp39-win32.whl", hash = "sha256:c94ae4faf2d09f7c81847c63843f84fe47bf6253c9d60b20f25edfd30fb12588"}, + {file = "websockets-10.4-cp39-cp39-win_amd64.whl", hash = "sha256:bbccd847aa0c3a69b5f691a84d2341a4f8a629c6922558f2a70611305f902d74"}, + {file = "websockets-10.4-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:82ff5e1cae4e855147fd57a2863376ed7454134c2bf49ec604dfe71e446e2193"}, + {file = "websockets-10.4-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d210abe51b5da0ffdbf7b43eed0cfdff8a55a1ab17abbec4301c9ff077dd0342"}, + {file = "websockets-10.4-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:942de28af58f352a6f588bc72490ae0f4ccd6dfc2bd3de5945b882a078e4e179"}, + {file = "websockets-10.4-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9b27d6c1c6cd53dc93614967e9ce00ae7f864a2d9f99fe5ed86706e1ecbf485"}, + {file = "websockets-10.4-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:3d3cac3e32b2c8414f4f87c1b2ab686fa6284a980ba283617404377cd448f631"}, + {file = "websockets-10.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:da39dd03d130162deb63da51f6e66ed73032ae62e74aaccc4236e30edccddbb0"}, + {file = "websockets-10.4-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389f8dbb5c489e305fb113ca1b6bdcdaa130923f77485db5b189de343a179393"}, + {file = "websockets-10.4-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09a1814bb15eff7069e51fed0826df0bc0702652b5cb8f87697d469d79c23576"}, + {file = "websockets-10.4-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff64a1d38d156d429404aaa84b27305e957fd10c30e5880d1765c9480bea490f"}, + {file = "websockets-10.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b343f521b047493dc4022dd338fc6db9d9282658862756b4f6fd0e996c1380e1"}, + {file = "websockets-10.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:932af322458da7e4e35df32f050389e13d3d96b09d274b22a7aa1808f292fee4"}, + {file = "websockets-10.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6a4162139374a49eb18ef5b2f4da1dd95c994588f5033d64e0bbfda4b6b6fcf"}, + {file = "websockets-10.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c57e4c1349fbe0e446c9fa7b19ed2f8a4417233b6984277cce392819123142d3"}, + {file = "websockets-10.4-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b627c266f295de9dea86bd1112ed3d5fafb69a348af30a2422e16590a8ecba13"}, + {file = "websockets-10.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:05a7233089f8bd355e8cbe127c2e8ca0b4ea55467861906b80d2ebc7db4d6b72"}, + {file = "websockets-10.4.tar.gz", hash = "sha256:eef610b23933c54d5d921c92578ae5f89813438fded840c2e9809d378dc765d3"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.10" +content-hash = "359e3182426fcf38d86350edb2617cf4b51eae877aadfefb7abf5d2ed26a65ea" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..378f610 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,18 @@ +[tool.poetry] +name = "FairHopper" +version = "0.1.0" +description = "" +authors = ["Eden Kirin "] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.10" +fastapi = "^0.95.0" +websockets = "^10.4" +uvicorn = "^0.21.1" +pydantic = "^1.10.7" + + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/settings_template.py b/settings_template.py new file mode 100644 index 0000000..a786306 --- /dev/null +++ b/settings_template.py @@ -0,0 +1,7 @@ +from hopper.models.config import BoardSettings, InactivityWatchdogSettings, Settings + +settings = Settings( + board=BoardSettings(), + inacivity_watchdog=InactivityWatchdogSettings(), + debug=None, +)