diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index af811ec..9cf302b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,6 +21,15 @@ jobs: - uses: actions/setup-python@v4 with: python-version: '3.11' + # because pre-commit uses external mypy + - name: install mypy + run: | + pip install poetry + poetry config virtualenvs.create false + poetry install --only main,typecheck + # https://github.com/typeddjango/django-stubs/issues/458 + - name: create .env file + run: cp example.env .env - uses: pre-commit/action@v3.0.0 unit_test: @@ -35,7 +44,9 @@ jobs: run: | pip install poetry poetry config virtualenvs.create false - poetry install + poetry install --only main + - name: create .env file + run: cp example.env .env - name: Run tests env: SECRET_KEY: secret diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3a19216..5df3082 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.13 + rev: v0.2.1 hooks: - id: ruff-format - id: ruff @@ -23,7 +23,12 @@ repos: exclude: >- ^.*.md$ - # - repo: https://github.com/pre-commit/mirrors-mypy - # rev: v1.8.0 - # hooks: - # - id: mypy + # local mypy because of stub dependencies + - repo: local + hooks: + - id: typecheck + name: Typecheck + entry: mypy . + types: [python] + language: system + pass_filenames: false diff --git a/Dockerfile b/Dockerfile index f311979..f5f427a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ ENV PYTHONUNBUFFERED=yes \ WORKDIR /src -COPY poetry.lock pyproject.toml . +COPY poetry.lock pyproject.toml ./ RUN : \ # psycopg runtime dep @@ -36,7 +36,8 @@ COPY src . RUN : \ && mkdir /home/website \ && mkdir /home/website/statics \ - && mkdir /home/website/media + && mkdir /home/website/media \ + && mkdir /home/website/logs # removes \r from script and makes it executable. # both of these are caused by windows users touching file and not configuring git properly @@ -44,4 +45,6 @@ RUN : \ && sed -i 's/\r//g' entrypoint.sh \ && chmod +x entrypoint.sh +RUN crontab -l | { cat; echo "* * * * * python /src/manage.py send_queued_mail >> /home/website/logs/send_mail.log 2>&1"; } | crontab - + ENTRYPOINT ["./entrypoint.sh"] diff --git a/README.md b/README.md index a781ff5..6a129c1 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,11 @@ The all-in-one backend application for [Unitystation](https://github.com/unityst ## Development guide -### Settings file +### Environment setup -Copy `example.env` to `.env` and customize it. +Copy `example.env` to `.env` and customize it. You can then start development by either using docker or running the project locally. -### Setting up python +### Setting up python to run the project locally You will need python 3.11+ @@ -60,6 +60,13 @@ Install dev dependencies poetry install ``` +#### Start the server + +from the src folder run +```sh +python manage.py runserver +``` + #### pre-commit pre-commit is a git hook which runs every time you make a commit to catch linting and formatting errors early. @@ -78,14 +85,15 @@ Docker (with help of compose) lets you launch entire project including database 2- Launch project by running `docker compose -f dev-compose.yml up --build`. -3- Test out the webui by accessing http://localhost:8000/ - -### Navigating web UI +### Try it out -Assuming you've managed to get a page running on http://localhost:8000/, we can now start doing things such as registering a test account. +After everything is done, you can access the web UI at http://localhost:8000/. Here you will see the automatic documentation for the API and you can test out the API end points. +Some other useful links: - http://localhost:8000/admin -> View all accounts and edit existing ones. -- http://localhost:8000/accounts/register -> Create an account (if you already don't have one) -- http://localhost:8000/accounts/verify-account -> Test account verfication manually. +- http://localhost:8000/accounts/register -> Create an account. +- http://localhost:8000/accounts/login-credentials -> Test loging in with a username and password. +- http://localhost:8000/accounts/login-token -> Test loging in with a token (see admin page if you lost the token after login with credentials). -To find more api end points or add new ones, check out `urls.py` under the respective folder of what feature you want mess around with. +You can also use [Bruno](https://www.usebruno.com/) (a Postman alternative) to test out the API. +The Bruno project is included in the repository and you can find it in the 'api-collection' folder in the root of the repository. diff --git a/api-collection/Auth/Mail confirmation/ConfirmAccount.bru b/api-collection/Auth/Mail confirmation/ConfirmAccount.bru new file mode 100644 index 0000000..d39a126 --- /dev/null +++ b/api-collection/Auth/Mail confirmation/ConfirmAccount.bru @@ -0,0 +1,11 @@ +meta { + name: ConfirmAccount + type: http + seq: 1 +} + +post { + url: {{baseUrl}}/accounts/confirm-account/ + body: none + auth: none +} diff --git a/api-collection/Auth/Mail confirmation/Resend mail confirmation.bru b/api-collection/Auth/Mail confirmation/Resend mail confirmation.bru new file mode 100644 index 0000000..c89db03 --- /dev/null +++ b/api-collection/Auth/Mail confirmation/Resend mail confirmation.bru @@ -0,0 +1,17 @@ +meta { + name: Resend mail confirmation + type: http + seq: 2 +} + +post { + url: {{baseUrl}}/accounts/resend-account-confirmation + body: json + auth: none +} + +body:json { + { + "email": "mail@mail.com" + } +} diff --git a/api-collection/Auth/ResetPassword/confirm the reset.bru b/api-collection/Auth/ResetPassword/confirm the reset.bru new file mode 100644 index 0000000..083d350 --- /dev/null +++ b/api-collection/Auth/ResetPassword/confirm the reset.bru @@ -0,0 +1,17 @@ +meta { + name: confirm the reset + type: http + seq: 2 +} + +post { + url: {{baseUrl}}/accounts/reset-password/ + body: json + auth: none +} + +body:json { + { + "password": "admin" + } +} diff --git a/api-collection/Auth/ResetPassword/request a reset.bru b/api-collection/Auth/ResetPassword/request a reset.bru new file mode 100644 index 0000000..ba36d97 --- /dev/null +++ b/api-collection/Auth/ResetPassword/request a reset.bru @@ -0,0 +1,17 @@ +meta { + name: request a reset + type: http + seq: 1 +} + +post { + url: {{baseUrl}}/accounts/reset-password/ + body: json + auth: none +} + +body:json { + { + "email": "admin@admin.com" + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 013697b..cdbacbe 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,9 +16,9 @@ services: expose: - 8000 command: gunicorn central_command.wsgi:application --bind 0.0.0.0:8000 - volumes: - - static-volume:/home/website/statics - - media-volume:/home/website/media + volumes: + - static-volume:/home/website/statics + - media-volume:/home/website/media volumes: db-data: diff --git a/example.env b/example.env index 6022369..c30f9a8 100644 --- a/example.env +++ b/example.env @@ -13,3 +13,8 @@ DB_ENGINE=django.db.backends.postgresql DB_NAME=postgres DB_HOST=db DB_PORT=5432 + +# Links to stuff +WEBSITE_URL = http://localhost:8000 +PASS_RESET_URL_SUFFIX = reset-password/ +ACCOUNT_CONFIRMATION_URL_SUFFIX = confirm-email/ diff --git a/poetry.lock b/poetry.lock index 61957f1..59563f0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -14,6 +14,99 @@ files = [ [package.extras] tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] +[[package]] +name = "attrs" +version = "23.2.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] +tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] + +[[package]] +name = "black" +version = "24.1.1" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-24.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2588021038bd5ada078de606f2a804cadd0a3cc6a79cb3e9bb3a8bf581325a4c"}, + {file = "black-24.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a95915c98d6e32ca43809d46d932e2abc5f1f7d582ffbe65a5b4d1588af7445"}, + {file = "black-24.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fa6a0e965779c8f2afb286f9ef798df770ba2b6cee063c650b96adec22c056a"}, + {file = "black-24.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:5242ecd9e990aeb995b6d03dc3b2d112d4a78f2083e5a8e86d566340ae80fec4"}, + {file = "black-24.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fc1ec9aa6f4d98d022101e015261c056ddebe3da6a8ccfc2c792cbe0349d48b7"}, + {file = "black-24.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0269dfdea12442022e88043d2910429bed717b2d04523867a85dacce535916b8"}, + {file = "black-24.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3d64db762eae4a5ce04b6e3dd745dcca0fb9560eb931a5be97472e38652a161"}, + {file = "black-24.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:5d7b06ea8816cbd4becfe5f70accae953c53c0e53aa98730ceccb0395520ee5d"}, + {file = "black-24.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e2c8dfa14677f90d976f68e0c923947ae68fa3961d61ee30976c388adc0b02c8"}, + {file = "black-24.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a21725862d0e855ae05da1dd25e3825ed712eaaccef6b03017fe0853a01aa45e"}, + {file = "black-24.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07204d078e25327aad9ed2c64790d681238686bce254c910de640c7cc4fc3aa6"}, + {file = "black-24.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:a83fe522d9698d8f9a101b860b1ee154c1d25f8a82ceb807d319f085b2627c5b"}, + {file = "black-24.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:08b34e85170d368c37ca7bf81cf67ac863c9d1963b2c1780c39102187ec8dd62"}, + {file = "black-24.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7258c27115c1e3b5de9ac6c4f9957e3ee2c02c0b39222a24dc7aa03ba0e986f5"}, + {file = "black-24.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40657e1b78212d582a0edecafef133cf1dd02e6677f539b669db4746150d38f6"}, + {file = "black-24.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e298d588744efda02379521a19639ebcd314fba7a49be22136204d7ed1782717"}, + {file = "black-24.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:34afe9da5056aa123b8bfda1664bfe6fb4e9c6f311d8e4a6eb089da9a9173bf9"}, + {file = "black-24.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:854c06fb86fd854140f37fb24dbf10621f5dab9e3b0c29a690ba595e3d543024"}, + {file = "black-24.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3897ae5a21ca132efa219c029cce5e6bfc9c3d34ed7e892113d199c0b1b444a2"}, + {file = "black-24.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:ecba2a15dfb2d97105be74bbfe5128bc5e9fa8477d8c46766505c1dda5883aac"}, + {file = "black-24.1.1-py3-none-any.whl", hash = "sha256:5cdc2e2195212208fbcae579b931407c1fa9997584f0a415421748aeafff1168"}, + {file = "black-24.1.1.tar.gz", hash = "sha256:48b5760dcbfe5cf97fd4fba23946681f3a81514c6ab8a45b50da67ac8fbc6c7b"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "bleach" +version = "6.1.0" +description = "An easy safelist-based HTML-sanitizing tool." +optional = false +python-versions = ">=3.8" +files = [ + {file = "bleach-6.1.0-py3-none-any.whl", hash = "sha256:3225f354cfc436b9789c66c4ee030194bee0568fbf9cbdad3bc8b5c26c5f12b6"}, + {file = "bleach-6.1.0.tar.gz", hash = "sha256:0a31f1837963c41d46bbf1331b8778e1308ea0791db03cc4e7357b97cf42a8fe"}, +] + +[package.dependencies] +six = ">=1.9.0" +tinycss2 = {version = ">=1.1.0,<1.3", optional = true, markers = "extra == \"css\""} +webencodings = "*" + +[package.extras] +css = ["tinycss2 (>=1.1.0,<1.3)"] + +[[package]] +name = "certifi" +version = "2024.2.2" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, +] + [[package]] name = "cffi" version = "1.16.0" @@ -89,49 +182,182 @@ 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" +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.7" +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.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf"}, - {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d"}, - {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a"}, - {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15"}, - {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a"}, - {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1"}, - {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157"}, - {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406"}, - {file = "cryptography-41.0.7-cp37-abi3-win32.whl", hash = "sha256:f983596065a18a2183e7f79ab3fd4c475205b839e02cbc0efbbf9666c4b3083d"}, - {file = "cryptography-41.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:90452ba79b8788fa380dfb587cca692976ef4e757b194b093d845e8d99f612f2"}, - {file = "cryptography-41.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:079b85658ea2f59c4f43b70f8119a52414cdb7be34da5d019a77bf96d473b960"}, - {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b640981bf64a3e978a56167594a0e97db71c89a479da8e175d8bb5be5178c003"}, - {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e3114da6d7f95d2dee7d3f4eec16dacff819740bbab931aff8648cb13c5ff5e7"}, - {file = "cryptography-41.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d5ec85080cce7b0513cfd233914eb8b7bbd0633f1d1703aa28d1dd5a72f678ec"}, - {file = "cryptography-41.0.7-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7a698cb1dac82c35fcf8fe3417a3aaba97de16a01ac914b89a0889d364d2f6be"}, - {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:37a138589b12069efb424220bf78eac59ca68b95696fc622b6ccc1c0a197204a"}, - {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:68a2dec79deebc5d26d617bfdf6e8aab065a4f34934b22d3b5010df3ba36612c"}, - {file = "cryptography-41.0.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:09616eeaef406f99046553b8a40fbf8b1e70795a91885ba4c96a70793de5504a"}, - {file = "cryptography-41.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48a0476626da912a44cc078f9893f292f0b3e4c739caf289268168d8f4702a39"}, - {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c7f3201ec47d5207841402594f1d7950879ef890c0c495052fa62f58283fde1a"}, - {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c5ca78485a255e03c32b513f8c2bc39fedb7f5c5f8535545bdc223a03b24f248"}, - {file = "cryptography-41.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6c391c021ab1f7a82da5d8d0b3cee2f4b2c455ec86c8aebbc84837a631ff309"}, - {file = "cryptography-41.0.7.tar.gz", hash = "sha256:13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc"}, + {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]] @@ -147,13 +373,13 @@ files = [ [[package]] name = "django" -version = "3.2.23" +version = "3.2.24" description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.6" files = [ - {file = "Django-3.2.23-py3-none-any.whl", hash = "sha256:d48608d5f62f2c1e260986835db089fa3b79d6f58510881d316b8d88345ae6e1"}, - {file = "Django-3.2.23.tar.gz", hash = "sha256:82968f3640e29ef4a773af2c28448f5f7a08d001c6ac05b32d02aeee6509508b"}, + {file = "Django-3.2.24-py3-none-any.whl", hash = "sha256:5dd5b787c3ba39637610fe700f54bf158e33560ea0dba600c19921e7ff926ec5"}, + {file = "Django-3.2.24.tar.gz", hash = "sha256:aaee9fb0fb4ebd4311520887ad2e33313d368846607f82a9a0ed461cd4c35b18"}, ] [package.dependencies] @@ -166,16 +392,25 @@ argon2 = ["argon2-cffi (>=19.1.0)"] bcrypt = ["bcrypt"] [[package]] -name = "django-email-verification" -version = "0.0.7" -description = "Email confirmation app for django" +name = "django-post-office" +version = "3.8.0" +description = "A Django app to monitor and send mail asynchronously, complete with template support." optional = false -python-versions = ">=3.7" +python-versions = "*" files = [ - {file = "django-email-verification-0.0.7.tar.gz", hash = "sha256:02ed6b47c1311a18475f11eb7d002381d13b1fba73b0ce30c3435cc887f86a3b"}, - {file = "django_email_verification-0.0.7-py3-none-any.whl", hash = "sha256:43e137f4651413620dddc3b8935e06c458ce0ffd1aeafdc0678a187b13118761"}, + {file = "django-post_office-3.8.0.tar.gz", hash = "sha256:0df8a3595d8b6088a933d66d984787172e75dd009fa57bb439c6587ea5746df4"}, + {file = "django_post_office-3.8.0-py3-none-any.whl", hash = "sha256:21ae03dd6d09036ae96469c5418305aa39855985f29b21374c7a013b2ff47098"}, ] +[package.dependencies] +bleach = {version = "*", extras = ["css"]} +django = ">=3.2" +pytz = "*" + +[package.extras] +prevent-xss = ["bleach"] +test = ["tox (>=2.3)"] + [[package]] name = "django-rest-knox" version = "4.2.0" @@ -192,6 +427,43 @@ cryptography = "*" django = ">=3.2" djangorestframework = "*" +[[package]] +name = "django-stubs" +version = "4.2.7" +description = "Mypy stubs for Django" +optional = false +python-versions = ">=3.8" +files = [ + {file = "django-stubs-4.2.7.tar.gz", hash = "sha256:8ccd2ff4ee5adf22b9e3b7b1a516d2e1c2191e9d94e672c35cc2bc3dd61e0f6b"}, + {file = "django_stubs-4.2.7-py3-none-any.whl", hash = "sha256:4cf4de258fa71adc6f2799e983091b9d46cfc67c6eebc68fe111218c9a62b3b8"}, +] + +[package.dependencies] +django = "*" +django-stubs-ext = ">=4.2.7" +mypy = {version = ">=1.7.0,<1.8.0", optional = true, markers = "extra == \"compatible-mypy\""} +types-pytz = "*" +types-PyYAML = "*" +typing-extensions = "*" + +[package.extras] +compatible-mypy = ["mypy (>=1.7.0,<1.8.0)"] + +[[package]] +name = "django-stubs-ext" +version = "4.2.7" +description = "Monkey-patching and extensions for django-stubs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "django-stubs-ext-4.2.7.tar.gz", hash = "sha256:519342ac0849cda1559746c9a563f03ff99f636b0ebe7c14b75e816a00dfddc3"}, + {file = "django_stubs_ext-4.2.7-py3-none-any.whl", hash = "sha256:45a5d102417a412e3606e3c358adb4744988a92b7b58ccf3fd64bddd5d04d14c"}, +] + +[package.dependencies] +django = "*" +typing-extensions = "*" + [[package]] name = "djangorestframework" version = "3.14.0" @@ -207,6 +479,56 @@ files = [ django = ">=3.0" pytz = "*" +[[package]] +name = "djangorestframework-stubs" +version = "3.14.5" +description = "PEP-484 stubs for django-rest-framework" +optional = false +python-versions = ">=3.8" +files = [ + {file = "djangorestframework-stubs-3.14.5.tar.gz", hash = "sha256:5dd6f638aa5291fb7863e6166128a6ed20bf4986e2fc5cf334e6afc841797a09"}, + {file = "djangorestframework_stubs-3.14.5-py3-none-any.whl", hash = "sha256:43d788fd50cda49b922cd411e59c5b8cdc3f3de49c02febae12ce42139f0269b"}, +] + +[package.dependencies] +django-stubs = [ + {version = ">=4.2.7"}, + {version = "*", extras = ["compatible-mypy"], optional = true, markers = "extra == \"compatible-mypy\""}, +] +mypy = {version = ">=1.7.0,<1.8.0", optional = true, markers = "extra == \"compatible-mypy\""} +requests = ">=2.0.0" +types-PyYAML = ">=5.4.3" +types-requests = ">=0.1.12" +typing-extensions = ">=3.10.0" + +[package.extras] +compatible-mypy = ["django-stubs[compatible-mypy]", "mypy (>=1.7.0,<1.8.0)"] +coreapi = ["coreapi (>=2.0.0)"] +markdown = ["types-Markdown (>=0.1.5)"] + +[[package]] +name = "drf-spectacular" +version = "0.27.1" +description = "Sane and flexible OpenAPI 3 schema generation for Django REST framework" +optional = false +python-versions = ">=3.6" +files = [ + {file = "drf-spectacular-0.27.1.tar.gz", hash = "sha256:452e0cff3c12ee057b897508a077562967b9e62717992eeec10e62dbbc7b5a33"}, + {file = "drf_spectacular-0.27.1-py3-none-any.whl", hash = "sha256:0a4cada4b7136a0bf17233476c066c511a048bc6a485ae2140326ac7ba4003b2"}, +] + +[package.dependencies] +Django = ">=2.2" +djangorestframework = ">=3.10.3" +inflection = ">=0.3.1" +jsonschema = ">=2.6.0" +PyYAML = ">=5.1" +uritemplate = ">=2.0.0" + +[package.extras] +offline = ["drf-spectacular-sidecar"] +sidecar = ["drf-spectacular-sidecar"] + [[package]] name = "filelock" version = "3.13.1" @@ -257,6 +579,120 @@ files = [ [package.extras] license = ["ukkonen"] +[[package]] +name = "idna" +version = "3.6" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, +] + +[[package]] +name = "inflection" +version = "0.5.1" +description = "A port of Ruby on Rails inflector to Python" +optional = false +python-versions = ">=3.5" +files = [ + {file = "inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"}, + {file = "inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417"}, +] + +[[package]] +name = "jsonschema" +version = "4.21.1" +description = "An implementation of JSON Schema validation for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jsonschema-4.21.1-py3-none-any.whl", hash = "sha256:7996507afae316306f9e2290407761157c6f78002dcf7419acb99822143d1c6f"}, + {file = "jsonschema-4.21.1.tar.gz", hash = "sha256:85727c00279f5fa6bedbe6238d2aa6403bedd8b4864ab11207d07df3cc1b2ee5"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +jsonschema-specifications = ">=2023.03.6" +referencing = ">=0.28.4" +rpds-py = ">=0.7.1" + +[package.extras] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] + +[[package]] +name = "jsonschema-specifications" +version = "2023.12.1" +description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jsonschema_specifications-2023.12.1-py3-none-any.whl", hash = "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c"}, + {file = "jsonschema_specifications-2023.12.1.tar.gz", hash = "sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc"}, +] + +[package.dependencies] +referencing = ">=0.31.0" + +[[package]] +name = "mypy" +version = "1.7.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5da84d7bf257fd8f66b4f759a904fd2c5a765f70d8b52dde62b521972a0a2357"}, + {file = "mypy-1.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a3637c03f4025f6405737570d6cbfa4f1400eb3c649317634d273687a09ffc2f"}, + {file = "mypy-1.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b633f188fc5ae1b6edca39dae566974d7ef4e9aaaae00bc36efe1f855e5173ac"}, + {file = "mypy-1.7.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d6ed9a3997b90c6f891138e3f83fb8f475c74db4ccaa942a1c7bf99e83a989a1"}, + {file = "mypy-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:1fe46e96ae319df21359c8db77e1aecac8e5949da4773c0274c0ef3d8d1268a9"}, + {file = "mypy-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:df67fbeb666ee8828f675fee724cc2cbd2e4828cc3df56703e02fe6a421b7401"}, + {file = "mypy-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a79cdc12a02eb526d808a32a934c6fe6df07b05f3573d210e41808020aed8b5d"}, + {file = "mypy-1.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f65f385a6f43211effe8c682e8ec3f55d79391f70a201575def73d08db68ead1"}, + {file = "mypy-1.7.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e81ffd120ee24959b449b647c4b2fbfcf8acf3465e082b8d58fd6c4c2b27e46"}, + {file = "mypy-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:f29386804c3577c83d76520abf18cfcd7d68264c7e431c5907d250ab502658ee"}, + {file = "mypy-1.7.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:87c076c174e2c7ef8ab416c4e252d94c08cd4980a10967754f91571070bf5fbe"}, + {file = "mypy-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cb8d5f6d0fcd9e708bb190b224089e45902cacef6f6915481806b0c77f7786d"}, + {file = "mypy-1.7.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93e76c2256aa50d9c82a88e2f569232e9862c9982095f6d54e13509f01222fc"}, + {file = "mypy-1.7.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cddee95dea7990e2215576fae95f6b78a8c12f4c089d7e4367564704e99118d3"}, + {file = "mypy-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:d01921dbd691c4061a3e2ecdbfbfad029410c5c2b1ee88946bf45c62c6c91210"}, + {file = "mypy-1.7.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:185cff9b9a7fec1f9f7d8352dff8a4c713b2e3eea9c6c4b5ff7f0edf46b91e41"}, + {file = "mypy-1.7.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7a7b1e399c47b18feb6f8ad4a3eef3813e28c1e871ea7d4ea5d444b2ac03c418"}, + {file = "mypy-1.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc9fe455ad58a20ec68599139ed1113b21f977b536a91b42bef3ffed5cce7391"}, + {file = "mypy-1.7.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d0fa29919d2e720c8dbaf07d5578f93d7b313c3e9954c8ec05b6d83da592e5d9"}, + {file = "mypy-1.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b53655a295c1ed1af9e96b462a736bf083adba7b314ae775563e3fb4e6795f5"}, + {file = "mypy-1.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c1b06b4b109e342f7dccc9efda965fc3970a604db70f8560ddfdee7ef19afb05"}, + {file = "mypy-1.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bf7a2f0a6907f231d5e41adba1a82d7d88cf1f61a70335889412dec99feeb0f8"}, + {file = "mypy-1.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:551d4a0cdcbd1d2cccdcc7cb516bb4ae888794929f5b040bb51aae1846062901"}, + {file = "mypy-1.7.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:55d28d7963bef00c330cb6461db80b0b72afe2f3c4e2963c99517cf06454e665"}, + {file = "mypy-1.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:870bd1ffc8a5862e593185a4c169804f2744112b4a7c55b93eb50f48e7a77010"}, + {file = "mypy-1.7.0-py3-none-any.whl", hash = "sha256:96650d9a4c651bc2a4991cf46f100973f656d69edc7faf91844e87fe627f7e96"}, + {file = "mypy-1.7.0.tar.gz", hash = "sha256:1e280b5697202efa698372d2f39e9a6713a0395a756b1c6bd48995f8d72690dc"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[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" @@ -271,20 +707,42 @@ files = [ [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 = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + [[package]] name = "platformdirs" -version = "4.1.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.8" files = [ - {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, - {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, + {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 = "pre-commit" @@ -412,13 +870,13 @@ cli = ["click (>=5.0)"] [[package]] name = "pytz" -version = "2023.3.post1" +version = "2024.1" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" files = [ - {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"}, - {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, + {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, + {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, ] [[package]] @@ -480,6 +938,150 @@ files = [ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] +[[package]] +name = "referencing" +version = "0.33.0" +description = "JSON Referencing + Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "referencing-0.33.0-py3-none-any.whl", hash = "sha256:39240f2ecc770258f28b642dd47fd74bc8b02484de54e1882b74b35ebd779bd5"}, + {file = "referencing-0.33.0.tar.gz", hash = "sha256:c775fedf74bc0f9189c2a3be1c12fd03e8c23f4d371dce795df44e06c5b412f7"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +rpds-py = ">=0.7.0" + +[[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 = "rpds-py" +version = "0.17.1" +description = "Python bindings to Rust's persistent data structures (rpds)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "rpds_py-0.17.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:4128980a14ed805e1b91a7ed551250282a8ddf8201a4e9f8f5b7e6225f54170d"}, + {file = "rpds_py-0.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ff1dcb8e8bc2261a088821b2595ef031c91d499a0c1b031c152d43fe0a6ecec8"}, + {file = "rpds_py-0.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d65e6b4f1443048eb7e833c2accb4fa7ee67cc7d54f31b4f0555b474758bee55"}, + {file = "rpds_py-0.17.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a71169d505af63bb4d20d23a8fbd4c6ce272e7bce6cc31f617152aa784436f29"}, + {file = "rpds_py-0.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:436474f17733c7dca0fbf096d36ae65277e8645039df12a0fa52445ca494729d"}, + {file = "rpds_py-0.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:10162fe3f5f47c37ebf6d8ff5a2368508fe22007e3077bf25b9c7d803454d921"}, + {file = "rpds_py-0.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:720215373a280f78a1814becb1312d4e4d1077b1202a56d2b0815e95ccb99ce9"}, + {file = "rpds_py-0.17.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:70fcc6c2906cfa5c6a552ba7ae2ce64b6c32f437d8f3f8eea49925b278a61453"}, + {file = "rpds_py-0.17.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:91e5a8200e65aaac342a791272c564dffcf1281abd635d304d6c4e6b495f29dc"}, + {file = "rpds_py-0.17.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:99f567dae93e10be2daaa896e07513dd4bf9c2ecf0576e0533ac36ba3b1d5394"}, + {file = "rpds_py-0.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24e4900a6643f87058a27320f81336d527ccfe503984528edde4bb660c8c8d59"}, + {file = "rpds_py-0.17.1-cp310-none-win32.whl", hash = "sha256:0bfb09bf41fe7c51413f563373e5f537eaa653d7adc4830399d4e9bdc199959d"}, + {file = "rpds_py-0.17.1-cp310-none-win_amd64.whl", hash = "sha256:20de7b7179e2031a04042e85dc463a93a82bc177eeba5ddd13ff746325558aa6"}, + {file = "rpds_py-0.17.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:65dcf105c1943cba45d19207ef51b8bc46d232a381e94dd38719d52d3980015b"}, + {file = "rpds_py-0.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:01f58a7306b64e0a4fe042047dd2b7d411ee82e54240284bab63e325762c1147"}, + {file = "rpds_py-0.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:071bc28c589b86bc6351a339114fb7a029f5cddbaca34103aa573eba7b482382"}, + {file = "rpds_py-0.17.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae35e8e6801c5ab071b992cb2da958eee76340e6926ec693b5ff7d6381441745"}, + {file = "rpds_py-0.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149c5cd24f729e3567b56e1795f74577aa3126c14c11e457bec1b1c90d212e38"}, + {file = "rpds_py-0.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e796051f2070f47230c745d0a77a91088fbee2cc0502e9b796b9c6471983718c"}, + {file = "rpds_py-0.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60e820ee1004327609b28db8307acc27f5f2e9a0b185b2064c5f23e815f248f8"}, + {file = "rpds_py-0.17.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1957a2ab607f9added64478a6982742eb29f109d89d065fa44e01691a20fc20a"}, + {file = "rpds_py-0.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8587fd64c2a91c33cdc39d0cebdaf30e79491cc029a37fcd458ba863f8815383"}, + {file = "rpds_py-0.17.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4dc889a9d8a34758d0fcc9ac86adb97bab3fb7f0c4d29794357eb147536483fd"}, + {file = "rpds_py-0.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2953937f83820376b5979318840f3ee47477d94c17b940fe31d9458d79ae7eea"}, + {file = "rpds_py-0.17.1-cp311-none-win32.whl", hash = "sha256:1bfcad3109c1e5ba3cbe2f421614e70439f72897515a96c462ea657261b96518"}, + {file = "rpds_py-0.17.1-cp311-none-win_amd64.whl", hash = "sha256:99da0a4686ada4ed0f778120a0ea8d066de1a0a92ab0d13ae68492a437db78bf"}, + {file = "rpds_py-0.17.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1dc29db3900cb1bb40353772417800f29c3d078dbc8024fd64655a04ee3c4bdf"}, + {file = "rpds_py-0.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82ada4a8ed9e82e443fcef87e22a3eed3654dd3adf6e3b3a0deb70f03e86142a"}, + {file = "rpds_py-0.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d36b2b59e8cc6e576f8f7b671e32f2ff43153f0ad6d0201250a7c07f25d570e"}, + {file = "rpds_py-0.17.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3677fcca7fb728c86a78660c7fb1b07b69b281964673f486ae72860e13f512ad"}, + {file = "rpds_py-0.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:516fb8c77805159e97a689e2f1c80655c7658f5af601c34ffdb916605598cda2"}, + {file = "rpds_py-0.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df3b6f45ba4515632c5064e35ca7f31d51d13d1479673185ba8f9fefbbed58b9"}, + {file = "rpds_py-0.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a967dd6afda7715d911c25a6ba1517975acd8d1092b2f326718725461a3d33f9"}, + {file = "rpds_py-0.17.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dbbb95e6fc91ea3102505d111b327004d1c4ce98d56a4a02e82cd451f9f57140"}, + {file = "rpds_py-0.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02866e060219514940342a1f84303a1ef7a1dad0ac311792fbbe19b521b489d2"}, + {file = "rpds_py-0.17.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2528ff96d09f12e638695f3a2e0c609c7b84c6df7c5ae9bfeb9252b6fa686253"}, + {file = "rpds_py-0.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd345a13ce06e94c753dab52f8e71e5252aec1e4f8022d24d56decd31e1b9b23"}, + {file = "rpds_py-0.17.1-cp312-none-win32.whl", hash = "sha256:2a792b2e1d3038daa83fa474d559acfd6dc1e3650ee93b2662ddc17dbff20ad1"}, + {file = "rpds_py-0.17.1-cp312-none-win_amd64.whl", hash = "sha256:292f7344a3301802e7c25c53792fae7d1593cb0e50964e7bcdcc5cf533d634e3"}, + {file = "rpds_py-0.17.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:8ffe53e1d8ef2520ebcf0c9fec15bb721da59e8ef283b6ff3079613b1e30513d"}, + {file = "rpds_py-0.17.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4341bd7579611cf50e7b20bb8c2e23512a3dc79de987a1f411cb458ab670eb90"}, + {file = "rpds_py-0.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f4eb548daf4836e3b2c662033bfbfc551db58d30fd8fe660314f86bf8510b93"}, + {file = "rpds_py-0.17.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b686f25377f9c006acbac63f61614416a6317133ab7fafe5de5f7dc8a06d42eb"}, + {file = "rpds_py-0.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e21b76075c01d65d0f0f34302b5a7457d95721d5e0667aea65e5bb3ab415c25"}, + {file = "rpds_py-0.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b86b21b348f7e5485fae740d845c65a880f5d1eda1e063bc59bef92d1f7d0c55"}, + {file = "rpds_py-0.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f175e95a197f6a4059b50757a3dca33b32b61691bdbd22c29e8a8d21d3914cae"}, + {file = "rpds_py-0.17.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1701fc54460ae2e5efc1dd6350eafd7a760f516df8dbe51d4a1c79d69472fbd4"}, + {file = "rpds_py-0.17.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:9051e3d2af8f55b42061603e29e744724cb5f65b128a491446cc029b3e2ea896"}, + {file = "rpds_py-0.17.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:7450dbd659fed6dd41d1a7d47ed767e893ba402af8ae664c157c255ec6067fde"}, + {file = "rpds_py-0.17.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5a024fa96d541fd7edaa0e9d904601c6445e95a729a2900c5aec6555fe921ed6"}, + {file = "rpds_py-0.17.1-cp38-none-win32.whl", hash = "sha256:da1ead63368c04a9bded7904757dfcae01eba0e0f9bc41d3d7f57ebf1c04015a"}, + {file = "rpds_py-0.17.1-cp38-none-win_amd64.whl", hash = "sha256:841320e1841bb53fada91c9725e766bb25009cfd4144e92298db296fb6c894fb"}, + {file = "rpds_py-0.17.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:f6c43b6f97209e370124baf2bf40bb1e8edc25311a158867eb1c3a5d449ebc7a"}, + {file = "rpds_py-0.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7d63ec01fe7c76c2dbb7e972fece45acbb8836e72682bde138e7e039906e2c"}, + {file = "rpds_py-0.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81038ff87a4e04c22e1d81f947c6ac46f122e0c80460b9006e6517c4d842a6ec"}, + {file = "rpds_py-0.17.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:810685321f4a304b2b55577c915bece4c4a06dfe38f6e62d9cc1d6ca8ee86b99"}, + {file = "rpds_py-0.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:25f071737dae674ca8937a73d0f43f5a52e92c2d178330b4c0bb6ab05586ffa6"}, + {file = "rpds_py-0.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa5bfb13f1e89151ade0eb812f7b0d7a4d643406caaad65ce1cbabe0a66d695f"}, + {file = "rpds_py-0.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfe07308b311a8293a0d5ef4e61411c5c20f682db6b5e73de6c7c8824272c256"}, + {file = "rpds_py-0.17.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a000133a90eea274a6f28adc3084643263b1e7c1a5a66eb0a0a7a36aa757ed74"}, + {file = "rpds_py-0.17.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d0e8a6434a3fbf77d11448c9c25b2f25244226cfbec1a5159947cac5b8c5fa4"}, + {file = "rpds_py-0.17.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:efa767c220d94aa4ac3a6dd3aeb986e9f229eaf5bce92d8b1b3018d06bed3772"}, + {file = "rpds_py-0.17.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:dbc56680ecf585a384fbd93cd42bc82668b77cb525343170a2d86dafaed2a84b"}, + {file = "rpds_py-0.17.1-cp39-none-win32.whl", hash = "sha256:270987bc22e7e5a962b1094953ae901395e8c1e1e83ad016c5cfcfff75a15a3f"}, + {file = "rpds_py-0.17.1-cp39-none-win_amd64.whl", hash = "sha256:2a7b2f2f56a16a6d62e55354dd329d929560442bd92e87397b7a9586a32e3e76"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a3264e3e858de4fc601741498215835ff324ff2482fd4e4af61b46512dd7fc83"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:f2f3b28b40fddcb6c1f1f6c88c6f3769cd933fa493ceb79da45968a21dccc920"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9584f8f52010295a4a417221861df9bea4c72d9632562b6e59b3c7b87a1522b7"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c64602e8be701c6cfe42064b71c84ce62ce66ddc6422c15463fd8127db3d8066"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:060f412230d5f19fc8c8b75f315931b408d8ebf56aec33ef4168d1b9e54200b1"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9412abdf0ba70faa6e2ee6c0cc62a8defb772e78860cef419865917d86c7342"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9737bdaa0ad33d34c0efc718741abaafce62fadae72c8b251df9b0c823c63b22"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9f0e4dc0f17dcea4ab9d13ac5c666b6b5337042b4d8f27e01b70fae41dd65c57"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:1db228102ab9d1ff4c64148c96320d0be7044fa28bd865a9ce628ce98da5973d"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:d8bbd8e56f3ba25a7d0cf980fc42b34028848a53a0e36c9918550e0280b9d0b6"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:be22ae34d68544df293152b7e50895ba70d2a833ad9566932d750d3625918b82"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bf046179d011e6114daf12a534d874958b039342b347348a78b7cdf0dd9d6041"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:1a746a6d49665058a5896000e8d9d2f1a6acba8a03b389c1e4c06e11e0b7f40d"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0b8bf5b8db49d8fd40f54772a1dcf262e8be0ad2ab0206b5a2ec109c176c0a4"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f7f4cb1f173385e8a39c29510dd11a78bf44e360fb75610594973f5ea141028b"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7fbd70cb8b54fe745301921b0816c08b6d917593429dfc437fd024b5ba713c58"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bdf1303df671179eaf2cb41e8515a07fc78d9d00f111eadbe3e14262f59c3d0"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad059a4bd14c45776600d223ec194e77db6c20255578bb5bcdd7c18fd169361"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3664d126d3388a887db44c2e293f87d500c4184ec43d5d14d2d2babdb4c64cad"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:698ea95a60c8b16b58be9d854c9f993c639f5c214cf9ba782eca53a8789d6b19"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:c3d2010656999b63e628a3c694f23020322b4178c450dc478558a2b6ef3cb9bb"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:938eab7323a736533f015e6069a7d53ef2dcc841e4e533b782c2bfb9fb12d84b"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1e626b365293a2142a62b9a614e1f8e331b28f3ca57b9f05ebbf4cf2a0f0bdc5"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:380e0df2e9d5d5d339803cfc6d183a5442ad7ab3c63c2a0982e8c824566c5ccc"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b760a56e080a826c2e5af09002c1a037382ed21d03134eb6294812dda268c811"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5576ee2f3a309d2bb403ec292d5958ce03953b0e57a11d224c1f134feaf8c40f"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3c3461ebb4c4f1bbc70b15d20b565759f97a5aaf13af811fcefc892e9197ba"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:637b802f3f069a64436d432117a7e58fab414b4e27a7e81049817ae94de45d8d"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffee088ea9b593cc6160518ba9bd319b5475e5f3e578e4552d63818773c6f56a"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3ac732390d529d8469b831949c78085b034bff67f584559340008d0f6041a049"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:93432e747fb07fa567ad9cc7aaadd6e29710e515aabf939dfbed8046041346c6"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:7b7d9ca34542099b4e185b3c2a2b2eda2e318a7dbde0b0d83357a6d4421b5296"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:0387ce69ba06e43df54e43968090f3626e231e4bc9150e4c3246947567695f68"}, + {file = "rpds_py-0.17.1.tar.gz", hash = "sha256:0210b2668f24c078307260bf88bdac9d6f1093635df5123789bfee4d8d7fc8e7"}, +] + [[package]] name = "ruff" version = "0.1.13" @@ -522,6 +1124,17 @@ docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments 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 = "sqlparse" version = "0.4.4" @@ -538,6 +1151,99 @@ dev = ["build", "flake8"] doc = ["sphinx"] test = ["pytest", "pytest-cov"] +[[package]] +name = "tinycss2" +version = "1.2.1" +description = "A tiny CSS parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tinycss2-1.2.1-py3-none-any.whl", hash = "sha256:2b80a96d41e7c3914b8cda8bc7f705a4d9c49275616e886103dd839dfc847847"}, + {file = "tinycss2-1.2.1.tar.gz", hash = "sha256:8cff3a8f066c2ec677c06dbc7b45619804a6938478d9d73c284b29d14ecb0627"}, +] + +[package.dependencies] +webencodings = ">=0.4" + +[package.extras] +doc = ["sphinx", "sphinx_rtd_theme"] +test = ["flake8", "isort", "pytest"] + +[[package]] +name = "types-pytz" +version = "2024.1.0.20240203" +description = "Typing stubs for pytz" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-pytz-2024.1.0.20240203.tar.gz", hash = "sha256:c93751ee20dfc6e054a0148f8f5227b9a00b79c90a4d3c9f464711a73179c89e"}, + {file = "types_pytz-2024.1.0.20240203-py3-none-any.whl", hash = "sha256:9679eef0365db3af91ef7722c199dbb75ee5c1b67e3c4dd7bfbeb1b8a71c21a3"}, +] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.12" +description = "Typing stubs for PyYAML" +optional = false +python-versions = "*" +files = [ + {file = "types-PyYAML-6.0.12.12.tar.gz", hash = "sha256:334373d392fde0fdf95af5c3f1661885fa10c52167b14593eb856289e1855062"}, + {file = "types_PyYAML-6.0.12.12-py3-none-any.whl", hash = "sha256:c05bc6c158facb0676674b7f11fe3960db4f389718e19e62bd2b84d6205cfd24"}, +] + +[[package]] +name = "types-requests" +version = "2.31.0.20240125" +description = "Typing stubs for requests" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-requests-2.31.0.20240125.tar.gz", hash = "sha256:03a28ce1d7cd54199148e043b2079cdded22d6795d19a2c2a6791a4b2b5e2eb5"}, + {file = "types_requests-2.31.0.20240125-py3-none-any.whl", hash = "sha256:9592a9a4cb92d6d75d9b491a41477272b710e021011a2a3061157e2fb1f1a5d1"}, +] + +[package.dependencies] +urllib3 = ">=2" + +[[package]] +name = "typing-extensions" +version = "4.9.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, + {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, +] + +[[package]] +name = "uritemplate" +version = "4.1.1" +description = "Implementation of RFC 6570 URI Templates" +optional = false +python-versions = ">=3.6" +files = [ + {file = "uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e"}, + {file = "uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0"}, +] + +[[package]] +name = "urllib3" +version = "2.2.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.0-py3-none-any.whl", hash = "sha256:ce3711610ddce217e6d113a2732fafad960a03fd0318c91faa79481e35c11224"}, + {file = "urllib3-2.2.0.tar.gz", hash = "sha256:051d961ad0c62a94e50ecf1af379c3aba230c66c710493493560c0c223c49f20"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + [[package]] name = "virtualenv" version = "20.25.0" @@ -558,6 +1264,17 @@ platformdirs = ">=3.9.1,<5" 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 = "webencodings" +version = "0.5.1" +description = "Character encoding aliases for legacy web content" +optional = false +python-versions = "*" +files = [ + {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, + {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, +] + [[package]] name = "whitenoise" version = "6.6.0" @@ -575,4 +1292,4 @@ brotli = ["Brotli"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "1ede7b38160d864315094a30c17bfb8f2aae901c802adb60b532228ede436c5b" +content-hash = "25fb4d62da63d1bf5427c409dd1bd97d3d451a16ff14487aaccfa48221848223" diff --git a/pyproject.toml b/pyproject.toml index 614db8e..34b3f5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,8 @@ ignore = [ "S101", # it is annoying to annotate django Meta model properties as CalssVar for no reason "RUF012", + # treats a link with the word "password" in it as a hardcoded password + "S105", ] select = [ # pyflakes @@ -49,20 +51,27 @@ fixable = [ combine-as-imports = true lines-between-types = 1 -# with 'mypy' and 'django-stubs[compatible-mypy]' installed -# [tool.mypy] -# plugins = ["mypy_django_plugin.main"] -# -# [tool.django-stubs] -# django_settings_module = "central_command.settings" -# -# [[tool.mypy.overrides]] -# module = [ -# "django_email_verification.*", -# "rest_framework.*", -# "knox.*", -# ] -# ignore_missing_imports = true +[tool.mypy] +show_column_numbers = true +show_error_codes = true + +# XXX: add new rules here +check_untyped_defs = true + +plugins = [ + "mypy_django_plugin.main", + "mypy_drf_plugin.main", +] + +[tool.django-stubs] +django_settings_module = "central_command.settings" + +[[tool.mypy.overrides]] +module = [ + "post_office.*", + "knox.*", +] +ignore_missing_imports = true [tool.poetry] name = "central-command" @@ -75,15 +84,24 @@ python = "^3.11" Django = "^3.2.12" djangorestframework = "^3.12.1" psycopg2-binary = "2.9.9" -django-email-verification = "^0.0.7" django-rest-knox = "^4.1.0" gunicorn = "^20.1.0" python-dotenv = "^0.19.2" whitenoise = "^6.2.0" +django-post-office = "^3.8.0" +drf-spectacular = "^0.27.1" + +[tool.poetry.group.lint.dependencies] +pre-commit = "3.6.0" +ruff = "0.1.13" +black = "24.1.1" -[tool.poetry.group.dev.dependencies] -pre-commit = "^3.6.0" -ruff = "^0.1.13" +# typecheck is separate for CI +[tool.poetry.group.typecheck.dependencies] +# django-stubs does not support 1.8 yet +mypy = "1.7" +django-stubs = {extras = ["compatible-mypy"], version = "4.2.7"} +djangorestframework-stubs = {extras = ["compatible-mypy"], version = "3.14.5"} [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/src/accounts/admin.py b/src/accounts/admin.py index 3232d57..41faa02 100644 --- a/src/accounts/admin.py +++ b/src/accounts/admin.py @@ -1,6 +1,28 @@ from django.contrib import admin -from .models import Account +from .models import Account, AccountConfirmation, PasswordResetRequestModel + + +class AccountConfirmationInline(admin.TabularInline): + model = AccountConfirmation + extra = 0 + readonly_fields = ("token", "created_at", "is_token_valid_display") + + def is_token_valid_display(self, instance): + return instance.is_token_valid() + + is_token_valid_display.short_description = "Is Token Valid" # type: ignore[attr-defined] + + +class PasswordResetRequestInline(admin.TabularInline): + model = PasswordResetRequestModel + extra = 0 + readonly_fields = ("token", "created_at", "is_token_valid_display") + + def is_token_valid_display(self, instance): + return instance.is_token_valid() + + is_token_valid_display.short_description = "Is Token Valid" # type: ignore[attr-defined] @admin.register(Account) @@ -10,6 +32,7 @@ class AccountAdminView(admin.ModelAdmin): "is_active", "unique_identifier", "username", + "is_confirmed", "is_verified", "legacy_id", ) @@ -32,9 +55,11 @@ class AccountAdminView(admin.ModelAdmin): "classes": ("wide",), "fields": ( "is_active", + "is_confirmed", "is_verified", ), }, ), ("Legacy", {"classes": ("wide",), "fields": ("legacy_id",)}), ) + inlines = [AccountConfirmationInline, PasswordResetRequestInline] diff --git a/src/accounts/api/serializers.py b/src/accounts/api/serializers.py index 795fbf3..8b1dccf 100644 --- a/src/accounts/api/serializers.py +++ b/src/accounts/api/serializers.py @@ -1,9 +1,11 @@ +import secrets + from django.conf import settings from django.contrib.auth import authenticate -from django_email_verification import sendConfirm +from django.contrib.auth.password_validation import validate_password from rest_framework import serializers -from ..models import Account +from ..models import Account, AccountConfirmation, PasswordResetRequestModel class PublicAccountDataSerializer(serializers.ModelSerializer): @@ -25,9 +27,9 @@ class Meta: def create(self, validated_data): """Create and return a new account""" - account = Account.objects.create_user(**validated_data) + account: Account = Account.objects.create_user(**validated_data) if settings.REQUIRE_EMAIL_CONFIRMATION: - sendConfirm(account) + account.send_confirmation_mail() return account @@ -36,10 +38,10 @@ class LoginWithCredentialsSerializer(serializers.Serializer): password = serializers.CharField(style={"input_type": "password"}) def validate(self, data): - account = authenticate(username=data["email"], password=data["password"]) + account: Account | None = authenticate(username=data["email"], password=data["password"]) # type: ignore[assignment] if account is None: raise serializers.ValidationError("Unable to login with provided credentials.") - if not account.is_active: + if not account.is_confirmed: raise serializers.ValidationError( "This account hasn't done the mail confirmation step or has been disabled." ) @@ -59,8 +61,8 @@ def update(self, instance, validated_data): instance.set_password(validated_data.get("password", instance.password)) if old_email != instance.email and settings.REQUIRE_EMAIL_CONFIRMATION: - instance.is_active = False - sendConfirm(instance) + instance.is_confirmed = False + instance.send_confirmation_mail() instance.save() return instance @@ -81,3 +83,63 @@ def validate(self, data): "Verification token seems invalid or maybe outdated. Try requesting a new one." ) return account + + +class ResetPasswordSerializer(serializers.ModelSerializer): + class Meta: + model = Account + fields = ("password",) + extra_kwargs = {"password": {"write_only": True}} + + def validate_password(self, value): + # Validate the password using Django's built-in validators + validate_password(value) + return value + + +class ResetPasswordRequestSerializer(serializers.ModelSerializer): + class Meta: + model = PasswordResetRequestModel + fields = ("email",) + + email = serializers.EmailField() + + def validate(self, data): + email = data["email"] + account = Account.objects.get(email=email) + if account is None: + raise serializers.ValidationError("Account with this email doesn't exist.") + + return { + "token": secrets.token_urlsafe(32), + "account": account, + } + + def create(self, validated_data): + return PasswordResetRequestModel.objects.create(**validated_data) + + +class ConfirmAccountSerializer(serializers.Serializer): + token = serializers.CharField() + + def validate(self, data): + try: + account_confirmation = AccountConfirmation.objects.get(token=data["token"]) + except AccountConfirmation.DoesNotExist: + raise serializers.ValidationError("Token is invalid or expired.") + + if not account_confirmation.is_token_valid(): + raise serializers.ValidationError("Token is invalid or expired.") + return account_confirmation + + +class ResendAccountSerializer(serializers.Serializer): + email = serializers.EmailField() + + def validate(self, data): + email = data["email"] + account = Account.objects.get(email=email) + if account is None: + raise Account.DoesNotExist("Account with this email doesn't exist.") + + return account diff --git a/src/accounts/api/urls.py b/src/accounts/api/urls.py index f8c0849..c2b9f28 100644 --- a/src/accounts/api/urls.py +++ b/src/accounts/api/urls.py @@ -2,11 +2,14 @@ from knox import views as knox_views from .views import ( + ConfirmAccountView, LoginWithCredentialsView, LoginWithTokenView, - PublicAccountDataView, RegisterAccountView, + RequestPasswordResetView, RequestVerificationTokenView, + ResendAccountConfirmationView, + ResetPasswordView, UpdateAccountView, VerifyAccountView, ) @@ -22,8 +25,6 @@ ), path("register", RegisterAccountView.as_view(), name="register"), path("update-account", UpdateAccountView.as_view(), name="update"), - path("account", PublicAccountDataView.as_view(), name="public-data"), - path("account/", PublicAccountDataView.as_view(), name="public-data"), path("logout", knox_views.LogoutView.as_view(), name="logout"), path("logoutall", knox_views.LogoutAllView.as_view(), name="logoutall"), path( @@ -32,4 +33,8 @@ name="request-verification-token", ), path("verify-account", VerifyAccountView.as_view(), name="verify-account"), + path("resend-account-confirmation", ResendAccountConfirmationView.as_view(), name="resend-account-confirmation"), + path("confirm-account/", ConfirmAccountView.as_view(), name="confirm"), + path("reset-password/", ResetPasswordView.as_view(), name="reset-password-token"), + path("reset-password/", RequestPasswordResetView.as_view(), name="reset-password"), ] diff --git a/src/accounts/api/views.py b/src/accounts/api/views.py index 7506f45..4c11217 100644 --- a/src/accounts/api/views.py +++ b/src/accounts/api/views.py @@ -1,49 +1,64 @@ +import logging + +from urllib.parse import urljoin from uuid import uuid4 +from commons.error_response import ErrorResponse +from commons.mail_wrapper import send_email_with_template +from django.conf import settings from django.core.exceptions import ObjectDoesNotExist, PermissionDenied +from drf_spectacular.utils import extend_schema from knox.models import AuthToken from knox.views import LoginView as KnoxLoginView from rest_framework import status -from rest_framework.generics import GenericAPIView, RetrieveAPIView +from rest_framework.generics import GenericAPIView from rest_framework.permissions import AllowAny from rest_framework.response import Response from rest_framework.serializers import ValidationError +from rest_framework.views import APIView from ..exceptions import MissingMailConfirmationError from ..models import Account from .serializers import ( + ConfirmAccountSerializer, LoginWithCredentialsSerializer, + PasswordResetRequestModel, PublicAccountDataSerializer, RegisterAccountSerializer, + ResendAccountSerializer, + ResetPasswordRequestSerializer, + ResetPasswordSerializer, UpdateAccountSerializer, VerifyAccountSerializer, ) - -class PublicAccountDataView(RetrieveAPIView): - permission_classes = (AllowAny,) - queryset = Account.objects.all() - serializer_class = PublicAccountDataSerializer +logger = logging.getLogger(__name__) class LoginWithTokenView(KnoxLoginView): + """ + Login by providing a token in the header of the request, Example: 'Authorization: Token '. + + **Public endpoint** + """ + permission_classes = (AllowAny,) + serializer_class = None def post(self, request, format=None): if request.auth is None: - return Response( - {"detail": "Invalid token."}, - status=status.HTTP_401_UNAUTHORIZED, - ) + return ErrorResponse("Invalid token", status.HTTP_401_UNAUTHORIZED) return super().post(request, format=None) def get_post_response_data(self, request, token, instance): + user: Account = request.user + try: - if not request.user.is_active: + if not user.is_confirmed: raise MissingMailConfirmationError() except MissingMailConfirmationError as e: - return {"error": e.detail} + return ErrorResponse(str(e), MissingMailConfirmationError.status_code) serializer = self.get_user_serializer_class() @@ -57,27 +72,30 @@ def get_user_serializer_class(self): class LoginWithCredentialsView(GenericAPIView): + """ + Login by providing email and password. + + **Public endpoint** + """ + permission_classes = (AllowAny,) serializer_class = LoginWithCredentialsSerializer def post(self, request): serializer = self.serializer_class(data=request.data) + try: serializer.is_valid(raise_exception=True) account = Account.objects.get(email=serializer.data["email"]) if not account.is_active: raise MissingMailConfirmationError() except ObjectDoesNotExist: - return Response( - data={"error": "account doesn't exist!"}, - status=status.HTTP_404_NOT_FOUND, - ) + return ErrorResponse("Account does not exist.", status.HTTP_404_NOT_FOUND) except MissingMailConfirmationError as e: - return Response(data={"error": e.detail}, status=e.status_code) + return ErrorResponse(str(e), status.HTTP_400_BAD_REQUEST) except ValidationError as e: - return Response(data={"error": e.detail}, status=e.status_code) - except Exception as e: - return Response(data={"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + return ErrorResponse(str(e), status.HTTP_400_BAD_REQUEST) + account = serializer.validated_data return Response( @@ -90,6 +108,12 @@ def post(self, request): class RegisterAccountView(GenericAPIView): + """ + Register a new account. + + **Public endpoint** + """ + permission_classes = (AllowAny,) serializer_class = RegisterAccountSerializer @@ -99,8 +123,7 @@ def post(self, request): serializer.is_valid(raise_exception=True) except ValidationError as e: return Response(data={"error": str(e)}, status=e.status_code) - except Exception as e: - return Response(data={"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + account = serializer.save() return Response( @@ -113,6 +136,12 @@ def post(self, request): class UpdateAccountView(GenericAPIView): + """ + Update your account data. + + **Requires Token authentication** + """ + serializer_class = UpdateAccountSerializer def post(self, request): @@ -121,38 +150,39 @@ def post(self, request): if request.user != account: raise PermissionDenied except ObjectDoesNotExist: - return Response({"error": "Account does not exist."}, status=status.HTTP_404_NOT_FOUND) + return ErrorResponse("Account does not exist.", status.HTTP_404_NOT_FOUND) except PermissionDenied: - return Response( - {"error": "You have no permission to do this action."}, - status=status.HTTP_403_FORBIDDEN, - ) + return ErrorResponse("You have no permission to do this action.", status.HTTP_403_FORBIDDEN) serializer = self.get_serializer(account, data=request.data) try: serializer.is_valid(raise_exception=True) except ValidationError as e: - return Response(data={"error": str(e)}, status=e.status_code) - except Exception as e: - return Response(data={"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + return ErrorResponse(str(e), e.status_code) + serializer.save() return Response(serializer.data, status=status.HTTP_200_OK) -class RequestVerificationTokenView(GenericAPIView): - def get(self, *args, **kwargs): +class RequestVerificationTokenView(APIView): + """ + Request a new verification token to verify your account in-game. + + **Requires Token authentication** + """ + + def post(self, *args, **kwargs): verification_token = uuid4() + try: account = Account.objects.get(pk=self.request.user.pk) if self.request.user != account: raise PermissionDenied except ObjectDoesNotExist: - return Response({"error": "Account does not exist."}, status=status.HTTP_404_NOT_FOUND) + return ErrorResponse("Account does not exist.", status.HTTP_404_NOT_FOUND) except PermissionDenied: - return Response( - {"error": "You have no permission to do this action."}, - status=status.HTTP_403_FORBIDDEN, - ) + return ErrorResponse("You have no permission to do this action.", status.HTTP_403_FORBIDDEN) + account.verification_token = verification_token account.save() return Response( @@ -165,19 +195,153 @@ def get(self, *args, **kwargs): class VerifyAccountView(GenericAPIView): + """ + Given a verification token, verify the account. + + **Public endpoint** + """ + permission_classes = (AllowAny,) serializer_class = VerifyAccountSerializer def post(self, request): serializer = self.get_serializer(data=request.data) + try: serializer.is_valid(raise_exception=True) except ValidationError as e: - return Response(data={"error": str(e)}, status=e.status_code) - except Exception as e: - return Response(data={"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + return ErrorResponse(str(e), e.status_code) account = Account.objects.get(unique_identifier=serializer.data["unique_identifier"]) public_data = PublicAccountDataSerializer(account).data return Response(public_data, status=status.HTTP_200_OK) + + +@extend_schema(operation_id="accounts_reset_password__create") +class ResetPasswordView(GenericAPIView): + """ + Given a reset token and new password, reset the account's password. + + **Public endpoint** + """ + + permission_classes = (AllowAny,) + serializer_class = ResetPasswordSerializer + + def post(self, request, reset_token): + serializer = self.serializer_class(data=request.data) + + try: + serializer.is_valid(raise_exception=True) + reset_request = PasswordResetRequestModel.objects.get(token=reset_token) + if not reset_request.is_token_valid(): + raise PasswordResetRequestModel.DoesNotExist + except ValidationError as e: + return ErrorResponse(str(e), e.status_code) + except PasswordResetRequestModel.DoesNotExist: + return ErrorResponse("Invalid link or expired.", status.HTTP_400_BAD_REQUEST) + + account = reset_request.account + account.set_password(serializer.validated_data["password"]) + account.save() + reset_request.delete() + return Response(status=status.HTTP_200_OK) + + +class RequestPasswordResetView(GenericAPIView): + """ + Request a password reset link for a given mail. + + **Public endpoint** + """ + + permission_classes = (AllowAny,) + serializer_class = ResetPasswordRequestSerializer + + def post(self, request): + serializer = self.serializer_class(data=request.data) + + try: + serializer.is_valid(raise_exception=True) + except ValidationError: + logger.warning( + "Attempted to reset password for non-existing account: %s", serializer.validated_data["email"] + ) + return Response(status=status.HTTP_200_OK) + + serializer.save() + link = urljoin(settings.PASS_RESET_URL, serializer.validated_data["token"]) + + send_email_with_template( + recipient=serializer.validated_data["account"].email, + subject="Reset your password", + template="password_reset.html", + context={"link": link}, + ) + + return Response(status=status.HTTP_200_OK) + + +class ConfirmAccountView(GenericAPIView): + """ + Given a confirmation token, confirm the account. + + **Public endpoint** + """ + + permission_classes = (AllowAny,) + serializer_class = ConfirmAccountSerializer + + def post(self, request, confirm_token): + serializer = self.serializer_class(data={"token": confirm_token}) + print(serializer) + + try: + serializer.is_valid(raise_exception=True) + except ValidationError as e: + return ErrorResponse(str(e), e.status_code) + + account_id = serializer.validated_data.account.unique_identifier + + try: + account = Account.objects.get(unique_identifier=account_id) + except Account.DoesNotExist: + return ErrorResponse("Account for this confirmation link does not exist.", status.HTTP_400_BAD_REQUEST) + + account.is_active = True + account.is_confirmed = True + account.save() + serializer.validated_data.delete() + + return Response(status=status.HTTP_200_OK) + + +class ResendAccountConfirmationView(GenericAPIView): + """ + Resend the confirmation mail for a given account. + + **Public endpoint** + """ + + permission_classes = (AllowAny,) + serializer_class = ResendAccountSerializer + + def post(self, request): + serializer = self.serializer_class(data=request.data) + + try: + serializer.is_valid(raise_exception=True) + except ValidationError as e: + return ErrorResponse(str(e), e.status_code) + except Account.DoesNotExist: + logger.warning( + "Attempted to resend account confirmation for non-existing account: %s", + serializer.validated_data["email"], + ) + return Response(status=status.HTTP_200_OK) + + account = serializer.validated_data + account.send_confirmation_mail() + + return Response(status=status.HTTP_200_OK) diff --git a/src/accounts/apps.py b/src/accounts/apps.py index 0cb51e6..f86a17a 100644 --- a/src/accounts/apps.py +++ b/src/accounts/apps.py @@ -4,3 +4,6 @@ class AccountsConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "accounts" + + def ready(self): + import commons.schema # noqa: F401 diff --git a/src/accounts/exceptions.py b/src/accounts/exceptions.py index 120dca0..056d818 100644 --- a/src/accounts/exceptions.py +++ b/src/accounts/exceptions.py @@ -6,3 +6,6 @@ class MissingMailConfirmationError(Exception): detail = "You must confirm your email before attempting to login." status_code = status.HTTP_418_IM_A_TEAPOT + + def __str__(self): + return self.detail diff --git a/src/accounts/migrations/0002_passwordresetrequestmodel.py b/src/accounts/migrations/0002_passwordresetrequestmodel.py new file mode 100644 index 0000000..86e668e --- /dev/null +++ b/src/accounts/migrations/0002_passwordresetrequestmodel.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.23 on 2024-01-23 23:29 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='PasswordResetRequestModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('token', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/src/accounts/migrations/0003_auto_20240124_1626.py b/src/accounts/migrations/0003_auto_20240124_1626.py new file mode 100644 index 0000000..0f9c6ca --- /dev/null +++ b/src/accounts/migrations/0003_auto_20240124_1626.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2.23 on 2024-01-24 16:26 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0002_passwordresetrequestmodel'), + ] + + operations = [ + migrations.AddField( + model_name='account', + name='is_confirmed', + field=models.BooleanField(default=False, help_text='Has this account been confirmed via email?', verbose_name='Confirmed'), + ), + migrations.CreateModel( + name='AccountConfirmation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('token', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/src/accounts/models.py b/src/accounts/models.py index 3ec431b..148de6e 100644 --- a/src/accounts/models.py +++ b/src/accounts/models.py @@ -1,7 +1,13 @@ +from datetime import timedelta +from secrets import token_urlsafe +from urllib.parse import urljoin from uuid import uuid4 +from commons.mail_wrapper import send_email_with_template +from django.conf import settings from django.contrib.auth.models import AbstractUser from django.db import models +from django.utils import timezone from .validators import AccountNameValidator, UsernameValidator @@ -35,6 +41,12 @@ class Account(AbstractUser): ), ) + is_confirmed = models.BooleanField( + default=False, + verbose_name="Confirmed", + help_text="Has this account been confirmed via email?", + ) + is_verified = models.BooleanField( default=False, verbose_name="Verified", @@ -69,3 +81,52 @@ def save(self, *args, **kwargs): def __str__(self): return f"{self.unique_identifier} as {self.username}" + + def send_confirmation_mail(self): + confirmation_token = token_urlsafe(32) + + previous_confirmations = AccountConfirmation.objects.filter(account=self) + previous_confirmations.delete() + + AccountConfirmation.objects.create( + token=confirmation_token, + account=self, + ) + + send_email_with_template( + recipient=self.email, + subject="Confirm your account", + template="confirm_template.html", + context={ + "user_name": self.username, + "link": urljoin(settings.ACCOUNT_CONFIRMATION_URL, confirmation_token), + }, + ) + + +class AccountConfirmation(models.Model): + token = models.TextField() + account = models.ForeignKey(Account, on_delete=models.CASCADE) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"Account confirmation request for {self.account} created at {self.created_at}" + + def is_token_valid(self): + if self.created_at is None: + return False + return (self.created_at + timedelta(minutes=settings.ACCOUNT_CONFIRMATION_TOKEN_TTL)) > timezone.now() + + +class PasswordResetRequestModel(models.Model): + token = models.TextField() + account = models.ForeignKey(Account, on_delete=models.CASCADE) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"Password reset request for {self.account} created at {self.created_at}" + + def is_token_valid(self): + if self.created_at is None: + return False + return (self.created_at + timedelta(minutes=settings.PASS_RESET_TOKEN_TTL)) > timezone.now() diff --git a/src/central_command/settings.py b/src/central_command/settings.py index 4d5a060..edada5a 100644 --- a/src/central_command/settings.py +++ b/src/central_command/settings.py @@ -14,11 +14,18 @@ from datetime import timedelta from pathlib import Path +from urllib.parse import urljoin + +from dotenv import load_dotenv + +# dotenv needs to be loaded again (after manage.py) because of mypy plugin which runs settings.py separately +# see https://github.com/typeddjango/django-stubs/issues/458 +load_dotenv() + # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent - # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ @@ -30,9 +37,6 @@ ALLOWED_HOSTS = ["*"] if DEBUG else ["localhost", "127.0.0.1"] - -# Application definition - INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", @@ -41,11 +45,12 @@ "django.contrib.messages", "whitenoise.runserver_nostatic", "django.contrib.staticfiles", - "django_email_verification", "rest_framework", "knox", + "post_office", "accounts", "persistence", + "drf_spectacular", ] # What user model to use for authentication? @@ -83,7 +88,6 @@ WSGI_APPLICATION = "central_command.wsgi.application" - # Database # https://docs.djangoproject.com/en/3.1/ref/settings/#databases @@ -99,10 +103,28 @@ } REST_FRAMEWORK = { + "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", "DEFAULT_AUTHENTICATION_CLASSES": ["knox.auth.TokenAuthentication"], "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"], "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", "PAGE_SIZE": 10, + "EXCEPTION_HANDLER": "commons.error_response.custom_exception_handler", +} + +SPECTACULAR_SETTINGS = { + "TITLE": "Central Command", + "DESCRIPTION": """ +The all-in-one backend application for Unitystation + +Features +* Account management and user validation. +* Server list management. +* In-game persistence. +* Works cross-fork! +* Modular architecture. + """, + "VERSION": None, + "SERVE_INCLUDE_SCHEMA": False, } # Token expiration @@ -113,20 +135,16 @@ EMAIL_HOST = os.environ.get("EMAIL_HOST") EMAIL_PORT = int(os.environ.get("EMAIL_PORT", 1337)) EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER") +DEFAULT_FROM_EMAI = EMAIL_HOST_USER EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD") -EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" +EMAIL_BACKEND = "post_office.EmailBackend" REQUIRE_EMAIL_CONFIRMATION = True -# Email confirmation settings -EMAIL_ACTIVE_FIELD = "is_active" -EMAIL_SERVER = os.environ.get("EMAIL_HOST") -EMAIL_ADDRESS = os.environ.get("EMAIL_HOST_USER") -EMAIL_FROM_ADDRESS = os.environ.get("EMAIL_HOST_USER") -EMAIL_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD") -EMAIL_MAIL_SUBJECT = "Confirm your Unitystation account" -EMAIL_MAIL_HTML = "registration/confirmation_email.html" -EMAIL_PAGE_TEMPLATE = "confirm_template.html" -EMAIL_PAGE_DOMAIN = os.environ.get("EMAIL_PAGE_DOMAIN") +POST_OFFICE = { + "BACKENDS": { + "default": "django.core.mail.backends.smtp.EmailBackend", + } +} # Password validation # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators @@ -146,7 +164,6 @@ }, ] - # Internationalization # https://docs.djangoproject.com/en/3.1/topics/i18n/ @@ -160,7 +177,6 @@ USE_TZ = True - # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.1/howto/static-files/ @@ -176,3 +192,14 @@ # Whitenoise statics compression and caching STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" + +# Website configuration +WEBSITE_URL = os.environ["WEBSITE_URL"] + +PASS_RESET_URL_SUFFIX = os.environ["PASS_RESET_URL_SUFFIX"] +PASS_RESET_URL = urljoin(WEBSITE_URL, PASS_RESET_URL_SUFFIX) +PASS_RESET_TOKEN_TTL = 60 # minutes + +ACCOUNT_CONFIRMATION_URL_SUFFIX = os.environ["ACCOUNT_CONFIRMATION_URL_SUFFIX"] +ACCOUNT_CONFIRMATION_URL = urljoin(WEBSITE_URL, ACCOUNT_CONFIRMATION_URL_SUFFIX) +ACCOUNT_CONFIRMATION_TOKEN_TTL = 24 # hours diff --git a/src/central_command/urls.py b/src/central_command/urls.py index 3183dd0..454b44f 100644 --- a/src/central_command/urls.py +++ b/src/central_command/urls.py @@ -13,13 +13,15 @@ 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ + from django.contrib import admin from django.urls import include, path -from django_email_verification import urls as mail_urls +from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView urlpatterns = [ + path("", SpectacularRedocView.as_view(url_name="schema"), name="redoc"), + path("schema/", SpectacularAPIView.as_view(), name="schema"), path("admin/", admin.site.urls), - path("email/", include(mail_urls)), # API REST FRAMEWORK path("accounts/", include("accounts.api.urls", "Accounts API")), path("persistence/", include("persistence.api.urls")), diff --git a/src/commons/__init__.py b/src/commons/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/commons/error_response.py b/src/commons/error_response.py new file mode 100644 index 0000000..268763e --- /dev/null +++ b/src/commons/error_response.py @@ -0,0 +1,28 @@ +from logging import getLogger + +from rest_framework import status +from rest_framework.response import Response +from rest_framework.views import exception_handler + +logger = getLogger(__name__) + + +class ErrorResponse(Response): + def __init__(self, message, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR): + super().__init__({"error": message}, status=status_code) + + +def custom_exception_handler(exc, context): + """ + This function handles all exceptions that are not handled by DRF's default exception handler. + """ + response = exception_handler(exc, context) + + if response is not None: + response.data["status_code"] = response.status_code + else: + logger.error("An unhandled error occurred: %s", exc) + logger.error("Context: %s", context) + return ErrorResponse(f"An unhandled error occurred: {exc}") + + return response diff --git a/src/commons/mail_wrapper.py b/src/commons/mail_wrapper.py new file mode 100644 index 0000000..9b21102 --- /dev/null +++ b/src/commons/mail_wrapper.py @@ -0,0 +1,14 @@ +from django.conf import settings +from django.template.loader import render_to_string +from post_office import mail + + +def send_email_with_template(recipient: str, subject: str, template: str, context: dict) -> None: + email_subject = f"Unitystation: {subject}" + email_body = render_to_string(template, context) + mail.send( + recipients=[recipient], + subject=email_subject, + html_message=email_body, + sender=settings.EMAIL_HOST_USER, + ) diff --git a/src/commons/schema.py b/src/commons/schema.py new file mode 100644 index 0000000..86072c7 --- /dev/null +++ b/src/commons/schema.py @@ -0,0 +1,13 @@ +from drf_spectacular.extensions import OpenApiAuthenticationExtension +from drf_spectacular.plumbing import build_bearer_security_scheme_object + + +class KnoxTokenScheme(OpenApiAuthenticationExtension): + target_class = "knox.auth.TokenAuthentication" + name = "knoxApiToken" + + def get_security_definition(self, auto_schema): + return build_bearer_security_scheme_object( + header_name="Authorization", + token_prefix=self.target.authenticate_header(""), + ) diff --git a/src/entrypoint.sh b/src/entrypoint.sh index 8656db8..7280f74 100755 --- a/src/entrypoint.sh +++ b/src/entrypoint.sh @@ -21,6 +21,9 @@ then echo "PostgreSQL started" fi +# start cron +crond -l 0 -d 0 -L /home/website/logs/cron.log + # flush all data in the database #python manage.py flush --no-input diff --git a/src/persistence/admin.py b/src/persistence/admin.py index e948d1c..10ae692 100644 --- a/src/persistence/admin.py +++ b/src/persistence/admin.py @@ -1,16 +1,6 @@ from django.contrib import admin -from .models import Character, Other, PolyPhrase - - -@admin.register(Other) -class OtherAdminView(admin.ModelAdmin): - pass - - -@admin.register(PolyPhrase) -class PolyPhraseAdminView(admin.ModelAdmin): - pass +from .models import Character @admin.register(Character) diff --git a/src/persistence/api/serializers.py b/src/persistence/api/serializers.py index a4c4207..e668320 100644 --- a/src/persistence/api/serializers.py +++ b/src/persistence/api/serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from ..models import Character, Other, PolyPhrase +from ..models import Character class CompatibleCharactersRequestSerializer(serializers.Serializer): @@ -22,18 +22,3 @@ class Meta: fields = ("id", "account", "fork_compatibility", "character_sheet_version", "data", "last_updated") read_only_fields = ("id", "account", "last_updated") - - -class OtherSerializer(serializers.ModelSerializer): - class Meta: - model = Other - fields = ( - "account", - "other_data", - ) - - -class PolyPhraseSerializer(serializers.ModelSerializer): - class Meta: - model = PolyPhrase - fields = ("id", "said_by", "phrase") diff --git a/src/persistence/api/urls.py b/src/persistence/api/urls.py index f0b30fc..b492d4a 100644 --- a/src/persistence/api/urls.py +++ b/src/persistence/api/urls.py @@ -6,11 +6,7 @@ GetAllCharactersByAccountView, GetCharacterByIdView, GetCompatibleCharacters, - RandomPolyPhraseView, - ReadOtherDataView, UpdateCharacterView, - WriteOtherDataView, - WritePolyPhraseView, ) app_name = "persistence" @@ -22,8 +18,4 @@ path("characters/compatible", GetCompatibleCharacters.as_view(), name="characters-compatible"), path("characters//update", UpdateCharacterView.as_view(), name="characters-patch"), path("characters//delete", DeleteCharacterView.as_view(), name="characters-delete"), - path("other-data/read", ReadOtherDataView.as_view(), name="read"), - path("other-data/write", WriteOtherDataView.as_view(), name="write"), - path("poly-says", RandomPolyPhraseView.as_view(), name="poly-says"), - path("poly-hears", WritePolyPhraseView.as_view(), name="poly-hears"), ] diff --git a/src/persistence/api/views.py b/src/persistence/api/views.py index f4cd392..2093fed 100644 --- a/src/persistence/api/views.py +++ b/src/persistence/api/views.py @@ -1,24 +1,28 @@ from django.core.exceptions import ObjectDoesNotExist, PermissionDenied -from rest_framework import permissions, status +from rest_framework import status from rest_framework.exceptions import ValidationError from rest_framework.generics import GenericAPIView, ListAPIView from rest_framework.response import Response -from ..models import Character, Other, PolyPhrase +from ..models import Character from .serializers import ( CharacterSerializer, CompatibleCharactersRequestSerializer, - OtherSerializer, - PolyPhraseSerializer, UpdateCharacterSerializer, ) class GetCharacterByIdView(GenericAPIView): + """ + Retrieves a character by its ID. The character must belong to the account of the user. + + **Requires Token Authentication.** + """ + serializer_class = CharacterSerializer def get_queryset(self): - return Character.objects.filter(account__unique_identifier=self.request.user.unique_identifier) + return Character.objects.filter(account__unique_identifier=self.request.user.unique_identifier) # type: ignore def get(self, request, pk): try: @@ -34,6 +38,12 @@ def get(self, request, pk): class GetCompatibleCharacters(ListAPIView): + """ + Retrieves a list of compatible characters for the user's account. + + **Requires Token Authentication.** + """ + serializer_class = CharacterSerializer def get_queryset(self): @@ -64,6 +74,12 @@ def get_queryset(self): class GetAllCharactersByAccountView(ListAPIView): + """ + Retrieves a list of all characters of an account, disregarding compatibility. + + **Requires Token Authentication.** + """ + serializer_class = CharacterSerializer def get_queryset(self): @@ -76,6 +92,12 @@ def get_queryset(self): class UpdateCharacterView(GenericAPIView): + """ + Updates a character by its ID. The character must belong to the account of the user. + + **Requires Token Authentication.** + """ + serializer_class = UpdateCharacterSerializer queryset = Character.objects.all() @@ -107,6 +129,12 @@ def put(self, request, pk): class DeleteCharacterView(GenericAPIView): + """ + Deletes a character by its ID. The character must belong to the account of the user. + + **Requires Token Authentication.** + """ + serializer_class = CharacterSerializer def delete(self, request, pk): @@ -128,6 +156,12 @@ def delete(self, request, pk): class CreateCharacterView(GenericAPIView): + """ + Creates a new character. + + **Requires Token Authentication.** + """ + serializer_class = CharacterSerializer def post(self, request): @@ -135,7 +169,7 @@ def post(self, request): data_with_account["account"] = request.user.pk serializer = self.serializer_class(data=data_with_account) - serializer.account = request.user + serializer.account = request.user # type: ignore try: serializer.is_valid(raise_exception=True) except ValidationError as e: @@ -146,94 +180,3 @@ def post(self, request): return Response(data, status=status.HTTP_403_FORBIDDEN) serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) - - -class ReadOtherDataView(GenericAPIView): - serializer_class = OtherSerializer - - def get(self, request): - try: - other = Other.objects.get(pk=request.user.pk) - except ObjectDoesNotExist: - data = {"error": "No data for this account could be found!"} - return Response(data, status=status.HTTP_404_NOT_FOUND) - except PermissionDenied: - data = {"error": "You do not have permission to view this data!"} - return Response(data, status=status.HTTP_403_FORBIDDEN) - serializer = self.serializer_class(other) - return Response(serializer.data, status=status.HTTP_200_OK) - - -class WriteOtherDataView(GenericAPIView): - serializer_class = OtherSerializer - - def post(self, request): - data = {"account": request.user.pk} - data["other_data"] = request.data.get("other_data") - - try: - if not request.user.is_authorized_server: - raise PermissionDenied - other = Other.objects.get(pk=request.user.pk) - except ObjectDoesNotExist: - serializer = self.serializer_class(data=data) - return self.try_write_to_record(serializer) - except PermissionDenied: - data = {"error": "You do not have permission to edit this data!"} - return Response(data, status=status.HTTP_403_FORBIDDEN) - else: - data = self.update_other_data_dict(data, other.other_data) - serializer = self.serializer_class(other, data=data) - return self.try_write_to_record(serializer) - - def update_other_data_dict(self, new_data: dict, old_data: dict) -> dict: - final_data = {"account": new_data["account"]} - for key, value in new_data["other_data"].items(): - old_data[key] = value - final_data["other_data"] = old_data - return final_data - - def try_write_to_record(self, serializer: OtherSerializer) -> Response: - try: - serializer.is_valid(raise_exception=True) - except ValidationError as e: - data = {"error": e.detail} - return Response(data, status=status.HTTP_400_BAD_REQUEST) - else: - serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) - - -class RandomPolyPhraseView(GenericAPIView): - permission_classes = (permissions.AllowAny,) - serializer_class = PolyPhraseSerializer - - def get(self, request): - try: - if PolyPhrase.objects.count() == 0: - raise ObjectDoesNotExist - phrase = PolyPhrase.objects.order_by("?").first() - except ObjectDoesNotExist: - data = {"error": "No phrases could be found!"} - return Response(data, status=status.HTTP_404_NOT_FOUND) - serializer = self.serializer_class(phrase) - return Response(serializer.data, status=status.HTTP_200_OK) - - -class WritePolyPhraseView(GenericAPIView): - serializer_class = PolyPhraseSerializer - - def post(self, request): - serializer = self.serializer_class(data=request.data) - try: - serializer.is_valid(raise_exception=True) - if not request.user.is_authorized_server: - raise PermissionDenied - except ValidationError as e: - data = {"error": str(e)} - return Response(data, status=status.HTTP_400_BAD_REQUEST) - except PermissionDenied: - data = {"error": "You do not have permission to write this data!"} - return Response(data, status=status.HTTP_403_FORBIDDEN) - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) diff --git a/src/persistence/apps.py b/src/persistence/apps.py index 8478caf..e5829d1 100644 --- a/src/persistence/apps.py +++ b/src/persistence/apps.py @@ -3,3 +3,6 @@ class PersistenceConfig(AppConfig): name = "persistence" + + def ready(self): + import commons.schema # noqa: F401 diff --git a/src/persistence/models.py b/src/persistence/models.py index 46d2052..ef70ab3 100644 --- a/src/persistence/models.py +++ b/src/persistence/models.py @@ -33,26 +33,3 @@ class Character(models.Model): def __str__(self): return f"{self.account.unique_identifier}'s character" - - -class Other(models.Model): - account = models.OneToOneField("accounts.Account", on_delete=models.CASCADE, primary_key=True) - """To what account/server is this extra unordered persistent data related to?""" - - other_data = models.JSONField(default=dict) - """The extra unordered persistent data.""" - - def __str__(self): - return f"{self.account.pk}'s other data" - - -class PolyPhrase(models.Model): - said_by = models.CharField(max_length=28, blank=True, default="Who knows?") - """What account identifier said this phrase originally? Can be blank""" - - phrase = models.CharField(max_length=128) - - def __str__(self): - if self.said_by: - return f"{self.said_by}: {self.phrase}" - return f"{self.pk}: {self.phrase}" diff --git a/src/templates/confirm_template.html b/src/templates/confirm_template.html index df2dec1..24d129f 100644 --- a/src/templates/confirm_template.html +++ b/src/templates/confirm_template.html @@ -32,12 +32,11 @@
-

