Project rename and restructure

This commit is contained in:
Eden Kirin
2023-03-25 13:21:07 +01:00
commit 0041b7d43e
21 changed files with 1328 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
.vscode
.idea
__pycache__
/env
/.venv
/settings.py

16
Makefile Normal file
View File

@ -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

316
README.md Normal file
View File

@ -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
}
}
]
}
]
}
```

0
hopper/__init__.py Normal file
View File

0
hopper/api/__init__.py Normal file
View File

View File

@ -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

73
hopper/api/dto.py Normal file
View File

@ -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

132
hopper/api/views.py Normal file
View File

@ -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))

View File

@ -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
###

153
hopper/engine.py Normal file
View File

@ -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}")

20
hopper/enums.py Normal file
View File

@ -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()

10
hopper/errors.py Normal file
View File

@ -0,0 +1,10 @@
class BaseError(Exception):
...
class PositionOutOfBounds(BaseError):
...
class Collision(BaseError):
...

View File

111
hopper/models/board.py Normal file
View File

@ -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),
)

29
hopper/models/config.py Normal file
View File

@ -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

34
hopper/models/player.py Normal file
View File

@ -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

47
hopper/watchdog.py Normal file
View File

@ -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

16
main.py Normal file
View File

@ -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()

293
poetry.lock generated Normal file
View File

@ -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"

18
pyproject.toml Normal file
View File

@ -0,0 +1,18 @@
[tool.poetry]
name = "FairHopper"
version = "0.1.0"
description = ""
authors = ["Eden Kirin <eden@ekirin.com>"]
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"

7
settings_template.py Normal file
View File

@ -0,0 +1,7 @@
from hopper.models.config import BoardSettings, InactivityWatchdogSettings, Settings
settings = Settings(
board=BoardSettings(),
inacivity_watchdog=InactivityWatchdogSettings(),
debug=None,
)