diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b7b1143 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +fly.toml +app +battlechess_standalone +client +data +ia +tests +.vscode +.pytest_cache +.github diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..38018f6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,28 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = false +insert_final_newline = true + +[*.sql] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = false +insert_final_newline = true + +[*.yaml, *.yml] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = false +insert_final_newline = true diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..5f8512e --- /dev/null +++ b/.flake8 @@ -0,0 +1,4 @@ +[flake8] +extend-ignore = E226,E302,E41,W503,E203 +max-line-length = 105 +exclude = .git,__pycache__,docs/source/conf.py,old,build,dist diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b14380c..66b538d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,19 +8,30 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.8] + python-version: ["3.8", "3.9", "3.10"] + poetry-version: ["1.7.1"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - - name: Install dependencies + - name: Run image + uses: abatilo/actions-poetry@v2 + with: + poetry-version: ${{ matrix.poetry-version }} + - name: Setup a local virtual environment (if no poetry.toml file) run: | - python -m pip install --upgrade pip - pip install flake8 pytest - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + poetry config virtualenvs.create true --local + poetry config virtualenvs.in-project true --local + - uses: actions/cache@v3 + name: Define a cache for the virtual environment based on the dependencies lock file + with: + path: ./.venv + key: venv-${{ hashFiles('poetry.lock') }} + - name: Install dependencies + run: poetry install #- name: Lint with flake8 # run: | # # stop the build if there are Python syntax errors or undefined names @@ -28,5 +39,7 @@ jobs: # # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide # flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with pytest - run: | - pytest + run: poetry run pytest -v + env: + SECRET_KEY: ${{ secrets.SECRET_KEY }} + diff --git a/.gitignore b/.gitignore index 4c43c6e..e707a8a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,7 @@ bin .vscode sql_app.db test.db +*.db +*.sqlite + +.env diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 0000000..b9fb3f3 --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,2 @@ +[settings] +profile=black diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..666a2b5 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,29 @@ +--- +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-yaml + args: ['--unsafe'] + - id: trailing-whitespace + - id: detect-private-key + - id: name-tests-test + args: ["--pytest-test-first"] + - repo: https://github.com/psf/black + rev: 22.12.0 + hooks: + - id: black + - repo: https://github.com/pycqa/isort + rev: 5.11.5 + hooks: + - id: isort + name: isort (python) + args: ["--profile", "black", "--filter-files"] + - repo: https://github.com/charliermarsh/ruff-pre-commit + # Ruff version. + rev: "v0.0.261" + hooks: + - id: ruff +fail_fast: false +files: ".*" +exclude: "desktop.ini" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..51588ba --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +# https://hub.docker.com/_/python +FROM python:3.10-slim-bookworm + +ENV PYTHONUNBUFFERED True + +RUN apt-get update && apt-get install -y \ + python3-pip \ + python3-venv \ + python3-dev \ + python3-setuptools \ + python3-wheel + +RUN mkdir -p /app +WORKDIR /app + +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +COPY . ./ + +EXPOSE 8000 + +CMD ["uvicorn", "battlechess.server.btchApi:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..f05bd78 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: uvicorn battlechess.server.btchApi:app --host=0.0.0.0 --port=${PORT:-5000} diff --git a/Readme.md b/README.md similarity index 98% rename from Readme.md rename to README.md index 8d29f3b..fba0b7d 100644 --- a/Readme.md +++ b/README.md @@ -35,6 +35,8 @@ That new rule has some direct consequences on the gameplay. ## Installation : +### Battlechess standalone (LAN) + - What you need : * [python 3.7](https://www.python.org/downloads/) (or more I guess...) * [pygame 1.9.2](http://www.pygame.org/download.shtml). On OS X with python 2.7, you should donwload this [installation file](http://www.pygame.org/ftp/pygame-1.9.2pre-py2.7-macosx10.7.mpkg.zip) @@ -57,9 +59,11 @@ That new rule has some direct consequences on the gameplay. `python battleChess.py -p http://sxbn.org/~antoine/games/2014_03_07_14_06_37_lance_hardwood_Vs_sniper.txt` `python battleChess.py -p ./2014_03_07_14_06_37_lance_hardwood_Vs_sniper.txt` +### Battlechess api server + - Using the server application : If you want to host your own server (you don't have to). You just need to run the api with - `$ uvicorn server.btchApi:app --reload` + `$ uvicorn battleches.server.btchApi:app --reload` program on a computer that can be reached through the network. Change the port and hostname if you want and pass those informations to the client application. Check `$ uvicorn --help`. You'll need to install the packages listed in `requirements.txt`. You can do so with `$ pip install -r requirements.txt` diff --git a/Readme.txt b/Readme.txt deleted file mode 100644 index 964b130..0000000 --- a/Readme.txt +++ /dev/null @@ -1,98 +0,0 @@ - ___ _ _ _ ___ _ - ( _`\ ( )_ ( )_ (_ ) ( _`\ ( ) - | (_) ) _ _ | ,_)| ,_) | | __ | ( (_)| |__ __ ___ ___ - | _ <' /'_` )| | | | | | /'__`\| | _ | _ `\ /'__`\/',__)/',__) - | (_) )( (_| || |_ | |_ | | ( ___/| (_( )| | | |( ___/\__, \\__, \ - (____/'`\__,_)`\__)`\__)(___)`\____)(____/'(_) (_)`\____)(____/(____/ - - -+----------- -| Abstract : -+----------- - * quick game : - python battleChess.py - * download : - git clone http://git.sxbn.org/battleChess.git - - - -+--------------- -| Introduction : -+--------------- -On a regular afternoon break at CVLab, a discussion was about to wake us (Pol, Raphael, Pen and Antoine) from our boredom. -"Chess, to brainy. BattleShip, lack some action... But wait, what if we mixed both ?" -BattleChess was born. Mixing rules from both games to make a new exciting one. - - -+-------- -| Rules : -+-------- -The rules are pretty straithforward for anyone who played chess before. The board and pieces are the same. They move and capture opponents the same way. The main difference arise from the fact that at a given time each player can only see the part of the board he actually controls. That's all his pieces positions and direct neighbooring cells. No more, no less. -That new rule has some direct consequences on the gameplay. -- Towers, bishops, and queens may be asked to move to an position without knowing if it can be reached safely or at all. Some unseen pieces could be in the way. If that happens, the moving piece goes as far as possible and take the blocking opponent piece. -- There is no way of preventing the king to put himself in a hazardous position without letting the player infer information about his opponent's position. So the king is free to move as he wishes and the game ends not on check-mate but on a king's death. -- Every powns which reaches the end of the board becomes a queen, you cannot choose, deal with it. - - -+--------------- -| Installation : -+--------------- - -- What you need : - * python 2.7 (or more I guess...) - * pygame 1.9.2 - * the game itself. It can be dowloaded via git using the following command - git clone http://git.sxbn.org/battleChess.git - -- Launching the game : - In the root directory ( battleChess/ ), you can directly launch the game using the command - python battleChess.py [NickName] [HOST] [PORT] - with 3 optional parameters : - Nickname : will be you name during the game. If not given, it will be chosen randomly - HOST : server name to connect to (see bellow). By default sxbn.org - PORT : which port to connect to. By default 8887 - There is and will be a server app running on sxbn.org, so I recommand no changing the HOST and PORT parameters unless you want to host your own games. - -- Replay mode : - You just got beaten and you don't know what just happend. Don't worry that happens a lot. Every game played are saved on the server and can be downloaded from this webpage : - http://git.sxbn.org/battleChess/games/ - You can either download the file or replay it from the url directly using one of the following commands : - python battleChess.py -p http://git.sxbn.org/battleChess/games/2014_03_07_14_06_37_lance_hardwood_Vs_sniper.txt - python battleChess.py -p ./2014_03_07_14_06_37_lance_hardwood_Vs_sniper.txt - -- Using the server application : - If you want to host your own server (you don't have to). You just need to run the server.py programm on a computer that can be reached through the network. Change the port and hostname if you want and pass those informations to the client application. - -- Information sent on the network : - If you worry about privacy, you can launch your own server. It's pretty straigthforward to see from the source code that nothing is sent anywhere else. - The only information sent and stored on the server are your nickname and the moves you make. - -+-------------- -| How to play : -+-------------- - -Regular game : - White play first. To move a piece, click on it and then click on the desired destination. Your piece will move ther, if it can. - You know it's your turn to move when the message on the top left says your color. - You can exit the game anytime with ESCAPE. - -Replay mode : - Use LEFT and RIGHT arrows to move step by step (and loop) into the game. - SPACE will reset to initial state. - - -+-------------------- -| Credits & Licence : -+-------------------- - -Code : - Antoine Letouzey -- antoine.letouzey@gmail.com - Pol Monsó-Purtí -- pol.monso@gmail.com - -Sprites : - Original sprites by Wikipedia user Cburnett, under Creative Commons Licence (CC BY-SA 3.0) - -Licence : GPL - - - diff --git a/core/__init__.py b/battlechess/__init__.py similarity index 100% rename from core/__init__.py rename to battlechess/__init__.py diff --git a/battlechess/core/Board.py b/battlechess/core/Board.py new file mode 100644 index 0000000..e426fb5 --- /dev/null +++ b/battlechess/core/Board.py @@ -0,0 +1,669 @@ +RQBPOS = [0, 0] +RKBPOS = [0, 7] +RQWPOS = [7, 0] +RKWPOS = [7, 7] +KBPOS = [0, 4] +KWPOS = [7, 4] +CASTLEQBPOS = [0, 2] +CASTLEKBPOS = [0, 6] +CASTLEQWPOS = [7, 2] +CASTLEKWPOS = [7, 6] +CASTLEABLE = sorted(["kb", "kw", "rqb", "rkb", "rqw", "rkw"]) + + +class Board(object): + def __init__(self): + self.reset() + self.taken = [] + self.castleable = list(CASTLEABLE) + self.enpassant = -1 + self.winner = None + self.enpassant = -1 + + def reset(self): + self.board = [["" for i in range(8)] for j in range(8)] + self.board[0] = ["rb", "nb", "bb", "qb", "kb", "bb", "nb", "rb"] + self.board[1] = ["pb", "pb", "pb", "pb", "pb", "pb", "pb", "pb"] + self.board[6] = ["pw", "pw", "pw", "pw", "pw", "pw", "pw", "pw"] + self.board[7] = ["rw", "nw", "bw", "qw", "kw", "bw", "nw", "rw"] + + # make a deep copy of the board + def copy(self): + res = Board() + res.taken = list(self.taken) + res.board = [list(b) for b in self.board] + res.castleable = list(self.castleable) + res.enpassant = self.enpassant + res.winner = self.winner + return res + + def isIn(self, i, j): + return i > -1 and i < 8 and j > -1 and j < 8 + + # tells wether a cell is free or not + # returns : + # 1 if the cell is empty + # 2 if the cell is occupied by a different player than the one given + # 0 if the cell is out of bound + def isFree(self, i, j, c=""): + if self.isIn(i, j): + if self.board[i][j] == "": + return 1 + if self.board[i][j][1] != c: + return 2 + else: + return 0 + + def takeEnPassant(self, i, j, ii, jj): + if ( + self.board[i][j][0] == "p" + and jj == self.enpassant + and (j == self.enpassant - 1 or j == self.enpassant + 1) + ): + if ( + self.board[i][j][1] == "w" + and i == 3 + or self.board[i][j][1] == "b" + and i == 4 + ): + # todo remove this error check + if self.board[i][self.enpassant] == "": + print( + "Nothing at " + + str([i, self.enpassant]) + + ". Assuming you killed normally" + ) + else: + self.taken.append(str(self.board[i][self.enpassant])) + self.board[i][self.enpassant] = "" + + def castleInfo(self, piece, i, j, ii, jj): + if piece in self.castleable: + if piece[1] == "w": + if [KWPOS, CASTLEQWPOS] == [ + [i, j], + [ii, jj], + ] and "rqw" in self.castleable: + return "rqw" + if [KWPOS, CASTLEKWPOS] == [ + [i, j], + [ii, jj], + ] and "rkw" in self.castleable: + return "rkw" + elif piece[1] == "b": + if [KBPOS, CASTLEQBPOS] == [ + [i, j], + [ii, jj], + ] and "rqb" in self.castleable: + return "rqb" + if [KBPOS, CASTLEKBPOS] == [ + [i, j], + [ii, jj], + ] and "rkb" in self.castleable: + return "rkb" + return "" + + def getRookReach(self, i, j, color): + a = 1 + pos = [] + while a: + f = self.isFree(i, j + a, color) + if f: + pos.append([i, j + a]) + a += 1 + if f == 2: + break + else: + break + a = 1 + while True: + f = self.isFree(i, j - a, color) + if f: + pos.append([i, j - a]) + a += 1 + if f == 2: + break + else: + break + a = 1 + while True: + f = self.isFree(i + a, j, color) + if f: + pos.append([i + a, j]) + a += 1 + if f == 2: + break + else: + break + a = 1 + while True: + f = self.isFree(i - a, j, color) + if f: + pos.append([i - a, j]) + a += 1 + if f == 2: + break + else: + break + return pos + + def getBishopReach(self, i, j, color): + a = 1 + pos = [] + while True: + f = self.isFree(i + a, j + a, color) + if f: + pos.append([i + a, j + a]) + a += 1 + if f == 2: + break + else: + break + a = 1 + while True: + f = self.isFree(i - a, j - a, color) + if f: + pos.append([i - a, j - a]) + a += 1 + if f == 2: + break + else: + break + a = 1 + while True: + f = self.isFree(i + a, j - a, color) + if f: + pos.append([i + a, j - a]) + a += 1 + if f == 2: + break + else: + break + a = 1 + while True: + f = self.isFree(i - a, j + a, color) + if f: + pos.append([i - a, j + a]) + a += 1 + if f == 2: + break + else: + break + return pos + + def getReachablePosition(self, i, j): + c = self.board[i][j] + pos = [] + if c == "": + return [] + color = c[1] + if c == "pb": + if self.isFree(i + 1, j, "b") == 1: + pos.append([i + 1, j]) + if self.isFree(i + 1, j + 1, "b") == 2: + pos.append([i + 1, j + 1]) + if self.isFree(i + 1, j - 1, "b") == 2: + pos.append([i + 1, j - 1]) + if i == 1 and self.isFree(i + 2, j) == 1 and self.isFree(i + 1, j) == 1: + pos.append([i + 2, j]) + if ( + self.enpassant != -1 + and i == 4 + and (j == self.enpassant - 1 or j == self.enpassant + 1) + ): + pos.append([i + 1, self.enpassant]) + elif c == "pw": + if self.isFree(i - 1, j, "w") == 1: + pos.append([i - 1, j]) + if self.isFree(i - 1, j + 1, "w") == 2: + pos.append([i - 1, j + 1]) + if self.isFree(i - 1, j - 1, "w") == 2: + pos.append([i - 1, j - 1]) + if i == 6 and self.isFree(i - 2, j) == 1 and self.isFree(i - 1, j) == 1: + + pos.append([i - 2, j]) + if ( + self.enpassant != -1 + and i == 3 + and (j == self.enpassant - 1 or j == self.enpassant + 1) + ): + pos.append([i - 1, self.enpassant]) + elif c[0] == "k": + pos.append([i, j + 1]) + pos.append([i, j - 1]) + pos.append([i + 1, j + 1]) + pos.append([i + 1, j - 1]) + pos.append([i + 1, j]) + pos.append([i - 1, j + 1]) + pos.append([i - 1, j - 1]) + pos.append([i - 1, j]) + if c in self.castleable: + if c[1] == "w": + if "rqw" in self.castleable: + if ( + self.isFree(7, 1) == 1 + and self.isFree(7, 2) == 1 + and self.isFree(7, 3) == 1 + ): + + pos.append([7, 2]) + if "rkw" in self.castleable: + if self.isFree(7, 6) == 1 and self.isFree(7, 5) == 1: + pos.append([7, 6]) + elif c[1] == "b": + if "rqb" in self.castleable: + if ( + self.isFree(0, 1) == 1 + and self.isFree(0, 2) == 1 + and self.isFree(0, 3) == 1 + ): + pos.append([0, 2]) + if "rkb" in self.castleable: + if self.isFree(0, 6) == 1 and self.isFree(0, 5) == 1: + pos.append([0, 6]) + elif c[0] == "n": + pos.append([i + 1, j + 2]) + pos.append([i + 1, j - 2]) + pos.append([i - 1, j + 2]) + pos.append([i - 1, j - 2]) + pos.append([i + 2, j + 1]) + pos.append([i + 2, j - 1]) + pos.append([i - 2, j + 1]) + pos.append([i - 2, j - 1]) + elif c[0] == "r": + pos = self.getRookReach(i, j, color) + elif c[0] == "b": + pos = self.getBishopReach(i, j, color) + elif c[0] == "q": + # rook + pos.extend(self.getRookReach(i, j, color)) + # bishop + pos.extend(self.getBishopReach(i, j, color)) + res = [] + for p in pos: + if self.isFree(p[0], p[1], color): + res.append(p) + return list(res) + + def getPossiblePosition(self, i, j): + c = self.board[i][j] + pos = [] + if c == "": + return [] + color = c[1] + if c == "pb": + pos.append([i + 1, j]) + if self.isFree(i + 1, j + 1, "b") == 2: + pos.append([i + 1, j + 1]) + if self.isFree(i + 1, j - 1, "b") == 2: + pos.append([i + 1, j - 1]) + if i == 1 and self.isFree(i + 1, j) == 1: + pos.append([i + 2, j]) + if ( + self.enpassant != -1 + and i == 4 + and (j == self.enpassant - 1 or j == self.enpassant + 1) + ): + pos.append([i + 1, self.enpassant]) + elif c == "pw": + pos.append([i - 1, j]) + if self.isFree(i - 1, j + 1, "w") == 2: + pos.append([i - 1, j + 1]) + if self.isFree(i - 1, j - 1, "w") == 2: + pos.append([i - 1, j - 1]) + if i == 6 and self.isFree(i - 1, j) == 1: + pos.append([i - 2, j]) + if ( + self.enpassant != -1 + and i == 3 + and (j == self.enpassant - 1 or j == self.enpassant + 1) + ): + pos.append([i - 1, self.enpassant]) + elif c[0] == "k": + pos.append([i, j + 1]) + pos.append([i, j - 1]) + pos.append([i + 1, j + 1]) + pos.append([i + 1, j - 1]) + pos.append([i + 1, j]) + pos.append([i - 1, j + 1]) + pos.append([i - 1, j - 1]) + pos.append([i - 1, j]) + if c in self.castleable: + if c[1] == "w": + if "rqw" in self.castleable: + pos.append([7, 2]) + if "rkw" in self.castleable: + pos.append([7, 6]) + elif c[1] == "b": + if "rqb" in self.castleable: + pos.append([0, 2]) + if "rkb" in self.castleable: + pos.append([0, 6]) + elif c[0] == "n": + pos.append([i + 1, j + 2]) + pos.append([i + 1, j - 2]) + pos.append([i - 1, j + 2]) + pos.append([i - 1, j - 2]) + pos.append([i + 2, j + 1]) + pos.append([i + 2, j - 1]) + pos.append([i - 2, j + 1]) + pos.append([i - 2, j - 1]) + elif c[0] == "r": + for a in range(1, 8): + pos.append([i, j + a]) + pos.append([i + a, j]) + pos.append([i, j - a]) + pos.append([i - a, j]) + elif c[0] == "b": + for a in range(1, 8): + pos.append([i + a, j + a]) + pos.append([i - a, j + a]) + pos.append([i + a, j - a]) + pos.append([i - a, j - a]) + elif c[0] == "q": + for a in range(1, 8): + pos.append([i, j + a]) + pos.append([i + a, j]) + pos.append([i, j - a]) + pos.append([i - a, j]) + pos.append([i + a, j + a]) + pos.append([i - a, j + a]) + pos.append([i + a, j - a]) + pos.append([i - a, j - a]) + res = [] + for p in pos: + if self.isIn(*p): + res.append(p) + return list(res) + + def getClosest(self, i, j, ti, tj, reach): + d = 20 + res = None + if ti - i == 0: + di = 0 + elif ti - i < 0: + di = -1 + else: + di = 1 + if tj - j == 0: + dj = 0 + elif tj - j < 0: + dj = -1 + else: + dj = 1 + for a in range(7, 0, -1): + if [i + a * di, j + a * dj] in reach: + return [i + a * di, j + a * dj] + return None + + def move(self, i, j, ii, jj, color=None): + + # are given position inside the board ? + if not self.isIn(i, j) or not self.isIn(ii, jj): + return False, [], f"{i}{j} or {ii} {jj} position outside the board" + # is there a piece in the source square ? + if not self.board[i][j]: + return False, [], f"{i}{j} is empty" + # the player is trying to move a piece from the other player + if color and self.board[i][j][1] != color: + return ( + False, + [], + "({},{}) is not {}".format(i, j, "white" if color == "w" else "black"), + ) + if self.board[ii][jj] != "" and self.board[i][j][1] == self.board[ii][jj][1]: + # same color + return ( + False, + [], + f"({ii},{jj}) is occupied by your own {self.board[ii][jj]} piece", + ) + reach = self.getReachablePosition( + i, j + ) # actually possible destination (obstacles, ennemies) + pos = self.getPossiblePosition(i, j) # anything in the range of the piece + if [ii, jj] not in pos: + print(f"Possible positions {pos}") + return ( + False, + [], + f"({ii},{jj}) is not a {self.board[i][j]} possible position for some reason", + ) + elif [ii, jj] not in reach: + res = self.getClosest(i, j, ii, jj, reach) + if res: + ii, jj = res + else: + return False, [], f"{ii}{jj} is not reachable" + if self.board[ii][jj] != "": + self.taken.append(str(self.board[ii][jj])) + + # check if we killed in passant + self.takeEnPassant(i, j, ii, jj) + # reset enpassant value + self.enpassant = -1 + # the pawn jumped, set it as 'en passant' pawn + if self.board[i][j][0] == "p": + if self.board[i][j][1] == "b" and i == 1 and ii == 3: + self.enpassant = j + elif self.board[i][j][1] == "w" and i == 6 and ii == 4: + self.enpassant = j + + # replace destination with origin + self.board[ii][jj] = self.board[i][j] + self.board[i][j] = "" + + # if a pawn reached the end of the board, it becomse a queen + if self.board[ii][jj][0] == "p" and (ii == 0 or ii == 7): + self.board[ii][jj] = "q" + self.board[ii][jj][1] + + # if we were performing a castle, move the tower too + whichRock = self.castleInfo(self.board[ii][jj], i, j, ii, jj) + if whichRock == "rqb": + self.board[0][0] = "" + self.board[0][3] = "rb" + elif whichRock == "rkb": + self.board[0][7] = "" + self.board[0][5] = "rb" + elif whichRock == "rqw": + self.board[7][0] = "" + self.board[7][3] = "rw" + elif whichRock == "rkw": + self.board[7][7] = "" + self.board[7][5] = "rw" + # if k or r, castle for that piece forbidden in the future + if self.board[ii][jj][0] == "k" and self.board[ii][jj] in self.castleable: + self.castleable.remove(self.board[ii][jj]) + elif self.board[ii][jj][0] == "r": + if [i, j] == RQBPOS and "rqb" in self.castleable: + self.castleable.remove("rqb") + elif [i, j] == RKBPOS and "rkb" in self.castleable: + self.castleable.remove("rkb") + elif [i, j] == RQWPOS and "rqw" in self.castleable: + self.castleable.remove("rqw") + elif [i, j] == RKWPOS and "rkw" in self.castleable: + self.castleable.remove("rkw") + + # check if we have a winner + if "kb" in self.taken: + self.winner = "w" + elif "kw" in self.taken: + self.winner = "b" + + return True, [i, j, ii, jj], "OK" + + # save the state of the board for a given player (or full state) + def dump(self, color=None): + boardCopy = self.copy() + # hide other player + if color: + visibility = [[False for i in range(8)] for j in range(8)] + for i in range(8): + for j in range(8): + if boardCopy.board[i][j].endswith(color): + for di in range(-1, 2): + for dj in range(-1, 2): + if boardCopy.isIn(i + di, j + dj): + visibility[i + di][j + dj] = True + for i in range(8): + for j in range(8): + if not visibility[i][j]: + boardCopy.board[i][j] = "" + return boardCopy + + # TODO refactor string representation of board + # dump as a string to ease portability with other apps + def toString(self, color=None): + visibility = [[True for i in range(8)] for j in range(8)] + if color: # hide if necessary + visibility = [[False for i in range(8)] for j in range(8)] + for i in range(8): + for j in range(8): + if self.board[i][j].endswith(color): + for di in range(-1, 2): + for dj in range(-1, 2): + if self.isIn(i + di, j + dj): + visibility[i + di][j + dj] = True + boardStr = "" + for i in range(8): + for j in range(8): + if not visibility[i][j]: + boardStr += "_" # 3 spaces + else: + boardStr += self.board[i][j] + "_" + boardStr = boardStr[:-1] # remove last '_' + takenStr = "_".join(self.taken) + if color: + castleableStr = "_".join([e for e in self.castleable if e.endswith(color)]) + else: + castleableStr = "_".join([e for e in self.castleable]) + # todo only send enpassant if it's actually possible, otherwise we are leaking information + if color == "b" and visibility[4][self.enpassant] is False: + enpassantStr = str(-1) + elif color == "w" and visibility[3][self.enpassant] is False: + enpassantStr = str(-1) + else: + enpassantStr = str(self.enpassant) + if self.winner: + winnerStr = self.winner + else: + winnerStr = "n" + res = ( + boardStr + + "#" + + takenStr + + "#" + + castleableStr + + "#" + + enpassantStr + + "#" + + winnerStr + ) + return res + + # update whole state from a string + def updateFromString(self, data): + boardStr, takenStr, castleableStr, enpassantStr, winnerStr = data.split("#") + for i, c in enumerate(boardStr.split("_")): + self.board[i // 8][i % 8] = c + if takenStr == "": + self.taken = [] + else: + self.taken = takenStr.split("_") + if castleableStr == "": + self.castleable = [] + else: + self.castleable = castleableStr.split("_") + self.enpassant = int(enpassantStr) + if winnerStr == "n": + self.winner = None + else: + self.winner = winnerStr + + def updateFromBoard(self, board): + self.taken = list(board.taken) + self.board = [list(b) for b in board.board] + self.castleable = list(board.castleable) + self.enpassant = board.enpassant + self.winner = board.winner + + def dbpiece2boardpiece(self, piecechar): + color = "b" if piecechar.isupper() else "w" + piece = "" if piecechar == "_" else piecechar.lower() + color + return piece + + def apicastle2board(self): + return { + "L": "rqb", + "S": "rkb", + "K": "kb", + "l": "rqw", + "s": "rkw", + "k": "kw", + } + + def boardcastle2api(self): + return { + "rqb": "L", + "rkb": "S", + "kb": "K", + "rqw": "l", + "rkw": "s", + "kw": "k", + } + + # TODO check what winner str format is Board() expecting. winner "white" "black" "None" + def updateFromElements(self, board, taken, castleable, enpassant, winner): + for i, c in enumerate(board): + self.board[i // 8][i % 8] = self.dbpiece2boardpiece(c) + self.taken = [self.dbpiece2boardpiece(c) for c in taken] + self.castleable = sorted([self.apicastle2board()[c] for c in castleable]) + self.enpassant = ord(enpassant) - ord("a") if enpassant is not None else -1 + self.winner = winner + + def toElements(self, color=None): + def bpiece(p): + return "_" if not p else p[0] if p[1] == "w" else p[0].upper() + + if color: + boardString = self.toString(color) + pieces = boardString.split("#")[0].split("_") + apiboard = "".join([bpiece(p) for p in pieces]) + else: + apiboard = "".join([bpiece(p) for r in self.board for p in r]) + + elements = { + "castleable": "".join( + sorted([self.boardcastle2api()[c] for c in self.castleable]) + ), + "taken": "".join([bpiece(p) for p in self.taken]), + "board": apiboard, + } + return elements + + +if __name__ == "__main__": + board = Board() + + for i in range(8): + for j in range(8): + print("%4s" % board.board[i][j]) + print() + print(board.taken) + print(board.castleable) + print(board.winner) + + print(board.move(1, 2, 4, 2)) # invalid move + print(board.move(6, 2, 4, 2)) # valid move + + print("----------------") + + board.updateFromString(board.toString("w")) + + for i in range(8): + for j in range(8): + print("%4s" % board.board[i][j]) + print() + print(board.taken) + print(board.castleable) + print(board.winner) diff --git a/server/btchServer.py b/battlechess/core/__init__.py similarity index 100% rename from server/btchServer.py rename to battlechess/core/__init__.py diff --git a/core/btchBoard.py b/battlechess/core/btchBoard.py similarity index 71% rename from core/btchBoard.py rename to battlechess/core/btchBoard.py index d838436..20f6aa3 100644 --- a/core/btchBoard.py +++ b/battlechess/core/btchBoard.py @@ -5,17 +5,18 @@ # create a board for each player # using list of lists requires deepcopy -class BtchBoard(): - +class BtchBoard: def __init__(self, board=None): self.board = board or self.startBoard() - self.taken = '' - self.castleable = 'LSKlsk' # TODO change for 'LSls' - self.enpassant = None #column -> 2-9 + self.taken = "" + self.castleable = "LSKlsk" # TODO change for 'LSls' + self.enpassant = None # column -> 2-9 self.winner = None def startBoard(self): - board = [[None, None, *['' for j in range(8)], None, None] for i in range(0, 12)] + board = [ + [None, None, *["" for j in range(8)], None, None] for i in range(0, 12) + ] # yapf: disable board[0] = [None]*12 @@ -32,8 +33,8 @@ def startBoard(self): def reset(self): self.board = self.startBoard() - self.taken = '' - self.castleable = 'KLSkls' + self.taken = "" + self.castleable = "KLSkls" self.enpassant = None self.winner = None @@ -57,29 +58,34 @@ def factory(cls, boardstr: str): # board, taken, castleable, enpassant = boardstr.split("#") b = BtchBoard() for i, c in enumerate(boardstr): - b.board[i // 8 + 2][i % 8 + 2] = c if c != '_' else '' + b.board[i // 8 + 2][i % 8 + 2] = c if c != "_" else "" return b @classmethod - def factoryFromElements(cls, board: str, taken: str, castleable: str, enpassant: int): + def factoryFromElements( + cls, board: str, taken: str, castleable: str, enpassant: int + ): b = BtchBoard() for i, c in enumerate(board): - b.board[i // 8 + 2][i % 8 + 2] = c if c != '_' else '' + b.board[i // 8 + 2][i % 8 + 2] = c if c != "_" else "" b.taken = taken b.castleable = castleable b.enpassant = enpassant return b def boardToStr(self): - def bpiece(p): - return '_' if not p else p + return "_" if not p else p - return ''.join([bpiece(self.board[i][j]) for i in range(2, 10) for j in range(2, 10)]) + return "".join( + [bpiece(self.board[i][j]) for i in range(2, 10) for j in range(2, 10)] + ) def prettyBoard(self): boardstr = self.boardToStr() - return '\n'.join([boardstr[index:index + 8] for index in range(0, len(boardstr), 8)]) + return "\n".join( + [boardstr[index : index + 8] for index in range(0, len(boardstr), 8)] + ) def toElements(self): @@ -88,12 +94,14 @@ def toElements(self): "taken": self.taken, "board": self.boardToStr(), "enpassant": self.enpassant, - "winner": self.winner + "winner": self.winner, } return elements def empty(self): - self.board = [[None, None, *['' for j in range(8)], None, None] for i in range(0, 12)] + self.board = [ + [None, None, *["" for j in range(8)], None, None] for i in range(0, 12) + ] self.board[0] = [None] * 12 self.board[1] = [None] * 12 self.board[10] = [None] * 12 @@ -107,27 +115,39 @@ def isIn(self, i, j): @staticmethod def isWhite(color): - return color == 'white' + return color == "white" @staticmethod def isBlack(color): - return color == 'black' + return color == "black" @staticmethod def getColor(c): - return 'black' if c.isupper() else 'white' if c.islower() else None + return "black" if c.isupper() else "white" if c.islower() else None def isFree(self, i, j): - return self.board[i][j] == '' + return self.board[i][j] == "" @staticmethod def isEnemy(color, piece): - return True if piece and (piece.isupper() and color == 'white' \ - or piece.islower() and color == 'black') else False + return ( + True + if piece + and ( + piece.isupper() + and color == "white" + or piece.islower() + and color == "black" + ) + else False + ) def extendboard(self, boardStr): - return "_" * 10 + "".join(["_" + boardStr[j * 8:(j + 1) * 8] + "_" for j in range(8) - ]) + "_" * 10 + return ( + "_" * 10 + + "".join(["_" + boardStr[j * 8 : (j + 1) * 8] + "_" for j in range(8)]) + + "_" * 10 + ) def shrinkboard(self): return [[self.board[i][j] for j in range(2, 10)] for i in range(2, 10)] @@ -137,7 +157,7 @@ def hasEnemy(self, i, j): for jj in [j - 1, j, j + 1]: for ii in [i - 1, i, i + 1]: cc = self.board[ii][jj] - if cc == '' or cc is None: + if cc == "" or cc is None: continue elif c.isupper() and cc.islower(): return True @@ -146,26 +166,28 @@ def hasEnemy(self, i, j): def filterSquare(self, color, i, j): c = self.board[i][j] - if c is None or c == '' or self.hasEnemy(i, j): + if c is None or c == "" or self.hasEnemy(i, j): return c - elif color == 'black': - return c if c.isupper() else '' - elif color == 'white': - return c if c.islower() else '' + elif color == "black": + return c if c.isupper() else "" + elif color == "white": + return c if c.islower() else "" else: print(f"{color} is not a color") return None # TODO we could filter the shrunk board if speed is an issue def filterBoard(self, color): - return [[self.filterSquare(color, i, j) for j in range(0, 12)] for i in range(0, 12)] + return [ + [self.filterSquare(color, i, j) for j in range(0, 12)] for i in range(0, 12) + ] def filterCastleable(self, color): - if color == 'black': - self.castleable = ''.join(c for c in self.castleable if c.isupper()) + if color == "black": + self.castleable = "".join(c for c in self.castleable if c.isupper()) # self.move = self.move if not self.move_number % 2 else None - elif color == 'white': - self.castleable = ''.join(c for c in self.castleable if c.islower()) + elif color == "white": + self.castleable = "".join(c for c in self.castleable if c.islower()) # self.move = self.move if self.move_number % 2 else None def filterEnpassant(self, color): @@ -197,22 +219,22 @@ def possibleMoves(self, color, i, j): moves = list() - if c.lower() == 'p': + if c.lower() == "p": moves = list(self.pawnMoves(color, i, j)) - elif c.lower() == 'r': + elif c.lower() == "r": moves = list(self.rookMoves(color, i, j)) - elif c.lower() == 'n': + elif c.lower() == "n": moves = list(self.knightMoves(color, i, j)) - elif c.lower() == 'b': + elif c.lower() == "b": moves = list(self.bishopMoves(color, i, j)) - elif c.lower() == 'q': + elif c.lower() == "q": moves = list(self.queenMoves(color, i, j)) - elif c.lower() == 'k': + elif c.lower() == "k": moves = list(self.kingMoves(color, i, j)) else: - print('Unknown piece {}'.format(c)) + print("Unknown piece {}".format(c)) - print('Possible moves {} at {}, {}: {}'.format(c, i, j, moves)) + print("Possible moves {} at {}, {}: {}".format(c, i, j, moves)) moves.sort() @@ -264,8 +286,8 @@ def kingMoves(self, color, i, j): if self.isEnemy(color, self.board[ii][jj]): yield (ii, jj) - #castles - k, l, s = 'KLS' if self.isBlack(color) else 'kls' + # castles + k, l, s = "KLS" if self.isBlack(color) else "kls" r = 2 if self.isBlack(color) else 9 if k in self.castleable: if l in self.castleable: @@ -276,10 +298,22 @@ def kingMoves(self, color, i, j): yield (r, 8) def knightMoves(self, color, i, j): - deltas = [(2, 1), (2, -1), (-2, 1), (-2, -1), (1, 2), (-1, 2), (1, -2), (-1, -2)] - return [(i + di, j + dj) - for di, dj in deltas - if self.isFree(i + di, j + dj) or self.isEnemy(color, self.board[i + di][j + dj])] + deltas = [ + (2, 1), + (2, -1), + (-2, 1), + (-2, -1), + (1, 2), + (-1, 2), + (1, -2), + (-1, -2), + ] + return [ + (i + di, j + dj) + for di, dj in deltas + if self.isFree(i + di, j + dj) + or self.isEnemy(color, self.board[i + di][j + dj]) + ] def pawnMoves(self, color, i, j): di = -1 if self.isWhite(color) else +1 @@ -289,9 +323,12 @@ def pawnMoves(self, color, i, j): yield (i + di, j) # 2-square move - if i == (8 if self.isWhite(color) else 3) \ - and self.isFree(i + di, j) and self.isFree(i + 2*di, j): - yield (i + 2 * di, j) + if ( + i == (8 if self.isWhite(color) else 3) + and self.isFree(i + di, j) + and self.isFree(i + 2 * di, j) + ): + yield (i + 2 * di, j) # kill if self.isEnemy(color, self.board[i + di][j - 1]): diff --git a/server/__init__.py b/battlechess/server/__init__.py similarity index 100% rename from server/__init__.py rename to battlechess/server/__init__.py diff --git a/server/btchApi.py b/battlechess/server/btchApi.py similarity index 50% rename from server/btchApi.py rename to battlechess/server/btchApi.py index 4f89382..41dfe59 100644 --- a/server/btchApi.py +++ b/battlechess/server/btchApi.py @@ -1,19 +1,28 @@ -from datetime import datetime, timedelta, timezone -from typing import Optional, Tuple, Set, List - -from sqlalchemy.orm import Session - -from fastapi import Depends, FastAPI, HTTPException, status, Body, File, UploadFile, Header -from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +import asyncio +import time +from datetime import timedelta +from typing import List, Union + +from fastapi import ( + Depends, + FastAPI, + File, + Header, + HTTPException, + Query, + UploadFile, + status, +) from fastapi.middleware.cors import CORSMiddleware +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from jose import JWTError, jwt -from pydantic import BaseModel -from sqlalchemy.sql.functions import user +from sqlalchemy.orm import Session +from typing_extensions import Annotated -from .config import SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES -from . import crud, models, schemas -from .schemas import Game, GameStatus -from .btchApiDB import SessionLocal, engine +from battlechess.server import crud, models, schemas +from battlechess.server.btchApiDB import SessionLocal, engine +from battlechess.server.config import ACCESS_TOKEN_EXPIRE_MINUTES, ALGORITHM, SECRET_KEY +from battlechess.server.schemas import GameStatus PASSWORD_MIN_LENGTH = 3 AVATAR_MAX_SIZE = 100000 @@ -43,10 +52,14 @@ def get_db(): finally: db.close() + def valid_content_length(content_length: int = Header(..., lt=AVATAR_MAX_SIZE)): return content_length -def get_current_user(db: Session = Depends(get_db), token: str = Depends(oauth2_scheme)): + +def get_current_user( + db: Session = Depends(get_db), token: str = Depends(oauth2_scheme) +): credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", @@ -68,25 +81,31 @@ def get_current_user(db: Session = Depends(get_db), token: str = Depends(oauth2_ def get_current_active_user(current_user: schemas.User = Depends(get_current_user)): if not current_user.is_active(): - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user" + ) return current_user -def get_game(gameUUID, - current_user: schemas.User = Depends(get_current_active_user), - db: Session = Depends(get_db)): +def get_game( + gameUUID, + current_user: schemas.User = Depends(get_current_active_user), + db: Session = Depends(get_db), +): # TODO check if public and owner/player game = crud.get_game_by_uuid(db, gameUUID) return game -def set_player(game: models.Game, - current_user: schemas.User = Depends(get_current_active_user), - db: Session = Depends(get_db)): +def set_player( + game: models.Game, + current_user: schemas.User = Depends(get_current_active_user), + db: Session = Depends(get_db), +): game.set_player(current_user) - # if all players are there, start - if game.is_full(): + # if all players are there and game is waiting, start + if game.is_waiting() and game.is_full(): crud.create_default_snap(db, current_user, game) game.start_game() @@ -98,12 +117,13 @@ def set_player(game: models.Game, @app.get("/version") def version(): - return {'version': "1.0"} + return {"version": "1.0"} @app.post("/token", response_model=schemas.Token) -def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), - db: Session = Depends(get_db)): +def login_for_access_token( + form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db) +): user = crud.authenticate_user(db, form_data.username, form_data.password) if not user: raise HTTPException( @@ -113,29 +133,33 @@ def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), ) access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) access_token = crud.create_access_token( - data={"sub": user.username}, expires_delta=access_token_expires) + data={"sub": user.username}, expires_delta=access_token_expires + ) return {"access_token": access_token, "token_type": "bearer"} -@app.get("/users/me/", response_model=schemas.User) +@app.get("/users/me", response_model=schemas.User) def read_users_me(current_user: schemas.User = Depends(get_current_active_user)): return current_user -@app.get("/users/me/games/", response_model=List[schemas.Game]) -def read_own_games(current_user: schemas.User = Depends(get_current_active_user), - db: Session = Depends(get_db)): +@app.get("/users/me/games", response_model=List[schemas.Game]) +def read_own_games( + current_user: schemas.User = Depends(get_current_active_user), + db: Session = Depends(get_db), +): print("read own games") games = crud.get_games_by_player(db, current_user) - print(f'{games}') return games -@app.get("/users/", response_model=List[schemas.User]) -def read_users(skip: int = 0, - limit: int = 100, - current_user: schemas.User = Depends(get_current_active_user), - db: Session = Depends(get_db)): +@app.get("/users", response_model=List[schemas.User]) +def read_users_all( + skip: int = 0, + limit: int = 100, + current_user: schemas.User = Depends(get_current_active_user), + db: Session = Depends(get_db), +): users = crud.get_users(db, skip=skip, limit=limit) return users @@ -147,9 +171,11 @@ def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): @app.get("/users/u/{userID}", response_model=schemas.User) -def read__single_user(userID: int, - current_user: schemas.User = Depends(get_current_active_user), - db: Session = Depends(get_db)): +def read__single_user( + userID: int, + current_user: schemas.User = Depends(get_current_active_user), + db: Session = Depends(get_db), +): user = crud.get_user_by_id(db, userID) return user @@ -167,13 +193,16 @@ def read__single_user(userID: int, # return game -@app.post("/users/") +@app.post("/users") def create_user(new_user: schemas.UserCreate, db: Session = Depends(get_db)): - if not (3 <= len(new_user.username) <= 15 and - len(new_user.plain_password) >= PASSWORD_MIN_LENGTH): + if not ( + 3 <= len(new_user.username) <= 15 + and len(new_user.plain_password) >= PASSWORD_MIN_LENGTH + ): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail=f"username should be of lenght (3-15) and password at least {PASSWORD_MIN_LENGTH} chars.", + detail=f"""username should be of lenght (3-15) + and password at least {PASSWORD_MIN_LENGTH} chars.""", headers={"WWW-Authenticate": "Bearer"}, ) if new_user.email is None: @@ -192,15 +221,17 @@ def create_user(new_user: schemas.UserCreate, db: Session = Depends(get_db)): headers={"WWW-Authenticate": "Bearer"}, ) else: - db_user = crud.create_user(db, new_user) + _ = crud.create_user(db, new_user) return crud.get_user_by_username(db, new_user.username) # TODO fix the put method for user @app.put("/users/update") -def update_user(updated_user: schemas.User, - current_user: schemas.User = Depends(get_current_active_user), - db: Session = Depends(get_db)): +def update_user( + updated_user: schemas.User, + current_user: schemas.User = Depends(get_current_active_user), + db: Session = Depends(get_db), +): if updated_user.email is None: updated_user.email = "" @@ -216,9 +247,11 @@ def update_user(updated_user: schemas.User, # first a default avatar is created, so the avatar is always updated @app.put("/users/u/{userID}/avatar", dependencies=[Depends(valid_content_length)]) -def update_avatar_file(file: UploadFile = File(...), - current_user: schemas.User = Depends(get_current_active_user), - db: Session = Depends(get_db)): +def update_avatar_file( + file: UploadFile = File(...), + current_user: schemas.User = Depends(get_current_active_user), + db: Session = Depends(get_db), +): # TODO check that file is sane before saving @@ -235,43 +268,77 @@ def update_avatar_file(file: UploadFile = File(...), output = crud.create_avatar_file(db, current_user, file) except Exception as err: raise HTTPException( - detail=f'{err} encountered while uploading {file.filename}', - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY) + detail=f"{err} encountered while uploading {file.filename}", + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + ) finally: file.file.close() return {"filename": output} -@app.post("/games/") -def post_new_game(new_game: schemas.GameCreate, - current_user: schemas.User = Depends(get_current_active_user), - db: Session = Depends(get_db)): - return crud.create_game(db, current_user, new_game) +@app.post("/games") +def post_new_game( + new_game: schemas.GameCreate, + current_user: schemas.User = Depends(get_current_active_user), + db: Session = Depends(get_db), +): + game = crud.create_game(db, current_user, new_game) + return game -@app.get("/games/{gameUUID}") -def get_game_by_uuid(gameUUID: str, - current_user: schemas.User = Depends(get_current_active_user), - db: Session = Depends(get_db)): - return crud.get_game_by_uuid(db, gameUUID) +@app.get("/games/{gameUUID}", response_model=schemas.Game) +def get_game_by_uuid( + gameUUID: str, + current_user: schemas.User = Depends(get_current_active_user), + db: Session = Depends(get_db), +): + game = crud.get_game_by_uuid(db, gameUUID) + if not game: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"game {gameUUID} not found", + headers={"WWW-Authenticate": "Bearer"}, + ) + return game + + +# TODO test and should it be a pydantic GameStatus? +@app.get("/games/{gameUUID}/status", response_model=str) +def get_game_status_by_uuid( + gameUUID: str, + current_user: schemas.User = Depends(get_current_active_user), + db: Session = Depends(get_db), +): + game = crud.get_game_by_uuid(db, gameUUID) + if not game: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"game {gameUUID} not found", + headers={"WWW-Authenticate": "Bearer"}, + ) + return game.status # lists available games @app.get("/games", response_model=List[schemas.Game]) -def list_available_games(status_filter: str = GameStatus.WAITING, - current_user: schemas.User = Depends(get_current_active_user), - db: Session = Depends(get_db)): - games = crud.get_public_game_by_status(db, current_user, status_filter) +def get_available_games( + status: Annotated[Union[List[str], None], Query()] = None, + current_user: schemas.User = Depends(get_current_active_user), + db: Session = Depends(get_db), +): + games = crud.get_games_by_status(db, current_user, status) return games -#TODO should be patch +# TODO should be patch # joines an existing game. error when game already started -@app.get("/games/{gameUUID}/join") -def join_game(gameUUID: str, - current_user: schemas.User = Depends(get_current_active_user), - db: Session = Depends(get_db)): +@app.get("/games/{gameUUID}/join", response_model=schemas.Game) +def join_game( + gameUUID: str, + current_user: schemas.User = Depends(get_current_active_user), + db: Session = Depends(get_db), +): game = get_game(gameUUID, current_user, db) if not game: raise HTTPException( @@ -304,45 +371,130 @@ def join_game(gameUUID: str, # return game -# either creates a new game or joins an existing unstarted random game. Random games can not be joined via "join_game". -@app.patch("/games") -def join_random_game(current_user: schemas.User = Depends(get_current_active_user), - db: Session = Depends(get_db)): +# either creates a new game or joins an existing unstarted random game. +# Random games can not be joined via "join_game". +@app.patch("/games", response_model=schemas.Game) +def join_random_game( + current_user: schemas.User = Depends(get_current_active_user), + db: Session = Depends(get_db), +): game = crud.get_random_public_game_waiting(db, current_user) if not game: - return {} + print("random game not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="available random game not found", + headers={"Authorization": "Bearer"}, + ) game = set_player(game, current_user, db) db.refresh(game) - + print(game.owner) return game # serialized board state @app.get("/games/{gameUUID}/board") -def query_board(gameUUID: str, - current_user: schemas.User = Depends(get_current_active_user), - db: Session = Depends(get_db)): +def query_board( + gameUUID: str, + current_user: schemas.User = Depends(get_current_active_user), + db: Session = Depends(get_db), +): pass +@app.get("/games/{gameUUID}/turn/me") +async def query_me_turn( + gameUUID: str, + current_user: schemas.User = Depends(get_current_active_user), + db: Session = Depends(get_db), + long_polling: bool = False, +): + game = get_game(gameUUID, current_user, db) + + if not game: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="game not found", + headers={"Authorization": "Bearer"}, + ) + + if game.get_player_color(current_user.id) is None: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"{current_user.username} is not a player of game {gameUUID}", + headers={"Authorization": "Bearer"}, + ) + + if game.is_finished(): + raise HTTPException( + status_code=status.HTTP_412_PRECONDITION_FAILED, + detail="game is over", + headers={"Authorization": "Bearer"}, + ) + + if not long_polling: + return game.turn == game.get_player_color(current_user.id) + + game = get_game(gameUUID, current_user, db) + start = time.time() + while True: + elapsed = time.time() - start + db.refresh(game) + caller_turn = game.turn == game.get_player_color(current_user.id) + print( + f"username: {current_user.username} game: {game.__dict__} and caller_turn {caller_turn}" + ) + if caller_turn or elapsed >= 10: + return caller_turn + else: + await asyncio.sleep(2) + + # who's turn is it (None means that the game is over) @app.get("/games/{gameUUID}/turn") -def query_turn(gameUUID: str, - current_user: schemas.User = Depends(get_current_active_user), - db: Session = Depends(get_db)): +async def query_turn( + gameUUID: str, + current_user: schemas.User = Depends(get_current_active_user), + db: Session = Depends(get_db), +): game = get_game(gameUUID, current_user, db) + + if not game: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="game not found", + headers={"Authorization": "Bearer"}, + ) + + if game.get_player_color(current_user.id) is None: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"{current_user.username} is not a player of game {gameUUID}", + headers={"Authorization": "Bearer"}, + ) + + if game.is_finished(): + raise HTTPException( + status_code=status.HTTP_412_PRECONDITION_FAILED, + detail="game is over", + headers={"Authorization": "Bearer"}, + ) + return game.turn -@app.post("/games/{gameUUID}/move") +# TODO we're not checking it's the request's player turn LOL +@app.post("/games/{gameUUID}/move", response_model=schemas.GameSnap) def post_move( gameUUID: str, - #move: dict = Body(...), # or pydantic or query parameter? Probably pydantic to make clear what a move is + # or pydantic or query parameter? Probably pydantic to make clear what a move is + # move: dict = Body(...), gameMove: schemas.GameMove, current_user: schemas.User = Depends(get_current_active_user), - db: Session = Depends(get_db)): + db: Session = Depends(get_db), +): game = get_game(gameUUID, current_user, db) if not game: raise HTTPException( @@ -358,9 +510,17 @@ def post_move( headers={"Authorization": "Bearer"}, ) + player_color = game.get_player_color(current_user.id) + if game.turn != player_color: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"It's {game.turn} turn and you're {player_color}", + headers={"Authorization": "Bearer"}, + ) + # It looks like modifying the pydantic model does not change the db model snap = crud.create_snap_by_move(db, current_user, game, gameMove) - snap4player = schemas.GameSnap.from_orm(snap) + snap4player = schemas.GameSnap.model_validate(snap) snap4player.prepare_for_player(game.get_player_color(current_user.id)) return snap4player @@ -368,10 +528,12 @@ def post_move( # TODO List[str] might throw ValidationError: # due to @app.get("/games/{gameUUID}/moves/{square}", response_model=List[str]) -def get_moves(gameUUID: str, - square: str, - current_user: schemas.User = Depends(get_current_active_user), - db: Session = Depends(get_db)): +def get_moves( + gameUUID: str, + square: str, + current_user: schemas.User = Depends(get_current_active_user), + db: Session = Depends(get_db), +): game = get_game(gameUUID, current_user, db) if not game: raise HTTPException( @@ -388,7 +550,7 @@ def get_moves(gameUUID: str, ) # TODO pydantic square validation possible? - if len(square) != 2 or square < 'a1' or 'h8' < square: + if len(square) != 2 or square < "a1" or "h8" < square: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="square is not of format ['a1', 'h8'] ", @@ -401,24 +563,49 @@ def get_moves(gameUUID: str, # TODO remove this if we're happy with a weird validation error message if moves: - assert type(moves[0]) == str + assert isinstance(moves[0], str) + return moves -@app.get("/games/{gameUUID}/snap") -def get_snap(gameUUID: str, - current_user: schemas.User = Depends(get_current_active_user), - db: Session = Depends(get_db)): +@app.get("/games/{gameUUID}/snap", response_model=schemas.GameSnap) +def get_snap( + gameUUID: str, + current_user: schemas.User = Depends(get_current_active_user), + db: Session = Depends(get_db), +): game = get_game(gameUUID, current_user, db) # user not allowed to query that game snap for now - if (game.status != GameStatus.OVER) and (current_user.id not in [game.white_id, game.black_id]): - game = None if not game: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="game not found", headers={"Authorization": "Bearer"}, ) + + if (game.status != GameStatus.OVER) and ( + current_user.id not in [game.white_id, game.black_id] + ): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="game not found", + headers={"Authorization": "Bearer"}, + ) + + if game.is_waiting(): + raise HTTPException( + status_code=status.HTTP_412_PRECONDITION_FAILED, + detail="a waiting game has no snaps", + headers={"Authorization": "Bearer"}, + ) + + if len(game.snaps) == 0: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="snap not found", + headers={"Authorization": "Bearer"}, + ) + snap = game.snaps[-1] if not snap: @@ -432,21 +619,26 @@ def get_snap(gameUUID: str, return snap player_color = "black" if current_user.id == game.black_id else "white" - snap4player = schemas.GameSnap.from_orm(snap) - print(f'preparing board for {current_user.username} {player_color}') - snap4player.prepare_for_player(player_color) + snap4player = schemas.GameSnap.model_validate(snap) + if game.status != GameStatus.OVER: + print(f"preparing board for {current_user.username} {player_color}") + snap4player.prepare_for_player(player_color) return snap4player -@app.get("/games/{gameUUID}/snap/{moveNum}") -def get_snap(gameUUID: str, - moveNum: int, - current_user: schemas.User = Depends(get_current_active_user), - db: Session = Depends(get_db)): +@app.get("/games/{gameUUID}/snap/{moveNum}", response_model=schemas.GameSnap) +def get_snap_by_move( + gameUUID: str, + moveNum: int, + current_user: schemas.User = Depends(get_current_active_user), + db: Session = Depends(get_db), +): game = get_game(gameUUID, current_user, db) # user not allowed to query that game snap for now - if (game.status != GameStatus.OVER) and (current_user.id not in [game.white_id, game.black_id]): + if (game.status != GameStatus.OVER) and ( + current_user.id not in [game.white_id, game.black_id] + ): game = None if not game: raise HTTPException( @@ -462,19 +654,28 @@ def get_snap(gameUUID: str, headers={"Authorization": "Bearer"}, ) - player_color = "black" if current_user.id == game.black_id else "white" - snap4player = schemas.GameSnap.from_orm(snap) - snap4player.prepare_for_player(player_color) + print(f"check if we need to prepare snap {game.status}") + + snap4player = schemas.GameSnap.model_validate(snap) + if game.status != GameStatus.OVER: + player_color = "black" if current_user.id == game.black_id else "white" + snap4player.prepare_for_player(player_color) + else: + print("game is over and snap is", snap4player) return snap4player -@app.get("/games/{gameUUID}/snaps") -def get_snaps(gameUUID: str, - current_user: schemas.User = Depends(get_current_active_user), - db: Session = Depends(get_db)): +@app.get("/games/{gameUUID}/snaps", response_model=List[schemas.GameSnap]) +def get_snaps( + gameUUID: str, + current_user: schemas.User = Depends(get_current_active_user), + db: Session = Depends(get_db), +): game = get_game(gameUUID, current_user, db) # user not allowed to query that game snap for now - if (game.status != GameStatus.OVER) and (current_user.id not in [game.white_id, game.black_id]): + if (game.status != GameStatus.OVER) and ( + current_user.id not in [game.white_id, game.black_id] + ): game = None if not game: raise HTTPException( @@ -487,7 +688,8 @@ def get_snaps(gameUUID: str, result = [] for snap in game.snaps: - snap4player = schemas.GameSnap.from_orm(snap) - snap4player.prepare_for_player(player_color) + snap4player = schemas.GameSnap.model_validate(snap) + if game.status != GameStatus.OVER: + snap4player.prepare_for_player(player_color) result.append(snap4player) return result diff --git a/server/btchApiDB.py b/battlechess/server/btchApiDB.py similarity index 79% rename from server/btchApiDB.py rename to battlechess/server/btchApiDB.py index c7a40c6..485aed2 100644 --- a/server/btchApiDB.py +++ b/battlechess/server/btchApiDB.py @@ -1,9 +1,8 @@ from sqlalchemy import create_engine -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import declarative_base, sessionmaker +from battlechess.server.config import SQLALCHEMY_DATABASE_URL - -SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db" +# SQLALCHEMY_DATABASE_URL = "sqlite:///./btchdb.sqlite" # SQLALCHEMY_DATABASE_URL = 'sqlite:///:memory:' # doesn't work # SQLALCHEMY_DATABASE_URL = "postgresql://user:password@postgresserver/db" @@ -24,4 +23,4 @@ def __enter__(self): return self.db def __exit__(self, exc_type, exc_value, traceback): - self.db.close() \ No newline at end of file + self.db.close() diff --git a/server/btchDB.py b/battlechess/server/btchDB.py similarity index 74% rename from server/btchDB.py rename to battlechess/server/btchDB.py index 92004e4..76681b2 100644 --- a/server/btchDB.py +++ b/battlechess/server/btchDB.py @@ -1,61 +1,67 @@ from __future__ import print_function -import sqlite3 + import os +import sqlite3 + class btchDB: - + BTCH_DB_PATH = "btch.db" PENDING = 0 ACCEPTED = 1 DECLINED = 2 MAXINVITE = 5 - + def __init__(self, path=None): self.connection = None self.cursor = None if path == None: self.path = BTCH_DB_PATH - else : + else: self.path = path if not os.path.exists(self.path): self.startEditing() self.create() self.stopEditing() - + def startEditing(self): if self.connection == None or self.cursor == None: self.connection = sqlite3.connect(self.path) self.cursor = self.connection.cursor() - + def stopEditing(self): if self.connection == None or self.cursor == None: print("warning, trying to close a connection to sqlite when none was open") - else : + else: self.connection.commit() self.connection.close() self.connection = None self.cursor = None - - + def execute(self, command, params=None): if self.connection == None or self.cursor == None: - print("warning, trying to execute a command without valid sqlite connection") + print( + "warning, trying to execute a command without valid sqlite connection" + ) elif params != None: self.cursor.execute(command, params) - else : + else: self.cursor.execute(command) - + def create(self): # Create table - self.execute('''CREATE TABLE users + self.execute( + """CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, login TEXT UNIQUE NOT NULL, pass TEXT NOT NULL, signup DATE DEFAULT (DATETIME('now')), active INTEGER DEFAULT 1, - email TEXT DEFAULT '')''') + email TEXT DEFAULT '')""" + ) - self.execute('''CREATE TABLE games + self.execute( + """CREATE TABLE games (id INTEGER PRIMARY KEY AUTOINCREMENT, idP1 INTEGER NOT NULL, idP2 INTEGER NOT NULL, @@ -65,17 +71,21 @@ def create(self): finished INTEGER DEFAULT 0, FOREIGN KEY(idP1) REFERENCES users(id), FOREIGN KEY(idP2) REFERENCES users(id) - )''') + )""" + ) - self.execute('''CREATE TABLE gamestates + self.execute( + """CREATE TABLE gamestates (id INTEGER PRIMARY KEY AUTOINCREMENT, idGame INTEGER NOT NULL, state TEXT NOT NULL, date DATE DEFAULT (DATETIME('now')), FOREIGN KEY(idGame) REFERENCES games(id) - )''') + )""" + ) - self.execute('''CREATE TABLE invitations + self.execute( + """CREATE TABLE invitations (id INTEGER PRIMARY KEY AUTOINCREMENT, idP1 INTEGER, idP2 INTEGER, @@ -85,16 +95,17 @@ def create(self): status INTEGER DEFAULT 0, FOREIGN KEY(idP1) REFERENCES users(id), FOREIGN KEY(idP2) REFERENCES users(id) - )''') + )""" + ) def clearAll(self): self.execute("DELETE FROM users") self.execute("DELETE FROM games") self.execute("DELETE FROM gamestates") self.execute("DELETE FROM invitations") - + def userExists(self, login): - self.execute('SELECT id FROM users WHERE login=?', (login,)) + self.execute("SELECT id FROM users WHERE login=?", (login,)) exists = len(self.cursor.fetchall()) != 0 return exists @@ -103,11 +114,14 @@ def newUser(self, login, password, email=None): return False if email == None: email = "" - self.execute("INSERT INTO users (login, pass, email) VALUES ('%s', '%s', '%s')"%(login, password, email)) + self.execute( + "INSERT INTO users (login, pass, email) VALUES ('%s', '%s', '%s')" + % (login, password, email) + ) return True def getUserId(self, login): - self.execute('SELECT id FROM users WHERE login=?', (login,)) + self.execute("SELECT id FROM users WHERE login=?", (login,)) result = self.cursor.fetchall() if len(result) != 1: return None @@ -115,7 +129,7 @@ def getUserId(self, login): return result[0][0] def checkPassword(self, login, password): - self.execute('SELECT id, pass FROM users WHERE login=?', (login,)) + self.execute("SELECT id, pass FROM users WHERE login=?", (login,)) result = self.cursor.fetchall() if len(result) != 1: return (False, None) @@ -123,26 +137,26 @@ def checkPassword(self, login, password): return (False, None) else: return (True, result[0][0]) - + def updateUserPassword(self, login, newPassword): userId = self.getUserId(login) - self.execute("UPDATE users SET pass = ? where id = ?", (newPassword, userId)) + self.execute("UPDATE users SET pass = ? where id = ?", (newPassword, userId)) return True def updateUserEmail(self, login, newEmail): userId = self.getUserId(login) - self.execute("UPDATE users SET email = ? where id = ?", (newEmail, userId)) + self.execute("UPDATE users SET email = ? where id = ?", (newEmail, userId)) return True - + def getUserEmail(self, login): userId = self.getUserId(login) - self.execute('SELECT email FROM users WHERE id=?', (userId,)) + self.execute("SELECT email FROM users WHERE id=?", (userId,)) result = self.cursor.fetchall() if len(result) != 1: return None else: return result[0][0] - + def canInvite(self, login, dest): currentInvitations = self.getUserCreatedInvites(login, btchDB.PENDING) if len(currentInvitations) >= btchDB.MAXINVITE: @@ -150,45 +164,63 @@ def canInvite(self, login, dest): idP2 = self.getUserId(dest) dests = [invite[2] for invite in currentInvitations] return idP2 not in dests - + def newInvite(self, login, dest, text): if self.canInvite(login, dest): idP1 = self.getUserId(login) idP2 = self.getUserId(dest) if idP1 == None or idP2 == None: return False - self.execute("INSERT INTO invitations (idP1, idP2, message) VALUES ('%s', '%s', '%s')"%(idP1, idP2, text)) + self.execute( + "INSERT INTO invitations (idP1, idP2, message) VALUES ('%s', '%s', '%s')" + % (idP1, idP2, text) + ) return True - else: + else: return False - + def getUserCreatedInvites(self, login, status=None): idP1 = self.getUserId(login) if status == None: - self.execute("SELECT id, idP1, idP2, message FROM invitations WHERE idP1=?", (idP1,)) + self.execute( + "SELECT id, idP1, idP2, message FROM invitations WHERE idP1=?", (idP1,) + ) else: - self.execute("SELECT id, idP1, idP2, message FROM invitations WHERE idP1=? AND status=?", (idP1,status)) + self.execute( + "SELECT id, idP1, idP2, message FROM invitations WHERE idP1=? AND status=?", + (idP1, status), + ) return self.cursor.fetchall() def getUserReceivedInvites(self, login, status=None): idP2 = self.getUserId(login) if status == None: - self.execute("SELECT id, idP1, idP2, message FROM invitations WHERE idP2=?", (idP2,)) + self.execute( + "SELECT id, idP1, idP2, message FROM invitations WHERE idP2=?", (idP2,) + ) else: - self.execute("SELECT id, idP1, idP2, message FROM invitations WHERE idP2=? AND status=?", (idP2,status)) + self.execute( + "SELECT id, idP1, idP2, message FROM invitations WHERE idP2=? AND status=?", + (idP2, status), + ) return self.cursor.fetchall() - + def acceptInvite(self, inviteId): - self.execute("UPDATE invitations SET status=? where id=?", (btchDB.ACCEPTED, inviteId)) - + self.execute( + "UPDATE invitations SET status=? where id=?", (btchDB.ACCEPTED, inviteId) + ) + def declineInvite(self, inviteId): - self.execute("UPDATE invitations SET status=? where id=?", (btchDB.DECLINED, inviteId)) - + self.execute( + "UPDATE invitations SET status=? where id=?", (btchDB.DECLINED, inviteId) + ) + -#=================== TESTING ======================= +# =================== TESTING ======================= import unittest + class TestDB(unittest.TestCase): def setUp(self): self.testPath = "testDB_1234.sqlite" @@ -196,7 +228,7 @@ def setUp(self): os.remove(self.testPath) self.DB = btchDB(self.testPath) self.DB.startEditing() - + def tearDown(self): self.DB.stopEditing() if os.path.exists(self.testPath): @@ -208,7 +240,7 @@ def testCreateUser(self): self.assertTrue(self.DB.newUser("pol", "pass")) self.assertFalse(self.DB.newUser("antoine", "pass", "toto@yop.com")) self.DB.clearAll() - + def testUserExists(self): self.DB.newUser("antoine", "pass", "toto@yop.com") self.DB.newUser("leo", "pass", "toto@yop.com") @@ -216,7 +248,7 @@ def testUserExists(self): self.assertTrue(self.DB.userExists("leo")) self.assertFalse(self.DB.userExists("Pol")) self.DB.clearAll() - + def testCheckLogin(self): self.DB.newUser("antoine", "pass", "toto@yop.com") self.DB.newUser("leo", "leoPass", "toto@yop.com") @@ -227,7 +259,7 @@ def testCheckLogin(self): validPassword, idUser = self.DB.checkPassword("antoine", "leoPass") self.assertFalse(validPassword) self.DB.clearAll() - + def testUpdatePassword(self): self.DB.newUser("antoine", "pass", "toto@yop.com") self.assertTrue(self.DB.checkPassword("antoine", "pass")[0]) @@ -235,7 +267,7 @@ def testUpdatePassword(self): self.assertFalse(self.DB.checkPassword("antoine", "pass")[0]) self.assertTrue(self.DB.checkPassword("antoine", "newPass")[0]) self.DB.clearAll() - + def testUpdateEmail(self): self.DB.newUser("antoine", "pass", "toto@yop.com") self.DB.newUser("leo", "pass") @@ -244,53 +276,67 @@ def testUpdateEmail(self): self.DB.updateUserEmail("leo", "new@ema.il") self.assertEqual(self.DB.getUserEmail("leo"), "new@ema.il") self.DB.clearAll() - + def testgetUserId(self): self.DB.newUser("antoine", "pass", "toto@yop.com") self.assertNotEqual(self.DB.getUserId("antoine"), None) self.assertEqual(self.DB.getUserId("leo"), None) self.DB.clearAll() - + def testCreateInvites(self): - for i in range(btchDB.MAXINVITE+2): - self.DB.newUser("p%d"%i, "pass") + for i in range(btchDB.MAXINVITE + 2): + self.DB.newUser("p%d" % i, "pass") for i in range(btchDB.MAXINVITE): - self.assertTrue(self.DB.newInvite("p0", "p%d"%(i+1), "hey")) - + self.assertTrue(self.DB.newInvite("p0", "p%d" % (i + 1), "hey")) + self.assertTrue(self.DB.newInvite("p1", "p0", "hey")) self.assertFalse(self.DB.newInvite("p1", "p0", "hey2")) - + for i in range(btchDB.MAXINVITE): - self.assertEqual(len(self.DB.getUserReceivedInvites("p%d"%(i+1))), 1) - self.assertEqual(len(self.DB.getUserReceivedInvites("p%d"%(i+1), btchDB.PENDING)), 1) - self.assertEqual(len(self.DB.getUserReceivedInvites("p%d"%(i+1), btchDB.ACCEPTED)), 0) - self.assertEqual(len(self.DB.getUserReceivedInvites("p%d"%(i+1), btchDB.DECLINED)), 0) - self.assertFalse(self.DB.newInvite("p0", "p%d"%(btchDB.MAXINVITE+1), "hey")) - self.assertEqual(len(self.DB.getUserCreatedInvites("p0", btchDB.PENDING)), btchDB.MAXINVITE) + self.assertEqual(len(self.DB.getUserReceivedInvites("p%d" % (i + 1))), 1) + self.assertEqual( + len(self.DB.getUserReceivedInvites("p%d" % (i + 1), btchDB.PENDING)), 1 + ) + self.assertEqual( + len(self.DB.getUserReceivedInvites("p%d" % (i + 1), btchDB.ACCEPTED)), 0 + ) + self.assertEqual( + len(self.DB.getUserReceivedInvites("p%d" % (i + 1), btchDB.DECLINED)), 0 + ) + self.assertFalse(self.DB.newInvite("p0", "p%d" % (btchDB.MAXINVITE + 1), "hey")) + self.assertEqual( + len(self.DB.getUserCreatedInvites("p0", btchDB.PENDING)), btchDB.MAXINVITE + ) self.assertEqual(len(self.DB.getUserCreatedInvites("p0", btchDB.ACCEPTED)), 0) self.assertEqual(len(self.DB.getUserCreatedInvites("p0", btchDB.DECLINED)), 0) self.assertEqual(len(self.DB.getUserCreatedInvites("p0")), btchDB.MAXINVITE) self.assertEqual(len(self.DB.getUserCreatedInvites("p1")), 1) self.assertEqual(len(self.DB.getUserCreatedInvites("p2")), 0) self.DB.clearAll() - + def testUpdateInvites(self): for i in range(4): - self.DB.newUser("p%d"%i, "pass") + self.DB.newUser("p%d" % i, "pass") self.DB.newInvite("p0", "p1", "hey") self.DB.newInvite("p0", "p2", "hey") self.DB.newInvite("p0", "p3", "hey") - for i in range(1,4): - self.assertEqual(len(self.DB.getUserReceivedInvites("p%d"%i)), 1) - self.assertEqual(len(self.DB.getUserReceivedInvites("p%d"%i, btchDB.PENDING)), 1) - self.assertEqual(len(self.DB.getUserReceivedInvites("p%d"%i, btchDB.ACCEPTED)), 0) - self.assertEqual(len(self.DB.getUserReceivedInvites("p%d"%i, btchDB.DECLINED)), 0) - + for i in range(1, 4): + self.assertEqual(len(self.DB.getUserReceivedInvites("p%d" % i)), 1) + self.assertEqual( + len(self.DB.getUserReceivedInvites("p%d" % i, btchDB.PENDING)), 1 + ) + self.assertEqual( + len(self.DB.getUserReceivedInvites("p%d" % i, btchDB.ACCEPTED)), 0 + ) + self.assertEqual( + len(self.DB.getUserReceivedInvites("p%d" % i, btchDB.DECLINED)), 0 + ) + inviteId = self.DB.getUserReceivedInvites("p1")[0] self.DB.acceptInvite(inviteId[0]) inviteId = self.DB.getUserReceivedInvites("p2")[0] self.DB.declineInvite(inviteId[0]) - + self.assertEqual(len(self.DB.getUserReceivedInvites("p1", btchDB.ACCEPTED)), 1) self.assertEqual(len(self.DB.getUserReceivedInvites("p2", btchDB.DECLINED)), 1) self.assertEqual(len(self.DB.getUserReceivedInvites("p1", btchDB.PENDING)), 0) @@ -299,9 +345,7 @@ def testUpdateInvites(self): self.assertEqual(len(self.DB.getUserCreatedInvites("p0", btchDB.ACCEPTED)), 1) self.assertEqual(len(self.DB.getUserCreatedInvites("p0", btchDB.DECLINED)), 1) self.DB.clearAll() - - + + if __name__ == "__main__": unittest.main() - - diff --git a/battlechess/server/config.py b/battlechess/server/config.py new file mode 100644 index 0000000..fb00bfb --- /dev/null +++ b/battlechess/server/config.py @@ -0,0 +1,11 @@ +# to get a string like this run: +# openssl rand -hex 32 + +from decouple import config + +SECRET_KEY = config("SECRET_KEY", default="e909bb995546a0378161ed18d4e44ab4525d735e07a52cec2eb9b3a86d39ee61") +ALGORITHM = config("ALGORITHM", default="HS256") +ACCESS_TOKEN_EXPIRE_MINUTES = config("ACCESS_TOKEN_EXPIRE_MINUTES", default=3000, cast=int) +HANDLEBASEURL = config("HANDLEBASEURL", default="https://bt.ch/") +SQLALCHEMY_DATABASE_URL = config("SQLALCHEMY_DATABASE_URL", default="sqlite:///./btchdb.sqlite") + diff --git a/server/crud.py b/battlechess/server/crud.py similarity index 63% rename from server/crud.py rename to battlechess/server/crud.py index 569d1ac..24bca43 100644 --- a/server/crud.py +++ b/battlechess/server/crud.py @@ -1,24 +1,20 @@ import random - -from pathlib import Path import shutil - -from sqlalchemy import or_, and_ -from sqlalchemy.orm import Session -from typing import Optional, Tuple, Set from datetime import datetime, timedelta, timezone -from jose import JWTError, jwt - -from . import models, schemas +from pathlib import Path +from typing import List, Optional -from .utils import (get_password_hash, verify_password, get_random_string, - defaultBoard) +from jose import jwt +from sqlalchemy import and_, or_ +from sqlalchemy.orm import Session -from .config import ( - SECRET_KEY, - ALGORITHM, - ACCESS_TOKEN_EXPIRE_MINUTES, - HANDLEBASEURL, +from battlechess.server import models, schemas +from battlechess.server.config import ALGORITHM, SECRET_KEY +from battlechess.server.utils import ( + defaultBoard, + get_password_hash, + get_random_string, + verify_password, ) @@ -27,8 +23,9 @@ def create_game_uuid(db: Session): uuid = get_random_string() # Check if it exists (and its idle?) Or we could add the id or something. for i in range(5): - repeatedHandleGame = db.query( - models.Game).filter(models.Game.uuid == uuid).first() + repeatedHandleGame = ( + db.query(models.Game).filter(models.Game.uuid == uuid).first() + ) if repeatedHandleGame is None: break @@ -42,16 +39,15 @@ def get_user(db: Session, user_id: int): def get_user_by_id(db: Session, userid: int): - return db.query( - models.User).filter(models.User.id == userid).first() + return db.query(models.User).filter(models.User.id == userid).first() + def get_user_by_email(db: Session, email: str): return db.query(models.User).filter(models.User.email == email).first() def get_user_by_username(db: Session, username: str): - return db.query( - models.User).filter(models.User.username == username).first() + return db.query(models.User).filter(models.User.username == username).first() def get_users(db: Session, skip: int = 0, limit: int = 100): @@ -65,7 +61,8 @@ def create_user(db: Session, user: schemas.UserCreate): full_name=user.full_name, email=user.email, avatar=user.avatar, - hashed_password=hashed_password) + hashed_password=hashed_password, + ) db.add(db_user) db.commit() db.refresh(db_user) @@ -81,7 +78,7 @@ def update_user(db: Session, current_user: schemas.User, updated_user: schemas.U # TODO allow changing more things other than fullname fullname = updated_user.full_name updated_user = current_user - updated_user['fullname'] = fullname + updated_user["fullname"] = fullname db_user.update(**updated_user) db.commit() @@ -94,7 +91,7 @@ def create_avatar_file(db: Session, user: schemas.User, file): avatar_dir = Path(__file__).parent.parent / "data" / "avatars" avatar_filepath = avatar_dir / f"{user.id}_avatar.jpeg" - with avatar_filepath.open('wb') as write_file: + with avatar_filepath.open("wb") as write_file: shutil.copyfileobj(file.file, write_file) return file.filename @@ -120,8 +117,7 @@ def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): return encoded_jwt -def create_game(db: Session, user: schemas.User, - gameOptions: schemas.GameCreate): +def create_game(db: Session, user: schemas.User, gameOptions: schemas.GameCreate): user = get_user_by_username(db, user.username) if not user: return False @@ -138,14 +134,16 @@ def create_game(db: Session, user: schemas.User, black_id = user.id # TODO list status strings somewhere - db_game = models.Game(owner_id=user.id, - created_at=datetime.now(timezone.utc), - uuid=uuid, - status="waiting", - turn="white", - white_id=white_id, - black_id=black_id, - public=gameOptions.public) + db_game = models.Game( + owner_id=user.id, + created_at=datetime.now(timezone.utc), + uuid=uuid, + status="waiting", + turn="white", + white_id=white_id, + black_id=black_id, + public=gameOptions.public, + ) db.add(db_game) db.commit() db.refresh(db_game) @@ -156,9 +154,14 @@ def get_games_by_owner(db: Session, user: schemas.User): return db.query(models.Game).filter(models.Game.owner == user).all() -def get_games_by_player(db: Session, user: schemas.User): - return db.query(models.Game).filter( - or_(models.Game.black == user, models.Game.white == user)).all() +def get_games_by_player( + db: Session, user: schemas.User, status: schemas.GameStatus = None +): + status_filter = or_(models.Game.black == user, models.Game.white == user) + if status: + status_filter = and_(status_filter, models.Game.status == status) + + return db.query(models.Game).filter(status_filter).all() def get_game_by_uuid(db: Session, gameUUID): @@ -166,11 +169,41 @@ def get_game_by_uuid(db: Session, gameUUID): def get_public_game_by_status(db: Session, user: schemas.User, status): - games = db.query(models.Game).filter( - and_(models.Game.status == status, - models.Game.white_id.is_not(user.id), - models.Game.black_id.is_not(user.id), - models.Game.public == True)).all() + games = ( + db.query(models.Game) + .filter( + and_( + models.Game.status == status, + models.Game.white_id.is_not(user.id), + models.Game.black_id.is_not(user.id), + bool(models.Game.public) is True, + ) + ) + .all() + ) + return games + + +def get_games_by_status( + db: Session, user: schemas.User, statuses: List[schemas.GameStatus] +): + + db_statuses: list(str) = [str(status) for status in statuses] if statuses else None + + games = ( + db.query(models.Game) + .filter( + and_( + models.Game.status.in_(db_statuses) if db_statuses else True, + or_( + models.Game.white_id == user.id, + models.Game.black_id == user.id, + bool(models.Game.public) is True, + ), + ) + ) + .all() + ) return games @@ -187,8 +220,11 @@ def get_random_public_game_waiting(db: Session, user: schemas.User): def get_snap(db: Session, user: schemas.User, gameUUID, move_number): game = get_game_by_uuid(db, gameUUID) query = db.query(models.GameSnap).filter( - and_(models.GameSnap.game_id == game.id, - models.GameSnap.move_number == move_number)) + and_( + models.GameSnap.game_id == game.id, + models.GameSnap.move_number == move_number, + ) + ) if query.count() > 1: print("Error: snap duplicate!") @@ -201,8 +237,9 @@ def get_snap(db: Session, user: schemas.User, gameUUID, move_number): # snap.prepare_for_player(color) -def create_snap_by_move(db: Session, user: schemas.User, game: schemas.Game, - gameMove: schemas.GameMove): +def create_snap_by_move( + db: Session, user: schemas.User, game: schemas.Game, gameMove: schemas.GameMove +): game = get_game_by_uuid(db, game.uuid) snapOptions = game.moveGame(gameMove.move) @@ -213,10 +250,11 @@ def create_snap_by_move(db: Session, user: schemas.User, game: schemas.Game, created_at=datetime.now(timezone.utc), game_id=game.id, move=gameMove.move, - board=snapOptions['board'], - taken=snapOptions['taken'], - castleable=snapOptions['castleable'], - move_number=snap.move_number + 1) + board=snapOptions["board"], + taken=snapOptions["taken"], + castleable=snapOptions["castleable"], + move_number=snap.move_number + 1, + ) db.add(db_snap) db.commit() db.refresh(db_snap) @@ -225,29 +263,27 @@ def create_snap_by_move(db: Session, user: schemas.User, game: schemas.Game, winner = db_snap.winner() if winner: game.winner = winner - game.status = 'finished' + game.status = schemas.GameStatus.OVER print( - f'Game {game.uuid} {game.white_id} vs {game.black_id} won by {game.winner}' + f"Game {game.uuid} {game.white_id} vs {game.black_id} won by {game.winner}" ) - + game.last_move_time = datetime.now(timezone.utc) game.refresh_turn() - color = None - if game.black_id == user.id: - color = 'b' - if game.white_id == user.id: - color = 'w' - - # deprecated in favor of pydantic prepare_for_player TODO pydantic elements - # elements = db_snap.filtered(color) - db.commit() return db_snap # TODO test -def create_snap_by_dict(db: Session, user: schemas.User, gameUUID: str, board: str, move: str, - taken: str, castleable: str): +def create_snap_by_dict( + db: Session, + user: schemas.User, + gameUUID: str, + board: str, + move: str, + taken: str, + castleable: str, +): game = get_game_by_uuid(db, gameUUID) last_snap = game.get_latest_snap() @@ -260,7 +296,8 @@ def create_snap_by_dict(db: Session, user: schemas.User, gameUUID: str, board: s move=move, taken=taken, castleable=castleable, - move_number=move_number) + move_number=move_number, + ) db.add(db_snap) db.commit() db.refresh(db_snap) @@ -274,8 +311,9 @@ def create_default_snap(db: Session, user: schemas.User, game: models.Game): board=defaultBoard(), move="", taken="", - castleable=''.join(sorted("LKSlks")), - move_number=0) + castleable="".join(sorted("LKSlks")), + move_number=0, + ) db.add(db_snap) db.commit() db.refresh(db_snap) diff --git a/server/models.py b/battlechess/server/models.py similarity index 80% rename from server/models.py rename to battlechess/server/models.py index 0e90f77..1c637a2 100644 --- a/server/models.py +++ b/battlechess/server/models.py @@ -1,13 +1,13 @@ -from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, DateTime -from sqlalchemy.orm import relationship +import datetime from fastapi import HTTPException, status +from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String +from sqlalchemy.orm import relationship -from .btchApiDB import Base -from .schemas import GameStatus -from core.Board import Board -from core.btchBoard import BtchBoard - -from .utils import ad2extij, extij2ad, ad2ij +from battlechess.core.Board import Board +from battlechess.core.btchBoard import BtchBoard +from battlechess.server.btchApiDB import Base +from battlechess.server.schemas import GameSnap, GameStatus +from battlechess.server.utils import ad2extij, ad2ij, extij2ad class User(Base): @@ -21,7 +21,7 @@ class User(Base): email = Column(String, unique=True, index=True) hashed_password = Column(String) status = Column(String, default="active") - created_at = Column(DateTime) + created_at = Column(DateTime, default=datetime.datetime.utcnow) # games = relationship("Game", back_populates="owner", foreign_keys='Game.owner_id') # whites = relationship("Game", back_populates="white") @@ -30,12 +30,13 @@ class User(Base): def is_active(self): return self.status == "active" + class Game(Base): __tablename__ = "game" id = Column(Integer, primary_key=True, index=True) - created_at = Column(DateTime) + created_at = Column(DateTime, default=datetime.datetime.utcnow) uuid = Column(String) owner_id = Column(Integer, ForeignKey("user.id")) white_id = Column(Integer, ForeignKey("user.id")) @@ -56,7 +57,7 @@ class Game(Base): snaps = relationship("GameSnap", back_populates="game") def reset(self): - self.turn = 'white' + self.turn = "white" self.winner = None firstsnap = self.snaps[0] self.snaps[:] = [firstsnap] @@ -64,11 +65,7 @@ def reset(self): def set_player(self, user: User): if self.white_id == user.id or self.black_id == user.id: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail="Player is already in this game", - headers={"WWW-Authenticate": "Bearer"}, - ) + return if not self.white_id and not self.black_id: # TODO random @@ -78,7 +75,7 @@ def set_player(self, user: User): elif not self.black_id: self.black_id = user.id else: - #error player already set + # error player already set raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Game is full", @@ -95,6 +92,9 @@ def get_player_color(self, user_id): return "black" return None + def is_waiting(self): + return self.status == GameStatus.WAITING + def is_finished(self): return self.status == GameStatus.OVER @@ -112,17 +112,18 @@ def start_game(self): def refresh_turn(self): if not self.snaps: print("[warning] game had no snaps. turn is white.") - self.turn = 'white' + self.turn = "white" self.turn = self.snaps[-1].getNextTurn() # TODO ensure that the turn color is guaranteed to be correct to the caller user's color # TODO should we create the snap here instead of returning # the snap options and delegating to the client? - def moveGame(self, move): + def moveGame(self, move) -> GameSnap: current_snap = self.get_latest_snap() new_snap_options = current_snap.moveSnap(move) return new_snap_options + class GameSnap(Base): __tablename__ = "gamesnap" @@ -141,16 +142,16 @@ class GameSnap(Base): def getNextTurn(self): if not self.game.is_running(): return None - colors = ['white', 'black'] - return colors[self.move_number%2] + colors = ["white", "black"] + return colors[self.move_number % 2] # TODO we need forfeit as an option # TODO does draw exist in battlechess? def winner(self): - if 'K' in self.taken: - return 'white' - if 'k' in self.taken: - return 'black' + if "K" in self.taken: + return "white" + if "k" in self.taken: + return "black" return None def snapOptionsFromBoard(self, board: Board, accepted_move): @@ -161,13 +162,13 @@ def snapOptionsFromBoard(self, board: Board, accepted_move): # TODO handle errors # TODO could be static method, utils ... def coordListToMove(self, coords): - abc = 'abcdefgh' - dig = '87654321' - return abc[coords[1]]+dig[coords[0]]+abc[coords[3]]+dig[coords[2]] + abc = "abcdefgh" + dig = "87654321" + return abc[coords[1]] + dig[coords[0]] + abc[coords[3]] + dig[coords[2]] # TODO handle errors def moveToCoordList(self, move): - dig = [None,7,6,5,4,3,2,1,0] + dig = [None, 7, 6, 5, 4, 3, 2, 1, 0] i = dig[int(move[1])] j = ord(move[0]) - 97 ii = dig[int(move[3])] @@ -178,7 +179,6 @@ def moveSnap(self, move): # TODO sync with board color = self.getNextTurn() - snapOptions = None # build board from model coordlist = self.moveToCoordList(move) board = self.toBoard() @@ -206,15 +206,21 @@ def toBoard(self): board = Board() board.reset() enpassantColumn = self.enpassantColumn() - enpassant = chr(enpassantColumn + ord('a')) if enpassantColumn is not None else None - winner = None # TODO better way to get for unit testing self.game.winner - board.updateFromElements(self.board, self.taken, self.castleable, enpassant, winner) + enpassant = ( + chr(enpassantColumn + ord("a")) if enpassantColumn is not None else None + ) + winner = None # TODO better way to get for unit testing self.game.winner + board.updateFromElements( + self.board, self.taken, self.castleable, enpassant, winner + ) return board def toBtchBoard(self) -> BtchBoard: enpassantColumn = self.enpassantColumn() enpassant = enpassantColumn + 2 if enpassantColumn is not None else None - return BtchBoard.factoryFromElements(self.board, self.taken, self.castleable, enpassant) + return BtchBoard.factoryFromElements( + self.board, self.taken, self.castleable, enpassant + ) def enpassantColumn(self): if not self.move: @@ -224,7 +230,7 @@ def enpassantColumn(self): ii, jj = ad2ij(self.move[2:4]) piece = self.board[ii * 8 + jj] - if piece in ['p', 'P']: + if piece in ["p", "P"]: return j if i in [1, 6] and ii in [3, 4] else None return None diff --git a/server/schemas.py b/battlechess/server/schemas.py similarity index 69% rename from server/schemas.py rename to battlechess/server/schemas.py index 6bc0fa6..7907e8c 100644 --- a/server/schemas.py +++ b/battlechess/server/schemas.py @@ -1,10 +1,11 @@ -from typing import List, Optional, Tuple -from datetime import datetime, time, timedelta +from datetime import datetime +from typing import Optional, Tuple + # from uuid import UUID # represented as string from pydantic import BaseModel -class GameStatus(): +class GameStatus: WAITING = "waiting" STARTED = "started" OVER = "over" @@ -19,6 +20,27 @@ class TokenData(BaseModel): username: Optional[str] = None +class UserBase(BaseModel): + username: str + full_name: Optional[str] = None + email: Optional[str] = None + avatar: Optional[str] = None + + +class UserCreate(UserBase): + plain_password: str + + +class User(UserBase): + id: int + status: str + + # games: List[Game] = [] + # whites: List[Game] = [] + # blacks: List[Game] = [] + + model_config = {"from_attributes": True} + class GameSnapBase(BaseModel): pass @@ -32,16 +54,22 @@ class GameSnap(GameSnapBase): taken: str castleable: str move_number: int + model_config = {"from_attributes": True} def extendboard(self, board): - return "_" * 10 + "".join(["_" + board[j * 8:(j + 1) * 8] + "_" for j in range(8) - ]) + "_" * 10 + return ( + "_" * 10 + + "".join(["_" + board[j * 8 : (j + 1) * 8] + "_" for j in range(8)]) + + "_" * 10 + ) def shrinkboard(self, extboard, inner=True): if not inner: - return "".join([extboard[j * 10 + 1:(j + 1) * 10 - 1] for j in range(1, 9)]) + return "".join( + [extboard[j * 10 + 1 : (j + 1) * 10 - 1] for j in range(1, 9)] + ) else: - return "".join([extboard[j * 10 + 1:(j + 1) * 10 - 1] for j in range(8)]) + return "".join([extboard[j * 10 + 1 : (j + 1) * 10 - 1] for j in range(8)]) # i,j extended board coordinates def hasEnemy(self, extboard, i, j): @@ -51,7 +79,7 @@ def hasEnemy(self, extboard, i, j): for j2 in [j - 1, j, j + 1]: for i2 in [i - 1, i, i + 1]: c2 = extboard[i2 * 10 + j2] - if c2 == '_': + if c2 == "_": continue elif c.isupper() and c2.islower(): return True @@ -60,10 +88,14 @@ def hasEnemy(self, extboard, i, j): def filterchar(self, color, c, extboard, i, j): # TODO replace Xs with _ when function has been tested enough. - if color == 'black': - return c if c == '_' or c.isupper() or self.hasEnemy(extboard, i, j) else 'X' - elif color == 'white': - return c if c == '_' or c.islower() or self.hasEnemy(extboard, i, j) else 'x' + if color == "black": + return ( + c if c == "_" or c.isupper() or self.hasEnemy(extboard, i, j) else "X" + ) + elif color == "white": + return ( + c if c == "_" or c.islower() or self.hasEnemy(extboard, i, j) else "x" + ) else: print(f"{color} is not a color") return None @@ -72,7 +104,7 @@ def filterBoard(self, color): # extend the board to skip doing bound checking extboard = self.extendboard(self.board) - #[(i,j,c) for j in range(1,9) for i,c in enumerate(foo[j*10+1:j*10+9])] + # [(i,j,c) for j in range(1,9) for i,c in enumerate(foo[j*10+1:j*10+9])] # print(''.join([ # self.filterchar(color, extboard[i * 10 + j], extboard, i, j) @@ -92,28 +124,23 @@ def filterBoard(self, color): def prepare_for_player(self, player_color: str): - #foreach enemy piece, check if theres a friendly piece around, delete if not + # foreach enemy piece, check if theres a friendly piece around, delete if not # filteredSnap = self.copy() # TODO no need to copy, schema objects don't modify the db - #remove other player board + # remove other player board self.board = self.filterBoard(player_color) - if player_color == 'black': - self.castleable = ''.join(c for c in self.castleable if c.isupper()) + if player_color == "black": + self.castleable = "".join(c for c in self.castleable if c.isupper()) self.move = self.move if not self.move_number % 2 else None - elif player_color == 'white': - self.castleable = ''.join(c for c in self.castleable if c.islower()) + elif player_color == "white": + self.castleable = "".join(c for c in self.castleable if c.islower()) self.move = self.move if self.move_number % 2 else None - class Config: - orm_mode = True - class FilteredGameSnap(GameSnap): - - class Config: - orm_mode = False + model_config = {"from_attributes": False} class GameMove(BaseModel): @@ -141,42 +168,18 @@ class Game(GameBase): uuid: str created_at: Optional[datetime] = None last_move_time: Optional[datetime] = None - owner_id: int - white_id: Optional[int] = None - black_id: Optional[int] = None + owner: UserBase + white: Optional[UserBase] = None + black: Optional[UserBase] = None status: str turn: Optional[str] = None winner: Optional[str] = None public: Optional[bool] = None - class Config: - orm_mode = True + model_config = {"from_attributes": True} class Move(BaseModel): origin: Tuple[int, int] destination: Tuple[int, int] color: str - - -class UserBase(BaseModel): - username: str - full_name: Optional[str] = None - email: Optional[str] = None - avatar: Optional[str] = None - - -class UserCreate(UserBase): - plain_password: str - - -class User(UserBase): - id: int - status: str - - # games: List[Game] = [] - # whites: List[Game] = [] - # blacks: List[Game] = [] - - class Config: - orm_mode = True \ No newline at end of file diff --git a/battlechess/server/utils.py b/battlechess/server/utils.py new file mode 100644 index 0000000..e3d7071 --- /dev/null +++ b/battlechess/server/utils.py @@ -0,0 +1,62 @@ +import random +import string +import bcrypt + +from battlechess.server.config import HANDLEBASEURL + +def verify_password(plain_password, hashed_password): + # hash driectly from db is bytes, but json is str + if type(hashed_password) is str: + hashed_password = bytes(hashed_password.encode('utf-8')) + + password_byte_enc = plain_password.encode('utf-8') + return bcrypt.checkpw(password=password_byte_enc, hashed_password=hashed_password) + + +def get_password_hash(password): + pwd_bytes = password.encode('utf-8') + salt = bcrypt.gensalt() + hashed_password = bcrypt.hashpw(password=pwd_bytes, salt=salt) + return hashed_password + + +# TODO use Random-Word or something for more user-friendly handles +def get_random_string(length=6): + # choose from all lowercase letter + letters = string.ascii_lowercase + result_str = "".join(random.choice(letters) for i in range(length)) + return result_str + + +def handle2uuid(uuid): + return HANDLEBASEURL + uuid + + +def defaultBoard(): + return ( + "RNBQKBNR" + "PPPPPPPP" + "________" + "________" + "________" + "________" + "pppppppp" + "rnbqkbnr" + ) + + +def extij2ad(i, j): + square = chr(j - 2 + 97) + str(8 - (i - 2)) + return square + + +def ad2extij(square): + i = 8 - int(square[1]) + 2 + j = ord(square[0]) - ord("a") + 2 + return (i, j) + + +def ad2ij(square): + i = 8 - int(square[1]) + j = ord(square[0]) - ord("a") + return (i, j) diff --git a/battleChess.py b/battlechess_standalone/battleChess.py similarity index 71% rename from battleChess.py rename to battlechess_standalone/battleChess.py index 87cc39b..ebab6b0 100644 --- a/battleChess.py +++ b/battlechess_standalone/battleChess.py @@ -2,16 +2,17 @@ # proublyd brought to you by Antoine Letouzey and Pol Monso ! -import pygame -import sys -import time +import random import socket +import sys import threading -import random +import time import urllib.request -from core.Board import Board + +import pygame +from .communication import recvData, sendData, waitForMessage +from battlechess.core.Board import Board from pygame.locals import * -from communication import sendData, recvData, waitForMessage # GLOBAL VARIABLES W, H = 512, 384 # wibndow prop @@ -36,101 +37,108 @@ def copy(self): return res def draw(self, screen, selected=None, turn=None): - screen.blit(self.sprite_board, (0,0)) + screen.blit(self.sprite_board, (0, 0)) for i in range(8): for j in range(8): c = self.board[i][j] - if c == '': + if c == "": continue else: dx = 0 dy = 0 - if c[1] == 'b': + if c[1] == "b": dx += 72 - if c[0] in ['n', 'r', 'b']: + if c[0] in ["n", "r", "b"]: dx += 36 - if c[0] in ['q', 'r']: + if c[0] in ["q", "r"]: dy += 36 - if c[0] in ['p', 'b']: + if c[0] in ["p", "b"]: dy += 72 patch_rect = (dx, dy, 36, 36) - screen.blit(self.sprite_pieces, (104+j*40, 30+i*40), patch_rect) + screen.blit( + self.sprite_pieces, (104 + j * 40, 30 + i * 40), patch_rect + ) if turn is not None: self.visibility = [[False for i in range(8)] for j in range(8)] # get white piece - color = 'b' + color = "b" if turn: - color = 'w' + color = "w" for i in range(8): for j in range(8): if self.board[i][j].endswith(color): - for di in range(-1,2): - for dj in range(-1,2): - if self.isIn(i+di, j+dj): - self.visibility[i+di][j+dj] = True + for di in range(-1, 2): + for dj in range(-1, 2): + if self.isIn(i + di, j + dj): + self.visibility[i + di][j + dj] = True fog = pygame.Surface((38, 38)) fog.set_alpha(150) fog.fill((0, 0, 0)) for i in range(8): for j in range(8): if not self.visibility[i][j]: - screen.blit(fog, (105+j*40, 30+i*40)) - # draw possible displacement positions + screen.blit(fog, (105 + j * 40, 30 + i * 40)) + # draw possible displacement positions s = pygame.Surface((34, 34)) s.set_alpha(128) s.fill((255, 0, 255)) if selected: pos = self.getPossiblePosition(selected[0], selected[1]) for p in pos: - screen.blit(s, (106+p[1]*40, 30+p[0]*40)) + screen.blit(s, (106 + p[1] * 40, 30 + p[0] * 40)) # draw taken pieces # white - for i, p in enumerate([p for p in self.taken if p[1] == 'w']): + for i, p in enumerate([p for p in self.taken if p[1] == "w"]): dx = 0 dy = 0 - if p[0] in ['n', 'r', 'b']: + if p[0] in ["n", "r", "b"]: dx += 36 - if p[0] in ['q', 'r']: + if p[0] in ["q", "r"]: dy += 36 - if p[0] in ['p', 'b']: + if p[0] in ["p", "b"]: dy += 72 patch_rect = (dx, dy, 36, 36) - screen.blit(self.sprite_pieces, (6+(i % 2)*40, 20+(i/2)*40), patch_rect) - for i, p in enumerate([p for p in self.taken if p[1] == 'b']): + screen.blit( + self.sprite_pieces, (6 + (i % 2) * 40, 20 + (i / 2) * 40), patch_rect + ) + for i, p in enumerate([p for p in self.taken if p[1] == "b"]): dx = 72 dy = 0 - if p[0] in ['n', 'r', 'b']: + if p[0] in ["n", "r", "b"]: dx += 36 - if p[0] in ['q', 'r']: + if p[0] in ["q", "r"]: dy += 36 - if p[0] in ['p', 'b']: + if p[0] in ["p", "b"]: dy += 72 patch_rect = (dx, dy, 36, 36) - screen.blit(self.sprite_pieces, (426+(i % 2)*40, 338-(i/2)*40), patch_rect) + screen.blit( + self.sprite_pieces, (426 + (i % 2) * 40, 338 - (i / 2) * 40), patch_rect + ) def click(self, pos): - i = int((pos[1]-30) / 40) - j = int((pos[0]-106) / 40) + i = int((pos[1] - 30) / 40) + j = int((pos[0] - 106) / 40) if self.isIn(i, j): return [i, j] return None + # **************************************************************************** def loadData(): - sprite_board = pygame.image.load('data/board.png').convert(24) - sprite_pieces = pygame.image.load('data/pieces.png') + sprite_board = pygame.image.load("data/board.png").convert(24) + sprite_pieces = pygame.image.load("data/pieces.png") # sprite_pieces.set_colorkey((255,0,255)) # for non-transparent png, chroma-keying sprite_pieces = sprite_pieces.convert_alpha(sprite_pieces) - sniper = pygame.mixer.Sound('data/sniper.wav') + sniper = pygame.mixer.Sound("data/sniper.wav") return [sprite_board, sprite_pieces, sniper] -# get keyboard to work +# get keyboard to work def events(): click = False - keys = [] + keys = [] for event in pygame.event.get(): if event.type == pygame.QUIT: return (False, None, keys) @@ -138,11 +146,11 @@ def events(): if event.key == pygame.K_ESCAPE: return (False, None, keys) if event.key == pygame.K_SPACE: - keys.append('SPACE') + keys.append("SPACE") if event.key == pygame.K_RIGHT: - keys.append('RIGHT') + keys.append("RIGHT") if event.key == pygame.K_LEFT: - keys.append('LEFT') + keys.append("LEFT") elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: mousex, mousey = pygame.mouse.get_pos() return (True, [mousex, mousey], keys) @@ -163,17 +171,17 @@ def __init__(self, sock): def waitForMessage(self, header): while self.running: if self.ready: - if self.header == 'OVER': - return [False, 'OVER', None] + if self.header == "OVER": + return [False, "OVER", None] if self.header == header: head, data = self.getDataAndRelease() - return [True, head, data] # object is there, ready to be read + return [True, head, data] # object is there, ready to be read else: # JUNK - print('got', self.header, 'instead of', header, ', still waiting') + print("got", self.header, "instead of", header, ", still waiting") self.getDataAndRelease() else: # don't check too much time.sleep(0.01) - return [False, 'OVER', None] # if thread stoped in the meantime + return [False, "OVER", None] # if thread stoped in the meantime # unary copy of the data and release the socket to make it ready to read more data def getDataAndRelease(self): @@ -187,12 +195,12 @@ def run(self): if not self.ready: try: self.header, self.data = recvData(self.sock) - if self.header == 'OVER': + if self.header == "OVER": self.running = False self.ready = True except Exception as e: - #print("GOT EXCEPTION IN MAIN LOOP") - #print(e) + # print("GOT EXCEPTION IN MAIN LOOP") + # print(e) pass time.sleep(0.01) self.sock.close() @@ -213,34 +221,35 @@ def __init__(self, host, port, nick): def run(self): self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - #self.sock.settimeout(0.01) - #create socket and connect - print('connecting to' , (self.host, self.port)) + # self.sock.settimeout(0.01) + # create socket and connect + print("connecting to", (self.host, self.port)) self.sock.connect((self.host, self.port)) # get player color - msg = waitForMessage(self.sock, 'COLR') + msg = waitForMessage(self.sock, "COLR") print("I'll be player ", msg) if msg == "white": self.localPlayer = WHITE else: self.localPlayer = BLACK # send nickname - sendData(self.sock, 'NICK', self.nick) + sendData(self.sock, "NICK", self.nick) # get replay filename - self.replayURL = waitForMessage(self.sock, 'URLR') + self.replayURL = waitForMessage(self.sock, "URLR") print("Game replay at:", self.replayURL) # get his nickname - self.opponent = waitForMessage(self.sock, 'NICK') + self.opponent = waitForMessage(self.sock, "NICK") print("Playing against", self.opponent) self.done = True while self.running: time.sleep(0.1) - + # return [sock, sockThread, localPlayer] + # **************************************************************************** @@ -261,14 +270,14 @@ def mainGameState(screen, localPlayer, sockThread, sock, board): screen.blit(text, (10, 10)) # other player's turn: if turn != localPlayer: - loop, _ , _ = events() # grab events and throw them away:) + loop, _, _ = events() # grab events and throw them away:) if sockThread.ready: - if sockThread.header == 'OVER': + if sockThread.header == "OVER": winner = localPlayer loop = False continue - elif sockThread.header != 'BORD': - print('got', sockThread.header, 'instead of BORD, still waiting') + elif sockThread.header != "BORD": + print("got", sockThread.header, "instead of BORD, still waiting") sockThread.ready = False continue else: @@ -292,10 +301,12 @@ def mainGameState(screen, localPlayer, sockThread, sock, board): if clickCell: cell = board.click(mpos) if cell: - sendData(sock, 'MOVE', [clickCell[0], clickCell[1], cell[0], cell[1]]) - loop, header, valid = sockThread.waitForMessage('VALD') + sendData( + sock, "MOVE", [clickCell[0], clickCell[1], cell[0], cell[1]] + ) + loop, header, valid = sockThread.waitForMessage("VALD") if valid: - loop, header, newBoard = sockThread.waitForMessage('BORD') + loop, header, newBoard = sockThread.waitForMessage("BORD") if loop: board.updateFromString(newBoard) turn = not turn @@ -304,19 +315,19 @@ def mainGameState(screen, localPlayer, sockThread, sock, board): if clickCell: piece = board.board[clickCell[0]][clickCell[1]] else: - piece = '' - if piece == '': + piece = "" + if piece == "": clickCell = None elif turn == WHITE: - if board.board[clickCell[0]][clickCell[1]][1] != 'w': + if board.board[clickCell[0]][clickCell[1]][1] != "w": clickCell = None elif turn == BLACK: - if board.board[clickCell[0]][clickCell[1]][1] != 'b': + if board.board[clickCell[0]][clickCell[1]][1] != "b": clickCell = None # check wether we have a winner if board.winner: - if board.winner == 'w': + if board.winner == "w": winner = WHITE else: winner = BLACK @@ -330,46 +341,56 @@ def mainGameState(screen, localPlayer, sockThread, sock, board): # prevents abrupt ending by displaying the complete board def endGameState(screen, winner, localPlayer, board): - replay_sprite = pygame.image.load('data/replay.png').convert(24) - newGame_sprite = pygame.image.load('data/new_game.png').convert(24) + replay_sprite = pygame.image.load("data/replay.png").convert(24) + newGame_sprite = pygame.image.load("data/new_game.png").convert(24) loop = True font = pygame.font.Font(None, 50) while loop: - loop, mpos , _ = events() + loop, mpos, _ = events() if mpos: x, y = mpos - if x>50 and x<206 and y>288 and y<338: - print('r') - return 'r' - elif x>306 and x<462 and y>288 and y<338: - print('new game') - return 'g' + if x > 50 and x < 206 and y > 288 and y < 338: + print("r") + return "r" + elif x > 306 and x < 462 and y > 288 and y < 338: + print("new game") + return "g" screen.fill(black) board.draw(screen, None, None) if winner == localPlayer: text = font.render("You WON !", 1, (255, 0, 0)) else: text = font.render("You lose...", 1, (255, 0, 0)) - screen.blit(text,(int(W/2.-text.get_width()/2.), int(H/2.-text.get_height()/2.))) - screen.blit(replay_sprite,(50, 3*H/4)) - screen.blit(newGame_sprite,(306, 3*H/4)) + screen.blit( + text, + ( + int(W / 2.0 - text.get_width() / 2.0), + int(H / 2.0 - text.get_height() / 2.0), + ), + ) + screen.blit(replay_sprite, (50, 3 * H / 4)) + screen.blit(newGame_sprite, (306, 3 * H / 4)) pygame.display.update() time.sleep(0.01) - return 'n' # nothing + return "n" # nothing + # intro state waiting for opponenent -def introGameState(screen, board, connectionThread = None): +def introGameState(screen, board, connectionThread=None): loop = True t0 = time.time() font = pygame.font.Font(None, 50) text = font.render("Waiting for opponent . .", 1, (255, 0, 0)) - posMess = (int(W/2.-text.get_width()/2.), int(H/2.-text.get_height()/2.)) + posMess = ( + int(W / 2.0 - text.get_width() / 2.0), + int(H / 2.0 - text.get_height() / 2.0), + ) while not connectionThread.done and loop: - loop, _ , _ = events() + loop, _, _ = events() screen.fill(black) board.draw(screen, None, None) - message = "Waiting for opponent "+'. '*(int(2*time.time()-2*t0) % 4) + message = "Waiting for opponent " + ". " * (int(2 * time.time() - 2 * t0) % 4) text = font.render(message, 1, (255, 0, 0)) screen.blit(text, posMess) pygame.display.update() @@ -378,28 +399,25 @@ def introGameState(screen, board, connectionThread = None): return loop - # regular two player game over network def networkGame(argv, screen, sprite_board, sprite_pieces, sniper): board = BoardPlayer(sprite_pieces, sprite_board, sniper) PORT = 8887 - HOST = "sxbn.org" - NICK = "anon_%d"%(random.randint(0,100)) + HOST = "sxbn.org" + NICK = "anon_%d" % (random.randint(0, 100)) if len(argv) == 1: print("Usage:\n\t", argv[0], "NICKNAME HOST PORT") if len(argv) > 1: - NICK = argv[1].replace(' ', '_') + NICK = argv[1].replace(" ", "_") if len(argv) > 2: HOST = argv[2] if len(argv) > 3: PORT = int(argv[3]) - print("connecting to", HOST+":"+str(PORT)) - + print("connecting to", HOST + ":" + str(PORT)) - localPlayer = None connectionThread = ConnectionThread(HOST, PORT, NICK) connectionThread.start() @@ -415,11 +433,10 @@ def networkGame(argv, screen, sprite_board, sprite_pieces, sniper): url = connectionThread.replayURL connectionThread.running = False connectionThread.join() - - pygame.display.set_caption(NICK+" Vs. "+opponent) + + pygame.display.set_caption(NICK + " Vs. " + opponent) sockThread = SockThread(sock) sockThread.start() - winner = mainGameState(screen, localPlayer, sockThread, sock, board) whatNext = endGameState(screen, winner, localPlayer, board) @@ -432,18 +449,14 @@ def networkGame(argv, screen, sprite_board, sprite_pieces, sniper): except: pass - if whatNext == 'r': + if whatNext == "r": replay(url, screen, sprite_board, sprite_pieces, sniper) - elif whatNext == 'g': + elif whatNext == "g": networkGame(argv, screen, sprite_board, sprite_pieces, sniper) - - - - def replay(url, screen, sprite_board, sprite_pieces, sniper): - + fic = urllib.request.urlopen(url) matchup = fic.readline() pygame.display.set_caption(matchup) @@ -459,37 +472,37 @@ def replay(url, screen, sprite_board, sprite_pieces, sniper): loop = True step = 0 while loop: - loop, _ , keys = events() + loop, _, keys = events() # reset to initial state - if 'SPACE' in keys: + if "SPACE" in keys: step = 0 turn = WHITE # one step forward - if 'RIGHT' in keys: + if "RIGHT" in keys: step += 1 turn = not turn if step >= len(boards): step = 0 turn = WHITE # one step backward - if 'LEFT' in keys: + if "LEFT" in keys: step -= 1 if step < 0: - step = len(boards)-1 + step = len(boards) - 1 if step % 2 == 0: turn = WHITE else: turn = BLACK screen.fill(black) - screen.blit(sprite_board, (0,0)) + screen.blit(sprite_board, (0, 0)) boards[step].draw(screen, None, turn) font = pygame.font.Font(None, 20) if turn: text = font.render("White", 1, (0, 0, 0)) else: text = font.render("Black", 1, (0, 0, 0)) - screen.blit(text,(10, 10)) + screen.blit(text, (10, 10)) pygame.display.update() time.sleep(0.01) @@ -497,22 +510,15 @@ def replay(url, screen, sprite_board, sprite_pieces, sniper): # out of the game loop - - - - - - -if __name__ == '__main__': +if __name__ == "__main__": pygame.init() pygame.mixer.init() - screen = pygame.display.set_mode((W,H)) + screen = pygame.display.set_mode((W, H)) sprite_board, sprite_pieces, sniper = loadData() - if len(sys.argv) > 2: - if sys.argv[1] == '-p': + if sys.argv[1] == "-p": if len(sys.argv) < 3: print("missing replay file") sys.exit() @@ -520,8 +526,6 @@ def replay(url, screen, sprite_board, sprite_pieces, sniper): pygame.quit() sys.exit() - networkGame(sys.argv, screen, sprite_board, sprite_pieces, sniper) pygame.quit() sys.exit() - diff --git a/communication.py b/battlechess_standalone/communication.py similarity index 63% rename from communication.py rename to battlechess_standalone/communication.py index 5bd4f09..acf8572 100644 --- a/communication.py +++ b/battlechess_standalone/communication.py @@ -1,4 +1,4 @@ -KNOWN_HEADERS = ['NICK', 'COLR', 'OVER', 'URLR', 'MOVE', 'BORD', 'VALD'] +KNOWN_HEADERS = ["NICK", "COLR", "OVER", "URLR", "MOVE", "BORD", "VALD"] # NICK : Nickname, data is a string # COLR : Player color, data is a string @@ -11,43 +11,43 @@ # generic tow way conversion from raw data to string representation def dataToString(header, data): - if header in ['NICK', 'COLR', 'URLR', 'BORD']: + if header in ["NICK", "COLR", "URLR", "BORD"]: return data - elif header == 'OVER': - return 'None' - elif header == 'MOVE': - return str(data[0])+str(data[1])+str(data[2])+str(data[3]) - elif header == 'VALD': + elif header == "OVER": + return "None" + elif header == "MOVE": + return str(data[0]) + str(data[1]) + str(data[2]) + str(data[3]) + elif header == "VALD": if data: - return 'T' + return "T" else: - return 'F' + return "F" else: - print('Unknown message type :', header) + print("Unknown message type :", header) raise ValueError() # and from string to raw data def stringToData(header, data): - if header in ['NICK', 'COLR', 'URLR', 'BORD']: + if header in ["NICK", "COLR", "URLR", "BORD"]: return data - elif header == 'OVER': + elif header == "OVER": return None - elif header == 'MOVE': + elif header == "MOVE": return [int(data[0]), int(data[1]), int(data[2]), int(data[3])] - elif header == 'VALD': - return data == 'T' + elif header == "VALD": + return data == "T" else: - print('Unknown message type :', header) + print("Unknown message type :", header) raise ValueError() # generic receive function def myreceive(sock, MSGLEN): - msg = '' + msg = "" while len(msg) < MSGLEN: - chunk = sock.recv(MSGLEN-len(msg)) - if chunk == '': + chunk = sock.recv(MSGLEN - len(msg)) + if chunk == "": raise RuntimeError("socket connection broken") msg = msg + chunk.decode("utf-8") return msg @@ -60,19 +60,19 @@ def sendData(sock, header, data): pack += "%05d" % (len(datas)) pack += datas # sock.send(pack) - sock.send(pack.encode('utf-8')) + sock.send(pack.encode("utf-8")) if header not in KNOWN_HEADERS: - print('Unknown message type :', header) + print("Unknown message type :", header) # receive a message and return the proper object def recvData(sock): header = myreceive(sock, 4) - size = int(myreceive(sock, 5)) - datas = myreceive(sock, size) - data = stringToData(header, datas) + size = int(myreceive(sock, 5)) + datas = myreceive(sock, size) + data = stringToData(header, datas) if header not in KNOWN_HEADERS: - print('Unknown message type :', header) + print("Unknown message type :", header) return list([header, data]) diff --git a/server.py b/battlechess_standalone/server.py similarity index 64% rename from server.py rename to battlechess_standalone/server.py index 7029fec..cd4d580 100644 --- a/server.py +++ b/battlechess_standalone/server.py @@ -2,11 +2,12 @@ import socket import sys -import traceback import threading import time -from core.Board import Board -from communication import sendData, recvData, waitForMessage +import traceback + +from battlechess.core.Board import Board +from .communication import recvData, sendData, waitForMessage class GameThread(threading.Thread): @@ -19,18 +20,18 @@ def __init__(self, client_1, client_2): self.board = Board() def run(self): - self.nick_1 = waitForMessage(self.client_1, 'NICK') - self.nick_2 = waitForMessage(self.client_2, 'NICK') + self.nick_1 = waitForMessage(self.client_1, "NICK") + self.nick_2 = waitForMessage(self.client_2, "NICK") filename = time.strftime("games/%Y_%m_%d_%H_%M_%S") - filename += "_"+self.nick_1+"_Vs_"+self.nick_2+".txt" + filename += "_" + self.nick_1 + "_Vs_" + self.nick_2 + ".txt" log = open(filename, "w") - log.write(self.nick_1+' Vs. '+self.nick_2+'\n') + log.write(self.nick_1 + " Vs. " + self.nick_2 + "\n") - sendData(self.client_1, 'URLR', "http://git.sxbn.org/battleChess/"+filename) - sendData(self.client_2, 'URLR', "http://git.sxbn.org/battleChess/"+filename) + sendData(self.client_1, "URLR", "http://git.sxbn.org/battleChess/" + filename) + sendData(self.client_2, "URLR", "http://git.sxbn.org/battleChess/" + filename) - sendData(self.client_1, 'NICK', self.nick_2) - sendData(self.client_2, 'NICK', self.nick_1) + sendData(self.client_1, "NICK", self.nick_2) + sendData(self.client_2, "NICK", self.nick_1) loop = True try: @@ -41,24 +42,24 @@ def run(self): if head == "OVER": loop = False raise ValueError() # jump to finaly - elif head == 'MOVE': + elif head == "MOVE": i, j, ii, jj = move - valid, pos, msg = self.board.move(i, j, ii, jj, 'w') + valid, pos, msg = self.board.move(i, j, ii, jj, "w") # print "got move from", [i,j], "to", [ii,jj], "from white", valid - sendData(self.client_1, 'VALD', valid) + sendData(self.client_1, "VALD", valid) else: - print('error : server was expecting MOVE, not', head) + print("error : server was expecting MOVE, not", head) raise ValueError() log.write("%d %d %d %d\n" % (i, j, ii, jj)) if self.board.winner: # if we have a winner, send the whole board endBoard = self.board.toString() - sendData(self.client_1, 'BORD', endBoard) - sendData(self.client_2, 'BORD', endBoard) + sendData(self.client_1, "BORD", endBoard) + sendData(self.client_2, "BORD", endBoard) break # game is over else: - sendData(self.client_1, 'BORD', self.board.toString('w')) - sendData(self.client_2, 'BORD', self.board.toString('b')) + sendData(self.client_1, "BORD", self.board.toString("w")) + sendData(self.client_2, "BORD", self.board.toString("b")) valid = False while not valid: @@ -66,24 +67,24 @@ def run(self): if head == "OVER": loop = False raise ValueError() # jump to finaly - elif head == 'MOVE': + elif head == "MOVE": i, j, ii, jj = move - valid, pos, msg = self.board.move(i, j, ii, jj, 'b') + valid, pos, msg = self.board.move(i, j, ii, jj, "b") # print "got move from", [i,j], "to", [ii,jj], "from black", valid - sendData(self.client_2, 'VALD', valid) + sendData(self.client_2, "VALD", valid) else: - print('error : server was expecting MOVE, not', head) + print("error : server was expecting MOVE, not", head) raise ValueError() log.write("%d %d %d %d\n" % (i, j, ii, jj)) if self.board.winner: # if we have awinner, send the whole board endBoard = self.board.toString() - sendData(self.client_1, 'BORD', endBoard) - sendData(self.client_2, 'BORD', endBoard) + sendData(self.client_1, "BORD", endBoard) + sendData(self.client_2, "BORD", endBoard) break # game is over else: - sendData(self.client_1, 'BORD', self.board.toString('w')) - sendData(self.client_2, 'BORD', self.board.toString('b')) + sendData(self.client_1, "BORD", self.board.toString("w")) + sendData(self.client_2, "BORD", self.board.toString("b")) except Exception as e: print(e) traceback.print_exc(file=sys.stdout) @@ -92,15 +93,15 @@ def run(self): # print "finishing the game" log.flush() log.close() - sendData(self.client_1, 'OVER', None) - sendData(self.client_2, 'OVER', None) + sendData(self.client_1, "OVER", None) + sendData(self.client_2, "OVER", None) self.client_1.close() self.client_2.close() -if __name__ == '__main__': +if __name__ == "__main__": PORT = 8887 - HOST = '' # default value + HOST = "" # default value if len(sys.argv) == 1: print("Usage:\n\t", sys.argv[0], "PORT") if len(sys.argv) > 1: @@ -116,7 +117,7 @@ def run(self): # register SIGINT to close socket def signal_handler(signal, frame): - print('You pressed Ctrl+C!') + print("You pressed Ctrl+C!") serversocket.close() sys.exit(0) @@ -127,9 +128,9 @@ def signal_handler(signal, frame): try: # accept connections from outside (client_1, address) = serversocket.accept() - sendData(client_1, 'COLR', 'white') + sendData(client_1, "COLR", "white") (client_2, address) = serversocket.accept() - sendData(client_2, 'COLR', 'black') + sendData(client_2, "COLR", "black") game = GameThread(client_1, client_2) game.start() except Exception as e: diff --git a/core/Board.py b/core/Board.py deleted file mode 100644 index aa4481d..0000000 --- a/core/Board.py +++ /dev/null @@ -1,598 +0,0 @@ -RQBPOS = [0, 0] -RKBPOS = [0, 7] -RQWPOS = [7, 0] -RKWPOS = [7, 7] -KBPOS = [0, 4] -KWPOS = [7, 4] -CASTLEQBPOS = [0, 2] -CASTLEKBPOS = [0, 6] -CASTLEQWPOS = [7, 2] -CASTLEKWPOS = [7, 6] -CASTLEABLE = sorted(['kb', 'kw', 'rqb', 'rkb', 'rqw', 'rkw']) - - -class Board(object): - def __init__(self): - self.reset() - self.taken = [] - self.castleable = list(CASTLEABLE) - self.enpassant = -1 - self.winner = None - self.enpassant = -1 - - def reset(self): - self.board = [['' for i in range(8)] for j in range(8)] - self.board[0] = ['rb', 'nb', 'bb', 'qb', 'kb', 'bb', 'nb', 'rb'] - self.board[1] = ['pb', 'pb', 'pb', 'pb', 'pb', 'pb', 'pb', 'pb'] - self.board[6] = ['pw', 'pw', 'pw', 'pw', 'pw', 'pw', 'pw', 'pw'] - self.board[7] = ['rw', 'nw', 'bw', 'qw', 'kw', 'bw', 'nw', 'rw'] - - # make a deep copy of the board - def copy(self): - res = Board() - res.taken = list(self.taken) - res.board = [list(b) for b in self.board] - res.castleable = list(self.castleable) - res.enpassant = self.enpassant - res.winner = self.winner - return res - - def isIn(self, i, j): - return i > -1 and i < 8 and j > -1 and j < 8 - - # tells wether a cell is free or not - # returns : - # 1 if the cell is empty - # 2 if the cell is occupied by a different player than the one given - # 0 if the cell is out of bound - def isFree(self, i, j, c=''): - if self.isIn(i, j): - if self.board[i][j] == '': - return 1 - if self.board[i][j][1] != c: - return 2 - else: - return 0 - - def takeEnPassant(self, i, j, ii, jj): - if self.board[i][j][0] == 'p' and jj == self.enpassant and (j == self.enpassant-1 or j == self.enpassant+1): - if(self.board[i][j][1] == 'w' and i == 3 or self.board[i][j][1] == 'b' and i == 4): - # todo remove this error check - if self.board[i][self.enpassant] == '': - print("Nothing at " + str([i, self.enpassant]) + ". Assuming you killed normally") - else: - self.taken.append(str(self.board[i][self.enpassant])) - self.board[i][self.enpassant] = '' - - def castleInfo(self, piece, i, j, ii, jj): - if piece in self.castleable: - if piece[1] == 'w': - if [KWPOS, CASTLEQWPOS] == [[i, j], [ii, jj]] and 'rqw' in self.castleable: - return 'rqw' - if [KWPOS, CASTLEKWPOS] == [[i, j], [ii, jj]] and 'rkw' in self.castleable: - return 'rkw' - elif piece[1] == 'b': - if [KBPOS, CASTLEQBPOS] == [[i, j], [ii, jj]] and 'rqb' in self.castleable: - return 'rqb' - if [KBPOS, CASTLEKBPOS] == [[i, j], [ii, jj]] and 'rkb' in self.castleable: - return 'rkb' - return '' - - def getRookReach(self, i, j, color): - a = 1 - pos = [] - while a: - f = self.isFree(i, j+a, color) - if f: - pos.append([i, j+a]) - a += 1 - if f == 2: - break - else: - break - a = 1 - while True: - f = self.isFree(i, j-a, color) - if f: - pos.append([i, j-a]) - a += 1 - if f == 2: - break - else: - break - a = 1 - while True: - f = self.isFree(i + a, j, color) - if f: - pos.append([i + a, j]) - a += 1 - if f == 2: - break - else: - break - a = 1 - while True: - f = self.isFree(i-a, j, color) - if f: - pos.append([i-a, j]) - a += 1 - if f == 2: - break - else: - break - return pos - - def getBishopReach(self, i, j, color): - a = 1 - pos = [] - while True: - f = self.isFree(i + a, j+a, color) - if f: - pos.append([i + a, j+a]) - a += 1 - if f == 2: - break - else: - break - a = 1 - while True: - f = self.isFree(i-a, j-a, color) - if f: - pos.append([i-a, j-a]) - a += 1 - if f == 2: - break - else: - break - a = 1 - while True: - f = self.isFree(i+a, j-a, color) - if f: - pos.append([i+a, j-a]) - a += 1 - if f == 2: - break - else: - break - a = 1 - while True: - f = self.isFree(i-a, j+a, color) - if f: - pos.append([i-a, j+a]) - a += 1 - if f == 2: - break - else: - break - return pos - - def getReachablePosition(self, i, j): - c = self.board[i][j] - pos = [] - if c == '': - return [] - color = c[1] - if c == 'pb': - if self.isFree(i+1, j, 'b') == 1: - pos.append([i+1, j]) - if self.isFree(i+1, j+1, 'b') == 2: - pos.append([i+1, j+1]) - if self.isFree(i+1, j-1, 'b') == 2: - pos.append([i+1, j-1]) - if i == 1 and self.isFree(i + 2, j) == 1 \ - and self.isFree(i + 1, j) == 1: - pos.append([i+2, j]) - if self.enpassant != -1 and i == 4 and (j == self.enpassant-1 or j == self.enpassant+1): - pos.append([i+1, self.enpassant]) - elif c == 'pw': - if self.isFree(i-1, j, 'w') == 1: - pos.append([i-1, j]) - if self.isFree(i-1, j+1, 'w') == 2: - pos.append([i-1, j+1]) - if self.isFree(i-1, j-1, 'w') == 2: - pos.append([i-1, j-1]) - if i == 6 and self.isFree(i - 2, j) == 1 and self.isFree(i - 1, - j) == 1: - - pos.append([i-2, j]) - if self.enpassant != -1 and i == 3 and (j == self.enpassant-1 or j == self.enpassant+1): - pos.append([i-1, self.enpassant]) - elif c[0] == 'k': - pos.append([i, j+1]) - pos.append([i, j-1]) - pos.append([i+1, j+1]) - pos.append([i+1, j-1]) - pos.append([i+1, j]) - pos.append([i-1, j+1]) - pos.append([i-1, j-1]) - pos.append([i-1, j]) - if c in self.castleable: - if c[1] == 'w': - if 'rqw' in self.castleable: - if self.isFree(7, 1) == 1 and self.isFree( - 7, 2) == 1 and self.isFree(7, 3) == 1: - - pos.append([7, 2]) - if 'rkw' in self.castleable: - if self.isFree(7, 6) == 1 and self.isFree(7, 5) == 1: - pos.append([7, 6]) - elif c[1] == 'b': - if 'rqb' in self.castleable: - if self.isFree(0, 1) == 1 and self.isFree(0, 2) == 1 \ - and self.isFree(0, 3) == 1: - pos.append([0, 2]) - if 'rkb' in self.castleable: - if self.isFree(0, 6) == 1 and self.isFree(0, 5) == 1: - pos.append([0, 6]) - elif c[0] == 'n': - pos.append([i+1, j+2]) - pos.append([i+1, j-2]) - pos.append([i-1, j+2]) - pos.append([i-1, j-2]) - pos.append([i+2, j+1]) - pos.append([i+2, j-1]) - pos.append([i-2, j+1]) - pos.append([i-2, j-1]) - elif c[0] == 'r': - pos = self.getRookReach(i, j, color) - elif c[0] == 'b': - pos = self.getBishopReach(i, j,color) - elif c[0] == 'q': - # rook - pos.extend(self.getRookReach(i, j,color)) - # bishop - pos.extend(self.getBishopReach(i, j,color)) - res = [] - for p in pos: - if self.isFree(p[0], p[1], color): - res.append(p) - return list(res) - - def getPossiblePosition(self, i, j): - c = self.board[i][j] - pos = [] - if c == '': - return [] - color = c[1] - if c == 'pb': - pos.append([i+1, j]) - if self.isFree(i+1, j+1, 'b') == 2: - pos.append([i+1, j+1]) - if self.isFree(i+1, j-1, 'b') == 2: - pos.append([i+1, j-1]) - if i == 1 and self.isFree(i+1, j) == 1: - pos.append([i+2, j]) - if self.enpassant != -1 and i == 4 and (j == self.enpassant-1 or j == self.enpassant+1): - pos.append([i+1, self.enpassant]) - elif c == 'pw': - pos.append([i-1, j]) - if self.isFree(i-1, j+1, 'w') == 2: - pos.append([i-1, j+1]) - if self.isFree(i-1, j-1, 'w') == 2: - pos.append([i-1, j-1]) - if i == 6 and self.isFree(i-1, j) == 1: - pos.append([i-2, j]) - if self.enpassant != -1 and i == 3 and (j == self.enpassant-1 or j == self.enpassant+1): - pos.append([i-1, self.enpassant]) - elif c[0] == 'k': - pos.append([i, j+1]) - pos.append([i, j-1]) - pos.append([i+1, j+1]) - pos.append([i+1, j-1]) - pos.append([i+1, j]) - pos.append([i-1, j+1]) - pos.append([i-1, j-1]) - pos.append([i-1, j]) - if c in self.castleable: - if c[1] == 'w': - if 'rqw' in self.castleable: - pos.append([7, 2]) - if 'rkw' in self.castleable: - pos.append([7, 6]) - elif c[1] == 'b': - if 'rqb' in self.castleable: - pos.append([0, 2]) - if 'rkb' in self.castleable: - pos.append([0, 6]) - elif c[0] == 'n': - pos.append([i+1, j+2]) - pos.append([i+1, j-2]) - pos.append([i-1, j+2]) - pos.append([i-1, j-2]) - pos.append([i+2, j+1]) - pos.append([i+2, j-1]) - pos.append([i-2, j+1]) - pos.append([i-2, j-1]) - elif c[0] == 'r': - for a in range(1, 8): - pos.append([i, j+a]) - pos.append([i+a, j]) - pos.append([i, j-a]) - pos.append([i-a, j]) - elif c[0] == 'b': - for a in range(1, 8): - pos.append([i+a, j+a]) - pos.append([i-a, j+a]) - pos.append([i+a, j-a]) - pos.append([i-a, j-a]) - elif c[0] == 'q': - for a in range(1, 8): - pos.append([i, j+a]) - pos.append([i+a, j]) - pos.append([i, j-a]) - pos.append([i-a, j]) - pos.append([i+a, j+a]) - pos.append([i-a, j+a]) - pos.append([i+a, j-a]) - pos.append([i-a, j-a]) - res = [] - for p in pos: - if self.isIn(*p): - res.append(p) - return list(res) - - def getClosest(self, i, j, ti, tj, reach): - d = 20 - res = None - if ti - i == 0: - di = 0 - elif ti - i < 0: - di = -1 - else: - di = 1 - if tj - j == 0: - dj = 0 - elif tj - j < 0: - dj = -1 - else: - dj = 1 - for a in range(7, 0, -1): - if [i+a*di, j+a*dj] in reach: - return [i+a*di, j+a*dj] - return None - - def move(self, i, j, ii, jj, color=None): - - # are given position inside the board ? - if not self.isIn(i, j) or not self.isIn(ii, jj): - return False, [], f'{i}{j} or {ii} {jj} position outside the board' - # is there a piece in the source square ? - if not self.board[i][j]: - return False, [], f'{i}{j} is empty' - # the player is trying to move a piece from the other player - if color and self.board[i][j][1] != color: - return False, [], '{}{} is not {}'.format(i,j,'white' if color == 'w' else 'black') - if self.board[ii][jj] != '' and self.board[i][j][1] == self.board[ii][jj][1]: - # same color - return False, [], f'{ii}{jj} is occupied by your own {self.board[ii][jj]} piece' - reach = self.getReachablePosition(i, j) # actually possible destination (obstacles, ennemies) - pos = self.getPossiblePosition(i, j) # anything in the range of the piece - if [ii, jj] not in pos: - print(f'Possible positions {pos}') - return False, [], f'{ii}{jj} is not a {self.board[i][j]} possible position for some reason' - elif [ii, jj] not in reach: - res = self.getClosest(i, j, ii, jj, reach) - if res: - ii, jj = res - else: - return False, [], f'{ii}{jj} is not reachable' - if self.board[ii][jj] != '': - self.taken.append(str(self.board[ii][jj])) - - # check if we killed in passant - self.takeEnPassant(i, j, ii, jj) - # reset enpassant value - self.enpassant = -1 - # the pawn jumped, set it as 'en passant' pawn - if self.board[i][j][0] == 'p': - if self.board[i][j][1] == 'b' and i == 1 and ii == 3: - self.enpassant = j - elif self.board[i][j][1] == 'w' and i == 6 and ii == 4: - self.enpassant = j - - # replace destination with origin - self.board[ii][jj] = self.board[i][j] - self.board[i][j] = '' - - # if a pawn reached the end of the board, it becomse a queen - if self.board[ii][jj][0] == 'p' and (ii == 0 or ii == 7): - self.board[ii][jj] = 'q'+self.board[ii][jj][1] - - # if we were performing a castle, move the tower too - whichRock = self.castleInfo(self.board[ii][jj], i, j, ii, jj) - if whichRock == 'rqb': - self.board[0][0] = '' - self.board[0][3] = 'rb' - elif whichRock == 'rkb': - self.board[0][7] = '' - self.board[0][5] = 'rb' - elif whichRock == 'rqw': - self.board[7][0] = '' - self.board[7][3] = 'rw' - elif whichRock == 'rkw': - self.board[7][7] = '' - self.board[7][5] = 'rw' - # if k or r, castle for that piece forbidden in the future - if self.board[ii][jj][0] == 'k' and self.board[ii][jj] in self.castleable: - self.castleable.remove(self.board[ii][jj]) - elif self.board[ii][jj][0] == 'r': - if [i, j] == RQBPOS and 'rqb' in self.castleable: - self.castleable.remove('rqb') - elif [i, j] == RKBPOS and 'rkb' in self.castleable: - self.castleable.remove('rkb') - elif [i, j] == RQWPOS and 'rqw' in self.castleable: - self.castleable.remove('rqw') - elif [i, j] == RKWPOS and 'rkw' in self.castleable: - self.castleable.remove('rkw') - - # check if we have a winner - if 'kb' in self.taken: - self.winner = 'w' - elif 'kw' in self.taken: - self.winner = 'b' - - return True, [i, j, ii, jj], 'OK' - - # save the state of the board for a given player (or full state) - def dump(self, color=None): - boardCopy = self.copy() - # hide other player - if color: - visibility = [[False for i in range(8)] for j in range(8)] - for i in range(8): - for j in range(8): - if boardCopy.board[i][j].endswith(color): - for di in range(-1, 2): - for dj in range(-1, 2): - if boardCopy.isIn(i+di, j+dj): - visibility[i+di][j+dj] = True - for i in range(8): - for j in range(8): - if not visibility[i][j]: - boardCopy.board[i][j] = '' - return boardCopy - - # TODO refactor string representation of board - # dump as a string to ease portability with other apps - def toString(self, color=None): - visibility = [[True for i in range(8)] for j in range(8)] - if color: # hide if necessary - visibility = [[False for i in range(8)] for j in range(8)] - for i in range(8): - for j in range(8): - if self.board[i][j].endswith(color): - for di in range(-1, 2): - for dj in range(-1, 2): - if self.isIn(i+di, j+dj): - visibility[i+di][j+dj] = True - boardStr = '' - for i in range(8): - for j in range(8): - if not visibility[i][j]: - boardStr += '_' # 3 spaces - else: - boardStr += self.board[i][j] + '_' - boardStr = boardStr[:-1] # remove last '_' - takenStr = '_'.join(self.taken) - if color: - castleableStr = '_'.join([e for e in self.castleable if e.endswith(color)]) - else: - castleableStr = '_'.join([e for e in self.castleable]) - # todo only send enpassant if it's actually possible, otherwise we are leaking information - if color == 'b' and visibility[4][self.enpassant] is False: - enpassantStr = str(-1) - elif color == 'w' and visibility[3][self.enpassant] is False: - enpassantStr = str(-1) - else: - enpassantStr = str(self.enpassant) - if self.winner: - winnerStr = self.winner - else: - winnerStr = 'n' - res = boardStr+'#'+takenStr+'#'+castleableStr+'#'+enpassantStr+'#'+winnerStr - return res - - # update whole state from a string - def updateFromString(self, data): - boardStr, takenStr, castleableStr, enpassantStr, winnerStr = data.split('#') - for i, c in enumerate(boardStr.split('_')): - self.board[i//8][i % 8] = c - if takenStr == '': - self.taken = [] - else: - self.taken = takenStr.split('_') - if castleableStr == '': - self.castleable = [] - else: - self.castleable = castleableStr.split('_') - self.enpassant = int(enpassantStr) - if winnerStr == 'n': - self.winner = None - else: - self.winner = winnerStr - - def updateFromBoard(self, board): - self.taken = list(board.taken) - self.board = [list(b) for b in board.board] - self.castleable = list(board.castleable) - self.enpassant = board.enpassant - self.winner = board.winner - - def dbpiece2boardpiece(self, piecechar): - color = 'b' if piecechar.isupper() else 'w' - piece = '' if piecechar == '_' else piecechar.lower() + color - return piece - - def apicastle2board(self): - return { - 'L': 'rqb', - 'S': 'rkb', - 'K': 'kb', - 'l': 'rqw', - 's': 'rkw', - 'k': 'kw', - } - - def boardcastle2api(self): - return { - 'rqb': 'L', - 'rkb': 'S', - 'kb': 'K', - 'rqw': 'l', - 'rkw': 's', - 'kw': 'k', - } - - # TODO check what winner str format is Board() expecting. winner "white" "black" "None" - def updateFromElements(self, board, taken, castleable, enpassant, winner): - for i, c in enumerate(board): - self.board[i//8][i % 8] = self.dbpiece2boardpiece(c) - self.taken = [self.dbpiece2boardpiece(c) for c in taken] - self.castleable = sorted([self.apicastle2board()[c] for c in castleable]) - self.enpassant = ord(enpassant) - ord('a') if enpassant is not None else -1 - self.winner = winner - - def toElements(self, color=None): - - def bpiece(p): - return '_' if not p else p[0] if p[1] == 'w' else p[0].upper() - - if color: - boardString = self.toString(color) - pieces = boardString.split('#')[0].split('_') - apiboard = ''.join([bpiece(p) for p in pieces]) - else: - apiboard = ''.join([bpiece(p) for r in self.board for p in r ]) - - elements = { - "castleable" : ''.join(sorted([self.boardcastle2api()[c] for c in self.castleable])), - "taken" : ''.join([bpiece(p) for p in self.taken]), - "board" : apiboard - } - return elements - -if __name__ == '__main__': - board = Board() - - for i in range(8): - for j in range(8): - print("%4s" % board.board[i][j]) - print() - print(board.taken) - print(board.castleable) - print(board.winner) - - print(board.move(1, 2, 4, 2)) # invalid move - print(board.move(6, 2, 4, 2)) # valid move - - print('----------------') - - board.updateFromString(board.toString('w')) - - for i in range(8): - for j in range(8): - print("%4s" % board.board[i][j]) - print() - print(board.taken) - print(board.castleable) - print(board.winner) diff --git a/core/test_Board.py b/core/test_Board.py deleted file mode 100644 index f0ea2b5..0000000 --- a/core/test_Board.py +++ /dev/null @@ -1,123 +0,0 @@ -import unittest -import unittest.mock as mock - -from core.Board import Board - -class Test_Board(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def fakeElements(self): - elements = { - 'board': ('RNBQKBNR' - 'PPPPPPPP' - '________' - '________' - '________' - '________' - 'pppppppp' - 'rnbqkbnr'), - 'taken': '', - 'castleable': 'LSKlsk', - 'enpassant': None, - 'winner': None, - } - - return elements - - def test__updateFromElements__board(self): - b = Board() - b.reset() - - expected = b.toString() - - b.updateFromElements(**self.fakeElements()) - - startStrUpdated = b.toString() - - self.assertEqual(startStrUpdated, expected) - - def test__toElements__initialboard(self): - b = Board() - b.reset() - - elements = b.toElements() - - expected = { - 'board': 'RNBQKBNRPPPPPPPP________________________________pppppppprnbqkbnr', - 'castleable': 'KLSkls', - 'taken': '' - } - - self.assertDictEqual(elements, expected) - - def test__toElements__someboard(self): - b = Board() - b.reset() - b.castleable = sorted(['kb', 'kw', 'rkb']) - b.taken = ['bb', 'rb', 'rw', 'pw'] - - elements = b.toElements() - - expected = { - 'board': 'RNBQKBNRPPPPPPPP________________________________pppppppprnbqkbnr', - 'castleable': 'KSk', - 'taken': 'BRrp' - } - - self.assertDictEqual(elements, expected) - - def test__toElements__anotherboard(self): - b = Board() - b.reset() - b.board[0][2] = '' - b.board[0][5] = '' - b.board[0][7] = '' - b.board[1][3] = 'pw' - b.board[1][4] = '' - b.board[5][7] = 'bb' - b.board[6][3] = '' - b.board[6][5] = '' - b.board[7][0] = '' - b.castleable = sorted(['kb', 'kw', 'rkb']) - b.taken = ['bb', 'rb', 'rw', 'pw', 'pb'] - - elements = b.toElements() - - expected = { - 'board': 'RN_QK_N_PPPp_PPP_______________________________Bppp_p_pp_nbqkbnr', - 'castleable': 'KSk', - 'taken': 'BRrpP' - } - - self.assertDictEqual(elements, expected) - - def test__toElementsFiltered__anotherboard(self): - b = Board() - b.reset() - b.board[0][2] = '' - b.board[0][5] = '' - b.board[0][7] = '' - b.board[1][3] = 'pw' - b.board[1][4] = '' - b.board[5][7] = 'bb' - b.board[6][3] = '' - b.board[6][5] = '' - b.board[7][0] = '' - - b.castleable = sorted(['kb', 'kw', 'rkb']) - b.taken = ['bb', 'rb', 'rw', 'pw', 'pb'] - - elements = b.toElements('w') - - expected = { - 'board': '___QK_____Pp___________________________________Bppp_p_pp_nbqkbnr', - 'castleable': 'KSk', - 'taken': 'BRrpP' - } - - self.assertDictEqual(elements, expected) \ No newline at end of file diff --git a/core/test_btchBoard.py b/core/test_btchBoard.py deleted file mode 100644 index 6ffedb3..0000000 --- a/core/test_btchBoard.py +++ /dev/null @@ -1,164 +0,0 @@ -import unittest -import unittest.mock as mock - -from core.btchBoard import BtchBoard - - -class Test_BtchBoard(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def startboardStr(self): - return ('RNBQKBNR' - 'PPPPPPPP' - '________' - '________' - '________' - '________' - 'pppppppp' - 'rnbqkbnr') - - def fakeElements(self): - elements = { - 'board': self.startboardStr(), - 'taken': '', - 'castleable': 'LSKlsk', - 'enpassant': None, - 'winner': None, - } - - return elements - - def squares2ascii(self, squares): - return "\n".join( - "".join('x' if (i, j) in squares else '_' for j in range(0, 12)) for i in range(0, 12)) - - def test__factory(self): - btchBoard = BtchBoard.factory(self.startboardStr()) - - self.assertEqual(btchBoard.toElements(), self.fakeElements()) - - def test__isEnemy(self): - - result = BtchBoard.isEnemy('white', None) - - self.assertFalse(result) - - result = BtchBoard.isEnemy('white', 'p') - - self.assertFalse(result) - - result = BtchBoard.isEnemy('white', 'P') - - self.assertTrue(result) - - result = BtchBoard.isEnemy('white', '_') - - self.assertFalse(result) - - def test__rookMoves__emptyBoard(self): - - b = BtchBoard() - b.empty() - - moves = sorted(sq for sq in b.rookMoves('white', 2, 2)) - - print('moves\n{}'.format(self.squares2ascii(moves))) - - expected = sorted([(i, 2) for i in range(3, 10)] + [(2, j) for j in range(3, 10)]) - - print('expected\n{}'.format(self.squares2ascii(expected))) - - self.assertListEqual(moves, expected) - - def test__rookMoves__startBoard(self): - - b = BtchBoard() - - moves = sorted(sq for sq in b.rookMoves('black', 2, 2)) - - expected = [] - - self.assertListEqual(moves, expected) - - def test__bishopMoves__emptyBoard(self): - - b = BtchBoard() - b.empty() - - moves = sorted(sq for sq in b.bishopMoves('white', 6, 6)) - - print('moves\n{}'.format(self.squares2ascii(moves))) - - expected = sorted([(i, i) for i in range(2, 10)] + [(3 + i, 9 - i) for i in range(0, 7)]) - expected.remove((6, 6)) - expected.remove((6, 6)) - - print('expected\n {}'.format(self.squares2ascii(expected))) - - self.assertListEqual(moves, expected) - - def test__moves__pawn(self): - b = BtchBoard() - - moves = sorted(sq for sq in b.pawnMoves('white', 8, 6)) - - expected = [(6, 6), (7, 6)] - - self.assertListEqual(moves, expected) - - def test__moves__manyMoves(self): - pass - - def test__moves__enpassant(self): - pass - - def test__moves__notMovingForbidden(self): - pass - - #check that an impossible move is possible if fogged enemies - def test__moves__unknownInfo(self): - pass - - def test__filter__startPosition(self): - color = 'white' - b = BtchBoard() - b.filter(color) - - expectedBoardStr = ('________' - '________' - '________' - '________' - '________' - '________' - 'pppppppp' - 'rnbqkbnr') - - expected = BtchBoard.factory(expectedBoardStr) - expected.castleable = 'lsk' - - self.assertDictEqual(b.toElements(), expected.toElements()) - - def test__moves__fog(self): - boardStr = ('________' - '________' - '________' - '________' - '________' - '________' - 'ppppppp_' - 'rnbqkbnr') - - b = BtchBoard.factory(boardStr) - - print('fog {} '.format(b.toElements())) - - moves = b.possibleMoves('white', 9, 9) - - expectedMoves = sorted([(i, 9) for i in range(2, 9)]) - - self.assertListEqual(moves, expectedMoves) diff --git a/server/config.py b/example.env similarity index 100% rename from server/config.py rename to example.env diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..4620b45 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1596 @@ +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.6.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, + {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.9\""} + +[[package]] +name = "anyio" +version = "3.7.1" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.7" +files = [ + {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, + {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, +] + +[package.dependencies] +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"] +test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (<0.22)"] + +[[package]] +name = "appnope" +version = "0.1.4" +description = "Disable App Nap on macOS >= 10.9" +optional = false +python-versions = ">=3.6" +files = [ + {file = "appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c"}, + {file = "appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee"}, +] + +[[package]] +name = "asttokens" +version = "2.4.1" +description = "Annotate AST trees with source code positions" +optional = false +python-versions = "*" +files = [ + {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, + {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, +] + +[package.dependencies] +six = ">=1.12.0" + +[package.extras] +astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] +test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] + +[[package]] +name = "backcall" +version = "0.2.0" +description = "Specifications for callback functions passed in to an API" +optional = false +python-versions = "*" +files = [ + {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, + {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, +] + +[[package]] +name = "bcrypt" +version = "4.1.2" +description = "Modern password hashing for your software and your servers" +optional = false +python-versions = ">=3.7" +files = [ + {file = "bcrypt-4.1.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:ac621c093edb28200728a9cca214d7e838529e557027ef0581685909acd28b5e"}, + {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea505c97a5c465ab8c3ba75c0805a102ce526695cd6818c6de3b1a38f6f60da1"}, + {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57fa9442758da926ed33a91644649d3e340a71e2d0a5a8de064fb621fd5a3326"}, + {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb3bd3321517916696233b5e0c67fd7d6281f0ef48e66812db35fc963a422a1c"}, + {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6cad43d8c63f34b26aef462b6f5e44fdcf9860b723d2453b5d391258c4c8e966"}, + {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:44290ccc827d3a24604f2c8bcd00d0da349e336e6503656cb8192133e27335e2"}, + {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:732b3920a08eacf12f93e6b04ea276c489f1c8fb49344f564cca2adb663b3e4c"}, + {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1c28973decf4e0e69cee78c68e30a523be441972c826703bb93099868a8ff5b5"}, + {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b8df79979c5bae07f1db22dcc49cc5bccf08a0380ca5c6f391cbb5790355c0b0"}, + {file = "bcrypt-4.1.2-cp37-abi3-win32.whl", hash = "sha256:fbe188b878313d01b7718390f31528be4010fed1faa798c5a1d0469c9c48c369"}, + {file = "bcrypt-4.1.2-cp37-abi3-win_amd64.whl", hash = "sha256:9800ae5bd5077b13725e2e3934aa3c9c37e49d3ea3d06318010aa40f54c63551"}, + {file = "bcrypt-4.1.2-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:71b8be82bc46cedd61a9f4ccb6c1a493211d031415a34adde3669ee1b0afbb63"}, + {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e3c6642077b0c8092580c819c1684161262b2e30c4f45deb000c38947bf483"}, + {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:387e7e1af9a4dd636b9505a465032f2f5cb8e61ba1120e79a0e1cd0b512f3dfc"}, + {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f70d9c61f9c4ca7d57f3bfe88a5ccf62546ffbadf3681bb1e268d9d2e41c91a7"}, + {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2a298db2a8ab20056120b45e86c00a0a5eb50ec4075b6142db35f593b97cb3fb"}, + {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:ba55e40de38a24e2d78d34c2d36d6e864f93e0d79d0b6ce915e4335aa81d01b1"}, + {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:3566a88234e8de2ccae31968127b0ecccbb4cddb629da744165db72b58d88ca4"}, + {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b90e216dc36864ae7132cb151ffe95155a37a14e0de3a8f64b49655dd959ff9c"}, + {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:69057b9fc5093ea1ab00dd24ede891f3e5e65bee040395fb1e66ee196f9c9b4a"}, + {file = "bcrypt-4.1.2-cp39-abi3-win32.whl", hash = "sha256:02d9ef8915f72dd6daaef40e0baeef8a017ce624369f09754baf32bb32dba25f"}, + {file = "bcrypt-4.1.2-cp39-abi3-win_amd64.whl", hash = "sha256:be3ab1071662f6065899fe08428e45c16aa36e28bc42921c4901a191fda6ee42"}, + {file = "bcrypt-4.1.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d75fc8cd0ba23f97bae88a6ec04e9e5351ff3c6ad06f38fe32ba50cbd0d11946"}, + {file = "bcrypt-4.1.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:a97e07e83e3262599434816f631cc4c7ca2aa8e9c072c1b1a7fec2ae809a1d2d"}, + {file = "bcrypt-4.1.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e51c42750b7585cee7892c2614be0d14107fad9581d1738d954a262556dd1aab"}, + {file = "bcrypt-4.1.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ba4e4cc26610581a6329b3937e02d319f5ad4b85b074846bf4fef8a8cf51e7bb"}, + {file = "bcrypt-4.1.2.tar.gz", hash = "sha256:33313a1200a3ae90b75587ceac502b048b840fc69e7f7a0905b5f87fac7a1258"}, +] + +[package.extras] +tests = ["pytest (>=3.2.1,!=3.3.0)"] +typecheck = ["mypy"] + +[[package]] +name = "black" +version = "23.12.1" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-23.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2"}, + {file = "black-23.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba"}, + {file = "black-23.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0"}, + {file = "black-23.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3"}, + {file = "black-23.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba"}, + {file = "black-23.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b"}, + {file = "black-23.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59"}, + {file = "black-23.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50"}, + {file = "black-23.12.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e"}, + {file = "black-23.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec"}, + {file = "black-23.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e"}, + {file = "black-23.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9"}, + {file = "black-23.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f"}, + {file = "black-23.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d"}, + {file = "black-23.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a"}, + {file = "black-23.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e"}, + {file = "black-23.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055"}, + {file = "black-23.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54"}, + {file = "black-23.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea"}, + {file = "black-23.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2"}, + {file = "black-23.12.1-py3-none-any.whl", hash = "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e"}, + {file = "black-23.12.1.tar.gz", hash = "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "certifi" +version = "2024.2.2" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, +] + +[[package]] +name = "cffi" +version = "1.16.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, + {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, + {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, + {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, + {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, + {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, + {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, +] + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +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 = "cryptography" +version = "42.0.2" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-42.0.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:701171f825dcab90969596ce2af253143b93b08f1a716d4b2a9d2db5084ef7be"}, + {file = "cryptography-42.0.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:61321672b3ac7aade25c40449ccedbc6db72c7f5f0fdf34def5e2f8b51ca530d"}, + {file = "cryptography-42.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea2c3ffb662fec8bbbfce5602e2c159ff097a4631d96235fcf0fb00e59e3ece4"}, + {file = "cryptography-42.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b15c678f27d66d247132cbf13df2f75255627bcc9b6a570f7d2fd08e8c081d2"}, + {file = "cryptography-42.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8e88bb9eafbf6a4014d55fb222e7360eef53e613215085e65a13290577394529"}, + {file = "cryptography-42.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a047682d324ba56e61b7ea7c7299d51e61fd3bca7dad2ccc39b72bd0118d60a1"}, + {file = "cryptography-42.0.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:36d4b7c4be6411f58f60d9ce555a73df8406d484ba12a63549c88bd64f7967f1"}, + {file = "cryptography-42.0.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:a00aee5d1b6c20620161984f8ab2ab69134466c51f58c052c11b076715e72929"}, + {file = "cryptography-42.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b97fe7d7991c25e6a31e5d5e795986b18fbbb3107b873d5f3ae6dc9a103278e9"}, + {file = "cryptography-42.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5fa82a26f92871eca593b53359c12ad7949772462f887c35edaf36f87953c0e2"}, + {file = "cryptography-42.0.2-cp37-abi3-win32.whl", hash = "sha256:4b063d3413f853e056161eb0c7724822a9740ad3caa24b8424d776cebf98e7ee"}, + {file = "cryptography-42.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:841ec8af7a8491ac76ec5a9522226e287187a3107e12b7d686ad354bb78facee"}, + {file = "cryptography-42.0.2-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:55d1580e2d7e17f45d19d3b12098e352f3a37fe86d380bf45846ef257054b242"}, + {file = "cryptography-42.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28cb2c41f131a5758d6ba6a0504150d644054fd9f3203a1e8e8d7ac3aea7f73a"}, + {file = "cryptography-42.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9097a208875fc7bbeb1286d0125d90bdfed961f61f214d3f5be62cd4ed8a446"}, + {file = "cryptography-42.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:44c95c0e96b3cb628e8452ec060413a49002a247b2b9938989e23a2c8291fc90"}, + {file = "cryptography-42.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2f9f14185962e6a04ab32d1abe34eae8a9001569ee4edb64d2304bf0d65c53f3"}, + {file = "cryptography-42.0.2-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:09a77e5b2e8ca732a19a90c5bca2d124621a1edb5438c5daa2d2738bfeb02589"}, + {file = "cryptography-42.0.2-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad28cff53f60d99a928dfcf1e861e0b2ceb2bc1f08a074fdd601b314e1cc9e0a"}, + {file = "cryptography-42.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:130c0f77022b2b9c99d8cebcdd834d81705f61c68e91ddd614ce74c657f8b3ea"}, + {file = "cryptography-42.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fa3dec4ba8fb6e662770b74f62f1a0c7d4e37e25b58b2bf2c1be4c95372b4a33"}, + {file = "cryptography-42.0.2-cp39-abi3-win32.whl", hash = "sha256:3dbd37e14ce795b4af61b89b037d4bc157f2cb23e676fa16932185a04dfbf635"}, + {file = "cryptography-42.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:8a06641fb07d4e8f6c7dda4fc3f8871d327803ab6542e33831c7ccfdcb4d0ad6"}, + {file = "cryptography-42.0.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:087887e55e0b9c8724cf05361357875adb5c20dec27e5816b653492980d20380"}, + {file = "cryptography-42.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a7ef8dd0bf2e1d0a27042b231a3baac6883cdd5557036f5e8df7139255feaac6"}, + {file = "cryptography-42.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4383b47f45b14459cab66048d384614019965ba6c1a1a141f11b5a551cace1b2"}, + {file = "cryptography-42.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:fbeb725c9dc799a574518109336acccaf1303c30d45c075c665c0793c2f79a7f"}, + {file = "cryptography-42.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:320948ab49883557a256eab46149df79435a22d2fefd6a66fe6946f1b9d9d008"}, + {file = "cryptography-42.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5ef9bc3d046ce83c4bbf4c25e1e0547b9c441c01d30922d812e887dc5f125c12"}, + {file = "cryptography-42.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:52ed9ebf8ac602385126c9a2fe951db36f2cb0c2538d22971487f89d0de4065a"}, + {file = "cryptography-42.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:141e2aa5ba100d3788c0ad7919b288f89d1fe015878b9659b307c9ef867d3a65"}, + {file = "cryptography-42.0.2.tar.gz", hash = "sha256:e0ec52ba3c7f1b7d813cd52649a5b3ef1fc0d433219dc8c93827c57eab6cf888"}, +] + +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] +nox = ["nox"] +pep8test = ["check-sdist", "click", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "decorator" +version = "5.1.1" +description = "Decorators for Humans" +optional = false +python-versions = ">=3.5" +files = [ + {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, + {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, +] + +[[package]] +name = "distlib" +version = "0.3.8" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, +] + +[[package]] +name = "ecdsa" +version = "0.18.0" +description = "ECDSA cryptographic signature library (pure python)" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "ecdsa-0.18.0-py2.py3-none-any.whl", hash = "sha256:80600258e7ed2f16b9aa1d7c295bd70194109ad5a30fdee0eaeefef1d4c559dd"}, + {file = "ecdsa-0.18.0.tar.gz", hash = "sha256:190348041559e21b22a1d65cee485282ca11a6f81d503fddb84d5017e9ed1e49"}, +] + +[package.dependencies] +six = ">=1.9.0" + +[package.extras] +gmpy = ["gmpy"] +gmpy2 = ["gmpy2"] + +[[package]] +name = "exceptiongroup" +version = "1.2.0" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "executing" +version = "2.0.1" +description = "Get the currently executing AST node of a frame, and other information" +optional = false +python-versions = ">=3.5" +files = [ + {file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"}, + {file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"}, +] + +[package.extras] +tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] + +[[package]] +name = "fastapi" +version = "0.104.1" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fastapi-0.104.1-py3-none-any.whl", hash = "sha256:752dc31160cdbd0436bb93bad51560b57e525cbb1d4bbf6f4904ceee75548241"}, + {file = "fastapi-0.104.1.tar.gz", hash = "sha256:e5e4540a7c5e1dcfbbcf5b903c234feddcdcd881f191977a1c5dfd917487e7ae"}, +] + +[package.dependencies] +anyio = ">=3.7.1,<4.0.0" +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.27.0,<0.28.0" +typing-extensions = ">=4.8.0" + +[package.extras] +all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "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)"] + +[[package]] +name = "filelock" +version = "3.13.1" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, + {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +typing = ["typing-extensions (>=4.8)"] + +[[package]] +name = "flake8" +version = "7.0.0" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.8.1" +files = [ + {file = "flake8-7.0.0-py2.py3-none-any.whl", hash = "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3"}, + {file = "flake8-7.0.0.tar.gz", hash = "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.11.0,<2.12.0" +pyflakes = ">=3.2.0,<3.3.0" + +[[package]] +name = "greenlet" +version = "3.0.3" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=3.7" +files = [ + {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, + {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, + {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, + {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, + {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, + {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, + {file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"}, + {file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"}, + {file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"}, + {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"}, + {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"}, + {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"}, + {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, + {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, + {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, + {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, +] + +[package.extras] +docs = ["Sphinx", "furo"] +test = ["objgraph", "psutil"] + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +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 = "httpcore" +version = "1.0.2" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7"}, + {file = "httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.13,<0.15" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<0.23.0)"] + +[[package]] +name = "httpx" +version = "0.25.2" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.25.2-py3-none-any.whl", hash = "sha256:a05d3d052d9b2dfce0e3896636467f8a5342fb2b902c819428e1ac65413ca118"}, + {file = "httpx-0.25.2.tar.gz", hash = "sha256:8b8fcaa0c8ea7b05edd69a094e63a2094c4efcb48129fb757361bc423c0ad9e8"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] + +[[package]] +name = "identify" +version = "2.5.33" +description = "File identification library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "identify-2.5.33-py2.py3-none-any.whl", hash = "sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34"}, + {file = "identify-2.5.33.tar.gz", hash = "sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.6" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "ipdb" +version = "0.13.13" +description = "IPython-enabled pdb" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "ipdb-0.13.13-py3-none-any.whl", hash = "sha256:45529994741c4ab6d2388bfa5d7b725c2cf7fe9deffabdb8a6113aa5ed449ed4"}, + {file = "ipdb-0.13.13.tar.gz", hash = "sha256:e3ac6018ef05126d442af680aad863006ec19d02290561ac88b8b1c0b0cfc726"}, +] + +[package.dependencies] +decorator = {version = "*", markers = "python_version > \"3.6\""} +ipython = {version = ">=7.31.1", markers = "python_version > \"3.6\""} +tomli = {version = "*", markers = "python_version > \"3.6\" and python_version < \"3.11\""} + +[[package]] +name = "ipython" +version = "8.12.3" +description = "IPython: Productive Interactive Computing" +optional = false +python-versions = ">=3.8" +files = [ + {file = "ipython-8.12.3-py3-none-any.whl", hash = "sha256:b0340d46a933d27c657b211a329d0be23793c36595acf9e6ef4164bc01a1804c"}, + {file = "ipython-8.12.3.tar.gz", hash = "sha256:3910c4b54543c2ad73d06579aa771041b7d5707b033bd488669b4cf544e3b363"}, +] + +[package.dependencies] +appnope = {version = "*", markers = "sys_platform == \"darwin\""} +backcall = "*" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +jedi = ">=0.16" +matplotlib-inline = "*" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} +pickleshare = "*" +prompt-toolkit = ">=3.0.30,<3.0.37 || >3.0.37,<3.1.0" +pygments = ">=2.4.0" +stack-data = "*" +traitlets = ">=5" +typing-extensions = {version = "*", markers = "python_version < \"3.10\""} + +[package.extras] +all = ["black", "curio", "docrepr", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.21)", "pandas", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] +black = ["black"] +doc = ["docrepr", "ipykernel", "matplotlib", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] +kernel = ["ipykernel"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["ipywidgets", "notebook"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["pytest (<7.1)", "pytest-asyncio", "testpath"] +test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.21)", "pandas", "pytest (<7.1)", "pytest-asyncio", "testpath", "trio"] + +[[package]] +name = "isort" +version = "5.13.2" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, + {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, +] + +[package.extras] +colors = ["colorama (>=0.4.6)"] + +[[package]] +name = "jedi" +version = "0.19.1" +description = "An autocompletion tool for Python that can be used for text editors." +optional = false +python-versions = ">=3.6" +files = [ + {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"}, + {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"}, +] + +[package.dependencies] +parso = ">=0.8.3,<0.9.0" + +[package.extras] +docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] + +[[package]] +name = "matplotlib-inline" +version = "0.1.6" +description = "Inline Matplotlib backend for Jupyter" +optional = false +python-versions = ">=3.5" +files = [ + {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"}, + {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"}, +] + +[package.dependencies] +traitlets = "*" + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "nodeenv" +version = "1.8.0" +description = "Node.js virtual environment builder" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, + {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, +] + +[package.dependencies] +setuptools = "*" + +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "parso" +version = "0.8.3" +description = "A Python Parser" +optional = false +python-versions = ">=3.6" +files = [ + {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, + {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, +] + +[package.extras] +qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] +testing = ["docopt", "pytest (<6.0.0)"] + +[[package]] +name = "pathlib2" +version = "2.3.7.post1" +description = "Object-oriented filesystem paths" +optional = false +python-versions = "*" +files = [ + {file = "pathlib2-2.3.7.post1-py2.py3-none-any.whl", hash = "sha256:5266a0fd000452f1b3467d782f079a4343c63aaa119221fbdc4e39577489ca5b"}, + {file = "pathlib2-2.3.7.post1.tar.gz", hash = "sha256:9fe0edad898b83c0c3e199c842b27ed216645d2e177757b2dd67384d4113c641"}, +] + +[package.dependencies] +six = "*" + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +description = "Pexpect allows easy control of interactive console applications." +optional = false +python-versions = "*" +files = [ + {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, + {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, +] + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +name = "pickleshare" +version = "0.7.5" +description = "Tiny 'shelve'-like database with concurrency support" +optional = false +python-versions = "*" +files = [ + {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, + {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, +] + +[[package]] +name = "platformdirs" +version = "4.2.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, + {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] + +[[package]] +name = "pluggy" +version = "1.4.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "3.5.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"}, + {file = "pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "prompt-toolkit" +version = "3.0.43" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "prompt_toolkit-3.0.43-py3-none-any.whl", hash = "sha256:a11a29cb3bf0a28a387fe5122cdb649816a957cd9261dcedf8c9f1fef33eacf6"}, + {file = "prompt_toolkit-3.0.43.tar.gz", hash = "sha256:3527b7af26106cbc65a040bcc84839a3566ec1b051bb0bfe953631e704b0ff7d"}, +] + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +optional = false +python-versions = "*" +files = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] + +[[package]] +name = "pure-eval" +version = "0.2.2" +description = "Safely evaluate AST nodes without side effects" +optional = false +python-versions = "*" +files = [ + {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, + {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, +] + +[package.extras] +tests = ["pytest"] + +[[package]] +name = "pyasn1" +version = "0.5.1" +description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "pyasn1-0.5.1-py2.py3-none-any.whl", hash = "sha256:4439847c58d40b1d0a573d07e3856e95333f1976294494c325775aeca506eb58"}, + {file = "pyasn1-0.5.1.tar.gz", hash = "sha256:6d391a96e59b23130a5cfa74d6fd7f388dbbe26cc8f1edf39fdddf08d9d6676c"}, +] + +[[package]] +name = "pycodestyle" +version = "2.11.1" +description = "Python style guide checker" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, + {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, +] + +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] + +[[package]] +name = "pydantic" +version = "2.6.1" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.6.1-py3-none-any.whl", hash = "sha256:0b6a909df3192245cb736509a92ff69e4fef76116feffec68e93a567347bae6f"}, + {file = "pydantic-2.6.1.tar.gz", hash = "sha256:4fd5c182a2488dc63e6d32737ff19937888001e2a6d86e94b3f233104a5d1fa9"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.16.2" +typing-extensions = ">=4.6.1" + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.16.2" +description = "" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.16.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3fab4e75b8c525a4776e7630b9ee48aea50107fea6ca9f593c98da3f4d11bf7c"}, + {file = "pydantic_core-2.16.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8bde5b48c65b8e807409e6f20baee5d2cd880e0fad00b1a811ebc43e39a00ab2"}, + {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2924b89b16420712e9bb8192396026a8fbd6d8726224f918353ac19c4c043d2a"}, + {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16aa02e7a0f539098e215fc193c8926c897175d64c7926d00a36188917717a05"}, + {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:936a787f83db1f2115ee829dd615c4f684ee48ac4de5779ab4300994d8af325b"}, + {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:459d6be6134ce3b38e0ef76f8a672924460c455d45f1ad8fdade36796df1ddc8"}, + {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9ee4febb249c591d07b2d4dd36ebcad0ccd128962aaa1801508320896575ef"}, + {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40a0bd0bed96dae5712dab2aba7d334a6c67cbcac2ddfca7dbcc4a8176445990"}, + {file = "pydantic_core-2.16.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:870dbfa94de9b8866b37b867a2cb37a60c401d9deb4a9ea392abf11a1f98037b"}, + {file = "pydantic_core-2.16.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:308974fdf98046db28440eb3377abba274808bf66262e042c412eb2adf852731"}, + {file = "pydantic_core-2.16.2-cp310-none-win32.whl", hash = "sha256:a477932664d9611d7a0816cc3c0eb1f8856f8a42435488280dfbf4395e141485"}, + {file = "pydantic_core-2.16.2-cp310-none-win_amd64.whl", hash = "sha256:8f9142a6ed83d90c94a3efd7af8873bf7cefed2d3d44387bf848888482e2d25f"}, + {file = "pydantic_core-2.16.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:406fac1d09edc613020ce9cf3f2ccf1a1b2f57ab00552b4c18e3d5276c67eb11"}, + {file = "pydantic_core-2.16.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ce232a6170dd6532096cadbf6185271e4e8c70fc9217ebe105923ac105da9978"}, + {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a90fec23b4b05a09ad988e7a4f4e081711a90eb2a55b9c984d8b74597599180f"}, + {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8aafeedb6597a163a9c9727d8a8bd363a93277701b7bfd2749fbefee2396469e"}, + {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9957433c3a1b67bdd4c63717eaf174ebb749510d5ea612cd4e83f2d9142f3fc8"}, + {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0d7a9165167269758145756db43a133608a531b1e5bb6a626b9ee24bc38a8f7"}, + {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dffaf740fe2e147fedcb6b561353a16243e654f7fe8e701b1b9db148242e1272"}, + {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8ed79883b4328b7f0bd142733d99c8e6b22703e908ec63d930b06be3a0e7113"}, + {file = "pydantic_core-2.16.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cf903310a34e14651c9de056fcc12ce090560864d5a2bb0174b971685684e1d8"}, + {file = "pydantic_core-2.16.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:46b0d5520dbcafea9a8645a8164658777686c5c524d381d983317d29687cce97"}, + {file = "pydantic_core-2.16.2-cp311-none-win32.whl", hash = "sha256:70651ff6e663428cea902dac297066d5c6e5423fda345a4ca62430575364d62b"}, + {file = "pydantic_core-2.16.2-cp311-none-win_amd64.whl", hash = "sha256:98dc6f4f2095fc7ad277782a7c2c88296badcad92316b5a6e530930b1d475ebc"}, + {file = "pydantic_core-2.16.2-cp311-none-win_arm64.whl", hash = "sha256:ef6113cd31411eaf9b39fc5a8848e71c72656fd418882488598758b2c8c6dfa0"}, + {file = "pydantic_core-2.16.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:88646cae28eb1dd5cd1e09605680c2b043b64d7481cdad7f5003ebef401a3039"}, + {file = "pydantic_core-2.16.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7b883af50eaa6bb3299780651e5be921e88050ccf00e3e583b1e92020333304b"}, + {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bf26c2e2ea59d32807081ad51968133af3025c4ba5753e6a794683d2c91bf6e"}, + {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:99af961d72ac731aae2a1b55ccbdae0733d816f8bfb97b41909e143de735f522"}, + {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02906e7306cb8c5901a1feb61f9ab5e5c690dbbeaa04d84c1b9ae2a01ebe9379"}, + {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5362d099c244a2d2f9659fb3c9db7c735f0004765bbe06b99be69fbd87c3f15"}, + {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ac426704840877a285d03a445e162eb258924f014e2f074e209d9b4ff7bf380"}, + {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b94cbda27267423411c928208e89adddf2ea5dd5f74b9528513f0358bba019cb"}, + {file = "pydantic_core-2.16.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6db58c22ac6c81aeac33912fb1af0e930bc9774166cdd56eade913d5f2fff35e"}, + {file = "pydantic_core-2.16.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:396fdf88b1b503c9c59c84a08b6833ec0c3b5ad1a83230252a9e17b7dfb4cffc"}, + {file = "pydantic_core-2.16.2-cp312-none-win32.whl", hash = "sha256:7c31669e0c8cc68400ef0c730c3a1e11317ba76b892deeefaf52dcb41d56ed5d"}, + {file = "pydantic_core-2.16.2-cp312-none-win_amd64.whl", hash = "sha256:a3b7352b48fbc8b446b75f3069124e87f599d25afb8baa96a550256c031bb890"}, + {file = "pydantic_core-2.16.2-cp312-none-win_arm64.whl", hash = "sha256:a9e523474998fb33f7c1a4d55f5504c908d57add624599e095c20fa575b8d943"}, + {file = "pydantic_core-2.16.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:ae34418b6b389d601b31153b84dce480351a352e0bb763684a1b993d6be30f17"}, + {file = "pydantic_core-2.16.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:732bd062c9e5d9582a30e8751461c1917dd1ccbdd6cafb032f02c86b20d2e7ec"}, + {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b52776a2e3230f4854907a1e0946eec04d41b1fc64069ee774876bbe0eab55"}, + {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef551c053692b1e39e3f7950ce2296536728871110e7d75c4e7753fb30ca87f4"}, + {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ebb892ed8599b23fa8f1799e13a12c87a97a6c9d0f497525ce9858564c4575a4"}, + {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa6c8c582036275997a733427b88031a32ffa5dfc3124dc25a730658c47a572f"}, + {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ba0884a91f1aecce75202473ab138724aa4fb26d7707f2e1fa6c3e68c84fbf"}, + {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7924e54f7ce5d253d6160090ddc6df25ed2feea25bfb3339b424a9dd591688bc"}, + {file = "pydantic_core-2.16.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69a7b96b59322a81c2203be537957313b07dd333105b73db0b69212c7d867b4b"}, + {file = "pydantic_core-2.16.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7e6231aa5bdacda78e96ad7b07d0c312f34ba35d717115f4b4bff6cb87224f0f"}, + {file = "pydantic_core-2.16.2-cp38-none-win32.whl", hash = "sha256:41dac3b9fce187a25c6253ec79a3f9e2a7e761eb08690e90415069ea4a68ff7a"}, + {file = "pydantic_core-2.16.2-cp38-none-win_amd64.whl", hash = "sha256:f685dbc1fdadb1dcd5b5e51e0a378d4685a891b2ddaf8e2bba89bd3a7144e44a"}, + {file = "pydantic_core-2.16.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:55749f745ebf154c0d63d46c8c58594d8894b161928aa41adbb0709c1fe78b77"}, + {file = "pydantic_core-2.16.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b30b0dd58a4509c3bd7eefddf6338565c4905406aee0c6e4a5293841411a1286"}, + {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18de31781cdc7e7b28678df7c2d7882f9692ad060bc6ee3c94eb15a5d733f8f7"}, + {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5864b0242f74b9dd0b78fd39db1768bc3f00d1ffc14e596fd3e3f2ce43436a33"}, + {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8f9186ca45aee030dc8234118b9c0784ad91a0bb27fc4e7d9d6608a5e3d386c"}, + {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc6f6c9be0ab6da37bc77c2dda5f14b1d532d5dbef00311ee6e13357a418e646"}, + {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa057095f621dad24a1e906747179a69780ef45cc8f69e97463692adbcdae878"}, + {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6ad84731a26bcfb299f9eab56c7932d46f9cad51c52768cace09e92a19e4cf55"}, + {file = "pydantic_core-2.16.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3b052c753c4babf2d1edc034c97851f867c87d6f3ea63a12e2700f159f5c41c3"}, + {file = "pydantic_core-2.16.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e0f686549e32ccdb02ae6f25eee40cc33900910085de6aa3790effd391ae10c2"}, + {file = "pydantic_core-2.16.2-cp39-none-win32.whl", hash = "sha256:7afb844041e707ac9ad9acad2188a90bffce2c770e6dc2318be0c9916aef1469"}, + {file = "pydantic_core-2.16.2-cp39-none-win_amd64.whl", hash = "sha256:9da90d393a8227d717c19f5397688a38635afec89f2e2d7af0df037f3249c39a"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5f60f920691a620b03082692c378661947d09415743e437a7478c309eb0e4f82"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:47924039e785a04d4a4fa49455e51b4eb3422d6eaacfde9fc9abf8fdef164e8a"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6294e76b0380bb7a61eb8a39273c40b20beb35e8c87ee101062834ced19c545"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe56851c3f1d6f5384b3051c536cc81b3a93a73faf931f404fef95217cf1e10d"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9d776d30cde7e541b8180103c3f294ef7c1862fd45d81738d156d00551005784"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:72f7919af5de5ecfaf1eba47bf9a5d8aa089a3340277276e5636d16ee97614d7"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:4bfcbde6e06c56b30668a0c872d75a7ef3025dc3c1823a13cf29a0e9b33f67e8"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ff7c97eb7a29aba230389a2661edf2e9e06ce616c7e35aa764879b6894a44b25"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9b5f13857da99325dcabe1cc4e9e6a3d7b2e2c726248ba5dd4be3e8e4a0b6d0e"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a7e41e3ada4cca5f22b478c08e973c930e5e6c7ba3588fb8e35f2398cdcc1545"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60eb8ceaa40a41540b9acae6ae7c1f0a67d233c40dc4359c256ad2ad85bdf5e5"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7beec26729d496a12fd23cf8da9944ee338c8b8a17035a560b585c36fe81af20"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:22c5f022799f3cd6741e24f0443ead92ef42be93ffda0d29b2597208c94c3753"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:eca58e319f4fd6df004762419612122b2c7e7d95ffafc37e890252f869f3fb2a"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed957db4c33bc99895f3a1672eca7e80e8cda8bd1e29a80536b4ec2153fa9804"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:459c0d338cc55d099798618f714b21b7ece17eb1a87879f2da20a3ff4c7628e2"}, + {file = "pydantic_core-2.16.2.tar.gz", hash = "sha256:0ba503850d8b8dcc18391f10de896ae51d37fe5fe43dbfb6a35c5c5cad271a06"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pyflakes" +version = "3.2.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, + {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, +] + +[[package]] +name = "pygame" +version = "2.5.2" +description = "Python Game Development" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pygame-2.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a0769eb628c818761755eb0a0ca8216b95270ea8cbcbc82227e39ac9644643da"}, + {file = "pygame-2.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed9a3d98adafa0805ccbaaff5d2996a2b5795381285d8437a4a5d248dbd12b4a"}, + {file = "pygame-2.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30d1618672a55e8c6669281ba264464b3ab563158e40d89e8c8b3faa0febebd"}, + {file = "pygame-2.5.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39690e9be9baf58b7359d1f3b2336e1fd6f92fedbbce42987be5df27f8d30718"}, + {file = "pygame-2.5.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03879ec299c9f4ba23901b2649a96b2143f0a5d787f0b6c39469989e2320caf1"}, + {file = "pygame-2.5.2-cp310-cp310-win32.whl", hash = "sha256:74e1d6284100e294f445832e6f6343be4fe4748decc4f8a51131ae197dae8584"}, + {file = "pygame-2.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:485239c7d32265fd35b76ae8f64f34b0637ae11e69d76de15710c4b9edcc7c8d"}, + {file = "pygame-2.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:34646ca20e163dc6f6cf8170f1e12a2e41726780112594ac061fa448cf7ccd75"}, + {file = "pygame-2.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3b8a6e351665ed26ea791f0e1fd649d3f483e8681892caef9d471f488f9ea5ee"}, + {file = "pygame-2.5.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc346965847aef00013fa2364f41a64f068cd096dcc7778fc306ca3735f0eedf"}, + {file = "pygame-2.5.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35632035fd81261f2d797fa810ea8c46111bd78ceb6089d52b61ed7dc3c5d05f"}, + {file = "pygame-2.5.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e24d05184e4195fe5ebcdce8b18ecb086f00182b9ae460a86682d312ce8d31f"}, + {file = "pygame-2.5.2-cp311-cp311-win32.whl", hash = "sha256:f02c1c7505af18d426d355ac9872bd5c916b27f7b0fe224749930662bea47a50"}, + {file = "pygame-2.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:6d58c8cf937815d3b7cdc0fa9590c5129cb2c9658b72d00e8a4568dea2ff1d42"}, + {file = "pygame-2.5.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1a2a43802bb5e89ce2b3b775744e78db4f9a201bf8d059b946c61722840ceea8"}, + {file = "pygame-2.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1c289f2613c44fe70a1e40769de4a49c5ab5a29b9376f1692bb1a15c9c1c9bfa"}, + {file = "pygame-2.5.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:074aa6c6e110c925f7f27f00c7733c6303407edc61d738882985091d1eb2ef17"}, + {file = "pygame-2.5.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe0228501ec616779a0b9c4299e837877783e18df294dd690b9ab0eed3d8aaab"}, + {file = "pygame-2.5.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31648d38ecdc2335ffc0e38fb18a84b3339730521505dac68514f83a1092e3f4"}, + {file = "pygame-2.5.2-cp312-cp312-win32.whl", hash = "sha256:224c308856334bc792f696e9278e50d099a87c116f7fc314cd6aa3ff99d21592"}, + {file = "pygame-2.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:dd2d2650faf54f9a0f5bd0db8409f79609319725f8f08af6507a0609deadcad4"}, + {file = "pygame-2.5.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9b30bc1220c457169571aac998e54b013aaeb732d2fd8744966cb1cfab1f61d1"}, + {file = "pygame-2.5.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78fcd7643358b886a44127ff7dec9041c056c212b3a98977674f83f99e9b12d3"}, + {file = "pygame-2.5.2-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35cf093a51cb294ede56c29d4acf41538c00f297fcf78a9b186fb7d23c0577b6"}, + {file = "pygame-2.5.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fe323acbf53a0195c8c98b1b941eba7ac24e3e2b28ae48e8cda566f15fc4945"}, + {file = "pygame-2.5.2-cp36-cp36m-win32.whl", hash = "sha256:5697528266b4716d9cdd44a5a1d210f4d86ef801d0f64ca5da5d0816704009d9"}, + {file = "pygame-2.5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:edda1f7cff4806a4fa39e0e8ccd75f38d1d340fa5fc52d8582ade87aca247d92"}, + {file = "pygame-2.5.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9bd738fd4ecc224769d0b4a719f96900a86578e26e0105193658a32966df2aae"}, + {file = "pygame-2.5.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30a8d7cf12363b4140bf2f93b5eec4028376ca1d0fe4b550588f836279485308"}, + {file = "pygame-2.5.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc12e4dea3e88ea8a553de6d56a37b704dbe2aed95105889f6afeb4b96e62097"}, + {file = "pygame-2.5.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b34c73cb328024f8db3cb6487a37e54000148988275d8d6e5adf99d9323c937"}, + {file = "pygame-2.5.2-cp37-cp37m-win32.whl", hash = "sha256:7d0a2794649defa57ef50b096a99f7113d3d0c2e32d1426cafa7d618eadce4c7"}, + {file = "pygame-2.5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:41f8779f52e0f6e6e6ccb8f0b5536e432bf386ee29c721a1c22cada7767b0cef"}, + {file = "pygame-2.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:677e37bc0ea7afd89dde5a88ced4458aa8656159c70a576eea68b5622ee1997b"}, + {file = "pygame-2.5.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:47a8415d2bd60e6909823b5643a1d4ef5cc29417d817f2a214b255f6fa3a1e4c"}, + {file = "pygame-2.5.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ff21201df6278b8ca2e948fb148ffe88f5481fd03760f381dd61e45954c7dff"}, + {file = "pygame-2.5.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d29a84b2e02814b9ba925357fd2e1df78efe5e1aa64dc3051eaed95d2b96eafd"}, + {file = "pygame-2.5.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d78485c4d21133d6b2fbb504cd544ca655e50b6eb551d2995b3aa6035928adda"}, + {file = "pygame-2.5.2-cp38-cp38-win32.whl", hash = "sha256:d851247239548aa357c4a6840fb67adc2d570ce7cb56988d036a723d26b48bff"}, + {file = "pygame-2.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:88d1cdacc2d3471eceab98bf0c93c14d3a8461f93e58e3d926f20d4de3a75554"}, + {file = "pygame-2.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4f1559e7efe4efb9dc19d2d811d702f325d9605f9f6f9ececa39ee6890c798f5"}, + {file = "pygame-2.5.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cf2191b756ceb0e8458a761d0c665b0c70b538570449e0d39b75a5ba94ac5cf0"}, + {file = "pygame-2.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6cf2257447ce7f2d6de37e5fb019d2bbe32ed05a5721ace8bc78c2d9beaf3aee"}, + {file = "pygame-2.5.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75cbbfaba2b81434d62631d0b08b85fab16cf4a36e40b80298d3868927e1299"}, + {file = "pygame-2.5.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:daca456d5b9f52e088e06a127dec182b3638a775684fb2260f25d664351cf1ae"}, + {file = "pygame-2.5.2-cp39-cp39-win32.whl", hash = "sha256:3b3e619e33d11c297d7a57a82db40681f9c2c3ae1d5bf06003520b4fe30c435d"}, + {file = "pygame-2.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:1822d534bb7fe756804647b6da2c9ea5d7a62d8796b2e15d172d3be085de28c6"}, + {file = "pygame-2.5.2-pp36-pypy36_pp73-win32.whl", hash = "sha256:e708fc8f709a0fe1d1876489345f2e443d47f3976d33455e2e1e937f972f8677"}, + {file = "pygame-2.5.2-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c13edebc43c240fb0532969e914f0ccefff5ae7e50b0b788d08ad2c15ef793e4"}, + {file = "pygame-2.5.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:263b4a7cbfc9fe2055abc21b0251cc17dea6dff750f0e1c598919ff350cdbffe"}, + {file = "pygame-2.5.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e58e2b0c791041e4bccafa5bd7650623ba1592b8fe62ae0a276b7d0ecb314b6c"}, + {file = "pygame-2.5.2-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0bd67426c02ffe6c9827fc4bcbda9442fbc451d29b17c83a3c088c56fef2c90"}, + {file = "pygame-2.5.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dcff6cbba1584cf7732ce1dbdd044406cd4f6e296d13bcb7fba963fb4aeefc9"}, + {file = "pygame-2.5.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ce4b6c0bfe44d00bb0998a6517bd0cf9455f642f30f91bc671ad41c05bf6f6ae"}, + {file = "pygame-2.5.2-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68c4e8e60b725ffc7a6c6ecd9bb5fcc5ed2d6e0e2a2c4a29a8454856ef16ad63"}, + {file = "pygame-2.5.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f3849f97372a3381c66955f99a0d58485ccd513c3d00c030b869094ce6997a6"}, + {file = "pygame-2.5.2.tar.gz", hash = "sha256:c1b89eb5d539e7ac5cf75513125fb5f2f0a2d918b1fd6e981f23bf0ac1b1c24a"}, +] + +[[package]] +name = "pygments" +version = "2.17.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, + {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, +] + +[package.extras] +plugins = ["importlib-metadata"] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pytest" +version = "7.4.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "python-decouple" +version = "3.8" +description = "Strict separation of settings from code." +optional = false +python-versions = "*" +files = [ + {file = "python-decouple-3.8.tar.gz", hash = "sha256:ba6e2657d4f376ecc46f77a3a615e058d93ba5e465c01bbe57289bfb7cce680f"}, + {file = "python_decouple-3.8-py3-none-any.whl", hash = "sha256:d0d45340815b25f4de59c974b855bb38d03151d81b037d9e3f463b0c9f8cbd66"}, +] + +[[package]] +name = "python-jose" +version = "3.3.0" +description = "JOSE implementation in Python" +optional = false +python-versions = "*" +files = [ + {file = "python-jose-3.3.0.tar.gz", hash = "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a"}, + {file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"}, +] + +[package.dependencies] +cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"cryptography\""} +ecdsa = "!=0.15" +pyasn1 = "*" +rsa = "*" + +[package.extras] +cryptography = ["cryptography (>=3.4.0)"] +pycrypto = ["pyasn1", "pycrypto (>=2.6.0,<2.7.0)"] +pycryptodome = ["pyasn1", "pycryptodome (>=3.3.1,<4.0.0)"] + +[[package]] +name = "python-multipart" +version = "0.0.6" +description = "A streaming multipart parser for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "python_multipart-0.0.6-py3-none-any.whl", hash = "sha256:ee698bab5ef148b0a760751c261902cd096e57e10558e11aca17646b74ee1c18"}, + {file = "python_multipart-0.0.6.tar.gz", hash = "sha256:e9925a80bb668529f1b67c7fdb0a5dacdd7cbfc6fb0bff3ea443fe22bdd62132"}, +] + +[package.extras] +dev = ["atomicwrites (==1.2.1)", "attrs (==19.2.0)", "coverage (==6.5.0)", "hatch", "invoke (==1.7.3)", "more-itertools (==4.3.0)", "pbr (==4.3.0)", "pluggy (==1.0.0)", "py (==1.11.0)", "pytest (==7.2.0)", "pytest-cov (==4.0.0)", "pytest-timeout (==2.1.0)", "pyyaml (==5.1)"] + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "rsa" +version = "4.9" +description = "Pure-Python RSA implementation" +optional = false +python-versions = ">=3.6,<4" +files = [ + {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, + {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, +] + +[package.dependencies] +pyasn1 = ">=0.1.3" + +[[package]] +name = "ruff" +version = "0.1.15" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"}, + {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"}, + {file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"}, + {file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"}, + {file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"}, + {file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"}, +] + +[[package]] +name = "setuptools" +version = "69.0.3" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"}, + {file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "sniffio" +version = "1.3.0" +description = "Sniff out which async library your code is running under" +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 = "sqlalchemy" +version = "2.0.25" +description = "Database Abstraction Library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "SQLAlchemy-2.0.25-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4344d059265cc8b1b1be351bfb88749294b87a8b2bbe21dfbe066c4199541ebd"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6f9e2e59cbcc6ba1488404aad43de005d05ca56e069477b33ff74e91b6319735"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84daa0a2055df9ca0f148a64fdde12ac635e30edbca80e87df9b3aaf419e144a"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc8b7dabe8e67c4832891a5d322cec6d44ef02f432b4588390017f5cec186a84"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f5693145220517b5f42393e07a6898acdfe820e136c98663b971906120549da5"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:db854730a25db7c956423bb9fb4bdd1216c839a689bf9cc15fada0a7fb2f4570"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-win32.whl", hash = "sha256:14a6f68e8fc96e5e8f5647ef6cda6250c780612a573d99e4d881581432ef1669"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-win_amd64.whl", hash = "sha256:87f6e732bccd7dcf1741c00f1ecf33797383128bd1c90144ac8adc02cbb98643"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:342d365988ba88ada8af320d43df4e0b13a694dbd75951f537b2d5e4cb5cd002"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f37c0caf14b9e9b9e8f6dbc81bc56db06acb4363eba5a633167781a48ef036ed"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa9373708763ef46782d10e950b49d0235bfe58facebd76917d3f5cbf5971aed"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d24f571990c05f6b36a396218f251f3e0dda916e0c687ef6fdca5072743208f5"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75432b5b14dc2fff43c50435e248b45c7cdadef73388e5610852b95280ffd0e9"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:884272dcd3ad97f47702965a0e902b540541890f468d24bd1d98bcfe41c3f018"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-win32.whl", hash = "sha256:e607cdd99cbf9bb80391f54446b86e16eea6ad309361942bf88318bcd452363c"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-win_amd64.whl", hash = "sha256:7d505815ac340568fd03f719446a589162d55c52f08abd77ba8964fbb7eb5b5f"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0dacf67aee53b16f365c589ce72e766efaabd2b145f9de7c917777b575e3659d"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b801154027107461ee992ff4b5c09aa7cc6ec91ddfe50d02bca344918c3265c6"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59a21853f5daeb50412d459cfb13cb82c089ad4c04ec208cd14dddd99fc23b39"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29049e2c299b5ace92cbed0c1610a7a236f3baf4c6b66eb9547c01179f638ec5"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b64b183d610b424a160b0d4d880995e935208fc043d0302dd29fee32d1ee3f95"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4f7a7d7fcc675d3d85fbf3b3828ecd5990b8d61bd6de3f1b260080b3beccf215"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-win32.whl", hash = "sha256:cf18ff7fc9941b8fc23437cc3e68ed4ebeff3599eec6ef5eebf305f3d2e9a7c2"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-win_amd64.whl", hash = "sha256:91f7d9d1c4dd1f4f6e092874c128c11165eafcf7c963128f79e28f8445de82d5"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bb209a73b8307f8fe4fe46f6ad5979649be01607f11af1eb94aa9e8a3aaf77f0"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:798f717ae7c806d67145f6ae94dc7c342d3222d3b9a311a784f371a4333212c7"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fdd402169aa00df3142149940b3bf9ce7dde075928c1886d9a1df63d4b8de62"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0d3cab3076af2e4aa5693f89622bef7fa770c6fec967143e4da7508b3dceb9b9"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:74b080c897563f81062b74e44f5a72fa44c2b373741a9ade701d5f789a10ba23"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-win32.whl", hash = "sha256:87d91043ea0dc65ee583026cb18e1b458d8ec5fc0a93637126b5fc0bc3ea68c4"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-win_amd64.whl", hash = "sha256:75f99202324383d613ddd1f7455ac908dca9c2dd729ec8584c9541dd41822a2c"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:420362338681eec03f53467804541a854617faed7272fe71a1bfdb07336a381e"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c88f0c7dcc5f99bdb34b4fd9b69b93c89f893f454f40219fe923a3a2fd11625"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3be4987e3ee9d9a380b66393b77a4cd6d742480c951a1c56a23c335caca4ce3"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a159111a0f58fb034c93eeba211b4141137ec4b0a6e75789ab7a3ef3c7e7e3"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8b8cb63d3ea63b29074dcd29da4dc6a97ad1349151f2d2949495418fd6e48db9"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:736ea78cd06de6c21ecba7416499e7236a22374561493b456a1f7ffbe3f6cdb4"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-win32.whl", hash = "sha256:10331f129982a19df4284ceac6fe87353ca3ca6b4ca77ff7d697209ae0a5915e"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-win_amd64.whl", hash = "sha256:c55731c116806836a5d678a70c84cb13f2cedba920212ba7dcad53260997666d"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:605b6b059f4b57b277f75ace81cc5bc6335efcbcc4ccb9066695e515dbdb3900"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:665f0a3954635b5b777a55111ababf44b4fc12b1f3ba0a435b602b6387ffd7cf"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecf6d4cda1f9f6cb0b45803a01ea7f034e2f1aed9475e883410812d9f9e3cfcf"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c51db269513917394faec5e5c00d6f83829742ba62e2ac4fa5c98d58be91662f"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:790f533fa5c8901a62b6fef5811d48980adeb2f51f1290ade8b5e7ba990ba3de"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1b1180cda6df7af84fe72e4530f192231b1f29a7496951db4ff38dac1687202d"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-win32.whl", hash = "sha256:555651adbb503ac7f4cb35834c5e4ae0819aab2cd24857a123370764dc7d7e24"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-win_amd64.whl", hash = "sha256:dc55990143cbd853a5d038c05e79284baedf3e299661389654551bd02a6a68d7"}, + {file = "SQLAlchemy-2.0.25-py3-none-any.whl", hash = "sha256:a86b4240e67d4753dc3092d9511886795b3c2852abe599cffe108952f7af7ac3"}, + {file = "SQLAlchemy-2.0.25.tar.gz", hash = "sha256:a2c69a7664fb2d54b8682dd774c3b54f67f84fa123cf84dda2a5f40dcaa04e08"}, +] + +[package.dependencies] +greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""} +typing-extensions = ">=4.6.0" + +[package.extras] +aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] +aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] +aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] +asyncio = ["greenlet (!=0.4.17)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"] +mssql = ["pyodbc"] +mssql-pymssql = ["pymssql"] +mssql-pyodbc = ["pyodbc"] +mypy = ["mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0)"] +mysql-connector = ["mysql-connector-python"] +oracle = ["cx_oracle (>=8)"] +oracle-oracledb = ["oracledb (>=1.0.1)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] +postgresql-pg8000 = ["pg8000 (>=1.29.1)"] +postgresql-psycopg = ["psycopg (>=3.0.7)"] +postgresql-psycopg2binary = ["psycopg2-binary"] +postgresql-psycopg2cffi = ["psycopg2cffi"] +postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] +pymysql = ["pymysql"] +sqlcipher = ["sqlcipher3_binary"] + +[[package]] +name = "sqlalchemy-utils" +version = "0.41.1" +description = "Various utility functions for SQLAlchemy." +optional = false +python-versions = ">=3.6" +files = [ + {file = "SQLAlchemy-Utils-0.41.1.tar.gz", hash = "sha256:a2181bff01eeb84479e38571d2c0718eb52042f9afd8c194d0d02877e84b7d74"}, + {file = "SQLAlchemy_Utils-0.41.1-py3-none-any.whl", hash = "sha256:6c96b0768ea3f15c0dc56b363d386138c562752b84f647fb8d31a2223aaab801"}, +] + +[package.dependencies] +SQLAlchemy = ">=1.3" + +[package.extras] +arrow = ["arrow (>=0.3.4)"] +babel = ["Babel (>=1.3)"] +color = ["colour (>=0.0.4)"] +encrypted = ["cryptography (>=0.6)"] +intervals = ["intervals (>=0.7.1)"] +password = ["passlib (>=1.6,<2.0)"] +pendulum = ["pendulum (>=2.0.5)"] +phone = ["phonenumbers (>=5.9.2)"] +test = ["Jinja2 (>=2.3)", "Pygments (>=1.2)", "backports.zoneinfo", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "isort (>=4.2.2)", "pg8000 (>=1.12.4)", "psycopg (>=3.1.8)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (>=2.7.1)", "python-dateutil (>=2.6)", "pytz (>=2014.2)"] +test-all = ["Babel (>=1.3)", "Jinja2 (>=2.3)", "Pygments (>=1.2)", "arrow (>=0.3.4)", "backports.zoneinfo", "colour (>=0.0.4)", "cryptography (>=0.6)", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "furl (>=0.4.1)", "intervals (>=0.7.1)", "isort (>=4.2.2)", "passlib (>=1.6,<2.0)", "pendulum (>=2.0.5)", "pg8000 (>=1.12.4)", "phonenumbers (>=5.9.2)", "psycopg (>=3.1.8)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (>=2.7.1)", "python-dateutil", "python-dateutil (>=2.6)", "pytz (>=2014.2)"] +timezone = ["python-dateutil"] +url = ["furl (>=0.4.1)"] + +[[package]] +name = "stack-data" +version = "0.6.3" +description = "Extract data from python stack frames and tracebacks for informative displays" +optional = false +python-versions = "*" +files = [ + {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, + {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, +] + +[package.dependencies] +asttokens = ">=2.1.0" +executing = ">=1.2.0" +pure-eval = "*" + +[package.extras] +tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] + +[[package]] +name = "starlette" +version = "0.27.0" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.7" +files = [ + {file = "starlette-0.27.0-py3-none-any.whl", hash = "sha256:918416370e846586541235ccd38a474c08b80443ed31c578a418e2209b3eef91"}, + {file = "starlette-0.27.0.tar.gz", hash = "sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75"}, +] + +[package.dependencies] +anyio = ">=3.4.0,<5" +typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} + +[package.extras] +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "traitlets" +version = "5.14.1" +description = "Traitlets Python configuration system" +optional = false +python-versions = ">=3.8" +files = [ + {file = "traitlets-5.14.1-py3-none-any.whl", hash = "sha256:2e5a030e6eff91737c643231bfcf04a65b0132078dad75e4936700b213652e74"}, + {file = "traitlets-5.14.1.tar.gz", hash = "sha256:8585105b371a04b8316a43d5ce29c098575c2e477850b62b848b964f1444527e"}, +] + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] +test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<7.5)", "pytest-mock", "pytest-mypy-testing"] + +[[package]] +name = "typing-extensions" +version = "4.9.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, + {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, +] + +[[package]] +name = "uvicorn" +version = "0.24.0.post1" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.8" +files = [ + {file = "uvicorn-0.24.0.post1-py3-none-any.whl", hash = "sha256:7c84fea70c619d4a710153482c0d230929af7bcf76c7bfa6de151f0a3a80121e"}, + {file = "uvicorn-0.24.0.post1.tar.gz", hash = "sha256:09c8e5a79dc466bdf28dead50093957db184de356fcdc48697bad3bde4c2588e"}, +] + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" +typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} + +[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 = "virtualenv" +version = "20.25.0" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"}, + {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + +[[package]] +name = "wcwidth" +version = "0.2.13" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +files = [ + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.8.1,<4.0" +content-hash = "b44dd588f282f7704805c5d270b1e27870bb1de75ff4a88f6666f6e681658ead" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5d4c6f8 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,41 @@ +[tool.poetry] +name = "battlechess" +version = "0.1.0" +description = "chess with fog of war" +authors = ["Antoine ", "Quim Nuss "] +license = "GPLv3" +readme = "README.md" + +[tool.poetry.dependencies] +python = ">=3.8.1,<4.0" +httpx = "^0.25.2" +python-decouple = "^3.8" +pathlib2 = "^2.3.7.post1" +bcrypt = "^4.1.2" +sqlalchemy-utils = "^0.41.1" + + +[tool.poetry.group.standalone.dependencies] +pygame = "^2.5.2" + + +[tool.poetry.group.api.dependencies] +fastapi = "^0.104.1" +sqlalchemy = "^2.0.23" +python-jose = {extras = ["cryptography"], version = "^3.3.0"} +uvicorn = "^0.24.0.post1" +python-multipart = "^0.0.6" + + +[tool.poetry.group.dev.dependencies] +pytest = "^7.4.3" +black = "^23.11.0" +pre-commit = "^3.5.0" +ipdb = "^0.13.13" +isort = "^5.12.0" +ruff = "^0.1.6" +flake8 = "^7.0.0" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/requirements.txt b/requirements.txt index cbe9c60..1e93d5d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,32 @@ -requests -python-jose[cryptography] -fastapi -passlib[bcrypt] -uvicorn -python-multipart -pytest -pygame -sqlalchemy -pathlib -Pillow \ No newline at end of file +annotated-types==0.6.0 ; python_version >= "3.8" and python_version < "4.0" +anyio==3.7.1 ; python_version >= "3.8" and python_version < "4.0" +bcrypt==4.1.2 ; python_version >= "3.8" and python_version < "4.0" +certifi==2023.11.17 ; python_version >= "3.8" and python_version < "4.0" +cffi==1.16.0 ; python_version >= "3.8" and python_version < "4.0" and platform_python_implementation != "PyPy" +click==8.1.7 ; python_version >= "3.8" and python_version < "4.0" +colorama==0.4.6 ; python_version >= "3.8" and python_version < "4.0" and platform_system == "Windows" +cryptography==42.0.2 ; python_version >= "3.8" and python_version < "4.0" +ecdsa==0.18.0 ; python_version >= "3.8" and python_version < "4.0" +exceptiongroup==1.2.0 ; python_version >= "3.8" and python_version < "3.11" +fastapi==0.104.1 ; python_version >= "3.8" and python_version < "4.0" +greenlet==3.0.3 ; python_version >= "3.8" and python_version < "4.0" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32") +h11==0.14.0 ; python_version >= "3.8" and python_version < "4.0" +httpcore==1.0.2 ; python_version >= "3.8" and python_version < "4.0" +httpx==0.25.2 ; python_version >= "3.8" and python_version < "4.0" +idna==3.6 ; python_version >= "3.8" and python_version < "4.0" +passlib[bcrypt]==1.7.4 ; python_version >= "3.8" and python_version < "4.0" +pathlib2==2.3.7.post1 ; python_version >= "3.8" and python_version < "4.0" +pyasn1==0.5.1 ; python_version >= "3.8" and python_version < "4.0" +pycparser==2.21 ; python_version >= "3.8" and python_version < "4.0" and platform_python_implementation != "PyPy" +pydantic-core==2.16.1 ; python_version >= "3.8" and python_version < "4.0" +pydantic==2.6.0 ; python_version >= "3.8" and python_version < "4.0" +python-decouple==3.8 ; python_version >= "3.8" and python_version < "4.0" +python-jose[cryptography]==3.3.0 ; python_version >= "3.8" and python_version < "4.0" +python-multipart==0.0.6 ; python_version >= "3.8" and python_version < "4.0" +rsa==4.9 ; python_version >= "3.8" and python_version < "4" +six==1.16.0 ; python_version >= "3.8" and python_version < "4.0" +sniffio==1.3.0 ; python_version >= "3.8" and python_version < "4.0" +sqlalchemy==2.0.25 ; python_version >= "3.8" and python_version < "4.0" +starlette==0.27.0 ; python_version >= "3.8" and python_version < "4.0" +typing-extensions==4.9.0 ; python_version >= "3.8" and python_version < "4.0" +uvicorn==0.24.0.post1 ; python_version >= "3.8" and python_version < "4.0" diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..dddcf59 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,5 @@ +# Generic, formatter-friendly config. +# select = ["B", "D3", "D4", "E", "F"] + +# Never enforce `E501` (line length violations). This should be handled by formatters. +ignore = ["E501", "D401"] \ No newline at end of file diff --git a/server/config_example.py b/server/config_example.py deleted file mode 100644 index 76667f6..0000000 --- a/server/config_example.py +++ /dev/null @@ -1,5 +0,0 @@ -# to get a string like this run: -# openssl rand -hex 32 -SECRET_KEY = "e909bb995546a0378161ed18d4e44ab4525d735e07a52cec2eb9b3a86d39ee61" -ALGORITHM = "HS256" -ACCESS_TOKEN_EXPIRE_MINUTES = 30 diff --git a/server/test_btchApi.py b/server/test_btchApi.py deleted file mode 100644 index dae1614..0000000 --- a/server/test_btchApi.py +++ /dev/null @@ -1,1534 +0,0 @@ -import unittest -import unittest.mock as mock -from datetime import datetime, timedelta, timezone - -from sqlalchemy import create_engine -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker - -from pathlib import Path - -import sys -try: - from PIL import Image -except ImportError: - print('PIL module is not installed. Some tests will be skipped') - - -from jose import JWTError, jwt - -from fastapi.testclient import TestClient -from fastapi import HTTPException, status - -from .btchApi import app, get_db - -from .btchApiDB import SessionLocal, Base, BtchDBContextManager -from . import crud, models -from .schemas import GameStatus -from .utils import get_password_hash, verify_password - -# TODO we might want to use a Test db context manager with -# all the setUpClass code in it to handle the db session -# and then rollback at the end of the test instead of dropping tables -# something like: -# class TestBtchDBContextManager: -# def __init__(self): -# SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db" - -# engine = create_engine( -# SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} -# ) - -# TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=cls.engine) - -# self.db = TestingSessionLocal() - -# def __enter__(self): -# return self.db - -# def __exit__(self, exc_type, exc_value, traceback): -# self.db.close() - -# uncomment this to debug SQL -# import logging -# logging.basicConfig() -# logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO) - - -class Test_Api(unittest.TestCase): - - @classmethod - def setUpClass(cls): - SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db" - - cls.engine = create_engine( - SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}) - - cls.TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=cls.engine) - - @classmethod - def override_get_db(cls): - try: - db = cls.TestingSessionLocal() - yield db - finally: - db.close() - - def setUp(self): - # TODO setUp correctly with begin-rollback instead of create-drop - # create db, users, games... - - # Base = declarative_base() - - Base.metadata.create_all(bind=self.engine) - - app.dependency_overrides[get_db] = self.override_get_db - - self.client = TestClient(app) - - self.db = self.TestingSessionLocal() - - def tearDown(self): - # delete db - self.db.close() - Base.metadata.drop_all(self.engine) - - def testDataDir(self): - return Path(__file__).parent.parent / "data" / "avatars" - - def fakeusersdb(self): - fake_users_db = { - "johndoe": { - "username": "johndoe", - "full_name": "John Doe", - "email": "johndoe@example.com", - "hashed_password": get_password_hash("secret"), - "disabled": False, - "avatar": None, - "created_at": datetime(2021, 1, 1, tzinfo=timezone.utc), - }, - "janedoe": { - "username": "janedoe", - "full_name": "Jane Doe", - "email": "janedoe@example.com", - "hashed_password": get_password_hash("secret"), - "disabled": False, - "avatar": None, - "created_at": datetime(2021, 1, 1, tzinfo=timezone.utc), - } - } - return fake_users_db - - def fakegamesdb(self): - fake_games_db = { - "lkml4a3.d3": { - "uuid": "lkml4a3.d3", - "owner": "johndoe", - "white": "johndoe", - "black": "janedoe", - "status": GameStatus.STARTED, - "public": False, - "turn": "black", - "created_at": datetime(2021, 1, 1, tzinfo=timezone.utc), - }, - "da39a3ee5e": { - "uuid": "da39a3ee5e", - "owner": "janedoe", - "white": "johndoe", - "black": "janedoe", - "status": GameStatus.OVER, - "public": False, - "winner": "johndoe", - "turn": None, - "created_at": datetime(2021, 3, 12, tzinfo=timezone.utc), - }, - "da40a3ee5e": { - "uuid": "da39a3ee5e", - "owner": "janedoe", - "white": None, - "black": "janedoe", - "status": GameStatus.WAITING, - "public": True, - "winner": None, - "created_at": datetime(2021, 3, 12, tzinfo=timezone.utc), - }, - "123fr12339": { - "uuid": "123fr12339", - "owner": "janedoe", - "white": "janedoe", - "black": None, - "status": GameStatus.WAITING, - "public": True, - "created_at": datetime(2021, 4, 5, tzinfo=timezone.utc), - }, - "d3255bfef9": { - "uuid": "d3255bfef9", - "owner": "johndoe", - "white": "johndoe", - "black": None, - "status": GameStatus.WAITING, - "public": False, - "created_at": datetime(2021, 4, 5, tzinfo=timezone.utc), - } - } - return fake_games_db - - def fakegamesnapsdb(self): - fake_games_snaps = [{ - "game_uuid": "lkml4a3.d3", - "move": "", - "board": ('RNBQKBNR' - 'PPPPPPPP' - '________' - '________' - '________' - '________' - 'pppppppp' - 'rnbqkbnr'), - "taken": "", - "castleable": "", - "move_number": 0, - "created_at": datetime(2021, 4, 5, 0, tzinfo=timezone.utc), - }, { - "game_uuid": "lkml4a3.d3", - "move": "d2d4", - "board": ('RNBQKBNR' - 'PPPPPPPP' - '________' - '________' - '___p____' - '________' - 'ppp_pppp' - 'rnbqkbnr'), - "taken": "", - "castleable": "", - "move_number": 1, - "created_at": datetime(2021, 4, 5, 10, tzinfo=timezone.utc), - }] - return fake_games_snaps - - def addFakeUsers(self, db): - for username, user in self.fakeusersdb().items(): - db_user = models.User( - username=user["username"], - full_name=user["full_name"], - email=user["email"], - hashed_password=user["hashed_password"]) - db.add(db_user) - db.commit() - firstusername, _ = self.fakeusersdb().keys() - return self.getToken(firstusername), firstusername - - def addFakeGames(self, db, fakegamesdb): - for uuid, game in fakegamesdb.items(): - owner = db.query(models.User).filter(models.User.username == game['owner']).first() - white = db.query(models.User).filter(models.User.username == game['white']).first() - black = db.query(models.User).filter(models.User.username == game['black']).first() - db_game = models.Game( - created_at=game["created_at"], - uuid=game["uuid"], - owner_id=owner.id, - white_id=white.id if white is not None else None, - black_id=black.id if black is not None else None, - status=game["status"], - last_move_time=None, - turn=game.get("turn", None), - public=game["public"]) - print(db_game.__dict__) - db.add(db_game) - db.commit() - # force None turn since db defaults to white on creation - if "turn" in game and game["turn"] == None: - db_game.turn = None - db.commit() - print( - f"adding game between {white.id if white is not None else None} and {black.id if black is not None else None}" - ) - return uuid - - def addFakeGameSnaps(self, db, fakegamesnaps): - # TODO get game from uuid - for snap in fakegamesnaps: - guuid = snap["game_uuid"] - - game = crud.get_game_by_uuid(db, guuid) - - db_snap = models.GameSnap( - created_at=snap["created_at"], - game_id=game.id, - board=snap["board"], - move=snap["move"], - taken=snap["taken"], - castleable=snap["castleable"], - move_number=snap["move_number"], - ) - db.add(db_snap) - db.commit() - - def addCustomGameSnap(self, db, boardStr, move): - guuid = "lkml4a3.d3" - - game = crud.get_game_by_uuid(db, guuid) - - db_snap = models.GameSnap( - created_at=datetime(2021, 4, 5, 10, tzinfo=timezone.utc), - game_id=game.id, - board=boardStr, - move=move, - taken="", - castleable="", - move_number=2, - ) - db.add(db_snap) - db.commit() - - def getToken(self, username): - return crud.create_access_token( - data={"sub": username}, expires_delta=timedelta(minutes=3000)) - - def classicSetup(self): - token, _ = self.addFakeUsers(self.db) - self.addFakeGames(self.db, self.fakegamesdb()) - firstgame_uuid = list(self.fakegamesdb().values())[0]["uuid"] - self.addFakeGameSnaps(self.db, self.fakegamesnapsdb()) - - return firstgame_uuid, token - - def test__version(self): - response = self.client.get("/version") - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), {"version": "1.0"}) - - def test__createUser(self): - hashed_password = get_password_hash("secret") - response = self.client.post( - "/users/", - json={ - "username": "alice", - "full_name": "Alice la Suisse", - "email": "alice@lasuisse.ch", - "plain_password": "secret" - }, - ) - - print(response.json()) - - self.assertEqual(response.status_code, 200) - self.assertIsNotNone(response.json()) - response_dict = response.json() - self.assertTrue(verify_password("secret", response_dict["hashed_password"])) - response_dict.pop("hashed_password", None) - self.assertDictEqual( - response_dict, { - "username": "alice", - "created_at": mock.ANY, - "full_name": "Alice la Suisse", - "email": "alice@lasuisse.ch", - "id": 1, - "avatar": None, - "status": "active", - }) - - def test__create_user__with_avatar(self): - hashed_password = get_password_hash("secret") - new_avatar = "images/avatar001.jpeg" - response = self.client.post( - "/users/", - json={ - "username": "alice", - "full_name": "Alice la Suisse", - "email": "alice@lasuisse.ch", - "avatar": new_avatar, - "plain_password": "secret" - }, - ) - - print(response.json()) - - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()['avatar'], new_avatar) - - # TODO fix the put method for user - def _test__update_user__full_name(self): - token, _ = self.addFakeUsers(self.db) - - oneUser = self.db.query(models.User)[1] - - new_full_name = "Alicia la catalana" - - response = self.client.put( - f'/users/update', - headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', - }, - json={ - "username": oneUser.username, - "full_name": new_full_name, - "email": oneUser.email, - "avatar": oneUser.avatar - }) - - print(response.json()) - self.assertEqual(response.status_code, 200) - self.assertDictEqual( - response.json(), { - "username": "alice", - "full_name": "Alice la Suisse", - "email": "alice@lasuisse.ch", - "avatar": new_full_name, - "plain_password": "secret" - }) - - @unittest.skipIf('PIL' not in sys.modules, reason="PIL module is not installed") - def test__upload_user__avatarImage(self): - token, _ = self.addFakeUsers(self.db) - - oneUser = self.db.query(models.User)[1] - filename = self.testDataDir() / "test_avatar.jpeg" - with open(filename, 'rb') as f: - img = Image.open(f) - try: - img.verify() - except (IOError, SyntaxError) as e: - print('Bad file:', filename) - - # TODO reset cursor instead of reopening - with open(filename, 'rb') as f: - response = self.client.put( - f'/users/u/{oneUser.id}/avatar', - headers={'Authorization': 'Bearer ' + token}, - files={'file': f}) - - print(response.json()) - self.assertEqual(response.status_code, 200) - - expected_avatar_dir = Path(__file__).parent.parent / "data" / "avatars" - expected_avatar_filepath = expected_avatar_dir / f"1_avatar.jpeg" - expected_avatar_file = Path(expected_avatar_filepath) - - # remove the test file from the config directory - expected_avatar_file.unlink() - - @unittest.skipIf('PIL' not in sys.modules, reason="PIL module is not installed") - def test__upload_user__avatarImage__file_too_big(self): - token, _ = self.addFakeUsers(self.db) - - oneUser = self.db.query(models.User)[1] - - img = Image.new(mode='RGB', size=(1000, 1000), color = 'red') - response = self.client.put( - f'/users/u/{oneUser.id}/avatar', - headers={'Authorization': 'Bearer ' + token}, - files={'file': img.tobytes()}) - - print(response.json()) - self.assertEqual(response.status_code, 422) - - def test__getUsers__unauthorized(self): - response = self.client.get("/users/") - self.assertEqual(response.status_code, 401) - - def test__authenticate(self): - - # add a user - response = self.client.post( - "/users/", - json={ - "username": "alice", - "full_name": "Alice la Suisse", - "email": "alice@lasuisse.ch", - "plain_password": "secret", - }, - ) - - # test auth - response = self.client.post( - "/token", - headers={"Content-Type": "application/x-www-form-urlencoded"}, - data={ - "username": "alice", - "password": "secret", - }, - ) - - self.assertEqual(response.status_code, 200) - self.assertListEqual(list(response.json().keys()), ['access_token', 'token_type']) - - def test__createUser__persistence(self): - response = self.client.post( - "/users/", - json={ - "username": "alice", - "full_name": "Alice la Suisse", - "email": "alice@lasuisse.ch", - "plain_password": "secret" - }, - ) - - response = self.client.post( - "/token", - headers={'Content-Type': 'application/x-www-form-urlencoded'}, - data={ - "username": "alice", - "password": "secret", - }, - ) - - self.assertIsNotNone(response.json()) - print(response.json()) - self.assertIn('access_token', response.json()) - token = response.json()['access_token'] - - response = self.client.get( - "/users/usernames", - headers={"Authorization": "Bearer " + token}, - ) - - self.assertEqual(response.status_code, 200) - self.assertListEqual(response.json(), ["alice"]) - - def test__addFakeUsers(self): - self.addFakeUsers(self.db) - - def test__getUsernames(self): - token, _ = self.addFakeUsers(self.db) - - users = self.db.query(models.User).all() - - response = self.client.get( - "/users/usernames", - headers={"Authorization": "Bearer " + token}, - ) - - self.assertEqual(response.status_code, 200) - self.assertListEqual(response.json(), ["johndoe", "janedoe"]) - - def test__getUserById(self): - token, _ = self.addFakeUsers(self.db) - - response = self.client.get( - "/users/u/1", - headers={"Authorization": "Bearer " + token}, - ) - - self.assertEqual(response.status_code, 200) - self.assertDictEqual( - response.json(), { - 'username': 'johndoe', - 'full_name': 'John Doe', - 'email': 'johndoe@example.com', - 'avatar': None, - 'id': 1, - 'status': 'active', - }) - - def test__getUserById__malformedId(self): - token, _ = self.addFakeUsers(self.db) - - response = self.client.get( - "/users/u/abcd", - headers={"Authorization": "Bearer " + token}, - ) - - self.assertEqual(response.status_code, 422) - print(response.json()) - self.assertEqual(response.json()['detail'][0]['type'], 'type_error.integer') - - def test__db_cleanup(self): - - users = self.db.query(models.User).all() - - self.assertListEqual(users, []) - - def test__createGame(self): - token, _ = self.addFakeUsers(self.db) - - response = self.client.post( - '/games/', - headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', - }, - json={ - 'public': False, - 'color': 'white' - }, - ) - - print(response.json()) - self.assertEqual(response.status_code, 200) - - self.assertDictEqual( - response.json(), { - 'black_id': None, - 'created_at': mock.ANY, - 'uuid': mock.ANY, - 'id': 1, - 'last_move_time': None, - 'owner_id': 1, - 'public': False, - 'status': GameStatus.WAITING, - 'turn': 'white', - 'white_id': response.json()["owner_id"], - 'winner': None, - }) - - def test__get_game_by_uuid(self): - token, _ = self.addFakeUsers(self.db) - uuid = self.addFakeGames(self.db, self.fakegamesdb()) - - response = self.client.get( - f'/games/{uuid}', - headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', - }, - json={ - 'random': False, - }, - ) - - print(response.json()) - self.assertEqual(response.status_code, 200) - self.assertDictEqual( - response.json(), { - 'black_id': None, - 'created_at': mock.ANY, - 'uuid': mock.ANY, - 'id': 5, - 'owner_id': 1, - 'last_move_time': None, - 'public': False, - 'status': GameStatus.WAITING, - 'turn': 'white', - 'white_id': 1, - 'winner': None, - }) - - def test__get_me_games(self): - token, _ = self.addFakeUsers(self.db) - uuid = self.addFakeGames(self.db, self.fakegamesdb()) - - response = self.client.get( - f'/users/me/games/', - headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', - }, - ) - - print(response.json()) - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.json()), 3) - self.assertDictEqual( - response.json()[0], { - 'black_id': 2, - 'created_at': mock.ANY, - 'uuid': mock.ANY, - 'id': 1, - 'last_move_time': None, - 'owner_id': 1, - 'public': False, - 'last_move_time': None, - 'status': 'started', - 'turn': 'black', - 'white_id': 1, - 'winner': None, - }) - - def test__getGames__finishedGame(self): - _, _ = self.addFakeUsers(self.db) - #change to second player - jane_token = self.getToken("janedoe") - john_token = self.getToken("johndoe") - self.addFakeGames(self.db, self.fakegamesdb()) - self.addFakeGameSnaps(self.db, self.fakegamesnapsdb()) - - response = self.client.get( - '/users/me/games', - headers={ - 'Authorization': 'Bearer ' + jane_token, - 'Content-Type': 'application/json', - }, - ) - print(response.json()) - self.assertEqual(response.status_code, 200) - games = response.json() - self.assertEqual(len(games), 4) - finishedgame = [g for g in games if g['status'] == GameStatus.OVER][0] - self.assertIsNone(finishedgame['turn']) - - # TODO test list random games before setting my player - def test__joinRandomGame(self): - token, _ = self.addFakeUsers(self.db) - uuid = self.addFakeGames(self.db, self.fakegamesdb()) - - oneUser = self.db.query(models.User)[1] - - response = self.client.patch( - '/games', - headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', - }, - ) - - # TODO join game (client chooses one) - - print(response.json()) - self.assertEqual(response.status_code, 200) - self.assertNotEqual(response.json(), {}) - self.assertTrue(response.json()['white_id'] == oneUser.id or - response.json()['black_id'] == oneUser.id) - self.assertDictEqual( - response.json(), { - 'black_id': mock.ANY, - 'created_at': mock.ANY, - 'uuid': mock.ANY, - 'last_move_time': None, - 'id': mock.ANY, - 'owner_id': mock.ANY, - 'public': True, - 'status': 'started', - 'turn': 'white', - 'white_id': mock.ANY, - 'winner': None, - }) - self.assertTrue(response.json()['black_id'] == oneUser.id or - response.json()['white_id'] == oneUser.id) - - # TODO deprecated, client chooses game and joins a random one - def test__joinRandomGame__noneAvailable(self): - token, _ = self.addFakeUsers(self.db) - gamesdbmod = self.fakegamesdb() - gamesdbmod['123fr12339']['status'] = 'done' - gamesdbmod['da40a3ee5e']['status'] = 'done' - uuid = self.addFakeGames(self.db, gamesdbmod) - - response = self.client.patch( - '/games', - headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', - }, - ) - - print(response.json()) - self.assertEqual(response.status_code, 200) - self.assertDictEqual(response.json(), {}) - - def test__listAvailableGames(self): - token, _ = self.addFakeUsers(self.db) - uuid = self.addFakeGames(self.db, self.fakegamesdb()) - - response = self.client.get( - '/games', - headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', - }, - ) - - print(response.json()) - self.assertEqual(response.status_code, 200) - self.maxDiff = None - self.assertListEqual(response.json(), [{ - 'id': 3, - 'uuid': mock.ANY, - 'created_at': mock.ANY, - 'owner_id': 2, - 'last_move_time': None, - 'public': True, - 'white_id': None, - 'black_id': 2, - 'status': GameStatus.WAITING, - 'turn': 'white', - 'winner': None, - }, { - 'black_id': None, - 'created_at': mock.ANY, - 'uuid': mock.ANY, - 'id': 4, - 'last_move_time': None, - 'owner_id': 2, - 'public': True, - 'status': GameStatus.WAITING, - 'turn': 'white', - 'white_id': 2, - 'winner': None, - }]) - - def test__joinGame__playerAlreadyInGame(self): - token, username = self.addFakeUsers(self.db) - self.addFakeGames(self.db, self.fakegamesdb()) - - uuid = self.fakegamesdb['123fr12339'] - - user = crud.get_user_by_username(self.db, username) - - game_before = self.db.query(models.Game).filter(models.Game.uuid == uuid).first() - - response = self.client.get( - f'/games/{uuid}/join', - headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', - }, - ) - - game = self.db.query(models.Game).filter(models.Game.uuid == uuid).first() - - print(response.json()) - self.assertEqual(response.status_code, 200) - if not game_before.black_id: - self.assertEqual(game.black_id, user.id) - if not game_before.white_id: - self.assertEqual(game.white_id, user.id) - self.assertDictEqual( - response.json(), { - 'black_id': game.black_id, - 'created_at': mock.ANY, - 'uuid': game.uuid, - 'id': game.id, - 'last_move_time': None, - 'owner_id': game.owner_id, - 'public': False, - 'status': GameStatus.WAITING, - 'turn': 'white', - 'white_id': game.white_id, - }) - - def test__joinGame__playerAlreadyInGame(self): - token, username = self.addFakeUsers(self.db) - uuid = self.addFakeGames(self.db, self.fakegamesdb()) - - user = crud.get_user_by_username(self.db, username) - - game_before = self.db.query(models.Game).filter(models.Game.uuid == uuid).first() - - response = self.client.get( - f'/games/{uuid}/join', - headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', - }, - ) - - self.assertEqual(response.status_code, 409) - self.assertDictEqual(response.json(), {'detail': 'Player is already in this game'}) - - def test__getsnap__byNum(self): - firstgame_uuid, token = self.classicSetup() - firstgame_white_player = list(self.fakegamesdb().values())[0]["white"] - token = self.getToken(firstgame_white_player) - self.addFakeGameSnaps(self.db, self.fakegamesnapsdb()) - - response = self.client.get( - f'/games/{firstgame_uuid}/snap/0', - headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', - }, - ) - - print(response.json()) - self.assertEqual(response.status_code, 200) - #yapf: disable - self.assertDictEqual( - response.json(), { - 'game_id': 1, - 'created_at': mock.ANY, - 'id': 1, - 'move': None, - 'taken': '', - 'castleable': '', - 'move_number': 0, - 'board': ('xxxxxxxx' - 'xxxxxxxx' - '________' - '________' - '________' - '________' - 'pppppppp' - 'rnbqkbnr'), - }) - #yapf: enable - - def test__getsnaps(self): - self.maxDiff = None - firstgame_uuid, token = self.classicSetup() - - response = self.client.get( - f'/games/{firstgame_uuid}/snaps', - headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', - }, - ) - - print(response.json()) - self.assertEqual(response.status_code, 200) - #yapf: disable - self.assertListEqual(response.json(), [{ - 'game_id': 1, - 'created_at': mock.ANY, - 'id': 1, - 'move': None, - 'taken': '', - 'castleable': '', - 'move_number': 0, - 'board': ('xxxxxxxx' - 'xxxxxxxx' - '________' - '________' - '________' - '________' - 'pppppppp' - 'rnbqkbnr'), - }, { - 'game_id': 1, - 'created_at': mock.ANY, - 'id': 2, - 'move': 'd2d4', - 'taken': '', - 'castleable': '', - 'move_number': 1, - 'board': ('xxxxxxxx' - 'xxxxxxxx' - '________' - '________' - '___p____' - '________' - 'ppp_pppp' - 'rnbqkbnr'), - }]) - #yapf: enable - - def test__getsnap__latest(self): - firstgame_uuid, token = self.classicSetup() - - response = self.client.get( - f'/games/{firstgame_uuid}/snap', - headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', - }, - ) - - print(response.json()) - self.assertEqual(response.status_code, 200) - #yapf: disable - self.assertDictEqual( - response.json(), { - 'game_id': 1, - 'created_at': mock.ANY, - 'id': 2, - 'move': 'd2d4', - 'taken': '', - 'castleable': '', - 'move_number': 1, - 'board': ('xxxxxxxx' - 'xxxxxxxx' - '________' - '________' - '___p____' - '________' - 'ppp_pppp' - 'rnbqkbnr') - }) - #yapf: enable - - def test__getTurn(self): - firstgame_uuid, token = self.classicSetup() - - response = self.client.get( - f'/games/{firstgame_uuid}/turn', - headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', - }, - ) - - print(response.json()) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), 'black') - - def test__move(self): - firstgame_uuid, token = self.classicSetup() - - # get previous game/board - - game_before = self.db.query(models.Game).filter(models.Game.uuid == firstgame_uuid).first() - - response = self.client.post( - f'/games/{firstgame_uuid}/move', - headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', - }, - json={ - "move": "d7d5", - }, - ) - - # get after game/board - - game_after = self.db.query(models.Game).filter(models.Game.uuid == firstgame_uuid).first() - - # test board is the expected one - print([snap.__dict__ for snap in game_after.snaps]) - - print(response.json()) - self.assertEqual(response.status_code, 200) - # self.assertDictEqual(response.json(), '') - - self.assertEqual( - game_before.get_latest_snap().board, - ('RNBQKBNR' - 'PPP_PPPP' - '________' - '___P____' - '___p____' - '________' - 'ppp_pppp' - 'rnbqkbnr'), - ) - - def test__move__filtered(self): - firstgame_uuid, _ = self.classicSetup() - #change to second player - token = self.getToken("janedoe") - - response = self.client.post( - f'/games/{firstgame_uuid}/move', - headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', - }, - json={ - "move": "d7d5", - }, - ) - - print(response.json()) - self.assertEqual(response.status_code, 200) - - self.assertEqual( - response.json()['board'], - ('RNBQKBNR' - 'PPP_PPPP' - '________' - '___P____' - '___p____' - '________' - 'XXX_XXXX' - 'XXXXXXXX'), - ) - - def test__possibleMoves__pawnMove(self): - firstgame_uuid, _ = self.classicSetup() - #change to second player - token = self.getToken("janedoe") - - square = 'd7' - - response = self.client.get( - f'/games/{firstgame_uuid}/moves/{square}', - headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', - }, - ) - - print("response: {}".format(response.json())) - self.assertEqual(response.status_code, 200) - - self.assertListEqual( - response.json(), - ['d6', 'd5'], - ) - - def test__possibleMoves__king(self): - firstgame_uuid, token = self.classicSetup() - - move = 'g3f3' - boardStr = ('____K___' - '________' - '________' - '__p_____' - '________' - '____pk__' - '___P_pp_' - '________') - - self.addCustomGameSnap(self.db, boardStr, move) - - square = 'f3' - - response = self.client.get( - f'/games/{firstgame_uuid}/moves/{square}', - headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', - }, - ) - - print("response: {}".format(response.json())) - self.assertEqual(response.status_code, 200) - - self.assertListEqual( - response.json(), - ['e4', 'f4', 'g4', 'g3', 'e2'], - ) - - def test__possibleMoves__pawn_enpassant_black(self): - firstgame_uuid, _ = self.classicSetup() - - token = self.getToken("janedoe") - - move = 'c2c4' - boardStr = ('____K___' - '________' - '________' - '________' - '__pP____' - '____pk__' - '_____pp_' - '________') - - self.addCustomGameSnap(self.db, boardStr, move) - - square = 'd4' - - response = self.client.get( - f'/games/{firstgame_uuid}/moves/{square}', - headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', - }, - ) - - print("response: {}".format(response.json())) - self.assertEqual(response.status_code, 200) - - self.assertListEqual( - response.json(), - ['c3', 'd3', 'e3'], - ) - - def test__possibleMoves__pawn_enpassant_white(self): - firstgame_uuid, token = self.classicSetup() - - move = 'd7d5' - boardStr = ('____K___' - '________' - '________' - '__pP____' - '________' - '____pk__' - '_____pp_' - '________') - - self.addCustomGameSnap(self.db, boardStr, move) - - square = 'c5' - - response = self.client.get( - f'/games/{firstgame_uuid}/moves/{square}', - headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', - }, - ) - - print("response: {}".format(response.json())) - self.assertEqual(response.status_code, 200) - - self.assertListEqual( - response.json(), - ['c6', 'd6'], - ) - - def test__possibleMoves__pawn_impossible_enpassant_black(self): - firstgame_uuid, _ = self.classicSetup() - - token = self.getToken("janedoe") - - move = 'c7c5' - boardStr = ('____K___' - '________' - '________' - '__pP____' - '________' - '____pk__' - '_____pp_' - '________') - - self.addCustomGameSnap(self.db, boardStr, move) - - square = 'd5' - - response = self.client.get( - f'/games/{firstgame_uuid}/moves/{square}', - headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', - }, - ) - - print("response: {}".format(response.json())) - self.assertEqual(response.status_code, 200) - - self.assertListEqual( - response.json(), - ['d4'], - ) - - def test__possibleMoves__pawn_take(self): - firstgame_uuid, token = self.classicSetup() - - move = 'f5f6' - boardStr = ('____K___' - '_____PP_' - '_____p__' - '________' - '________' - '_____k__' - '_____pp_' - '________') - - self.addCustomGameSnap(self.db, boardStr, move) - - square = 'f6' - - response = self.client.get( - f'/games/{firstgame_uuid}/moves/{square}', - headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', - }, - ) - - print("response: {}".format(response.json())) - self.assertEqual(response.status_code, 200) - - self.assertListEqual( - response.json(), - ['g7'], - ) - - def send_move(self, game_uuid, move, token): - response = self.client.post( - f'/games/{game_uuid}/move', - headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', - }, - json={ - "move": move, - }, - ) - return response - - def prettyBoard(self, boardStr): - print(' abcdefgh') - print(' 01234567') - for i in range(8): - print('{} - {} - {}'.format(i, boardStr[8 * i:8 * i + 8], 8 - i)) - - def test__move__filtered_pawn(self): - _, _ = self.addFakeUsers(self.db) - #change to second player - jane_token = self.getToken("janedoe") - john_token = self.getToken("johndoe") - self.addFakeGames(self.db, self.fakegamesdb()) - firstgame_uuid = list(self.fakegamesdb().values())[0]["uuid"] - self.addFakeGameSnaps(self.db, self.fakegamesnapsdb()) - - tokens = [jane_token, john_token] - moves = ['e7e5', 'd4e5', 'h7h6', 'e5e6'] - - response = self.send_move(firstgame_uuid, moves[0], tokens[0 % 2]) - print(response.json()) - self.prettyBoard(response.json()['board']) - self.assertEqual(response.status_code, 200) - - self.assertEqual( - response.json()['board'], - ('RNBQKBNR' - 'PPPP_PPP' - '________' - '____P___' - '___p____' - '________' - 'XXX_XXXX' - 'XXXXXXXX'), - ) - - response = self.send_move(firstgame_uuid, moves[1], tokens[1 % 2]) - print(response.json()) - self.prettyBoard(response.json()['board']) - self.assertEqual(response.status_code, 200) - - response = self.send_move(firstgame_uuid, moves[2], tokens[2 % 2]) - print(response.json()) - self.prettyBoard(response.json()['board']) - self.assertEqual(response.status_code, 200) - - self.assertEqual( - response.json()['board'], - ('RNBQKBNR' - 'PPPP_PP_' - '_______P' - '____X___' - '________' - '________' - 'XXX_XXXX' - 'XXXXXXXX'), - ) - - # use this method as reference to reproduce any game moves - # TODO use a virgin game instead of the firstgame_uuid - def test__move__fogTest(self): - _, _ = self.addFakeUsers(self.db) - #change to second player - jane_token = self.getToken("janedoe") - john_token = self.getToken("johndoe") - self.addFakeGames(self.db, self.fakegamesdb()) - firstgame_uuid = list(self.fakegamesdb().values())[0]["uuid"] - self.addFakeGameSnaps(self.db, self.fakegamesnapsdb()) - - tokens = [jane_token, john_token] - moves = ['e7e6', 'g2g4', 'd8h4', 'f2f4', 'a7a6'] - - print(tokens) - - for i, move in enumerate(moves): - print("move {} for {}".format(move, tokens[i % 2])) - response = self.send_move(firstgame_uuid, move, tokens[i % 2]) - print(response.json()) - self.assertEqual(response.status_code, 200) - self.prettyBoard(response.json()['board']) - - self.assertEqual( - response.json()['board'], - ('RNB_KBNR' - '_PPP_PPP' - 'P___P___' - '________' - '___X_XpQ' - '________' - 'XXX_X__X' - 'XXXXXXXX'), - ) - - def test__integrationTest__foolscheckmate(self): - - # create johndoe - # create janedoe - - response = self.client.post( - "/users/", - json={ - "username": "johndoe", - "full_name": "John Le Dow", - "email": "john@doe.cat", - "plain_password": "secret" - }, - ) - - john_id = response.json()['id'] - - self.assertEqual(response.status_code, 200) - - response = self.client.post( - "/users/", - json={ - "username": "janedoe", - "full_name": "Jane Le Dow", - "email": "jane@doe.cat", - "plain_password": "secret" - }, - ) - - jane_id = response.json()['id'] - - self.assertEqual(response.status_code, 200) - - # authenticate - response = self.client.post( - "/token", - headers={"Content-Type": "application/x-www-form-urlencoded"}, - data={ - "username": "johndoe", - "password": "secret", - }, - ) - - self.assertEqual(response.status_code, 200) - john_token = response.json()['access_token'] - - response = self.client.post( - "/token", - headers={"Content-Type": "application/x-www-form-urlencoded"}, - data={ - "username": "janedoe", - "password": "secret", - }, - ) - - self.assertEqual(response.status_code, 200) - jane_token = response.json()['access_token'] - - # john create game - - response = self.client.post( - '/games/', - headers={ - 'Authorization': 'Bearer ' + john_token, - 'Content-Type': 'application/json', - }, - json={ - 'public': False, - 'color': 'white', - }, - ) - - self.assertEqual(response.status_code, 200) - - game_uuid = response.json()['uuid'] - - # john already joined and jane joins game - - # check if game started - response = self.client.get( - f'/games/{game_uuid}', - headers={ - 'Authorization': 'Bearer ' + jane_token, - 'Content-Type': 'application/json', - }, - ) - - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()['status'], GameStatus.WAITING) - - white_id = response.json()['white_id'] - black_id = response.json()['black_id'] - jane_color = None if not white_id else 'white' if white_id == jane_id else 'black' - john_color = None if not white_id else 'white' if response.json( - )['white_id'] == john_id else 'black' - - print(f'jane color is {jane_color}') - - response = self.client.get( - f'/games/{game_uuid}/join', - headers={ - 'Authorization': 'Bearer ' + jane_token, - 'Content-Type': 'application/json', - }, - ) - - self.assertEqual(response.status_code, 200) - - print(response.json()) - - # john send move - # jane send move - - moves = ['f2f3', 'e7e5', 'g2g4', 'd8h4', 'f3f4', 'h4e1'] - - boards = [ - ('xxxxxxxxxxxxxxxx_____________________________p__ppppp_pprnbqkbnr', - 'RNBQKBNRPPPPPPPP_____________________________X__XXXXX_XXXXXXXXXX'), - ('xxxxxxxxxxxx_xxx____________x________________p__ppppp_pprnbqkbnr', - 'RNBQKBNRPPPP_PPP____________P________________X__XXXXX_XXXXXXXXXX'), - ('xxxxxxxxxxxx_xxx____________x_________p______p__ppppp__prnbqkbnr', - 'RNBQKBNRPPPP_PPP____________P_________X______X__XXXXX__XXXXXXXXX'), - ('xxx_xxxxxxxx_xxx____________x_________pQ_____p__ppppp__prnbqkbnr', - 'RNB_KBNRPPPP_PPP____________P_________pQ_____X__XXXXX__XXXXXXXXX'), - ('xxx_xxxxxxxx_xxx____________P________ppQ________ppppp__prnbqkbnr', - 'RNB_KBNRPPPP_PPP____________P________ppQ________XXXXX__XXXXXXXXX'), - ('xxx_xxxxxxxx_xxx____________P________ppQ_____p__ppppp__prnbqkbnr', - 'RNB_KBNRPPPP_PPP____________P________pX______X__pppXX__XXXXqQbXX'), - ] - - tokens = [john_token, jane_token] - - for i, move in enumerate(moves): - response = self.send_move(game_uuid, move, tokens[i % 2]) - print(f'ran move {move} by {jane_color if jane_token == tokens[i%2] else john_color}') - self.assertEqual(response.status_code, 200) - - # they ask for game and turn - - response = self.client.get( - f'/games/{game_uuid}/turn', - headers={ - 'Authorization': 'Bearer ' + jane_token, - 'Content-Type': 'application/json', - }, - ) - - self.assertEqual(response.status_code, 200) - jane_turn = response.json() - response = self.client.get( - f'/games/{game_uuid}/turn', - headers={ - 'Authorization': 'Bearer ' + john_token, - 'Content-Type': 'application/json', - }, - ) - - self.assertEqual(response.status_code, 200) - john_turn = response.json() - - # TODO what happens after checkmate? - self.assertEqual(jane_turn, john_turn) - - response = self.client.get( - f'/games/{game_uuid}/snap', - headers={ - 'Authorization': 'Bearer ' + jane_token, - 'Content-Type': 'application/json', - }, - ) - - print(self.prettyBoard(response.json()['board'])) - - # no winner - if john_turn or jane_turn: - #TODO note that this assert will fail if an enemy piece is seen - if jane_color == 'white': - self.assertEqual(response.json()['board'], boards[i][0]) - else: - self.assertEqual(response.json()['board'], boards[i][1]) - - response = self.client.get( - f'/games/{game_uuid}/snap', - headers={ - 'Authorization': 'Bearer ' + john_token, - 'Content-Type': 'application/json', - }, - ) - - if john_turn or jane_turn: - if john_color == 'white': - self.assertEqual(response.json()['board'], boards[i][0]) - else: - self.assertEqual(response.json()['board'], boards[i][1]) - - # checkmate - - response = self.client.get( - f'/games/{game_uuid}', - headers={ - 'Authorization': 'Bearer ' + jane_token, - 'Content-Type': 'application/json', - }, - ) - - print("{} with {} won the game".format( - "janedoe" if jane_color == response.json()['winner'] else "johndoe", jane_color)) - - self.assertEqual(response.json()['winner'], 'black') diff --git a/server/test_btchApi_autoplay.py b/server/test_btchApi_autoplay.py deleted file mode 100644 index 916e0f7..0000000 --- a/server/test_btchApi_autoplay.py +++ /dev/null @@ -1,284 +0,0 @@ -import unittest -import unittest.mock as mock -from datetime import datetime, timedelta, timezone - -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker - -from fastapi.testclient import TestClient -from fastapi import HTTPException, status - -from .btchApi import app, get_db - -from .btchApiDB import Base -from . import crud, models -from .schemas import GameStatus -from .utils import get_password_hash - -import json - - -class Test_Api_Autoplay(unittest.TestCase): - - @classmethod - def setUpClass(cls): - SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db" - - cls.engine = create_engine( - SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}) - - cls.TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=cls.engine) - - @classmethod - def override_get_db(cls): - try: - db = cls.TestingSessionLocal() - yield db - finally: - db.close() - - def setUp(self): - # TODO setUp correctly with begin-rollback instead of create-drop - # create db, users, games... - - # Base = declarative_base() - - Base.metadata.create_all(bind=self.engine) - - app.dependency_overrides[get_db] = self.override_get_db - - self.client = TestClient(app) - - self.db = self.TestingSessionLocal() - - def tearDown(self): - # delete db - self.db.close() - Base.metadata.drop_all(self.engine) - - def fakeusersdb(self): - fake_users_db = { - "johndoe": { - "username": "johndoe", - "full_name": "John Doe", - "email": "johndoe@example.com", - "hashed_password": get_password_hash("secret"), - "disabled": False, - "avatar": None, - "created_at": datetime(2021, 1, 1, tzinfo=timezone.utc), - }, - "janedoe": { - "username": "janedoe", - "full_name": "Jane Doe", - "email": "janedoe@example.com", - "hashed_password": get_password_hash("secret"), - "disabled": False, - "avatar": None, - "created_at": datetime(2021, 1, 1, tzinfo=timezone.utc), - } - } - return fake_users_db - - def fakegamesdb(self): - fake_games_db = { - "lkml4a3.d3": { - "uuid": "lkml4a3.d3", - "owner": "johndoe", - "white": "johndoe", - "black": "janedoe", - "status": GameStatus.STARTED, - "public": False, - "turn": "black", - "created_at": datetime(2021, 1, 1, tzinfo=timezone.utc), - } - } - return fake_games_db - - def fakegamesnapsdb(self): - fake_games_snaps = [{ - "game_uuid": "lkml4a3.d3", - "move": "", - "board": ('RNBQKBNR' - 'PPPPPPPP' - '________' - '________' - '________' - '________' - 'pppppppp' - 'rnbqkbnr'), - "taken": "", - "castleable": "LKSlks", - "move_number": 0, - "created_at": datetime(2021, 4, 5, 0, tzinfo=timezone.utc), - }] - return fake_games_snaps - - def addFakeUsers(self, db): - for username, user in self.fakeusersdb().items(): - db_user = models.User( - username=user["username"], - full_name=user["full_name"], - email=user["email"], - hashed_password=user["hashed_password"]) - db.add(db_user) - db.commit() - firstusername, _ = self.fakeusersdb().keys() - return self.getToken(firstusername), firstusername - - def addFakeGames(self, db, fakegamesdb): - for uuid, game in fakegamesdb.items(): - owner = db.query(models.User).filter(models.User.username == game['owner']).first() - white = db.query(models.User).filter(models.User.username == game['white']).first() - black = db.query(models.User).filter(models.User.username == game['black']).first() - db_game = models.Game( - created_at=game["created_at"], - uuid=game["uuid"], - owner_id=owner.id, - white_id=white.id if white is not None else None, - black_id=black.id if black is not None else None, - status=game["status"], - last_move_time=None, - turn=game.get("turn", None), - public=game["public"]) - print(db_game.__dict__) - db.add(db_game) - db.commit() - # force None turn since db defaults to white on creation - if "turn" in game and game["turn"] == None: - db_game.turn = None - db.commit() - print( - f"adding game between {white.id if white is not None else None} and {black.id if black is not None else None}" - ) - return uuid - - def addFakeGameSnaps(self, db, fakegamesnaps): - # TODO get game from uuid - for snap in fakegamesnaps: - guuid = snap["game_uuid"] - - game = crud.get_game_by_uuid(db, guuid) - - db_snap = models.GameSnap( - created_at=snap["created_at"], - game_id=game.id, - board=snap["board"], - move=snap["move"], - taken=snap["taken"], - castleable=snap["castleable"], - move_number=snap["move_number"], - ) - db.add(db_snap) - db.commit() - - def getToken(self, username): - return crud.create_access_token( - data={"sub": username}, expires_delta=timedelta(minutes=3000)) - - def test__version(self): - response = self.client.get("/version") - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), {"version": "1.0"}) - - def send_move(self, game_uuid, move, token): - response = self.client.post( - f'/games/{game_uuid}/move', - headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', - }, - json={ - "move": move, - }, - ) - return response - - def prettyBoard(self, boardStr): - print(' abcdefgh') - print(' 01234567') - for i in range(8): - print('{} - {} - {}'.format(i, boardStr[8 * i:8 * i + 8], 8 - i)) - - def resetGame(self, db, uuid): - game = db.query(models.Game).filter(models.Game.uuid == uuid).first() - game.reset() - db.commit() - - def test__move__MrExonGame__OneGame__EnPassant(self): - _, _ = self.addFakeUsers(self.db) - jane_token = self.getToken("janedoe") - john_token = self.getToken("johndoe") - self.addFakeGames(self.db, self.fakegamesdb()) - firstgame_uuid = list(self.fakegamesdb().values())[0]["uuid"] - self.addFakeGameSnaps(self.db, self.fakegamesnapsdb()) - - tokens = [jane_token, john_token] - - # read games - # for game in games: - # for moves in game['moves'] - with open('ia/algebraic2icu/icu/mrexongames.txt') as json_file: - data = json.load(json_file) - moves = data['https://lichess.org/auXLYNj1'] - for i, move in enumerate(moves): - response = self.send_move(firstgame_uuid, move, tokens[i % 2]) - - if response.status_code == 200: - self.prettyBoard(response.json()['board']) - else: - print(response.json()) - self.assertEqual(response.status_code, 200) - - def test__move__MrExonGame__Enpassant2(self): - _, _ = self.addFakeUsers(self.db) - jane_token = self.getToken("janedoe") - john_token = self.getToken("johndoe") - self.addFakeGames(self.db, self.fakegamesdb()) - firstgame_uuid = list(self.fakegamesdb().values())[0]["uuid"] - self.addFakeGameSnaps(self.db, self.fakegamesnapsdb()) - - tokens = [jane_token, john_token] - - # read games - # for game in games: - # for moves in game['moves'] - with open('ia/algebraic2icu/icu/mrexongames.txt') as json_file: - data = json.load(json_file) - moves = data['https://lichess.org/X1Nk72xr'] - for i, move in enumerate(moves): - response = self.send_move(firstgame_uuid, move, tokens[i % 2]) - - if response.status_code == 200: - self.prettyBoard(response.json()['board']) - else: - print(response.json()) - self.assertEqual(response.status_code, 200) - - @unittest.skip("Extremely slow test. Run with -s option.") - def test__move__MrExonGames(self): - _, _ = self.addFakeUsers(self.db) - jane_token = self.getToken("janedoe") - john_token = self.getToken("johndoe") - self.addFakeGames(self.db, self.fakegamesdb()) - firstgame_uuid = list(self.fakegamesdb().values())[0]["uuid"] - self.addFakeGameSnaps(self.db, self.fakegamesnapsdb()) - - tokens = [jane_token, john_token] - - # read games - # for game in games: - # for moves in game['moves'] - with open('ia/algebraic2icu/icu/mrexongames.txt') as json_file: - data = json.load(json_file) - for game, moves in data.items(): - print('Game: {} moves {}'.format(game, moves)) - for move in moves: - response = self.send_move(firstgame_uuid, move, tokens[0 % 2]) - - if response.status_code == 200: - self.prettyBoard(response.json()['board']) - else: - print(response.json()) - self.assertEqual(response.status_code, 200) - - self.resetGame(self.db, firstgame_uuid) diff --git a/server/utils.py b/server/utils.py deleted file mode 100644 index c999c97..0000000 --- a/server/utils.py +++ /dev/null @@ -1,52 +0,0 @@ -import random -import string -from passlib.context import CryptContext - -from .config import HANDLEBASEURL - -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") - -def verify_password(plain_password, hashed_password): - # TODO catch UnknownHashError for plain-text stored passwords - return pwd_context.verify(plain_password, hashed_password) - -def get_password_hash(password): - return pwd_context.hash(password) - -# TODO use Random-Word or something for more user-friendly handles -def get_random_string(length=6): - # choose from all lowercase letter - letters = string.ascii_lowercase - result_str = ''.join(random.choice(letters) for i in range(length)) - return result_str - -def handle2uuid(uuid): - return HANDLEBASEURL + uuid - -def defaultBoard(): - return ( - 'RNBQKBNR' - 'PPPPPPPP' - '________' - '________' - '________' - '________' - 'pppppppp' - 'rnbqkbnr' - ) - - -def extij2ad(i, j): - square = chr(j - 2 + 97) + str(8 - (i - 2)) - return square - - -def ad2extij(square): - i = 8 - int(square[1]) + 2 - j = ord(square[0]) - ord('a') + 2 - return (i, j) - -def ad2ij(square): - i = 8 - int(square[1]) - j = ord(square[0]) - ord('a') - return (i, j) \ No newline at end of file diff --git a/test_communication.py b/test_communication.py deleted file mode 100644 index 1feb21b..0000000 --- a/test_communication.py +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/python -import unittest -from unittest.mock import MagicMock - -import communication - -def test_sendData(): - pass diff --git a/tests/battlechess_standalone/test_communication.py b/tests/battlechess_standalone/test_communication.py new file mode 100644 index 0000000..31251d2 --- /dev/null +++ b/tests/battlechess_standalone/test_communication.py @@ -0,0 +1,5 @@ +#!/usr/bin/python + + +def test_sendData(): + pass diff --git a/tests/core/test_Board.py b/tests/core/test_Board.py new file mode 100644 index 0000000..23bf20e --- /dev/null +++ b/tests/core/test_Board.py @@ -0,0 +1,114 @@ +from battlechess.core.Board import Board + +def fakeElements(): + elements = { + "board": ( + "RNBQKBNR" + "PPPPPPPP" + "________" + "________" + "________" + "________" + "pppppppp" + "rnbqkbnr" + ), + "taken": "", + "castleable": "LSKlsk", + "enpassant": None, + "winner": None, + } + + return elements + +def test__updateFromElements__board(): + b = Board() + b.reset() + + expected = b.toString() + + b.updateFromElements(**fakeElements()) + + startStrUpdated = b.toString() + + assert startStrUpdated == expected + +def test__toElements__initialboard(): + b = Board() + b.reset() + + elements = b.toElements() + + expected = { + "board": "RNBQKBNRPPPPPPPP________________________________pppppppprnbqkbnr", + "castleable": "KLSkls", + "taken": "", + } + + assert elements == expected + +def test__toElements__someboard(): + b = Board() + b.reset() + b.castleable = sorted(["kb", "kw", "rkb"]) + b.taken = ["bb", "rb", "rw", "pw"] + + elements = b.toElements() + + expected = { + "board": "RNBQKBNRPPPPPPPP________________________________pppppppprnbqkbnr", + "castleable": "KSk", + "taken": "BRrp", + } + + assert elements == expected + +def test__toElements__anotherboard(): + b = Board() + b.reset() + b.board[0][2] = "" + b.board[0][5] = "" + b.board[0][7] = "" + b.board[1][3] = "pw" + b.board[1][4] = "" + b.board[5][7] = "bb" + b.board[6][3] = "" + b.board[6][5] = "" + b.board[7][0] = "" + b.castleable = sorted(["kb", "kw", "rkb"]) + b.taken = ["bb", "rb", "rw", "pw", "pb"] + + elements = b.toElements() + + expected = { + "board": "RN_QK_N_PPPp_PPP_______________________________Bppp_p_pp_nbqkbnr", + "castleable": "KSk", + "taken": "BRrpP", + } + + assert elements == expected + +def test__toElementsFiltered__anotherboard(): + b = Board() + b.reset() + b.board[0][2] = "" + b.board[0][5] = "" + b.board[0][7] = "" + b.board[1][3] = "pw" + b.board[1][4] = "" + b.board[5][7] = "bb" + b.board[6][3] = "" + b.board[6][5] = "" + b.board[7][0] = "" + + b.castleable = sorted(["kb", "kw", "rkb"]) + b.taken = ["bb", "rb", "rw", "pw", "pb"] + + elements = b.toElements("w") + + expected = { + "board": "___QK_____Pp___________________________________Bppp_p_pp_nbqkbnr", + "castleable": "KSk", + "taken": "BRrpP", + } + + assert elements == expected diff --git a/tests/core/test_btchBoard.py b/tests/core/test_btchBoard.py new file mode 100644 index 0000000..5d582fb --- /dev/null +++ b/tests/core/test_btchBoard.py @@ -0,0 +1,165 @@ +from battlechess.core.btchBoard import BtchBoard + + +def startboardStr(): + return ( + "RNBQKBNR" + "PPPPPPPP" + "________" + "________" + "________" + "________" + "pppppppp" + "rnbqkbnr" + ) + +def fakeElements(): + elements = { + "board": startboardStr(), + "taken": "", + "castleable": "LSKlsk", + "enpassant": None, + "winner": None, + } + + return elements + +def squares2ascii(squares): + return "\n".join( + "".join("x" if (i, j) in squares else "_" for j in range(0, 12)) + for i in range(0, 12) + ) + +def test__factory(): + btchBoard = BtchBoard.factory(startboardStr()) + + btchBoard.toElements() == fakeElements() + +def test__isEnemy(): + + result = BtchBoard.isEnemy("white", None) + + assert not result + + result = BtchBoard.isEnemy("white", "p") + + assert not result + + result = BtchBoard.isEnemy("white", "P") + + assert result + + result = BtchBoard.isEnemy("white", "_") + + assert not result + +def test__rookMoves__emptyBoard(): + + b = BtchBoard() + b.empty() + + moves = sorted(sq for sq in b.rookMoves("white", 2, 2)) + + print("moves\n{}".format(squares2ascii(moves))) + + expected = sorted( + [(i, 2) for i in range(3, 10)] + [(2, j) for j in range(3, 10)] + ) + + print("expected\n{}".format(squares2ascii(expected))) + + assert moves == expected + +def test__rookMoves__startBoard(): + + b = BtchBoard() + + moves = sorted(sq for sq in b.rookMoves("black", 2, 2)) + + expected = [] + + assert moves == expected + +def test__bishopMoves__emptyBoard(): + + b = BtchBoard() + b.empty() + + moves = sorted(sq for sq in b.bishopMoves("white", 6, 6)) + + print("moves\n{}".format(squares2ascii(moves))) + + expected = sorted( + [(i, i) for i in range(2, 10)] + [(3 + i, 9 - i) for i in range(0, 7)] + ) + expected.remove((6, 6)) + expected.remove((6, 6)) + + print("expected\n {}".format(squares2ascii(expected))) + + assert moves == expected + +def test__moves__pawn(): + b = BtchBoard() + + moves = sorted(sq for sq in b.pawnMoves("white", 8, 6)) + + expected = [(6, 6), (7, 6)] + + assert moves == expected + +def test__moves__manyMoves(): + pass + +def test__moves__enpassant(): + pass + +def test__moves__notMovingForbidden(): + pass + +# check that an impossible move is possible if fogged enemies +def test__moves__unknownInfo(): + pass + +def test__filter__startPosition(): + color = "white" + b = BtchBoard() + b.filter(color) + + expectedBoardStr = ( + "________" + "________" + "________" + "________" + "________" + "________" + "pppppppp" + "rnbqkbnr" + ) + + expected = BtchBoard.factory(expectedBoardStr) + expected.castleable = "lsk" + + assert b.toElements() == expected.toElements() + +def test__moves__fog(): + boardStr = ( + "________" + "________" + "________" + "________" + "________" + "________" + "ppppppp_" + "rnbqkbnr" + ) + + b = BtchBoard.factory(boardStr) + + print("fog {} ".format(b.toElements())) + + moves = b.possibleMoves("white", 9, 9) + + expectedMoves = sorted([(i, 9) for i in range(2, 9)]) + + assert moves == expectedMoves diff --git a/tests/server/conftest.py b/tests/server/conftest.py new file mode 100644 index 0000000..8f1cb00 --- /dev/null +++ b/tests/server/conftest.py @@ -0,0 +1,353 @@ +from datetime import datetime, timedelta, timezone + +import pytest +from fastapi.testclient import TestClient +from httpx import AsyncClient +from sqlalchemy import create_engine +from sqlalchemy.orm import Session +from sqlalchemy_utils import create_database, database_exists + +from battlechess.server import crud, models +from battlechess.server.btchApi import app, get_db +from battlechess.server.btchApiDB import Base +from battlechess.server.schemas import GameStatus +from battlechess.server.utils import get_password_hash + + +@pytest.fixture +def fakeusersdb(): + fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": get_password_hash("secret"), + "disabled": False, + "avatar": None, + "created_at": datetime(2021, 1, 1, tzinfo=timezone.utc), + }, + "janedoe": { + "username": "janedoe", + "full_name": "Jane Doe", + "email": "janedoe@example.com", + "hashed_password": get_password_hash("secret"), + "disabled": False, + "avatar": None, + "created_at": datetime(2021, 1, 1, tzinfo=timezone.utc), + }, + } + return fake_users_db + + +@pytest.fixture(scope="function") +def janedoe(): + return { + "avatar": None, + "email": "janedoe@example.com", + "full_name": "Jane Doe", + "username": "janedoe", + } + + +@pytest.fixture(scope="function") +def johndoe(): + return { + "avatar": None, + "email": "johndoe@example.com", + "full_name": "John Doe", + "username": "johndoe", + } + + +@pytest.fixture +def fakegamesdb(): + fake_games_db = { + "lkml4a3.d3": { + "uuid": "lkml4a3.d3", + "owner": "johndoe", + "white": "johndoe", + "black": "janedoe", + "status": GameStatus.STARTED, + "public": False, + "turn": "black", + "created_at": datetime(2021, 1, 1, tzinfo=timezone.utc), + }, + "da39a3ee5e": { + "uuid": "da39a3ee5e", + "owner": "janedoe", + "white": "johndoe", + "black": "janedoe", + "status": GameStatus.OVER, + "public": False, + "winner": "johndoe", + "turn": None, + "created_at": datetime(2021, 3, 12, tzinfo=timezone.utc), + }, + "da40a3ee5e": { + "uuid": "da39a3ee5e", + "owner": "janedoe", + "white": None, + "black": "janedoe", + "status": GameStatus.WAITING, + "public": True, + "winner": None, + "created_at": datetime(2021, 3, 12, tzinfo=timezone.utc), + }, + "123fr12339": { + "uuid": "123fr12339", + "owner": "janedoe", + "white": "janedoe", + "black": None, + "status": GameStatus.WAITING, + "public": True, + "created_at": datetime(2021, 4, 5, tzinfo=timezone.utc), + }, + "d3255bfef9": { + "uuid": "d3255bfef9", + "owner": "johndoe", + "white": "johndoe", + "black": None, + "status": GameStatus.WAITING, + "public": False, + "created_at": datetime(2021, 4, 5, tzinfo=timezone.utc), + }, + } + return fake_games_db + + +def fakegamesnapsdb(): + fake_games_snaps = [ + { + "game_uuid": "lkml4a3.d3", + "move": "", + "board": ( + "RNBQKBNR" + "PPPPPPPP" + "________" + "________" + "________" + "________" + "pppppppp" + "rnbqkbnr" + ), + "taken": "", + "castleable": "LKSlks", + "move_number": 0, + "created_at": datetime(2021, 4, 5, 0, tzinfo=timezone.utc), + }, + { + "game_uuid": "lkml4a3.d3", + "move": "d2d4", + "board": ( + "RNBQKBNR" + "PPPPPPPP" + "________" + "________" + "___p____" + "________" + "ppp_pppp" + "rnbqkbnr" + ), + "taken": "", + "castleable": "LKSlks", + "move_number": 1, + "created_at": datetime(2021, 4, 5, 10, tzinfo=timezone.utc), + }, + ] + return fake_games_snaps + + +@pytest.fixture(scope="function") +def addFakeUsers(db, fakeusersdb): + for username, user in fakeusersdb.items(): + db_user = models.User( + username=user["username"], + full_name=user["full_name"], + email=user["email"], + hashed_password=user["hashed_password"], + ) + db.add(db_user) + db.commit() + firstusername, _ = fakeusersdb.keys() + return getToken(firstusername), firstusername + + +def addFakeGamesFromDict(db, gamesdb): + for uuid, game in gamesdb.items(): + owner = ( + db.query(models.User).filter(models.User.username == game["owner"]).first() + ) + white = ( + db.query(models.User).filter(models.User.username == game["white"]).first() + ) + black = ( + db.query(models.User).filter(models.User.username == game["black"]).first() + ) + db_game = models.Game( + created_at=game["created_at"], + uuid=game["uuid"], + owner_id=owner.id, + white_id=white.id if white is not None else None, + black_id=black.id if black is not None else None, + status=game["status"], + last_move_time=None, + turn=game.get("turn", None), + public=game["public"], + ) + print(db_game.__dict__) + db.add(db_game) + db.commit() + # force None turn since db defaults to white on creation + if "turn" in game and game["turn"] is None: + db_game.turn = None + db.commit() + print( + f"""adding game between {white.id if white is not None else None} + and {black.id if black is not None else None} + """ + ) + return uuid + + +@pytest.fixture(scope="function") +def addFakeGames(db, fakegamesdb): + gamesdb = fakegamesdb + return addFakeGamesFromDict(db, gamesdb) + + +@pytest.fixture(scope="function") +def addFakeDoneGames(db, fakegamesdb): + gamesdbmod = fakegamesdb + gamesdbmod["123fr12339"]["status"] = "done" + gamesdbmod["da40a3ee5e"]["status"] = "done" + return addFakeGamesFromDict(db, gamesdbmod) + + +@pytest.fixture(scope="function") +def addFakeGameSnaps(db): + # TODO get game from uuid + for snap in fakegamesnapsdb(): + guuid = snap["game_uuid"] + + game = crud.get_game_by_uuid(db, guuid) + + db_snap = models.GameSnap( + created_at=snap["created_at"], + game_id=game.id, + board=snap["board"], + move=snap["move"], + taken=snap["taken"], + castleable=snap["castleable"], + move_number=snap["move_number"], + ) + db.add(db_snap) + db.commit() + + +@pytest.fixture(scope="function") +def addFakeGameStartSnap(db): + snap = fakegamesnapsdb()[0] + guuid = snap["game_uuid"] + + game = crud.get_game_by_uuid(db, guuid) + + db_snap = models.GameSnap( + created_at=snap["created_at"], + game_id=game.id, + board=snap["board"], + move=snap["move"], + taken=snap["taken"], + castleable=snap["castleable"], + move_number=snap["move_number"], + ) + db.add(db_snap) + db.commit() + + +@pytest.fixture(scope="function") +def addCustomGameSnap(request, db): + boardStr, move = request.param + guuid = "lkml4a3.d3" + + game = crud.get_game_by_uuid(db, guuid) + + db_snap = models.GameSnap( + created_at=datetime(2021, 4, 5, 10, tzinfo=timezone.utc), + game_id=game.id, + board=boardStr, + move=move, + taken="", + castleable="", + move_number=2, + ) + db.add(db_snap) + db.commit() + + +def getToken(username): + return crud.create_access_token( + data={"sub": username}, expires_delta=timedelta(minutes=3000) + ) + + +@pytest.fixture(scope="function") +def classicSetup(db, addFakeUsers, addFakeGames, fakegamesdb, addFakeGameSnaps): + token, _ = addFakeUsers + firstgame_uuid = list(fakegamesdb.values())[0]["uuid"] + + return firstgame_uuid, token + + +@pytest.fixture(scope="session") +def db_engine(): + SQLALCHEMY_DATABASE_URL = "sqlite:///./test_db.db" + + engine = create_engine(SQLALCHEMY_DATABASE_URL) + if not database_exists: + create_database(engine.url) + + Base.metadata.create_all(bind=engine) + yield engine + + +@pytest.fixture(scope="function") +def db(db_engine): + connection = db_engine.connect() + + # begin a non-ORM transaction + transaction = connection.begin() # noqa + + # bind an individual Session to the connection + db = Session(bind=connection) + # db = Session(db_engine) + + yield db + + db.rollback() + connection.close() + + +@pytest.fixture(scope="function") +def client(db): + app.dependency_overrides[get_db] = lambda: db + + with TestClient(app) as c: + yield c + + +@pytest.fixture(scope="function") +async def asyncclient(db): + app.dependency_overrides[get_db] = lambda: db + + async with AsyncClient(app=app, base_url="http://test") as c: + yield c + + +@pytest.fixture(scope="function") +def game_setup(db, addFakeUsers, addFakeGames, addFakeGameStartSnap, fakegamesdb): + _, _ = addFakeUsers + jane_token = getToken("janedoe") + john_token = getToken("johndoe") + firstgame_uuid = list(fakegamesdb.values())[0]["uuid"] + + return firstgame_uuid, john_token, jane_token diff --git a/tests/server/test_btchApi.py b/tests/server/test_btchApi.py new file mode 100644 index 0000000..5fe929e --- /dev/null +++ b/tests/server/test_btchApi.py @@ -0,0 +1,1359 @@ +import sys +import time +import unittest.mock as mock +from datetime import timedelta +from pathlib import Path + +import pytest + +try: + from PIL import Image +except ImportError: + print("PIL module is not installed. Some tests will be skipped") + + +from battlechess.server import crud, models +from battlechess.server.schemas import GameStatus +from battlechess.server.utils import get_password_hash, verify_password + + +def dataTestDir(): + return Path(__file__).parent.parent / "data" / "avatars" + + +def getToken(username): + return crud.create_access_token( + data={"sub": username}, expires_delta=timedelta(minutes=3000) + ) + + +def test__version(client): + response = client.get("/version") + assert response.status_code == 200 + assert response.json() == {"version": "1.0"} + + +def test__createUser(client): + get_password_hash("secret") + response = client.post( + "/users/", + json={ + "username": "alice", + "full_name": "Alice la Suisse", + "email": "alice@lasuisse.ch", + "plain_password": "secret", + }, + ) + + print(response.json()) + + assert response.status_code == 200 + assert response.json() is not None + response_dict = response.json() + assert verify_password("secret", response_dict["hashed_password"]) + + response_dict.pop("hashed_password", None) + assert response_dict == { + "username": "alice", + "created_at": mock.ANY, + "full_name": "Alice la Suisse", + "email": "alice@lasuisse.ch", + "id": 1, + "avatar": None, + "status": "active", + } + + +def test__create_user__with_avatar(client): + get_password_hash("secret") + new_avatar = "images/avatar001.jpeg" + response = client.post( + "/users/", + json={ + "username": "alice", + "full_name": "Alice la Suisse", + "email": "alice@lasuisse.ch", + "avatar": new_avatar, + "plain_password": "secret", + }, + ) + + print(response.json()) + + assert response.status_code == 200 + assert response.json()["avatar"] == new_avatar + + +# TODO fix the put method for user +def _test__update_user__full_name(db, client, addFakeUsers): + token, _ = addFakeUsers + + oneUser = db.query(models.User)[1] + + new_full_name = "Alicia la catalana" + + response = client.put( + "/users/update", + headers={ + "Authorization": "Bearer " + token, + "Content-Type": "application/json", + }, + json={ + "username": oneUser.username, + "full_name": new_full_name, + "email": oneUser.email, + "avatar": oneUser.avatar, + }, + ) + + print(response.json()) + assert response.status_code == 200 + assert response.json() == { + "username": "alice", + "full_name": "Alice la Suisse", + "email": "alice@lasuisse.ch", + "avatar": new_full_name, + "plain_password": "secret", + } + + +@pytest.mark.skipif("PIL" not in sys.modules, reason="PIL module is not installed") +def test__upload_user__avatarImage(db, client, addFakeUsers): + token, _ = addFakeUsers + + oneUser = db.query(models.User)[1] + filename = dataTestDir() / "test_avatar.jpeg" + with open(filename, "rb") as f: + img = Image.open(f) + try: + img.verify() + except (IOError, SyntaxError): + print("Bad file:", filename) + + # TODO reset cursor instead of reopening + with open(filename, "rb") as f: + response = client.put( + f"/users/u/{oneUser.id}/avatar", + headers={"Authorization": "Bearer " + token}, + files={"file": f}, + ) + + print(response.json()) + assert response.status_code == 200 + + expected_avatar_dir = Path(__file__).parent.parent / "data" / "avatars" + expected_avatar_filepath = expected_avatar_dir / "1_avatar.jpeg" + expected_avatar_file = Path(expected_avatar_filepath) + + # remove the test file from the config directory + expected_avatar_file.unlink() + + +@pytest.mark.skipif("PIL" not in sys.modules, reason="PIL module is not installed") +def test__upload_user__avatarImage__file_too_big(db, client, addFakeUsers): + token, _ = addFakeUsers + + oneUser = db.query(models.User)[1] + + img = Image.new(mode="RGB", size=(1000, 1000), color="red") + response = client.put( + f"/users/u/{oneUser.id}/avatar", + headers={"Authorization": "Bearer " + token}, + files={"file": img.tobytes()}, + ) + + print(response.json()) + assert response.status_code == 422 + + +def test__getUsers__unauthorized(client): + response = client.get("/users/") + assert response.status_code == 401 + + +def test__authenticate(client): + # add a user + response = client.post( + "/users/", + json={ + "username": "alice", + "full_name": "Alice la Suisse", + "email": "alice@lasuisse.ch", + "plain_password": "secret", + }, + ) + + # test auth + response = client.post( + "/token", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + data={ + "username": "alice", + "password": "secret", + }, + ) + + assert response.status_code == 200 + assert list(response.json().keys()) == ["access_token", "token_type"] + + +def test__createUser__persistence(client): + response = client.post( + "/users/", + json={ + "username": "alice", + "full_name": "Alice la Suisse", + "email": "alice@lasuisse.ch", + "plain_password": "secret", + }, + ) + + response = client.post( + "/token", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + data={ + "username": "alice", + "password": "secret", + }, + ) + + assert response.json() is not None + print(response.json()) + assert "access_token" in response.json() + token = response.json()["access_token"] + + response = client.get( + "/users/usernames", + headers={"Authorization": "Bearer " + token}, + ) + + assert response.status_code == 200 + assert response.json() == ["alice"] + + +def test__addFakeUsers(db, addFakeUsers): + pass + + +def test__getUsernames(db, client, addFakeUsers): + token, _ = addFakeUsers + + db.query(models.User).all() + + response = client.get( + "/users/usernames", + headers={"Authorization": "Bearer " + token}, + ) + + assert response.status_code == 200 + assert response.json() == ["johndoe", "janedoe"] + + +def test__getUserById(db, client, addFakeUsers): + token, _ = addFakeUsers + + response = client.get( + "/users/u/1", + headers={"Authorization": "Bearer " + token}, + ) + + assert response.status_code == 200 + assert response.json() == { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "avatar": None, + "id": 1, + "status": "active", + } + + +def test__getUserById__malformedId(db, client, addFakeUsers): + token, _ = addFakeUsers + + response = client.get( + "/users/u/abcd", + headers={"Authorization": "Bearer " + token}, + ) + + assert response.status_code == 422 + print(response.json()) + assert response.json()["detail"][0]["type"] == "int_parsing" + + +def test__db_cleanup(db): + users = db.query(models.User).all() + + assert users == [] + + +def test__createGame(db, client, addFakeUsers): + token, _ = addFakeUsers + + response = client.post( + "/games/", + headers={ + "Authorization": "Bearer " + token, + "Content-Type": "application/json", + }, + json={"public": False, "color": "white"}, + ) + + print(response.json()) + assert response.status_code == 200 + + assert response.json() == { + "black_id": None, + "created_at": mock.ANY, + "uuid": mock.ANY, + "id": 1, + "last_move_time": None, + "owner_id": 1, + "public": False, + "status": GameStatus.WAITING, + "turn": "white", + "white_id": response.json()["owner_id"], + "winner": None, + } + + +def test__get_game_by_uuid(db, client, addFakeUsers, addFakeGames, johndoe): + token, _ = addFakeUsers + uuid = addFakeGames + + response = client.get( + f"/games/{uuid}", + headers={ + "Authorization": "Bearer " + token, + "Content-Type": "application/json", + }, + params={ + "random": False, + }, + ) + + print(response.json()) + assert response.status_code == 200 + assert response.json() == { + "black": None, + "created_at": mock.ANY, + "uuid": mock.ANY, + "id": 5, + "owner": johndoe, + "last_move_time": None, + "public": False, + "status": GameStatus.WAITING, + "turn": "white", + "white": johndoe, + "winner": None, + } + + +def test__get_me_games(db, client, addFakeUsers, addFakeGames, johndoe, janedoe): + token, _ = addFakeUsers + + response = client.get( + "/users/me/games/", + headers={ + "Authorization": "Bearer " + token, + "Content-Type": "application/json", + }, + ) + + print(response.json()) + assert response.status_code == 200 + assert len(response.json()) == 3 + assert response.json()[0] == { + "black": janedoe, + "created_at": mock.ANY, + "uuid": mock.ANY, + "id": 1, + "last_move_time": None, + "owner": johndoe, + "public": False, + "status": "started", + "turn": "black", + "white": johndoe, + "winner": None, + } + + +def test__getGames__finishedGame( + db, client, addFakeUsers, addFakeGames, addFakeGameSnaps, johndoe, janedoe +): + # change to second player + jane_token = getToken("janedoe") + _ = getToken("johndoe") + + response = client.get( + "/users/me/games", + headers={ + "Authorization": "Bearer " + jane_token, + "Content-Type": "application/json", + }, + ) + print(response.json()) + assert response.status_code == 200 + games = response.json() + assert len(games) == 4 + finishedgame = [g for g in games if g["status"] == GameStatus.OVER][0] + assert finishedgame["turn"] is None + + +# TODO test list random games before setting my player +def test__joinRandomGame(db, client, addFakeUsers, addFakeGames): + token, _ = addFakeUsers + + oneUser = db.query(models.User)[1] + + response = client.patch( + "/games", + headers={ + "Authorization": "Bearer " + token, + "Content-Type": "application/json", + }, + ) + + # TODO join game (client chooses one) + game_dict = response.json() + print(game_dict) + assert response.status_code == 200 + assert game_dict != {} + assert ( + game_dict["white"]["username"] == oneUser.username + or game_dict["black"]["username"] == oneUser.username + ) + assert game_dict == { + "black": mock.ANY, + "created_at": mock.ANY, + "uuid": mock.ANY, + "last_move_time": None, + "id": mock.ANY, + "owner": mock.ANY, + "public": True, + "status": "started", + "turn": "white", + "white": mock.ANY, + "winner": None, + } + + +# TODO deprecated, client chooses game and joins a random one +def test__joinRandomGame__noneAvailable(db, client, addFakeUsers, addFakeDoneGames): + token, _ = addFakeUsers + + response = client.patch( + "/games", + headers={ + "Authorization": "Bearer " + token, + "Content-Type": "application/json", + }, + ) + + print(response.json()) + assert response.status_code == 404 + assert response.json() == {"detail": "available random game not found"} + + +def test__get_available_games__all(db, client, addFakeUsers, addFakeGames): + token, _ = addFakeUsers + + response = client.get( + "/games", + headers={ + "Authorization": "Bearer " + token, + "Content-Type": "application/json", + }, + ) + + print(response.json()) + assert response.status_code == 200 + game_ids = [game["id"] for game in response.json()] + assert game_ids == [1, 2, 3, 4, 5] + + +def test__get_available_games__waiting(db, client, addFakeUsers, addFakeGames): + token, _ = addFakeUsers + + response = client.get( + "/games", + headers={ + "Authorization": "Bearer " + token, + "Content-Type": "application/json", + }, + params={"status": ["waiting"]}, + ) + + print(response.json()) + assert response.status_code == 200 + game_ids = [(game["id"], game["status"]) for game in response.json()] + assert game_ids == [ + (3, GameStatus.WAITING), + (4, GameStatus.WAITING), + (5, GameStatus.WAITING), + ] + + +# TODO this test was deactivated by mistake +def _test__joinGame__playerAlreadyInGame(db, client, addFakeUsers, addFakeGames): + token, username = addFakeUsers + + uuid = "123fr12339" + + user = crud.get_user_by_username(db, username) + + game_before = db.query(models.Game).filter(models.Game.uuid == uuid).first() + + response = client.get( + f"/games/{uuid}/join", + headers={ + "Authorization": "Bearer " + token, + "Content-Type": "application/json", + }, + ) + + game = db.query(models.Game).filter(models.Game.uuid == uuid).first() + + print(response.json()) + assert response.status_code == 200 + if not game_before.black_id: + assert game.black_id == user.id + if not game_before.white_id: + assert game.white_id == user.id + assert response.json() == { + "black_id": game.black_id, + "created_at": mock.ANY, + "uuid": game.uuid, + "id": game.id, + "last_move_time": None, + "owner_id": game.owner_id, + "public": False, + "status": GameStatus.WAITING, + "turn": "white", + "white_id": game.white_id, + } + + +def test__joinGame__playerAlreadyInGame__simple(db, client, addFakeUsers, addFakeGames): + token, username = addFakeUsers + uuid = addFakeGames + + crud.get_user_by_username(db, username) + + db.query(models.Game).filter(models.Game.uuid == uuid).first() + + response = client.get( + f"/games/{uuid}/join", + headers={ + "Authorization": "Bearer " + token, + "Content-Type": "application/json", + }, + ) + + assert response.status_code == 200 + + +def test__getsnap__byNum(db, client, classicSetup, fakegamesdb, addFakeGameSnaps): + firstgame_uuid, token = classicSetup + firstgame_white_player = list(fakegamesdb.values())[0]["white"] + token = getToken(firstgame_white_player) + + response = client.get( + f"/games/{firstgame_uuid}/snap/0", + headers={ + "Authorization": "Bearer " + token, + "Content-Type": "application/json", + }, + ) + + print(response.json()) + assert response.status_code == 200 + # yapf: disable + assert response.json() == { + 'game_id': 1, + 'created_at': mock.ANY, + 'id': 1, + 'move': None, + 'taken': '', + 'castleable': 'lks', + 'move_number': 0, + 'board': ('xxxxxxxx' + 'xxxxxxxx' + '________' + '________' + '________' + '________' + 'pppppppp' + 'rnbqkbnr'), + } + # yapf: enable + + +def test__getsnaps(db, client, classicSetup): + firstgame_uuid, token = classicSetup + + response = client.get( + f"/games/{firstgame_uuid}/snaps", + headers={ + "Authorization": "Bearer " + token, + "Content-Type": "application/json", + }, + ) + + print(response.json()) + assert response.status_code == 200 + # yapf: disable + assert response.json() == [{ + 'game_id': 1, + 'created_at': mock.ANY, + 'id': 1, + 'move': None, + 'taken': '', + 'castleable': 'lks', + 'move_number': 0, + 'board': ('xxxxxxxx' + 'xxxxxxxx' + '________' + '________' + '________' + '________' + 'pppppppp' + 'rnbqkbnr'), + }, { + 'game_id': 1, + 'created_at': mock.ANY, + 'id': 2, + 'move': 'd2d4', + 'taken': '', + 'castleable': 'lks', + 'move_number': 1, + 'board': ('xxxxxxxx' + 'xxxxxxxx' + '________' + '________' + '___p____' + '________' + 'ppp_pppp' + 'rnbqkbnr'), + }] + # yapf: enable + + +def test__getsnap__latest(db, client, classicSetup): + firstgame_uuid, token = classicSetup + + response = client.get( + f"/games/{firstgame_uuid}/snap", + headers={ + "Authorization": "Bearer " + token, + "Content-Type": "application/json", + }, + ) + + print(response.json()) + assert response.status_code == 200 + # yapf: disable + assert response.json() == { + 'game_id': 1, + 'created_at': mock.ANY, + 'id': 2, + 'move': 'd2d4', + 'taken': '', + 'castleable': 'lks', + 'move_number': 1, + 'board': ('xxxxxxxx' + 'xxxxxxxx' + '________' + '________' + '___p____' + '________' + 'ppp_pppp' + 'rnbqkbnr') + } + # yapf: enable + + +def test__getTurn(db, client, classicSetup): + firstgame_uuid, token = classicSetup + + response = client.get( + f"/games/{firstgame_uuid}/turn", + headers={ + "Authorization": "Bearer " + token, + "Content-Type": "application/json", + }, + ) + + print(response.json()) + assert response.status_code == 200 + assert response.json() == "black" + + +@pytest.mark.skip(reason="slow test") +def test__getTurn__long_polling(client, classicSetup): + firstgame_uuid, _ = classicSetup + token = getToken("janedoe") + + start = time.time() + _ = client.get( + f"/games/{firstgame_uuid}/turn", + headers={ + "Authorization": "Bearer " + token, + "Content-Type": "application/json", + }, + params={"long_polling": True}, + ) + + elapsed = time.time() - start + assert elapsed > 5 + + +def test__move(db, client, classicSetup): + firstgame_uuid, token = classicSetup + jane_token = getToken("janedoe") + # get previous game/board + + game_before = ( + db.query(models.Game).filter(models.Game.uuid == firstgame_uuid).first() + ) + + response = client.post( + f"/games/{firstgame_uuid}/move", + headers={ + "Authorization": "Bearer " + jane_token, + "Content-Type": "application/json", + }, + json={ + "move": "d7d5", + }, + ) + + # get after game/board + + game_after = ( + db.query(models.Game).filter(models.Game.uuid == firstgame_uuid).first() + ) + + # test board is the expected one + print([snap.__dict__ for snap in game_after.snaps]) + + print(response.json()) + assert response.status_code == 200 + # assert DictEqual(response.json(), '') + + assert game_before.get_latest_snap().board == ( + "RNBQKBNR" + "PPP_PPPP" + "________" + "___P____" + "___p____" + "________" + "ppp_pppp" + "rnbqkbnr" + ) + + +def test__move__wrong_turn(db, client, classicSetup): + firstgame_uuid, john_token = classicSetup + + response = client.post( + f"/games/{firstgame_uuid}/move", + headers={ + "Authorization": "Bearer " + john_token, + "Content-Type": "application/json", + }, + json={ + "move": "d7d5", + }, + ) + + assert response.status_code == 403 + + +def test__move__filtered(db, client, classicSetup): + firstgame_uuid, _ = classicSetup + # change to second player + token = getToken("janedoe") + + response = client.post( + f"/games/{firstgame_uuid}/move", + headers={ + "Authorization": "Bearer " + token, + "Content-Type": "application/json", + }, + json={ + "move": "d7d5", + }, + ) + + print(response.json()) + assert response.status_code == 200 + + assert response.json()["board"] == ( + "RNBQKBNR" + "PPP_PPPP" + "________" + "___P____" + "___p____" + "________" + "XXX_XXXX" + "XXXXXXXX" + ) + + +def test__possibleMoves__pawnMove(db, client, classicSetup): + firstgame_uuid, _ = classicSetup + # change to second player + token = getToken("janedoe") + + square = "d7" + + response = client.get( + f"/games/{firstgame_uuid}/moves/{square}", + headers={ + "Authorization": "Bearer " + token, + "Content-Type": "application/json", + }, + ) + + print("response: {}".format(response.json())) + assert response.status_code == 200 + + assert response.json() == ["d6", "d5"] + + +@pytest.mark.parametrize( + "addCustomGameSnap", + [ + ( + ( + "____K___" + "________" + "________" + "__p_____" + "________" + "____pk__" + "___P_pp_" + "________" + ), + "g3f3", + ) + ], + indirect=True, +) +def test__possibleMoves__king(db, client, classicSetup, addCustomGameSnap): + firstgame_uuid, token = classicSetup + + square = "f3" + + response = client.get( + f"/games/{firstgame_uuid}/moves/{square}", + headers={ + "Authorization": "Bearer " + token, + "Content-Type": "application/json", + }, + ) + + print("response: {}".format(response.json())) + assert response.status_code == 200 + + assert response.json() == ["e4", "f4", "g4", "g3", "e2"] + + +@pytest.mark.parametrize( + "addCustomGameSnap", + [ + ( + ( + "____K___" + "________" + "________" + "________" + "__pP____" + "____pk__" + "_____pp_" + "________" + ), + "c2c4", + ) + ], + indirect=True, +) +def test__possibleMoves__pawn_enpassant_black( + db, client, classicSetup, addCustomGameSnap +): + firstgame_uuid, _ = classicSetup + + token = getToken("janedoe") + + square = "d4" + + response = client.get( + f"/games/{firstgame_uuid}/moves/{square}", + headers={ + "Authorization": "Bearer " + token, + "Content-Type": "application/json", + }, + ) + + print("response: {}".format(response.json())) + assert response.status_code == 200 + + assert response.json() == ["c3", "d3", "e3"] + + +@pytest.mark.parametrize( + "addCustomGameSnap", + [ + ( + ( + "____K___" + "________" + "________" + "__pP____" + "________" + "____pk__" + "_____pp_" + "________" + ), + "d7d5", + ) + ], + indirect=True, +) +def test__possibleMoves__pawn_enpassant_white( + db, client, classicSetup, addCustomGameSnap +): + firstgame_uuid, token = classicSetup + + square = "c5" + + response = client.get( + f"/games/{firstgame_uuid}/moves/{square}", + headers={ + "Authorization": "Bearer " + token, + "Content-Type": "application/json", + }, + ) + + print("response: {}".format(response.json())) + assert response.status_code == 200 + + assert response.json() == ["c6", "d6"] + + +@pytest.mark.parametrize( + "addCustomGameSnap", + [ + ( + ( + "____K___" + "________" + "________" + "__pP____" + "________" + "____pk__" + "_____pp_" + "________" + ), + "c7c5", + ) + ], + indirect=True, +) +def test__possibleMoves__pawn_impossible_enpassant_black( + db, client, classicSetup, addCustomGameSnap +): + firstgame_uuid, _ = classicSetup + + token = getToken("janedoe") + + square = "d5" + + response = client.get( + f"/games/{firstgame_uuid}/moves/{square}", + headers={ + "Authorization": "Bearer " + token, + "Content-Type": "application/json", + }, + ) + + print("response: {}".format(response.json())) + assert response.status_code == 200 + + assert response.json() == ["d4"] + + +@pytest.mark.parametrize( + "addCustomGameSnap", + [ + ( + ( + "____K___" + "_____PP_" + "_____p__" + "________" + "________" + "_____k__" + "_____pp_" + "________" + ), + "f5f6", + ) + ], + indirect=True, +) +def test__possibleMoves__pawn_take(db, client, classicSetup, addCustomGameSnap): + firstgame_uuid, token = classicSetup + + square = "f6" + + response = client.get( + f"/games/{firstgame_uuid}/moves/{square}", + headers={ + "Authorization": "Bearer " + token, + "Content-Type": "application/json", + }, + ) + + print("response: {}".format(response.json())) + assert response.status_code == 200 + + assert response.json() == ["g7"] + + +def send_move(client, game_uuid, move, token): + response = client.post( + f"/games/{game_uuid}/move", + headers={ + "Authorization": "Bearer " + token, + "Content-Type": "application/json", + }, + json={ + "move": move, + }, + ) + return response + + +def prettyBoard(boardStr): + print(" abcdefgh") + print(" 01234567") + for i in range(8): + print("{} - {} - {}".format(i, boardStr[8 * i : 8 * i + 8], 8 - i)) + + +def test__move__filtered_pawn(db, client, game_setup, addFakeGameSnaps): + firstgame_uuid, john_token, jane_token = game_setup + + tokens = [jane_token, john_token] + moves = ["e7e5", "d4e5", "h7h6", "e5e6"] + + response = send_move(client, firstgame_uuid, moves[0], tokens[0 % 2]) + print(response.json()) + prettyBoard(response.json()["board"]) + assert response.status_code == 200 + + assert response.json()["board"] == ( + "RNBQKBNR" + "PPPP_PPP" + "________" + "____P___" + "___p____" + "________" + "XXX_XXXX" + "XXXXXXXX" + ) + + response = send_move(client, firstgame_uuid, moves[1], tokens[1 % 2]) + print(response.json()) + prettyBoard(response.json()["board"]) + assert response.status_code == 200 + + response = send_move(client, firstgame_uuid, moves[2], tokens[2 % 2]) + print(response.json()) + prettyBoard(response.json()["board"]) + assert response.status_code == 200 + + assert response.json()["board"] == ( + "RNBQKBNR" + "PPPP_PP_" + "_______P" + "____X___" + "________" + "________" + "XXX_XXXX" + "XXXXXXXX" + ) + + +# use this method as reference to reproduce any game moves +# TODO use a virgin game instead of the firstgame_uuid +def test__move__fogTest(db, client, game_setup, addFakeGameSnaps): + firstgame_uuid, john_token, jane_token = game_setup + + tokens = [jane_token, john_token] + moves = ["e7e6", "g2g4", "d8h4", "f2f4", "a7a6"] + + print(tokens) + + for i, move in enumerate(moves): + print("move {} for {}".format(move, tokens[i % 2])) + response = send_move(client, firstgame_uuid, move, tokens[i % 2]) + print(response.json()) + assert response.status_code == 200 + prettyBoard(response.json()["board"]) + + assert response.json()["board"] == ( + "RNB_KBNR" + "_PPP_PPP" + "P___P___" + "________" + "___X_XpQ" + "________" + "XXX_X__X" + "XXXXXXXX" + ) + + +def test__integrationTest__foolscheckmate(client): + # create johndoe + # create janedoe + + response = client.post( + "/users/", + json={ + "username": "johndoe", + "full_name": "John Le Dow", + "email": "john@doe.cat", + "plain_password": "secret", + }, + ) + + john_username = response.json()["username"] + + assert response.status_code == 200 + + response = client.post( + "/users/", + json={ + "username": "janedoe", + "full_name": "Jane Le Dow", + "email": "jane@doe.cat", + "plain_password": "secret", + }, + ) + + jane_username = response.json()["username"] + + assert response.status_code == 200 + + # authenticate + response = client.post( + "/token", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + data={ + "username": "johndoe", + "password": "secret", + }, + ) + + assert response.status_code == 200 + john_token = response.json()["access_token"] + + response = client.post( + "/token", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + data={ + "username": "janedoe", + "password": "secret", + }, + ) + + assert response.status_code == 200 + jane_token = response.json()["access_token"] + + # john create game + + response = client.post( + "/games/", + headers={ + "Authorization": "Bearer " + john_token, + "Content-Type": "application/json", + }, + json={ + "public": False, + "color": "white", + }, + ) + + assert response.status_code == 200 + + game_uuid = response.json()["uuid"] + + # john already joined and jane joins game + + # check if game started + response = client.get( + f"/games/{game_uuid}", + headers={ + "Authorization": "Bearer " + jane_token, + "Content-Type": "application/json", + }, + ) + + assert response.status_code == 200 + assert response.json()["status"] == GameStatus.WAITING + + white = response.json()["white"] + response.json()["black"] + jane_color = ( + None + if not white + else "white" + if white["username"] == jane_username + else "black" + ) + john_color = ( + None + if not white + else "white" + if white["username"] == john_username + else "black" + ) + + print(f"jane color is {jane_color}") + + response = client.get( + f"/games/{game_uuid}/join", + headers={ + "Authorization": "Bearer " + jane_token, + "Content-Type": "application/json", + }, + ) + + assert response.status_code == 200 + + print(response.json()) + + # john send move + # jane send move + + moves = ["f2f3", "e7e5", "g2g4", "d8h4", "f3f4", "h4e1"] + + boards = [ + ( + "xxxxxxxxxxxxxxxx_____________________________p__ppppp_pprnbqkbnr", + "RNBQKBNRPPPPPPPP_____________________________X__XXXXX_XXXXXXXXXX", + ), + ( + "xxxxxxxxxxxx_xxx____________x________________p__ppppp_pprnbqkbnr", + "RNBQKBNRPPPP_PPP____________P________________X__XXXXX_XXXXXXXXXX", + ), + ( + "xxxxxxxxxxxx_xxx____________x_________p______p__ppppp__prnbqkbnr", + "RNBQKBNRPPPP_PPP____________P_________X______X__XXXXX__XXXXXXXXX", + ), + ( + "xxx_xxxxxxxx_xxx____________x_________pQ_____p__ppppp__prnbqkbnr", + "RNB_KBNRPPPP_PPP____________P_________pQ_____X__XXXXX__XXXXXXXXX", + ), + ( + "xxx_xxxxxxxx_xxx____________P________ppQ________ppppp__prnbqkbnr", + "RNB_KBNRPPPP_PPP____________P________ppQ________XXXXX__XXXXXXXXX", + ), + ( + "xxx_xxxxxxxx_xxx____________P________ppQ_____p__ppppp__prnbqkbnr", + "RNB_KBNRPPPP_PPP____________P________pX______X__pppXX__XXXXqQbXX", + ), + ] + + tokens = [john_token, jane_token] + + for i, move in enumerate(moves): + response = send_move(client, game_uuid, move, tokens[i % 2]) + print( + f"ran move {move} by {jane_color if jane_token == tokens[i%2] else john_color}" + ) + assert response.status_code == 200 + + # they ask for game and turn + + response = client.get( + f"/games/{game_uuid}/turn", + headers={ + "Authorization": "Bearer " + jane_token, + "Content-Type": "application/json", + }, + ) + + assert response.status_code == 200 or response.status_code == 412 + + jane_turn = response.json() + + response = client.get( + f"/games/{game_uuid}/turn", + headers={ + "Authorization": "Bearer " + john_token, + "Content-Type": "application/json", + }, + ) + + assert response.status_code == 200 or response.status_code == 412 + + game_finished = not (response.status_code == 200) + + john_turn = response.json() + + assert jane_turn == john_turn + + response = client.get( + f"/games/{game_uuid}/snap", + headers={ + "Authorization": "Bearer " + jane_token, + "Content-Type": "application/json", + }, + ) + + print(prettyBoard(response.json()["board"])) + + # no winner + if not game_finished: + # TODO note that this assert will fail if an enemy piece is seen + if jane_color == "white": + assert response.json()["board"] == boards[i][0] + else: + assert response.json()["board"] == boards[i][1] + + response = client.get( + f"/games/{game_uuid}/snap", + headers={ + "Authorization": "Bearer " + john_token, + "Content-Type": "application/json", + }, + ) + + if not game_finished: + if john_color == "white": + assert response.json()["board"] == boards[i][0] + else: + assert response.json()["board"] == boards[i][1] + + # checkmate + + response = client.get( + f"/games/{game_uuid}", + headers={ + "Authorization": "Bearer " + jane_token, + "Content-Type": "application/json", + }, + ) + + print( + "{} with {} won the game".format( + "janedoe" if jane_color == response.json()["winner"] else "johndoe", + jane_color, + ) + ) + + assert response.json()["winner"] == "black" diff --git a/tests/server/test_btchApi_async.py b/tests/server/test_btchApi_async.py new file mode 100644 index 0000000..a4c95c3 --- /dev/null +++ b/tests/server/test_btchApi_async.py @@ -0,0 +1,122 @@ +import asyncio +import time +from datetime import timedelta + +import pytest + +from battlechess.server import crud + + +@pytest.fixture(scope="module") +def event_loop(): + loop = asyncio.get_event_loop_policy().new_event_loop() + yield loop + loop.close() + + +@pytest.fixture +def anyio_backend(): + return "asyncio" + + +def getToken(username): + return crud.create_access_token( + data={"sub": username}, expires_delta=timedelta(minutes=3000) + ) + + +@pytest.mark.skip(reason="slow test") +@pytest.mark.anyio +async def test__getTurn__long_polling_async(asyncclient, classicSetup): + firstgame_uuid, john_token = classicSetup + + start = time.time() + + response = await asyncclient.get( + f"/games/{firstgame_uuid}/turn", + headers={ + "Authorization": "Bearer " + john_token, + "Content-Type": "application/json", + }, + params={"long_polling": True}, + ) + assert response.status_code == 200 + + elapsed = time.time() - start + assert elapsed > 5 + + +@pytest.mark.anyio +async def test__getTurn__long_polling_move(asyncclient, classicSetup): + firstgame_uuid, john_token = classicSetup + jane_token = getToken("janedoe") + + background_tasks = set() + + hard_turn_timeout = 10 + + start = time.time() + + # Check that's it's not john'd turn + long_polling = False + response = await asyncclient.get( + f"/games/{firstgame_uuid}/turn/me", + headers={ + "Authorization": "Bearer " + john_token, + "Content-Type": "application/json", + }, + params={"long_polling": long_polling}, + ) + short_polling_elapsed = time.time() - start + assert response.status_code == 200 + assert response.json() is False + # ensure that non-long-polling get is non blocking + assert short_polling_elapsed < 1 + + start = time.time() + # ask turn again but with long polling + long_polling = True + premature_turn_ask_task = asyncio.create_task( + asyncclient.get( + f"/games/{firstgame_uuid}/turn/me", + headers={ + "Authorization": "Bearer " + john_token, + "Content-Type": "application/json", + }, + params={"long_polling": long_polling}, + ) + ) + + create_task_elapsed = time.time() - start + # ensure that create_task get is non blocking + assert create_task_elapsed < 1 + + background_tasks.add(premature_turn_ask_task) + premature_turn_ask_task.add_done_callback(background_tasks.discard) + await asyncio.sleep(1) # only blocks current task + # time.sleep(1) # blocks everything + if premature_turn_ask_task.done(): + print("turn returned too early!", premature_turn_ask_task.result().json()) + assert not premature_turn_ask_task.done() + + # Jane (black) moves + move_response = await asyncclient.post( + f"/games/{firstgame_uuid}/move", + headers={ + "Authorization": "Bearer " + jane_token, + "Content-Type": "application/json", + }, + json={ + "move": "d7d5", + }, + ) + + assert move_response.status_code == 200 + # gather turn and assert it didn't timeout + await premature_turn_ask_task + elapsed = time.time() - start + assert elapsed < hard_turn_timeout / 2 + response = premature_turn_ask_task.result() + print(f"task result {response.json()}") + assert response.json() is True + assert response.status_code == 200 diff --git a/tests/server/test_btchApi_autoplay.py b/tests/server/test_btchApi_autoplay.py new file mode 100644 index 0000000..5e36ca9 --- /dev/null +++ b/tests/server/test_btchApi_autoplay.py @@ -0,0 +1,105 @@ +import json + +import pytest + +from battlechess.server import models + + +def prettyBoard(boardStr): + print(" abcdefgh") + print(" 01234567") + for i in range(8): + print("{} - {} - {}".format(i, boardStr[8 * i : 8 * i + 8], 8 - i)) + + +def send_move(client, game_uuid, move, token): + response = client.post( + f"/games/{game_uuid}/move", + headers={ + "Authorization": "Bearer " + token, + "Content-Type": "application/json", + }, + json={ + "move": move, + }, + ) + return response + + +def resetGame(db, uuid): + game = db.query(models.Game).filter(models.Game.uuid == uuid).first() + game.reset() + db.commit() + + +def test__move__MrExonGame__OneGame__EnPassant(client, game_setup): + + firstgame_uuid, john_token, jane_token = game_setup + + tokens = [jane_token, john_token] + + # read games + # for game in games: + # for moves in game['moves'] + with open("ia/algebraic2icu/icu/mrexongames.txt") as json_file: + data = json.load(json_file) + moves = data["https://lichess.org/auXLYNj1"] + for i, move in enumerate(moves): + response = send_move(client, firstgame_uuid, move, tokens[i % 2]) + + if response.status_code == 200: + prettyBoard(response.json()["board"]) + else: + print(response.json()) + response.status_code == 200 + + +def test__move__MrExonGame__Enpassant2(db, client, game_setup): + + firstgame_uuid, john_token, jane_token = game_setup + + tokens = [john_token, jane_token] + + resetGame(db, firstgame_uuid) + + # read games + # for game in games: + # for moves in game['moves'] + with open("ia/algebraic2icu/icu/mrexongames.txt") as json_file: + data = json.load(json_file) + moves = data["https://lichess.org/X1Nk72xr"] + for i, move in enumerate(moves): + response = send_move(client, firstgame_uuid, move, tokens[i % 2]) + + if response.status_code == 200: + prettyBoard(response.json()["board"]) + else: + print(response.json()) + assert response.status_code == 200 + + +@pytest.mark.skip(reason="Extremely slow test. Run with -s option.") +def test__move__MrExonGames(db, client, game_setup): + firstgame_uuid, john_token, jane_token = game_setup + + tokens = [john_token, jane_token] + + resetGame(db, firstgame_uuid) + + # read games + # for game in games: + # for moves in game['moves'] + with open("ia/algebraic2icu/icu/mrexongames.txt") as json_file: + data = json.load(json_file) + for game, moves in data.items(): + print("Game: {} moves {}".format(game, moves)) + for i, move in enumerate(moves): + response = send_move(client, firstgame_uuid, move, tokens[i % 2]) + + if response.status_code == 200: + prettyBoard(response.json()["board"]) + else: + print(response.json()) + assert response.status_code == 200 + + resetGame(db, firstgame_uuid) diff --git a/server/test_models.py b/tests/server/test_models.py similarity index 68% rename from server/test_models.py rename to tests/server/test_models.py index 59e7b32..9592b91 100644 --- a/server/test_models.py +++ b/tests/server/test_models.py @@ -1,19 +1,16 @@ -from server.schemas import GameStatus import unittest +from datetime import datetime, timezone from sqlalchemy import create_engine -from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker -from datetime import datetime, timedelta, timezone +from battlechess.core.Board import Board +from battlechess.server import models +from battlechess.server.btchApiDB import Base +from battlechess.server.schemas import GameStatus -from . import models -from .btchApiDB import SessionLocal, Base, BtchDBContextManager -from .schemas import GameStatus -from core.Board import Board class Test_Models(unittest.TestCase): - @classmethod def setUpClass(cls): SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db" @@ -22,8 +19,9 @@ def setUpClass(cls): SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} ) - cls.TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=cls.engine) - + cls.TestingSessionLocal = sessionmaker( + autocommit=False, autoflush=False, bind=cls.engine + ) def setUp(self): # TODO setUp correctly with begin-rollback instead of create-drop @@ -59,7 +57,7 @@ def fakeusersdb(self): "status": "active", "avatar": None, "created_at": datetime(2021, 1, 1, tzinfo=timezone.utc), - } + }, } return fake_users_db @@ -67,14 +65,16 @@ def fakesnap(self): snap = { "game_uuid": "lkml4a3.d3", "move": "", - "board": ('RNBQKBNR' - 'PPPPPPPP' - '________' - '________' - '________' - '________' - 'pppppppp' - 'rnbqkbnr'), + "board": ( + "RNBQKBNR" + "PPPPPPPP" + "________" + "________" + "________" + "________" + "pppppppp" + "rnbqkbnr" + ), "taken": "", "castleable": sorted("LSKlsk"), "move_number": 0, @@ -104,7 +104,7 @@ def addFakeUsers(self, db): def test__toBoard(self): - self.maxDiff=None + self.maxDiff = None snap = self.fakesnap() db_snap = models.GameSnap( created_at=snap["created_at"], @@ -118,40 +118,44 @@ def test__toBoard(self): board = db_snap.toBoard() - self.assertEqual(board.toString(), ( - "rb_nb_bb_qb_kb_bb_nb_rb_" - "pb_pb_pb_pb_pb_pb_pb_pb_" - "________" - "________" - "________" - "________" - "pw_pw_pw_pw_pw_pw_pw_pw_" - "rw_nw_bw_qw_kw_bw_nw_rw" - "##kb_kw_rkb_rkw_rqb_rqw#-1#n" - ) + self.assertEqual( + board.toString(), + ( + "rb_nb_bb_qb_kb_bb_nb_rb_" + "pb_pb_pb_pb_pb_pb_pb_pb_" + "________" + "________" + "________" + "________" + "pw_pw_pw_pw_pw_pw_pw_pw_" + "rw_nw_bw_qw_kw_bw_nw_rw" + "##kb_kw_rkb_rkw_rqb_rqw#-1#n" + ), ) def test__Board__move(self): - self.maxDiff=None + self.maxDiff = None board = Board() board.reset() - status, accepted_move_list, msg = board.move(6,4,4,4) + status, accepted_move_list, msg = board.move(6, 4, 4, 4) print(f"new board {status} - {accepted_move_list} - {msg}") print(board.toString()) self.assertTrue(status) - self.assertListEqual(accepted_move_list, [6,4,4,4]) - self.assertEqual(board.toString(), ( - "rb_nb_bb_qb_kb_bb_nb_rb_" - "pb_pb_pb_pb_pb_pb_pb_pb_" - "________" - "________" - "____pw____" - "________" - "pw_pw_pw_pw__pw_pw_pw_" - "rw_nw_bw_qw_kw_bw_nw_rw" - "##kb_kw_rkb_rkw_rqb_rqw#4#n" - ) + self.assertListEqual(accepted_move_list, [6, 4, 4, 4]) + self.assertEqual( + board.toString(), + ( + "rb_nb_bb_qb_kb_bb_nb_rb_" + "pb_pb_pb_pb_pb_pb_pb_pb_" + "________" + "________" + "____pw____" + "________" + "pw_pw_pw_pw__pw_pw_pw_" + "rw_nw_bw_qw_kw_bw_nw_rw" + "##kb_kw_rkb_rkw_rqb_rqw#4#n" + ), ) def test__GameSnap__coordListToMove(self): @@ -167,9 +171,9 @@ def test__GameSnap__coordListToMove(self): move_number=fakesnap["move_number"], ) - move = snap.coordListToMove([6,3,4,3]) - print(f'move {move}') - self.assertEqual(move,"d2d4") + move = snap.coordListToMove([6, 3, 4, 3]) + print(f"move {move}") + self.assertEqual(move, "d2d4") def test__GameSnap__moveToCoordList(self): @@ -186,7 +190,7 @@ def test__GameSnap__moveToCoordList(self): move = snap.moveToCoordList("d2d4") - self.assertEqual(move,[6,3,4,3]) + self.assertEqual(move, [6, 3, 4, 3]) # deprecated: default snap is created via api set_player def _test__Game__startGameCreatesSnapIfEmpty(self): @@ -195,15 +199,15 @@ def _test__Game__startGameCreatesSnapIfEmpty(self): janedoe = users[1] game = self.fakegame() db_game = models.Game( - created_at=game["created_at"], - uuid=game["uuid"], - owner_id=johndoe.id, - white_id=johndoe.id, - black_id=janedoe.id, - status=game["status"], - last_move_time=None, - turn=game.get("turn", "white"), - public=game["public"] + created_at=game["created_at"], + uuid=game["uuid"], + owner_id=johndoe.id, + white_id=johndoe.id, + black_id=janedoe.id, + status=game["status"], + last_move_time=None, + turn=game.get("turn", "white"), + public=game["public"], ) self.db.add(db_game) self.db.commit() @@ -216,7 +220,7 @@ def _test__Game__startGameCreatesSnapIfEmpty(self): def test__User__setAvatar(self): users = self.addFakeUsers(self.db) - avatar = self.fakeusersdb()['johndoe']['avatar'] + avatar = self.fakeusersdb()["johndoe"]["avatar"] johndoe = users[0] self.assertEqual(johndoe.avatar, avatar)