From 2d567cd1cd6002a708698c2300164794f3ca86a0 Mon Sep 17 00:00:00 2001 From: quimnuss Date: Fri, 24 Nov 2023 21:00:12 +0100 Subject: [PATCH 01/43] modernize battlechess --- .pre-commit-config.yaml | 29 + poetry.lock | 1585 +++++++++++++++++++++++++++++++++++++++ pyproject.toml | 36 + 3 files changed, 1650 insertions(+) create mode 100644 .pre-commit-config.yaml create mode 100644 poetry.lock create mode 100644 pyproject.toml 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/poetry.lock b/poetry.lock new file mode 100644 index 0000000..e9f3826 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1585 @@ +# 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.3" +description = "Disable App Nap on macOS >= 10.9" +optional = false +python-versions = "*" +files = [ + {file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"}, + {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"}, +] + +[[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.0.1" +description = "Modern password hashing for your software and your servers" +optional = false +python-versions = ">=3.6" +files = [ + {file = "bcrypt-4.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0eaa47d4661c326bfc9d08d16debbc4edf78778e6aaba29c1bc7ce67214d4410"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae88eca3024bb34bb3430f964beab71226e761f51b912de5133470b649d82344"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:a522427293d77e1c29e303fc282e2d71864579527a04ddcfda6d4f8396c6c36a"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:fbdaec13c5105f0c4e5c52614d04f0bca5f5af007910daa8b6b12095edaa67b3"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ca3204d00d3cb2dfed07f2d74a25f12fc12f73e606fcaa6975d1f7ae69cacbb2"}, + {file = "bcrypt-4.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:089098effa1bc35dc055366740a067a2fc76987e8ec75349eb9484061c54f535"}, + {file = "bcrypt-4.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e9a51bbfe7e9802b5f3508687758b564069ba937748ad7b9e890086290d2f79e"}, + {file = "bcrypt-4.0.1-cp36-abi3-win32.whl", hash = "sha256:2caffdae059e06ac23fce178d31b4a702f2a3264c20bfb5ff541b338194d8fab"}, + {file = "bcrypt-4.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:8a68f4341daf7522fe8d73874de8906f3a339048ba406be6ddc1b3ccb16fc0d9"}, + {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf4fa8b2ca74381bb5442c089350f09a3f17797829d958fad058d6e44d9eb83c"}, + {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:67a97e1c405b24f19d08890e7ae0c4f7ce1e56a712a016746c8b2d7732d65d4b"}, + {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b3b85202d95dd568efcb35b53936c5e3b3600c7cdcc6115ba461df3a8e89f38d"}, + {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbb03eec97496166b704ed663a53680ab57c5084b2fc98ef23291987b525cb7d"}, + {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:5ad4d32a28b80c5fa6671ccfb43676e8c1cc232887759d1cd7b6f56ea4355215"}, + {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b57adba8a1444faf784394de3436233728a1ecaeb6e07e8c22c8848f179b893c"}, + {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b2cea8a9ed3d55b4491887ceadb0106acf7c6387699fca771af56b1cdeeda"}, + {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:2b3ac11cf45161628f1f3733263e63194f22664bf4d0c0f3ab34099c02134665"}, + {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3100851841186c25f127731b9fa11909ab7b1df6fc4b9f8353f4f1fd952fbf71"}, + {file = "bcrypt-4.0.1.tar.gz", hash = "sha256:27d375903ac8261cfe4047f6709d16f7d18d39b1ec92aaf72af989552a650ebd"}, +] + +[package.extras] +tests = ["pytest (>=3.2.1,!=3.3.0)"] +typecheck = ["mypy"] + +[[package]] +name = "black" +version = "23.11.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-23.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dbea0bb8575c6b6303cc65017b46351dc5953eea5c0a59d7b7e3a2d2f433a911"}, + {file = "black-23.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:412f56bab20ac85927f3a959230331de5614aecda1ede14b373083f62ec24e6f"}, + {file = "black-23.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d136ef5b418c81660ad847efe0e55c58c8208b77a57a28a503a5f345ccf01394"}, + {file = "black-23.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:6c1cac07e64433f646a9a838cdc00c9768b3c362805afc3fce341af0e6a9ae9f"}, + {file = "black-23.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cf57719e581cfd48c4efe28543fea3d139c6b6f1238b3f0102a9c73992cbb479"}, + {file = "black-23.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:698c1e0d5c43354ec5d6f4d914d0d553a9ada56c85415700b81dc90125aac244"}, + {file = "black-23.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:760415ccc20f9e8747084169110ef75d545f3b0932ee21368f63ac0fee86b221"}, + {file = "black-23.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:58e5f4d08a205b11800332920e285bd25e1a75c54953e05502052738fe16b3b5"}, + {file = "black-23.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:45aa1d4675964946e53ab81aeec7a37613c1cb71647b5394779e6efb79d6d187"}, + {file = "black-23.11.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c44b7211a3a0570cc097e81135faa5f261264f4dfaa22bd5ee2875a4e773bd6"}, + {file = "black-23.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a9acad1451632021ee0d146c8765782a0c3846e0e0ea46659d7c4f89d9b212b"}, + {file = "black-23.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:fc7f6a44d52747e65a02558e1d807c82df1d66ffa80a601862040a43ec2e3142"}, + {file = "black-23.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7f622b6822f02bfaf2a5cd31fdb7cd86fcf33dab6ced5185c35f5db98260b055"}, + {file = "black-23.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:250d7e60f323fcfc8ea6c800d5eba12f7967400eb6c2d21ae85ad31c204fb1f4"}, + {file = "black-23.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5133f5507007ba08d8b7b263c7aa0f931af5ba88a29beacc4b2dc23fcefe9c06"}, + {file = "black-23.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:421f3e44aa67138ab1b9bfbc22ee3780b22fa5b291e4db8ab7eee95200726b07"}, + {file = "black-23.11.0-py3-none-any.whl", hash = "sha256:54caaa703227c6e0c87b76326d0862184729a69b73d3b7305b6288e1d830067e"}, + {file = "black-23.11.0.tar.gz", hash = "sha256:4c68855825ff432d197229846f971bc4d6666ce90492e5b02013bcaca4d9ab05"}, +] + +[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)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "certifi" +version = "2023.11.17" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, + {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, +] + +[[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 = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + +[[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 = "41.0.5" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-41.0.5-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:da6a0ff8f1016ccc7477e6339e1d50ce5f59b88905585f77193ebd5068f1e797"}, + {file = "cryptography-41.0.5-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b948e09fe5fb18517d99994184854ebd50b57248736fd4c720ad540560174ec5"}, + {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d38e6031e113b7421db1de0c1b1f7739564a88f1684c6b89234fbf6c11b75147"}, + {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e270c04f4d9b5671ebcc792b3ba5d4488bf7c42c3c241a3748e2599776f29696"}, + {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ec3b055ff8f1dce8e6ef28f626e0972981475173d7973d63f271b29c8a2897da"}, + {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:7d208c21e47940369accfc9e85f0de7693d9a5d843c2509b3846b2db170dfd20"}, + {file = "cryptography-41.0.5-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8254962e6ba1f4d2090c44daf50a547cd5f0bf446dc658a8e5f8156cae0d8548"}, + {file = "cryptography-41.0.5-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:a48e74dad1fb349f3dc1d449ed88e0017d792997a7ad2ec9587ed17405667e6d"}, + {file = "cryptography-41.0.5-cp37-abi3-win32.whl", hash = "sha256:d3977f0e276f6f5bf245c403156673db103283266601405376f075c849a0b936"}, + {file = "cryptography-41.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:73801ac9736741f220e20435f84ecec75ed70eda90f781a148f1bad546963d81"}, + {file = "cryptography-41.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3be3ca726e1572517d2bef99a818378bbcf7d7799d5372a46c79c29eb8d166c1"}, + {file = "cryptography-41.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e886098619d3815e0ad5790c973afeee2c0e6e04b4da90b88e6bd06e2a0b1b72"}, + {file = "cryptography-41.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:573eb7128cbca75f9157dcde974781209463ce56b5804983e11a1c462f0f4e88"}, + {file = "cryptography-41.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0c327cac00f082013c7c9fb6c46b7cc9fa3c288ca702c74773968173bda421bf"}, + {file = "cryptography-41.0.5-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:227ec057cd32a41c6651701abc0328135e472ed450f47c2766f23267b792a88e"}, + {file = "cryptography-41.0.5-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:22892cc830d8b2c89ea60148227631bb96a7da0c1b722f2aac8824b1b7c0b6b8"}, + {file = "cryptography-41.0.5-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5a70187954ba7292c7876734183e810b728b4f3965fbe571421cb2434d279179"}, + {file = "cryptography-41.0.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:88417bff20162f635f24f849ab182b092697922088b477a7abd6664ddd82291d"}, + {file = "cryptography-41.0.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c707f7afd813478e2019ae32a7c49cd932dd60ab2d2a93e796f68236b7e1fbf1"}, + {file = "cryptography-41.0.5-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:580afc7b7216deeb87a098ef0674d6ee34ab55993140838b14c9b83312b37b86"}, + {file = "cryptography-41.0.5-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1e91467c65fe64a82c689dc6cf58151158993b13eb7a7f3f4b7f395636723"}, + {file = "cryptography-41.0.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0d2a6a598847c46e3e321a7aef8af1436f11c27f1254933746304ff014664d84"}, + {file = "cryptography-41.0.5.tar.gz", hash = "sha256:392cb88b597247177172e02da6b7a63deeff1937fa6fec3bbf902ebd75d97ec7"}, +] + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] +nox = ["nox"] +pep8test = ["black", "check-sdist", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["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.7" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, + {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, +] + +[[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 = "greenlet" +version = "3.0.1" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=3.7" +files = [ + {file = "greenlet-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f89e21afe925fcfa655965ca8ea10f24773a1791400989ff32f467badfe4a064"}, + {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28e89e232c7593d33cac35425b58950789962011cc274aa43ef8865f2e11f46d"}, + {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8ba29306c5de7717b5761b9ea74f9c72b9e2b834e24aa984da99cbfc70157fd"}, + {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19bbdf1cce0346ef7341705d71e2ecf6f41a35c311137f29b8a2dc2341374565"}, + {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:599daf06ea59bfedbec564b1692b0166a0045f32b6f0933b0dd4df59a854caf2"}, + {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b641161c302efbb860ae6b081f406839a8b7d5573f20a455539823802c655f63"}, + {file = "greenlet-3.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d57e20ba591727da0c230ab2c3f200ac9d6d333860d85348816e1dca4cc4792e"}, + {file = "greenlet-3.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5805e71e5b570d490938d55552f5a9e10f477c19400c38bf1d5190d760691846"}, + {file = "greenlet-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:52e93b28db27ae7d208748f45d2db8a7b6a380e0d703f099c949d0f0d80b70e9"}, + {file = "greenlet-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f7bfb769f7efa0eefcd039dd19d843a4fbfbac52f1878b1da2ed5793ec9b1a65"}, + {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91e6c7db42638dc45cf2e13c73be16bf83179f7859b07cfc139518941320be96"}, + {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1757936efea16e3f03db20efd0cd50a1c86b06734f9f7338a90c4ba85ec2ad5a"}, + {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19075157a10055759066854a973b3d1325d964d498a805bb68a1f9af4aaef8ec"}, + {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9d21aaa84557d64209af04ff48e0ad5e28c5cca67ce43444e939579d085da72"}, + {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2847e5d7beedb8d614186962c3d774d40d3374d580d2cbdab7f184580a39d234"}, + {file = "greenlet-3.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:97e7ac860d64e2dcba5c5944cfc8fa9ea185cd84061c623536154d5a89237884"}, + {file = "greenlet-3.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b2c02d2ad98116e914d4f3155ffc905fd0c025d901ead3f6ed07385e19122c94"}, + {file = "greenlet-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:22f79120a24aeeae2b4471c711dcf4f8c736a2bb2fabad2a67ac9a55ea72523c"}, + {file = "greenlet-3.0.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:100f78a29707ca1525ea47388cec8a049405147719f47ebf3895e7509c6446aa"}, + {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60d5772e8195f4e9ebf74046a9121bbb90090f6550f81d8956a05387ba139353"}, + {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:daa7197b43c707462f06d2c693ffdbb5991cbb8b80b5b984007de431493a319c"}, + {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea6b8aa9e08eea388c5f7a276fabb1d4b6b9d6e4ceb12cc477c3d352001768a9"}, + {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d11ebbd679e927593978aa44c10fc2092bc454b7d13fdc958d3e9d508aba7d0"}, + {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dbd4c177afb8a8d9ba348d925b0b67246147af806f0b104af4d24f144d461cd5"}, + {file = "greenlet-3.0.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20107edf7c2c3644c67c12205dc60b1bb11d26b2610b276f97d666110d1b511d"}, + {file = "greenlet-3.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8bef097455dea90ffe855286926ae02d8faa335ed8e4067326257cb571fc1445"}, + {file = "greenlet-3.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:b2d3337dcfaa99698aa2377c81c9ca72fcd89c07e7eb62ece3f23a3fe89b2ce4"}, + {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80ac992f25d10aaebe1ee15df45ca0d7571d0f70b645c08ec68733fb7a020206"}, + {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:337322096d92808f76ad26061a8f5fccb22b0809bea39212cd6c406f6a7060d2"}, + {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9934adbd0f6e476f0ecff3c94626529f344f57b38c9a541f87098710b18af0a"}, + {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc4d815b794fd8868c4d67602692c21bf5293a75e4b607bb92a11e821e2b859a"}, + {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41bdeeb552d814bcd7fb52172b304898a35818107cc8778b5101423c9017b3de"}, + {file = "greenlet-3.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6e6061bf1e9565c29002e3c601cf68569c450be7fc3f7336671af7ddb4657166"}, + {file = "greenlet-3.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:fa24255ae3c0ab67e613556375a4341af04a084bd58764731972bcbc8baeba36"}, + {file = "greenlet-3.0.1-cp37-cp37m-win32.whl", hash = "sha256:b489c36d1327868d207002391f662a1d163bdc8daf10ab2e5f6e41b9b96de3b1"}, + {file = "greenlet-3.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f33f3258aae89da191c6ebaa3bc517c6c4cbc9b9f689e5d8452f7aedbb913fa8"}, + {file = "greenlet-3.0.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:d2905ce1df400360463c772b55d8e2518d0e488a87cdea13dd2c71dcb2a1fa16"}, + {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a02d259510b3630f330c86557331a3b0e0c79dac3d166e449a39363beaae174"}, + {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55d62807f1c5a1682075c62436702aaba941daa316e9161e4b6ccebbbf38bda3"}, + {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3fcc780ae8edbb1d050d920ab44790201f027d59fdbd21362340a85c79066a74"}, + {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4eddd98afc726f8aee1948858aed9e6feeb1758889dfd869072d4465973f6bfd"}, + {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eabe7090db68c981fca689299c2d116400b553f4b713266b130cfc9e2aa9c5a9"}, + {file = "greenlet-3.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f2f6d303f3dee132b322a14cd8765287b8f86cdc10d2cb6a6fae234ea488888e"}, + {file = "greenlet-3.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d923ff276f1c1f9680d32832f8d6c040fe9306cbfb5d161b0911e9634be9ef0a"}, + {file = "greenlet-3.0.1-cp38-cp38-win32.whl", hash = "sha256:0b6f9f8ca7093fd4433472fd99b5650f8a26dcd8ba410e14094c1e44cd3ceddd"}, + {file = "greenlet-3.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:990066bff27c4fcf3b69382b86f4c99b3652bab2a7e685d968cd4d0cfc6f67c6"}, + {file = "greenlet-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ce85c43ae54845272f6f9cd8320d034d7a946e9773c693b27d620edec825e376"}, + {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89ee2e967bd7ff85d84a2de09df10e021c9b38c7d91dead95b406ed6350c6997"}, + {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87c8ceb0cf8a5a51b8008b643844b7f4a8264a2c13fcbcd8a8316161725383fe"}, + {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d6a8c9d4f8692917a3dc7eb25a6fb337bff86909febe2f793ec1928cd97bedfc"}, + {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fbc5b8f3dfe24784cee8ce0be3da2d8a79e46a276593db6868382d9c50d97b1"}, + {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85d2b77e7c9382f004b41d9c72c85537fac834fb141b0296942d52bf03fe4a3d"}, + {file = "greenlet-3.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:696d8e7d82398e810f2b3622b24e87906763b6ebfd90e361e88eb85b0e554dc8"}, + {file = "greenlet-3.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:329c5a2e5a0ee942f2992c5e3ff40be03e75f745f48847f118a3cfece7a28546"}, + {file = "greenlet-3.0.1-cp39-cp39-win32.whl", hash = "sha256:cf868e08690cb89360eebc73ba4be7fb461cfbc6168dd88e2fbbe6f31812cd57"}, + {file = "greenlet-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:ac4a39d1abae48184d420aa8e5e63efd1b75c8444dd95daa3e03f6c6310e9619"}, + {file = "greenlet-3.0.1.tar.gz", hash = "sha256:816bd9488a94cba78d93e1abb58000e8266fa9cc2aa9ccdd6eb0696acb24005b"}, +] + +[package.extras] +docs = ["Sphinx"] +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 = "identify" +version = "2.5.32" +description = "File identification library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "identify-2.5.32-py2.py3-none-any.whl", hash = "sha256:0b7656ef6cba81664b783352c73f8c24b39cf82f926f78f4550eda928e5e0545"}, + {file = "identify-2.5.32.tar.gz", hash = "sha256:5d9979348ec1a21c768ae07e0a652924538e8bce67313a73cb0f681cf08ba407"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.5" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.5-py3-none-any.whl", hash = "sha256:79b8f0ac92d2351be5f6122356c9a592c96d81c9a79e4b488bf2a6a15f88057a"}, + {file = "idna-3.5.tar.gz", hash = "sha256:27009fe2735bf8723353582d48575b23c533cc2c2de7b5a68908d91b5eb18d08"}, +] + +[[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.12.0" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, + {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, +] + +[package.extras] +colors = ["colorama (>=0.4.3)"] +pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] +plugins = ["setuptools"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] + +[[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 = "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 = "passlib" +version = "1.7.4" +description = "comprehensive password hashing framework supporting over 30 schemes" +optional = false +python-versions = "*" +files = [ + {file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"}, + {file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"}, +] + +[package.dependencies] +bcrypt = {version = ">=3.1.0", optional = true, markers = "extra == \"bcrypt\""} + +[package.extras] +argon2 = ["argon2-cffi (>=18.2.0)"] +bcrypt = ["bcrypt (>=3.1.0)"] +build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"] +totp = ["cryptography"] + +[[package]] +name = "pathlib" +version = "1.0.1" +description = "Object-oriented filesystem paths" +optional = false +python-versions = "*" +files = [ + {file = "pathlib-1.0.1-py3-none-any.whl", hash = "sha256:f35f95ab8b0f59e6d354090350b44a80a80635d22efdedfa84c7ad1cf0a74147"}, + {file = "pathlib-1.0.1.tar.gz", hash = "sha256:6940718dfc3eff4258203ad5021090933e5c04707d5ca8cc9e73c94a7894ea9f"}, +] + +[[package]] +name = "pathspec" +version = "0.11.2" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, + {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, +] + +[[package]] +name = "pexpect" +version = "4.8.0" +description = "Pexpect allows easy control of interactive console applications." +optional = false +python-versions = "*" +files = [ + {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, + {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, +] + +[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.0.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-4.0.0-py3-none-any.whl", hash = "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b"}, + {file = "platformdirs-4.0.0.tar.gz", hash = "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731"}, +] + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] + +[[package]] +name = "pluggy" +version = "1.3.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, +] + +[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.41" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "prompt_toolkit-3.0.41-py3-none-any.whl", hash = "sha256:f36fe301fafb7470e86aaf90f036eef600a3210be4decf461a5b1ca8403d3cb2"}, + {file = "prompt_toolkit-3.0.41.tar.gz", hash = "sha256:941367d97fc815548822aa26c2a269fdc4eb21e9ec05fc5d447cf09bad5d75f0"}, +] + +[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 = "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.5.2" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic-2.5.2-py3-none-any.whl", hash = "sha256:80c50fb8e3dcecfddae1adbcc00ec5822918490c99ab31f6cf6140ca1c1429f0"}, + {file = "pydantic-2.5.2.tar.gz", hash = "sha256:ff177ba64c6faf73d7afa2e8cad38fd456c0dbe01c9954e71038001cd15a6edd"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.14.5" +typing-extensions = ">=4.6.1" + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.14.5" +description = "" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic_core-2.14.5-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:7e88f5696153dc516ba6e79f82cc4747e87027205f0e02390c21f7cb3bd8abfd"}, + {file = "pydantic_core-2.14.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4641e8ad4efb697f38a9b64ca0523b557c7931c5f84e0fd377a9a3b05121f0de"}, + {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:774de879d212db5ce02dfbf5b0da9a0ea386aeba12b0b95674a4ce0593df3d07"}, + {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ebb4e035e28f49b6f1a7032920bb9a0c064aedbbabe52c543343d39341a5b2a3"}, + {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b53e9ad053cd064f7e473a5f29b37fc4cc9dc6d35f341e6afc0155ea257fc911"}, + {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aa1768c151cf562a9992462239dfc356b3d1037cc5a3ac829bb7f3bda7cc1f9"}, + {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eac5c82fc632c599f4639a5886f96867ffced74458c7db61bc9a66ccb8ee3113"}, + {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2ae91f50ccc5810b2f1b6b858257c9ad2e08da70bf890dee02de1775a387c66"}, + {file = "pydantic_core-2.14.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6b9ff467ffbab9110e80e8c8de3bcfce8e8b0fd5661ac44a09ae5901668ba997"}, + {file = "pydantic_core-2.14.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:61ea96a78378e3bd5a0be99b0e5ed00057b71f66115f5404d0dae4819f495093"}, + {file = "pydantic_core-2.14.5-cp310-none-win32.whl", hash = "sha256:bb4c2eda937a5e74c38a41b33d8c77220380a388d689bcdb9b187cf6224c9720"}, + {file = "pydantic_core-2.14.5-cp310-none-win_amd64.whl", hash = "sha256:b7851992faf25eac90bfcb7bfd19e1f5ffa00afd57daec8a0042e63c74a4551b"}, + {file = "pydantic_core-2.14.5-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:4e40f2bd0d57dac3feb3a3aed50f17d83436c9e6b09b16af271b6230a2915459"}, + {file = "pydantic_core-2.14.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ab1cdb0f14dc161ebc268c09db04d2c9e6f70027f3b42446fa11c153521c0e88"}, + {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aae7ea3a1c5bb40c93cad361b3e869b180ac174656120c42b9fadebf685d121b"}, + {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:60b7607753ba62cf0739177913b858140f11b8af72f22860c28eabb2f0a61937"}, + {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2248485b0322c75aee7565d95ad0e16f1c67403a470d02f94da7344184be770f"}, + {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:823fcc638f67035137a5cd3f1584a4542d35a951c3cc68c6ead1df7dac825c26"}, + {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96581cfefa9123accc465a5fd0cc833ac4d75d55cc30b633b402e00e7ced00a6"}, + {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a33324437018bf6ba1bb0f921788788641439e0ed654b233285b9c69704c27b4"}, + {file = "pydantic_core-2.14.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9bd18fee0923ca10f9a3ff67d4851c9d3e22b7bc63d1eddc12f439f436f2aada"}, + {file = "pydantic_core-2.14.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:853a2295c00f1d4429db4c0fb9475958543ee80cfd310814b5c0ef502de24dda"}, + {file = "pydantic_core-2.14.5-cp311-none-win32.whl", hash = "sha256:cb774298da62aea5c80a89bd58c40205ab4c2abf4834453b5de207d59d2e1651"}, + {file = "pydantic_core-2.14.5-cp311-none-win_amd64.whl", hash = "sha256:e87fc540c6cac7f29ede02e0f989d4233f88ad439c5cdee56f693cc9c1c78077"}, + {file = "pydantic_core-2.14.5-cp311-none-win_arm64.whl", hash = "sha256:57d52fa717ff445cb0a5ab5237db502e6be50809b43a596fb569630c665abddf"}, + {file = "pydantic_core-2.14.5-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:e60f112ac88db9261ad3a52032ea46388378034f3279c643499edb982536a093"}, + {file = "pydantic_core-2.14.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6e227c40c02fd873c2a73a98c1280c10315cbebe26734c196ef4514776120aeb"}, + {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0cbc7fff06a90bbd875cc201f94ef0ee3929dfbd5c55a06674b60857b8b85ed"}, + {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:103ef8d5b58596a731b690112819501ba1db7a36f4ee99f7892c40da02c3e189"}, + {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c949f04ecad823f81b1ba94e7d189d9dfb81edbb94ed3f8acfce41e682e48cef"}, + {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1452a1acdf914d194159439eb21e56b89aa903f2e1c65c60b9d874f9b950e5d"}, + {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb4679d4c2b089e5ef89756bc73e1926745e995d76e11925e3e96a76d5fa51fc"}, + {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf9d3fe53b1ee360e2421be95e62ca9b3296bf3f2fb2d3b83ca49ad3f925835e"}, + {file = "pydantic_core-2.14.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:70f4b4851dbb500129681d04cc955be2a90b2248d69273a787dda120d5cf1f69"}, + {file = "pydantic_core-2.14.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:59986de5710ad9613ff61dd9b02bdd2f615f1a7052304b79cc8fa2eb4e336d2d"}, + {file = "pydantic_core-2.14.5-cp312-none-win32.whl", hash = "sha256:699156034181e2ce106c89ddb4b6504c30db8caa86e0c30de47b3e0654543260"}, + {file = "pydantic_core-2.14.5-cp312-none-win_amd64.whl", hash = "sha256:5baab5455c7a538ac7e8bf1feec4278a66436197592a9bed538160a2e7d11e36"}, + {file = "pydantic_core-2.14.5-cp312-none-win_arm64.whl", hash = "sha256:e47e9a08bcc04d20975b6434cc50bf82665fbc751bcce739d04a3120428f3e27"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:af36f36538418f3806048f3b242a1777e2540ff9efaa667c27da63d2749dbce0"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:45e95333b8418ded64745f14574aa9bfc212cb4fbeed7a687b0c6e53b5e188cd"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e47a76848f92529879ecfc417ff88a2806438f57be4a6a8bf2961e8f9ca9ec7"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d81e6987b27bc7d101c8597e1cd2bcaa2fee5e8e0f356735c7ed34368c471550"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34708cc82c330e303f4ce87758828ef6e457681b58ce0e921b6e97937dd1e2a3"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c1988019752138b974c28f43751528116bcceadad85f33a258869e641d753"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e4d090e73e0725b2904fdbdd8d73b8802ddd691ef9254577b708d413bf3006e"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5c7d5b5005f177764e96bd584d7bf28d6e26e96f2a541fdddb934c486e36fd59"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a71891847f0a73b1b9eb86d089baee301477abef45f7eaf303495cd1473613e4"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a717aef6971208f0851a2420b075338e33083111d92041157bbe0e2713b37325"}, + {file = "pydantic_core-2.14.5-cp37-none-win32.whl", hash = "sha256:de790a3b5aa2124b8b78ae5faa033937a72da8efe74b9231698b5a1dd9be3405"}, + {file = "pydantic_core-2.14.5-cp37-none-win_amd64.whl", hash = "sha256:6c327e9cd849b564b234da821236e6bcbe4f359a42ee05050dc79d8ed2a91588"}, + {file = "pydantic_core-2.14.5-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:ef98ca7d5995a82f43ec0ab39c4caf6a9b994cb0b53648ff61716370eadc43cf"}, + {file = "pydantic_core-2.14.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6eae413494a1c3f89055da7a5515f32e05ebc1a234c27674a6956755fb2236f"}, + {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcf4e6d85614f7a4956c2de5a56531f44efb973d2fe4a444d7251df5d5c4dcfd"}, + {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6637560562134b0e17de333d18e69e312e0458ee4455bdad12c37100b7cad706"}, + {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77fa384d8e118b3077cccfcaf91bf83c31fe4dc850b5e6ee3dc14dc3d61bdba1"}, + {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16e29bad40bcf97aac682a58861249ca9dcc57c3f6be22f506501833ddb8939c"}, + {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:531f4b4252fac6ca476fbe0e6f60f16f5b65d3e6b583bc4d87645e4e5ddde331"}, + {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:074f3d86f081ce61414d2dc44901f4f83617329c6f3ab49d2bc6c96948b2c26b"}, + {file = "pydantic_core-2.14.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c2adbe22ab4babbca99c75c5d07aaf74f43c3195384ec07ccbd2f9e3bddaecec"}, + {file = "pydantic_core-2.14.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0f6116a558fd06d1b7c2902d1c4cf64a5bd49d67c3540e61eccca93f41418124"}, + {file = "pydantic_core-2.14.5-cp38-none-win32.whl", hash = "sha256:fe0a5a1025eb797752136ac8b4fa21aa891e3d74fd340f864ff982d649691867"}, + {file = "pydantic_core-2.14.5-cp38-none-win_amd64.whl", hash = "sha256:079206491c435b60778cf2b0ee5fd645e61ffd6e70c47806c9ed51fc75af078d"}, + {file = "pydantic_core-2.14.5-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:a6a16f4a527aae4f49c875da3cdc9508ac7eef26e7977952608610104244e1b7"}, + {file = "pydantic_core-2.14.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:abf058be9517dc877227ec3223f0300034bd0e9f53aebd63cf4456c8cb1e0863"}, + {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49b08aae5013640a3bfa25a8eebbd95638ec3f4b2eaf6ed82cf0c7047133f03b"}, + {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c2d97e906b4ff36eb464d52a3bc7d720bd6261f64bc4bcdbcd2c557c02081ed2"}, + {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3128e0bbc8c091ec4375a1828d6118bc20404883169ac95ffa8d983b293611e6"}, + {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88e74ab0cdd84ad0614e2750f903bb0d610cc8af2cc17f72c28163acfcf372a4"}, + {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c339dabd8ee15f8259ee0f202679b6324926e5bc9e9a40bf981ce77c038553db"}, + {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3387277f1bf659caf1724e1afe8ee7dbc9952a82d90f858ebb931880216ea955"}, + {file = "pydantic_core-2.14.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ba6b6b3846cfc10fdb4c971980a954e49d447cd215ed5a77ec8190bc93dd7bc5"}, + {file = "pydantic_core-2.14.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ca61d858e4107ce5e1330a74724fe757fc7135190eb5ce5c9d0191729f033209"}, + {file = "pydantic_core-2.14.5-cp39-none-win32.whl", hash = "sha256:ec1e72d6412f7126eb7b2e3bfca42b15e6e389e1bc88ea0069d0cc1742f477c6"}, + {file = "pydantic_core-2.14.5-cp39-none-win_amd64.whl", hash = "sha256:c0b97ec434041827935044bbbe52b03d6018c2897349670ff8fe11ed24d1d4ab"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:79e0a2cdbdc7af3f4aee3210b1172ab53d7ddb6a2d8c24119b5706e622b346d0"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:678265f7b14e138d9a541ddabbe033012a2953315739f8cfa6d754cc8063e8ca"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b15e855ae44f0c6341ceb74df61b606e11f1087e87dcb7482377374aac6abe"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09b0e985fbaf13e6b06a56d21694d12ebca6ce5414b9211edf6f17738d82b0f8"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3ad873900297bb36e4b6b3f7029d88ff9829ecdc15d5cf20161775ce12306f8a"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2d0ae0d8670164e10accbeb31d5ad45adb71292032d0fdb9079912907f0085f4"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d37f8ec982ead9ba0a22a996129594938138a1503237b87318392a48882d50b7"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:35613015f0ba7e14c29ac6c2483a657ec740e5ac5758d993fdd5870b07a61d8b"}, + {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:ab4ea451082e684198636565224bbb179575efc1658c48281b2c866bfd4ddf04"}, + {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ce601907e99ea5b4adb807ded3570ea62186b17f88e271569144e8cca4409c7"}, + {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb2ed8b3fe4bf4506d6dab3b93b83bbc22237e230cba03866d561c3577517d18"}, + {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:70f947628e074bb2526ba1b151cee10e4c3b9670af4dbb4d73bc8a89445916b5"}, + {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4bc536201426451f06f044dfbf341c09f540b4ebdb9fd8d2c6164d733de5e634"}, + {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4791cf0f8c3104ac668797d8c514afb3431bc3305f5638add0ba1a5a37e0d88"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:038c9f763e650712b899f983076ce783175397c848da04985658e7628cbe873b"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:27548e16c79702f1e03f5628589c6057c9ae17c95b4c449de3c66b589ead0520"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c97bee68898f3f4344eb02fec316db93d9700fb1e6a5b760ffa20d71d9a46ce3"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9b759b77f5337b4ea024f03abc6464c9f35d9718de01cfe6bae9f2e139c397e"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:439c9afe34638ace43a49bf72d201e0ffc1a800295bed8420c2a9ca8d5e3dbb3"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ba39688799094c75ea8a16a6b544eb57b5b0f3328697084f3f2790892510d144"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ccd4d5702bb90b84df13bd491be8d900b92016c5a455b7e14630ad7449eb03f8"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:81982d78a45d1e5396819bbb4ece1fadfe5f079335dd28c4ab3427cd95389944"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:7f8210297b04e53bc3da35db08b7302a6a1f4889c79173af69b72ec9754796b8"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:8c8a8812fe6f43a3a5b054af6ac2d7b8605c7bcab2804a8a7d68b53f3cd86e00"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:206ed23aecd67c71daf5c02c3cd19c0501b01ef3cbf7782db9e4e051426b3d0d"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2027d05c8aebe61d898d4cffd774840a9cb82ed356ba47a90d99ad768f39789"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40180930807ce806aa71eda5a5a5447abb6b6a3c0b4b3b1b1962651906484d68"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:615a0a4bff11c45eb3c1996ceed5bdaa2f7b432425253a7c2eed33bb86d80abc"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5e412d717366e0677ef767eac93566582518fe8be923361a5c204c1a62eaafe"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:513b07e99c0a267b1d954243845d8a833758a6726a3b5d8948306e3fe14675e3"}, + {file = "pydantic_core-2.14.5.tar.gz", hash = "sha256:6d30226dfc816dd0fdf120cae611dd2215117e4f9b124af8c60ab9093b6e8e71"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[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.3" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, + {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, +] + +[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-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 = "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 = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[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 = "setuptools" +version = "69.0.2" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-69.0.2-py3-none-any.whl", hash = "sha256:1e8fdff6797d3865f37397be788a4e3cba233608e9b509382a2777d25ebde7f2"}, + {file = "setuptools-69.0.2.tar.gz", hash = "sha256:735896e78a4742605974de002ac60562d286fa8051a7e2299445e8e8fbb01aa6"}, +] + +[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.23" +description = "Database Abstraction Library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "SQLAlchemy-2.0.23-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:638c2c0b6b4661a4fd264f6fb804eccd392745c5887f9317feb64bb7cb03b3ea"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3b5036aa326dc2df50cba3c958e29b291a80f604b1afa4c8ce73e78e1c9f01d"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:787af80107fb691934a01889ca8f82a44adedbf5ef3d6ad7d0f0b9ac557e0c34"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c14eba45983d2f48f7546bb32b47937ee2cafae353646295f0e99f35b14286ab"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0666031df46b9badba9bed00092a1ffa3aa063a5e68fa244acd9f08070e936d3"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:89a01238fcb9a8af118eaad3ffcc5dedaacbd429dc6fdc43fe430d3a941ff965"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-win32.whl", hash = "sha256:cabafc7837b6cec61c0e1e5c6d14ef250b675fa9c3060ed8a7e38653bd732ff8"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-win_amd64.whl", hash = "sha256:87a3d6b53c39cd173990de2f5f4b83431d534a74f0e2f88bd16eabb5667e65c6"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d5578e6863eeb998980c212a39106ea139bdc0b3f73291b96e27c929c90cd8e1"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:62d9e964870ea5ade4bc870ac4004c456efe75fb50404c03c5fd61f8bc669a72"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c80c38bd2ea35b97cbf7c21aeb129dcbebbf344ee01a7141016ab7b851464f8e"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75eefe09e98043cff2fb8af9796e20747ae870c903dc61d41b0c2e55128f958d"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd45a5b6c68357578263d74daab6ff9439517f87da63442d244f9f23df56138d"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a86cb7063e2c9fb8e774f77fbf8475516d270a3e989da55fa05d08089d77f8c4"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-win32.whl", hash = "sha256:b41f5d65b54cdf4934ecede2f41b9c60c9f785620416e8e6c48349ab18643855"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-win_amd64.whl", hash = "sha256:9ca922f305d67605668e93991aaf2c12239c78207bca3b891cd51a4515c72e22"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d0f7fb0c7527c41fa6fcae2be537ac137f636a41b4c5a4c58914541e2f436b45"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7c424983ab447dab126c39d3ce3be5bee95700783204a72549c3dceffe0fc8f4"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f508ba8f89e0a5ecdfd3761f82dda2a3d7b678a626967608f4273e0dba8f07ac"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6463aa765cf02b9247e38b35853923edbf2f6fd1963df88706bc1d02410a5577"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e599a51acf3cc4d31d1a0cf248d8f8d863b6386d2b6782c5074427ebb7803bda"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fd54601ef9cc455a0c61e5245f690c8a3ad67ddb03d3b91c361d076def0b4c60"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-win32.whl", hash = "sha256:42d0b0290a8fb0165ea2c2781ae66e95cca6e27a2fbe1016ff8db3112ac1e846"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-win_amd64.whl", hash = "sha256:227135ef1e48165f37590b8bfc44ed7ff4c074bf04dc8d6f8e7f1c14a94aa6ca"}, + {file = "SQLAlchemy-2.0.23-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:14aebfe28b99f24f8a4c1346c48bc3d63705b1f919a24c27471136d2f219f02d"}, + {file = "SQLAlchemy-2.0.23-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e983fa42164577d073778d06d2cc5d020322425a509a08119bdcee70ad856bf"}, + {file = "SQLAlchemy-2.0.23-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e0dc9031baa46ad0dd5a269cb7a92a73284d1309228be1d5935dac8fb3cae24"}, + {file = "SQLAlchemy-2.0.23-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5f94aeb99f43729960638e7468d4688f6efccb837a858b34574e01143cf11f89"}, + {file = "SQLAlchemy-2.0.23-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:63bfc3acc970776036f6d1d0e65faa7473be9f3135d37a463c5eba5efcdb24c8"}, + {file = "SQLAlchemy-2.0.23-cp37-cp37m-win32.whl", hash = "sha256:f48ed89dd11c3c586f45e9eec1e437b355b3b6f6884ea4a4c3111a3358fd0c18"}, + {file = "SQLAlchemy-2.0.23-cp37-cp37m-win_amd64.whl", hash = "sha256:1e018aba8363adb0599e745af245306cb8c46b9ad0a6fc0a86745b6ff7d940fc"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:64ac935a90bc479fee77f9463f298943b0e60005fe5de2aa654d9cdef46c54df"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c4722f3bc3c1c2fcc3702dbe0016ba31148dd6efcd2a2fd33c1b4897c6a19693"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4af79c06825e2836de21439cb2a6ce22b2ca129bad74f359bddd173f39582bf5"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:683ef58ca8eea4747737a1c35c11372ffeb84578d3aab8f3e10b1d13d66f2bc4"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d4041ad05b35f1f4da481f6b811b4af2f29e83af253bf37c3c4582b2c68934ab"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aeb397de65a0a62f14c257f36a726945a7f7bb60253462e8602d9b97b5cbe204"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-win32.whl", hash = "sha256:42ede90148b73fe4ab4a089f3126b2cfae8cfefc955c8174d697bb46210c8306"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-win_amd64.whl", hash = "sha256:964971b52daab357d2c0875825e36584d58f536e920f2968df8d581054eada4b"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:616fe7bcff0a05098f64b4478b78ec2dfa03225c23734d83d6c169eb41a93e55"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0e680527245895aba86afbd5bef6c316831c02aa988d1aad83c47ffe92655e74"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9585b646ffb048c0250acc7dad92536591ffe35dba624bb8fd9b471e25212a35"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4895a63e2c271ffc7a81ea424b94060f7b3b03b4ea0cd58ab5bb676ed02f4221"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cc1d21576f958c42d9aec68eba5c1a7d715e5fc07825a629015fe8e3b0657fb0"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:967c0b71156f793e6662dd839da54f884631755275ed71f1539c95bbada9aaab"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-win32.whl", hash = "sha256:0a8c6aa506893e25a04233bc721c6b6cf844bafd7250535abb56cb6cc1368884"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-win_amd64.whl", hash = "sha256:f3420d00d2cb42432c1d0e44540ae83185ccbbc67a6054dcc8ab5387add6620b"}, + {file = "SQLAlchemy-2.0.23-py3-none-any.whl", hash = "sha256:31952bbc527d633b9479f5f81e8b9dfada00b91d6baba021a869095f1a97006d"}, + {file = "SQLAlchemy-2.0.23.tar.gz", hash = "sha256:c1bda93cbbe4aa2aa0aa8655c5aeda505cd219ff3e8da91d1d329e143e4aff69"}, +] + +[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.2.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 = "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.13.0" +description = "Traitlets Python configuration system" +optional = false +python-versions = ">=3.8" +files = [ + {file = "traitlets-5.13.0-py3-none-any.whl", hash = "sha256:baf991e61542da48fe8aef8b779a9ea0aa38d8a54166ee250d5af5ecf4486619"}, + {file = "traitlets-5.13.0.tar.gz", hash = "sha256:9b232b9430c8f57288c1024b34a8f0251ddcc47268927367a0dd3eeaca40deb5"}, +] + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] +test = ["argcomplete (>=3.0.3)", "mypy (>=1.6.0)", "pre-commit", "pytest (>=7.0,<7.5)", "pytest-mock", "pytest-mypy-testing"] + +[[package]] +name = "typing-extensions" +version = "4.8.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, + {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, +] + +[[package]] +name = "urllib3" +version = "2.1.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"}, + {file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[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.24.7" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.24.7-py3-none-any.whl", hash = "sha256:a18b3fd0314ca59a2e9f4b556819ed07183b3e9a3702ecfe213f593d44f7b3fd"}, + {file = "virtualenv-20.24.7.tar.gz", hash = "sha256:69050ffb42419c91f6c1284a7b24e0475d793447e35929b488bf6a0aade39353"}, +] + +[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.12" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +files = [ + {file = "wcwidth-0.2.12-py2.py3-none-any.whl", hash = "sha256:f26ec43d96c8cbfed76a5075dac87680124fa84e0855195a6184da9c187f133c"}, + {file = "wcwidth-0.2.12.tar.gz", hash = "sha256:f01c104efdf57971bcb756f054dd58ddec5204dd15fa31d6503ea57947d97c02"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.8" +content-hash = "c18cf5649aa1d852de2a5bd83ae6525ad63ce09d23da9a9e830ac93c7a27f57a" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1880ea2 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,36 @@ +[tool.poetry] +name = "battlechess" +version = "0.1.0" +description = "chess with fog of war" +authors = ["guyver2@github.com", "quimnuss@github.com"] +license = "GPLv3" +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.8" +requests = "^2.31.0" +pathlib = "^1.0.1" + + +[tool.poetry.group.standalone.dependencies] +pygame = "^2.5.2" + + +[tool.poetry.group.api.dependencies] +fastapi = "^0.104.1" +sqlalchemy = "^2.0.23" +passlib = {extras = ["bcrypt"], version = "^1.7.4"} +python-jose = {extras = ["cryptography"], version = "^3.3.0"} +uvicorn = "^0.24.0.post1" + + +[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" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" From 19b1601f6c2d84748abe1ef43ee1d6c11e777cf9 Mon Sep 17 00:00:00 2001 From: quimnuss Date: Fri, 24 Nov 2023 21:37:21 +0100 Subject: [PATCH 02/43] package battlechess, put tests in its own directory --- .editorconfig | 28 + .isort.cfg | 2 + Readme.md | 134 --- Readme.txt | 98 -- {core => battlechess}/__init__.py | 0 battlechess/core/Board.py | 669 +++++++++++ .../core/__init__.py | 0 {core => battlechess/core}/btchBoard.py | 143 ++- {server => battlechess/server}/__init__.py | 0 {server => battlechess/server}/btchApi.py | 226 ++-- {server => battlechess/server}/btchApiDB.py | 3 +- {server => battlechess/server}/btchDB.py | 196 ++-- battlechess/server/btchServer.py | 0 {server => battlechess/server}/config.py | 2 +- .../server}/config_example.py | 0 {server => battlechess/server}/crud.py | 135 ++- {server => battlechess/server}/models.py | 56 +- {server => battlechess/server}/schemas.py | 49 +- {server => battlechess/server}/utils.py | 31 +- core/Board.py | 598 ---------- core/test_Board.py | 123 -- core/test_btchBoard.py | 164 --- poetry.lock | 223 ++-- pyproject.toml | 6 +- ruff.toml | 5 + server.py | 69 +- tests/core/test_Board.py | 125 ++ tests/core/test_btchBoard.py | 175 +++ {server => tests/server}/test_btchApi.py | 1020 +++++++++-------- .../server}/test_btchApi_autoplay.py | 122 +- {server => tests/server}/test_models.py | 121 +- 31 files changed, 2338 insertions(+), 2185 deletions(-) create mode 100644 .editorconfig create mode 100644 .isort.cfg delete mode 100644 Readme.md delete mode 100644 Readme.txt rename {core => battlechess}/__init__.py (100%) create mode 100644 battlechess/core/Board.py rename server/btchServer.py => battlechess/core/__init__.py (100%) rename {core => battlechess/core}/btchBoard.py (71%) rename {server => battlechess/server}/__init__.py (100%) rename {server => battlechess/server}/btchApi.py (73%) rename {server => battlechess/server}/btchApiDB.py (97%) rename {server => battlechess/server}/btchDB.py (74%) create mode 100644 battlechess/server/btchServer.py rename {server => battlechess/server}/config.py (85%) rename {server => battlechess/server}/config_example.py (100%) rename {server => battlechess/server}/crud.py (71%) rename {server => battlechess/server}/models.py (85%) rename {server => battlechess/server}/schemas.py (75%) rename {server => battlechess/server}/utils.py (74%) delete mode 100644 core/Board.py delete mode 100644 core/test_Board.py delete mode 100644 core/test_btchBoard.py create mode 100644 ruff.toml create mode 100644 tests/core/test_Board.py create mode 100644 tests/core/test_btchBoard.py rename {server => tests/server}/test_btchApi.py (60%) rename {server => tests/server}/test_btchApi_autoplay.py (76%) rename {server => tests/server}/test_models.py (69%) 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/.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/Readme.md b/Readme.md deleted file mode 100644 index 8d29f3b..0000000 --- a/Readme.md +++ /dev/null @@ -1,134 +0,0 @@ - ___ _ _ _ ___ _ - ( _`\ ( )_ ( )_ (_ ) ( _`\ ( ) - | (_) ) _ _ | ,_)| ,_) | | __ | ( (_)| |__ __ ___ ___ - | _ <' /'_` )| | | | | | /'__`\| | _ | _ `\ /'__`\/',__)/',__) - | (_) )( (_| || |_ | |_ | | ( ___/| (_( )| | | |( ___/\__, \\__, \ - (____/'`\__,_)`\__)`\__)(___)`\____)(____/'(_) (_)`\____)(____/(____/ - - ---- - -[![Build Status](https://travis-ci.org/quimnuss/battlechess.svg?branch=backend)](https://travis-ci.org/quimnuss/battlechess) - -## Abstract -- Quick game : - `python battleChess.py` -- Download : - `git clone https://github.com/quimnuss/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 pawns which reaches the end of the board becomes a queen, you cannot choose, deal with it. - - -## Installation : - -- 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) - * the game itself. It can be downloaded via git using the following command: - ```git clone http://git.sxbn.org/battleChess.git``` - -- Launching the game : - There is and will be a server app running on sxbn.org, so I recommend no changing the HOST and PORT parameters unless you want to host your own games. - 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 - - -- Replay mode : - You just got beaten and you don't know what just happened. Don't worry that happens a lot. Every game played are saved on the server and can be downloaded from this webpage : [http://sxbn.org/~antoine/games/](http://sxbn.org/~antoine/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://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` - -- 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` - 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` - - You can go to `localhost:8000/docs` for the documentation and usage examples. Create a user and then authenticate on the upper right with `johndoe` and `secret` - -- 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. - -- Tests: - You can use the `$ pytest` runner to run the tests from the root of the project. - -## 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 there, 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. - - -## Android & iOS Apps : - - -- Android .apk file can be downloaded from this link : [http://sxbn.org/~antoine/git/battlechess/BattleChess.apk](http://sxbn.org/~antoine/git/battlechess/BattleChess.apk) - -- iOS app is ready and working, but thanks to Apple's App Store terms we cannot release it without paying... - - -### Building the app - -If you are builind the app from source and it complains that cocos2dx doesn't have neon_matrix_impl.c, copy it from cocos2dmods/ to the directory where it is looking for it. - - -## Credits & Licence : - - -- Code : - Antoine Letouzey -- [antoine.letouzey@gmail.com](antoine.letouzey@gmail.com) - Pol Monsó-Purtí -- [pol.monso@gmail.com](pol.monso@gmail.com) - -- Sprites : - Original sprites by Wikipedia user Cburnett, under Creative Commons Licence (CC BY-SA 3.0) - -- Licence : GPL - -## Screenshots : -![img1](http://sxbn.org/~antoine/git/battlechess/1.jpg) -![img2](http://sxbn.org/~antoine/git/battlechess/2.jpg) -![img3](http://sxbn.org/~antoine/git/battlechess/3.jpg) -![img4](http://sxbn.org/~antoine/git/battlechess/4.jpg) -![img5](http://sxbn.org/~antoine/git/battlechess/5.jpg) - -## Screenshots Android & iOS App : - -

- -

- -![m2](https://user-images.githubusercontent.com/4179721/32983274-5a8e9868-cc92-11e7-94f0-cadc6b50e038.png) -![m3](https://user-images.githubusercontent.com/4179721/32983275-5ab8462c-cc92-11e7-8e4d-becde7cfe0da.png) -![m4](https://user-images.githubusercontent.com/4179721/32983276-5ad82ab4-cc92-11e7-98a3-419f788f1670.png) -![m5](https://user-images.githubusercontent.com/4179721/32983277-5afbe6f2-cc92-11e7-880c-8efd8ecb53db.png) - -

- -

- 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..2a64efb --- /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 73% rename from server/btchApi.py rename to battlechess/server/btchApi.py index 4f89382..e893f48 100644 --- a/server/btchApi.py +++ b/battlechess/server/btchApi.py @@ -1,19 +1,27 @@ 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 +from typing import List, Optional, Set, Tuple + +from fastapi import ( + Body, + Depends, + FastAPI, + File, + Header, + HTTPException, + 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.orm import Session from sqlalchemy.sql.functions import user -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 .config import ACCESS_TOKEN_EXPIRE_MINUTES, ALGORITHM, SECRET_KEY +from .schemas import Game, GameStatus PASSWORD_MIN_LENGTH = 3 AVATAR_MAX_SIZE = 100000 @@ -43,10 +51,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,21 +80,27 @@ 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 @@ -98,12 +116,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,7 +132,8 @@ 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"} @@ -123,19 +143,23 @@ def read_users_me(current_user: schemas.User = Depends(get_current_active_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)): +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}') + 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)): +def read_users( + 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 @@ -169,8 +195,10 @@ def read__single_user(userID: int, @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.", @@ -198,9 +226,11 @@ def create_user(new_user: schemas.UserCreate, db: Session = Depends(get_db)): # 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 +246,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,8 +267,9 @@ 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() @@ -244,34 +277,42 @@ def update_avatar_file(file: UploadFile = File(...), @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)): +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.get("/games/{gameUUID}") -def get_game_by_uuid(gameUUID: str, - current_user: schemas.User = Depends(get_current_active_user), - db: Session = Depends(get_db)): +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) # 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)): +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) 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)): +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( @@ -306,8 +347,10 @@ def join_game(gameUUID: str, # 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)): +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 {} @@ -321,17 +364,21 @@ def join_random_game(current_user: schemas.User = Depends(get_current_active_use # 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 # 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)): +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) return game.turn @@ -339,10 +386,11 @@ def query_turn(gameUUID: str, @app.post("/games/{gameUUID}/move") def post_move( gameUUID: str, - #move: dict = Body(...), # or pydantic or query parameter? Probably pydantic to make clear what a move is + # move: dict = Body(...), # or pydantic or query parameter? Probably pydantic to make clear what a move is 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( @@ -368,10 +416,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 +438,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'] ", @@ -406,12 +456,16 @@ def get_moves(gameUUID: str, @app.get("/games/{gameUUID}/snap") -def get_snap(gameUUID: str, - current_user: schemas.User = Depends(get_current_active_user), - db: Session = Depends(get_db)): +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]): + if (game.status != GameStatus.OVER) and ( + current_user.id not in [game.white_id, game.black_id] + ): game = None if not game: raise HTTPException( @@ -433,20 +487,24 @@ def get_snap(gameUUID: str, 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}') + 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)): +def get_snap( + 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( @@ -469,12 +527,16 @@ def get_snap(gameUUID: str, @app.get("/games/{gameUUID}/snaps") -def get_snaps(gameUUID: str, - current_user: schemas.User = Depends(get_current_active_user), - db: Session = Depends(get_db)): +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( diff --git a/server/btchApiDB.py b/battlechess/server/btchApiDB.py similarity index 97% rename from server/btchApiDB.py rename to battlechess/server/btchApiDB.py index c7a40c6..eebd5cd 100644 --- a/server/btchApiDB.py +++ b/battlechess/server/btchApiDB.py @@ -2,7 +2,6 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker - SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db" # 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/btchServer.py b/battlechess/server/btchServer.py new file mode 100644 index 0000000..e69de29 diff --git a/server/config.py b/battlechess/server/config.py similarity index 85% rename from server/config.py rename to battlechess/server/config.py index c5f465c..5981505 100644 --- a/server/config.py +++ b/battlechess/server/config.py @@ -3,4 +3,4 @@ SECRET_KEY = "e909bb995546a0378161ed18d4e44ab4525d735e07a52cec2eb9b3a86d39ee61" ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 3000 -HANDLEBASEURL = "https://bt.ch/" \ No newline at end of file +HANDLEBASEURL = "https://bt.ch/" diff --git a/server/config_example.py b/battlechess/server/config_example.py similarity index 100% rename from server/config_example.py rename to battlechess/server/config_example.py diff --git a/server/crud.py b/battlechess/server/crud.py similarity index 71% rename from server/crud.py rename to battlechess/server/crud.py index 569d1ac..5b616ca 100644 --- a/server/crud.py +++ b/battlechess/server/crud.py @@ -1,25 +1,16 @@ 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 pathlib import Path +from typing import Optional, Set, Tuple + from jose import JWTError, jwt +from sqlalchemy import and_, or_ +from sqlalchemy.orm import Session from . import models, schemas - -from .utils import (get_password_hash, verify_password, get_random_string, - defaultBoard) - -from .config import ( - SECRET_KEY, - ALGORITHM, - ACCESS_TOKEN_EXPIRE_MINUTES, - HANDLEBASEURL, -) +from .config import ACCESS_TOKEN_EXPIRE_MINUTES, ALGORITHM, HANDLEBASEURL, SECRET_KEY +from .utils import defaultBoard, get_password_hash, get_random_string, verify_password # TODO redo this. I hate myself for writing it. @@ -27,8 +18,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 +34,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 +56,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 +73,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 +86,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 +112,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 +129,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) @@ -157,8 +150,11 @@ def get_games_by_owner(db: Session, user: schemas.User): 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() + return ( + db.query(models.Game) + .filter(or_(models.Game.black == user, models.Game.white == user)) + .all() + ) def get_game_by_uuid(db: Session, gameUUID): @@ -166,11 +162,18 @@ 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), + models.Game.public == True, + ) + ) + .all() + ) return games @@ -187,8 +190,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 +207,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 +220,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,18 +233,18 @@ 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 = "finished" 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.refresh_turn() color = None if game.black_id == user.id: - color = 'b' + color = "b" if game.white_id == user.id: - color = 'w' + color = "w" # deprecated in favor of pydantic prepare_for_player TODO pydantic elements # elements = db_snap.filtered(color) @@ -246,8 +254,15 @@ def create_snap_by_move(db: Session, user: schemas.User, game: schemas.Game, # 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 +275,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 +290,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 85% rename from server/models.py rename to battlechess/server/models.py index 0e90f77..697830d 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 from fastapi import HTTPException, status +from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String +from sqlalchemy.orm import relationship + +from battlechess.core.Board import Board +from battlechess.core.btchBoard import BtchBoard 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 .utils import ad2extij, ad2ij, extij2ad class User(Base): @@ -30,6 +30,7 @@ class User(Base): def is_active(self): return self.status == "active" + class Game(Base): __tablename__ = "game" @@ -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] @@ -78,7 +79,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", @@ -112,7 +113,7 @@ 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 @@ -123,6 +124,7 @@ def moveGame(self, move): new_snap_options = current_snap.moveSnap(move) return new_snap_options + class GameSnap(Base): __tablename__ = "gamesnap" @@ -141,16 +143,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 +163,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])] @@ -206,15 +208,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 +232,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 75% rename from server/schemas.py rename to battlechess/server/schemas.py index 6bc0fa6..a9e5d9b 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 typing import List, Optional, Tuple + # from uuid import UUID # represented as string from pydantic import BaseModel -class GameStatus(): +class GameStatus: WAITING = "waiting" STARTED = "started" OVER = "over" @@ -34,14 +35,19 @@ class GameSnap(GameSnapBase): move_number: int 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 +57,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 +66,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 +82,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,18 +102,18 @@ 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: @@ -111,7 +121,6 @@ class Config: class FilteredGameSnap(GameSnap): - class Config: orm_mode = False @@ -179,4 +188,4 @@ class User(UserBase): # blacks: List[Game] = [] class Config: - orm_mode = True \ No newline at end of file + orm_mode = True diff --git a/server/utils.py b/battlechess/server/utils.py similarity index 74% rename from server/utils.py rename to battlechess/server/utils.py index c999c97..2d15bc6 100644 --- a/server/utils.py +++ b/battlechess/server/utils.py @@ -1,38 +1,44 @@ 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)) + 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' + "RNBQKBNR" + "PPPPPPPP" + "________" + "________" + "________" + "________" + "pppppppp" + "rnbqkbnr" ) @@ -43,10 +49,11 @@ def extij2ad(i, j): def ad2extij(square): i = 8 - int(square[1]) + 2 - j = ord(square[0]) - ord('a') + 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 + j = ord(square[0]) - ord("a") + return (i, j) 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/poetry.lock b/poetry.lock index e9f3826..17a3bec 100644 --- a/poetry.lock +++ b/poetry.lock @@ -237,105 +237,6 @@ files = [ {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, ] -[[package]] -name = "charset-normalizer" -version = "3.3.2" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, - {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, -] - [[package]] name = "click" version = "8.1.7" @@ -591,6 +492,51 @@ files = [ {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.32" @@ -1220,6 +1166,20 @@ 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" @@ -1279,27 +1239,6 @@ files = [ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] -[[package]] -name = "requests" -version = "2.31.0" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.7" -files = [ - {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, - {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - [[package]] name = "rsa" version = "4.9" @@ -1314,6 +1253,32 @@ files = [ [package.dependencies] pyasn1 = ">=0.1.3" +[[package]] +name = "ruff" +version = "0.1.6" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.1.6-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:88b8cdf6abf98130991cbc9f6438f35f6e8d41a02622cc5ee130a02a0ed28703"}, + {file = "ruff-0.1.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5c549ed437680b6105a1299d2cd30e4964211606eeb48a0ff7a93ef70b902248"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cf5f701062e294f2167e66d11b092bba7af6a057668ed618a9253e1e90cfd76"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:05991ee20d4ac4bb78385360c684e4b417edd971030ab12a4fbd075ff535050e"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87455a0c1f739b3c069e2f4c43b66479a54dea0276dd5d4d67b091265f6fd1dc"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:683aa5bdda5a48cb8266fcde8eea2a6af4e5700a392c56ea5fb5f0d4bfdc0240"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:137852105586dcbf80c1717facb6781555c4e99f520c9c827bd414fac67ddfb6"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd98138a98d48a1c36c394fd6b84cd943ac92a08278aa8ac8c0fdefcf7138f35"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a0cd909d25f227ac5c36d4e7e681577275fb74ba3b11d288aff7ec47e3ae745"}, + {file = "ruff-0.1.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8fd1c62a47aa88a02707b5dd20c5ff20d035d634aa74826b42a1da77861b5ff"}, + {file = "ruff-0.1.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fd89b45d374935829134a082617954120d7a1470a9f0ec0e7f3ead983edc48cc"}, + {file = "ruff-0.1.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:491262006e92f825b145cd1e52948073c56560243b55fb3b4ecb142f6f0e9543"}, + {file = "ruff-0.1.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ea284789861b8b5ca9d5443591a92a397ac183d4351882ab52f6296b4fdd5462"}, + {file = "ruff-0.1.6-py3-none-win32.whl", hash = "sha256:1610e14750826dfc207ccbcdd7331b6bd285607d4181df9c1c6ae26646d6848a"}, + {file = "ruff-0.1.6-py3-none-win_amd64.whl", hash = "sha256:4558b3e178145491e9bc3b2ee3c4b42f19d19384eaa5c59d10acf6e8f8b57e33"}, + {file = "ruff-0.1.6-py3-none-win_arm64.whl", hash = "sha256:03910e81df0d8db0e30050725a5802441c2022ea3ae4fe0609b76081731accbc"}, + {file = "ruff-0.1.6.tar.gz", hash = "sha256:1b09f29b16c6ead5ea6b097ef2764b42372aebe363722f1605ecbcd2b9207184"}, +] + [[package]] name = "setuptools" version = "69.0.2" @@ -1513,22 +1478,6 @@ files = [ {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, ] -[[package]] -name = "urllib3" -version = "2.1.0" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.8" -files = [ - {file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"}, - {file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"}, -] - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] - [[package]] name = "uvicorn" version = "0.24.0.post1" @@ -1582,4 +1531,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "c18cf5649aa1d852de2a5bd83ae6525ad63ce09d23da9a9e830ac93c7a27f57a" +content-hash = "c331f57027f61a9d00ff7475b39d47243df7c47dbf450b069bf206f135650ecb" diff --git a/pyproject.toml b/pyproject.toml index 1880ea2..8c6ff2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,14 +2,14 @@ name = "battlechess" version = "0.1.0" description = "chess with fog of war" -authors = ["guyver2@github.com", "quimnuss@github.com"] +authors = ["Antoine ", "Quim Nuss "] license = "GPLv3" readme = "README.md" [tool.poetry.dependencies] python = "^3.8" -requests = "^2.31.0" pathlib = "^1.0.1" +httpx = "^0.25.2" [tool.poetry.group.standalone.dependencies] @@ -22,6 +22,7 @@ sqlalchemy = "^2.0.23" passlib = {extras = ["bcrypt"], version = "^1.7.4"} python-jose = {extras = ["cryptography"], version = "^3.3.0"} uvicorn = "^0.24.0.post1" +python-multipart = "^0.0.6" [tool.poetry.group.dev.dependencies] @@ -30,6 +31,7 @@ black = "^23.11.0" pre-commit = "^3.5.0" ipdb = "^0.13.13" isort = "^5.12.0" +ruff = "^0.1.6" [build-system] requires = ["poetry-core"] 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.py b/server.py index 7029fec..bc33ad9 100644 --- a/server.py +++ b/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/tests/core/test_Board.py b/tests/core/test_Board.py new file mode 100644 index 0000000..ec55eae --- /dev/null +++ b/tests/core/test_Board.py @@ -0,0 +1,125 @@ +import unittest +import unittest.mock as mock + +from battlechess.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) diff --git a/tests/core/test_btchBoard.py b/tests/core/test_btchBoard.py new file mode 100644 index 0000000..390bfc4 --- /dev/null +++ b/tests/core/test_btchBoard.py @@ -0,0 +1,175 @@ +import unittest +import unittest.mock as mock + +from battlechess.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/test_btchApi.py b/tests/server/test_btchApi.py similarity index 60% rename from server/test_btchApi.py rename to tests/server/test_btchApi.py index dae1614..8a6c3ae 100644 --- a/server/test_btchApi.py +++ b/tests/server/test_btchApi.py @@ -1,31 +1,28 @@ +import sys import unittest import unittest.mock as mock from datetime import datetime, timedelta, timezone +from pathlib import Path 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') + 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 fastapi.testclient import TestClient +from jose import JWTError, jwt -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 +from battlechess.server import crud, models +from battlechess.server.btchApi import app, get_db +from battlechess.server.btchApiDB import Base, BtchDBContextManager, SessionLocal +from battlechess.server.schemas import GameStatus +from battlechess.server.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 @@ -56,15 +53,17 @@ 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}) + 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 + ) @classmethod def override_get_db(cls): @@ -115,7 +114,7 @@ def fakeusersdb(self): "disabled": False, "avatar": None, "created_at": datetime(2021, 1, 1, tzinfo=timezone.utc), - } + }, } return fake_users_db @@ -169,42 +168,49 @@ def fakegamesdb(self): "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), - }] + 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): @@ -213,7 +219,8 @@ def addFakeUsers(self, db): username=user["username"], full_name=user["full_name"], email=user["email"], - hashed_password=user["hashed_password"]) + hashed_password=user["hashed_password"], + ) db.add(db_user) db.commit() firstusername, _ = self.fakeusersdb().keys() @@ -221,9 +228,21 @@ def addFakeUsers(self, db): 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() + 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"], @@ -233,7 +252,8 @@ def addFakeGames(self, db, fakegamesdb): status=game["status"], last_move_time=None, turn=game.get("turn", None), - public=game["public"]) + public=game["public"], + ) print(db_game.__dict__) db.add(db_game) db.commit() @@ -284,7 +304,8 @@ def addCustomGameSnap(self, db, boardStr, move): def getToken(self, username): return crud.create_access_token( - data={"sub": username}, expires_delta=timedelta(minutes=3000)) + data={"sub": username}, expires_delta=timedelta(minutes=3000) + ) def classicSetup(self): token, _ = self.addFakeUsers(self.db) @@ -307,7 +328,7 @@ def test__createUser(self): "username": "alice", "full_name": "Alice la Suisse", "email": "alice@lasuisse.ch", - "plain_password": "secret" + "plain_password": "secret", }, ) @@ -319,7 +340,8 @@ def test__createUser(self): self.assertTrue(verify_password("secret", response_dict["hashed_password"])) response_dict.pop("hashed_password", None) self.assertDictEqual( - response_dict, { + response_dict, + { "username": "alice", "created_at": mock.ANY, "full_name": "Alice la Suisse", @@ -327,7 +349,8 @@ def test__createUser(self): "id": 1, "avatar": None, "status": "active", - }) + }, + ) def test__create_user__with_avatar(self): hashed_password = get_password_hash("secret") @@ -339,14 +362,14 @@ def test__create_user__with_avatar(self): "full_name": "Alice la Suisse", "email": "alice@lasuisse.ch", "avatar": new_avatar, - "plain_password": "secret" + "plain_password": "secret", }, ) print(response.json()) self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()['avatar'], new_avatar) + self.assertEqual(response.json()["avatar"], new_avatar) # TODO fix the put method for user def _test__update_user__full_name(self): @@ -357,48 +380,52 @@ def _test__update_user__full_name(self): new_full_name = "Alicia la catalana" response = self.client.put( - f'/users/update', + f"/users/update", headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', + "Authorization": "Bearer " + token, + "Content-Type": "application/json", }, json={ "username": oneUser.username, "full_name": new_full_name, "email": oneUser.email, - "avatar": oneUser.avatar - }) + "avatar": oneUser.avatar, + }, + ) print(response.json()) self.assertEqual(response.status_code, 200) self.assertDictEqual( - response.json(), { + response.json(), + { "username": "alice", "full_name": "Alice la Suisse", "email": "alice@lasuisse.ch", "avatar": new_full_name, - "plain_password": "secret" - }) + "plain_password": "secret", + }, + ) - @unittest.skipIf('PIL' not in sys.modules, reason="PIL module is not installed") + @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: + with open(filename, "rb") as f: img = Image.open(f) try: img.verify() except (IOError, SyntaxError) as e: - print('Bad file:', filename) + print("Bad file:", filename) # TODO reset cursor instead of reopening - with open(filename, 'rb') as f: + with open(filename, "rb") as f: response = self.client.put( - f'/users/u/{oneUser.id}/avatar', - headers={'Authorization': 'Bearer ' + token}, - files={'file': f}) + f"/users/u/{oneUser.id}/avatar", + headers={"Authorization": "Bearer " + token}, + files={"file": f}, + ) print(response.json()) self.assertEqual(response.status_code, 200) @@ -410,17 +437,18 @@ def test__upload_user__avatarImage(self): # 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") + @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') + 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()}) + f"/users/u/{oneUser.id}/avatar", + headers={"Authorization": "Bearer " + token}, + files={"file": img.tobytes()}, + ) print(response.json()) self.assertEqual(response.status_code, 422) @@ -453,7 +481,9 @@ def test__authenticate(self): ) self.assertEqual(response.status_code, 200) - self.assertListEqual(list(response.json().keys()), ['access_token', 'token_type']) + self.assertListEqual( + list(response.json().keys()), ["access_token", "token_type"] + ) def test__createUser__persistence(self): response = self.client.post( @@ -462,13 +492,13 @@ def test__createUser__persistence(self): "username": "alice", "full_name": "Alice la Suisse", "email": "alice@lasuisse.ch", - "plain_password": "secret" + "plain_password": "secret", }, ) response = self.client.post( "/token", - headers={'Content-Type': 'application/x-www-form-urlencoded'}, + headers={"Content-Type": "application/x-www-form-urlencoded"}, data={ "username": "alice", "password": "secret", @@ -477,8 +507,8 @@ def test__createUser__persistence(self): self.assertIsNotNone(response.json()) print(response.json()) - self.assertIn('access_token', response.json()) - token = response.json()['access_token'] + self.assertIn("access_token", response.json()) + token = response.json()["access_token"] response = self.client.get( "/users/usernames", @@ -514,14 +544,16 @@ def test__getUserById(self): 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', - }) + 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) @@ -533,7 +565,7 @@ def test__getUserById__malformedId(self): self.assertEqual(response.status_code, 422) print(response.json()) - self.assertEqual(response.json()['detail'][0]['type'], 'type_error.integer') + self.assertEqual(response.json()["detail"][0]["type"], "type_error.integer") def test__db_cleanup(self): @@ -545,76 +577,77 @@ def test__createGame(self): token, _ = self.addFakeUsers(self.db) response = self.client.post( - '/games/', + "/games/", headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', - }, - json={ - 'public': False, - 'color': 'white' + "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, - }) + 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}', + f"/games/{uuid}", headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', + "Authorization": "Bearer " + token, + "Content-Type": "application/json", }, json={ - 'random': False, + "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, - }) + 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/', + f"/users/me/games/", headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', + "Authorization": "Bearer " + token, + "Content-Type": "application/json", }, ) @@ -622,42 +655,44 @@ def test__get_me_games(self): 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, - }) + 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 + # 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', + "/users/me/games", headers={ - 'Authorization': 'Bearer ' + jane_token, - 'Content-Type': 'application/json', + "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']) + 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): @@ -667,10 +702,10 @@ def test__joinRandomGame(self): oneUser = self.db.query(models.User)[1] response = self.client.patch( - '/games', + "/games", headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', + "Authorization": "Bearer " + token, + "Content-Type": "application/json", }, ) @@ -679,38 +714,44 @@ def test__joinRandomGame(self): 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.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) + 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' + gamesdbmod["123fr12339"]["status"] = "done" + gamesdbmod["da40a3ee5e"]["status"] = "done" uuid = self.addFakeGames(self.db, gamesdbmod) response = self.client.patch( - '/games', + "/games", headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', + "Authorization": "Bearer " + token, + "Content-Type": "application/json", }, ) @@ -723,57 +764,65 @@ def test__listAvailableGames(self): uuid = self.addFakeGames(self.db, self.fakegamesdb()) response = self.client.get( - '/games', + "/games", headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', + "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, - }]) + 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'] + 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() + game_before = ( + self.db.query(models.Game).filter(models.Game.uuid == uuid).first() + ) response = self.client.get( - f'/games/{uuid}/join', + f"/games/{uuid}/join", headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', + "Authorization": "Bearer " + token, + "Content-Type": "application/json", }, ) @@ -786,18 +835,20 @@ def test__joinGame__playerAlreadyInGame(self): 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, - }) + 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) @@ -805,18 +856,22 @@ def test__joinGame__playerAlreadyInGame(self): user = crud.get_user_by_username(self.db, username) - game_before = self.db.query(models.Game).filter(models.Game.uuid == uuid).first() + game_before = ( + self.db.query(models.Game).filter(models.Game.uuid == uuid).first() + ) response = self.client.get( - f'/games/{uuid}/join', + f"/games/{uuid}/join", headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', + "Authorization": "Bearer " + token, + "Content-Type": "application/json", }, ) self.assertEqual(response.status_code, 409) - self.assertDictEqual(response.json(), {'detail': 'Player is already in this game'}) + self.assertDictEqual( + response.json(), {"detail": "Player is already in this game"} + ) def test__getsnap__byNum(self): firstgame_uuid, token = self.classicSetup() @@ -825,16 +880,16 @@ def test__getsnap__byNum(self): self.addFakeGameSnaps(self.db, self.fakegamesnapsdb()) response = self.client.get( - f'/games/{firstgame_uuid}/snap/0', + f"/games/{firstgame_uuid}/snap/0", headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', + "Authorization": "Bearer " + token, + "Content-Type": "application/json", }, ) print(response.json()) self.assertEqual(response.status_code, 200) - #yapf: disable + # yapf: disable self.assertDictEqual( response.json(), { 'game_id': 1, @@ -853,23 +908,23 @@ def test__getsnap__byNum(self): 'pppppppp' 'rnbqkbnr'), }) - #yapf: enable + # yapf: enable def test__getsnaps(self): self.maxDiff = None firstgame_uuid, token = self.classicSetup() response = self.client.get( - f'/games/{firstgame_uuid}/snaps', + f"/games/{firstgame_uuid}/snaps", headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', + "Authorization": "Bearer " + token, + "Content-Type": "application/json", }, ) print(response.json()) self.assertEqual(response.status_code, 200) - #yapf: disable + # yapf: disable self.assertListEqual(response.json(), [{ 'game_id': 1, 'created_at': mock.ANY, @@ -903,22 +958,22 @@ def test__getsnaps(self): 'ppp_pppp' 'rnbqkbnr'), }]) - #yapf: enable + # yapf: enable def test__getsnap__latest(self): firstgame_uuid, token = self.classicSetup() response = self.client.get( - f'/games/{firstgame_uuid}/snap', + f"/games/{firstgame_uuid}/snap", headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', + "Authorization": "Bearer " + token, + "Content-Type": "application/json", }, ) print(response.json()) self.assertEqual(response.status_code, 200) - #yapf: disable + # yapf: disable self.assertDictEqual( response.json(), { 'game_id': 1, @@ -937,35 +992,39 @@ def test__getsnap__latest(self): 'ppp_pppp' 'rnbqkbnr') }) - #yapf: enable + # yapf: enable def test__getTurn(self): firstgame_uuid, token = self.classicSetup() response = self.client.get( - f'/games/{firstgame_uuid}/turn', + f"/games/{firstgame_uuid}/turn", headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', + "Authorization": "Bearer " + token, + "Content-Type": "application/json", }, ) print(response.json()) self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), 'black') + 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() + game_before = ( + self.db.query(models.Game) + .filter(models.Game.uuid == firstgame_uuid) + .first() + ) response = self.client.post( - f'/games/{firstgame_uuid}/move', + f"/games/{firstgame_uuid}/move", headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', + "Authorization": "Bearer " + token, + "Content-Type": "application/json", }, json={ "move": "d7d5", @@ -974,7 +1033,11 @@ def test__move(self): # get after game/board - game_after = self.db.query(models.Game).filter(models.Game.uuid == firstgame_uuid).first() + 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]) @@ -985,26 +1048,28 @@ def test__move(self): self.assertEqual( game_before.get_latest_snap().board, - ('RNBQKBNR' - 'PPP_PPPP' - '________' - '___P____' - '___p____' - '________' - 'ppp_pppp' - 'rnbqkbnr'), + ( + "RNBQKBNR" + "PPP_PPPP" + "________" + "___P____" + "___p____" + "________" + "ppp_pppp" + "rnbqkbnr" + ), ) def test__move__filtered(self): firstgame_uuid, _ = self.classicSetup() - #change to second player + # change to second player token = self.getToken("janedoe") response = self.client.post( - f'/games/{firstgame_uuid}/move', + f"/games/{firstgame_uuid}/move", headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', + "Authorization": "Bearer " + token, + "Content-Type": "application/json", }, json={ "move": "d7d5", @@ -1015,29 +1080,31 @@ def test__move__filtered(self): self.assertEqual(response.status_code, 200) self.assertEqual( - response.json()['board'], - ('RNBQKBNR' - 'PPP_PPPP' - '________' - '___P____' - '___p____' - '________' - 'XXX_XXXX' - 'XXXXXXXX'), + response.json()["board"], + ( + "RNBQKBNR" + "PPP_PPPP" + "________" + "___P____" + "___p____" + "________" + "XXX_XXXX" + "XXXXXXXX" + ), ) def test__possibleMoves__pawnMove(self): firstgame_uuid, _ = self.classicSetup() - #change to second player + # change to second player token = self.getToken("janedoe") - square = 'd7' + square = "d7" response = self.client.get( - f'/games/{firstgame_uuid}/moves/{square}', + f"/games/{firstgame_uuid}/moves/{square}", headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', + "Authorization": "Bearer " + token, + "Content-Type": "application/json", }, ) @@ -1046,31 +1113,33 @@ def test__possibleMoves__pawnMove(self): self.assertListEqual( response.json(), - ['d6', 'd5'], + ["d6", "d5"], ) def test__possibleMoves__king(self): firstgame_uuid, token = self.classicSetup() - move = 'g3f3' - boardStr = ('____K___' - '________' - '________' - '__p_____' - '________' - '____pk__' - '___P_pp_' - '________') + move = "g3f3" + boardStr = ( + "____K___" + "________" + "________" + "__p_____" + "________" + "____pk__" + "___P_pp_" + "________" + ) self.addCustomGameSnap(self.db, boardStr, move) - square = 'f3' + square = "f3" response = self.client.get( - f'/games/{firstgame_uuid}/moves/{square}', + f"/games/{firstgame_uuid}/moves/{square}", headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', + "Authorization": "Bearer " + token, + "Content-Type": "application/json", }, ) @@ -1079,7 +1148,7 @@ def test__possibleMoves__king(self): self.assertListEqual( response.json(), - ['e4', 'f4', 'g4', 'g3', 'e2'], + ["e4", "f4", "g4", "g3", "e2"], ) def test__possibleMoves__pawn_enpassant_black(self): @@ -1087,25 +1156,27 @@ def test__possibleMoves__pawn_enpassant_black(self): token = self.getToken("janedoe") - move = 'c2c4' - boardStr = ('____K___' - '________' - '________' - '________' - '__pP____' - '____pk__' - '_____pp_' - '________') + move = "c2c4" + boardStr = ( + "____K___" + "________" + "________" + "________" + "__pP____" + "____pk__" + "_____pp_" + "________" + ) self.addCustomGameSnap(self.db, boardStr, move) - square = 'd4' + square = "d4" response = self.client.get( - f'/games/{firstgame_uuid}/moves/{square}', + f"/games/{firstgame_uuid}/moves/{square}", headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', + "Authorization": "Bearer " + token, + "Content-Type": "application/json", }, ) @@ -1114,31 +1185,33 @@ def test__possibleMoves__pawn_enpassant_black(self): self.assertListEqual( response.json(), - ['c3', 'd3', 'e3'], + ["c3", "d3", "e3"], ) def test__possibleMoves__pawn_enpassant_white(self): firstgame_uuid, token = self.classicSetup() - move = 'd7d5' - boardStr = ('____K___' - '________' - '________' - '__pP____' - '________' - '____pk__' - '_____pp_' - '________') + move = "d7d5" + boardStr = ( + "____K___" + "________" + "________" + "__pP____" + "________" + "____pk__" + "_____pp_" + "________" + ) self.addCustomGameSnap(self.db, boardStr, move) - square = 'c5' + square = "c5" response = self.client.get( - f'/games/{firstgame_uuid}/moves/{square}', + f"/games/{firstgame_uuid}/moves/{square}", headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', + "Authorization": "Bearer " + token, + "Content-Type": "application/json", }, ) @@ -1147,7 +1220,7 @@ def test__possibleMoves__pawn_enpassant_white(self): self.assertListEqual( response.json(), - ['c6', 'd6'], + ["c6", "d6"], ) def test__possibleMoves__pawn_impossible_enpassant_black(self): @@ -1155,25 +1228,27 @@ def test__possibleMoves__pawn_impossible_enpassant_black(self): token = self.getToken("janedoe") - move = 'c7c5' - boardStr = ('____K___' - '________' - '________' - '__pP____' - '________' - '____pk__' - '_____pp_' - '________') + move = "c7c5" + boardStr = ( + "____K___" + "________" + "________" + "__pP____" + "________" + "____pk__" + "_____pp_" + "________" + ) self.addCustomGameSnap(self.db, boardStr, move) - square = 'd5' + square = "d5" response = self.client.get( - f'/games/{firstgame_uuid}/moves/{square}', + f"/games/{firstgame_uuid}/moves/{square}", headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', + "Authorization": "Bearer " + token, + "Content-Type": "application/json", }, ) @@ -1182,31 +1257,33 @@ def test__possibleMoves__pawn_impossible_enpassant_black(self): self.assertListEqual( response.json(), - ['d4'], + ["d4"], ) def test__possibleMoves__pawn_take(self): firstgame_uuid, token = self.classicSetup() - move = 'f5f6' - boardStr = ('____K___' - '_____PP_' - '_____p__' - '________' - '________' - '_____k__' - '_____pp_' - '________') + move = "f5f6" + boardStr = ( + "____K___" + "_____PP_" + "_____p__" + "________" + "________" + "_____k__" + "_____pp_" + "________" + ) self.addCustomGameSnap(self.db, boardStr, move) - square = 'f6' + square = "f6" response = self.client.get( - f'/games/{firstgame_uuid}/moves/{square}', + f"/games/{firstgame_uuid}/moves/{square}", headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', + "Authorization": "Bearer " + token, + "Content-Type": "application/json", }, ) @@ -1215,15 +1292,15 @@ def test__possibleMoves__pawn_take(self): self.assertListEqual( response.json(), - ['g7'], + ["g7"], ) def send_move(self, game_uuid, move, token): response = self.client.post( - f'/games/{game_uuid}/move', + f"/games/{game_uuid}/move", headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', + "Authorization": "Bearer " + token, + "Content-Type": "application/json", }, json={ "move": move, @@ -1232,14 +1309,14 @@ def send_move(self, game_uuid, move, token): return response def prettyBoard(self, boardStr): - print(' abcdefgh') - print(' 01234567') + print(" abcdefgh") + print(" 01234567") for i in range(8): - print('{} - {} - {}'.format(i, boardStr[8 * i:8 * i + 8], 8 - i)) + 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 + # change to second player jane_token = self.getToken("janedoe") john_token = self.getToken("johndoe") self.addFakeGames(self.db, self.fakegamesdb()) @@ -1247,52 +1324,56 @@ def test__move__filtered_pawn(self): self.addFakeGameSnaps(self.db, self.fakegamesnapsdb()) tokens = [jane_token, john_token] - moves = ['e7e5', 'd4e5', 'h7h6', 'e5e6'] + 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.prettyBoard(response.json()["board"]) self.assertEqual(response.status_code, 200) self.assertEqual( - response.json()['board'], - ('RNBQKBNR' - 'PPPP_PPP' - '________' - '____P___' - '___p____' - '________' - 'XXX_XXXX' - 'XXXXXXXX'), + 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.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.prettyBoard(response.json()["board"]) self.assertEqual(response.status_code, 200) self.assertEqual( - response.json()['board'], - ('RNBQKBNR' - 'PPPP_PP_' - '_______P' - '____X___' - '________' - '________' - 'XXX_XXXX' - 'XXXXXXXX'), + 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 + # change to second player jane_token = self.getToken("janedoe") john_token = self.getToken("johndoe") self.addFakeGames(self.db, self.fakegamesdb()) @@ -1300,7 +1381,7 @@ def test__move__fogTest(self): self.addFakeGameSnaps(self.db, self.fakegamesnapsdb()) tokens = [jane_token, john_token] - moves = ['e7e6', 'g2g4', 'd8h4', 'f2f4', 'a7a6'] + moves = ["e7e6", "g2g4", "d8h4", "f2f4", "a7a6"] print(tokens) @@ -1309,18 +1390,20 @@ def test__move__fogTest(self): 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.prettyBoard(response.json()["board"]) self.assertEqual( - response.json()['board'], - ('RNB_KBNR' - '_PPP_PPP' - 'P___P___' - '________' - '___X_XpQ' - '________' - 'XXX_X__X' - 'XXXXXXXX'), + response.json()["board"], + ( + "RNB_KBNR" + "_PPP_PPP" + "P___P___" + "________" + "___X_XpQ" + "________" + "XXX_X__X" + "XXXXXXXX" + ), ) def test__integrationTest__foolscheckmate(self): @@ -1334,11 +1417,11 @@ def test__integrationTest__foolscheckmate(self): "username": "johndoe", "full_name": "John Le Dow", "email": "john@doe.cat", - "plain_password": "secret" + "plain_password": "secret", }, ) - john_id = response.json()['id'] + john_id = response.json()["id"] self.assertEqual(response.status_code, 200) @@ -1348,11 +1431,11 @@ def test__integrationTest__foolscheckmate(self): "username": "janedoe", "full_name": "Jane Le Dow", "email": "jane@doe.cat", - "plain_password": "secret" + "plain_password": "secret", }, ) - jane_id = response.json()['id'] + jane_id = response.json()["id"] self.assertEqual(response.status_code, 200) @@ -1367,7 +1450,7 @@ def test__integrationTest__foolscheckmate(self): ) self.assertEqual(response.status_code, 200) - john_token = response.json()['access_token'] + john_token = response.json()["access_token"] response = self.client.post( "/token", @@ -1379,53 +1462,60 @@ def test__integrationTest__foolscheckmate(self): ) self.assertEqual(response.status_code, 200) - jane_token = response.json()['access_token'] + jane_token = response.json()["access_token"] # john create game response = self.client.post( - '/games/', + "/games/", headers={ - 'Authorization': 'Bearer ' + john_token, - 'Content-Type': 'application/json', + "Authorization": "Bearer " + john_token, + "Content-Type": "application/json", }, json={ - 'public': False, - 'color': 'white', + "public": False, + "color": "white", }, ) self.assertEqual(response.status_code, 200) - game_uuid = response.json()['uuid'] + game_uuid = response.json()["uuid"] # john already joined and jane joins game # check if game started response = self.client.get( - f'/games/{game_uuid}', + f"/games/{game_uuid}", headers={ - 'Authorization': 'Bearer ' + jane_token, - 'Content-Type': 'application/json', + "Authorization": "Bearer " + jane_token, + "Content-Type": "application/json", }, ) self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()['status'], GameStatus.WAITING) + 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' + 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}') + print(f"jane color is {jane_color}") response = self.client.get( - f'/games/{game_uuid}/join', + f"/games/{game_uuid}/join", headers={ - 'Authorization': 'Bearer ' + jane_token, - 'Content-Type': 'application/json', + "Authorization": "Bearer " + jane_token, + "Content-Type": "application/json", }, ) @@ -1436,47 +1526,61 @@ def test__integrationTest__foolscheckmate(self): # john send move # jane send move - moves = ['f2f3', 'e7e5', 'g2g4', 'd8h4', 'f3f4', 'h4e1'] + 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'), + ( + "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}') + 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', + f"/games/{game_uuid}/turn", headers={ - 'Authorization': 'Bearer ' + jane_token, - 'Content-Type': 'application/json', + "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', + f"/games/{game_uuid}/turn", headers={ - 'Authorization': 'Bearer ' + john_token, - 'Content-Type': 'application/json', + "Authorization": "Bearer " + john_token, + "Content-Type": "application/json", }, ) @@ -1487,48 +1591,52 @@ def test__integrationTest__foolscheckmate(self): self.assertEqual(jane_turn, john_turn) response = self.client.get( - f'/games/{game_uuid}/snap', + f"/games/{game_uuid}/snap", headers={ - 'Authorization': 'Bearer ' + jane_token, - 'Content-Type': 'application/json', + "Authorization": "Bearer " + jane_token, + "Content-Type": "application/json", }, ) - print(self.prettyBoard(response.json()['board'])) + 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]) + # 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]) + self.assertEqual(response.json()["board"], boards[i][1]) response = self.client.get( - f'/games/{game_uuid}/snap', + f"/games/{game_uuid}/snap", headers={ - 'Authorization': 'Bearer ' + john_token, - 'Content-Type': 'application/json', + "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]) + if john_color == "white": + self.assertEqual(response.json()["board"], boards[i][0]) else: - self.assertEqual(response.json()['board'], boards[i][1]) + self.assertEqual(response.json()["board"], boards[i][1]) # checkmate response = self.client.get( - f'/games/{game_uuid}', + f"/games/{game_uuid}", headers={ - 'Authorization': 'Bearer ' + jane_token, - 'Content-Type': 'application/json', + "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)) + print( + "{} with {} won the game".format( + "janedoe" if jane_color == response.json()["winner"] else "johndoe", + jane_color, + ) + ) - self.assertEqual(response.json()['winner'], 'black') + self.assertEqual(response.json()["winner"], "black") diff --git a/server/test_btchApi_autoplay.py b/tests/server/test_btchApi_autoplay.py similarity index 76% rename from server/test_btchApi_autoplay.py rename to tests/server/test_btchApi_autoplay.py index 916e0f7..850e832 100644 --- a/server/test_btchApi_autoplay.py +++ b/tests/server/test_btchApi_autoplay.py @@ -1,33 +1,32 @@ +import json import unittest import unittest.mock as mock from datetime import datetime, timedelta, timezone +from fastapi import HTTPException, status +from fastapi.testclient import TestClient 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 +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 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}) + 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 + ) @classmethod def override_get_db(cls): @@ -75,7 +74,7 @@ def fakeusersdb(self): "disabled": False, "avatar": None, "created_at": datetime(2021, 1, 1, tzinfo=timezone.utc), - } + }, } return fake_users_db @@ -95,22 +94,26 @@ def fakegamesdb(self): 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), - }] + 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): @@ -119,7 +122,8 @@ def addFakeUsers(self, db): username=user["username"], full_name=user["full_name"], email=user["email"], - hashed_password=user["hashed_password"]) + hashed_password=user["hashed_password"], + ) db.add(db_user) db.commit() firstusername, _ = self.fakeusersdb().keys() @@ -127,9 +131,21 @@ def addFakeUsers(self, db): 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() + 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"], @@ -139,7 +155,8 @@ def addFakeGames(self, db, fakegamesdb): status=game["status"], last_move_time=None, turn=game.get("turn", None), - public=game["public"]) + public=game["public"], + ) print(db_game.__dict__) db.add(db_game) db.commit() @@ -173,7 +190,8 @@ def addFakeGameSnaps(self, db, fakegamesnaps): def getToken(self, username): return crud.create_access_token( - data={"sub": username}, expires_delta=timedelta(minutes=3000)) + data={"sub": username}, expires_delta=timedelta(minutes=3000) + ) def test__version(self): response = self.client.get("/version") @@ -182,10 +200,10 @@ def test__version(self): def send_move(self, game_uuid, move, token): response = self.client.post( - f'/games/{game_uuid}/move', + f"/games/{game_uuid}/move", headers={ - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', + "Authorization": "Bearer " + token, + "Content-Type": "application/json", }, json={ "move": move, @@ -194,10 +212,10 @@ def send_move(self, game_uuid, move, token): return response def prettyBoard(self, boardStr): - print(' abcdefgh') - print(' 01234567') + print(" abcdefgh") + print(" 01234567") for i in range(8): - print('{} - {} - {}'.format(i, boardStr[8 * i:8 * i + 8], 8 - i)) + 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() @@ -217,14 +235,14 @@ def test__move__MrExonGame__OneGame__EnPassant(self): # read games # for game in games: # for moves in game['moves'] - with open('ia/algebraic2icu/icu/mrexongames.txt') as json_file: + with open("ia/algebraic2icu/icu/mrexongames.txt") as json_file: data = json.load(json_file) - moves = data['https://lichess.org/auXLYNj1'] + 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']) + self.prettyBoard(response.json()["board"]) else: print(response.json()) self.assertEqual(response.status_code, 200) @@ -242,14 +260,14 @@ def test__move__MrExonGame__Enpassant2(self): # read games # for game in games: # for moves in game['moves'] - with open('ia/algebraic2icu/icu/mrexongames.txt') as json_file: + with open("ia/algebraic2icu/icu/mrexongames.txt") as json_file: data = json.load(json_file) - moves = data['https://lichess.org/X1Nk72xr'] + 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']) + self.prettyBoard(response.json()["board"]) else: print(response.json()) self.assertEqual(response.status_code, 200) @@ -268,15 +286,15 @@ def test__move__MrExonGames(self): # read games # for game in games: # for moves in game['moves'] - with open('ia/algebraic2icu/icu/mrexongames.txt') as json_file: + 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)) + 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']) + self.prettyBoard(response.json()["board"]) else: print(response.json()) self.assertEqual(response.status_code, 200) diff --git a/server/test_models.py b/tests/server/test_models.py similarity index 69% rename from server/test_models.py rename to tests/server/test_models.py index 59e7b32..4add6d4 100644 --- a/server/test_models.py +++ b/tests/server/test_models.py @@ -1,19 +1,17 @@ -from server.schemas import GameStatus import unittest +from datetime import datetime, timedelta, 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, BtchDBContextManager, SessionLocal +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 +20,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 +58,7 @@ def fakeusersdb(self): "status": "active", "avatar": None, "created_at": datetime(2021, 1, 1, tzinfo=timezone.utc), - } + }, } return fake_users_db @@ -67,14 +66,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 +105,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 +119,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 +172,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 +191,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 +200,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 +221,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) From 6dfe6436a6dffe0db08cd7260178393901c9aaf1 Mon Sep 17 00:00:00 2001 From: quimnuss Date: Fri, 24 Nov 2023 22:08:12 +0100 Subject: [PATCH 03/43] move battlechess standalone (v1) to another directory --- .../battleChess.py | 248 +++++++++--------- .../communication.py | 50 ++-- 2 files changed, 151 insertions(+), 147 deletions(-) rename battleChess.py => battlechess_standalone/battleChess.py (72%) rename communication.py => battlechess_standalone/communication.py (63%) diff --git a/battleChess.py b/battlechess_standalone/battleChess.py similarity index 72% rename from battleChess.py rename to battlechess_standalone/battleChess.py index 87cc39b..829755b 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 + +import pygame +from communication import recvData, sendData, waitForMessage from 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]) From a6ab642b966eb2a0abfa441653e3484d894bfd21 Mon Sep 17 00:00:00 2001 From: quimnuss Date: Fri, 24 Nov 2023 22:41:58 +0100 Subject: [PATCH 04/43] update readme --- README.md | 138 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..7cccb4b --- /dev/null +++ b/README.md @@ -0,0 +1,138 @@ + ___ _ _ _ ___ _ + ( _`\ ( )_ ( )_ (_ ) ( _`\ ( ) + | (_) ) _ _ | ,_)| ,_) | | __ | ( (_)| |__ __ ___ ___ + | _ <' /'_` )| | | | | | /'__`\| | _ | _ `\ /'__`\/',__)/',__) + | (_) )( (_| || |_ | |_ | | ( ___/| (_( )| | | |( ___/\__, \\__, \ + (____/'`\__,_)`\__)`\__)(___)`\____)(____/'(_) (_)`\____)(____/(____/ + + +--- + +[![Build Status](https://travis-ci.org/quimnuss/battlechess.svg?branch=backend)](https://travis-ci.org/quimnuss/battlechess) + +## Abstract +- Quick game : + `python battleChess.py` +- Download : + `git clone https://github.com/quimnuss/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 pawns which reaches the end of the board becomes a queen, you cannot choose, deal with it. + + +## 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) + * the game itself. It can be downloaded via git using the following command: + ```git clone http://git.sxbn.org/battleChess.git``` + +- Launching the game : + There is and will be a server app running on sxbn.org, so I recommend no changing the HOST and PORT parameters unless you want to host your own games. + 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 + + +- Replay mode : + You just got beaten and you don't know what just happened. Don't worry that happens a lot. Every game played are saved on the server and can be downloaded from this webpage : [http://sxbn.org/~antoine/games/](http://sxbn.org/~antoine/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://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` + 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` + + You can go to `localhost:8000/docs` for the documentation and usage examples. Create a user and then authenticate on the upper right with `johndoe` and `secret` + +- 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. + +- Tests: + You can use the `$ pytest` runner to run the tests from the root of the project. + +## 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 there, 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. + + +## Android & iOS Apps : + + +- Android .apk file can be downloaded from this link : [http://sxbn.org/~antoine/git/battlechess/BattleChess.apk](http://sxbn.org/~antoine/git/battlechess/BattleChess.apk) + +- iOS app is ready and working, but thanks to Apple's App Store terms we cannot release it without paying... + + +### Building the app + +If you are builind the app from source and it complains that cocos2dx doesn't have neon_matrix_impl.c, copy it from cocos2dmods/ to the directory where it is looking for it. + + +## Credits & Licence : + + +- Code : + Antoine Letouzey -- [antoine.letouzey@gmail.com](antoine.letouzey@gmail.com) + Pol Monsó-Purtí -- [pol.monso@gmail.com](pol.monso@gmail.com) + +- Sprites : + Original sprites by Wikipedia user Cburnett, under Creative Commons Licence (CC BY-SA 3.0) + +- Licence : GPL + +## Screenshots : +![img1](http://sxbn.org/~antoine/git/battlechess/1.jpg) +![img2](http://sxbn.org/~antoine/git/battlechess/2.jpg) +![img3](http://sxbn.org/~antoine/git/battlechess/3.jpg) +![img4](http://sxbn.org/~antoine/git/battlechess/4.jpg) +![img5](http://sxbn.org/~antoine/git/battlechess/5.jpg) + +## Screenshots Android & iOS App : + +

+ +

+ +![m2](https://user-images.githubusercontent.com/4179721/32983274-5a8e9868-cc92-11e7-94f0-cadc6b50e038.png) +![m3](https://user-images.githubusercontent.com/4179721/32983275-5ab8462c-cc92-11e7-8e4d-becde7cfe0da.png) +![m4](https://user-images.githubusercontent.com/4179721/32983276-5ad82ab4-cc92-11e7-98a3-419f788f1670.png) +![m5](https://user-images.githubusercontent.com/4179721/32983277-5afbe6f2-cc92-11e7-880c-8efd8ecb53db.png) + +

+ +

+ From 8e7d7e92319e53b29d3a43780e438497bfb078f4 Mon Sep 17 00:00:00 2001 From: quimnuss Date: Sat, 25 Nov 2023 00:01:46 +0100 Subject: [PATCH 05/43] apply ruff linting fixes --- battlechess/server/btchApi.py | 33 +++++---------- battlechess/server/crud.py | 17 ++------ battlechess/server/models.py | 10 ++--- battlechess/server/schemas.py | 4 +- test_communication.py | 8 ---- .../test_communication.py | 5 +++ tests/core/test_Board.py | 1 - tests/core/test_btchBoard.py | 1 - tests/server/test_btchApi.py | 42 ++++++++----------- tests/server/test_btchApi_autoplay.py | 4 +- tests/server/test_models.py | 5 +-- 11 files changed, 47 insertions(+), 83 deletions(-) delete mode 100644 test_communication.py create mode 100644 tests/battlechess_standalone/test_communication.py diff --git a/battlechess/server/btchApi.py b/battlechess/server/btchApi.py index e893f48..5381fb7 100644 --- a/battlechess/server/btchApi.py +++ b/battlechess/server/btchApi.py @@ -1,27 +1,16 @@ -from datetime import datetime, timedelta, timezone -from typing import List, Optional, Set, Tuple - -from fastapi import ( - Body, - Depends, - FastAPI, - File, - Header, - HTTPException, - UploadFile, - status, -) +from datetime import timedelta +from typing import List + +from fastapi import Depends, FastAPI, File, Header, HTTPException, 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.orm import Session -from sqlalchemy.sql.functions import user -from . import crud, models, schemas -from .btchApiDB import SessionLocal, engine -from .config import ACCESS_TOKEN_EXPIRE_MINUTES, ALGORITHM, SECRET_KEY -from .schemas import Game, GameStatus +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 @@ -154,7 +143,7 @@ def read_own_games( @app.get("/users/", response_model=List[schemas.User]) -def read_users( +def read_users_all( skip: int = 0, limit: int = 100, current_user: schemas.User = Depends(get_current_active_user), @@ -220,7 +209,7 @@ 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) @@ -494,7 +483,7 @@ def get_snap( @app.get("/games/{gameUUID}/snap/{moveNum}") -def get_snap( +def get_snap_by_move( gameUUID: str, moveNum: int, current_user: schemas.User = Depends(get_current_active_user), diff --git a/battlechess/server/crud.py b/battlechess/server/crud.py index 5b616ca..57099fe 100644 --- a/battlechess/server/crud.py +++ b/battlechess/server/crud.py @@ -2,14 +2,14 @@ import shutil from datetime import datetime, timedelta, timezone from pathlib import Path -from typing import Optional, Set, Tuple +from typing import Optional -from jose import JWTError, jwt +from jose import jwt from sqlalchemy import and_, or_ from sqlalchemy.orm import Session from . import models, schemas -from .config import ACCESS_TOKEN_EXPIRE_MINUTES, ALGORITHM, HANDLEBASEURL, SECRET_KEY +from .config import ALGORITHM, SECRET_KEY from .utils import defaultBoard, get_password_hash, get_random_string, verify_password @@ -169,7 +169,7 @@ def get_public_game_by_status(db: Session, user: schemas.User, status): models.Game.status == status, models.Game.white_id.is_not(user.id), models.Game.black_id.is_not(user.id), - models.Game.public == True, + models.Game.public is True, ) ) .all() @@ -240,15 +240,6 @@ def create_snap_by_move( 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 diff --git a/battlechess/server/models.py b/battlechess/server/models.py index 697830d..189033a 100644 --- a/battlechess/server/models.py +++ b/battlechess/server/models.py @@ -4,10 +4,9 @@ from battlechess.core.Board import Board from battlechess.core.btchBoard import BtchBoard - -from .btchApiDB import Base -from .schemas import GameStatus -from .utils import ad2extij, ad2ij, extij2ad +from battlechess.server.btchApiDB import Base +from battlechess.server.schemas import GameSnap, GameStatus +from battlechess.server.utils import ad2extij, ad2ij, extij2ad class User(Base): @@ -119,7 +118,7 @@ def refresh_turn(self): # 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 @@ -180,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() diff --git a/battlechess/server/schemas.py b/battlechess/server/schemas.py index a9e5d9b..7c622c3 100644 --- a/battlechess/server/schemas.py +++ b/battlechess/server/schemas.py @@ -1,5 +1,5 @@ -from datetime import datetime, time, timedelta -from typing import List, Optional, Tuple +from datetime import datetime +from typing import Optional, Tuple # from uuid import UUID # represented as string from pydantic import BaseModel 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 index ec55eae..737988a 100644 --- a/tests/core/test_Board.py +++ b/tests/core/test_Board.py @@ -1,5 +1,4 @@ import unittest -import unittest.mock as mock from battlechess.core.Board import Board diff --git a/tests/core/test_btchBoard.py b/tests/core/test_btchBoard.py index 390bfc4..7ac1534 100644 --- a/tests/core/test_btchBoard.py +++ b/tests/core/test_btchBoard.py @@ -1,5 +1,4 @@ import unittest -import unittest.mock as mock from battlechess.core.btchBoard import BtchBoard diff --git a/tests/server/test_btchApi.py b/tests/server/test_btchApi.py index 8a6c3ae..c84a3ca 100644 --- a/tests/server/test_btchApi.py +++ b/tests/server/test_btchApi.py @@ -5,7 +5,6 @@ from pathlib import Path from sqlalchemy import create_engine -from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker try: @@ -14,13 +13,11 @@ print("PIL module is not installed. Some tests will be skipped") -from fastapi import HTTPException, status from fastapi.testclient import TestClient -from jose import JWTError, jwt from battlechess.server import crud, models from battlechess.server.btchApi import app, get_db -from battlechess.server.btchApiDB import Base, BtchDBContextManager, SessionLocal +from battlechess.server.btchApiDB import Base from battlechess.server.schemas import GameStatus from battlechess.server.utils import get_password_hash, verify_password @@ -258,7 +255,7 @@ def addFakeGames(self, db, fakegamesdb): db.add(db_game) db.commit() # force None turn since db defaults to white on creation - if "turn" in game and game["turn"] == None: + if "turn" in game and game["turn"] is None: db_game.turn = None db.commit() print( @@ -321,7 +318,7 @@ def test__version(self): self.assertEqual(response.json(), {"version": "1.0"}) def test__createUser(self): - hashed_password = get_password_hash("secret") + get_password_hash("secret") response = self.client.post( "/users/", json={ @@ -353,7 +350,7 @@ def test__createUser(self): ) def test__create_user__with_avatar(self): - hashed_password = get_password_hash("secret") + get_password_hash("secret") new_avatar = "images/avatar001.jpeg" response = self.client.post( "/users/", @@ -380,7 +377,7 @@ def _test__update_user__full_name(self): new_full_name = "Alicia la catalana" response = self.client.put( - f"/users/update", + "/users/update", headers={ "Authorization": "Bearer " + token, "Content-Type": "application/json", @@ -416,7 +413,7 @@ def test__upload_user__avatarImage(self): img = Image.open(f) try: img.verify() - except (IOError, SyntaxError) as e: + except (IOError, SyntaxError): print("Bad file:", filename) # TODO reset cursor instead of reopening @@ -431,7 +428,7 @@ def test__upload_user__avatarImage(self): 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_filepath = expected_avatar_dir / "1_avatar.jpeg" expected_avatar_file = Path(expected_avatar_filepath) # remove the test file from the config directory @@ -524,7 +521,7 @@ def test__addFakeUsers(self): def test__getUsernames(self): token, _ = self.addFakeUsers(self.db) - users = self.db.query(models.User).all() + self.db.query(models.User).all() response = self.client.get( "/users/usernames", @@ -641,10 +638,10 @@ def test__get_game_by_uuid(self): def test__get_me_games(self): token, _ = self.addFakeUsers(self.db) - uuid = self.addFakeGames(self.db, self.fakegamesdb()) + self.addFakeGames(self.db, self.fakegamesdb()) response = self.client.get( - f"/users/me/games/", + "/users/me/games/", headers={ "Authorization": "Bearer " + token, "Content-Type": "application/json", @@ -664,7 +661,6 @@ def test__get_me_games(self): "last_move_time": None, "owner_id": 1, "public": False, - "last_move_time": None, "status": "started", "turn": "black", "white_id": 1, @@ -676,7 +672,7 @@ def test__getGames__finishedGame(self): _, _ = self.addFakeUsers(self.db) # change to second player jane_token = self.getToken("janedoe") - john_token = self.getToken("johndoe") + _ = self.getToken("johndoe") self.addFakeGames(self.db, self.fakegamesdb()) self.addFakeGameSnaps(self.db, self.fakegamesnapsdb()) @@ -697,7 +693,7 @@ def test__getGames__finishedGame(self): # 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()) + self.addFakeGames(self.db, self.fakegamesdb()) oneUser = self.db.query(models.User)[1] @@ -745,7 +741,7 @@ def test__joinRandomGame__noneAvailable(self): gamesdbmod = self.fakegamesdb() gamesdbmod["123fr12339"]["status"] = "done" gamesdbmod["da40a3ee5e"]["status"] = "done" - uuid = self.addFakeGames(self.db, gamesdbmod) + self.addFakeGames(self.db, gamesdbmod) response = self.client.patch( "/games", @@ -761,7 +757,7 @@ def test__joinRandomGame__noneAvailable(self): def test__listAvailableGames(self): token, _ = self.addFakeUsers(self.db) - uuid = self.addFakeGames(self.db, self.fakegamesdb()) + self.addFakeGames(self.db, self.fakegamesdb()) response = self.client.get( "/games", @@ -850,15 +846,13 @@ def test__joinGame__playerAlreadyInGame(self): }, ) - def test__joinGame__playerAlreadyInGame(self): + def test__joinGame__playerAlreadyInGame__simple(self): token, username = self.addFakeUsers(self.db) uuid = self.addFakeGames(self.db, self.fakegamesdb()) - user = crud.get_user_by_username(self.db, username) + crud.get_user_by_username(self.db, username) - game_before = ( - self.db.query(models.Game).filter(models.Game.uuid == uuid).first() - ) + (self.db.query(models.Game).filter(models.Game.uuid == uuid).first()) response = self.client.get( f"/games/{uuid}/join", @@ -1497,7 +1491,7 @@ def test__integrationTest__foolscheckmate(self): self.assertEqual(response.json()["status"], GameStatus.WAITING) white_id = response.json()["white_id"] - black_id = response.json()["black_id"] + response.json()["black_id"] jane_color = ( None if not white_id else "white" if white_id == jane_id else "black" ) diff --git a/tests/server/test_btchApi_autoplay.py b/tests/server/test_btchApi_autoplay.py index 850e832..d5fb60a 100644 --- a/tests/server/test_btchApi_autoplay.py +++ b/tests/server/test_btchApi_autoplay.py @@ -1,9 +1,7 @@ import json import unittest -import unittest.mock as mock from datetime import datetime, timedelta, timezone -from fastapi import HTTPException, status from fastapi.testclient import TestClient from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker @@ -161,7 +159,7 @@ def addFakeGames(self, db, fakegamesdb): db.add(db_game) db.commit() # force None turn since db defaults to white on creation - if "turn" in game and game["turn"] == None: + if "turn" in game and game["turn"] is None: db_game.turn = None db.commit() print( diff --git a/tests/server/test_models.py b/tests/server/test_models.py index 4add6d4..9592b91 100644 --- a/tests/server/test_models.py +++ b/tests/server/test_models.py @@ -1,13 +1,12 @@ import unittest -from datetime import datetime, timedelta, timezone +from datetime import datetime, timezone from sqlalchemy import create_engine -from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker from battlechess.core.Board import Board from battlechess.server import models -from battlechess.server.btchApiDB import Base, BtchDBContextManager, SessionLocal +from battlechess.server.btchApiDB import Base from battlechess.server.schemas import GameStatus From eca93d1a9fe5d6b17bc1cf32fce21b8ecb6a9b48 Mon Sep 17 00:00:00 2001 From: quimnuss Date: Sat, 25 Nov 2023 00:13:14 +0100 Subject: [PATCH 06/43] fix deprecation tests --- battlechess/server/btchApi.py | 8 ++++---- battlechess/server/schemas.py | 4 ++++ tests/server/test_btchApi.py | 4 ++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/battlechess/server/btchApi.py b/battlechess/server/btchApi.py index 5381fb7..f94fd87 100644 --- a/battlechess/server/btchApi.py +++ b/battlechess/server/btchApi.py @@ -397,7 +397,7 @@ def post_move( # 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 @@ -475,7 +475,7 @@ def get_snap( return snap player_color = "black" if current_user.id == game.black_id else "white" - snap4player = schemas.GameSnap.from_orm(snap) + snap4player = schemas.GameSnap.model_validate(snap) print(f"preparing board for {current_user.username} {player_color}") snap4player.prepare_for_player(player_color) @@ -510,7 +510,7 @@ def get_snap_by_move( ) player_color = "black" if current_user.id == game.black_id else "white" - snap4player = schemas.GameSnap.from_orm(snap) + snap4player = schemas.GameSnap.model_validate(snap) snap4player.prepare_for_player(player_color) return snap4player @@ -538,7 +538,7 @@ def get_snaps( result = [] for snap in game.snaps: - snap4player = schemas.GameSnap.from_orm(snap) + snap4player = schemas.GameSnap.model_validate(snap) snap4player.prepare_for_player(player_color) result.append(snap4player) return result diff --git a/battlechess/server/schemas.py b/battlechess/server/schemas.py index 7c622c3..60167a5 100644 --- a/battlechess/server/schemas.py +++ b/battlechess/server/schemas.py @@ -118,11 +118,13 @@ def prepare_for_player(self, player_color: str): class Config: orm_mode = True + from_attributes = True class FilteredGameSnap(GameSnap): class Config: orm_mode = False + from_attributes = True class GameMove(BaseModel): @@ -160,6 +162,7 @@ class Game(GameBase): class Config: orm_mode = True + from_attributes = True class Move(BaseModel): @@ -189,3 +192,4 @@ class User(UserBase): class Config: orm_mode = True + from_attributes = True diff --git a/tests/server/test_btchApi.py b/tests/server/test_btchApi.py index c84a3ca..7021932 100644 --- a/tests/server/test_btchApi.py +++ b/tests/server/test_btchApi.py @@ -562,7 +562,7 @@ def test__getUserById__malformedId(self): self.assertEqual(response.status_code, 422) print(response.json()) - self.assertEqual(response.json()["detail"][0]["type"], "type_error.integer") + self.assertEqual(response.json()["detail"][0]["type"], "int_parsing") def test__db_cleanup(self): @@ -612,7 +612,7 @@ def test__get_game_by_uuid(self): "Authorization": "Bearer " + token, "Content-Type": "application/json", }, - json={ + params={ "random": False, }, ) From 8ae8bb94adc27ba25628c65acbd0735102c15dc9 Mon Sep 17 00:00:00 2001 From: quimnuss Date: Sun, 26 Nov 2023 00:31:12 +0100 Subject: [PATCH 07/43] fix all tests and deprecations --- battlechess/server/btchApi.py | 3 ++- battlechess/server/btchApiDB.py | 3 +-- battlechess/server/crud.py | 2 +- battlechess/server/schemas.py | 17 ++++------------- tests/server/test_btchApi.py | 7 ++++--- 5 files changed, 12 insertions(+), 20 deletions(-) diff --git a/battlechess/server/btchApi.py b/battlechess/server/btchApi.py index f94fd87..363cc00 100644 --- a/battlechess/server/btchApi.py +++ b/battlechess/server/btchApi.py @@ -271,7 +271,8 @@ def post_new_game( current_user: schemas.User = Depends(get_current_active_user), db: Session = Depends(get_db), ): - return crud.create_game(db, current_user, new_game) + game = crud.create_game(db, current_user, new_game) + return game @app.get("/games/{gameUUID}") diff --git a/battlechess/server/btchApiDB.py b/battlechess/server/btchApiDB.py index eebd5cd..9af75e6 100644 --- a/battlechess/server/btchApiDB.py +++ b/battlechess/server/btchApiDB.py @@ -1,6 +1,5 @@ from sqlalchemy import create_engine -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import declarative_base, sessionmaker SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db" # SQLALCHEMY_DATABASE_URL = 'sqlite:///:memory:' # doesn't work diff --git a/battlechess/server/crud.py b/battlechess/server/crud.py index 57099fe..0a07428 100644 --- a/battlechess/server/crud.py +++ b/battlechess/server/crud.py @@ -169,7 +169,7 @@ def get_public_game_by_status(db: Session, user: schemas.User, status): models.Game.status == status, models.Game.white_id.is_not(user.id), models.Game.black_id.is_not(user.id), - models.Game.public is True, + bool(models.Game.public) is True, ) ) .all() diff --git a/battlechess/server/schemas.py b/battlechess/server/schemas.py index 60167a5..e02a062 100644 --- a/battlechess/server/schemas.py +++ b/battlechess/server/schemas.py @@ -33,6 +33,7 @@ class GameSnap(GameSnapBase): taken: str castleable: str move_number: int + model_config = {"from_attributes": True} def extendboard(self, board): return ( @@ -116,15 +117,9 @@ def prepare_for_player(self, player_color: str): 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 - from_attributes = True - class FilteredGameSnap(GameSnap): - class Config: - orm_mode = False - from_attributes = True + model_config = {"from_attributes": False} class GameMove(BaseModel): @@ -160,9 +155,7 @@ class Game(GameBase): winner: Optional[str] = None public: Optional[bool] = None - class Config: - orm_mode = True - from_attributes = True + model_config = {"from_attributes": True} class Move(BaseModel): @@ -190,6 +183,4 @@ class User(UserBase): # whites: List[Game] = [] # blacks: List[Game] = [] - class Config: - orm_mode = True - from_attributes = True + model_config = {"from_attributes": True} diff --git a/tests/server/test_btchApi.py b/tests/server/test_btchApi.py index 7021932..3c7b772 100644 --- a/tests/server/test_btchApi.py +++ b/tests/server/test_btchApi.py @@ -757,7 +757,7 @@ def test__joinRandomGame__noneAvailable(self): def test__listAvailableGames(self): token, _ = self.addFakeUsers(self.db) - self.addFakeGames(self.db, self.fakegamesdb()) + _ = self.addFakeGames(self.db, self.fakegamesdb()) response = self.client.get( "/games", @@ -802,11 +802,12 @@ def test__listAvailableGames(self): ], ) - def test__joinGame__playerAlreadyInGame(self): + # TODO this test was deactivated by mistake + def _test__joinGame__playerAlreadyInGame(self): token, username = self.addFakeUsers(self.db) self.addFakeGames(self.db, self.fakegamesdb()) - uuid = self.fakegamesdb["123fr12339"] + uuid = "123fr12339" user = crud.get_user_by_username(self.db, username) From 55fbc3acadb86ed188d57a4e0acd3d1e5a46b29a Mon Sep 17 00:00:00 2001 From: quimnuss Date: Sun, 26 Nov 2023 14:13:04 +0100 Subject: [PATCH 08/43] update github actions to use poetry --- .github/workflows/main.yml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b14380c..b6e1fd8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,19 +8,22 @@ 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: Run image + uses: abatilo/actions-poetry@v2 + with: + poetry-version: ${{ matrix.poetry-version }} - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install flake8 pytest - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + poetry install #- name: Lint with flake8 # run: | # # stop the build if there are Python syntax errors or undefined names From ac2fcef6c559287892bcbddb42e8bde4373b0afe Mon Sep 17 00:00:00 2001 From: quimnuss Date: Sun, 26 Nov 2023 14:15:39 +0100 Subject: [PATCH 09/43] github action use virtualenv --- .github/workflows/main.yml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b6e1fd8..685adbf 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,9 +21,17 @@ jobs: uses: abatilo/actions-poetry@v2 with: poetry-version: ${{ matrix.poetry-version }} - - name: Install dependencies + - name: Setup a local virtual environment (if no poetry.toml file) run: | - poetry install + 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 @@ -31,5 +39,4 @@ 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 From 230700c17312312cb9e27cfdd3eac7f5d6ecd67b Mon Sep 17 00:00:00 2001 From: quimnuss Date: Sun, 26 Nov 2023 19:31:11 +0100 Subject: [PATCH 10/43] use environment variables via decouple instead of config.py --- battlechess/server/config.py | 5 ++++- battlechess/server/crud.py | 11 ++++++++--- poetry.lock | 13 ++++++++++++- pyproject.toml | 1 + 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/battlechess/server/config.py b/battlechess/server/config.py index 5981505..1d85752 100644 --- a/battlechess/server/config.py +++ b/battlechess/server/config.py @@ -1,6 +1,9 @@ # to get a string like this run: # openssl rand -hex 32 -SECRET_KEY = "e909bb995546a0378161ed18d4e44ab4525d735e07a52cec2eb9b3a86d39ee61" + +from decouple import config + +SECRET_KEY = config("SECRET_KEY") ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 3000 HANDLEBASEURL = "https://bt.ch/" diff --git a/battlechess/server/crud.py b/battlechess/server/crud.py index 0a07428..31fa9f8 100644 --- a/battlechess/server/crud.py +++ b/battlechess/server/crud.py @@ -8,9 +8,14 @@ from sqlalchemy import and_, or_ from sqlalchemy.orm import Session -from . import models, schemas -from .config import ALGORITHM, SECRET_KEY -from .utils import defaultBoard, get_password_hash, get_random_string, verify_password +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, +) # TODO redo this. I hate myself for writing it. diff --git a/poetry.lock b/poetry.lock index 17a3bec..4df2679 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1144,6 +1144,17 @@ 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" @@ -1531,4 +1542,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "c331f57027f61a9d00ff7475b39d47243df7c47dbf450b069bf206f135650ecb" +content-hash = "f57d78c781f935bf7e568b5e6fe55dc7e4a1bcf522027d417da711d98b73e0a2" diff --git a/pyproject.toml b/pyproject.toml index 8c6ff2f..9c8158d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ readme = "README.md" python = "^3.8" pathlib = "^1.0.1" httpx = "^0.25.2" +python-decouple = "^3.8" [tool.poetry.group.standalone.dependencies] From eb488374f8518c72043a34ea4b56b8f5687afabf Mon Sep 17 00:00:00 2001 From: quimnuss Date: Sun, 26 Nov 2023 19:38:12 +0100 Subject: [PATCH 11/43] add environment secret to github action --- .github/workflows/main.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 685adbf..66b538d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -40,3 +40,6 @@ jobs: # flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with pytest run: poetry run pytest -v + env: + SECRET_KEY: ${{ secrets.SECRET_KEY }} + From 71315801517325a822d758fe85db268154922de3 Mon Sep 17 00:00:00 2001 From: quimnuss Date: Sun, 26 Nov 2023 19:47:22 +0100 Subject: [PATCH 12/43] upgrade requirements just in case, add example .env --- .gitignore | 2 ++ example.env | 6 ++++++ requirements.txt | 21 ++++++++++----------- 3 files changed, 18 insertions(+), 11 deletions(-) create mode 100644 example.env diff --git a/.gitignore b/.gitignore index 4c43c6e..5e05608 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ bin .vscode sql_app.db test.db + +.env diff --git a/example.env b/example.env new file mode 100644 index 0000000..c5f465c --- /dev/null +++ b/example.env @@ -0,0 +1,6 @@ +# to get a string like this run: +# openssl rand -hex 32 +SECRET_KEY = "e909bb995546a0378161ed18d4e44ab4525d735e07a52cec2eb9b3a86d39ee61" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 3000 +HANDLEBASEURL = "https://bt.ch/" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index cbe9c60..a3d5f2d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,10 @@ -requests -python-jose[cryptography] -fastapi -passlib[bcrypt] -uvicorn -python-multipart -pytest -pygame -sqlalchemy -pathlib -Pillow \ No newline at end of file +anyio==3.7.1 ; python_version >= "3.8" and python_version < "4.0" +certifi==2023.11.17 ; python_version >= "3.8" and python_version < "4.0" +exceptiongroup==1.2.0 ; python_version >= "3.8" and python_version < "3.11" +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.5 ; python_version >= "3.8" and python_version < "4.0" +pathlib==1.0.1 ; python_version >= "3.8" and python_version < "4.0" +python-decouple==3.8 ; python_version >= "3.8" and python_version < "4.0" +sniffio==1.3.0 ; python_version >= "3.8" and python_version < "4.0" From c797012f5eb7931b1d99a6a5754e3e80fd26545c Mon Sep 17 00:00:00 2001 From: Quim Date: Sun, 24 Dec 2023 11:55:45 +0100 Subject: [PATCH 13/43] chore: move all standalone code to directory, update readme --- README.md | 2 +- battlechess_standalone/battleChess.py | 4 ++-- server.py => battlechess_standalone/server.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename server.py => battlechess_standalone/server.py (98%) diff --git a/README.md b/README.md index 7cccb4b..fba0b7d 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ That new rule has some direct consequences on the gameplay. - 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/battlechess_standalone/battleChess.py b/battlechess_standalone/battleChess.py index 829755b..ebab6b0 100644 --- a/battlechess_standalone/battleChess.py +++ b/battlechess_standalone/battleChess.py @@ -10,8 +10,8 @@ import urllib.request import pygame -from communication import recvData, sendData, waitForMessage -from core.Board import Board +from .communication import recvData, sendData, waitForMessage +from battlechess.core.Board import Board from pygame.locals import * # GLOBAL VARIABLES diff --git a/server.py b/battlechess_standalone/server.py similarity index 98% rename from server.py rename to battlechess_standalone/server.py index bc33ad9..cd4d580 100644 --- a/server.py +++ b/battlechess_standalone/server.py @@ -7,7 +7,7 @@ import traceback from battlechess.core.Board import Board -from communication import recvData, sendData, waitForMessage +from .communication import recvData, sendData, waitForMessage class GameThread(threading.Thread): From 2daf23051cd5877774c254b4d959160effb13f9f Mon Sep 17 00:00:00 2001 From: Quim Date: Sat, 6 Jan 2024 23:36:57 +0100 Subject: [PATCH 14/43] change response to no random games to 404 --- battlechess/server/btchApi.py | 9 +++++++-- tests/server/test_btchApi.py | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/battlechess/server/btchApi.py b/battlechess/server/btchApi.py index 363cc00..9ae8e7a 100644 --- a/battlechess/server/btchApi.py +++ b/battlechess/server/btchApi.py @@ -265,7 +265,7 @@ def update_avatar_file( return {"filename": output} -@app.post("/games/") +@app.post("/games") def post_new_game( new_game: schemas.GameCreate, current_user: schemas.User = Depends(get_current_active_user), @@ -343,7 +343,12 @@ def join_random_game( ): 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) diff --git a/tests/server/test_btchApi.py b/tests/server/test_btchApi.py index 3c7b772..fa2b4d7 100644 --- a/tests/server/test_btchApi.py +++ b/tests/server/test_btchApi.py @@ -752,8 +752,8 @@ def test__joinRandomGame__noneAvailable(self): ) print(response.json()) - self.assertEqual(response.status_code, 200) - self.assertDictEqual(response.json(), {}) + self.assertEqual(response.status_code, 404) + self.assertDictEqual(response.json(), {'detail': 'available random game not found'}) def test__listAvailableGames(self): token, _ = self.addFakeUsers(self.db) From f181908b51c0f1d40f7a290096f654c62349c420 Mon Sep 17 00:00:00 2001 From: Quim Date: Sun, 7 Jan 2024 19:14:55 +0100 Subject: [PATCH 15/43] remove trailing / from all endpoints --- battlechess/server/btchApi.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/battlechess/server/btchApi.py b/battlechess/server/btchApi.py index 9ae8e7a..b6bcea7 100644 --- a/battlechess/server/btchApi.py +++ b/battlechess/server/btchApi.py @@ -126,12 +126,12 @@ def login_for_access_token( 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]) +@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), @@ -142,7 +142,7 @@ def read_own_games( return games -@app.get("/users/", response_model=List[schemas.User]) +@app.get("/users", response_model=List[schemas.User]) def read_users_all( skip: int = 0, limit: int = 100, @@ -182,7 +182,7 @@ def read__single_user( # 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 From 9e45222833fcc463733fa4b418e554ce24ad6040 Mon Sep 17 00:00:00 2001 From: Quim Date: Tue, 9 Jan 2024 00:09:55 +0100 Subject: [PATCH 16/43] return btch user instead of db ids --- battlechess/server/schemas.py | 48 +++++++++++++++++------------------ tests/server/test_btchApi.py | 35 ++++++++++++++++++------- 2 files changed, 50 insertions(+), 33 deletions(-) diff --git a/battlechess/server/schemas.py b/battlechess/server/schemas.py index e02a062..38e47ec 100644 --- a/battlechess/server/schemas.py +++ b/battlechess/server/schemas.py @@ -20,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 @@ -147,9 +168,9 @@ 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 @@ -163,24 +184,3 @@ class Move(BaseModel): 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] = [] - - model_config = {"from_attributes": True} diff --git a/tests/server/test_btchApi.py b/tests/server/test_btchApi.py index fa2b4d7..677fd4a 100644 --- a/tests/server/test_btchApi.py +++ b/tests/server/test_btchApi.py @@ -115,6 +115,23 @@ def fakeusersdb(self): } return fake_users_db + + def janedoe(self): + return { + 'avatar': None, + 'email': 'janedoe@example.com', + 'full_name': 'Jane Doe', + 'username': 'janedoe' + } + + def johndoe(self): + return { + 'avatar': None, + 'email': 'johndoe@example.com', + 'full_name': 'John Doe', + 'username': 'johndoe' + } + def fakegamesdb(self): fake_games_db = { "lkml4a3.d3": { @@ -654,16 +671,16 @@ def test__get_me_games(self): self.assertDictEqual( response.json()[0], { - "black_id": 2, + "black": self.janedoe(), "created_at": mock.ANY, "uuid": mock.ANY, "id": 1, "last_move_time": None, - "owner_id": 1, + "owner": self.johndoe(), "public": False, "status": "started", "turn": "black", - "white_id": 1, + "white": self.johndoe(), "winner": None, }, ) @@ -777,26 +794,26 @@ def test__listAvailableGames(self): "id": 3, "uuid": mock.ANY, "created_at": mock.ANY, - "owner_id": 2, + "owner": self.janedoe(), "last_move_time": None, "public": True, - "white_id": None, - "black_id": 2, + "white": None, + "black": self.janedoe(), "status": GameStatus.WAITING, "turn": "white", "winner": None, }, { - "black_id": None, + "black": None, "created_at": mock.ANY, "uuid": mock.ANY, "id": 4, "last_move_time": None, - "owner_id": 2, + "owner": self.janedoe(), "public": True, "status": GameStatus.WAITING, "turn": "white", - "white_id": 2, + "white": self.janedoe(), "winner": None, }, ], From 1bda628c6827b8c07670469affcc2d7880109e38 Mon Sep 17 00:00:00 2001 From: Quim Date: Tue, 9 Jan 2024 18:02:45 +0100 Subject: [PATCH 17/43] improve error message indicating it's a coordinate --- battlechess/core/Board.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/battlechess/core/Board.py b/battlechess/core/Board.py index 2a64efb..e426fb5 100644 --- a/battlechess/core/Board.py +++ b/battlechess/core/Board.py @@ -409,14 +409,14 @@ def move(self, i, j, ii, jj, color=None): return ( False, [], - "{}{} is not {}".format(i, j, "white" if color == "w" else "black"), + "({},{}) 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", + f"({ii},{jj}) is occupied by your own {self.board[ii][jj]} piece", ) reach = self.getReachablePosition( i, j @@ -427,7 +427,7 @@ def move(self, i, j, ii, jj, color=None): return ( False, [], - f"{ii}{jj} is not a {self.board[i][j]} possible position for some reason", + 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) From 8db5a4c5178667758e238edcdb365f6ed1fe8812 Mon Sep 17 00:00:00 2001 From: Quim Date: Wed, 10 Jan 2024 23:54:38 +0100 Subject: [PATCH 18/43] wip: add schemas as response models to all endpoints, wip passing filter options to games --- battlechess/server/btchApi.py | 19 +++++++++++-------- battlechess/server/crud.py | 8 ++++++-- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/battlechess/server/btchApi.py b/battlechess/server/btchApi.py index b6bcea7..4cb6ccc 100644 --- a/battlechess/server/btchApi.py +++ b/battlechess/server/btchApi.py @@ -133,11 +133,14 @@ def read_users_me(current_user: schemas.User = Depends(get_current_active_user)) @app.get("/users/me/games", response_model=List[schemas.Game]) def read_own_games( + status: str | None = None, 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) + game_status : schemas.GameStatus = schemas.GameStatus.WAITING if status == 'waiting' else None + print(f'{game_status} and {status}') + games = crud.get_games_by_player(db, current_user, game_status) print(f"{games}") return games @@ -297,7 +300,7 @@ def list_available_games( # TODO should be patch # joines an existing game. error when game already started -@app.get("/games/{gameUUID}/join") +@app.get("/games/{gameUUID}/join", response_model=schemas.Game) def join_game( gameUUID: str, current_user: schemas.User = Depends(get_current_active_user), @@ -336,7 +339,7 @@ def join_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") +@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), @@ -353,7 +356,7 @@ def join_random_game( game = set_player(game, current_user, db) db.refresh(game) - + print(game.owner) return game @@ -378,7 +381,7 @@ def query_turn( return game.turn -@app.post("/games/{gameUUID}/move") +@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 @@ -450,7 +453,7 @@ def get_moves( return moves -@app.get("/games/{gameUUID}/snap") +@app.get("/games/{gameUUID}/snap", response_model=schemas.GameSnap) def get_snap( gameUUID: str, current_user: schemas.User = Depends(get_current_active_user), @@ -488,7 +491,7 @@ def get_snap( return snap4player -@app.get("/games/{gameUUID}/snap/{moveNum}") +@app.get("/games/{gameUUID}/snap/{moveNum}", response_model=schemas.GameSnap) def get_snap_by_move( gameUUID: str, moveNum: int, @@ -521,7 +524,7 @@ def get_snap_by_move( return snap4player -@app.get("/games/{gameUUID}/snaps") +@app.get("/games/{gameUUID}/snaps", response_model=List[schemas.GameSnap]) def get_snaps( gameUUID: str, current_user: schemas.User = Depends(get_current_active_user), diff --git a/battlechess/server/crud.py b/battlechess/server/crud.py index 31fa9f8..7839528 100644 --- a/battlechess/server/crud.py +++ b/battlechess/server/crud.py @@ -154,10 +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): +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(or_(models.Game.black == user, models.Game.white == user)) + .filter(status_filter) .all() ) From f3393fe497b1589fa033aa871e52c5e1da754070 Mon Sep 17 00:00:00 2001 From: quimnuss Date: Thu, 11 Jan 2024 19:44:08 +0100 Subject: [PATCH 19/43] support optional game status filter on game listing --- battlechess/server/btchApi.py | 28 ++++++---- battlechess/server/crud.py | 37 ++++++++++--- tests/server/test_btchApi.py | 98 +++++++++++++++-------------------- 3 files changed, 89 insertions(+), 74 deletions(-) diff --git a/battlechess/server/btchApi.py b/battlechess/server/btchApi.py index 4cb6ccc..2bd53f4 100644 --- a/battlechess/server/btchApi.py +++ b/battlechess/server/btchApi.py @@ -1,11 +1,21 @@ from datetime import timedelta -from typing import List - -from fastapi import Depends, FastAPI, File, Header, HTTPException, UploadFile, status +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 sqlalchemy.orm import Session +from typing_extensions import Annotated from battlechess.server import crud, models, schemas from battlechess.server.btchApiDB import SessionLocal, engine @@ -133,15 +143,11 @@ def read_users_me(current_user: schemas.User = Depends(get_current_active_user)) @app.get("/users/me/games", response_model=List[schemas.Game]) def read_own_games( - status: str | None = None, current_user: schemas.User = Depends(get_current_active_user), db: Session = Depends(get_db), ): print("read own games") - game_status : schemas.GameStatus = schemas.GameStatus.WAITING if status == 'waiting' else None - print(f'{game_status} and {status}') - games = crud.get_games_by_player(db, current_user, game_status) - print(f"{games}") + games = crud.get_games_by_player(db, current_user) return games @@ -289,12 +295,12 @@ def get_game_by_uuid( # lists available games @app.get("/games", response_model=List[schemas.Game]) -def list_available_games( - status_filter: str = GameStatus.WAITING, +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_public_game_by_status(db, current_user, status_filter) + games = crud.get_games_by_status(db, current_user, status) return games diff --git a/battlechess/server/crud.py b/battlechess/server/crud.py index 7839528..1802970 100644 --- a/battlechess/server/crud.py +++ b/battlechess/server/crud.py @@ -2,7 +2,7 @@ import shutil from datetime import datetime, timedelta, timezone from pathlib import Path -from typing import Optional +from typing import List, Optional from jose import jwt from sqlalchemy import and_, or_ @@ -154,16 +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, status: schemas.GameStatus = None): +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() - ) + + return db.query(models.Game).filter(status_filter).all() def get_game_by_uuid(db: Session, gameUUID): @@ -186,6 +184,29 @@ def get_public_game_by_status(db: Session, user: schemas.User, status): 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 + + def get_random_public_game_waiting(db: Session, user: schemas.User): public_games = get_public_game_by_status(db, user, "waiting") numAvailableGames = len(public_games) diff --git a/tests/server/test_btchApi.py b/tests/server/test_btchApi.py index 677fd4a..e1972b2 100644 --- a/tests/server/test_btchApi.py +++ b/tests/server/test_btchApi.py @@ -115,21 +115,20 @@ def fakeusersdb(self): } return fake_users_db - def janedoe(self): return { - 'avatar': None, - 'email': 'janedoe@example.com', - 'full_name': 'Jane Doe', - 'username': 'janedoe' + "avatar": None, + "email": "janedoe@example.com", + "full_name": "Jane Doe", + "username": "janedoe", } - + def johndoe(self): return { - 'avatar': None, - 'email': 'johndoe@example.com', - 'full_name': 'John Doe', - 'username': 'johndoe' + "avatar": None, + "email": "johndoe@example.com", + "full_name": "John Doe", + "username": "johndoe", } def fakegamesdb(self): @@ -723,34 +722,30 @@ def test__joinRandomGame(self): ) # TODO join game (client chooses one) - - print(response.json()) + game_dict = response.json() + print(game_dict) self.assertEqual(response.status_code, 200) - self.assertNotEqual(response.json(), {}) + self.assertNotEqual(game_dict, {}) self.assertTrue( - response.json()["white_id"] == oneUser.id - or response.json()["black_id"] == oneUser.id + game_dict["white"]["username"] == oneUser.username + or game_dict["black"]["username"] == oneUser.username ) self.assertDictEqual( - response.json(), + game_dict, { - "black_id": mock.ANY, + "black": mock.ANY, "created_at": mock.ANY, "uuid": mock.ANY, "last_move_time": None, "id": mock.ANY, - "owner_id": mock.ANY, + "owner": mock.ANY, "public": True, "status": "started", "turn": "white", - "white_id": mock.ANY, + "white": 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): @@ -770,9 +765,11 @@ def test__joinRandomGame__noneAvailable(self): print(response.json()) self.assertEqual(response.status_code, 404) - self.assertDictEqual(response.json(), {'detail': 'available random game not found'}) + self.assertDictEqual( + response.json(), {"detail": "available random game not found"} + ) - def test__listAvailableGames(self): + def test__get_available_games__all(self): token, _ = self.addFakeUsers(self.db) _ = self.addFakeGames(self.db, self.fakegamesdb()) @@ -786,37 +783,28 @@ def test__listAvailableGames(self): print(response.json()) self.assertEqual(response.status_code, 200) - self.maxDiff = None + game_ids = [game["id"] for game in response.json()] + self.assertListEqual(game_ids, [1, 2, 3, 4, 5]) + + def test__get_available_games__waiting(self): + token, _ = self.addFakeUsers(self.db) + _ = self.addFakeGames(self.db, self.fakegamesdb()) + + response = self.client.get( + "/games", + headers={ + "Authorization": "Bearer " + token, + "Content-Type": "application/json", + }, + params={"status": ["waiting"]}, + ) + + print(response.json()) + self.assertEqual(response.status_code, 200) + game_ids = [(game["id"], game["status"]) for game in response.json()] self.assertListEqual( - response.json(), - [ - { - "id": 3, - "uuid": mock.ANY, - "created_at": mock.ANY, - "owner": self.janedoe(), - "last_move_time": None, - "public": True, - "white": None, - "black": self.janedoe(), - "status": GameStatus.WAITING, - "turn": "white", - "winner": None, - }, - { - "black": None, - "created_at": mock.ANY, - "uuid": mock.ANY, - "id": 4, - "last_move_time": None, - "owner": self.janedoe(), - "public": True, - "status": GameStatus.WAITING, - "turn": "white", - "white": self.janedoe(), - "winner": None, - }, - ], + game_ids, + [(3, GameStatus.WAITING), (4, GameStatus.WAITING), (5, GameStatus.WAITING)], ) # TODO this test was deactivated by mistake From 350fce1d41e70d69e9701a4287a467a859589155 Mon Sep 17 00:00:00 2001 From: Quim Date: Sun, 21 Jan 2024 00:58:24 +0100 Subject: [PATCH 20/43] just return OK if joining a game the player is already in --- battlechess/server/btchApi.py | 4 ++-- battlechess/server/models.py | 9 ++++----- tests/server/test_btchApi.py | 5 +---- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/battlechess/server/btchApi.py b/battlechess/server/btchApi.py index 2bd53f4..9f0e27b 100644 --- a/battlechess/server/btchApi.py +++ b/battlechess/server/btchApi.py @@ -102,8 +102,8 @@ def set_player( ): 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() diff --git a/battlechess/server/models.py b/battlechess/server/models.py index 189033a..2753456 100644 --- a/battlechess/server/models.py +++ b/battlechess/server/models.py @@ -64,11 +64,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 @@ -95,6 +91,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 diff --git a/tests/server/test_btchApi.py b/tests/server/test_btchApi.py index e1972b2..dfe1127 100644 --- a/tests/server/test_btchApi.py +++ b/tests/server/test_btchApi.py @@ -868,10 +868,7 @@ def test__joinGame__playerAlreadyInGame__simple(self): }, ) - self.assertEqual(response.status_code, 409) - self.assertDictEqual( - response.json(), {"detail": "Player is already in this game"} - ) + self.assertEqual(response.status_code, 200) def test__getsnap__byNum(self): firstgame_uuid, token = self.classicSetup() From 018315fd7e7ec58e76f419e634f2d19db91c100e Mon Sep 17 00:00:00 2001 From: Quim Date: Sun, 21 Jan 2024 11:47:24 +0100 Subject: [PATCH 21/43] return precondition failed when asking for snaps on a waiting game --- battlechess/server/btchApi.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/battlechess/server/btchApi.py b/battlechess/server/btchApi.py index 9f0e27b..da5dad0 100644 --- a/battlechess/server/btchApi.py +++ b/battlechess/server/btchApi.py @@ -477,6 +477,21 @@ def get_snap( 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: From 34d384421f1e7df1e20891be2571762567e0f74e Mon Sep 17 00:00:00 2001 From: Quim Date: Mon, 22 Jan 2024 00:22:15 +0100 Subject: [PATCH 22/43] don't prepare snap if game is over (replay), add status endpoint, fix finished game is over string --- battlechess/server/btchApi.py | 44 +++++++++++++++++++++++++++++------ battlechess/server/crud.py | 2 +- tests/server/test_btchApi.py | 20 ++++++++-------- 3 files changed, 48 insertions(+), 18 deletions(-) diff --git a/battlechess/server/btchApi.py b/battlechess/server/btchApi.py index da5dad0..aa75103 100644 --- a/battlechess/server/btchApi.py +++ b/battlechess/server/btchApi.py @@ -284,13 +284,36 @@ def post_new_game( return game -@app.get("/games/{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), ): - return crud.get_game_by_uuid(db, gameUUID) + 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 @@ -506,8 +529,9 @@ def get_snap( player_color = "black" if current_user.id == game.black_id else "white" snap4player = schemas.GameSnap.model_validate(snap) - print(f"preparing board for {current_user.username} {player_color}") - snap4player.prepare_for_player(player_color) + if game.status != GameStatus.OVER: + print(f"preparing board for {current_user.username} {player_color}") + snap4player.prepare_for_player(player_color) return snap4player @@ -539,9 +563,14 @@ def get_snap_by_move( headers={"Authorization": "Bearer"}, ) - player_color = "black" if current_user.id == game.black_id else "white" + print(f"check if we need to prepare snap {game.status}") + snap4player = schemas.GameSnap.model_validate(snap) - snap4player.prepare_for_player(player_color) + 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 @@ -569,6 +598,7 @@ def get_snaps( for snap in game.snaps: snap4player = schemas.GameSnap.model_validate(snap) - snap4player.prepare_for_player(player_color) + if game.status != GameStatus.OVER: + snap4player.prepare_for_player(player_color) result.append(snap4player) return result diff --git a/battlechess/server/crud.py b/battlechess/server/crud.py index 1802970..15c0a0e 100644 --- a/battlechess/server/crud.py +++ b/battlechess/server/crud.py @@ -263,7 +263,7 @@ def create_snap_by_move( 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}" ) diff --git a/tests/server/test_btchApi.py b/tests/server/test_btchApi.py index dfe1127..a5b61b6 100644 --- a/tests/server/test_btchApi.py +++ b/tests/server/test_btchApi.py @@ -638,16 +638,16 @@ def test__get_game_by_uuid(self): self.assertDictEqual( response.json(), { - "black_id": None, + "black": None, "created_at": mock.ANY, "uuid": mock.ANY, "id": 5, - "owner_id": 1, + "owner": self.johndoe(), "last_move_time": None, "public": False, "status": GameStatus.WAITING, "turn": "white", - "white_id": 1, + "white": self.johndoe(), "winner": None, }, ) @@ -1418,7 +1418,7 @@ def test__integrationTest__foolscheckmate(self): }, ) - john_id = response.json()["id"] + john_username = response.json()["username"] self.assertEqual(response.status_code, 200) @@ -1432,7 +1432,7 @@ def test__integrationTest__foolscheckmate(self): }, ) - jane_id = response.json()["id"] + jane_username = response.json()["username"] self.assertEqual(response.status_code, 200) @@ -1493,16 +1493,16 @@ def test__integrationTest__foolscheckmate(self): self.assertEqual(response.status_code, 200) self.assertEqual(response.json()["status"], GameStatus.WAITING) - white_id = response.json()["white_id"] - response.json()["black_id"] + white = response.json()["white"] + response.json()["black"] jane_color = ( - None if not white_id else "white" if white_id == jane_id else "black" + None if not white else "white" if white["username"] == jane_username else "black" ) john_color = ( None - if not white_id + if not white else "white" - if response.json()["white_id"] == john_id + if white['username'] == john_username else "black" ) From a83110cc8698d43e279861c5ba0b18225c2889d1 Mon Sep 17 00:00:00 2001 From: Quim Date: Thu, 1 Feb 2024 20:27:16 +0100 Subject: [PATCH 23/43] use decouple with defaults, update dependencies, add dockerfile --- .dockerignore | 10 + .gitignore | 2 + Dockerfile | 24 + Procfile | 1 + battlechess/server/btchApiDB.py | 3 +- battlechess/server/btchServer.py | 0 battlechess/server/config.py | 10 +- battlechess/server/config_example.py | 5 - battlechess/server/utils.py | 2 +- poetry.lock | 741 +++++++++++++-------------- pyproject.toml | 2 +- requirements.txt | 26 +- 12 files changed, 439 insertions(+), 387 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 Procfile delete mode 100644 battlechess/server/btchServer.py delete mode 100644 battlechess/server/config_example.py 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/.gitignore b/.gitignore index 5e05608..e707a8a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,5 +12,7 @@ bin .vscode sql_app.db test.db +*.db +*.sqlite .env 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/battlechess/server/btchApiDB.py b/battlechess/server/btchApiDB.py index 9af75e6..485aed2 100644 --- a/battlechess/server/btchApiDB.py +++ b/battlechess/server/btchApiDB.py @@ -1,7 +1,8 @@ from sqlalchemy import create_engine 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" diff --git a/battlechess/server/btchServer.py b/battlechess/server/btchServer.py deleted file mode 100644 index e69de29..0000000 diff --git a/battlechess/server/config.py b/battlechess/server/config.py index 1d85752..fb00bfb 100644 --- a/battlechess/server/config.py +++ b/battlechess/server/config.py @@ -3,7 +3,9 @@ from decouple import config -SECRET_KEY = config("SECRET_KEY") -ALGORITHM = "HS256" -ACCESS_TOKEN_EXPIRE_MINUTES = 3000 -HANDLEBASEURL = "https://bt.ch/" +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/battlechess/server/config_example.py b/battlechess/server/config_example.py deleted file mode 100644 index 76667f6..0000000 --- a/battlechess/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/battlechess/server/utils.py b/battlechess/server/utils.py index 2d15bc6..31275a1 100644 --- a/battlechess/server/utils.py +++ b/battlechess/server/utils.py @@ -3,7 +3,7 @@ from passlib.context import CryptContext -from .config import HANDLEBASEURL +from battlechess.server.config import HANDLEBASEURL pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") diff --git a/poetry.lock b/poetry.lock index 4df2679..e74d971 100644 --- a/poetry.lock +++ b/poetry.lock @@ -77,32 +77,38 @@ files = [ [[package]] name = "bcrypt" -version = "4.0.1" +version = "4.1.2" description = "Modern password hashing for your software and your servers" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "bcrypt-4.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f"}, - {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0"}, - {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0eaa47d4661c326bfc9d08d16debbc4edf78778e6aaba29c1bc7ce67214d4410"}, - {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae88eca3024bb34bb3430f964beab71226e761f51b912de5133470b649d82344"}, - {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:a522427293d77e1c29e303fc282e2d71864579527a04ddcfda6d4f8396c6c36a"}, - {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:fbdaec13c5105f0c4e5c52614d04f0bca5f5af007910daa8b6b12095edaa67b3"}, - {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ca3204d00d3cb2dfed07f2d74a25f12fc12f73e606fcaa6975d1f7ae69cacbb2"}, - {file = "bcrypt-4.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:089098effa1bc35dc055366740a067a2fc76987e8ec75349eb9484061c54f535"}, - {file = "bcrypt-4.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e9a51bbfe7e9802b5f3508687758b564069ba937748ad7b9e890086290d2f79e"}, - {file = "bcrypt-4.0.1-cp36-abi3-win32.whl", hash = "sha256:2caffdae059e06ac23fce178d31b4a702f2a3264c20bfb5ff541b338194d8fab"}, - {file = "bcrypt-4.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:8a68f4341daf7522fe8d73874de8906f3a339048ba406be6ddc1b3ccb16fc0d9"}, - {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf4fa8b2ca74381bb5442c089350f09a3f17797829d958fad058d6e44d9eb83c"}, - {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:67a97e1c405b24f19d08890e7ae0c4f7ce1e56a712a016746c8b2d7732d65d4b"}, - {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b3b85202d95dd568efcb35b53936c5e3b3600c7cdcc6115ba461df3a8e89f38d"}, - {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbb03eec97496166b704ed663a53680ab57c5084b2fc98ef23291987b525cb7d"}, - {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:5ad4d32a28b80c5fa6671ccfb43676e8c1cc232887759d1cd7b6f56ea4355215"}, - {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b57adba8a1444faf784394de3436233728a1ecaeb6e07e8c22c8848f179b893c"}, - {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b2cea8a9ed3d55b4491887ceadb0106acf7c6387699fca771af56b1cdeeda"}, - {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:2b3ac11cf45161628f1f3733263e63194f22664bf4d0c0f3ab34099c02134665"}, - {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3100851841186c25f127731b9fa11909ab7b1df6fc4b9f8353f4f1fd952fbf71"}, - {file = "bcrypt-4.0.1.tar.gz", hash = "sha256:27d375903ac8261cfe4047f6709d16f7d18d39b1ec92aaf72af989552a650ebd"}, + {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] @@ -111,29 +117,33 @@ typecheck = ["mypy"] [[package]] name = "black" -version = "23.11.0" +version = "23.12.1" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ - {file = "black-23.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dbea0bb8575c6b6303cc65017b46351dc5953eea5c0a59d7b7e3a2d2f433a911"}, - {file = "black-23.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:412f56bab20ac85927f3a959230331de5614aecda1ede14b373083f62ec24e6f"}, - {file = "black-23.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d136ef5b418c81660ad847efe0e55c58c8208b77a57a28a503a5f345ccf01394"}, - {file = "black-23.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:6c1cac07e64433f646a9a838cdc00c9768b3c362805afc3fce341af0e6a9ae9f"}, - {file = "black-23.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cf57719e581cfd48c4efe28543fea3d139c6b6f1238b3f0102a9c73992cbb479"}, - {file = "black-23.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:698c1e0d5c43354ec5d6f4d914d0d553a9ada56c85415700b81dc90125aac244"}, - {file = "black-23.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:760415ccc20f9e8747084169110ef75d545f3b0932ee21368f63ac0fee86b221"}, - {file = "black-23.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:58e5f4d08a205b11800332920e285bd25e1a75c54953e05502052738fe16b3b5"}, - {file = "black-23.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:45aa1d4675964946e53ab81aeec7a37613c1cb71647b5394779e6efb79d6d187"}, - {file = "black-23.11.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c44b7211a3a0570cc097e81135faa5f261264f4dfaa22bd5ee2875a4e773bd6"}, - {file = "black-23.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a9acad1451632021ee0d146c8765782a0c3846e0e0ea46659d7c4f89d9b212b"}, - {file = "black-23.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:fc7f6a44d52747e65a02558e1d807c82df1d66ffa80a601862040a43ec2e3142"}, - {file = "black-23.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7f622b6822f02bfaf2a5cd31fdb7cd86fcf33dab6ced5185c35f5db98260b055"}, - {file = "black-23.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:250d7e60f323fcfc8ea6c800d5eba12f7967400eb6c2d21ae85ad31c204fb1f4"}, - {file = "black-23.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5133f5507007ba08d8b7b263c7aa0f931af5ba88a29beacc4b2dc23fcefe9c06"}, - {file = "black-23.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:421f3e44aa67138ab1b9bfbc22ee3780b22fa5b291e4db8ab7eee95200726b07"}, - {file = "black-23.11.0-py3-none-any.whl", hash = "sha256:54caaa703227c6e0c87b76326d0862184729a69b73d3b7305b6288e1d830067e"}, - {file = "black-23.11.0.tar.gz", hash = "sha256:4c68855825ff432d197229846f971bc4d6666ce90492e5b02013bcaca4d9ab05"}, + {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] @@ -147,7 +157,7 @@ typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] +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)"] @@ -264,47 +274,56 @@ files = [ [[package]] name = "cryptography" -version = "41.0.5" +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-41.0.5-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:da6a0ff8f1016ccc7477e6339e1d50ce5f59b88905585f77193ebd5068f1e797"}, - {file = "cryptography-41.0.5-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b948e09fe5fb18517d99994184854ebd50b57248736fd4c720ad540560174ec5"}, - {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d38e6031e113b7421db1de0c1b1f7739564a88f1684c6b89234fbf6c11b75147"}, - {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e270c04f4d9b5671ebcc792b3ba5d4488bf7c42c3c241a3748e2599776f29696"}, - {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ec3b055ff8f1dce8e6ef28f626e0972981475173d7973d63f271b29c8a2897da"}, - {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:7d208c21e47940369accfc9e85f0de7693d9a5d843c2509b3846b2db170dfd20"}, - {file = "cryptography-41.0.5-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8254962e6ba1f4d2090c44daf50a547cd5f0bf446dc658a8e5f8156cae0d8548"}, - {file = "cryptography-41.0.5-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:a48e74dad1fb349f3dc1d449ed88e0017d792997a7ad2ec9587ed17405667e6d"}, - {file = "cryptography-41.0.5-cp37-abi3-win32.whl", hash = "sha256:d3977f0e276f6f5bf245c403156673db103283266601405376f075c849a0b936"}, - {file = "cryptography-41.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:73801ac9736741f220e20435f84ecec75ed70eda90f781a148f1bad546963d81"}, - {file = "cryptography-41.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3be3ca726e1572517d2bef99a818378bbcf7d7799d5372a46c79c29eb8d166c1"}, - {file = "cryptography-41.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e886098619d3815e0ad5790c973afeee2c0e6e04b4da90b88e6bd06e2a0b1b72"}, - {file = "cryptography-41.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:573eb7128cbca75f9157dcde974781209463ce56b5804983e11a1c462f0f4e88"}, - {file = "cryptography-41.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0c327cac00f082013c7c9fb6c46b7cc9fa3c288ca702c74773968173bda421bf"}, - {file = "cryptography-41.0.5-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:227ec057cd32a41c6651701abc0328135e472ed450f47c2766f23267b792a88e"}, - {file = "cryptography-41.0.5-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:22892cc830d8b2c89ea60148227631bb96a7da0c1b722f2aac8824b1b7c0b6b8"}, - {file = "cryptography-41.0.5-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5a70187954ba7292c7876734183e810b728b4f3965fbe571421cb2434d279179"}, - {file = "cryptography-41.0.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:88417bff20162f635f24f849ab182b092697922088b477a7abd6664ddd82291d"}, - {file = "cryptography-41.0.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c707f7afd813478e2019ae32a7c49cd932dd60ab2d2a93e796f68236b7e1fbf1"}, - {file = "cryptography-41.0.5-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:580afc7b7216deeb87a098ef0674d6ee34ab55993140838b14c9b83312b37b86"}, - {file = "cryptography-41.0.5-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1e91467c65fe64a82c689dc6cf58151158993b13eb7a7f3f4b7f395636723"}, - {file = "cryptography-41.0.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0d2a6a598847c46e3e321a7aef8af1436f11c27f1254933746304ff014664d84"}, - {file = "cryptography-41.0.5.tar.gz", hash = "sha256:392cb88b597247177172e02da6b7a63deeff1937fa6fec3bbf902ebd75d97ec7"}, + {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 = ">=1.12" +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)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] +docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] nox = ["nox"] -pep8test = ["black", "check-sdist", "mypy", "ruff"] +pep8test = ["check-sdist", "click", "mypy", "ruff"] sdist = ["build"] ssh = ["bcrypt (>=3.1.5)"] -test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test-randomorder = ["pytest-randomly"] [[package]] @@ -320,13 +339,13 @@ files = [ [[package]] name = "distlib" -version = "0.3.7" +version = "0.3.8" description = "Distribution utilities" optional = false python-versions = "*" files = [ - {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, - {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, ] [[package]] @@ -413,72 +432,73 @@ typing = ["typing-extensions (>=4.8)"] [[package]] name = "greenlet" -version = "3.0.1" +version = "3.0.3" description = "Lightweight in-process concurrent programming" optional = false python-versions = ">=3.7" files = [ - {file = "greenlet-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f89e21afe925fcfa655965ca8ea10f24773a1791400989ff32f467badfe4a064"}, - {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28e89e232c7593d33cac35425b58950789962011cc274aa43ef8865f2e11f46d"}, - {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8ba29306c5de7717b5761b9ea74f9c72b9e2b834e24aa984da99cbfc70157fd"}, - {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19bbdf1cce0346ef7341705d71e2ecf6f41a35c311137f29b8a2dc2341374565"}, - {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:599daf06ea59bfedbec564b1692b0166a0045f32b6f0933b0dd4df59a854caf2"}, - {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b641161c302efbb860ae6b081f406839a8b7d5573f20a455539823802c655f63"}, - {file = "greenlet-3.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d57e20ba591727da0c230ab2c3f200ac9d6d333860d85348816e1dca4cc4792e"}, - {file = "greenlet-3.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5805e71e5b570d490938d55552f5a9e10f477c19400c38bf1d5190d760691846"}, - {file = "greenlet-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:52e93b28db27ae7d208748f45d2db8a7b6a380e0d703f099c949d0f0d80b70e9"}, - {file = "greenlet-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f7bfb769f7efa0eefcd039dd19d843a4fbfbac52f1878b1da2ed5793ec9b1a65"}, - {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91e6c7db42638dc45cf2e13c73be16bf83179f7859b07cfc139518941320be96"}, - {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1757936efea16e3f03db20efd0cd50a1c86b06734f9f7338a90c4ba85ec2ad5a"}, - {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19075157a10055759066854a973b3d1325d964d498a805bb68a1f9af4aaef8ec"}, - {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9d21aaa84557d64209af04ff48e0ad5e28c5cca67ce43444e939579d085da72"}, - {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2847e5d7beedb8d614186962c3d774d40d3374d580d2cbdab7f184580a39d234"}, - {file = "greenlet-3.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:97e7ac860d64e2dcba5c5944cfc8fa9ea185cd84061c623536154d5a89237884"}, - {file = "greenlet-3.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b2c02d2ad98116e914d4f3155ffc905fd0c025d901ead3f6ed07385e19122c94"}, - {file = "greenlet-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:22f79120a24aeeae2b4471c711dcf4f8c736a2bb2fabad2a67ac9a55ea72523c"}, - {file = "greenlet-3.0.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:100f78a29707ca1525ea47388cec8a049405147719f47ebf3895e7509c6446aa"}, - {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60d5772e8195f4e9ebf74046a9121bbb90090f6550f81d8956a05387ba139353"}, - {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:daa7197b43c707462f06d2c693ffdbb5991cbb8b80b5b984007de431493a319c"}, - {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea6b8aa9e08eea388c5f7a276fabb1d4b6b9d6e4ceb12cc477c3d352001768a9"}, - {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d11ebbd679e927593978aa44c10fc2092bc454b7d13fdc958d3e9d508aba7d0"}, - {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dbd4c177afb8a8d9ba348d925b0b67246147af806f0b104af4d24f144d461cd5"}, - {file = "greenlet-3.0.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20107edf7c2c3644c67c12205dc60b1bb11d26b2610b276f97d666110d1b511d"}, - {file = "greenlet-3.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8bef097455dea90ffe855286926ae02d8faa335ed8e4067326257cb571fc1445"}, - {file = "greenlet-3.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:b2d3337dcfaa99698aa2377c81c9ca72fcd89c07e7eb62ece3f23a3fe89b2ce4"}, - {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80ac992f25d10aaebe1ee15df45ca0d7571d0f70b645c08ec68733fb7a020206"}, - {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:337322096d92808f76ad26061a8f5fccb22b0809bea39212cd6c406f6a7060d2"}, - {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9934adbd0f6e476f0ecff3c94626529f344f57b38c9a541f87098710b18af0a"}, - {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc4d815b794fd8868c4d67602692c21bf5293a75e4b607bb92a11e821e2b859a"}, - {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41bdeeb552d814bcd7fb52172b304898a35818107cc8778b5101423c9017b3de"}, - {file = "greenlet-3.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6e6061bf1e9565c29002e3c601cf68569c450be7fc3f7336671af7ddb4657166"}, - {file = "greenlet-3.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:fa24255ae3c0ab67e613556375a4341af04a084bd58764731972bcbc8baeba36"}, - {file = "greenlet-3.0.1-cp37-cp37m-win32.whl", hash = "sha256:b489c36d1327868d207002391f662a1d163bdc8daf10ab2e5f6e41b9b96de3b1"}, - {file = "greenlet-3.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f33f3258aae89da191c6ebaa3bc517c6c4cbc9b9f689e5d8452f7aedbb913fa8"}, - {file = "greenlet-3.0.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:d2905ce1df400360463c772b55d8e2518d0e488a87cdea13dd2c71dcb2a1fa16"}, - {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a02d259510b3630f330c86557331a3b0e0c79dac3d166e449a39363beaae174"}, - {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55d62807f1c5a1682075c62436702aaba941daa316e9161e4b6ccebbbf38bda3"}, - {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3fcc780ae8edbb1d050d920ab44790201f027d59fdbd21362340a85c79066a74"}, - {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4eddd98afc726f8aee1948858aed9e6feeb1758889dfd869072d4465973f6bfd"}, - {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eabe7090db68c981fca689299c2d116400b553f4b713266b130cfc9e2aa9c5a9"}, - {file = "greenlet-3.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f2f6d303f3dee132b322a14cd8765287b8f86cdc10d2cb6a6fae234ea488888e"}, - {file = "greenlet-3.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d923ff276f1c1f9680d32832f8d6c040fe9306cbfb5d161b0911e9634be9ef0a"}, - {file = "greenlet-3.0.1-cp38-cp38-win32.whl", hash = "sha256:0b6f9f8ca7093fd4433472fd99b5650f8a26dcd8ba410e14094c1e44cd3ceddd"}, - {file = "greenlet-3.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:990066bff27c4fcf3b69382b86f4c99b3652bab2a7e685d968cd4d0cfc6f67c6"}, - {file = "greenlet-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ce85c43ae54845272f6f9cd8320d034d7a946e9773c693b27d620edec825e376"}, - {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89ee2e967bd7ff85d84a2de09df10e021c9b38c7d91dead95b406ed6350c6997"}, - {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87c8ceb0cf8a5a51b8008b643844b7f4a8264a2c13fcbcd8a8316161725383fe"}, - {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d6a8c9d4f8692917a3dc7eb25a6fb337bff86909febe2f793ec1928cd97bedfc"}, - {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fbc5b8f3dfe24784cee8ce0be3da2d8a79e46a276593db6868382d9c50d97b1"}, - {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85d2b77e7c9382f004b41d9c72c85537fac834fb141b0296942d52bf03fe4a3d"}, - {file = "greenlet-3.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:696d8e7d82398e810f2b3622b24e87906763b6ebfd90e361e88eb85b0e554dc8"}, - {file = "greenlet-3.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:329c5a2e5a0ee942f2992c5e3ff40be03e75f745f48847f118a3cfece7a28546"}, - {file = "greenlet-3.0.1-cp39-cp39-win32.whl", hash = "sha256:cf868e08690cb89360eebc73ba4be7fb461cfbc6168dd88e2fbbe6f31812cd57"}, - {file = "greenlet-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:ac4a39d1abae48184d420aa8e5e63efd1b75c8444dd95daa3e03f6c6310e9619"}, - {file = "greenlet-3.0.1.tar.gz", hash = "sha256:816bd9488a94cba78d93e1abb58000e8266fa9cc2aa9ccdd6eb0696acb24005b"}, + {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"] +docs = ["Sphinx", "furo"] test = ["objgraph", "psutil"] [[package]] @@ -539,13 +559,13 @@ socks = ["socksio (==1.*)"] [[package]] name = "identify" -version = "2.5.32" +version = "2.5.33" description = "File identification library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "identify-2.5.32-py2.py3-none-any.whl", hash = "sha256:0b7656ef6cba81664b783352c73f8c24b39cf82f926f78f4550eda928e5e0545"}, - {file = "identify-2.5.32.tar.gz", hash = "sha256:5d9979348ec1a21c768ae07e0a652924538e8bce67313a73cb0f681cf08ba407"}, + {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] @@ -553,13 +573,13 @@ license = ["ukkonen"] [[package]] name = "idna" -version = "3.5" +version = "3.6" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.5-py3-none-any.whl", hash = "sha256:79b8f0ac92d2351be5f6122356c9a592c96d81c9a79e4b488bf2a6a15f88057a"}, - {file = "idna-3.5.tar.gz", hash = "sha256:27009fe2735bf8723353582d48575b23c533cc2c2de7b5a68908d91b5eb18d08"}, + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, ] [[package]] @@ -630,20 +650,17 @@ test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.21)", "pa [[package]] name = "isort" -version = "5.12.0" +version = "5.13.2" description = "A Python utility / library to sort Python imports." optional = false python-versions = ">=3.8.0" files = [ - {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, - {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, + {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.3)"] -pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] -plugins = ["setuptools"] -requirements-deprecated-finder = ["pip-api", "pipreqs"] +colors = ["colorama (>=0.4.6)"] [[package]] name = "jedi" @@ -750,36 +767,39 @@ build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fullt totp = ["cryptography"] [[package]] -name = "pathlib" -version = "1.0.1" +name = "pathlib2" +version = "2.3.7.post1" description = "Object-oriented filesystem paths" optional = false python-versions = "*" files = [ - {file = "pathlib-1.0.1-py3-none-any.whl", hash = "sha256:f35f95ab8b0f59e6d354090350b44a80a80635d22efdedfa84c7ad1cf0a74147"}, - {file = "pathlib-1.0.1.tar.gz", hash = "sha256:6940718dfc3eff4258203ad5021090933e5c04707d5ca8cc9e73c94a7894ea9f"}, + {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.11.2" +version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, - {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, + {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.8.0" +version = "4.9.0" description = "Pexpect allows easy control of interactive console applications." optional = false python-versions = "*" files = [ - {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, - {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, + {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] @@ -798,28 +818,28 @@ files = [ [[package]] name = "platformdirs" -version = "4.0.0" +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.7" +python-versions = ">=3.8" files = [ - {file = "platformdirs-4.0.0-py3-none-any.whl", hash = "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b"}, - {file = "platformdirs-4.0.0.tar.gz", hash = "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731"}, + {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.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] +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.3.0" +version = "1.4.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" files = [ - {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, - {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, ] [package.extras] @@ -846,13 +866,13 @@ virtualenv = ">=20.10.0" [[package]] name = "prompt-toolkit" -version = "3.0.41" +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.41-py3-none-any.whl", hash = "sha256:f36fe301fafb7470e86aaf90f036eef600a3210be4decf461a5b1ca8403d3cb2"}, - {file = "prompt_toolkit-3.0.41.tar.gz", hash = "sha256:941367d97fc815548822aa26c2a269fdc4eb21e9ec05fc5d447cf09bad5d75f0"}, + {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] @@ -907,18 +927,18 @@ files = [ [[package]] name = "pydantic" -version = "2.5.2" +version = "2.6.0" description = "Data validation using Python type hints" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pydantic-2.5.2-py3-none-any.whl", hash = "sha256:80c50fb8e3dcecfddae1adbcc00ec5822918490c99ab31f6cf6140ca1c1429f0"}, - {file = "pydantic-2.5.2.tar.gz", hash = "sha256:ff177ba64c6faf73d7afa2e8cad38fd456c0dbe01c9954e71038001cd15a6edd"}, + {file = "pydantic-2.6.0-py3-none-any.whl", hash = "sha256:1440966574e1b5b99cf75a13bec7b20e3512e8a61b894ae252f56275e2c465ae"}, + {file = "pydantic-2.6.0.tar.gz", hash = "sha256:ae887bd94eb404b09d86e4d12f93893bdca79d766e738528c6fa1c849f3c6bcf"}, ] [package.dependencies] annotated-types = ">=0.4.0" -pydantic-core = "2.14.5" +pydantic-core = "2.16.1" typing-extensions = ">=4.6.1" [package.extras] @@ -926,116 +946,90 @@ email = ["email-validator (>=2.0.0)"] [[package]] name = "pydantic-core" -version = "2.14.5" +version = "2.16.1" description = "" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.14.5-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:7e88f5696153dc516ba6e79f82cc4747e87027205f0e02390c21f7cb3bd8abfd"}, - {file = "pydantic_core-2.14.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4641e8ad4efb697f38a9b64ca0523b557c7931c5f84e0fd377a9a3b05121f0de"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:774de879d212db5ce02dfbf5b0da9a0ea386aeba12b0b95674a4ce0593df3d07"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ebb4e035e28f49b6f1a7032920bb9a0c064aedbbabe52c543343d39341a5b2a3"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b53e9ad053cd064f7e473a5f29b37fc4cc9dc6d35f341e6afc0155ea257fc911"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aa1768c151cf562a9992462239dfc356b3d1037cc5a3ac829bb7f3bda7cc1f9"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eac5c82fc632c599f4639a5886f96867ffced74458c7db61bc9a66ccb8ee3113"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2ae91f50ccc5810b2f1b6b858257c9ad2e08da70bf890dee02de1775a387c66"}, - {file = "pydantic_core-2.14.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6b9ff467ffbab9110e80e8c8de3bcfce8e8b0fd5661ac44a09ae5901668ba997"}, - {file = "pydantic_core-2.14.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:61ea96a78378e3bd5a0be99b0e5ed00057b71f66115f5404d0dae4819f495093"}, - {file = "pydantic_core-2.14.5-cp310-none-win32.whl", hash = "sha256:bb4c2eda937a5e74c38a41b33d8c77220380a388d689bcdb9b187cf6224c9720"}, - {file = "pydantic_core-2.14.5-cp310-none-win_amd64.whl", hash = "sha256:b7851992faf25eac90bfcb7bfd19e1f5ffa00afd57daec8a0042e63c74a4551b"}, - {file = "pydantic_core-2.14.5-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:4e40f2bd0d57dac3feb3a3aed50f17d83436c9e6b09b16af271b6230a2915459"}, - {file = "pydantic_core-2.14.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ab1cdb0f14dc161ebc268c09db04d2c9e6f70027f3b42446fa11c153521c0e88"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aae7ea3a1c5bb40c93cad361b3e869b180ac174656120c42b9fadebf685d121b"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:60b7607753ba62cf0739177913b858140f11b8af72f22860c28eabb2f0a61937"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2248485b0322c75aee7565d95ad0e16f1c67403a470d02f94da7344184be770f"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:823fcc638f67035137a5cd3f1584a4542d35a951c3cc68c6ead1df7dac825c26"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96581cfefa9123accc465a5fd0cc833ac4d75d55cc30b633b402e00e7ced00a6"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a33324437018bf6ba1bb0f921788788641439e0ed654b233285b9c69704c27b4"}, - {file = "pydantic_core-2.14.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9bd18fee0923ca10f9a3ff67d4851c9d3e22b7bc63d1eddc12f439f436f2aada"}, - {file = "pydantic_core-2.14.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:853a2295c00f1d4429db4c0fb9475958543ee80cfd310814b5c0ef502de24dda"}, - {file = "pydantic_core-2.14.5-cp311-none-win32.whl", hash = "sha256:cb774298da62aea5c80a89bd58c40205ab4c2abf4834453b5de207d59d2e1651"}, - {file = "pydantic_core-2.14.5-cp311-none-win_amd64.whl", hash = "sha256:e87fc540c6cac7f29ede02e0f989d4233f88ad439c5cdee56f693cc9c1c78077"}, - {file = "pydantic_core-2.14.5-cp311-none-win_arm64.whl", hash = "sha256:57d52fa717ff445cb0a5ab5237db502e6be50809b43a596fb569630c665abddf"}, - {file = "pydantic_core-2.14.5-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:e60f112ac88db9261ad3a52032ea46388378034f3279c643499edb982536a093"}, - {file = "pydantic_core-2.14.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6e227c40c02fd873c2a73a98c1280c10315cbebe26734c196ef4514776120aeb"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0cbc7fff06a90bbd875cc201f94ef0ee3929dfbd5c55a06674b60857b8b85ed"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:103ef8d5b58596a731b690112819501ba1db7a36f4ee99f7892c40da02c3e189"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c949f04ecad823f81b1ba94e7d189d9dfb81edbb94ed3f8acfce41e682e48cef"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1452a1acdf914d194159439eb21e56b89aa903f2e1c65c60b9d874f9b950e5d"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb4679d4c2b089e5ef89756bc73e1926745e995d76e11925e3e96a76d5fa51fc"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf9d3fe53b1ee360e2421be95e62ca9b3296bf3f2fb2d3b83ca49ad3f925835e"}, - {file = "pydantic_core-2.14.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:70f4b4851dbb500129681d04cc955be2a90b2248d69273a787dda120d5cf1f69"}, - {file = "pydantic_core-2.14.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:59986de5710ad9613ff61dd9b02bdd2f615f1a7052304b79cc8fa2eb4e336d2d"}, - {file = "pydantic_core-2.14.5-cp312-none-win32.whl", hash = "sha256:699156034181e2ce106c89ddb4b6504c30db8caa86e0c30de47b3e0654543260"}, - {file = "pydantic_core-2.14.5-cp312-none-win_amd64.whl", hash = "sha256:5baab5455c7a538ac7e8bf1feec4278a66436197592a9bed538160a2e7d11e36"}, - {file = "pydantic_core-2.14.5-cp312-none-win_arm64.whl", hash = "sha256:e47e9a08bcc04d20975b6434cc50bf82665fbc751bcce739d04a3120428f3e27"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:af36f36538418f3806048f3b242a1777e2540ff9efaa667c27da63d2749dbce0"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:45e95333b8418ded64745f14574aa9bfc212cb4fbeed7a687b0c6e53b5e188cd"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e47a76848f92529879ecfc417ff88a2806438f57be4a6a8bf2961e8f9ca9ec7"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d81e6987b27bc7d101c8597e1cd2bcaa2fee5e8e0f356735c7ed34368c471550"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34708cc82c330e303f4ce87758828ef6e457681b58ce0e921b6e97937dd1e2a3"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c1988019752138b974c28f43751528116bcceadad85f33a258869e641d753"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e4d090e73e0725b2904fdbdd8d73b8802ddd691ef9254577b708d413bf3006e"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5c7d5b5005f177764e96bd584d7bf28d6e26e96f2a541fdddb934c486e36fd59"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a71891847f0a73b1b9eb86d089baee301477abef45f7eaf303495cd1473613e4"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a717aef6971208f0851a2420b075338e33083111d92041157bbe0e2713b37325"}, - {file = "pydantic_core-2.14.5-cp37-none-win32.whl", hash = "sha256:de790a3b5aa2124b8b78ae5faa033937a72da8efe74b9231698b5a1dd9be3405"}, - {file = "pydantic_core-2.14.5-cp37-none-win_amd64.whl", hash = "sha256:6c327e9cd849b564b234da821236e6bcbe4f359a42ee05050dc79d8ed2a91588"}, - {file = "pydantic_core-2.14.5-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:ef98ca7d5995a82f43ec0ab39c4caf6a9b994cb0b53648ff61716370eadc43cf"}, - {file = "pydantic_core-2.14.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6eae413494a1c3f89055da7a5515f32e05ebc1a234c27674a6956755fb2236f"}, - {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcf4e6d85614f7a4956c2de5a56531f44efb973d2fe4a444d7251df5d5c4dcfd"}, - {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6637560562134b0e17de333d18e69e312e0458ee4455bdad12c37100b7cad706"}, - {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77fa384d8e118b3077cccfcaf91bf83c31fe4dc850b5e6ee3dc14dc3d61bdba1"}, - {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16e29bad40bcf97aac682a58861249ca9dcc57c3f6be22f506501833ddb8939c"}, - {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:531f4b4252fac6ca476fbe0e6f60f16f5b65d3e6b583bc4d87645e4e5ddde331"}, - {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:074f3d86f081ce61414d2dc44901f4f83617329c6f3ab49d2bc6c96948b2c26b"}, - {file = "pydantic_core-2.14.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c2adbe22ab4babbca99c75c5d07aaf74f43c3195384ec07ccbd2f9e3bddaecec"}, - {file = "pydantic_core-2.14.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0f6116a558fd06d1b7c2902d1c4cf64a5bd49d67c3540e61eccca93f41418124"}, - {file = "pydantic_core-2.14.5-cp38-none-win32.whl", hash = "sha256:fe0a5a1025eb797752136ac8b4fa21aa891e3d74fd340f864ff982d649691867"}, - {file = "pydantic_core-2.14.5-cp38-none-win_amd64.whl", hash = "sha256:079206491c435b60778cf2b0ee5fd645e61ffd6e70c47806c9ed51fc75af078d"}, - {file = "pydantic_core-2.14.5-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:a6a16f4a527aae4f49c875da3cdc9508ac7eef26e7977952608610104244e1b7"}, - {file = "pydantic_core-2.14.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:abf058be9517dc877227ec3223f0300034bd0e9f53aebd63cf4456c8cb1e0863"}, - {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49b08aae5013640a3bfa25a8eebbd95638ec3f4b2eaf6ed82cf0c7047133f03b"}, - {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c2d97e906b4ff36eb464d52a3bc7d720bd6261f64bc4bcdbcd2c557c02081ed2"}, - {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3128e0bbc8c091ec4375a1828d6118bc20404883169ac95ffa8d983b293611e6"}, - {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88e74ab0cdd84ad0614e2750f903bb0d610cc8af2cc17f72c28163acfcf372a4"}, - {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c339dabd8ee15f8259ee0f202679b6324926e5bc9e9a40bf981ce77c038553db"}, - {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3387277f1bf659caf1724e1afe8ee7dbc9952a82d90f858ebb931880216ea955"}, - {file = "pydantic_core-2.14.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ba6b6b3846cfc10fdb4c971980a954e49d447cd215ed5a77ec8190bc93dd7bc5"}, - {file = "pydantic_core-2.14.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ca61d858e4107ce5e1330a74724fe757fc7135190eb5ce5c9d0191729f033209"}, - {file = "pydantic_core-2.14.5-cp39-none-win32.whl", hash = "sha256:ec1e72d6412f7126eb7b2e3bfca42b15e6e389e1bc88ea0069d0cc1742f477c6"}, - {file = "pydantic_core-2.14.5-cp39-none-win_amd64.whl", hash = "sha256:c0b97ec434041827935044bbbe52b03d6018c2897349670ff8fe11ed24d1d4ab"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:79e0a2cdbdc7af3f4aee3210b1172ab53d7ddb6a2d8c24119b5706e622b346d0"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:678265f7b14e138d9a541ddabbe033012a2953315739f8cfa6d754cc8063e8ca"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b15e855ae44f0c6341ceb74df61b606e11f1087e87dcb7482377374aac6abe"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09b0e985fbaf13e6b06a56d21694d12ebca6ce5414b9211edf6f17738d82b0f8"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3ad873900297bb36e4b6b3f7029d88ff9829ecdc15d5cf20161775ce12306f8a"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2d0ae0d8670164e10accbeb31d5ad45adb71292032d0fdb9079912907f0085f4"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d37f8ec982ead9ba0a22a996129594938138a1503237b87318392a48882d50b7"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:35613015f0ba7e14c29ac6c2483a657ec740e5ac5758d993fdd5870b07a61d8b"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:ab4ea451082e684198636565224bbb179575efc1658c48281b2c866bfd4ddf04"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ce601907e99ea5b4adb807ded3570ea62186b17f88e271569144e8cca4409c7"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb2ed8b3fe4bf4506d6dab3b93b83bbc22237e230cba03866d561c3577517d18"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:70f947628e074bb2526ba1b151cee10e4c3b9670af4dbb4d73bc8a89445916b5"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4bc536201426451f06f044dfbf341c09f540b4ebdb9fd8d2c6164d733de5e634"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4791cf0f8c3104ac668797d8c514afb3431bc3305f5638add0ba1a5a37e0d88"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:038c9f763e650712b899f983076ce783175397c848da04985658e7628cbe873b"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:27548e16c79702f1e03f5628589c6057c9ae17c95b4c449de3c66b589ead0520"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c97bee68898f3f4344eb02fec316db93d9700fb1e6a5b760ffa20d71d9a46ce3"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9b759b77f5337b4ea024f03abc6464c9f35d9718de01cfe6bae9f2e139c397e"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:439c9afe34638ace43a49bf72d201e0ffc1a800295bed8420c2a9ca8d5e3dbb3"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ba39688799094c75ea8a16a6b544eb57b5b0f3328697084f3f2790892510d144"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ccd4d5702bb90b84df13bd491be8d900b92016c5a455b7e14630ad7449eb03f8"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:81982d78a45d1e5396819bbb4ece1fadfe5f079335dd28c4ab3427cd95389944"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:7f8210297b04e53bc3da35db08b7302a6a1f4889c79173af69b72ec9754796b8"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:8c8a8812fe6f43a3a5b054af6ac2d7b8605c7bcab2804a8a7d68b53f3cd86e00"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:206ed23aecd67c71daf5c02c3cd19c0501b01ef3cbf7782db9e4e051426b3d0d"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2027d05c8aebe61d898d4cffd774840a9cb82ed356ba47a90d99ad768f39789"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40180930807ce806aa71eda5a5a5447abb6b6a3c0b4b3b1b1962651906484d68"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:615a0a4bff11c45eb3c1996ceed5bdaa2f7b432425253a7c2eed33bb86d80abc"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5e412d717366e0677ef767eac93566582518fe8be923361a5c204c1a62eaafe"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:513b07e99c0a267b1d954243845d8a833758a6726a3b5d8948306e3fe14675e3"}, - {file = "pydantic_core-2.14.5.tar.gz", hash = "sha256:6d30226dfc816dd0fdf120cae611dd2215117e4f9b124af8c60ab9093b6e8e71"}, + {file = "pydantic_core-2.16.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:300616102fb71241ff477a2cbbc847321dbec49428434a2f17f37528721c4948"}, + {file = "pydantic_core-2.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5511f962dd1b9b553e9534c3b9c6a4b0c9ded3d8c2be96e61d56f933feef9e1f"}, + {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98f0edee7ee9cc7f9221af2e1b95bd02810e1c7a6d115cfd82698803d385b28f"}, + {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9795f56aa6b2296f05ac79d8a424e94056730c0b860a62b0fdcfe6340b658cc8"}, + {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c45f62e4107ebd05166717ac58f6feb44471ed450d07fecd90e5f69d9bf03c48"}, + {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:462d599299c5971f03c676e2b63aa80fec5ebc572d89ce766cd11ca8bcb56f3f"}, + {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ebaa4bf6386a3b22eec518da7d679c8363fb7fb70cf6972161e5542f470798"}, + {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:99f9a50b56713a598d33bc23a9912224fc5d7f9f292444e6664236ae471ddf17"}, + {file = "pydantic_core-2.16.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8ec364e280db4235389b5e1e6ee924723c693cbc98e9d28dc1767041ff9bc388"}, + {file = "pydantic_core-2.16.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:653a5dfd00f601a0ed6654a8b877b18d65ac32c9d9997456e0ab240807be6cf7"}, + {file = "pydantic_core-2.16.1-cp310-none-win32.whl", hash = "sha256:1661c668c1bb67b7cec96914329d9ab66755911d093bb9063c4c8914188af6d4"}, + {file = "pydantic_core-2.16.1-cp310-none-win_amd64.whl", hash = "sha256:561be4e3e952c2f9056fba5267b99be4ec2afadc27261505d4992c50b33c513c"}, + {file = "pydantic_core-2.16.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:102569d371fadc40d8f8598a59379c37ec60164315884467052830b28cc4e9da"}, + {file = "pydantic_core-2.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:735dceec50fa907a3c314b84ed609dec54b76a814aa14eb90da31d1d36873a5e"}, + {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e83ebbf020be727d6e0991c1b192a5c2e7113eb66e3def0cd0c62f9f266247e4"}, + {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:30a8259569fbeec49cfac7fda3ec8123486ef1b729225222f0d41d5f840b476f"}, + {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:920c4897e55e2881db6a6da151198e5001552c3777cd42b8a4c2f72eedc2ee91"}, + {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5247a3d74355f8b1d780d0f3b32a23dd9f6d3ff43ef2037c6dcd249f35ecf4c"}, + {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d5bea8012df5bb6dda1e67d0563ac50b7f64a5d5858348b5c8cb5043811c19d"}, + {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ed3025a8a7e5a59817b7494686d449ebfbe301f3e757b852c8d0d1961d6be864"}, + {file = "pydantic_core-2.16.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:06f0d5a1d9e1b7932477c172cc720b3b23c18762ed7a8efa8398298a59d177c7"}, + {file = "pydantic_core-2.16.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:150ba5c86f502c040b822777e2e519b5625b47813bd05f9273a8ed169c97d9ae"}, + {file = "pydantic_core-2.16.1-cp311-none-win32.whl", hash = "sha256:d6cbdf12ef967a6aa401cf5cdf47850559e59eedad10e781471c960583f25aa1"}, + {file = "pydantic_core-2.16.1-cp311-none-win_amd64.whl", hash = "sha256:afa01d25769af33a8dac0d905d5c7bb2d73c7c3d5161b2dd6f8b5b5eea6a3c4c"}, + {file = "pydantic_core-2.16.1-cp311-none-win_arm64.whl", hash = "sha256:1a2fe7b00a49b51047334d84aafd7e39f80b7675cad0083678c58983662da89b"}, + {file = "pydantic_core-2.16.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f478ec204772a5c8218e30eb813ca43e34005dff2eafa03931b3d8caef87d51"}, + {file = "pydantic_core-2.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f1936ef138bed2165dd8573aa65e3095ef7c2b6247faccd0e15186aabdda7f66"}, + {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99d3a433ef5dc3021c9534a58a3686c88363c591974c16c54a01af7efd741f13"}, + {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd88f40f2294440d3f3c6308e50d96a0d3d0973d6f1a5732875d10f569acef49"}, + {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fac641bbfa43d5a1bed99d28aa1fded1984d31c670a95aac1bf1d36ac6ce137"}, + {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72bf9308a82b75039b8c8edd2be2924c352eda5da14a920551a8b65d5ee89253"}, + {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb4363e6c9fc87365c2bc777a1f585a22f2f56642501885ffc7942138499bf54"}, + {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:20f724a023042588d0f4396bbbcf4cffd0ddd0ad3ed4f0d8e6d4ac4264bae81e"}, + {file = "pydantic_core-2.16.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fb4370b15111905bf8b5ba2129b926af9470f014cb0493a67d23e9d7a48348e8"}, + {file = "pydantic_core-2.16.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23632132f1fd608034f1a56cc3e484be00854db845b3a4a508834be5a6435a6f"}, + {file = "pydantic_core-2.16.1-cp312-none-win32.whl", hash = "sha256:b9f3e0bffad6e238f7acc20c393c1ed8fab4371e3b3bc311020dfa6020d99212"}, + {file = "pydantic_core-2.16.1-cp312-none-win_amd64.whl", hash = "sha256:a0b4cfe408cd84c53bab7d83e4209458de676a6ec5e9c623ae914ce1cb79b96f"}, + {file = "pydantic_core-2.16.1-cp312-none-win_arm64.whl", hash = "sha256:d195add190abccefc70ad0f9a0141ad7da53e16183048380e688b466702195dd"}, + {file = "pydantic_core-2.16.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:502c062a18d84452858f8aea1e520e12a4d5228fc3621ea5061409d666ea1706"}, + {file = "pydantic_core-2.16.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d8c032ccee90b37b44e05948b449a2d6baed7e614df3d3f47fe432c952c21b60"}, + {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:920f4633bee43d7a2818e1a1a788906df5a17b7ab6fe411220ed92b42940f818"}, + {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f5d37ff01edcbace53a402e80793640c25798fb7208f105d87a25e6fcc9ea06"}, + {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:399166f24c33a0c5759ecc4801f040dbc87d412c1a6d6292b2349b4c505effc9"}, + {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ac89ccc39cd1d556cc72d6752f252dc869dde41c7c936e86beac5eb555041b66"}, + {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73802194f10c394c2bedce7a135ba1d8ba6cff23adf4217612bfc5cf060de34c"}, + {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8fa00fa24ffd8c31fac081bf7be7eb495be6d248db127f8776575a746fa55c95"}, + {file = "pydantic_core-2.16.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:601d3e42452cd4f2891c13fa8c70366d71851c1593ed42f57bf37f40f7dca3c8"}, + {file = "pydantic_core-2.16.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07982b82d121ed3fc1c51faf6e8f57ff09b1325d2efccaa257dd8c0dd937acca"}, + {file = "pydantic_core-2.16.1-cp38-none-win32.whl", hash = "sha256:d0bf6f93a55d3fa7a079d811b29100b019784e2ee6bc06b0bb839538272a5610"}, + {file = "pydantic_core-2.16.1-cp38-none-win_amd64.whl", hash = "sha256:fbec2af0ebafa57eb82c18c304b37c86a8abddf7022955d1742b3d5471a6339e"}, + {file = "pydantic_core-2.16.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a497be217818c318d93f07e14502ef93d44e6a20c72b04c530611e45e54c2196"}, + {file = "pydantic_core-2.16.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:694a5e9f1f2c124a17ff2d0be613fd53ba0c26de588eb4bdab8bca855e550d95"}, + {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d4dfc66abea3ec6d9f83e837a8f8a7d9d3a76d25c9911735c76d6745950e62c"}, + {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8655f55fe68c4685673265a650ef71beb2d31871c049c8b80262026f23605ee3"}, + {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21e3298486c4ea4e4d5cc6fb69e06fb02a4e22089304308817035ac006a7f506"}, + {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71b4a48a7427f14679f0015b13c712863d28bb1ab700bd11776a5368135c7d60"}, + {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dca874e35bb60ce4f9f6665bfbfad050dd7573596608aeb9e098621ac331dc"}, + {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fa496cd45cda0165d597e9d6f01e36c33c9508f75cf03c0a650018c5048f578e"}, + {file = "pydantic_core-2.16.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5317c04349472e683803da262c781c42c5628a9be73f4750ac7d13040efb5d2d"}, + {file = "pydantic_core-2.16.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:42c29d54ed4501a30cd71015bf982fa95e4a60117b44e1a200290ce687d3e640"}, + {file = "pydantic_core-2.16.1-cp39-none-win32.whl", hash = "sha256:ba07646f35e4e49376c9831130039d1b478fbfa1215ae62ad62d2ee63cf9c18f"}, + {file = "pydantic_core-2.16.1-cp39-none-win_amd64.whl", hash = "sha256:2133b0e412a47868a358713287ff9f9a328879da547dc88be67481cdac529118"}, + {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d25ef0c33f22649b7a088035fd65ac1ce6464fa2876578df1adad9472f918a76"}, + {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:99c095457eea8550c9fa9a7a992e842aeae1429dab6b6b378710f62bfb70b394"}, + {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b49c604ace7a7aa8af31196abbf8f2193be605db6739ed905ecaf62af31ccae0"}, + {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c56da23034fe66221f2208c813d8aa509eea34d97328ce2add56e219c3a9f41c"}, + {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cebf8d56fee3b08ad40d332a807ecccd4153d3f1ba8231e111d9759f02edfd05"}, + {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:1ae8048cba95f382dba56766525abca438328455e35c283bb202964f41a780b0"}, + {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:780daad9e35b18d10d7219d24bfb30148ca2afc309928e1d4d53de86822593dc"}, + {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c94b5537bf6ce66e4d7830c6993152940a188600f6ae044435287753044a8fe2"}, + {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:adf28099d061a25fbcc6531febb7a091e027605385de9fe14dd6a97319d614cf"}, + {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:644904600c15816a1f9a1bafa6aab0d21db2788abcdf4e2a77951280473f33e1"}, + {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87bce04f09f0552b66fca0c4e10da78d17cb0e71c205864bab4e9595122cb9d9"}, + {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:877045a7969ace04d59516d5d6a7dee13106822f99a5d8df5e6822941f7bedc8"}, + {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9c46e556ee266ed3fb7b7a882b53df3c76b45e872fdab8d9cf49ae5e91147fd7"}, + {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4eebbd049008eb800f519578e944b8dc8e0f7d59a5abb5924cc2d4ed3a1834ff"}, + {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:c0be58529d43d38ae849a91932391eb93275a06b93b79a8ab828b012e916a206"}, + {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b1fc07896fc1851558f532dffc8987e526b682ec73140886c831d773cef44b76"}, + {file = "pydantic_core-2.16.1.tar.gz", hash = "sha256:daff04257b49ab7f4b3f73f98283d3dbb1a65bf3500d55c7beac3c66c310fe34"}, ] [package.dependencies] @@ -1124,13 +1118,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pytest" -version = "7.4.3" +version = "7.4.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, - {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, ] [package.dependencies] @@ -1216,6 +1210,7 @@ files = [ {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_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {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"}, @@ -1266,39 +1261,39 @@ pyasn1 = ">=0.1.3" [[package]] name = "ruff" -version = "0.1.6" +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.6-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:88b8cdf6abf98130991cbc9f6438f35f6e8d41a02622cc5ee130a02a0ed28703"}, - {file = "ruff-0.1.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5c549ed437680b6105a1299d2cd30e4964211606eeb48a0ff7a93ef70b902248"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cf5f701062e294f2167e66d11b092bba7af6a057668ed618a9253e1e90cfd76"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:05991ee20d4ac4bb78385360c684e4b417edd971030ab12a4fbd075ff535050e"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87455a0c1f739b3c069e2f4c43b66479a54dea0276dd5d4d67b091265f6fd1dc"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:683aa5bdda5a48cb8266fcde8eea2a6af4e5700a392c56ea5fb5f0d4bfdc0240"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:137852105586dcbf80c1717facb6781555c4e99f520c9c827bd414fac67ddfb6"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd98138a98d48a1c36c394fd6b84cd943ac92a08278aa8ac8c0fdefcf7138f35"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a0cd909d25f227ac5c36d4e7e681577275fb74ba3b11d288aff7ec47e3ae745"}, - {file = "ruff-0.1.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8fd1c62a47aa88a02707b5dd20c5ff20d035d634aa74826b42a1da77861b5ff"}, - {file = "ruff-0.1.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fd89b45d374935829134a082617954120d7a1470a9f0ec0e7f3ead983edc48cc"}, - {file = "ruff-0.1.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:491262006e92f825b145cd1e52948073c56560243b55fb3b4ecb142f6f0e9543"}, - {file = "ruff-0.1.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ea284789861b8b5ca9d5443591a92a397ac183d4351882ab52f6296b4fdd5462"}, - {file = "ruff-0.1.6-py3-none-win32.whl", hash = "sha256:1610e14750826dfc207ccbcdd7331b6bd285607d4181df9c1c6ae26646d6848a"}, - {file = "ruff-0.1.6-py3-none-win_amd64.whl", hash = "sha256:4558b3e178145491e9bc3b2ee3c4b42f19d19384eaa5c59d10acf6e8f8b57e33"}, - {file = "ruff-0.1.6-py3-none-win_arm64.whl", hash = "sha256:03910e81df0d8db0e30050725a5802441c2022ea3ae4fe0609b76081731accbc"}, - {file = "ruff-0.1.6.tar.gz", hash = "sha256:1b09f29b16c6ead5ea6b097ef2764b42372aebe363722f1605ecbcd2b9207184"}, + {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.2" +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.2-py3-none-any.whl", hash = "sha256:1e8fdff6797d3865f37397be788a4e3cba233608e9b509382a2777d25ebde7f2"}, - {file = "setuptools-69.0.2.tar.gz", hash = "sha256:735896e78a4742605974de002ac60562d286fa8051a7e2299445e8e8fbb01aa6"}, + {file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"}, + {file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"}, ] [package.extras] @@ -1330,70 +1325,70 @@ files = [ [[package]] name = "sqlalchemy" -version = "2.0.23" +version = "2.0.25" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" files = [ - {file = "SQLAlchemy-2.0.23-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:638c2c0b6b4661a4fd264f6fb804eccd392745c5887f9317feb64bb7cb03b3ea"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3b5036aa326dc2df50cba3c958e29b291a80f604b1afa4c8ce73e78e1c9f01d"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:787af80107fb691934a01889ca8f82a44adedbf5ef3d6ad7d0f0b9ac557e0c34"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c14eba45983d2f48f7546bb32b47937ee2cafae353646295f0e99f35b14286ab"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0666031df46b9badba9bed00092a1ffa3aa063a5e68fa244acd9f08070e936d3"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:89a01238fcb9a8af118eaad3ffcc5dedaacbd429dc6fdc43fe430d3a941ff965"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-win32.whl", hash = "sha256:cabafc7837b6cec61c0e1e5c6d14ef250b675fa9c3060ed8a7e38653bd732ff8"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-win_amd64.whl", hash = "sha256:87a3d6b53c39cd173990de2f5f4b83431d534a74f0e2f88bd16eabb5667e65c6"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d5578e6863eeb998980c212a39106ea139bdc0b3f73291b96e27c929c90cd8e1"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:62d9e964870ea5ade4bc870ac4004c456efe75fb50404c03c5fd61f8bc669a72"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c80c38bd2ea35b97cbf7c21aeb129dcbebbf344ee01a7141016ab7b851464f8e"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75eefe09e98043cff2fb8af9796e20747ae870c903dc61d41b0c2e55128f958d"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd45a5b6c68357578263d74daab6ff9439517f87da63442d244f9f23df56138d"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a86cb7063e2c9fb8e774f77fbf8475516d270a3e989da55fa05d08089d77f8c4"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-win32.whl", hash = "sha256:b41f5d65b54cdf4934ecede2f41b9c60c9f785620416e8e6c48349ab18643855"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-win_amd64.whl", hash = "sha256:9ca922f305d67605668e93991aaf2c12239c78207bca3b891cd51a4515c72e22"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d0f7fb0c7527c41fa6fcae2be537ac137f636a41b4c5a4c58914541e2f436b45"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7c424983ab447dab126c39d3ce3be5bee95700783204a72549c3dceffe0fc8f4"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f508ba8f89e0a5ecdfd3761f82dda2a3d7b678a626967608f4273e0dba8f07ac"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6463aa765cf02b9247e38b35853923edbf2f6fd1963df88706bc1d02410a5577"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e599a51acf3cc4d31d1a0cf248d8f8d863b6386d2b6782c5074427ebb7803bda"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fd54601ef9cc455a0c61e5245f690c8a3ad67ddb03d3b91c361d076def0b4c60"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-win32.whl", hash = "sha256:42d0b0290a8fb0165ea2c2781ae66e95cca6e27a2fbe1016ff8db3112ac1e846"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-win_amd64.whl", hash = "sha256:227135ef1e48165f37590b8bfc44ed7ff4c074bf04dc8d6f8e7f1c14a94aa6ca"}, - {file = "SQLAlchemy-2.0.23-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:14aebfe28b99f24f8a4c1346c48bc3d63705b1f919a24c27471136d2f219f02d"}, - {file = "SQLAlchemy-2.0.23-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e983fa42164577d073778d06d2cc5d020322425a509a08119bdcee70ad856bf"}, - {file = "SQLAlchemy-2.0.23-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e0dc9031baa46ad0dd5a269cb7a92a73284d1309228be1d5935dac8fb3cae24"}, - {file = "SQLAlchemy-2.0.23-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5f94aeb99f43729960638e7468d4688f6efccb837a858b34574e01143cf11f89"}, - {file = "SQLAlchemy-2.0.23-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:63bfc3acc970776036f6d1d0e65faa7473be9f3135d37a463c5eba5efcdb24c8"}, - {file = "SQLAlchemy-2.0.23-cp37-cp37m-win32.whl", hash = "sha256:f48ed89dd11c3c586f45e9eec1e437b355b3b6f6884ea4a4c3111a3358fd0c18"}, - {file = "SQLAlchemy-2.0.23-cp37-cp37m-win_amd64.whl", hash = "sha256:1e018aba8363adb0599e745af245306cb8c46b9ad0a6fc0a86745b6ff7d940fc"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:64ac935a90bc479fee77f9463f298943b0e60005fe5de2aa654d9cdef46c54df"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c4722f3bc3c1c2fcc3702dbe0016ba31148dd6efcd2a2fd33c1b4897c6a19693"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4af79c06825e2836de21439cb2a6ce22b2ca129bad74f359bddd173f39582bf5"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:683ef58ca8eea4747737a1c35c11372ffeb84578d3aab8f3e10b1d13d66f2bc4"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d4041ad05b35f1f4da481f6b811b4af2f29e83af253bf37c3c4582b2c68934ab"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aeb397de65a0a62f14c257f36a726945a7f7bb60253462e8602d9b97b5cbe204"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-win32.whl", hash = "sha256:42ede90148b73fe4ab4a089f3126b2cfae8cfefc955c8174d697bb46210c8306"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-win_amd64.whl", hash = "sha256:964971b52daab357d2c0875825e36584d58f536e920f2968df8d581054eada4b"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:616fe7bcff0a05098f64b4478b78ec2dfa03225c23734d83d6c169eb41a93e55"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0e680527245895aba86afbd5bef6c316831c02aa988d1aad83c47ffe92655e74"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9585b646ffb048c0250acc7dad92536591ffe35dba624bb8fd9b471e25212a35"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4895a63e2c271ffc7a81ea424b94060f7b3b03b4ea0cd58ab5bb676ed02f4221"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cc1d21576f958c42d9aec68eba5c1a7d715e5fc07825a629015fe8e3b0657fb0"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:967c0b71156f793e6662dd839da54f884631755275ed71f1539c95bbada9aaab"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-win32.whl", hash = "sha256:0a8c6aa506893e25a04233bc721c6b6cf844bafd7250535abb56cb6cc1368884"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-win_amd64.whl", hash = "sha256:f3420d00d2cb42432c1d0e44540ae83185ccbbc67a6054dcc8ab5387add6620b"}, - {file = "SQLAlchemy-2.0.23-py3-none-any.whl", hash = "sha256:31952bbc527d633b9479f5f81e8b9dfada00b91d6baba021a869095f1a97006d"}, - {file = "SQLAlchemy-2.0.23.tar.gz", hash = "sha256:c1bda93cbbe4aa2aa0aa8655c5aeda505cd219ff3e8da91d1d329e143e4aff69"}, + {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.2.0" +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)"] +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)"] @@ -1403,7 +1398,7 @@ mssql-pyodbc = ["pyodbc"] mypy = ["mypy (>=0.910)"] mysql = ["mysqlclient (>=1.4.0)"] mysql-connector = ["mysql-connector-python"] -oracle = ["cx-oracle (>=8)"] +oracle = ["cx_oracle (>=8)"] oracle-oracledb = ["oracledb (>=1.0.1)"] postgresql = ["psycopg2 (>=2.7)"] postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] @@ -1413,7 +1408,7 @@ postgresql-psycopg2binary = ["psycopg2-binary"] postgresql-psycopg2cffi = ["psycopg2cffi"] postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] pymysql = ["pymysql"] -sqlcipher = ["sqlcipher3-binary"] +sqlcipher = ["sqlcipher3_binary"] [[package]] name = "stack-data" @@ -1465,28 +1460,28 @@ files = [ [[package]] name = "traitlets" -version = "5.13.0" +version = "5.14.1" description = "Traitlets Python configuration system" optional = false python-versions = ">=3.8" files = [ - {file = "traitlets-5.13.0-py3-none-any.whl", hash = "sha256:baf991e61542da48fe8aef8b779a9ea0aa38d8a54166ee250d5af5ecf4486619"}, - {file = "traitlets-5.13.0.tar.gz", hash = "sha256:9b232b9430c8f57288c1024b34a8f0251ddcc47268927367a0dd3eeaca40deb5"}, + {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.6.0)", "pre-commit", "pytest (>=7.0,<7.5)", "pytest-mock", "pytest-mypy-testing"] +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.8.0" +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.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, - {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, + {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]] @@ -1510,13 +1505,13 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", [[package]] name = "virtualenv" -version = "20.24.7" +version = "20.25.0" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.24.7-py3-none-any.whl", hash = "sha256:a18b3fd0314ca59a2e9f4b556819ed07183b3e9a3702ecfe213f593d44f7b3fd"}, - {file = "virtualenv-20.24.7.tar.gz", hash = "sha256:69050ffb42419c91f6c1284a7b24e0475d793447e35929b488bf6a0aade39353"}, + {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"}, + {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"}, ] [package.dependencies] @@ -1530,16 +1525,16 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [[package]] name = "wcwidth" -version = "0.2.12" +version = "0.2.13" description = "Measures the displayed width of unicode strings in a terminal" optional = false python-versions = "*" files = [ - {file = "wcwidth-0.2.12-py2.py3-none-any.whl", hash = "sha256:f26ec43d96c8cbfed76a5075dac87680124fa84e0855195a6184da9c187f133c"}, - {file = "wcwidth-0.2.12.tar.gz", hash = "sha256:f01c104efdf57971bcb756f054dd58ddec5204dd15fa31d6503ea57947d97c02"}, + {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" -content-hash = "f57d78c781f935bf7e568b5e6fe55dc7e4a1bcf522027d417da711d98b73e0a2" +content-hash = "955a650a5aa137470ed74c366afeb94a0dce3020c63419c0430fccdb0691a3a3" diff --git a/pyproject.toml b/pyproject.toml index 9c8158d..7b1294b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,9 +8,9 @@ readme = "README.md" [tool.poetry.dependencies] python = "^3.8" -pathlib = "^1.0.1" httpx = "^0.25.2" python-decouple = "^3.8" +pathlib2 = "^2.3.7.post1" [tool.poetry.group.standalone.dependencies] diff --git a/requirements.txt b/requirements.txt index a3d5f2d..1e93d5d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,32 @@ +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.5 ; python_version >= "3.8" and python_version < "4.0" -pathlib==1.0.1 ; 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" From 9112657dd05c4aef8f53f1e03a5166d0a97ccd5d Mon Sep 17 00:00:00 2001 From: Quim Date: Sun, 4 Feb 2024 15:51:22 +0100 Subject: [PATCH 24/43] 404 on game uuid not found --- battlechess/server/btchApi.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/battlechess/server/btchApi.py b/battlechess/server/btchApi.py index aa75103..63dfd81 100644 --- a/battlechess/server/btchApi.py +++ b/battlechess/server/btchApi.py @@ -490,11 +490,16 @@ def get_snap( ): game = get_game(gameUUID, current_user, db) # user not allowed to query that game snap for now + 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] ): - game = None - if not game: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="game not found", From b48da809f0079502ba5b9dbfd162ee3ecc4e6531 Mon Sep 17 00:00:00 2001 From: Quim Date: Mon, 5 Feb 2024 15:51:35 +0100 Subject: [PATCH 25/43] create game and user created_at default to now --- battlechess/server/models.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/battlechess/server/models.py b/battlechess/server/models.py index 2753456..1c637a2 100644 --- a/battlechess/server/models.py +++ b/battlechess/server/models.py @@ -1,3 +1,4 @@ +import datetime from fastapi import HTTPException, status from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String from sqlalchemy.orm import relationship @@ -20,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") @@ -35,7 +36,7 @@ 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")) From 0f87ec0eb7f9af7d3c2d2fc69f39241c699eef84 Mon Sep 17 00:00:00 2001 From: Quim Date: Sat, 10 Feb 2024 13:42:13 +0100 Subject: [PATCH 26/43] :warning: remove passlib since it's deprecated might need recomputing password in db? --- battlechess/server/utils.py | 19 +++++++++++-------- poetry.lock | 22 +--------------------- pyproject.toml | 2 +- 3 files changed, 13 insertions(+), 30 deletions(-) diff --git a/battlechess/server/utils.py b/battlechess/server/utils.py index 31275a1..e3d7071 100644 --- a/battlechess/server/utils.py +++ b/battlechess/server/utils.py @@ -1,20 +1,23 @@ import random import string - -from passlib.context import CryptContext +import bcrypt from battlechess.server.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) + # 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): - return pwd_context.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 diff --git a/poetry.lock b/poetry.lock index e74d971..bbc32a6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -746,26 +746,6 @@ files = [ qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] testing = ["docopt", "pytest (<6.0.0)"] -[[package]] -name = "passlib" -version = "1.7.4" -description = "comprehensive password hashing framework supporting over 30 schemes" -optional = false -python-versions = "*" -files = [ - {file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"}, - {file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"}, -] - -[package.dependencies] -bcrypt = {version = ">=3.1.0", optional = true, markers = "extra == \"bcrypt\""} - -[package.extras] -argon2 = ["argon2-cffi (>=18.2.0)"] -bcrypt = ["bcrypt (>=3.1.0)"] -build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"] -totp = ["cryptography"] - [[package]] name = "pathlib2" version = "2.3.7.post1" @@ -1537,4 +1517,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "955a650a5aa137470ed74c366afeb94a0dce3020c63419c0430fccdb0691a3a3" +content-hash = "907aa7c5252f2feabb285454676e0665a80c78fe444dac2ceedf50ca89e42518" diff --git a/pyproject.toml b/pyproject.toml index 7b1294b..cb518a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ python = "^3.8" httpx = "^0.25.2" python-decouple = "^3.8" pathlib2 = "^2.3.7.post1" +bcrypt = "^4.1.2" [tool.poetry.group.standalone.dependencies] @@ -20,7 +21,6 @@ pygame = "^2.5.2" [tool.poetry.group.api.dependencies] fastapi = "^0.104.1" sqlalchemy = "^2.0.23" -passlib = {extras = ["bcrypt"], version = "^1.7.4"} python-jose = {extras = ["cryptography"], version = "^3.3.0"} uvicorn = "^0.24.0.post1" python-multipart = "^0.0.6" From 6241bdb35a79ea5717ec442a48b3521a957a9baa Mon Sep 17 00:00:00 2001 From: Quim Date: Thu, 8 Feb 2024 19:41:44 +0100 Subject: [PATCH 27/43] long poll semi-test --- battlechess/server/btchApi.py | 16 +++++++++++++++- tests/server/test_btchApi.py | 23 +++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/battlechess/server/btchApi.py b/battlechess/server/btchApi.py index 63dfd81..2d749ea 100644 --- a/battlechess/server/btchApi.py +++ b/battlechess/server/btchApi.py @@ -1,3 +1,4 @@ +import time from datetime import timedelta from typing import List, Union @@ -405,9 +406,22 @@ def query_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) - return game.turn + if not long_polling: + return game.turn + + print("long polling") + start = time.time() + elapsed = 0 + while elapsed < 10: + elapsed = time.time() - start + game = get_game(gameUUID, current_user, db) + if game.turn != game.get_player_color(current_user.id): + print(f"{current_user.username} is {game.get_player_color(current_user.id)} is not {game.turn}") + return game.turn + time.sleep(1) @app.post("/games/{gameUUID}/move", response_model=schemas.GameSnap) diff --git a/tests/server/test_btchApi.py b/tests/server/test_btchApi.py index a5b61b6..c4e3bc7 100644 --- a/tests/server/test_btchApi.py +++ b/tests/server/test_btchApi.py @@ -1,4 +1,6 @@ import sys +import time +import asyncio import unittest import unittest.mock as mock from datetime import datetime, timedelta, timezone @@ -1006,6 +1008,27 @@ def test__getTurn(self): self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), "black") + @unittest.skip("slow test") + def test__getTurn__long_polling(self): + firstgame_uuid, _ = self.classicSetup() + token = self.getToken("janedoe") + + start = time.time() + response = self.client.get( + f"/games/{firstgame_uuid}/turn", + headers={ + "Authorization": "Bearer " + token, + "Content-Type": "application/json", + }, + params={ + "long_polling": True + } + ) + + elapsed = time.time() - start + self.assertGreater(elapsed,5) + + def test__move(self): firstgame_uuid, token = self.classicSetup() From 75e5dbf03e5bb25970f7bf1a7ef73dbad5b0cbdd Mon Sep 17 00:00:00 2001 From: Quim Date: Thu, 8 Feb 2024 21:15:30 +0100 Subject: [PATCH 28/43] switch from unittest to pytest --- tests/server/conftest.py | 68 + tests/server/test_btchApi.py | 3013 ++++++++++++------------- tests/server/test_btchApi_autoplay.py | 368 +-- 3 files changed, 1596 insertions(+), 1853 deletions(-) create mode 100644 tests/server/conftest.py diff --git a/tests/server/conftest.py b/tests/server/conftest.py new file mode 100644 index 0000000..a0daa9c --- /dev/null +++ b/tests/server/conftest.py @@ -0,0 +1,68 @@ +import pytest +from datetime import datetime, timedelta, timezone + +from fastapi.testclient import TestClient +from sqlalchemy import create_engine +from sqlalchemy.orm import Session +from sqlalchemy_utils import create_database +from sqlalchemy_utils import 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, verify_password + +@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() + + # 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 + +def getToken(username): + return crud.create_access_token( + data={"sub": username}, expires_delta=timedelta(minutes=3000) + ) + +@pytest.fixture(scope="function") +def game_setup(db): + _, _ = addFakeUsers(db) + jane_token = getToken("janedoe") + john_token = getToken("johndoe") + addFakeGames(db, fakegamesdb()) + firstgame_uuid = list(fakegamesdb().values())[0]["uuid"] + addFakeGameSnaps(db, fakegamesnapsdb()) + + return firstgame_uuid, john_token, jane_token + + diff --git a/tests/server/test_btchApi.py b/tests/server/test_btchApi.py index c4e3bc7..7086fb7 100644 --- a/tests/server/test_btchApi.py +++ b/tests/server/test_btchApi.py @@ -1,7 +1,7 @@ import sys import time import asyncio -import unittest +import pytest import unittest.mock as mock from datetime import datetime, timedelta, timezone from pathlib import Path @@ -14,7 +14,6 @@ except ImportError: print("PIL module is not installed. Some tests will be skipped") - from fastapi.testclient import TestClient from battlechess.server import crud, models @@ -23,1640 +22,1512 @@ from battlechess.server.schemas import GameStatus from battlechess.server.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... +def testDataDir(): + return Path(__file__).parent.parent / "data" / "avatars" - # 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 janedoe(self): - return { +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, - "email": "janedoe@example.com", - "full_name": "Jane Doe", + "created_at": datetime(2021, 1, 1, tzinfo=timezone.utc), + }, + "janedoe": { "username": "janedoe", - } - - def johndoe(self): - return { + "full_name": "Jane Doe", + "email": "janedoe@example.com", + "hashed_password": get_password_hash("secret"), + "disabled": False, "avatar": None, - "email": "johndoe@example.com", - "full_name": "John Doe", - "username": "johndoe", - } - - 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"] 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 - - 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): - 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): - 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( - "/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): - 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 / "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) - - 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"], "int_parsing") - - 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", - }, - params={ - "random": False, - }, - ) - - print(response.json()) - self.assertEqual(response.status_code, 200) - self.assertDictEqual( - response.json(), - { - "black": None, - "created_at": mock.ANY, - "uuid": mock.ANY, - "id": 5, - "owner": self.johndoe(), - "last_move_time": None, - "public": False, - "status": GameStatus.WAITING, - "turn": "white", - "white": self.johndoe(), - "winner": None, - }, - ) - - def test__get_me_games(self): - token, _ = self.addFakeUsers(self.db) - self.addFakeGames(self.db, self.fakegamesdb()) - - response = self.client.get( - "/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": self.janedoe(), - "created_at": mock.ANY, - "uuid": mock.ANY, - "id": 1, - "last_move_time": None, - "owner": self.johndoe(), - "public": False, - "status": "started", - "turn": "black", - "white": self.johndoe(), - "winner": None, - }, - ) - - def test__getGames__finishedGame(self): - _, _ = self.addFakeUsers(self.db) - # change to second player - jane_token = self.getToken("janedoe") - _ = 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) - 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) - game_dict = response.json() - print(game_dict) - self.assertEqual(response.status_code, 200) - self.assertNotEqual(game_dict, {}) - self.assertTrue( - game_dict["white"]["username"] == oneUser.username - or game_dict["black"]["username"] == oneUser.username - ) - self.assertDictEqual( - 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(self): - token, _ = self.addFakeUsers(self.db) - gamesdbmod = self.fakegamesdb() - gamesdbmod["123fr12339"]["status"] = "done" - gamesdbmod["da40a3ee5e"]["status"] = "done" - 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, 404) - self.assertDictEqual( - response.json(), {"detail": "available random game not found"} - ) - - def test__get_available_games__all(self): - token, _ = self.addFakeUsers(self.db) - _ = 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) - game_ids = [game["id"] for game in response.json()] - self.assertListEqual(game_ids, [1, 2, 3, 4, 5]) - - def test__get_available_games__waiting(self): - token, _ = self.addFakeUsers(self.db) - _ = self.addFakeGames(self.db, self.fakegamesdb()) - - response = self.client.get( - "/games", - headers={ - "Authorization": "Bearer " + token, - "Content-Type": "application/json", - }, - params={"status": ["waiting"]}, - ) - - print(response.json()) - self.assertEqual(response.status_code, 200) - game_ids = [(game["id"], game["status"]) for game in response.json()] - self.assertListEqual( - game_ids, - [(3, GameStatus.WAITING), (4, GameStatus.WAITING), (5, GameStatus.WAITING)], - ) - - # TODO this test was deactivated by mistake - def _test__joinGame__playerAlreadyInGame(self): - token, username = self.addFakeUsers(self.db) - self.addFakeGames(self.db, self.fakegamesdb()) - - uuid = "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__simple(self): - token, username = self.addFakeUsers(self.db) - uuid = self.addFakeGames(self.db, self.fakegamesdb()) - - crud.get_user_by_username(self.db, username) - - (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, 200) - - 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") - - @unittest.skip("slow test") - def test__getTurn__long_polling(self): - firstgame_uuid, _ = self.classicSetup() - token = self.getToken("janedoe") - - start = time.time() - response = self.client.get( - f"/games/{firstgame_uuid}/turn", - headers={ - "Authorization": "Bearer " + token, - "Content-Type": "application/json", - }, - params={ - "long_polling": True - } - ) - - elapsed = time.time() - start - self.assertGreater(elapsed,5) - - - 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, - ( + "created_at": datetime(2021, 1, 1, tzinfo=timezone.utc), + }, + } + return fake_users_db + +def janedoe(): + return { + "avatar": None, + "email": "janedoe@example.com", + "full_name": "Jane Doe", + "username": "janedoe", + } + +def johndoe(): + return { + "avatar": None, + "email": "johndoe@example.com", + "full_name": "John Doe", + "username": "johndoe", + } + +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" - "PPP_PPPP" + "PPPPPPPP" "________" - "___P____" - "___p____" "________" - "ppp_pppp" + "________" + "________" + "pppppppp" "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"], - ( + "taken": "", + "castleable": "", + "move_number": 0, + "created_at": datetime(2021, 4, 5, 0, tzinfo=timezone.utc), + }, + { + "game_uuid": "lkml4a3.d3", + "move": "d2d4", + "board": ( "RNBQKBNR" - "PPP_PPPP" + "PPPPPPPP" + "________" "________" - "___P____" "___p____" "________" - "XXX_XXXX" - "XXXXXXXX" + "ppp_pppp" + "rnbqkbnr" ), + "taken": "", + "castleable": "", + "move_number": 1, + "created_at": datetime(2021, 4, 5, 10, tzinfo=timezone.utc), + }, + ] + return fake_games_snaps + +def addFakeUsers(db): + 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 addFakeGames(db, fakegamesdb): + for uuid, game in fakegamesdb.items(): + owner = ( + db.query(models.User) + .filter(models.User.username == game["owner"]) + .first() ) - - 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", - }, + white = ( + db.query(models.User) + .filter(models.User.username == game["white"]) + .first() ) - - print("response: {}".format(response.json())) - self.assertEqual(response.status_code, 200) - - self.assertListEqual( - response.json(), - ["c3", "d3", "e3"], + black = ( + db.query(models.User) + .filter(models.User.username == game["black"]) + .first() ) - - def test__possibleMoves__pawn_enpassant_white(self): - firstgame_uuid, token = self.classicSetup() - - move = "d7d5" - boardStr = ( - "____K___" - "________" - "________" - "__pP____" - "________" - "____pk__" - "_____pp_" - "________" + 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 - self.addCustomGameSnap(self.db, boardStr, move) - - square = "c5" +def addFakeGameSnaps(db, fakegamesnaps): + # TODO get game from uuid + for snap in fakegamesnaps: + guuid = snap["game_uuid"] - 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) + game = crud.get_game_by_uuid(db, guuid) - self.assertListEqual( - response.json(), - ["c6", "d6"], + 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 test__possibleMoves__pawn_impossible_enpassant_black(self): - firstgame_uuid, _ = self.classicSetup() - - token = self.getToken("janedoe") - - move = "c7c5" - boardStr = ( - "____K___" - "________" - "________" - "__pP____" - "________" - "____pk__" - "_____pp_" - "________" - ) +def addCustomGameSnap(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(username): + return crud.create_access_token( + data={"sub": username}, expires_delta=timedelta(minutes=3000) + ) + +def classicSetup(db): + token, _ = addFakeUsers(db) + addFakeGames(db, fakegamesdb()) + firstgame_uuid = list(fakegamesdb().values())[0]["uuid"] + addFakeGameSnaps(db, fakegamesnapsdb()) + + return firstgame_uuid, token + +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): + token, _ = addFakeUsers(db) + + 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): + token, _ = addFakeUsers(db) + + oneUser = db.query(models.User)[1] + filename = testDataDir() / "test_avatar.jpeg" + with open(filename, "rb") as f: + img = Image.open(f) + try: + img.verify() + except (IOError, SyntaxError): + print("Bad file:", filename) - self.addCustomGameSnap(self.db, boardStr, move) + # 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): + token, _ = addFakeUsers(db) + + 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(db) + +def test__getUsernames(db, client): + token, _ = addFakeUsers(db) + + 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): + token, _ = addFakeUsers(db) + + 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): + token, _ = addFakeUsers(db) + + 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): + token, _ = addFakeUsers(db) + + 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): + token, _ = addFakeUsers(db) + uuid = addFakeGames(db, fakegamesdb()) + + 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): + token, _ = addFakeUsers(db) + addFakeGames(db, fakegamesdb()) + + 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(db) + # change to second player + jane_token = getToken("janedoe") + _ = getToken("johndoe") + addFakeGames(db, fakegamesdb()) + addFakeGameSnaps(db, fakegamesnapsdb()) + + 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): + token, _ = addFakeUsers(db) + addFakeGames(db, fakegamesdb()) + + 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): + token, _ = addFakeUsers(db) + gamesdbmod = fakegamesdb() + gamesdbmod["123fr12339"]["status"] = "done" + gamesdbmod["da40a3ee5e"]["status"] = "done" + addFakeGames(db, gamesdbmod) + + 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): + token, _ = addFakeUsers(db) + _ = addFakeGames(db, fakegamesdb()) + + 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): + token, _ = addFakeUsers(db) + _ = addFakeGames(db, fakegamesdb()) + + 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): + token, username = addFakeUsers(db) + addFakeGames(db, fakegamesdb()) + + 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): + token, username = addFakeUsers(db) + uuid = addFakeGames(db, fakegamesdb()) + + 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): + firstgame_uuid, token = classicSetup(db) + firstgame_white_player = list(fakegamesdb().values())[0]["white"] + token = getToken(firstgame_white_player) + addFakeGameSnaps(db, fakegamesnapsdb()) + + 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': '', + 'move_number': 0, + 'board': ('xxxxxxxx' + 'xxxxxxxx' + '________' + '________' + '________' + '________' + 'pppppppp' + 'rnbqkbnr'), + } + # yapf: enable + +def test__getsnaps(db, client): + firstgame_uuid, token = classicSetup(db) + + 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': '', + '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(db, client): + firstgame_uuid, token = classicSetup(db) + + 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': '', + 'move_number': 1, + 'board': ('xxxxxxxx' + 'xxxxxxxx' + '________' + '________' + '___p____' + '________' + 'ppp_pppp' + 'rnbqkbnr') + } + # yapf: enable + +def test__getTurn(db, client): + firstgame_uuid, token = classicSetup(db) + + 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" + +@unittest.skip("slow test") +def test__getTurn__long_polling(client): + firstgame_uuid, _ = classicSetup(db) + token = getToken("janedoe") + + start = time.time() + response = 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): + firstgame_uuid, token = classicSetup(db) + + # 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 " + 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__filtered(db, client): + firstgame_uuid, _ = classicSetup(db) + # 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): + firstgame_uuid, _ = classicSetup(db) + # 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"] + +def test__possibleMoves__king(db, client): + firstgame_uuid, token = classicSetup(db) + + move = "g3f3" + boardStr = ( + "____K___" + "________" + "________" + "__p_____" + "________" + "____pk__" + "___P_pp_" + "________" + ) + + addCustomGameSnap(db, boardStr, move) + + 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"] + +def test__possibleMoves__pawn_enpassant_black(db, client): + firstgame_uuid, _ = classicSetup(db) + + token = getToken("janedoe") + + move = "c2c4" + boardStr = ( + "____K___" + "________" + "________" + "________" + "__pP____" + "____pk__" + "_____pp_" + "________" + ) + + addCustomGameSnap(db, boardStr, move) + + 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"] + +def test__possibleMoves__pawn_enpassant_white(db, client): + firstgame_uuid, token = classicSetup(db) + + move = "d7d5" + boardStr = ( + "____K___" + "________" + "________" + "__pP____" + "________" + "____pk__" + "_____pp_" + "________" + ) + + addCustomGameSnap(db, boardStr, move) + + 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"] + +def test__possibleMoves__pawn_impossible_enpassant_black(db, client): + firstgame_uuid, _ = classicSetup(db) + + token = getToken("janedoe") + + move = "c7c5" + boardStr = ( + "____K___" + "________" + "________" + "__pP____" + "________" + "____pk__" + "_____pp_" + "________" + ) + + addCustomGameSnap(db, boardStr, move) + + 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"] + +def test__possibleMoves__pawn_take(db, client): + firstgame_uuid, token = classicSetup(db) + + move = "f5f6" + boardStr = ( + "____K___" + "_____PP_" + "_____p__" + "________" + "________" + "_____k__" + "_____pp_" + "________" + ) + + addCustomGameSnap(db, boardStr, move) + + 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): + _, _ = addFakeUsers(db) + # change to second player + jane_token = getToken("janedoe") + john_token = getToken("johndoe") + addFakeGames(db, fakegamesdb()) + firstgame_uuid = list(fakegamesdb().values())[0]["uuid"] + addFakeGameSnaps(db, fakegamesnapsdb()) + + 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): + _, _ = addFakeUsers(db) + # change to second player + jane_token = getToken("janedoe") + john_token = getToken("johndoe") + addFakeGames(db, fakegamesdb()) + firstgame_uuid = list(fakegamesdb().values())[0]["uuid"] + addFakeGameSnaps(db, 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 = 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", + }, + ) - square = "d5" + john_username = response.json()["username"] - response = self.client.get( - f"/games/{firstgame_uuid}/moves/{square}", - headers={ - "Authorization": "Bearer " + token, - "Content-Type": "application/json", - }, - ) + assert response.status_code == 200 - print("response: {}".format(response.json())) - self.assertEqual(response.status_code, 200) + response = client.post( + "/users/", + json={ + "username": "janedoe", + "full_name": "Jane Le Dow", + "email": "jane@doe.cat", + "plain_password": "secret", + }, + ) - self.assertListEqual( - response.json(), - ["d4"], - ) + jane_username = response.json()["username"] - def test__possibleMoves__pawn_take(self): - firstgame_uuid, token = self.classicSetup() - - move = "f5f6" - boardStr = ( - "____K___" - "_____PP_" - "_____p__" - "________" - "________" - "_____k__" - "_____pp_" - "________" - ) + assert response.status_code == 200 - self.addCustomGameSnap(self.db, boardStr, move) + # authenticate + response = client.post( + "/token", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + data={ + "username": "johndoe", + "password": "secret", + }, + ) - square = "f6" + assert response.status_code == 200 + john_token = response.json()["access_token"] - response = self.client.get( - f"/games/{firstgame_uuid}/moves/{square}", - headers={ - "Authorization": "Bearer " + token, - "Content-Type": "application/json", - }, + 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 - print("response: {}".format(response.json())) - self.assertEqual(response.status_code, 200) - - self.assertListEqual( - response.json(), - ["g7"], - ) + # they ask for game and turn - def send_move(self, game_uuid, move, token): - response = self.client.post( - f"/games/{game_uuid}/move", + response = client.get( + f"/games/{game_uuid}/turn", headers={ - "Authorization": "Bearer " + token, + "Authorization": "Bearer " + jane_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_username = response.json()["username"] - - 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_username = response.json()["username"] - - 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/", + assert response.status_code == 200 + jane_turn = response.json() + response = client.get( + f"/games/{game_uuid}/turn", 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"] + assert response.status_code == 200 + john_turn = response.json() - # john already joined and jane joins game + # TODO what happens after checkmate? + assert jane_turn == john_turn - # check if game started - response = self.client.get( - f"/games/{game_uuid}", + response = client.get( + f"/games/{game_uuid}/snap", headers={ "Authorization": "Bearer " + jane_token, "Content-Type": "application/json", }, ) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()["status"], GameStatus.WAITING) + print(prettyBoard(response.json()["board"])) - 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" - ) + # 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": + assert response.json()["board"] == boards[i][0] + else: + assert response.json()["board"] == boards[i][1] - print(f"jane color is {jane_color}") - - response = self.client.get( - f"/games/{game_uuid}/join", + response = client.get( + f"/games/{game_uuid}/snap", headers={ - "Authorization": "Bearer " + jane_token, + "Authorization": "Bearer " + john_token, "Content-Type": "application/json", }, ) - self.assertEqual(response.status_code, 200) + if john_turn or jane_turn: + if john_color == "white": + assert response.json()["board"] == boards[i][0] + else: + assert response.json()["board"]== boards[i][1] - print(response.json()) - - # john send move - # jane send move + # checkmate - moves = ["f2f3", "e7e5", "g2g4", "d8h4", "f3f4", "h4e1"] + response = client.get( + f"/games/{game_uuid}", + headers={ + "Authorization": "Bearer " + jane_token, + "Content-Type": "application/json", + }, + ) - 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, ) + ) - print( - "{} with {} won the game".format( - "janedoe" if jane_color == response.json()["winner"] else "johndoe", - jane_color, - ) - ) + assert response.json()["winner"] == "black" - self.assertEqual(response.json()["winner"], "black") diff --git a/tests/server/test_btchApi_autoplay.py b/tests/server/test_btchApi_autoplay.py index d5fb60a..d7200d1 100644 --- a/tests/server/test_btchApi_autoplay.py +++ b/tests/server/test_btchApi_autoplay.py @@ -1,5 +1,5 @@ import json -import unittest +import pytest from datetime import datetime, timedelta, timezone from fastapi.testclient import TestClient @@ -12,289 +12,93 @@ from battlechess.server.schemas import GameStatus from battlechess.server.utils import get_password_hash - -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"] 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 - - 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]) +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(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/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 = [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 = send_move(client, firstgame_uuid, move, tokens[0 % 2]) if response.status_code == 200: - self.prettyBoard(response.json()["board"]) + 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) + assert response.status_code == 200 - self.resetGame(self.db, firstgame_uuid) + db.rollback() + #resetGame(db, firstgame_uuid) From 3f6e3af7a39e22f6d030e250160c3c45c5abdfcb Mon Sep 17 00:00:00 2001 From: Quim Date: Thu, 8 Feb 2024 23:26:25 +0100 Subject: [PATCH 29/43] moved all functions to fixtures --- tests/server/conftest.py | 265 +++++++++++++++++++- tests/server/test_btchApi.py | 463 ++++++++--------------------------- 2 files changed, 366 insertions(+), 362 deletions(-) diff --git a/tests/server/conftest.py b/tests/server/conftest.py index a0daa9c..110ecaf 100644 --- a/tests/server/conftest.py +++ b/tests/server/conftest.py @@ -13,6 +13,262 @@ from battlechess.server.schemas import GameStatus from battlechess.server.utils import get_password_hash, verify_password +@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": "", + "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 + +@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 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" @@ -54,14 +310,13 @@ def getToken(username): data={"sub": username}, expires_delta=timedelta(minutes=3000) ) + @pytest.fixture(scope="function") -def game_setup(db): - _, _ = addFakeUsers(db) +def game_setup(db, addFakeUsers, addFakeGames, addFakeGameSnaps, fakegamesdb): + _, _ = addFakeUsers jane_token = getToken("janedoe") john_token = getToken("johndoe") - addFakeGames(db, fakegamesdb()) - firstgame_uuid = list(fakegamesdb().values())[0]["uuid"] - addFakeGameSnaps(db, fakegamesnapsdb()) + 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 index 7086fb7..8b40274 100644 --- a/tests/server/test_btchApi.py +++ b/tests/server/test_btchApi.py @@ -26,242 +26,11 @@ def testDataDir(): return Path(__file__).parent.parent / "data" / "avatars" -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 - -def janedoe(): - return { - "avatar": None, - "email": "janedoe@example.com", - "full_name": "Jane Doe", - "username": "janedoe", - } - -def johndoe(): - return { - "avatar": None, - "email": "johndoe@example.com", - "full_name": "John Doe", - "username": "johndoe", - } - -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": "", - "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(db): - 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 addFakeGames(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"] 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 - -def addFakeGameSnaps(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(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(username): return crud.create_access_token( data={"sub": username}, expires_delta=timedelta(minutes=3000) ) -def classicSetup(db): - token, _ = addFakeUsers(db) - addFakeGames(db, fakegamesdb()) - firstgame_uuid = list(fakegamesdb().values())[0]["uuid"] - addFakeGameSnaps(db, fakegamesnapsdb()) - - return firstgame_uuid, token - def test__version(client): response = client.get("/version") assert response.status_code == 200 @@ -319,8 +88,8 @@ def test__create_user__with_avatar(client): # TODO fix the put method for user -def _test__update_user__full_name(db, client): - token, _ = addFakeUsers(db) +def _test__update_user__full_name(db, client, addFakeUsers): + token, _ = addFakeUsers oneUser = db.query(models.User)[1] @@ -351,8 +120,8 @@ def _test__update_user__full_name(db, client): } @pytest.mark.skipif("PIL" not in sys.modules, reason="PIL module is not installed") -def test__upload_user__avatarImage(db, client): - token, _ = addFakeUsers(db) +def test__upload_user__avatarImage(db, client, addFakeUsers): + token, _ = addFakeUsers oneUser = db.query(models.User)[1] filename = testDataDir() / "test_avatar.jpeg" @@ -382,8 +151,8 @@ def test__upload_user__avatarImage(db, client): 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): - token, _ = addFakeUsers(db) +def test__upload_user__avatarImage__file_too_big(db, client, addFakeUsers): + token, _ = addFakeUsers oneUser = db.query(models.User)[1] @@ -460,11 +229,11 @@ def test__createUser__persistence(client): assert response.status_code == 200 assert response.json() == ["alice"] -def test__addFakeUsers(db): - addFakeUsers(db) +def test__addFakeUsers(db, addFakeUsers): + pass -def test__getUsernames(db, client): - token, _ = addFakeUsers(db) +def test__getUsernames(db, client, addFakeUsers): + token, _ = addFakeUsers db.query(models.User).all() @@ -476,8 +245,8 @@ def test__getUsernames(db, client): assert response.status_code == 200 assert response.json() == ["johndoe", "janedoe"] -def test__getUserById(db, client): - token, _ = addFakeUsers(db) +def test__getUserById(db, client, addFakeUsers): + token, _ = addFakeUsers response = client.get( "/users/u/1", @@ -494,8 +263,8 @@ def test__getUserById(db, client): "status": "active", } -def test__getUserById__malformedId(db, client): - token, _ = addFakeUsers(db) +def test__getUserById__malformedId(db, client, addFakeUsers): + token, _ = addFakeUsers response = client.get( "/users/u/abcd", @@ -512,8 +281,8 @@ def test__db_cleanup(db): assert users == [] -def test__createGame(db, client): - token, _ = addFakeUsers(db) +def test__createGame(db, client, addFakeUsers): + token, _ = addFakeUsers response = client.post( "/games/", @@ -541,9 +310,9 @@ def test__createGame(db, client): "winner": None, } -def test__get_game_by_uuid(db, client): - token, _ = addFakeUsers(db) - uuid = addFakeGames(db, fakegamesdb()) +def test__get_game_by_uuid(db, client, addFakeUsers, addFakeGames, johndoe): + token, _ = addFakeUsers + uuid = addFakeGames response = client.get( f"/games/{uuid}", @@ -563,18 +332,17 @@ def test__get_game_by_uuid(db, client): "created_at": mock.ANY, "uuid": mock.ANY, "id": 5, - "owner": johndoe(), + "owner": johndoe, "last_move_time": None, "public": False, "status": GameStatus.WAITING, "turn": "white", - "white": johndoe(), + "white": johndoe, "winner": None, } -def test__get_me_games(db, client): - token, _ = addFakeUsers(db) - addFakeGames(db, fakegamesdb()) +def test__get_me_games(db, client, addFakeUsers, addFakeGames, johndoe, janedoe): + token, _ = addFakeUsers response = client.get( "/users/me/games/", @@ -588,26 +356,23 @@ def test__get_me_games(db, client): assert response.status_code == 200 assert len(response.json()) == 3 assert response.json()[0] == { - "black": janedoe(), + "black": janedoe, "created_at": mock.ANY, "uuid": mock.ANY, "id": 1, "last_move_time": None, - "owner": johndoe(), + "owner": johndoe, "public": False, "status": "started", "turn": "black", - "white": johndoe(), + "white": johndoe, "winner": None, } -def test__getGames__finishedGame(db, client): - _, _ = addFakeUsers(db) +def test__getGames__finishedGame(db, client, addFakeUsers, addFakeGames, addFakeGameSnaps, johndoe, janedoe): # change to second player jane_token = getToken("janedoe") _ = getToken("johndoe") - addFakeGames(db, fakegamesdb()) - addFakeGameSnaps(db, fakegamesnapsdb()) response = client.get( "/users/me/games", @@ -624,9 +389,8 @@ def test__getGames__finishedGame(db, client): assert finishedgame["turn"] is None # TODO test list random games before setting my player -def test__joinRandomGame(db, client): - token, _ = addFakeUsers(db) - addFakeGames(db, fakegamesdb()) +def test__joinRandomGame(db, client, addFakeUsers, addFakeGames): + token, _ = addFakeUsers oneUser = db.query(models.User)[1] @@ -662,12 +426,8 @@ def test__joinRandomGame(db, client): } # TODO deprecated, client chooses game and joins a random one -def test__joinRandomGame__noneAvailable(db, client): - token, _ = addFakeUsers(db) - gamesdbmod = fakegamesdb() - gamesdbmod["123fr12339"]["status"] = "done" - gamesdbmod["da40a3ee5e"]["status"] = "done" - addFakeGames(db, gamesdbmod) +def test__joinRandomGame__noneAvailable(db, client, addFakeUsers, addFakeDoneGames): + token, _ = addFakeUsers response = client.patch( "/games", @@ -681,9 +441,8 @@ def test__joinRandomGame__noneAvailable(db, client): assert response.status_code == 404 assert response.json() == {"detail": "available random game not found"} -def test__get_available_games__all(db, client): - token, _ = addFakeUsers(db) - _ = addFakeGames(db, fakegamesdb()) +def test__get_available_games__all(db, client, addFakeUsers, addFakeGames): + token, _ = addFakeUsers response = client.get( "/games", @@ -698,9 +457,8 @@ def test__get_available_games__all(db, client): 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): - token, _ = addFakeUsers(db) - _ = addFakeGames(db, fakegamesdb()) +def test__get_available_games__waiting(db, client, addFakeUsers, addFakeGames): + token, _ = addFakeUsers response = client.get( "/games", @@ -719,9 +477,8 @@ def test__get_available_games__waiting(db, client): ] # TODO this test was deactivated by mistake -def _test__joinGame__playerAlreadyInGame(db, client): - token, username = addFakeUsers(db) - addFakeGames(db, fakegamesdb()) +def _test__joinGame__playerAlreadyInGame(db, client, addFakeUsers, addFakeGames): + token, username = addFakeUsers uuid = "123fr12339" @@ -760,9 +517,9 @@ def _test__joinGame__playerAlreadyInGame(db, client): "white_id": game.white_id, } -def test__joinGame__playerAlreadyInGame__simple(db, client): - token, username = addFakeUsers(db) - uuid = addFakeGames(db, fakegamesdb()) +def test__joinGame__playerAlreadyInGame__simple(db, client, addFakeUsers, addFakeGames): + token, username = addFakeUsers + uuid = addFakeGames crud.get_user_by_username(db, username) @@ -778,11 +535,10 @@ def test__joinGame__playerAlreadyInGame__simple(db, client): assert response.status_code == 200 -def test__getsnap__byNum(db, client): - firstgame_uuid, token = classicSetup(db) - firstgame_white_player = list(fakegamesdb().values())[0]["white"] +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) - addFakeGameSnaps(db, fakegamesnapsdb()) response = client.get( f"/games/{firstgame_uuid}/snap/0", @@ -814,8 +570,8 @@ def test__getsnap__byNum(db, client): } # yapf: enable -def test__getsnaps(db, client): - firstgame_uuid, token = classicSetup(db) +def test__getsnaps(db, client, classicSetup): + firstgame_uuid, token = classicSetup response = client.get( f"/games/{firstgame_uuid}/snaps", @@ -863,8 +619,8 @@ def test__getsnaps(db, client): }] # yapf: enable -def test__getsnap__latest(db, client): - firstgame_uuid, token = classicSetup(db) +def test__getsnap__latest(db, client, classicSetup): + firstgame_uuid, token = classicSetup response = client.get( f"/games/{firstgame_uuid}/snap", @@ -896,8 +652,8 @@ def test__getsnap__latest(db, client): } # yapf: enable -def test__getTurn(db, client): - firstgame_uuid, token = classicSetup(db) +def test__getTurn(db, client, classicSetup): + firstgame_uuid, token = classicSetup response = client.get( f"/games/{firstgame_uuid}/turn", @@ -911,9 +667,9 @@ def test__getTurn(db, client): assert response.status_code == 200 assert response.json() == "black" -@unittest.skip("slow test") -def test__getTurn__long_polling(client): - firstgame_uuid, _ = classicSetup(db) +@pytest.mark.skip(reason="slow test") +def test__getTurn__long_polling(client, classicSetup): + firstgame_uuid, _ = classicSetup token = getToken("janedoe") start = time.time() @@ -932,8 +688,8 @@ def test__getTurn__long_polling(client): assert elapsed > 5 -def test__move(db, client): - firstgame_uuid, token = classicSetup(db) +def test__move(db, client, classicSetup): + firstgame_uuid, token = classicSetup # get previous game/board @@ -980,8 +736,8 @@ def test__move(db, client): "rnbqkbnr" ) -def test__move__filtered(db, client): - firstgame_uuid, _ = classicSetup(db) +def test__move__filtered(db, client, classicSetup): + firstgame_uuid, _ = classicSetup # change to second player token = getToken("janedoe") @@ -1010,8 +766,8 @@ def test__move__filtered(db, client): "XXXXXXXX" ) -def test__possibleMoves__pawnMove(db, client): - firstgame_uuid, _ = classicSetup(db) +def test__possibleMoves__pawnMove(db, client, classicSetup): + firstgame_uuid, _ = classicSetup # change to second player token = getToken("janedoe") @@ -1030,11 +786,9 @@ def test__possibleMoves__pawnMove(db, client): assert response.json() == ["d6", "d5"] -def test__possibleMoves__king(db, client): - firstgame_uuid, token = classicSetup(db) - - move = "g3f3" - boardStr = ( +@pytest.mark.parametrize( + "addCustomGameSnap", + [(( "____K___" "________" "________" @@ -1043,9 +797,11 @@ def test__possibleMoves__king(db, client): "____pk__" "___P_pp_" "________" - ) - - addCustomGameSnap(db, boardStr, move) + ), "g3f3")], + indirect=True +) +def test__possibleMoves__king(db, client, classicSetup, addCustomGameSnap): + firstgame_uuid, token = classicSetup square = "f3" @@ -1062,13 +818,10 @@ def test__possibleMoves__king(db, client): assert response.json() == ["e4", "f4", "g4", "g3", "e2"] -def test__possibleMoves__pawn_enpassant_black(db, client): - firstgame_uuid, _ = classicSetup(db) - token = getToken("janedoe") - - move = "c2c4" - boardStr = ( +@pytest.mark.parametrize( + "addCustomGameSnap", + [(( "____K___" "________" "________" @@ -1077,9 +830,13 @@ def test__possibleMoves__pawn_enpassant_black(db, client): "____pk__" "_____pp_" "________" - ) + ), "c2c4")], + indirect=True +) +def test__possibleMoves__pawn_enpassant_black(db, client, classicSetup, addCustomGameSnap): + firstgame_uuid, _ = classicSetup - addCustomGameSnap(db, boardStr, move) + token = getToken("janedoe") square = "d4" @@ -1096,11 +853,9 @@ def test__possibleMoves__pawn_enpassant_black(db, client): assert response.json() == ["c3", "d3", "e3"] -def test__possibleMoves__pawn_enpassant_white(db, client): - firstgame_uuid, token = classicSetup(db) - - move = "d7d5" - boardStr = ( +@pytest.mark.parametrize( + "addCustomGameSnap", + [(( "____K___" "________" "________" @@ -1109,9 +864,11 @@ def test__possibleMoves__pawn_enpassant_white(db, client): "____pk__" "_____pp_" "________" - ) - - addCustomGameSnap(db, boardStr, move) + ), "d7d5")], + indirect=True +) +def test__possibleMoves__pawn_enpassant_white(db, client, classicSetup, addCustomGameSnap): + firstgame_uuid, token = classicSetup square = "c5" @@ -1128,13 +885,10 @@ def test__possibleMoves__pawn_enpassant_white(db, client): assert response.json() == ["c6", "d6"] -def test__possibleMoves__pawn_impossible_enpassant_black(db, client): - firstgame_uuid, _ = classicSetup(db) - - token = getToken("janedoe") - move = "c7c5" - boardStr = ( +@pytest.mark.parametrize( + "addCustomGameSnap", + [(( "____K___" "________" "________" @@ -1143,9 +897,13 @@ def test__possibleMoves__pawn_impossible_enpassant_black(db, client): "____pk__" "_____pp_" "________" - ) + ), "c7c5")], + indirect=True +) +def test__possibleMoves__pawn_impossible_enpassant_black(db, client, classicSetup, addCustomGameSnap): + firstgame_uuid, _ = classicSetup - addCustomGameSnap(db, boardStr, move) + token = getToken("janedoe") square = "d5" @@ -1162,11 +920,10 @@ def test__possibleMoves__pawn_impossible_enpassant_black(db, client): assert response.json() == ["d4"] -def test__possibleMoves__pawn_take(db, client): - firstgame_uuid, token = classicSetup(db) - move = "f5f6" - boardStr = ( +@pytest.mark.parametrize( + "addCustomGameSnap", + [(( "____K___" "_____PP_" "_____p__" @@ -1175,9 +932,11 @@ def test__possibleMoves__pawn_take(db, client): "_____k__" "_____pp_" "________" - ) - - addCustomGameSnap(db, boardStr, move) + ), "f5f6")], + indirect=True +) +def test__possibleMoves__pawn_take(db, client, classicSetup, addCustomGameSnap): + firstgame_uuid, token = classicSetup square = "f6" @@ -1213,14 +972,9 @@ def prettyBoard(boardStr): for i in range(8): print("{} - {} - {}".format(i, boardStr[8 * i : 8 * i + 8], 8 - i)) -def test__move__filtered_pawn(db, client): - _, _ = addFakeUsers(db) - # change to second player - jane_token = getToken("janedoe") - john_token = getToken("johndoe") - addFakeGames(db, fakegamesdb()) - firstgame_uuid = list(fakegamesdb().values())[0]["uuid"] - addFakeGameSnaps(db, fakegamesnapsdb()) +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"] @@ -1264,14 +1018,9 @@ def test__move__filtered_pawn(db, client): # 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): - _, _ = addFakeUsers(db) - # change to second player - jane_token = getToken("janedoe") - john_token = getToken("johndoe") - addFakeGames(db, fakegamesdb()) - firstgame_uuid = list(fakegamesdb().values())[0]["uuid"] - addFakeGameSnaps(db, fakegamesnapsdb()) +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"] From 711d86bc5843655af95cbd32c2385fc9f28c6661 Mon Sep 17 00:00:00 2001 From: Quim Date: Thu, 8 Feb 2024 23:45:46 +0100 Subject: [PATCH 30/43] fixed api tests, now using pytest fixtures --- tests/server/conftest.py | 25 ++++++++++++++++++++++--- tests/server/test_btchApi.py | 8 ++++---- tests/server/test_btchApi_autoplay.py | 3 +-- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/tests/server/conftest.py b/tests/server/conftest.py index 110ecaf..4193e26 100644 --- a/tests/server/conftest.py +++ b/tests/server/conftest.py @@ -126,7 +126,7 @@ def fakegamesnapsdb(): "rnbqkbnr" ), "taken": "", - "castleable": "", + "castleable": "LKSlks", "move_number": 0, "created_at": datetime(2021, 4, 5, 0, tzinfo=timezone.utc), }, @@ -144,7 +144,7 @@ def fakegamesnapsdb(): "rnbqkbnr" ), "taken": "", - "castleable": "", + "castleable": "LKSlks", "move_number": 1, "created_at": datetime(2021, 4, 5, 10, tzinfo=timezone.utc), }, @@ -238,6 +238,25 @@ def addFakeGameSnaps(db): 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 @@ -312,7 +331,7 @@ def getToken(username): @pytest.fixture(scope="function") -def game_setup(db, addFakeUsers, addFakeGames, addFakeGameSnaps, fakegamesdb): +def game_setup(db, addFakeUsers, addFakeGames, addFakeGameStartSnap, fakegamesdb): _, _ = addFakeUsers jane_token = getToken("janedoe") john_token = getToken("johndoe") diff --git a/tests/server/test_btchApi.py b/tests/server/test_btchApi.py index 8b40274..9917b97 100644 --- a/tests/server/test_btchApi.py +++ b/tests/server/test_btchApi.py @@ -557,7 +557,7 @@ def test__getsnap__byNum(db, client, classicSetup, fakegamesdb, addFakeGameSnaps 'id': 1, 'move': None, 'taken': '', - 'castleable': '', + 'castleable': 'lks', 'move_number': 0, 'board': ('xxxxxxxx' 'xxxxxxxx' @@ -590,7 +590,7 @@ def test__getsnaps(db, client, classicSetup): 'id': 1, 'move': None, 'taken': '', - 'castleable': '', + 'castleable': 'lks', 'move_number': 0, 'board': ('xxxxxxxx' 'xxxxxxxx' @@ -606,7 +606,7 @@ def test__getsnaps(db, client, classicSetup): 'id': 2, 'move': 'd2d4', 'taken': '', - 'castleable': '', + 'castleable': 'lks', 'move_number': 1, 'board': ('xxxxxxxx' 'xxxxxxxx' @@ -639,7 +639,7 @@ def test__getsnap__latest(db, client, classicSetup): 'id': 2, 'move': 'd2d4', 'taken': '', - 'castleable': '', + 'castleable': 'lks', 'move_number': 1, 'board': ('xxxxxxxx' 'xxxxxxxx' diff --git a/tests/server/test_btchApi_autoplay.py b/tests/server/test_btchApi_autoplay.py index d7200d1..82d6c12 100644 --- a/tests/server/test_btchApi_autoplay.py +++ b/tests/server/test_btchApi_autoplay.py @@ -100,5 +100,4 @@ def test__move__MrExonGames(db, client, game_setup): print(response.json()) assert response.status_code == 200 - db.rollback() - #resetGame(db, firstgame_uuid) + resetGame(db, firstgame_uuid) From 7ce2350b9d7527a09261b40dd189dd5b25763ffc Mon Sep 17 00:00:00 2001 From: Quim Date: Thu, 8 Feb 2024 23:54:54 +0100 Subject: [PATCH 31/43] moved board tests to pytest --- tests/core/test_Board.py | 234 +++++++++++++++++------------------ tests/core/test_btchBoard.py | 231 +++++++++++++++++----------------- 2 files changed, 223 insertions(+), 242 deletions(-) diff --git a/tests/core/test_Board.py b/tests/core/test_Board.py index 737988a..23bf20e 100644 --- a/tests/core/test_Board.py +++ b/tests/core/test_Board.py @@ -1,124 +1,114 @@ -import unittest - from battlechess.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) +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 index 7ac1534..5d582fb 100644 --- a/tests/core/test_btchBoard.py +++ b/tests/core/test_btchBoard.py @@ -1,174 +1,165 @@ -import unittest - from battlechess.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 startboardStr(): + return ( + "RNBQKBNR" + "PPPPPPPP" + "________" + "________" + "________" + "________" + "pppppppp" + "rnbqkbnr" + ) - def fakeElements(self): - elements = { - "board": self.startboardStr(), - "taken": "", - "castleable": "LSKlsk", - "enpassant": None, - "winner": None, - } +def fakeElements(): + elements = { + "board": startboardStr(), + "taken": "", + "castleable": "LSKlsk", + "enpassant": None, + "winner": None, + } - return elements + 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 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(self): - btchBoard = BtchBoard.factory(self.startboardStr()) +def test__factory(): + btchBoard = BtchBoard.factory(startboardStr()) - self.assertEqual(btchBoard.toElements(), self.fakeElements()) + btchBoard.toElements() == fakeElements() - def test__isEnemy(self): +def test__isEnemy(): - result = BtchBoard.isEnemy("white", None) + result = BtchBoard.isEnemy("white", None) - self.assertFalse(result) + assert not result - result = BtchBoard.isEnemy("white", "p") + result = BtchBoard.isEnemy("white", "p") - self.assertFalse(result) + assert not result - result = BtchBoard.isEnemy("white", "P") + result = BtchBoard.isEnemy("white", "P") - self.assertTrue(result) + assert result - result = BtchBoard.isEnemy("white", "_") + result = BtchBoard.isEnemy("white", "_") - self.assertFalse(result) + assert not result - def test__rookMoves__emptyBoard(self): +def test__rookMoves__emptyBoard(): - b = BtchBoard() - b.empty() + b = BtchBoard() + b.empty() - moves = sorted(sq for sq in b.rookMoves("white", 2, 2)) + moves = sorted(sq for sq in b.rookMoves("white", 2, 2)) - print("moves\n{}".format(self.squares2ascii(moves))) + print("moves\n{}".format(squares2ascii(moves))) - expected = sorted( - [(i, 2) for i in range(3, 10)] + [(2, j) for j in range(3, 10)] - ) + 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))) + print("expected\n{}".format(squares2ascii(expected))) - self.assertListEqual(moves, expected) + assert moves == expected - def test__rookMoves__startBoard(self): +def test__rookMoves__startBoard(): - b = BtchBoard() + b = BtchBoard() - moves = sorted(sq for sq in b.rookMoves("black", 2, 2)) + moves = sorted(sq for sq in b.rookMoves("black", 2, 2)) - expected = [] + expected = [] - self.assertListEqual(moves, expected) + assert moves == expected - def test__bishopMoves__emptyBoard(self): +def test__bishopMoves__emptyBoard(): - b = BtchBoard() - b.empty() + b = BtchBoard() + b.empty() - moves = sorted(sq for sq in b.bishopMoves("white", 6, 6)) + moves = sorted(sq for sq in b.bishopMoves("white", 6, 6)) - print("moves\n{}".format(self.squares2ascii(moves))) + 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)) + 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))) + print("expected\n {}".format(squares2ascii(expected))) - self.assertListEqual(moves, expected) + assert moves == expected - def test__moves__pawn(self): - b = BtchBoard() +def test__moves__pawn(): + b = BtchBoard() - moves = sorted(sq for sq in b.pawnMoves("white", 8, 6)) + moves = sorted(sq for sq in b.pawnMoves("white", 8, 6)) - expected = [(6, 6), (7, 6)] + expected = [(6, 6), (7, 6)] - self.assertListEqual(moves, expected) + assert moves == expected - def test__moves__manyMoves(self): - pass +def test__moves__manyMoves(): + pass - def test__moves__enpassant(self): - pass +def test__moves__enpassant(): + pass - def test__moves__notMovingForbidden(self): - pass +def test__moves__notMovingForbidden(): + pass - # check that an impossible move is possible if fogged enemies - def test__moves__unknownInfo(self): - pass +# check that an impossible move is possible if fogged enemies +def test__moves__unknownInfo(): + pass - def test__filter__startPosition(self): - color = "white" - b = BtchBoard() - b.filter(color) +def test__filter__startPosition(): + color = "white" + b = BtchBoard() + b.filter(color) - expectedBoardStr = ( - "________" - "________" - "________" - "________" - "________" - "________" - "pppppppp" - "rnbqkbnr" - ) + expectedBoardStr = ( + "________" + "________" + "________" + "________" + "________" + "________" + "pppppppp" + "rnbqkbnr" + ) - expected = BtchBoard.factory(expectedBoardStr) - expected.castleable = "lsk" + expected = BtchBoard.factory(expectedBoardStr) + expected.castleable = "lsk" - self.assertDictEqual(b.toElements(), expected.toElements()) + assert b.toElements() == expected.toElements() - def test__moves__fog(self): - boardStr = ( - "________" - "________" - "________" - "________" - "________" - "________" - "ppppppp_" - "rnbqkbnr" - ) +def test__moves__fog(): + boardStr = ( + "________" + "________" + "________" + "________" + "________" + "________" + "ppppppp_" + "rnbqkbnr" + ) - b = BtchBoard.factory(boardStr) + b = BtchBoard.factory(boardStr) - print("fog {} ".format(b.toElements())) + print("fog {} ".format(b.toElements())) - moves = b.possibleMoves("white", 9, 9) + moves = b.possibleMoves("white", 9, 9) - expectedMoves = sorted([(i, 9) for i in range(2, 9)]) + expectedMoves = sorted([(i, 9) for i in range(2, 9)]) - self.assertListEqual(moves, expectedMoves) + assert moves == expectedMoves From 60bfa60293f5e150645465287f2504af02599fec Mon Sep 17 00:00:00 2001 From: Quim Date: Fri, 9 Feb 2024 19:32:05 +0100 Subject: [PATCH 32/43] add sqlalchemy-utils dependency --- poetry.lock | 30 +++++++++++++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index bbc32a6..6793e40 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1390,6 +1390,34 @@ 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" @@ -1517,4 +1545,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "907aa7c5252f2feabb285454676e0665a80c78fe444dac2ceedf50ca89e42518" +content-hash = "2f1d9f88bb0104ae572305737044e08285cddba9880425a546b007ada97ea5a9" diff --git a/pyproject.toml b/pyproject.toml index cb518a7..a6f9630 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ 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] From 30434229e8da8884c77a0b5f8584e70a09696063 Mon Sep 17 00:00:00 2001 From: Quim Date: Sat, 10 Feb 2024 15:55:00 +0100 Subject: [PATCH 33/43] 403 on wrong turn move. Closes #2 --- battlechess/server/btchApi.py | 8 ++++++++ tests/server/test_btchApi.py | 20 ++++++++++++++++++-- tests/server/test_btchApi_autoplay.py | 14 +++++++++----- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/battlechess/server/btchApi.py b/battlechess/server/btchApi.py index 2d749ea..3a27eb1 100644 --- a/battlechess/server/btchApi.py +++ b/battlechess/server/btchApi.py @@ -447,6 +447,14 @@ 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.model_validate(snap) diff --git a/tests/server/test_btchApi.py b/tests/server/test_btchApi.py index 9917b97..ece8228 100644 --- a/tests/server/test_btchApi.py +++ b/tests/server/test_btchApi.py @@ -690,7 +690,7 @@ def test__getTurn__long_polling(client, classicSetup): def test__move(db, client, classicSetup): firstgame_uuid, token = classicSetup - + jane_token = getToken('janedoe') # get previous game/board game_before = ( @@ -702,7 +702,7 @@ def test__move(db, client, classicSetup): response = client.post( f"/games/{firstgame_uuid}/move", headers={ - "Authorization": "Bearer " + token, + "Authorization": "Bearer " + jane_token, "Content-Type": "application/json", }, json={ @@ -736,6 +736,22 @@ def test__move(db, client, classicSetup): "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 diff --git a/tests/server/test_btchApi_autoplay.py b/tests/server/test_btchApi_autoplay.py index 82d6c12..f42d9f6 100644 --- a/tests/server/test_btchApi_autoplay.py +++ b/tests/server/test_btchApi_autoplay.py @@ -57,11 +57,13 @@ def test__move__MrExonGame__OneGame__EnPassant(client, game_setup): print(response.json()) response.status_code == 200 -def test__move__MrExonGame__Enpassant2(client, game_setup): +def test__move__MrExonGame__Enpassant2(db, client, game_setup): firstgame_uuid, john_token, jane_token = game_setup - tokens = [jane_token, john_token] + tokens = [john_token, jane_token] + + resetGame(db, firstgame_uuid) # read games # for game in games: @@ -82,7 +84,9 @@ def test__move__MrExonGame__Enpassant2(client, game_setup): def test__move__MrExonGames(db, client, game_setup): firstgame_uuid, john_token, jane_token = game_setup - tokens = [jane_token, john_token] + tokens = [john_token, jane_token] + + resetGame(db, firstgame_uuid) # read games # for game in games: @@ -91,8 +95,8 @@ def test__move__MrExonGames(db, client, game_setup): data = json.load(json_file) for game, moves in data.items(): print("Game: {} moves {}".format(game, moves)) - for move in moves: - response = send_move(client, firstgame_uuid, move, tokens[0 % 2]) + 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"]) From 881eaa6edef74925ab6ea2b0b1664b6379099b90 Mon Sep 17 00:00:00 2001 From: Quim Date: Sat, 10 Feb 2024 00:29:47 +0100 Subject: [PATCH 34/43] trying to make async tests --- battlechess/server/btchApi.py | 3 +++ tests/server/test_btchApi.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/battlechess/server/btchApi.py b/battlechess/server/btchApi.py index 3a27eb1..b2910d8 100644 --- a/battlechess/server/btchApi.py +++ b/battlechess/server/btchApi.py @@ -118,6 +118,9 @@ def set_player( def version(): return {"version": "1.0"} +@app.get("/") +async def root(): + return {"message": "Tomato"} @app.post("/token", response_model=schemas.Token) def login_for_access_token( diff --git a/tests/server/test_btchApi.py b/tests/server/test_btchApi.py index ece8228..30ce6ea 100644 --- a/tests/server/test_btchApi.py +++ b/tests/server/test_btchApi.py @@ -14,6 +14,7 @@ except ImportError: print("PIL module is not installed. Some tests will be skipped") +from httpx import AsyncClient from fastapi.testclient import TestClient from battlechess.server import crud, models @@ -687,6 +688,35 @@ def test__getTurn__long_polling(client, classicSetup): elapsed = time.time() - start assert elapsed > 5 +@pytest.mark.anyio +async def test_root(): + async with AsyncClient(app=app, base_url="http://test") as ac: + response = await ac.get("/") + assert response.status_code == 200 + assert response.json() == {"message": "Tomato"} + +@pytest.mark.anyio +async def test__getTurn__long_polling_async(classicSetup): + firstgame_uuid, _ = classicSetup + token = getToken("janedoe") + + start = time.time() + async with AsyncClient(app=app, base_url="http://test") as ac: + response = await ac.get( + f"/games/{firstgame_uuid}/turn", + headers={ + "Authorization": "Bearer " + token, + "Content-Type": "application/json", + }, + params={ + "long_polling": True + } + ) + assert response.status_code == 200 + + elapsed = time.time() - start + assert elapsed > 5 + def test__move(db, client, classicSetup): firstgame_uuid, token = classicSetup From 13434b80577fe647f60fe7778c4ddb800bd428c1 Mon Sep 17 00:00:00 2001 From: quimnuss Date: Sat, 10 Feb 2024 02:36:03 +0100 Subject: [PATCH 35/43] asyncio testing working --- battlechess/server/btchApi.py | 16 +- poetry.lock | 183 ++++++++++---------- tests/server/conftest.py | 43 +++-- tests/server/test_btchApi.py | 267 ++++++++++++++++------------- tests/server/test_btchApi_async.py | 48 ++++++ 5 files changed, 328 insertions(+), 229 deletions(-) create mode 100644 tests/server/test_btchApi_async.py diff --git a/battlechess/server/btchApi.py b/battlechess/server/btchApi.py index b2910d8..506599a 100644 --- a/battlechess/server/btchApi.py +++ b/battlechess/server/btchApi.py @@ -118,9 +118,6 @@ def set_player( def version(): return {"version": "1.0"} -@app.get("/") -async def root(): - return {"message": "Tomato"} @app.post("/token", response_model=schemas.Token) def login_for_access_token( @@ -294,7 +291,7 @@ def get_game_by_uuid( current_user: schemas.User = Depends(get_current_active_user), db: Session = Depends(get_db), ): - game = crud.get_game_by_uuid(db, gameUUID) + game = crud.get_game_by_uuid(db, gameUUID) if not game: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -303,6 +300,7 @@ def get_game_by_uuid( ) 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( @@ -409,12 +407,12 @@ def query_turn( gameUUID: str, current_user: schemas.User = Depends(get_current_active_user), db: Session = Depends(get_db), - long_polling: bool = False + long_polling: bool = False, ): game = get_game(gameUUID, current_user, db) if not long_polling: return game.turn - + print("long polling") start = time.time() elapsed = 0 @@ -422,7 +420,9 @@ def query_turn( elapsed = time.time() - start game = get_game(gameUUID, current_user, db) if game.turn != game.get_player_color(current_user.id): - print(f"{current_user.username} is {game.get_player_color(current_user.id)} is not {game.turn}") + print( + f"{current_user.username} is {game.get_player_color(current_user.id)} is not {game.turn}" + ) return game.turn time.sleep(1) @@ -600,7 +600,7 @@ def get_snap_by_move( 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) + print("game is over and snap is", snap4player) return snap4player diff --git a/poetry.lock b/poetry.lock index 6793e40..fae5797 100644 --- a/poetry.lock +++ b/poetry.lock @@ -37,13 +37,13 @@ trio = ["trio (<0.22)"] [[package]] name = "appnope" -version = "0.1.3" +version = "0.1.4" description = "Disable App Nap on macOS >= 10.9" optional = false -python-versions = "*" +python-versions = ">=3.6" files = [ - {file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"}, - {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"}, + {file = "appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c"}, + {file = "appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee"}, ] [[package]] @@ -163,13 +163,13 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" -version = "2023.11.17" +version = "2024.2.2" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, - {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, ] [[package]] @@ -907,18 +907,18 @@ files = [ [[package]] name = "pydantic" -version = "2.6.0" +version = "2.6.1" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.6.0-py3-none-any.whl", hash = "sha256:1440966574e1b5b99cf75a13bec7b20e3512e8a61b894ae252f56275e2c465ae"}, - {file = "pydantic-2.6.0.tar.gz", hash = "sha256:ae887bd94eb404b09d86e4d12f93893bdca79d766e738528c6fa1c849f3c6bcf"}, + {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.1" +pydantic-core = "2.16.2" typing-extensions = ">=4.6.1" [package.extras] @@ -926,90 +926,90 @@ email = ["email-validator (>=2.0.0)"] [[package]] name = "pydantic-core" -version = "2.16.1" +version = "2.16.2" description = "" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.16.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:300616102fb71241ff477a2cbbc847321dbec49428434a2f17f37528721c4948"}, - {file = "pydantic_core-2.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5511f962dd1b9b553e9534c3b9c6a4b0c9ded3d8c2be96e61d56f933feef9e1f"}, - {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98f0edee7ee9cc7f9221af2e1b95bd02810e1c7a6d115cfd82698803d385b28f"}, - {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9795f56aa6b2296f05ac79d8a424e94056730c0b860a62b0fdcfe6340b658cc8"}, - {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c45f62e4107ebd05166717ac58f6feb44471ed450d07fecd90e5f69d9bf03c48"}, - {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:462d599299c5971f03c676e2b63aa80fec5ebc572d89ce766cd11ca8bcb56f3f"}, - {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ebaa4bf6386a3b22eec518da7d679c8363fb7fb70cf6972161e5542f470798"}, - {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:99f9a50b56713a598d33bc23a9912224fc5d7f9f292444e6664236ae471ddf17"}, - {file = "pydantic_core-2.16.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8ec364e280db4235389b5e1e6ee924723c693cbc98e9d28dc1767041ff9bc388"}, - {file = "pydantic_core-2.16.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:653a5dfd00f601a0ed6654a8b877b18d65ac32c9d9997456e0ab240807be6cf7"}, - {file = "pydantic_core-2.16.1-cp310-none-win32.whl", hash = "sha256:1661c668c1bb67b7cec96914329d9ab66755911d093bb9063c4c8914188af6d4"}, - {file = "pydantic_core-2.16.1-cp310-none-win_amd64.whl", hash = "sha256:561be4e3e952c2f9056fba5267b99be4ec2afadc27261505d4992c50b33c513c"}, - {file = "pydantic_core-2.16.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:102569d371fadc40d8f8598a59379c37ec60164315884467052830b28cc4e9da"}, - {file = "pydantic_core-2.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:735dceec50fa907a3c314b84ed609dec54b76a814aa14eb90da31d1d36873a5e"}, - {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e83ebbf020be727d6e0991c1b192a5c2e7113eb66e3def0cd0c62f9f266247e4"}, - {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:30a8259569fbeec49cfac7fda3ec8123486ef1b729225222f0d41d5f840b476f"}, - {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:920c4897e55e2881db6a6da151198e5001552c3777cd42b8a4c2f72eedc2ee91"}, - {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5247a3d74355f8b1d780d0f3b32a23dd9f6d3ff43ef2037c6dcd249f35ecf4c"}, - {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d5bea8012df5bb6dda1e67d0563ac50b7f64a5d5858348b5c8cb5043811c19d"}, - {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ed3025a8a7e5a59817b7494686d449ebfbe301f3e757b852c8d0d1961d6be864"}, - {file = "pydantic_core-2.16.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:06f0d5a1d9e1b7932477c172cc720b3b23c18762ed7a8efa8398298a59d177c7"}, - {file = "pydantic_core-2.16.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:150ba5c86f502c040b822777e2e519b5625b47813bd05f9273a8ed169c97d9ae"}, - {file = "pydantic_core-2.16.1-cp311-none-win32.whl", hash = "sha256:d6cbdf12ef967a6aa401cf5cdf47850559e59eedad10e781471c960583f25aa1"}, - {file = "pydantic_core-2.16.1-cp311-none-win_amd64.whl", hash = "sha256:afa01d25769af33a8dac0d905d5c7bb2d73c7c3d5161b2dd6f8b5b5eea6a3c4c"}, - {file = "pydantic_core-2.16.1-cp311-none-win_arm64.whl", hash = "sha256:1a2fe7b00a49b51047334d84aafd7e39f80b7675cad0083678c58983662da89b"}, - {file = "pydantic_core-2.16.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f478ec204772a5c8218e30eb813ca43e34005dff2eafa03931b3d8caef87d51"}, - {file = "pydantic_core-2.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f1936ef138bed2165dd8573aa65e3095ef7c2b6247faccd0e15186aabdda7f66"}, - {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99d3a433ef5dc3021c9534a58a3686c88363c591974c16c54a01af7efd741f13"}, - {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd88f40f2294440d3f3c6308e50d96a0d3d0973d6f1a5732875d10f569acef49"}, - {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fac641bbfa43d5a1bed99d28aa1fded1984d31c670a95aac1bf1d36ac6ce137"}, - {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72bf9308a82b75039b8c8edd2be2924c352eda5da14a920551a8b65d5ee89253"}, - {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb4363e6c9fc87365c2bc777a1f585a22f2f56642501885ffc7942138499bf54"}, - {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:20f724a023042588d0f4396bbbcf4cffd0ddd0ad3ed4f0d8e6d4ac4264bae81e"}, - {file = "pydantic_core-2.16.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fb4370b15111905bf8b5ba2129b926af9470f014cb0493a67d23e9d7a48348e8"}, - {file = "pydantic_core-2.16.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23632132f1fd608034f1a56cc3e484be00854db845b3a4a508834be5a6435a6f"}, - {file = "pydantic_core-2.16.1-cp312-none-win32.whl", hash = "sha256:b9f3e0bffad6e238f7acc20c393c1ed8fab4371e3b3bc311020dfa6020d99212"}, - {file = "pydantic_core-2.16.1-cp312-none-win_amd64.whl", hash = "sha256:a0b4cfe408cd84c53bab7d83e4209458de676a6ec5e9c623ae914ce1cb79b96f"}, - {file = "pydantic_core-2.16.1-cp312-none-win_arm64.whl", hash = "sha256:d195add190abccefc70ad0f9a0141ad7da53e16183048380e688b466702195dd"}, - {file = "pydantic_core-2.16.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:502c062a18d84452858f8aea1e520e12a4d5228fc3621ea5061409d666ea1706"}, - {file = "pydantic_core-2.16.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d8c032ccee90b37b44e05948b449a2d6baed7e614df3d3f47fe432c952c21b60"}, - {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:920f4633bee43d7a2818e1a1a788906df5a17b7ab6fe411220ed92b42940f818"}, - {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f5d37ff01edcbace53a402e80793640c25798fb7208f105d87a25e6fcc9ea06"}, - {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:399166f24c33a0c5759ecc4801f040dbc87d412c1a6d6292b2349b4c505effc9"}, - {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ac89ccc39cd1d556cc72d6752f252dc869dde41c7c936e86beac5eb555041b66"}, - {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73802194f10c394c2bedce7a135ba1d8ba6cff23adf4217612bfc5cf060de34c"}, - {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8fa00fa24ffd8c31fac081bf7be7eb495be6d248db127f8776575a746fa55c95"}, - {file = "pydantic_core-2.16.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:601d3e42452cd4f2891c13fa8c70366d71851c1593ed42f57bf37f40f7dca3c8"}, - {file = "pydantic_core-2.16.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07982b82d121ed3fc1c51faf6e8f57ff09b1325d2efccaa257dd8c0dd937acca"}, - {file = "pydantic_core-2.16.1-cp38-none-win32.whl", hash = "sha256:d0bf6f93a55d3fa7a079d811b29100b019784e2ee6bc06b0bb839538272a5610"}, - {file = "pydantic_core-2.16.1-cp38-none-win_amd64.whl", hash = "sha256:fbec2af0ebafa57eb82c18c304b37c86a8abddf7022955d1742b3d5471a6339e"}, - {file = "pydantic_core-2.16.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a497be217818c318d93f07e14502ef93d44e6a20c72b04c530611e45e54c2196"}, - {file = "pydantic_core-2.16.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:694a5e9f1f2c124a17ff2d0be613fd53ba0c26de588eb4bdab8bca855e550d95"}, - {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d4dfc66abea3ec6d9f83e837a8f8a7d9d3a76d25c9911735c76d6745950e62c"}, - {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8655f55fe68c4685673265a650ef71beb2d31871c049c8b80262026f23605ee3"}, - {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21e3298486c4ea4e4d5cc6fb69e06fb02a4e22089304308817035ac006a7f506"}, - {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71b4a48a7427f14679f0015b13c712863d28bb1ab700bd11776a5368135c7d60"}, - {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dca874e35bb60ce4f9f6665bfbfad050dd7573596608aeb9e098621ac331dc"}, - {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fa496cd45cda0165d597e9d6f01e36c33c9508f75cf03c0a650018c5048f578e"}, - {file = "pydantic_core-2.16.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5317c04349472e683803da262c781c42c5628a9be73f4750ac7d13040efb5d2d"}, - {file = "pydantic_core-2.16.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:42c29d54ed4501a30cd71015bf982fa95e4a60117b44e1a200290ce687d3e640"}, - {file = "pydantic_core-2.16.1-cp39-none-win32.whl", hash = "sha256:ba07646f35e4e49376c9831130039d1b478fbfa1215ae62ad62d2ee63cf9c18f"}, - {file = "pydantic_core-2.16.1-cp39-none-win_amd64.whl", hash = "sha256:2133b0e412a47868a358713287ff9f9a328879da547dc88be67481cdac529118"}, - {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d25ef0c33f22649b7a088035fd65ac1ce6464fa2876578df1adad9472f918a76"}, - {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:99c095457eea8550c9fa9a7a992e842aeae1429dab6b6b378710f62bfb70b394"}, - {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b49c604ace7a7aa8af31196abbf8f2193be605db6739ed905ecaf62af31ccae0"}, - {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c56da23034fe66221f2208c813d8aa509eea34d97328ce2add56e219c3a9f41c"}, - {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cebf8d56fee3b08ad40d332a807ecccd4153d3f1ba8231e111d9759f02edfd05"}, - {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:1ae8048cba95f382dba56766525abca438328455e35c283bb202964f41a780b0"}, - {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:780daad9e35b18d10d7219d24bfb30148ca2afc309928e1d4d53de86822593dc"}, - {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c94b5537bf6ce66e4d7830c6993152940a188600f6ae044435287753044a8fe2"}, - {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:adf28099d061a25fbcc6531febb7a091e027605385de9fe14dd6a97319d614cf"}, - {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:644904600c15816a1f9a1bafa6aab0d21db2788abcdf4e2a77951280473f33e1"}, - {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87bce04f09f0552b66fca0c4e10da78d17cb0e71c205864bab4e9595122cb9d9"}, - {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:877045a7969ace04d59516d5d6a7dee13106822f99a5d8df5e6822941f7bedc8"}, - {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9c46e556ee266ed3fb7b7a882b53df3c76b45e872fdab8d9cf49ae5e91147fd7"}, - {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4eebbd049008eb800f519578e944b8dc8e0f7d59a5abb5924cc2d4ed3a1834ff"}, - {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:c0be58529d43d38ae849a91932391eb93275a06b93b79a8ab828b012e916a206"}, - {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b1fc07896fc1851558f532dffc8987e526b682ec73140886c831d773cef44b76"}, - {file = "pydantic_core-2.16.1.tar.gz", hash = "sha256:daff04257b49ab7f4b3f73f98283d3dbb1a65bf3500d55c7beac3c66c310fe34"}, + {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] @@ -1190,7 +1190,6 @@ files = [ {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_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {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"}, diff --git a/tests/server/conftest.py b/tests/server/conftest.py index 4193e26..397693e 100644 --- a/tests/server/conftest.py +++ b/tests/server/conftest.py @@ -1,11 +1,11 @@ -import pytest 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 -from sqlalchemy_utils import database_exists +from sqlalchemy_utils import create_database, database_exists from battlechess.server import crud, models from battlechess.server.btchApi import app, get_db @@ -13,6 +13,7 @@ from battlechess.server.schemas import GameStatus from battlechess.server.utils import get_password_hash, verify_password + @pytest.fixture def fakeusersdb(): fake_users_db = { @@ -37,6 +38,7 @@ def fakeusersdb(): } return fake_users_db + @pytest.fixture(scope="function") def janedoe(): return { @@ -46,6 +48,7 @@ def janedoe(): "username": "janedoe", } + @pytest.fixture(scope="function") def johndoe(): return { @@ -55,6 +58,7 @@ def johndoe(): "username": "johndoe", } + @pytest.fixture def fakegamesdb(): fake_games_db = { @@ -110,6 +114,7 @@ def fakegamesdb(): } return fake_games_db + def fakegamesnapsdb(): fake_games_snaps = [ { @@ -151,6 +156,7 @@ def fakegamesnapsdb(): ] return fake_games_snaps + @pytest.fixture(scope="function") def addFakeUsers(db, fakeusersdb): for username, user in fakeusersdb.items(): @@ -169,19 +175,13 @@ def addFakeUsers(db, fakeusersdb): def addFakeGamesFromDict(db, gamesdb): for uuid, game in gamesdb.items(): owner = ( - db.query(models.User) - .filter(models.User.username == game["owner"]) - .first() + db.query(models.User).filter(models.User.username == game["owner"]).first() ) white = ( - db.query(models.User) - .filter(models.User.username == game["white"]) - .first() + db.query(models.User).filter(models.User.username == game["white"]).first() ) black = ( - db.query(models.User) - .filter(models.User.username == game["black"]) - .first() + db.query(models.User).filter(models.User.username == game["black"]).first() ) db_game = models.Game( created_at=game["created_at"], @@ -206,11 +206,13 @@ def addFakeGamesFromDict(db, gamesdb): ) 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 @@ -218,6 +220,7 @@ def addFakeDoneGames(db, fakegamesdb): gamesdbmod["da40a3ee5e"]["status"] = "done" return addFakeGamesFromDict(db, gamesdbmod) + @pytest.fixture(scope="function") def addFakeGameSnaps(db): # TODO get game from uuid @@ -238,6 +241,7 @@ def addFakeGameSnaps(db): db.add(db_snap) db.commit() + @pytest.fixture(scope="function") def addFakeGameStartSnap(db): snap = fakegamesnapsdb()[0] @@ -257,6 +261,7 @@ def addFakeGameStartSnap(db): db.add(db_snap) db.commit() + @pytest.fixture(scope="function") def addCustomGameSnap(request, db): boardStr, move = request.param @@ -276,11 +281,13 @@ def addCustomGameSnap(request, db): 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 @@ -288,6 +295,7 @@ def classicSetup(db, addFakeUsers, addFakeGames, fakegamesdb, addFakeGameSnaps): return firstgame_uuid, token + @pytest.fixture(scope="session") def db_engine(): SQLALCHEMY_DATABASE_URL = "sqlite:///./test_db.db" @@ -324,6 +332,15 @@ def client(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 + + def getToken(username): return crud.create_access_token( data={"sub": username}, expires_delta=timedelta(minutes=3000) @@ -338,5 +355,3 @@ def game_setup(db, addFakeUsers, addFakeGames, addFakeGameStartSnap, fakegamesdb 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 index 30ce6ea..65314be 100644 --- a/tests/server/test_btchApi.py +++ b/tests/server/test_btchApi.py @@ -1,11 +1,11 @@ +import asyncio import sys import time -import asyncio -import pytest import unittest.mock as mock from datetime import datetime, timedelta, timezone from pathlib import Path +import pytest from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker @@ -14,8 +14,8 @@ except ImportError: print("PIL module is not installed. Some tests will be skipped") -from httpx import AsyncClient from fastapi.testclient import TestClient +from httpx import AsyncClient from battlechess.server import crud, models from battlechess.server.btchApi import app, get_db @@ -27,11 +27,13 @@ def testDataDir(): 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 @@ -56,7 +58,7 @@ def test__createUser(client): 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", @@ -68,6 +70,7 @@ def test__createUser(client): "status": "active", } + def test__create_user__with_avatar(client): get_password_hash("secret") new_avatar = "images/avatar001.jpeg" @@ -120,6 +123,7 @@ def _test__update_user__full_name(db, client, addFakeUsers): "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 @@ -151,6 +155,7 @@ def test__upload_user__avatarImage(db, client, addFakeUsers): # 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 @@ -167,10 +172,12 @@ def test__upload_user__avatarImage__file_too_big(db, client, addFakeUsers): 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 @@ -197,6 +204,7 @@ def test__authenticate(client): assert response.status_code == 200 assert list(response.json().keys()) == ["access_token", "token_type"] + def test__createUser__persistence(client): response = client.post( "/users/", @@ -230,9 +238,11 @@ def test__createUser__persistence(client): assert response.status_code == 200 assert response.json() == ["alice"] + def test__addFakeUsers(db, addFakeUsers): pass + def test__getUsernames(db, client, addFakeUsers): token, _ = addFakeUsers @@ -246,6 +256,7 @@ def test__getUsernames(db, client, addFakeUsers): assert response.status_code == 200 assert response.json() == ["johndoe", "janedoe"] + def test__getUserById(db, client, addFakeUsers): token, _ = addFakeUsers @@ -264,6 +275,7 @@ def test__getUserById(db, client, addFakeUsers): "status": "active", } + def test__getUserById__malformedId(db, client, addFakeUsers): token, _ = addFakeUsers @@ -276,12 +288,14 @@ def test__getUserById__malformedId(db, client, addFakeUsers): 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 @@ -311,6 +325,7 @@ def test__createGame(db, client, addFakeUsers): "winner": None, } + def test__get_game_by_uuid(db, client, addFakeUsers, addFakeGames, johndoe): token, _ = addFakeUsers uuid = addFakeGames @@ -342,6 +357,7 @@ def test__get_game_by_uuid(db, client, addFakeUsers, addFakeGames, johndoe): "winner": None, } + def test__get_me_games(db, client, addFakeUsers, addFakeGames, johndoe, janedoe): token, _ = addFakeUsers @@ -368,9 +384,12 @@ def test__get_me_games(db, client, addFakeUsers, addFakeGames, johndoe, janedoe) "turn": "black", "white": johndoe, "winner": None, - } + } + -def test__getGames__finishedGame(db, client, addFakeUsers, addFakeGames, addFakeGameSnaps, johndoe, janedoe): +def test__getGames__finishedGame( + db, client, addFakeUsers, addFakeGames, addFakeGameSnaps, johndoe, janedoe +): # change to second player jane_token = getToken("janedoe") _ = getToken("johndoe") @@ -389,6 +408,7 @@ def test__getGames__finishedGame(db, client, addFakeUsers, addFakeGames, addFake 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 @@ -426,6 +446,7 @@ def test__joinRandomGame(db, client, addFakeUsers, addFakeGames): "winner": None, } + # TODO deprecated, client chooses game and joins a random one def test__joinRandomGame__noneAvailable(db, client, addFakeUsers, addFakeDoneGames): token, _ = addFakeUsers @@ -442,6 +463,7 @@ def test__joinRandomGame__noneAvailable(db, client, addFakeUsers, addFakeDoneGam 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 @@ -458,6 +480,7 @@ def test__get_available_games__all(db, client, addFakeUsers, addFakeGames): 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 @@ -474,9 +497,12 @@ def test__get_available_games__waiting(db, client, addFakeUsers, addFakeGames): 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) + (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 @@ -485,9 +511,7 @@ def _test__joinGame__playerAlreadyInGame(db, client, addFakeUsers, addFakeGames) user = crud.get_user_by_username(db, username) - game_before = ( - db.query(models.Game).filter(models.Game.uuid == uuid).first() - ) + game_before = db.query(models.Game).filter(models.Game.uuid == uuid).first() response = client.get( f"/games/{uuid}/join", @@ -518,6 +542,7 @@ def _test__joinGame__playerAlreadyInGame(db, client, addFakeUsers, addFakeGames) "white_id": game.white_id, } + def test__joinGame__playerAlreadyInGame__simple(db, client, addFakeUsers, addFakeGames): token, username = addFakeUsers uuid = addFakeGames @@ -536,6 +561,7 @@ def test__joinGame__playerAlreadyInGame__simple(db, client, addFakeUsers, addFak 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"] @@ -571,6 +597,7 @@ def test__getsnap__byNum(db, client, classicSetup, fakegamesdb, addFakeGameSnaps } # yapf: enable + def test__getsnaps(db, client, classicSetup): firstgame_uuid, token = classicSetup @@ -620,6 +647,7 @@ def test__getsnaps(db, client, classicSetup): }] # yapf: enable + def test__getsnap__latest(db, client, classicSetup): firstgame_uuid, token = classicSetup @@ -653,6 +681,7 @@ def test__getsnap__latest(db, client, classicSetup): } # yapf: enable + def test__getTurn(db, client, classicSetup): firstgame_uuid, token = classicSetup @@ -668,6 +697,7 @@ def test__getTurn(db, client, classicSetup): 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 @@ -680,43 +710,12 @@ def test__getTurn__long_polling(client, classicSetup): "Authorization": "Bearer " + token, "Content-Type": "application/json", }, - params={ - "long_polling": True - } + params={"long_polling": True}, ) elapsed = time.time() - start assert elapsed > 5 -@pytest.mark.anyio -async def test_root(): - async with AsyncClient(app=app, base_url="http://test") as ac: - response = await ac.get("/") - assert response.status_code == 200 - assert response.json() == {"message": "Tomato"} - -@pytest.mark.anyio -async def test__getTurn__long_polling_async(classicSetup): - firstgame_uuid, _ = classicSetup - token = getToken("janedoe") - - start = time.time() - async with AsyncClient(app=app, base_url="http://test") as ac: - response = await ac.get( - f"/games/{firstgame_uuid}/turn", - headers={ - "Authorization": "Bearer " + token, - "Content-Type": "application/json", - }, - params={ - "long_polling": True - } - ) - assert response.status_code == 200 - - elapsed = time.time() - start - assert elapsed > 5 - def test__move(db, client, classicSetup): firstgame_uuid, token = classicSetup @@ -724,9 +723,7 @@ def test__move(db, client, classicSetup): # get previous game/board game_before = ( - db.query(models.Game) - .filter(models.Game.uuid == firstgame_uuid) - .first() + db.query(models.Game).filter(models.Game.uuid == firstgame_uuid).first() ) response = client.post( @@ -743,9 +740,7 @@ def test__move(db, client, classicSetup): # get after game/board game_after = ( - db.query(models.Game) - .filter(models.Game.uuid == firstgame_uuid) - .first() + db.query(models.Game).filter(models.Game.uuid == firstgame_uuid).first() ) # test board is the expected one @@ -812,6 +807,7 @@ def test__move__filtered(db, client, classicSetup): "XXXXXXXX" ) + def test__possibleMoves__pawnMove(db, client, classicSetup): firstgame_uuid, _ = classicSetup # change to second player @@ -832,19 +828,25 @@ def test__possibleMoves__pawnMove(db, client, classicSetup): assert response.json() == ["d6", "d5"] + @pytest.mark.parametrize( - "addCustomGameSnap", - [(( - "____K___" - "________" - "________" - "__p_____" - "________" - "____pk__" - "___P_pp_" - "________" - ), "g3f3")], - indirect=True + "addCustomGameSnap", + [ + ( + ( + "____K___" + "________" + "________" + "__p_____" + "________" + "____pk__" + "___P_pp_" + "________" + ), + "g3f3", + ) + ], + indirect=True, ) def test__possibleMoves__king(db, client, classicSetup, addCustomGameSnap): firstgame_uuid, token = classicSetup @@ -866,20 +868,27 @@ def test__possibleMoves__king(db, client, classicSetup, addCustomGameSnap): @pytest.mark.parametrize( - "addCustomGameSnap", - [(( - "____K___" - "________" - "________" - "________" - "__pP____" - "____pk__" - "_____pp_" - "________" - ), "c2c4")], - indirect=True + "addCustomGameSnap", + [ + ( + ( + "____K___" + "________" + "________" + "________" + "__pP____" + "____pk__" + "_____pp_" + "________" + ), + "c2c4", + ) + ], + indirect=True, ) -def test__possibleMoves__pawn_enpassant_black(db, client, classicSetup, addCustomGameSnap): +def test__possibleMoves__pawn_enpassant_black( + db, client, classicSetup, addCustomGameSnap +): firstgame_uuid, _ = classicSetup token = getToken("janedoe") @@ -899,21 +908,29 @@ def test__possibleMoves__pawn_enpassant_black(db, client, classicSetup, addCusto assert response.json() == ["c3", "d3", "e3"] + @pytest.mark.parametrize( - "addCustomGameSnap", - [(( - "____K___" - "________" - "________" - "__pP____" - "________" - "____pk__" - "_____pp_" - "________" - ), "d7d5")], - indirect=True + "addCustomGameSnap", + [ + ( + ( + "____K___" + "________" + "________" + "__pP____" + "________" + "____pk__" + "_____pp_" + "________" + ), + "d7d5", + ) + ], + indirect=True, ) -def test__possibleMoves__pawn_enpassant_white(db, client, classicSetup, addCustomGameSnap): +def test__possibleMoves__pawn_enpassant_white( + db, client, classicSetup, addCustomGameSnap +): firstgame_uuid, token = classicSetup square = "c5" @@ -933,20 +950,27 @@ def test__possibleMoves__pawn_enpassant_white(db, client, classicSetup, addCusto @pytest.mark.parametrize( - "addCustomGameSnap", - [(( - "____K___" - "________" - "________" - "__pP____" - "________" - "____pk__" - "_____pp_" - "________" - ), "c7c5")], - indirect=True + "addCustomGameSnap", + [ + ( + ( + "____K___" + "________" + "________" + "__pP____" + "________" + "____pk__" + "_____pp_" + "________" + ), + "c7c5", + ) + ], + indirect=True, ) -def test__possibleMoves__pawn_impossible_enpassant_black(db, client, classicSetup, addCustomGameSnap): +def test__possibleMoves__pawn_impossible_enpassant_black( + db, client, classicSetup, addCustomGameSnap +): firstgame_uuid, _ = classicSetup token = getToken("janedoe") @@ -968,18 +992,23 @@ def test__possibleMoves__pawn_impossible_enpassant_black(db, client, classicSetu @pytest.mark.parametrize( - "addCustomGameSnap", - [(( - "____K___" - "_____PP_" - "_____p__" - "________" - "________" - "_____k__" - "_____pp_" - "________" - ), "f5f6")], - indirect=True + "addCustomGameSnap", + [ + ( + ( + "____K___" + "_____PP_" + "_____p__" + "________" + "________" + "_____k__" + "_____pp_" + "________" + ), + "f5f6", + ) + ], + indirect=True, ) def test__possibleMoves__pawn_take(db, client, classicSetup, addCustomGameSnap): firstgame_uuid, token = classicSetup @@ -999,6 +1028,7 @@ def test__possibleMoves__pawn_take(db, client, classicSetup, addCustomGameSnap): assert response.json() == ["g7"] + def send_move(client, game_uuid, move, token): response = client.post( f"/games/{game_uuid}/move", @@ -1012,12 +1042,14 @@ def send_move(client, game_uuid, move, token): ) 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 @@ -1062,6 +1094,7 @@ def test__move__filtered_pawn(db, client, game_setup, addFakeGameSnaps): "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): @@ -1091,6 +1124,7 @@ def test__move__fogTest(db, client, game_setup, addFakeGameSnaps): "XXXXXXXX" ) + def test__integrationTest__foolscheckmate(client): # create johndoe @@ -1184,13 +1218,17 @@ def test__integrationTest__foolscheckmate(client): white = response.json()["white"] response.json()["black"] jane_color = ( - None if not white else "white" if white["username"] == jane_username else "black" + 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 + if white["username"] == john_username else "black" ) @@ -1305,7 +1343,7 @@ def test__integrationTest__foolscheckmate(client): if john_color == "white": assert response.json()["board"] == boards[i][0] else: - assert response.json()["board"]== boards[i][1] + assert response.json()["board"] == boards[i][1] # checkmate @@ -1325,4 +1363,3 @@ def test__integrationTest__foolscheckmate(client): ) 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..3bc7898 --- /dev/null +++ b/tests/server/test_btchApi_async.py @@ -0,0 +1,48 @@ +import asyncio +import time +from datetime import datetime, timedelta, timezone + +import pytest +from httpx import AsyncClient + +from battlechess.server import crud, models +from battlechess.server.btchApi import app, get_db + + +@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.anyio +async def test__getTurn__long_polling_async(asyncclient, classicSetup): + firstgame_uuid, john_token = classicSetup + token = getToken("janedoe") + + start = time.time() + + response = await asyncclient.get( + f"/games/{firstgame_uuid}/turn", + headers={ + "Authorization": "Bearer " + token, + "Content-Type": "application/json", + }, + params={"long_polling": True}, + ) + assert response.status_code == 200 + + elapsed = time.time() - start + assert elapsed > 5 From 47415649fc4c49e4928f14816283c0e46f6891b7 Mon Sep 17 00:00:00 2001 From: Quim Date: Sat, 10 Feb 2024 13:19:17 +0100 Subject: [PATCH 36/43] proper async long_polling testing with create_task --- battlechess/server/btchApi.py | 19 ++++++-- tests/server/test_btchApi.py | 4 +- tests/server/test_btchApi_async.py | 72 ++++++++++++++++++++++++++++-- 3 files changed, 87 insertions(+), 8 deletions(-) diff --git a/battlechess/server/btchApi.py b/battlechess/server/btchApi.py index 506599a..5d561c0 100644 --- a/battlechess/server/btchApi.py +++ b/battlechess/server/btchApi.py @@ -410,6 +410,14 @@ def query_turn( 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 not long_polling: return game.turn @@ -419,14 +427,19 @@ def query_turn( while elapsed < 10: elapsed = time.time() - start game = get_game(gameUUID, current_user, db) - if game.turn != game.get_player_color(current_user.id): + if game.turn == game.get_player_color(current_user.id): + print(f":) it's {game.turn} turn and {current_user.username} is {game.get_player_color(current_user.id)}!") + return game.turn + else: print( - f"{current_user.username} is {game.get_player_color(current_user.id)} is not {game.turn}" + f"turn request {elapsed} s: {current_user.username} is {game.get_player_color(current_user.id)}. It's {game.turn} turn" ) - return game.turn time.sleep(1) + # should we return exception instead? + return game.turn +# 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, diff --git a/tests/server/test_btchApi.py b/tests/server/test_btchApi.py index 65314be..e15a5e1 100644 --- a/tests/server/test_btchApi.py +++ b/tests/server/test_btchApi.py @@ -24,7 +24,7 @@ from battlechess.server.utils import get_password_hash, verify_password -def testDataDir(): +def dataTestDir(): return Path(__file__).parent.parent / "data" / "avatars" @@ -129,7 +129,7 @@ def test__upload_user__avatarImage(db, client, addFakeUsers): token, _ = addFakeUsers oneUser = db.query(models.User)[1] - filename = testDataDir() / "test_avatar.jpeg" + filename = dataTestDir() / "test_avatar.jpeg" with open(filename, "rb") as f: img = Image.open(f) try: diff --git a/tests/server/test_btchApi_async.py b/tests/server/test_btchApi_async.py index 3bc7898..00bf877 100644 --- a/tests/server/test_btchApi_async.py +++ b/tests/server/test_btchApi_async.py @@ -26,18 +26,17 @@ def getToken(username): 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 - token = getToken("janedoe") start = time.time() response = await asyncclient.get( f"/games/{firstgame_uuid}/turn", headers={ - "Authorization": "Bearer " + token, + "Authorization": "Bearer " + john_token, "Content-Type": "application/json", }, params={"long_polling": True}, @@ -46,3 +45,70 @@ async def test__getTurn__long_polling_async(asyncclient, classicSetup): 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", + headers={ + "Authorization": "Bearer " + john_token, + "Content-Type": "application/json", + }, + params={"long_polling": long_polling}, + ) + elapsed = time.time() - start + assert response.status_code == 200 + assert response.json() == 'black' + # ensure that non-long-polling get is non blocking + assert elapsed < 1 + + # ask turn again but with long polling + long_polling = True + premature_turn_ask_task = asyncio.create_task( + asyncclient.get( + f"/games/{firstgame_uuid}/turn", + headers={ + "Authorization": "Bearer " + john_token, + "Content-Type": "application/json", + }, + params={"long_polling": long_polling}, + ) + ) + + background_tasks.add(premature_turn_ask_task) + premature_turn_ask_task.add_done_callback(background_tasks.discard) + await asyncio.sleep(1) + 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() == 'white' + assert response.status_code == 200 From 32d1d1a32c1d88295a0af7afe8850541dd35d142 Mon Sep 17 00:00:00 2001 From: Quim Date: Sat, 10 Feb 2024 16:20:52 +0100 Subject: [PATCH 37/43] async turn with better doc --- battlechess/server/btchApi.py | 5 +++-- tests/server/test_btchApi_async.py | 14 +++++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/battlechess/server/btchApi.py b/battlechess/server/btchApi.py index 5d561c0..ff5b68b 100644 --- a/battlechess/server/btchApi.py +++ b/battlechess/server/btchApi.py @@ -1,4 +1,5 @@ import time +import asyncio from datetime import timedelta from typing import List, Union @@ -403,7 +404,7 @@ def query_board( # who's turn is it (None means that the game is over) @app.get("/games/{gameUUID}/turn") -def query_turn( +async def query_turn( gameUUID: str, current_user: schemas.User = Depends(get_current_active_user), db: Session = Depends(get_db), @@ -434,7 +435,7 @@ def query_turn( print( f"turn request {elapsed} s: {current_user.username} is {game.get_player_color(current_user.id)}. It's {game.turn} turn" ) - time.sleep(1) + await asyncio.sleep(1) # should we return exception instead? return game.turn diff --git a/tests/server/test_btchApi_async.py b/tests/server/test_btchApi_async.py index 00bf877..e6f6061 100644 --- a/tests/server/test_btchApi_async.py +++ b/tests/server/test_btchApi_async.py @@ -67,12 +67,13 @@ async def test__getTurn__long_polling_move(asyncclient, classicSetup): }, params={"long_polling": long_polling}, ) - elapsed = time.time() - start + short_polling_elapsed = time.time() - start assert response.status_code == 200 assert response.json() == 'black' # ensure that non-long-polling get is non blocking - assert elapsed < 1 + 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( @@ -86,9 +87,16 @@ async def test__getTurn__long_polling_move(asyncclient, classicSetup): ) ) + 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) + 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 From 5ebc193afd9e5c5ee5e219ad24b0ba6b660a6b1f Mon Sep 17 00:00:00 2001 From: Quim Date: Mon, 12 Feb 2024 00:17:18 +0100 Subject: [PATCH 38/43] db refresh on turn//me --- battlechess/server/btchApi.py | 46 +++++++++++++++++++++++++++++++++-- battlechess/server/crud.py | 2 +- battlechess/server/schemas.py | 1 - 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/battlechess/server/btchApi.py b/battlechess/server/btchApi.py index ff5b68b..f092a88 100644 --- a/battlechess/server/btchApi.py +++ b/battlechess/server/btchApi.py @@ -401,6 +401,50 @@ def query_board( ): pass +@app.get("/games/{gameUUID}/turn/me") +async def query_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(1) # who's turn is it (None means that the game is over) @app.get("/games/{gameUUID}/turn") @@ -422,14 +466,12 @@ async def query_turn( if not long_polling: return game.turn - print("long polling") start = time.time() elapsed = 0 while elapsed < 10: elapsed = time.time() - start game = get_game(gameUUID, current_user, db) if game.turn == game.get_player_color(current_user.id): - print(f":) it's {game.turn} turn and {current_user.username} is {game.get_player_color(current_user.id)}!") return game.turn else: print( diff --git a/battlechess/server/crud.py b/battlechess/server/crud.py index 15c0a0e..24bca43 100644 --- a/battlechess/server/crud.py +++ b/battlechess/server/crud.py @@ -267,7 +267,7 @@ def create_snap_by_move( print( 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() db.commit() diff --git a/battlechess/server/schemas.py b/battlechess/server/schemas.py index 38e47ec..7907e8c 100644 --- a/battlechess/server/schemas.py +++ b/battlechess/server/schemas.py @@ -183,4 +183,3 @@ class Move(BaseModel): origin: Tuple[int, int] destination: Tuple[int, int] color: str - From a46e62dacba19ea0db6d77b4aaeb9198d2056d5d Mon Sep 17 00:00:00 2001 From: Quim Date: Tue, 13 Feb 2024 00:10:00 +0100 Subject: [PATCH 39/43] increase turn recheck to 2 seconds --- battlechess/server/btchApi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/battlechess/server/btchApi.py b/battlechess/server/btchApi.py index f092a88..2992205 100644 --- a/battlechess/server/btchApi.py +++ b/battlechess/server/btchApi.py @@ -444,7 +444,7 @@ async def query_turn( if caller_turn or elapsed >= 10: return caller_turn else: - await asyncio.sleep(1) + await asyncio.sleep(2) # who's turn is it (None means that the game is over) @app.get("/games/{gameUUID}/turn") From cae7147fc8c7c2278bde3db8e5794049a8cacea2 Mon Sep 17 00:00:00 2001 From: Quim Date: Mon, 12 Feb 2024 00:18:38 +0100 Subject: [PATCH 40/43] remove long_polling from /turn, use /turn/me for longpolling --- battlechess/server/btchApi.py | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/battlechess/server/btchApi.py b/battlechess/server/btchApi.py index 2992205..f13f418 100644 --- a/battlechess/server/btchApi.py +++ b/battlechess/server/btchApi.py @@ -452,7 +452,6 @@ async def query_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) @@ -463,23 +462,20 @@ async def query_turn( headers={"Authorization": "Bearer"}, ) - if not long_polling: - return game.turn + 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"}, + ) - start = time.time() - elapsed = 0 - while elapsed < 10: - elapsed = time.time() - start - game = get_game(gameUUID, current_user, db) - if game.turn == game.get_player_color(current_user.id): - return game.turn - else: - print( - f"turn request {elapsed} s: {current_user.username} is {game.get_player_color(current_user.id)}. It's {game.turn} turn" - ) - await asyncio.sleep(1) + if game.is_finished(): + raise HTTPException( + status_code=status.HTTP_412_PRECONDITION_FAILED, + detail="game is over", + headers={"Authorization": "Bearer"}, + ) - # should we return exception instead? return game.turn # TODO we're not checking it's the request's player turn LOL From d4946825a5d784967c19508a16095d7a3e6bf583 Mon Sep 17 00:00:00 2001 From: Quim Date: Mon, 12 Feb 2024 23:12:12 +0100 Subject: [PATCH 41/43] adapt tests to returning 412 when asking for turn on a finished game --- tests/server/test_btchApi.py | 14 +++++++++----- tests/server/test_btchApi_async.py | 8 ++++---- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/tests/server/test_btchApi.py b/tests/server/test_btchApi.py index e15a5e1..cccd7ae 100644 --- a/tests/server/test_btchApi.py +++ b/tests/server/test_btchApi.py @@ -1297,8 +1297,10 @@ def test__integrationTest__foolscheckmate(client): }, ) - assert response.status_code == 200 + assert response.status_code == 200 or response.status_code == 412 + jane_turn = response.json() + response = client.get( f"/games/{game_uuid}/turn", headers={ @@ -1307,10 +1309,12 @@ def test__integrationTest__foolscheckmate(client): }, ) - assert response.status_code == 200 + assert response.status_code == 200 or response.status_code == 412 + + game_finished = not (response.status_code == 200) + john_turn = response.json() - # TODO what happens after checkmate? assert jane_turn == john_turn response = client.get( @@ -1324,7 +1328,7 @@ def test__integrationTest__foolscheckmate(client): print(prettyBoard(response.json()["board"])) # no winner - if john_turn or jane_turn: + 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] @@ -1339,7 +1343,7 @@ def test__integrationTest__foolscheckmate(client): }, ) - if john_turn or jane_turn: + if not game_finished: if john_color == "white": assert response.json()["board"] == boards[i][0] else: diff --git a/tests/server/test_btchApi_async.py b/tests/server/test_btchApi_async.py index e6f6061..9ad10af 100644 --- a/tests/server/test_btchApi_async.py +++ b/tests/server/test_btchApi_async.py @@ -60,7 +60,7 @@ async def test__getTurn__long_polling_move(asyncclient, classicSetup): # Check that's it's not john'd turn long_polling = False response = await asyncclient.get( - f"/games/{firstgame_uuid}/turn", + f"/games/{firstgame_uuid}/turn/me", headers={ "Authorization": "Bearer " + john_token, "Content-Type": "application/json", @@ -69,7 +69,7 @@ async def test__getTurn__long_polling_move(asyncclient, classicSetup): ) short_polling_elapsed = time.time() - start assert response.status_code == 200 - assert response.json() == 'black' + assert response.json() == False # ensure that non-long-polling get is non blocking assert short_polling_elapsed < 1 @@ -78,7 +78,7 @@ async def test__getTurn__long_polling_move(asyncclient, classicSetup): long_polling = True premature_turn_ask_task = asyncio.create_task( asyncclient.get( - f"/games/{firstgame_uuid}/turn", + f"/games/{firstgame_uuid}/turn/me", headers={ "Authorization": "Bearer " + john_token, "Content-Type": "application/json", @@ -118,5 +118,5 @@ async def test__getTurn__long_polling_move(asyncclient, classicSetup): assert elapsed < hard_turn_timeout/2 response = premature_turn_ask_task.result() print(f"task result {response.json()}") - assert response.json() == 'white' + assert response.json() == True assert response.status_code == 200 From 943d5bfddeb7e77776a25115b982a768e10d97a5 Mon Sep 17 00:00:00 2001 From: quimnuss Date: Wed, 14 Feb 2024 21:56:04 +0100 Subject: [PATCH 42/43] fix linting errors --- .flake8 | 4 ++ battlechess/server/btchApi.py | 24 ++++++++---- poetry.lock | 53 ++++++++++++++++++++++++++- pyproject.toml | 3 +- tests/server/conftest.py | 14 +++---- tests/server/test_btchApi.py | 50 ++++++++++--------------- tests/server/test_btchApi_async.py | 18 ++++----- tests/server/test_btchApi_autoplay.py | 18 ++++----- 8 files changed, 115 insertions(+), 69 deletions(-) create mode 100644 .flake8 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..6d4f2d9 --- /dev/null +++ b/.flake8 @@ -0,0 +1,4 @@ +[flake8] +ignore = E226,E302,E41 +max-line-length = 105 +exclude = .git,__pycache__,docs/source/conf.py,old,build,dist diff --git a/battlechess/server/btchApi.py b/battlechess/server/btchApi.py index f13f418..994c412 100644 --- a/battlechess/server/btchApi.py +++ b/battlechess/server/btchApi.py @@ -1,5 +1,5 @@ -import time import asyncio +import time from datetime import timedelta from typing import List, Union @@ -201,7 +201,8 @@ def create_user(new_user: schemas.UserCreate, db: Session = Depends(get_db)): ): 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: @@ -370,7 +371,8 @@ def join_game( # return game -# either creates a new game or joins an existing unstarted random game. Random games can not be joined via "join_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", response_model=schemas.Game) def join_random_game( current_user: schemas.User = Depends(get_current_active_user), @@ -401,8 +403,9 @@ def query_board( ): pass + @app.get("/games/{gameUUID}/turn/me") -async def query_turn( +async def query_me_turn( gameUUID: str, current_user: schemas.User = Depends(get_current_active_user), db: Session = Depends(get_db), @@ -439,13 +442,16 @@ async def query_turn( 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}") + 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") async def query_turn( @@ -478,11 +484,13 @@ async def query_turn( return game.turn + # 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), @@ -555,7 +563,7 @@ def get_moves( # 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 diff --git a/poetry.lock b/poetry.lock index fae5797..4620b45 100644 --- a/poetry.lock +++ b/poetry.lock @@ -430,6 +430,22 @@ docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1 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" @@ -695,6 +711,17 @@ files = [ [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" @@ -894,6 +921,17 @@ files = [ {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" @@ -1015,6 +1053,17 @@ files = [ [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" @@ -1543,5 +1592,5 @@ files = [ [metadata] lock-version = "2.0" -python-versions = "^3.8" -content-hash = "2f1d9f88bb0104ae572305737044e08285cddba9880425a546b007ada97ea5a9" +python-versions = ">=3.8.1,<4.0" +content-hash = "b44dd588f282f7704805c5d270b1e27870bb1de75ff4a88f6666f6e681658ead" diff --git a/pyproject.toml b/pyproject.toml index a6f9630..5d4c6f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ license = "GPLv3" readme = "README.md" [tool.poetry.dependencies] -python = "^3.8" +python = ">=3.8.1,<4.0" httpx = "^0.25.2" python-decouple = "^3.8" pathlib2 = "^2.3.7.post1" @@ -34,6 +34,7 @@ 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"] diff --git a/tests/server/conftest.py b/tests/server/conftest.py index 397693e..8f1cb00 100644 --- a/tests/server/conftest.py +++ b/tests/server/conftest.py @@ -11,7 +11,7 @@ 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, verify_password +from battlechess.server.utils import get_password_hash @pytest.fixture @@ -202,7 +202,9 @@ def addFakeGamesFromDict(db, gamesdb): 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}" + 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 @@ -313,7 +315,7 @@ def db(db_engine): connection = db_engine.connect() # begin a non-ORM transaction - transaction = connection.begin() + transaction = connection.begin() # noqa # bind an individual Session to the connection db = Session(bind=connection) @@ -341,12 +343,6 @@ async def asyncclient(db): yield c -def getToken(username): - return crud.create_access_token( - data={"sub": username}, expires_delta=timedelta(minutes=3000) - ) - - @pytest.fixture(scope="function") def game_setup(db, addFakeUsers, addFakeGames, addFakeGameStartSnap, fakegamesdb): _, _ = addFakeUsers diff --git a/tests/server/test_btchApi.py b/tests/server/test_btchApi.py index cccd7ae..5fe929e 100644 --- a/tests/server/test_btchApi.py +++ b/tests/server/test_btchApi.py @@ -1,25 +1,18 @@ -import asyncio import sys import time import unittest.mock as mock -from datetime import datetime, timedelta, timezone +from datetime import timedelta from pathlib import Path import pytest -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker try: from PIL import Image except ImportError: print("PIL module is not installed. Some tests will be skipped") -from fastapi.testclient import TestClient -from httpx import AsyncClient 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, verify_password @@ -179,7 +172,6 @@ def test__getUsers__unauthorized(client): def test__authenticate(client): - # add a user response = client.post( "/users/", @@ -290,7 +282,6 @@ def test__getUserById__malformedId(db, client, addFakeUsers): def test__db_cleanup(db): - users = db.query(models.User).all() assert users == [] @@ -621,13 +612,13 @@ def test__getsnaps(db, client, classicSetup): 'castleable': 'lks', 'move_number': 0, 'board': ('xxxxxxxx' - 'xxxxxxxx' - '________' - '________' - '________' - '________' - 'pppppppp' - 'rnbqkbnr'), + 'xxxxxxxx' + '________' + '________' + '________' + '________' + 'pppppppp' + 'rnbqkbnr'), }, { 'game_id': 1, 'created_at': mock.ANY, @@ -637,13 +628,13 @@ def test__getsnaps(db, client, classicSetup): 'castleable': 'lks', 'move_number': 1, 'board': ('xxxxxxxx' - 'xxxxxxxx' - '________' - '________' - '___p____' - '________' - 'ppp_pppp' - 'rnbqkbnr'), + 'xxxxxxxx' + '________' + '________' + '___p____' + '________' + 'ppp_pppp' + 'rnbqkbnr'), }] # yapf: enable @@ -704,7 +695,7 @@ def test__getTurn__long_polling(client, classicSetup): token = getToken("janedoe") start = time.time() - response = client.get( + _ = client.get( f"/games/{firstgame_uuid}/turn", headers={ "Authorization": "Bearer " + token, @@ -719,7 +710,7 @@ def test__getTurn__long_polling(client, classicSetup): def test__move(db, client, classicSetup): firstgame_uuid, token = classicSetup - jane_token = getToken('janedoe') + jane_token = getToken("janedoe") # get previous game/board game_before = ( @@ -761,9 +752,10 @@ def test__move(db, client, classicSetup): "rnbqkbnr" ) + def test__move__wrong_turn(db, client, classicSetup): firstgame_uuid, john_token = classicSetup - + response = client.post( f"/games/{firstgame_uuid}/move", headers={ @@ -777,6 +769,7 @@ def test__move__wrong_turn(db, client, classicSetup): assert response.status_code == 403 + def test__move__filtered(db, client, classicSetup): firstgame_uuid, _ = classicSetup # change to second player @@ -1051,7 +1044,6 @@ def prettyBoard(boardStr): def test__move__filtered_pawn(db, client, game_setup, addFakeGameSnaps): - firstgame_uuid, john_token, jane_token = game_setup tokens = [jane_token, john_token] @@ -1098,7 +1090,6 @@ def test__move__filtered_pawn(db, client, game_setup, addFakeGameSnaps): # 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] @@ -1126,7 +1117,6 @@ def test__move__fogTest(db, client, game_setup, addFakeGameSnaps): def test__integrationTest__foolscheckmate(client): - # create johndoe # create janedoe diff --git a/tests/server/test_btchApi_async.py b/tests/server/test_btchApi_async.py index 9ad10af..a4c95c3 100644 --- a/tests/server/test_btchApi_async.py +++ b/tests/server/test_btchApi_async.py @@ -1,12 +1,10 @@ import asyncio import time -from datetime import datetime, timedelta, timezone +from datetime import timedelta import pytest -from httpx import AsyncClient -from battlechess.server import crud, models -from battlechess.server.btchApi import app, get_db +from battlechess.server import crud @pytest.fixture(scope="module") @@ -26,6 +24,7 @@ def getToken(username): 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): @@ -46,6 +45,7 @@ async def test__getTurn__long_polling_async(asyncclient, classicSetup): elapsed = time.time() - start assert elapsed > 5 + @pytest.mark.anyio async def test__getTurn__long_polling_move(asyncclient, classicSetup): firstgame_uuid, john_token = classicSetup @@ -69,7 +69,7 @@ async def test__getTurn__long_polling_move(asyncclient, classicSetup): ) short_polling_elapsed = time.time() - start assert response.status_code == 200 - assert response.json() == False + assert response.json() is False # ensure that non-long-polling get is non blocking assert short_polling_elapsed < 1 @@ -93,12 +93,12 @@ async def test__getTurn__long_polling_move(asyncclient, classicSetup): 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 + 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", @@ -115,8 +115,8 @@ async def test__getTurn__long_polling_move(asyncclient, classicSetup): # gather turn and assert it didn't timeout await premature_turn_ask_task elapsed = time.time() - start - assert elapsed < hard_turn_timeout/2 + assert elapsed < hard_turn_timeout / 2 response = premature_turn_ask_task.result() print(f"task result {response.json()}") - assert response.json() == True + 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 index f42d9f6..5e36ca9 100644 --- a/tests/server/test_btchApi_autoplay.py +++ b/tests/server/test_btchApi_autoplay.py @@ -1,16 +1,9 @@ import json + import pytest -from datetime import datetime, timedelta, timezone -from fastapi.testclient import TestClient -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker +from battlechess.server import models -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 def prettyBoard(boardStr): print(" abcdefgh") @@ -18,6 +11,7 @@ def prettyBoard(boardStr): 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", @@ -31,11 +25,13 @@ def send_move(client, game_uuid, move, token): ) 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 @@ -57,8 +53,9 @@ def test__move__MrExonGame__OneGame__EnPassant(client, game_setup): 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] @@ -80,6 +77,7 @@ def test__move__MrExonGame__Enpassant2(db, client, game_setup): 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 From 005e6b33a017990fd7633f45a2fe2615e3baf4cd Mon Sep 17 00:00:00 2001 From: quimnuss Date: Wed, 14 Feb 2024 22:02:58 +0100 Subject: [PATCH 43/43] fix more linting tests --- .flake8 | 2 +- battlechess/server/btchApi.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.flake8 b/.flake8 index 6d4f2d9..5f8512e 100644 --- a/.flake8 +++ b/.flake8 @@ -1,4 +1,4 @@ [flake8] -ignore = E226,E302,E41 +extend-ignore = E226,E302,E41,W503,E203 max-line-length = 105 exclude = .git,__pycache__,docs/source/conf.py,old,build,dist diff --git a/battlechess/server/btchApi.py b/battlechess/server/btchApi.py index 994c412..41dfe59 100644 --- a/battlechess/server/btchApi.py +++ b/battlechess/server/btchApi.py @@ -564,6 +564,7 @@ def get_moves( # TODO remove this if we're happy with a weird validation error message if moves: assert isinstance(moves[0], str) + return moves