Welcome to Unitystation!

-

Hi there,

+

Welcome to Unitystation, {{ user_name }}!

+

Hello {{ user_name }},

We're excited to have you on board! To get started with your new account, please confirm your email address by clicking the button below:

Confirm my account

If you did not initiate this request, please disregard this email, or contact us for support if you feel this is an error.

-

Once confirmed, you'll have full access to all the features and excitement that Unitystation offers!

Need help? Join our community on Discord for support and interaction.

Thank you for joining us,

The Unitystation Team

diff --git a/src/templates/password_reset.html b/src/templates/password_reset.html new file mode 100644 index 0000000..9a99bb8 --- /dev/null +++ b/src/templates/password_reset.html @@ -0,0 +1,46 @@ + + + + Unitystation - Password Reset + + + +
+

Password Reset Request

+

Reset password here

+

If you did not initiate this request, please disregard this email, or contact us for support if you feel this is an error.

+

Need help? Join our community on Discord for support and interaction.

+

Thank you for playing unitystation,

+

The Unitystation Team

+
+ + + diff --git a/src/tests/test_unhandled_exceptions.py b/src/tests/test_unhandled_exceptions.py new file mode 100644 index 0000000..a29f9b3 --- /dev/null +++ b/src/tests/test_unhandled_exceptions.py @@ -0,0 +1,24 @@ +from django.test import RequestFactory, TestCase +from rest_framework import status +from rest_framework.permissions import AllowAny +from rest_framework.views import APIView + + +class ExceptionView(APIView): + permission_classes = (AllowAny,) + + def get(self, request): + raise Exception("This is a test exception") + + +class CustomExceptionHandlerTest(TestCase): + def setUp(self): + self.factory = RequestFactory() + + def test_custom_exception_handler(self): + request = self.factory.get("/fake-url") # The URL doesn't matter here + response = ExceptionView.as_view()(request) + + self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR) + self.assertEqual(response.data["error"], "An unhandled error occurred: This is a test exception") + self.assertIn("error", response.data)