diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..18217f5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +*.tar +*.png +*.md +.env +.git/* +.gitignore +*.pyc +*.sh +tests/ +.vscode/ +__pycache__/ +.pytest_cache +*.env +.venv \ No newline at end of file diff --git a/.github/workflows/docker-image-tag.yml b/.github/workflows/docker-image-tag.yml new file mode 100644 index 0000000..8e78b43 --- /dev/null +++ b/.github/workflows/docker-image-tag.yml @@ -0,0 +1,25 @@ +name: Docker Image CI - tags + +on: + push: + tags: [ "*" ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - + name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - + name: Build and push + uses: docker/build-push-action@v3 + with: + push: true + tags: moralcode/classclockapi:${{ github.ref_name }} diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 0000000..6a2eb84 --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,29 @@ +name: Docker Image CI + +on: + push: + branches: [ "main" ] +# tags: [ "main" ] + +jobs: + build: + runs-on: ubuntu-latest + steps: +# - +# name: Set up QEMU +# uses: docker/setup-qemu-action@v2 + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - + name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - + name: Build and push + uses: docker/build-push-action@v3 + with: + push: true + tags: moralcode/classclockapi:main diff --git a/.gitignore b/.gitignore index d773b90..ab7140e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,160 @@ -__pycache__ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + + + + /startserver.sh classclockapi_venv/ @@ -7,3 +162,4 @@ classclockapi_venv/ prodstart\.sh *.pyc +*.env \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..12e80af --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,51 @@ +## 0.3.3 +- add optional sentry monitoring +- add TRUSTED_PROXY_COUNT environment variable to allow the app to run behind a proxy +- increase rate limits +- improve handling of 429 errors + +## 0.3.2 +- Improvements to the admin role checks +- Fix an issue where the token for the auth0management endpoint for checking user roles would expire and cause requests to return less data than they should + +## 0.3.1 +- Improvements to logging +- Improved interactions with Auth0 management API by using higher level HTTP libraries and correcting the URL path for accessing the API +- updated role checking for protected endpoints to be a little more robust and more reliably parse the data from the management API + +Known issues: +- Will not start up if environment variables are quoted. + +## 0.3.0 +- Introduce database migrations using flask-migrate. +- Introduce crude soft-deletion +- Add a new endpoint at `/bellschedules` for logged-in admins to list the schedules for every school that they are the owner of +- disable strict slashes on all endpoints (`.../endpoint/` and `.../endpoint` should now act the same.) +- Updates to CORS settings +- Improvements to generated API documententation +- update dependencies and connection string settings to fix some crashing on startup + + +### Updating to 0.3.0 +The recommended/easiest way to update databases created prior to 0.3.0 is to create a new database. However, this may not be possible in all cases. + +Alternatively, databases created prior to 0.3.0 should be upgraded as follows: +1. perform a backup +2. determine what commit the database was created from +3. apply any database schema changes (as indicated by changes to the `db_schema.py` file) to get your database up to the same schema as version 0.2.0 +4. run the following command against your database: `flask db stamp 93d6649210e1` to set up the versioning table +5. run the migrations using `flask db upgrade`. This will update your database schema so that it contains the latest changes + +## 0.2.0 +- improved logging +- dependency updates +- begin using sqlalchemy ORM for managing DB queries +- add a mehcanism for populating the database with demo data +- Switch from JSONAPI to a more regular JSON output format +- Create an automatically-generated documentation page with swagger +- add a basic HTML homepage to the API's main URL for any visitors that come looking +- add beta and staging site URLs to CORS origins +- add a /ping endpoint to allow the frontend to check connectivity +- Dockerize the app +## 0.1.0 +Initial tagged version \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..1129ae8 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,9 @@ +# Contributing to the ClassClock API + + +Here are some things that may be helpful for maintainers or contributors + + +## making changes to the schema +If you make changes to the DB schema, generate a new migration to allow existing users to upgrade their databases. This can be done with the command `FLASK_APP=api.py pipenv run flask db migrate -m ""`. Use a short, descriptive message to describe what was changed. TO upgrade, run `FLASK_APP=api.py pipenv run flask db upgrade` to update your local db to the new schema. Don't forget to also document the changes in the changelog you made to the app and which app versions are compatible with which DB versions. + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0cddf9c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.8-slim-buster + +WORKDIR /classclock-api + +RUN pip install pipenv + + +COPY Pipfile Pipfile.lock /classclock-api/ + +RUN pipenv install + +COPY . /classclock-api/ + +ENTRYPOINT pipenv run gunicorn --workers 1 --bind 0.0.0.0 api:app diff --git a/Pipfile b/Pipfile index ba03989..aa3f98b 100644 --- a/Pipfile +++ b/Pipfile @@ -4,22 +4,30 @@ url = "https://pypi.org/simple" verify_ssl = true [dev-packages] +flask-migrate = "*" [packages] six = "~=1.12.0" flasgger = "~=0.9.3" flask-marshmallow = "~=0.10.1" -Flask = "~=1.1.1" +Flask = "~=2.0.0" Flask-Cors = "~=3.0.8" -Flask-Limiter = "~=1.0.1" -gunicorn = "~=19.9.0" +Flask-Limiter = "~=3.2.0" +gunicorn = "~=22.0.0" +werkzeug = "~=2.0.0" sqlalchemy = "~=1.3.13" -flask-sqlalchemy = "~=2.4.1" +flask-sqlalchemy = "~=2.5.0" marshmallow-sqlalchemy = "~=0.23" apispec = "~=3.3.0" apispec-webframeworks = "~=0.5.2" mysql-connector-python = "8.0.24" python-jose = "*" +python-dotenv = "*" +flask-migrate = "~=3.0.0" +requests = "*" +sentry-sdk = "*" +psycopg2-binary = "*" +ordered-set = "*" [requires] python_version = "3.8" diff --git a/Pipfile.lock b/Pipfile.lock index dc27fa4..669102b 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "6f00ba8a80f0a867eee207baf88da038679febf4545c4d47888187769a5766ed" + "sha256": "f2e8690265236431a4831c0145e3a83db4636b070b8373468c1bef2ca5a3d4f3" }, "pipfile-spec": 6, "requires": { @@ -16,10 +16,15 @@ ] }, "default": { - "apispec": { - "extras": [ - "yaml" + "alembic": { + "hashes": [ + "sha256:1acdd7a3a478e208b0503cd73614d5e4c6efafa4e73518bb60e4f2846a37b1c5", + "sha256:496e888245a53adf1498fcab31713a469c65836f8de76e01399aa1c3e90dd213" ], + "markers": "python_version >= '3.8'", + "version": "==1.14.1" + }, + "apispec": { "hashes": [ "sha256:a1df9ec6b2cd0edf45039ef025abd7f0660808fa2edf737d3ba1cf5ef1a4625b", "sha256:d23ebd5b71e541e031b02a19db10b5e6d5ef8452c552833e3e1afc836b40b1ad" @@ -37,43 +42,143 @@ }, "attrs": { "hashes": [ - "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", - "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" + "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", + "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b" + ], + "markers": "python_version >= '3.8'", + "version": "==25.3.0" + }, + "certifi": { + "hashes": [ + "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", + "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5" + ], + "markers": "python_version >= '3.7'", + "version": "==2025.8.3" + }, + "charset-normalizer": { + "hashes": [ + "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", + "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", + "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", + "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", + "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", + "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", + "sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c", + "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", + "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", + "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", + "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432", + "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", + "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", + "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", + "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", + "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19", + "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", + "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e", + "sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4", + "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7", + "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312", + "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", + "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", + "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", + "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", + "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99", + "sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b", + "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", + "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", + "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", + "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", + "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", + "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", + "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc", + "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", + "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", + "sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a", + "sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40", + "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", + "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", + "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", + "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", + "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05", + "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", + "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", + "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", + "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", + "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34", + "sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9", + "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", + "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", + "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", + "sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b", + "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", + "sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942", + "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", + "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", + "sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b", + "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", + "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", + "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", + "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", + "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", + "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", + "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", + "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", + "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", + "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", + "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca", + "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", + "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", + "sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb", + "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", + "sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557", + "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", + "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7", + "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", + "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", + "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==21.2.0" + "markers": "python_version >= '3.7'", + "version": "==3.4.3" }, "click": { "hashes": [ - "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", - "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" + "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", + "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a" + ], + "markers": "python_version >= '3.7'", + "version": "==8.1.8" + }, + "deprecated": { + "hashes": [ + "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", + "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==7.1.2" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.2.18" }, "ecdsa": { "hashes": [ - "sha256:64c613005f13efec6541bb0a33290d0d03c27abab5f15fbab20fb0ee162bdd8e", - "sha256:e108a5fe92c67639abae3260e43561af914e7fd0d27bae6d2ec1312ae7934dfe" + "sha256:30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3", + "sha256:478cba7b62555866fcb3bb3fe985e06decbdb68ef55713c4e5ab98c57d508e61" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.14.1" + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==0.19.1" }, "flasgger": { "hashes": [ - "sha256:0603941cf4003626b4ee551ca87331f1d17b8eecce500ccf1a1f1d3a332fc94a", - "sha256:6ebea406b5beecd77e8da42550f380d4d05a6107bc90b69ce9e77aee7612e2d0" + "sha256:ca098e10bfbb12f047acc6299cc70a33851943a746e550d86e65e60d4df245fb" ], "index": "pypi", - "version": "==0.9.5" + "version": "==0.9.7.1" }, "flask": { "hashes": [ - "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060", - "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557" + "sha256:59da8a3170004800a2837844bfa84d49b022550616070f7cb1a659682b2e7c9f", + "sha256:e1120c228ca2f553b470df4a5fa927ab66258467526069981b3eb0a91902687d" ], "index": "pypi", - "version": "==1.1.2" + "version": "==2.0.3" }, "flask-cors": { "hashes": [ @@ -85,11 +190,11 @@ }, "flask-limiter": { "hashes": [ - "sha256:473aa5bc97310406aa8c12ab3dc080697bcfa8cd21a6d0aba30916911bbc673c", - "sha256:8cce98dcf25bf2ddbb824c2b503b4fc8e1a139154240fd2c60d9306bad8a0db8" + "sha256:919b4af05b7d4162a699bfdc24357feeec876b70b1916150c3b4337ad711a995", + "sha256:e56140f2d48a3ee7d9d6e49bb81015e30cd1e99dc9e44adebb0c1127b89b3dc8" ], "index": "pypi", - "version": "==1.0.1" + "version": "==3.2.0" }, "flask-marshmallow": { "hashes": [ @@ -99,180 +204,337 @@ "index": "pypi", "version": "==0.10.1" }, + "flask-migrate": { + "hashes": [ + "sha256:4d42e8f861d78cb6e9319afcba5bf76062e5efd7784184dd2a1cccd9de34a702", + "sha256:df9043d2050df3c0e0f6313f6b529b62c837b6033c20335e9d0b4acdf2c40e23" + ], + "index": "pypi", + "version": "==3.0.1" + }, "flask-sqlalchemy": { "hashes": [ - "sha256:05b31d2034dd3f2a685cbbae4cfc4ed906b2a733cff7964ada450fd5e462b84e", - "sha256:bfc7150eaf809b1c283879302f04c42791136060c6eeb12c0c6674fb1291fae5" + "sha256:2bda44b43e7cacb15d4e05ff3cc1f8bc97936cc464623424102bfc2c35e95912", + "sha256:f12c3d4cc5cc7fdcc148b9527ea05671718c3ea45d50c7e732cceb33f574b390" ], "index": "pypi", - "version": "==2.4.4" + "version": "==2.5.1" }, "gunicorn": { "hashes": [ - "sha256:aa8e0b40b4157b36a5df5e599f45c9c76d6af43845ba3b3b0efe2c70473c2471", - "sha256:fa2662097c66f920f53f70621c6c58ca4a3c4d3434205e608e121b5b3b71f4f3" + "sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9", + "sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63" ], "index": "pypi", - "version": "==19.9.0" + "version": "==22.0.0" + }, + "idna": { + "hashes": [ + "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", + "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" + ], + "markers": "python_version >= '3.6'", + "version": "==3.10" + }, + "importlib-metadata": { + "hashes": [ + "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", + "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7" + ], + "markers": "python_version < '3.9'", + "version": "==8.5.0" + }, + "importlib-resources": { + "hashes": [ + "sha256:980862a1d16c9e147a59603677fa2aa5fd82b87f223b6cb870695bcfce830065", + "sha256:ac29d5f956f01d5e4bb63102a5a19957f1b9175e45649977264a1416783bb717" + ], + "markers": "python_version < '3.9'", + "version": "==6.4.5" }, "itsdangerous": { "hashes": [ - "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", - "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" + "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", + "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.1.0" + "markers": "python_version >= '3.8'", + "version": "==2.2.0" }, "jinja2": { "hashes": [ - "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", - "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6" + "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", + "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==2.11.3" + "markers": "python_version >= '3.7'", + "version": "==3.1.6" }, "jsonschema": { "hashes": [ - "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163", - "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a" + "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", + "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566" ], - "version": "==3.2.0" + "markers": "python_version >= '3.8'", + "version": "==4.23.0" + }, + "jsonschema-specifications": { + "hashes": [ + "sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc", + "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c" + ], + "markers": "python_version >= '3.8'", + "version": "==2023.12.1" }, "limits": { "hashes": [ - "sha256:0e5f8b10f18dd809eb2342f5046eb9aa5e4e69a0258567b5f4aa270647d438b3", - "sha256:f0c3319f032c4bfad68438ed1325c0fac86dac64582c7c25cddc87a0b658fa20" + "sha256:6571b0c567bfa175a35fed9f8a954c0c92f1c3200804282f1b8f1de4ad98a953", + "sha256:9767f7233da4255e9904b79908a728e8ec0984c0b086058b4cbbd309aea553f6" + ], + "markers": "python_version >= '3.8'", + "version": "==3.13.0" + }, + "mako": { + "hashes": [ + "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", + "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59" + ], + "markers": "python_version >= '3.8'", + "version": "==1.3.10" + }, + "markdown-it-py": { + "hashes": [ + "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", + "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb" ], - "version": "==1.5.1" + "markers": "python_version >= '3.8'", + "version": "==3.0.0" }, "markupsafe": { "hashes": [ - "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", - "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", - "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", - "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", - "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", - "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f", - "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39", - "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", - "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", - "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014", - "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f", - "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", - "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", - "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", - "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", - "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", - "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", - "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", - "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", - "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85", - "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1", - "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", - "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", - "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", - "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850", - "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0", - "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", - "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", - "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb", - "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", - "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", - "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", - "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1", - "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2", - "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", - "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", - "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", - "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7", - "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", - "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8", - "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", - "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193", - "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", - "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b", - "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", - "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", - "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5", - "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c", - "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032", - "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", - "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be", - "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621" + "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", + "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", + "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", + "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", + "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", + "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", + "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", + "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df", + "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", + "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", + "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", + "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", + "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", + "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371", + "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2", + "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", + "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52", + "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", + "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", + "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", + "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", + "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", + "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", + "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", + "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", + "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", + "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", + "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", + "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", + "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9", + "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", + "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", + "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", + "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", + "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", + "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", + "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a", + "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", + "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", + "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", + "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", + "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", + "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", + "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", + "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", + "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f", + "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50", + "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", + "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", + "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", + "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", + "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", + "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", + "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", + "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf", + "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", + "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", + "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", + "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", + "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.1.1" + "markers": "python_version >= '3.7'", + "version": "==2.1.5" }, "marshmallow": { "hashes": [ - "sha256:0dd42891a5ef288217ed6410917f3c6048f585f8692075a0052c24f9bfff9dfd", - "sha256:16e99cb7f630c0ef4d7d364ed0109ac194268dde123966076ab3dafb9ae3906b" + "sha256:4972f529104a220bb8637d595aa4c9762afbe7f7a77d82dc58c1615d70c5823e", + "sha256:71a2dce49ef901c3f97ed296ae5051135fd3febd2bf43afe0ae9a82143a494d9" ], - "markers": "python_version >= '3.5'", - "version": "==3.11.1" + "markers": "python_version >= '3.8'", + "version": "==3.22.0" }, "marshmallow-sqlalchemy": { "hashes": [ - "sha256:f1491f83833ac9c8406ba603458b1447fdfd904194833aab4b3cc01ef3646944", - "sha256:f861888ae3299f2c1f18cd94f02147ced70cd1b4986b2c5077e4a1036018d2a2" + "sha256:2ab0f1280c793e5aec81deab3e63ec23688ddfe05e5f38ac960368a1079520a1", + "sha256:c31b3bdf794de1d78c53e1c495502cbb3eeb06ed216869980c71d6159e7e9e66" ], "index": "pypi", - "version": "==0.25.0" + "version": "==0.28.2" + }, + "mdurl": { + "hashes": [ + "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", + "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" + ], + "markers": "python_version >= '3.7'", + "version": "==0.1.2" }, "mistune": { "hashes": [ - "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e", - "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4" + "sha256:93691da911e5d9d2e23bc54472892aff676df27a75274962ff9edc210364266d", + "sha256:b5a7f801d389f724ec702840c11d8fc48f2b33519102fc7ee739e8177b672164" ], - "version": "==0.8.4" + "markers": "python_version >= '3.8'", + "version": "==3.1.4" }, "mysql-connector-python": { "hashes": [ - "sha256:13acb968c2b9ed165a569d5144a6fe6bb2b3073385b43387a2568e97979157d1", - "sha256:1be5cbb864c43ab86618e8bff0a489c2393fb788b9b789ce357933c8d56a64a8", - "sha256:1cf8e667e16fa02c31ff83e8069f6e99d0a61f99e2a6b2ed364e48591d83fb52", - "sha256:23a327e20dd048ad421465f6f946127b316b9c5aeb9fb85a037703fe71ccf6de", - "sha256:2665ccdf682bdf12f09fa40bad96167014c3ff53250483fa19068dd465ad5821", - "sha256:2a6d672b5ec7860977c9c7c3ab55d57227157853b7dac035d04e5d869d41b45f", - "sha256:3aa9366e4ef69f5a4349719d4ec2a7c59dac584de7e01cec5bb1c34b557377a5", - "sha256:41b1c1992181dd2f2c3ee8f4f13230baa22cf2872d40ba94bc8790a07aa27dc9", - "sha256:528fbdf2f4ee700b38579aad6969939cce294aa9644a1ba8479aa2c169804074", - "sha256:5f5d49ffb563c6c0fe9b607cab02a7cf98814677f8d4a66e0acdfec521f91db2", - "sha256:644f3ec2da1b36acfba5f286b36422aad42760284d6fd9585c17a93e5c667663", - "sha256:73f0fb24ff5ffc49811efd6514ce48393ced8615d070dfefe2c4cddb9302a2e7", - "sha256:9bb84e8cee2668d4f202ed7298bb52c08b85e22aee9a05f2b2176cb0ca786cc3", - "sha256:b3d92d57f145a79cf8e57ab9ecdae8acf3c5c631a9483a3e52450b473e08a8dd", - "sha256:e2734f01b8416aec9d0e506889522ee2ea8666afee433f3595df1e9c57057cdf", - "sha256:e39a72bb3e9c2082be16396838dd905f2fa0a545898cd06c913d0519b58b5619", - "sha256:e46ea2b7df8e3a525b3121f831290fac32a94553ef91d56ed02557236dbd14c0" + "sha256:016d81bb1499dee8b77c82464244e98f10d3671ceefb4023adc559267d1fad50", + "sha256:052058cf3dc0bf183ab522132f3b18a614a26f3e392ae886efcdab38d4f4fc42", + "sha256:134b71e439e2eafaee4c550365221ae2890dd54fb76227c64a87a94a07fe79b4", + "sha256:2a8f451c4d700802fdfe515890c14974766c322213df2ceed3b27752929dc70f", + "sha256:2dcf05355315e5c7c81e9eca34395d78f29c4da3662e869e42dd7b16380f92ce", + "sha256:38c229d76cd1dea8465357855f2b2842b7a9b201f17dea13b0eab7d3b9d6ad74", + "sha256:67fc2b2e67a63963c633fc884f285a8de5a626967a3cc5f5d48ac3e8d15b122d", + "sha256:6d92c58f71c691f86ad35bb2f3e13d7a9cc1c84ce0b04c146e5980e450faeff1", + "sha256:72bfd0213364c2bea0244f6432ababb2f204cff43f4f886c65dca2be11f536ee", + "sha256:7af7f68198f2aca3a520e1201fe2b329331e0ca19a481f3b3451cb0746f56c01", + "sha256:823190e7f2a9b4bcc574ab6bb72a33802933e1a8c171594faad90162d2d27758", + "sha256:853c5916d188ef2c357a474e15ac81cafae6085e599ceb9b2b0bcb9104118e63", + "sha256:8a404db37864acca43fd76222d1fbc7ff8d17d4ce02d803289c2141c2693ce9e", + "sha256:9199d6ecc81576602990178f0c2fb71737c53a598c8a2f51e1097a53fcfaee40", + "sha256:933c3e39d30cc6f9ff636d27d18aa3f1341b23d803ade4b57a76f91c26d14066", + "sha256:a48534b881c176557ddc78527c8c75b4c9402511e972670ad33c5e49d31eddfe", + "sha256:a688ea65b2ea771b9b69dc409377240a7cab7c1aafef46cd75219d5a94ba49e0", + "sha256:ac92b2f2a9307ac0c4aafdfcf7ecf01ec92dfebd9140f8c95353adfbf5822cd4", + "sha256:b267a6c000b7f98e6436a9acefa5582a9662e503b0632a2562e3093a677f6845", + "sha256:b8639d8aa381a7d19b92ca1a32448f09baaf80787e50187d1f7d072191430768", + "sha256:c01aad36f0c34ca3f642018be37fd0d55c546f088837cba88f1a1aff408c63dd", + "sha256:ca8349fe56ce39498d9b5ca8eabba744774e94d85775259f26a43a03e8825429", + "sha256:ced1fa55e653d28f66c4f3569ed524d4d92098119dcd80c2fa026872a30eba55", + "sha256:e90a7b96ce2c6a60f6e2609b0c83f45bd55e144cc7c2a9714e344938827da363", + "sha256:eacc353dcf6f39665d4ca3311ded5ddae0f5a117f03107991d4185ffa59fd890", + "sha256:f41cb8da8bb487ed60329ac31789c50621f0e6d2c26abc7d4ae2383838fb1b93" + ], + "index": "pypi", + "version": "==9.0.0" + }, + "ordered-set": { + "hashes": [ + "sha256:046e1132c71fcf3330438a539928932caf51ddbc582496833e23de611de14562", + "sha256:694a8e44c87657c59292ede72891eb91d34131f6531463aab3009191c77364a8" ], "index": "pypi", - "version": "==8.0.24" - }, - "protobuf": { - "hashes": [ - "sha256:11301f1993f67dc81fc5c4756623652c899f7b5574b1e095d63bfc78347b11f3", - "sha256:228eecbedd46d75010f1e0f8ce34dbcd11ae5a40c165a9fc9d330a58aa302818", - "sha256:2cec059f4821c8f58890920a5c8a828ea027d46d5b18cb5e9dd4c727c65a2aa0", - "sha256:49dd3550bb42050d1676378c3fef91ff058d7023b77ac6f3179eb2a1c6c562d7", - "sha256:4bb7064727953d9187f6806230968b98e1ce4ec03bd737600e8380a9e5a6ac15", - "sha256:648178381d9dbbc736849443151533ab958e7b8bcbd658a62ad10906552fddcc", - "sha256:675d8e7463e03cf8343792935d62b80d90839d56a228c941dfdddda946eea066", - "sha256:80b233553ff500378becc372721541902c567e51b2654b68513d7b89c43dbc4b", - "sha256:8c0d3a5aa3412a440a9384349f6095991e8a5012e619cf5f57f042829f65cdb3", - "sha256:8ec649186f5443cab12692438190988bb9058dbfa5851d10d59a1c7214159a5f", - "sha256:9191e97d92b62424423ce5a5604047fd76c80a4f463fbd10c9d8b82928f152cf", - "sha256:b85ad5fe54163350067a53c0c170211c9f752dd4b4d8f339eb5aea8e987614a9", - "sha256:bc1ba824ff750c2ead1e7b8dd049bb5ddf8658d056cf4b989f04c68b049a47a7", - "sha256:be831508b9207156309a88b9d115dbae0caa4a987b05f1aa4fed4c5ac53ec51b", - "sha256:bf9a5caa0e0093552c2cc6051facef15b9c9ad4b1bde70430964edf99eaf2dad", - "sha256:c0f760adc1dd3dfe6d13af529b2ab42bb3fff1a2a00a6873b583b4ce0048ddff", - "sha256:c356d038a4e4cf52ccd7aff8022fc6a76daa0be5ecf2517ed75b87d32be0405d", - "sha256:c94ee9832fded92a11920b10d75c5aa7f4ef910b0bd463c039e102c8d7eb2c46", - "sha256:d3b9988f1a3591d900a090887ae4c41592e252bef5b249ad7e1fc46c21617534", - "sha256:ef9c2d0b3c0935725b547175745ceacb86f4d410b1e984d47e320c9efb1936c5" - ], - "version": "==3.16.0" + "version": "==4.1.0" + }, + "packaging": { + "hashes": [ + "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", + "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f" + ], + "markers": "python_version >= '3.8'", + "version": "==24.2" + }, + "pkgutil-resolve-name": { + "hashes": [ + "sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174", + "sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e" + ], + "markers": "python_version < '3.9'", + "version": "==1.3.10" + }, + "psycopg2-binary": { + "hashes": [ + "sha256:04392983d0bb89a8717772a193cfaac58871321e3ec69514e1c4e0d4957b5aff", + "sha256:056470c3dc57904bbf63d6f534988bafc4e970ffd50f6271fc4ee7daad9498a5", + "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f", + "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5", + "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0", + "sha256:19721ac03892001ee8fdd11507e6a2e01f4e37014def96379411ca99d78aeb2c", + "sha256:1a6784f0ce3fec4edc64e985865c17778514325074adf5ad8f80636cd029ef7c", + "sha256:2286791ececda3a723d1910441c793be44625d86d1a4e79942751197f4d30341", + "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f", + "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7", + "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d", + "sha256:270934a475a0e4b6925b5f804e3809dd5f90f8613621d062848dd82f9cd62007", + "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", + "sha256:2ad26b467a405c798aaa1458ba09d7e2b6e5f96b1ce0ac15d82fd9f95dc38a92", + "sha256:2b3d2491d4d78b6b14f76881905c7a8a8abcf974aad4a8a0b065273a0ed7a2cb", + "sha256:2ce3e21dc3437b1d960521eca599d57408a695a0d3c26797ea0f72e834c7ffe5", + "sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5", + "sha256:3216ccf953b3f267691c90c6fe742e45d890d8272326b4a8b20850a03d05b7b8", + "sha256:32581b3020c72d7a421009ee1c6bf4a131ef5f0a968fab2e2de0c9d2bb4577f1", + "sha256:35958ec9e46432d9076286dda67942ed6d968b9c3a6a2fd62b48939d1d78bf68", + "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73", + "sha256:3c18f74eb4386bf35e92ab2354a12c17e5eb4d9798e4c0ad3a00783eae7cd9f1", + "sha256:3c4745a90b78e51d9ba06e2088a2fe0c693ae19cc8cb051ccda44e8df8a6eb53", + "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d", + "sha256:3e9c76f0ac6f92ecfc79516a8034a544926430f7b080ec5a0537bca389ee0906", + "sha256:48b338f08d93e7be4ab2b5f1dbe69dc5e9ef07170fe1f86514422076d9c010d0", + "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2", + "sha256:512d29bb12608891e349af6a0cccedce51677725a921c07dba6342beaf576f9a", + "sha256:5a507320c58903967ef7384355a4da7ff3f28132d679aeb23572753cbf2ec10b", + "sha256:5c370b1e4975df846b0277b4deba86419ca77dbc25047f535b0bb03d1a544d44", + "sha256:6b269105e59ac96aba877c1707c600ae55711d9dcd3fc4b5012e4af68e30c648", + "sha256:6d4fa1079cab9018f4d0bd2db307beaa612b0d13ba73b5c6304b9fe2fb441ff7", + "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f", + "sha256:73aa0e31fa4bb82578f3a6c74a73c273367727de397a7a0f07bd83cbea696baa", + "sha256:7559bce4b505762d737172556a4e6ea8a9998ecac1e39b5233465093e8cee697", + "sha256:79625966e176dc97ddabc142351e0409e28acf4660b88d1cf6adb876d20c490d", + "sha256:7a813c8bdbaaaab1f078014b9b0b13f5de757e2b5d9be6403639b298a04d218b", + "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526", + "sha256:7f4152f8f76d2023aac16285576a9ecd2b11a9895373a1f10fd9db54b3ff06b4", + "sha256:7f5d859928e635fa3ce3477704acee0f667b3a3d3e4bb109f2b18d4005f38287", + "sha256:851485a42dbb0bdc1edcdabdb8557c09c9655dfa2ca0460ff210522e073e319e", + "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673", + "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0", + "sha256:8aabf1c1a04584c168984ac678a668094d831f152859d06e055288fa515e4d30", + "sha256:8aecc5e80c63f7459a1a2ab2c64df952051df196294d9f739933a9f6687e86b3", + "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e", + "sha256:8de718c0e1c4b982a54b41779667242bc630b2197948405b7bd8ce16bcecac92", + "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a", + "sha256:b5f86c56eeb91dc3135b3fd8a95dc7ae14c538a2f3ad77a19645cf55bab1799c", + "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8", + "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909", + "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47", + "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864", + "sha256:d00924255d7fc916ef66e4bf22f354a940c67179ad3fd7067d7a0a9c84d2fbfc", + "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00", + "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb", + "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539", + "sha256:e5720a5d25e3b99cd0dc5c8a440570469ff82659bb09431c1439b92caf184d3b", + "sha256:e8b58f0a96e7a1e341fc894f62c1177a7c83febebb5ff9123b579418fdc8a481", + "sha256:e984839e75e0b60cfe75e351db53d6db750b00de45644c5d1f7ee5d1f34a1ce5", + "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4", + "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64", + "sha256:ecced182e935529727401b24d76634a357c71c9275b356efafd8a2a91ec07392", + "sha256:ee0e8c683a7ff25d23b55b11161c2663d4b099770f6085ff0a20d4505778d6b4", + "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1", + "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1", + "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567", + "sha256:ffe8ed017e4ed70f68b7b371d84b7d4a790368db9203dfc2d222febd3a9c8863" + ], + "index": "pypi", + "version": "==2.9.10" }, "pyasn1": { "hashes": [ @@ -290,64 +552,240 @@ "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2", "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3" ], + "markers": "python_version >= '3.8'", "version": "==0.4.8" }, - "pyrsistent": { + "pygments": { + "hashes": [ + "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", + "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b" + ], + "markers": "python_version >= '3.8'", + "version": "==2.19.2" + }, + "python-dotenv": { "hashes": [ - "sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e" + "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", + "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a" ], - "markers": "python_version >= '3.5'", - "version": "==0.17.3" + "index": "pypi", + "version": "==1.0.1" }, "python-jose": { "hashes": [ - "sha256:4e4192402e100b5fb09de5a8ea6bcc39c36ad4526341c123d401e2561720335b", - "sha256:67d7dfff599df676b04a996520d9be90d6cdb7e6dd10b4c7cacc0c3e2e92f2be" + "sha256:9a9a40f418ced8ecaf7e3b28d69887ceaa76adad3bcaa6dae0d9e596fec1d680", + "sha256:9c9f616819652d109bd889ecd1e15e9a162b9b94d682534c9c2146092945b78f" ], "index": "pypi", - "version": "==3.2.0" + "version": "==3.4.0" }, "pyyaml": { "hashes": [ - "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf", - "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696", - "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393", - "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77", - "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922", - "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5", - "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8", - "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10", - "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc", - "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018", - "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e", - "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253", - "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347", - "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183", - "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541", - "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb", - "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185", - "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc", - "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db", - "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa", - "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46", - "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122", - "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b", - "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63", - "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df", - "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc", - "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247", - "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6", - "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0" - ], - "version": "==5.4.1" + "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", + "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", + "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", + "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", + "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", + "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", + "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", + "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", + "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", + "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", + "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", + "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", + "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", + "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", + "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", + "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", + "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", + "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", + "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", + "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", + "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", + "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", + "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", + "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", + "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", + "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", + "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", + "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", + "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", + "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", + "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", + "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", + "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", + "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", + "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", + "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", + "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", + "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", + "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", + "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", + "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", + "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", + "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", + "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", + "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", + "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", + "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", + "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", + "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", + "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", + "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", + "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", + "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4" + ], + "markers": "python_version >= '3.8'", + "version": "==6.0.2" + }, + "referencing": { + "hashes": [ + "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c", + "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de" + ], + "markers": "python_version >= '3.8'", + "version": "==0.35.1" + }, + "requests": { + "hashes": [ + "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", + "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422" + ], + "index": "pypi", + "version": "==2.32.4" + }, + "rich": { + "hashes": [ + "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", + "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90" + ], + "markers": "python_version >= '3.8'", + "version": "==13.9.4" + }, + "rpds-py": { + "hashes": [ + "sha256:02a0629ec053fc013808a85178524e3cb63a61dbc35b22499870194a63578fb9", + "sha256:07924c1b938798797d60c6308fa8ad3b3f0201802f82e4a2c41bb3fafb44cc28", + "sha256:07f59760ef99f31422c49038964b31c4dfcfeb5d2384ebfc71058a7c9adae2d2", + "sha256:0a3a1e9ee9728b2c1734f65d6a1d376c6f2f6fdcc13bb007a08cc4b1ff576dc5", + "sha256:0a90c373ea2975519b58dece25853dbcb9779b05cc46b4819cb1917e3b3215b6", + "sha256:0ad56edabcdb428c2e33bbf24f255fe2b43253b7d13a2cdbf05de955217313e6", + "sha256:0b581f47257a9fce535c4567782a8976002d6b8afa2c39ff616edf87cbeff712", + "sha256:0f8f741b6292c86059ed175d80eefa80997125b7c478fb8769fd9ac8943a16c0", + "sha256:0fc212779bf8411667234b3cdd34d53de6c2b8b8b958e1e12cb473a5f367c338", + "sha256:13c56de6518e14b9bf6edde23c4c39dac5b48dcf04160ea7bce8fca8397cdf86", + "sha256:142c0a5124d9bd0e2976089484af5c74f47bd3298f2ed651ef54ea728d2ea42c", + "sha256:14511a539afee6f9ab492b543060c7491c99924314977a55c98bfa2ee29ce78c", + "sha256:15a842bb369e00295392e7ce192de9dcbf136954614124a667f9f9f17d6a216f", + "sha256:16d4477bcb9fbbd7b5b0e4a5d9b493e42026c0bf1f06f723a9353f5153e75d30", + "sha256:1791ff70bc975b098fe6ecf04356a10e9e2bd7dc21fa7351c1742fdeb9b4966f", + "sha256:19b73643c802f4eaf13d97f7855d0fb527fbc92ab7013c4ad0e13a6ae0ed23bd", + "sha256:200a23239781f46149e6a415f1e870c5ef1e712939fe8fa63035cd053ac2638e", + "sha256:2249280b870e6a42c0d972339e9cc22ee98730a99cd7f2f727549af80dd5a963", + "sha256:2b431c777c9653e569986ecf69ff4a5dba281cded16043d348bf9ba505486f36", + "sha256:2cc3712a4b0b76a1d45a9302dd2f53ff339614b1c29603a911318f2357b04dd2", + "sha256:2fbb0ffc754490aff6dabbf28064be47f0f9ca0b9755976f945214965b3ace7e", + "sha256:32b922e13d4c0080d03e7b62991ad7f5007d9cd74e239c4b16bc85ae8b70252d", + "sha256:36785be22066966a27348444b40389f8444671630063edfb1a2eb04318721e17", + "sha256:37fe0f12aebb6a0e3e17bb4cd356b1286d2d18d2e93b2d39fe647138458b4bcb", + "sha256:3aea7eed3e55119635a74bbeb80b35e776bafccb70d97e8ff838816c124539f1", + "sha256:3c6afcf2338e7f374e8edc765c79fbcb4061d02b15dd5f8f314a4af2bdc7feb5", + "sha256:3ccb8ac2d3c71cda472b75af42818981bdacf48d2e21c36331b50b4f16930163", + "sha256:3d089d0b88996df627693639d123c8158cff41c0651f646cd8fd292c7da90eaf", + "sha256:3dd645e2b0dcb0fd05bf58e2e54c13875847687d0b71941ad2e757e5d89d4356", + "sha256:3e310838a5801795207c66c73ea903deda321e6146d6f282e85fa7e3e4854804", + "sha256:42cbde7789f5c0bcd6816cb29808e36c01b960fb5d29f11e052215aa85497c93", + "sha256:483b29f6f7ffa6af845107d4efe2e3fa8fb2693de8657bc1849f674296ff6a5a", + "sha256:4888e117dd41b9d34194d9e31631af70d3d526efc363085e3089ab1a62c32ed1", + "sha256:49fe9b04b6fa685bd39237d45fad89ba19e9163a1ccaa16611a812e682913496", + "sha256:4a5a844f68776a7715ecb30843b453f07ac89bad393431efbf7accca3ef599c1", + "sha256:4a916087371afd9648e1962e67403c53f9c49ca47b9680adbeef79da3a7811b0", + "sha256:4f676e21db2f8c72ff0936f895271e7a700aa1f8d31b40e4e43442ba94973899", + "sha256:518d2ca43c358929bf08f9079b617f1c2ca6e8848f83c1225c88caeac46e6cbc", + "sha256:5265505b3d61a0f56618c9b941dc54dc334dc6e660f1592d112cd103d914a6db", + "sha256:55cd1fa4ecfa6d9f14fbd97ac24803e6f73e897c738f771a9fe038f2f11ff07c", + "sha256:58b1d5dd591973d426cbb2da5e27ba0339209832b2f3315928c9790e13f159e8", + "sha256:59240685e7da61fb78f65a9f07f8108e36a83317c53f7b276b4175dc44151684", + "sha256:5b48e790e0355865197ad0aca8cde3d8ede347831e1959e158369eb3493d2191", + "sha256:5d4eea0761e37485c9b81400437adb11c40e13ef513375bbd6973e34100aeb06", + "sha256:648386ddd1e19b4a6abab69139b002bc49ebf065b596119f8f37c38e9ecee8ff", + "sha256:653647b8838cf83b2e7e6a0364f49af96deec64d2a6578324db58380cff82aca", + "sha256:6740a3e8d43a32629bb9b009017ea5b9e713b7210ba48ac8d4cb6d99d86c8ee8", + "sha256:6889469bfdc1eddf489729b471303739bf04555bb151fe8875931f8564309afc", + "sha256:68cb0a499f2c4a088fd2f521453e22ed3527154136a855c62e148b7883b99f9a", + "sha256:6aa97af1558a9bef4025f8f5d8c60d712e0a3b13a2fe875511defc6ee77a1ab7", + "sha256:6b73c67850ca7cae0f6c56f71e356d7e9fa25958d3e18a64927c2d930859b8e4", + "sha256:6c8e9340ce5a52f95fa7d3b552b35c7e8f3874d74a03a8a69279fd5fca5dc751", + "sha256:6ca91093a4a8da4afae7fe6a222c3b53ee4eef433ebfee4d54978a103435159e", + "sha256:754bbed1a4ca48479e9d4182a561d001bbf81543876cdded6f695ec3d465846b", + "sha256:762703bdd2b30983c1d9e62b4c88664df4a8a4d5ec0e9253b0231171f18f6d75", + "sha256:78f0b6877bfce7a3d1ff150391354a410c55d3cdce386f862926a4958ad5ab7e", + "sha256:7a07ced2b22f0cf0b55a6a510078174c31b6d8544f3bc00c2bcee52b3d613f74", + "sha256:7dca7081e9a0c3b6490a145593f6fe3173a94197f2cb9891183ef75e9d64c425", + "sha256:7e21b7031e17c6b0e445f42ccc77f79a97e2687023c5746bfb7a9e45e0921b84", + "sha256:7f5179583d7a6cdb981151dd349786cbc318bab54963a192692d945dd3f6435d", + "sha256:83cba698cfb3c2c5a7c3c6bac12fe6c6a51aae69513726be6411076185a8b24a", + "sha256:842c19a6ce894493563c3bd00d81d5100e8e57d70209e84d5491940fdb8b9e3a", + "sha256:84b8382a90539910b53a6307f7c35697bc7e6ffb25d9c1d4e998a13e842a5e83", + "sha256:8ba6f89cac95c0900d932c9efb7f0fb6ca47f6687feec41abcb1bd5e2bd45535", + "sha256:8bbe951244a838a51289ee53a6bae3a07f26d4e179b96fc7ddd3301caf0518eb", + "sha256:925d176a549f4832c6f69fa6026071294ab5910e82a0fe6c6228fce17b0706bd", + "sha256:92b68b79c0da2a980b1c4197e56ac3dd0c8a149b4603747c4378914a68706979", + "sha256:93da1d3db08a827eda74356f9f58884adb254e59b6664f64cc04cdff2cc19b0d", + "sha256:95f3b65d2392e1c5cec27cff08fdc0080270d5a1a4b2ea1d51d5f4a2620ff08d", + "sha256:9c4cb04a16b0f199a8c9bf807269b2f63b7b5b11425e4a6bd44bd6961d28282c", + "sha256:a624cc00ef2158e04188df5e3016385b9353638139a06fb77057b3498f794782", + "sha256:a649dfd735fff086e8a9d0503a9f0c7d01b7912a333c7ae77e1515c08c146dad", + "sha256:a94e52537a0e0a85429eda9e49f272ada715506d3b2431f64b8a3e34eb5f3e75", + "sha256:aa7ac11e294304e615b43f8c441fee5d40094275ed7311f3420d805fde9b07b4", + "sha256:b41b6321805c472f66990c2849e152aff7bc359eb92f781e3f606609eac877ad", + "sha256:b71b8666eeea69d6363248822078c075bac6ed135faa9216aa85f295ff009b1e", + "sha256:b9c2fe36d1f758b28121bef29ed1dee9b7a2453e997528e7d1ac99b94892527c", + "sha256:bb63804105143c7e24cee7db89e37cb3f3941f8e80c4379a0b355c52a52b6780", + "sha256:be5ef2f1fc586a7372bfc355986226484e06d1dc4f9402539872c8bb99e34b01", + "sha256:c142b88039b92e7e0cb2552e8967077e3179b22359e945574f5e2764c3953dcf", + "sha256:c14937af98c4cc362a1d4374806204dd51b1e12dded1ae30645c298e5a5c4cb1", + "sha256:ca449520e7484534a2a44faf629362cae62b660601432d04c482283c47eaebab", + "sha256:cd945871335a639275eee904caef90041568ce3b42f402c6959b460d25ae8732", + "sha256:d0b937b2a1988f184a3e9e577adaa8aede21ec0b38320d6009e02bd026db04fa", + "sha256:d126b52e4a473d40232ec2052a8b232270ed1f8c9571aaf33f73a14cc298c24f", + "sha256:d8761c3c891cc51e90bc9926d6d2f59b27beaf86c74622c8979380a29cc23ac3", + "sha256:d9ecb51120de61e4604650666d1f2b68444d46ae18fd492245a08f53ad2b7711", + "sha256:da584ff96ec95e97925174eb8237e32f626e7a1a97888cdd27ee2f1f24dd0ad8", + "sha256:dbcf360c9e3399b056a238523146ea77eeb2a596ce263b8814c900263e46031a", + "sha256:dbddc10776ca7ebf2a299c41a4dde8ea0d8e3547bfd731cb87af2e8f5bf8962d", + "sha256:dc73505153798c6f74854aba69cc75953888cf9866465196889c7cdd351e720c", + "sha256:e13de156137b7095442b288e72f33503a469aa1980ed856b43c353ac86390519", + "sha256:e1791c4aabd117653530dccd24108fa03cc6baf21f58b950d0a73c3b3b29a350", + "sha256:e75ba609dba23f2c95b776efb9dd3f0b78a76a151e96f96cc5b6b1b0004de66f", + "sha256:e79059d67bea28b53d255c1437b25391653263f0e69cd7dec170d778fdbca95e", + "sha256:ecd27a66740ffd621d20b9a2f2b5ee4129a56e27bfb9458a3bcc2e45794c96cb", + "sha256:f009c69bc8c53db5dfab72ac760895dc1f2bc1b62ab7408b253c8d1ec52459fc", + "sha256:f16bc1334853e91ddaaa1217045dd7be166170beec337576818461268a3de67f", + "sha256:f19169781dddae7478a32301b499b2858bc52fc45a112955e798ee307e294977", + "sha256:fa3060d885657abc549b2a0f8e1b79699290e5d83845141717c6c90c2df38311", + "sha256:fa41a64ac5b08b292906e248549ab48b69c5428f3987b09689ab2441f267d04d", + "sha256:fbf15aff64a163db29a91ed0868af181d6f68ec1a3a7d5afcfe4501252840bad", + "sha256:fe00a9057d100e69b4ae4a094203a708d65b0f345ed546fdef86498bf5390982" + ], + "markers": "python_version >= '3.8'", + "version": "==0.20.1" }, "rsa": { "hashes": [ - "sha256:78f9a9bf4e7be0c5ded4583326e7461e3a3c5aae24073648b4bdfa797d78c9d2", - "sha256:9d689e6ca1b3038bc82bf8d23e944b6b6037bc02301a574935b2dd946e0353b9" + "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", + "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75" ], - "markers": "python_version >= '3.5' and python_version < '4'", - "version": "==4.7.2" + "markers": "python_version >= '3.6' and python_version < '4'", + "version": "==4.9.1" + }, + "sentry-sdk": { + "hashes": [ + "sha256:38c98e3cbb620dd3dd80a8d6e39c753d453dd41f8a9df581b0584c19a52bc926", + "sha256:e9e8f3c795044beb59f2c8f4c6b9b0f9779e5e604099882df05eec525e782cc6" + ], + "index": "pypi", + "version": "==2.35.2" }, "six": { "hashes": [ @@ -397,14 +835,336 @@ "index": "pypi", "version": "==1.3.24" }, + "typing-extensions": { + "hashes": [ + "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", + "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef" + ], + "markers": "python_version >= '3.8'", + "version": "==4.13.2" + }, + "urllib3": { + "hashes": [ + "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", + "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9" + ], + "markers": "python_version >= '3.8'", + "version": "==2.2.3" + }, "werkzeug": { "hashes": [ - "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43", - "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c" + "sha256:1421ebfc7648a39a5c58c601b154165d05cf47a3cd0ccb70857cbdacf6c8f2b8", + "sha256:b863f8ff057c522164b6067c9e28b041161b4be5ba4d0daceeaa50a163822d3c" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.0.1" + "index": "pypi", + "version": "==2.0.3" + }, + "wrapt": { + "hashes": [ + "sha256:02b551d101f31694fc785e58e0720ef7d9a10c4e62c1c9358ce6f63f23e30a56", + "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", + "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", + "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", + "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", + "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", + "sha256:0f5f51a6466667a5a356e6381d362d259125b57f059103dd9fdc8c0cf1d14139", + "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", + "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", + "sha256:1f23fa283f51c890eda8e34e4937079114c74b4c81d2b2f1f1d94948f5cc3d7f", + "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", + "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", + "sha256:24c2ed34dc222ed754247a2702b1e1e89fdbaa4016f324b4b8f1a802d4ffe87f", + "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", + "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", + "sha256:30ce38e66630599e1193798285706903110d4f057aab3168a34b7fdc85569afc", + "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05", + "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd", + "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", + "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", + "sha256:3e62d15d3cfa26e3d0788094de7b64efa75f3a53875cdbccdf78547aed547a81", + "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", + "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", + "sha256:46acc57b331e0b3bcb3e1ca3b421d65637915cfcd65eb783cb2f78a511193f9b", + "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", + "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", + "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", + "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", + "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", + "sha256:55cbbc356c2842f39bcc553cf695932e8b30e30e797f961860afb308e6b1bb7c", + "sha256:59923aa12d0157f6b82d686c3fd8e1166fa8cdfb3e17b42ce3b6147ff81528df", + "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", + "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", + "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", + "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", + "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", + "sha256:656873859b3b50eeebe6db8b1455e99d90c26ab058db8e427046dbc35c3140a5", + "sha256:65d1d00fbfb3ea5f20add88bbc0f815150dbbde3b026e6c24759466c8b5a9ef9", + "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", + "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", + "sha256:70d86fa5197b8947a2fa70260b48e400bf2ccacdcab97bb7de47e3d1e6312225", + "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", + "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", + "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", + "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", + "sha256:758895b01d546812d1f42204bd443b8c433c44d090248bf22689df673ccafe00", + "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", + "sha256:7e18f01b0c3e4a07fe6dfdb00e29049ba17eadbc5e7609a2a3a4af83ab7d710a", + "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", + "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04", + "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", + "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", + "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390", + "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", + "sha256:a7c06742645f914f26c7f1fa47b8bc4c91d222f76ee20116c43d5ef0912bba2d", + "sha256:a9a2203361a6e6404f80b99234fe7fb37d1fc73487b5a78dc1aa5b97201e0f22", + "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", + "sha256:ad85e269fe54d506b240d2d7b9f5f2057c2aa9a2ea5b32c66f8902f768117ed2", + "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18", + "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6", + "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", + "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", + "sha256:caea3e9c79d5f0d2c6d9ab96111601797ea5da8e6d0723f77eabb0d4068d2b2f", + "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", + "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", + "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", + "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", + "sha256:df7d30371a2accfe4013e90445f6388c570f103d61019b6b7c57e0265250072a", + "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", + "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", + "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", + "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", + "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2", + "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418", + "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", + "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", + "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", + "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", + "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775", + "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", + "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c" + ], + "markers": "python_version >= '3.8'", + "version": "==1.17.3" + }, + "zipp": { + "hashes": [ + "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350", + "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29" + ], + "markers": "python_version < '3.10'", + "version": "==3.20.2" } }, - "develop": {} + "develop": { + "alembic": { + "hashes": [ + "sha256:1acdd7a3a478e208b0503cd73614d5e4c6efafa4e73518bb60e4f2846a37b1c5", + "sha256:496e888245a53adf1498fcab31713a469c65836f8de76e01399aa1c3e90dd213" + ], + "markers": "python_version >= '3.8'", + "version": "==1.14.1" + }, + "click": { + "hashes": [ + "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", + "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a" + ], + "markers": "python_version >= '3.7'", + "version": "==8.1.8" + }, + "flask": { + "hashes": [ + "sha256:59da8a3170004800a2837844bfa84d49b022550616070f7cb1a659682b2e7c9f", + "sha256:e1120c228ca2f553b470df4a5fa927ab66258467526069981b3eb0a91902687d" + ], + "index": "pypi", + "version": "==2.0.3" + }, + "flask-migrate": { + "hashes": [ + "sha256:4d42e8f861d78cb6e9319afcba5bf76062e5efd7784184dd2a1cccd9de34a702", + "sha256:df9043d2050df3c0e0f6313f6b529b62c837b6033c20335e9d0b4acdf2c40e23" + ], + "index": "pypi", + "version": "==3.0.1" + }, + "flask-sqlalchemy": { + "hashes": [ + "sha256:2bda44b43e7cacb15d4e05ff3cc1f8bc97936cc464623424102bfc2c35e95912", + "sha256:f12c3d4cc5cc7fdcc148b9527ea05671718c3ea45d50c7e732cceb33f574b390" + ], + "index": "pypi", + "version": "==2.5.1" + }, + "importlib-metadata": { + "hashes": [ + "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", + "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7" + ], + "markers": "python_version < '3.9'", + "version": "==8.5.0" + }, + "importlib-resources": { + "hashes": [ + "sha256:980862a1d16c9e147a59603677fa2aa5fd82b87f223b6cb870695bcfce830065", + "sha256:ac29d5f956f01d5e4bb63102a5a19957f1b9175e45649977264a1416783bb717" + ], + "markers": "python_version < '3.9'", + "version": "==6.4.5" + }, + "itsdangerous": { + "hashes": [ + "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", + "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173" + ], + "markers": "python_version >= '3.8'", + "version": "==2.2.0" + }, + "jinja2": { + "hashes": [ + "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", + "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67" + ], + "markers": "python_version >= '3.7'", + "version": "==3.1.6" + }, + "mako": { + "hashes": [ + "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", + "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59" + ], + "markers": "python_version >= '3.8'", + "version": "==1.3.10" + }, + "markupsafe": { + "hashes": [ + "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", + "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", + "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", + "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", + "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", + "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", + "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", + "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df", + "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", + "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", + "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", + "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", + "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", + "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371", + "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2", + "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", + "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52", + "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", + "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", + "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", + "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", + "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", + "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", + "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", + "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", + "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", + "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", + "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", + "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", + "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9", + "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", + "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", + "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", + "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", + "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", + "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", + "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a", + "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", + "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", + "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", + "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", + "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", + "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", + "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", + "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", + "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f", + "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50", + "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", + "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", + "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", + "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", + "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", + "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", + "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", + "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf", + "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", + "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", + "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", + "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", + "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68" + ], + "markers": "python_version >= '3.7'", + "version": "==2.1.5" + }, + "sqlalchemy": { + "hashes": [ + "sha256:014ea143572fee1c18322b7908140ad23b3994036ef4c0d630110faf942652f8", + "sha256:0172423a27fbcae3751ef016663b72e1a516777de324a76e30efa170dbd3dd2d", + "sha256:01aa5f803db724447c1d423ed583e42bf5264c597fd55e4add4301f163b0be48", + "sha256:0352db1befcbed2f9282e72843f1963860bf0e0472a4fa5cf8ee084318e0e6ab", + "sha256:09083c2487ca3c0865dc588e07aeaa25416da3d95f7482c07e92f47e080aa17b", + "sha256:0d5d862b1cfbec5028ce1ecac06a3b42bc7703eb80e4b53fceb2738724311443", + "sha256:14f0eb5db872c231b20c18b1e5806352723a3a89fb4254af3b3e14f22eaaec75", + "sha256:1e2f89d2e5e3c7a88e25a3b0e43626dba8db2aa700253023b82e630d12b37109", + "sha256:26155ea7a243cbf23287f390dba13d7927ffa1586d3208e0e8d615d0c506f996", + "sha256:2ed6343b625b16bcb63c5b10523fd15ed8934e1ed0f772c534985e9f5e73d894", + "sha256:34fcec18f6e4b24b4a5f6185205a04f1eab1e56f8f1d028a2a03694ebcc2ddd4", + "sha256:4d0e3515ef98aa4f0dc289ff2eebb0ece6260bbf37c2ea2022aad63797eacf60", + "sha256:5de2464c254380d8a6c20a2746614d5a436260be1507491442cf1088e59430d2", + "sha256:6607ae6cd3a07f8a4c3198ffbf256c261661965742e2b5265a77cd5c679c9bba", + "sha256:8110e6c414d3efc574543109ee618fe2c1f96fa31833a1ff36cc34e968c4f233", + "sha256:816de75418ea0953b5eb7b8a74933ee5a46719491cd2b16f718afc4b291a9658", + "sha256:861e459b0e97673af6cc5e7f597035c2e3acdfb2608132665406cded25ba64c7", + "sha256:87a2725ad7d41cd7376373c15fd8bf674e9c33ca56d0b8036add2d634dba372e", + "sha256:a006d05d9aa052657ee3e4dc92544faae5fcbaafc6128217310945610d862d39", + "sha256:bce28277f308db43a6b4965734366f533b3ff009571ec7ffa583cb77539b84d6", + "sha256:c10ff6112d119f82b1618b6dc28126798481b9355d8748b64b9b55051eb4f01b", + "sha256:d375d8ccd3cebae8d90270f7aa8532fe05908f79e78ae489068f3b4eee5994e8", + "sha256:d37843fb8df90376e9e91336724d78a32b988d3d20ab6656da4eb8ee3a45b63c", + "sha256:e47e257ba5934550d7235665eee6c911dc7178419b614ba9e1fbb1ce6325b14f", + "sha256:e98d09f487267f1e8d1179bf3b9d7709b30a916491997137dd24d6ae44d18d79", + "sha256:ebbb777cbf9312359b897bf81ba00dae0f5cb69fba2a18265dcc18a6f5ef7519", + "sha256:ee5f5188edb20a29c1cc4a039b074fdc5575337c9a68f3063449ab47757bb064", + "sha256:f03bd97650d2e42710fbe4cf8a59fae657f191df851fc9fc683ecef10746a375", + "sha256:f1149d6e5c49d069163e58a3196865e4321bad1803d7886e07d8710de392c548", + "sha256:f3c5c52f7cb8b84bfaaf22d82cb9e6e9a8297f7c2ed14d806a0f5e4d22e83fb7", + "sha256:f597a243b8550a3a0b15122b14e49d8a7e622ba1c9d29776af741f1845478d79", + "sha256:fc1f2a5a5963e2e73bac4926bdaf7790c4d7d77e8fc0590817880e22dd9d0b8b", + "sha256:fc4cddb0b474b12ed7bdce6be1b9edc65352e8ce66bc10ff8cbbfb3d4047dbf4", + "sha256:fcb251305fa24a490b6a9ee2180e5f8252915fb778d3dafc70f9cc3f863827b9" + ], + "index": "pypi", + "version": "==1.3.24" + }, + "typing-extensions": { + "hashes": [ + "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", + "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef" + ], + "markers": "python_version >= '3.8'", + "version": "==4.13.2" + }, + "werkzeug": { + "hashes": [ + "sha256:1421ebfc7648a39a5c58c601b154165d05cf47a3cd0ccb70857cbdacf6c8f2b8", + "sha256:b863f8ff057c522164b6067c9e28b041161b4be5ba4d0daceeaa50a163822d3c" + ], + "index": "pypi", + "version": "==2.0.3" + }, + "zipp": { + "hashes": [ + "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350", + "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29" + ], + "markers": "python_version < '3.10'", + "version": "==3.20.2" + } + } } diff --git a/README.md b/README.md index 0e98321..20fd999 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # ClassClockAPI +![Docker Pulls](https://img.shields.io/docker/pulls/moralcode/classclockapi) + This is the backend that provides access to the ClassClock database. ## Environment Variables @@ -15,9 +17,28 @@ This is the backend that provides access to the ClassClock database. | API_IDENTIFIER | no default | Your Auth0 api identifier. This may be your API domain name. i.e. `https://api.yourdomain.com` | | AUTH0_CLIENT_ID | no default | Your Auth0 Client ID | | AUTH0_CLIENT_SECRET | no default | Your Auth0 Client Secret | +| SENTRY_DSN | no default | The dsn URL from the sentry.io setup in case you wish to set up error monitoring | +| TRUSTED_PROXY_COUNT | no default | The number of proxies that are in between users and the app itself. Setting this too high can create security problems. Setting too low can cause rate limiting to not work. see [here](https://flask-limiter.readthedocs.io/en/stable/recipes.html#deploying-an-application-behind-a-proxy) for what this is used for | + + +## First time Setup + +1. Prepare an empty database and have its configuration information handy (login, hostname/port, db name .etc) +2. Install all dependencies (including dev dependencies) using `pipenv install -d` +3. set up a `.env` file with your database configuration settings from earlier +4. create the db by running `pipenv run python3 createdb.py`. add the `--demo` flag to createdb if you want to include demo data. + +After this you should be ready to run the API. + +## Docker +This API has been set up to run in a docker container. +The simplest way to run it is to: +1. aquire the container either from a docker repository or by running `docker build . -t classclock-api` in a cloned version of this repo +2. set up the environment variables you want per the above table +3. use a command like `docker run -p 8000:8000 --rm -it --env-file dev.env classclock-api:latest` to run the container interactively using `dev.env` as the source of the environment variables. The app will start up on port 8000. -## Setup command +## Contributing -To run the setup script, first create an empty database and a new user that can access the database. Then, with the environment variables set appropriately as listed above, execute `python3 api.py setup`. If you have not specified the `DB_USERNAME` or `DB_PASSWORD` environment variables, you will be prompted for them interactively when the script runs. \ No newline at end of file +If you are interested in making changes to the ClassClock API, see the [CONTRIBUTING](./CONTRIBUTING.md) file for details on how to do so. \ No newline at end of file diff --git a/api.py b/api.py index faded8b..d325136 100644 --- a/api.py +++ b/api.py @@ -1,216 +1,61 @@ from flask import Flask, render_template import logging -from versions import v0 +from blueprints import v0, main +from docs import create_docs from flask_limiter import Limiter -from flasgger import Swagger -from common.helpers import get_request_origin_identifier, make_error_object, respond +from flask_limiter.util import get_remote_address +from common.helpers import make_error_object, respond from common.db_schema import db from common.schemas import * -from flasgger import APISpec, Schema, Swagger, fields -from apispec.ext.marshmallow import MarshmallowPlugin -from apispec_webframeworks.flask import FlaskPlugin -import os, sys, getpass -from os import environ as env -from werkzeug.exceptions import HTTPException - - -#default values are only needed for some of these -db_username = env.get("DB_USERNAME") -db_password = env.get("DB_PASSWORD") -db_host = os.getenv("DB_HOST", "localhost") -db_name = os.getenv("DB_NAME", "classclock") +from auth import db_connection_string +from flask_migrate import Migrate +from werkzeug.middleware.proxy_fix import ProxyFix -if sys.argv[1] in ("setup", "demo"): - if not env.get("DB_CONNECTION_URL"): - if not db_username: - db_username = input('database username: ') - if not db_password: - db_password = getpass.getpass('database password (input not shown): ') +from os import environ as env +if env.get("SENTRY_DSN"): + # app.logger.info("Detected Sentry DSN, setting up sentry...") -db_connection_string=os.getenv("DB_CONNECTION_URL", - 'mysql+mysqlconnector://{user}:{pw}@{url}/{db}'.format( - user=db_username, - pw=db_password, - url=db_host, - db=db_name + import sentry_sdk + sentry_sdk.init( + dsn=env.get("SENTRY_DSN"), + # Set traces_sample_rate to 1.0 to capture 100% + # of transactions for performance monitoring. + # We recommend adjusting this value in production. + traces_sample_rate=1.0 ) -) - -app = Flask(__name__) -limiter = Limiter(app, default_limits=[ - "25/hour", "5/minute"], key_func=get_request_origin_identifier, headers_enabled=True) - -app.register_blueprint(v0.blueprint, url_prefix='/v0') - - -# Create an APISpec -#info: { -spec = APISpec( - title="ClassClock API", - version="0.1", - openapi_version='2.0', - plugins=[ - FlaskPlugin(), - MarshmallowPlugin(), - ], - options= { - "description": "The development version of the ClassClock API", - "servers": [ - { - "url": "https://api.classclock.app/v0", - "description": "ClassClock API Server" - }, - { - "url": "https://localhost:8000/v0", - "description": "Dev server" - } - ], - "externalDocs": { - "description": "This API might loosely follow the JSON:API specofocation", - "url": "https://jsonapi.org" - }#, -# "contact": { -# # "responsibleOrganization": "ME", -# # "responsibleDeveloper": "Me", -# # "email": "me@me.com", -# # "url": "www.me.com", -# }, -# "termsOfService": "http://me.com/terms", - } -) - - -template = spec.to_flasgger( - app, - definitions=[SchoolSchema, BellScheduleSchema], - paths=[] -) - -swagger = Swagger(app, config={ - "headers": [ - ], - "specs": [ - { - "endpoint": 'apispec_v1', - "route": '/apispec_v1.json', - "rule_filter": lambda rule: True, # all in - "model_filter": lambda tag: True, # all in - } - ], - "static_url_path": "/flasgger_static", - # "static_folder": "static", # must be set by user - "swagger_ui": True, - "specs_route": "/v0/docs/", - "basePath": "/v0", - "host": "api.classclock.app", # overrides localhost:500 - "schemes": [ - "https" - ], - "securityDefinitions": { - "ApiKeyAuth": { - "type": "apiKey", - "in": "header", - "name": "Authentication" - } - } -}, -template=template) +def create_app(config_filename=None): + app = Flask(__name__) + if env.get("TRUSTED_PROXY_COUNT"): + app.logger.info("Detected value for TRUSTED_PROXY_COUNT, setting up ProxyFix..." + env.get("TRUSTED_PROXY_COUNT")) + # for example if the request goes through one proxy + # before hitting your application server + app.wsgi_app = ProxyFix(app.wsgi_app, x_for=int(env.get("TRUSTED_PROXY_COUNT") or 0)) -app.config.update(SQLALCHEMY_DATABASE_URI=db_connection_string,DEBUG=True, SQLALCHEMY_TRACK_MODIFICATIONS=False) -db.init_app(app) + if config_filename: + app.config.from_pyfile(config_filename) -if sys.argv[1] == "setup": -# https://stackoverflow.com/a/46541219 - with app.app_context(): - db.create_all() - db.session.commit() - print("Done.") - exit(0) -elif sys.argv[1] == "demo": -# https://stackoverflow.com/a/46541219 - with app.app_context(): - from common.db_schema import * - import datetime - import time + limiter = Limiter(get_remote_address, app=app, default_limits=[ + "500/hour", "100/minute"], headers_enabled=True) + app.register_blueprint(v0.blueprint, url_prefix='/v0') + app.register_blueprint(main.main_pages) - def today_plus(sch, num_days): - return BellScheduleDate(school_id = sch, date=datetime.date.today() + datetime.timedelta(days=num_days)) - - s = School(owner_id="1234567890", full_name="Demonstration High School", acronym="DHS") - - - sc1 = BellSchedule( - school_id = s.id, - name="Even Day", - display_name= "Even", - dates=[ - today_plus(s.id, 0), - today_plus(s.id, 2), - today_plus(s.id, 4) - ] - ) - - sc2 = BellSchedule( - school_id = s.id, - name="Odd Day", - display_name="Odd", - dates=[ - today_plus(s.id, 1), - today_plus(s.id, 3) - ] - ) - - - s.schedules = [sc1,sc2] - - - sc1.meeting_times = [ - BellScheduleMeetingTime( - bell_schedule_id =sc1.id, - name="First Period", - start_time=datetime.time(hour=8, minute=25, second=0, microsecond=0), - end_time=datetime.time(9,25) - ) - ] - - sc2.meeting_times = [ - BellScheduleMeetingTime( - bell_schedule_id =sc2.id, - name="First Period", - start_time=datetime.time(8,45), - end_time=datetime.time(9,45) - ) - - ] - - db.session.add(s) - db.session.commit() - print("Done.") - exit(0) - - -@app.errorhandler(HTTPException) -def handle_HTTP_error(e): - return respond( - make_error_object( - e.code, title=e.name, message=e.description), - code=e.code - ) - + app.config.update(SQLALCHEMY_DATABASE_URI=db_connection_string,DEBUG=True, SQLALCHEMY_TRACK_MODIFICATIONS=False) + db.init_app(app) + migrate = Migrate(app, db) -@app.route("/", methods=['GET']) -def home(): - return render_template('home.html') + create_docs(app) + return app if __name__ == "__main__": - app.run() + create_app().run() else: + app=create_app() gunicorn_logger = logging.getLogger('gunicorn.error') app.logger.handlers = gunicorn_logger.handlers app.logger.setLevel(gunicorn_logger.level) diff --git a/auth.py b/auth.py new file mode 100644 index 0000000..38436ed --- /dev/null +++ b/auth.py @@ -0,0 +1,79 @@ + + +import os, sys, getpass +from os import environ as env + +#default values are only needed for some of these +db_username = env.get("DB_USERNAME") +db_password = env.get("DB_PASSWORD") +db_host = os.getenv("DB_HOST", "localhost") +db_name = os.getenv("DB_NAME", "classclock") + +connection_url = f'mysql+mysqlconnector://{db_username}:{db_password}@{db_host}/{db_name}?charset=utf8mb4&collation=utf8mb4_general_ci' +db_connection_string = os.getenv("DB_CONNECTION_URL", connection_url) + +db_connection_string = os.getenv("DATABASE_URL", connection_url) + +# elif sys.argv[1] == "demo": +# # https://stackoverflow.com/a/46541219 +# with app.app_context(): +# from common.db_schema import * +# import datetime +# import time + + +# def today_plus(sch, num_days): +# return BellScheduleDate(school_id = sch, date=datetime.date.today() + datetime.timedelta(days=num_days)) + +# s = School(owner_id="1234567890", full_name="Demonstration High School", acronym="DHS") + + +# sc1 = BellSchedule( +# school_id = s.id, +# name="Even Day", +# display_name= "Even", +# dates=[ +# today_plus(s.id, 0), +# today_plus(s.id, 2), +# today_plus(s.id, 4) +# ] +# ) + +# sc2 = BellSchedule( +# school_id = s.id, +# name="Odd Day", +# display_name="Odd", +# dates=[ +# today_plus(s.id, 1), +# today_plus(s.id, 3) +# ] +# ) + + +# s.schedules = [sc1,sc2] + + +# sc1.meeting_times = [ +# BellScheduleMeetingTime( +# bell_schedule_id =sc1.id, +# name="First Period", +# start_time=datetime.time(hour=8, minute=25, second=0, microsecond=0), +# end_time=datetime.time(9,25) +# ) +# ] + +# sc2.meeting_times = [ +# BellScheduleMeetingTime( +# bell_schedule_id =sc2.id, +# name="First Period", +# start_time=datetime.time(8,45), +# end_time=datetime.time(9,45) +# ) + +# ] + +# db.session.add(s) +# db.session.commit() +# print("Done.") +# exit(0) + \ No newline at end of file diff --git a/versions/__init__.py b/blueprints/__init__.py similarity index 100% rename from versions/__init__.py rename to blueprints/__init__.py diff --git a/blueprints/main.py b/blueprints/main.py new file mode 100644 index 0000000..ea23271 --- /dev/null +++ b/blueprints/main.py @@ -0,0 +1,20 @@ +from flask import Blueprint, render_template, abort +from jinja2 import TemplateNotFound +from werkzeug.exceptions import HTTPException + + +main_pages = Blueprint('main_pages', __name__, + template_folder='templates') + +@main_pages.route("/", methods=['GET']) +def home(): + return render_template('home.html') + + +@main_pages.errorhandler(HTTPException) +def handle_HTTP_error(e): + return respond( + make_error_object( + e.code, title=e.name, message=e.description), + code=e.code + ) \ No newline at end of file diff --git a/versions/v0.py b/blueprints/v0.py similarity index 70% rename from versions/v0.py rename to blueprints/v0.py index 470747a..6fe37fc 100644 --- a/versions/v0.py +++ b/blueprints/v0.py @@ -4,6 +4,8 @@ from flask import current_app, json from os import environ as env +from flask_limiter.util import get_remote_address +from common.helpers import respond from flask import Blueprint, abort, jsonify, request from werkzeug.exceptions import HTTPException from flask_cors import CORS @@ -33,7 +35,10 @@ blueprint = Blueprint('v0', __name__) -CORS(blueprint, origins=["https://web.classclock.app", "https://beta.web.classclock.app"], allow_headers=[ +flex_url = "http://localhost:3000" if env.get("FLASK_ENV") == 'development' else "classclock-*-moralcode.vercel.app" + + +CORS(blueprint, origins=["https://web.classclock.app", "https://beta.web.classclock.app", flex_url], allow_headers=[ "Accept", "Authorization", "Content-Type"]) # supports_credentials=True) @@ -80,12 +85,18 @@ # """ -@blueprint.route("/ping/", methods=['GET']) +@blueprint.route("/ping", strict_slashes=False, methods=['GET']) def ping(): + """ Returns the text "pong" as a connectivity check + --- + responses: + 200: + description: the text "pong" + """ return "pong" # TODO: add a search parameter -@blueprint.route("/schools/", methods=['GET']) +@blueprint.route("/schools", strict_slashes=False, methods=['GET']) @check_headers def list_schools(): """ Returns a list of schools @@ -100,12 +111,12 @@ def list_schools(): """ school_list = [] - schools = SchoolDB.query.all() + schools = SchoolDB.query.filter_by(soft_deleted=False).all() - return respond(SchoolSchema().dump(schools, many=True)) + return respond(SchoolSchema(exclude=('soft_deleted',)).dump(schools, many=True)) -@blueprint.route("/school//", methods=['GET']) +@blueprint.route("/school/", strict_slashes=False, methods=['GET']) @check_headers def get_school(school_id): """ Returns a single school @@ -130,7 +141,7 @@ def get_school(school_id): required: false """ - school = SchoolDB.query.filter_by(id=school_id).first() + school = SchoolDB.query.filter_by(id=school_id, soft_deleted=False).first() #double check this if school is None: raise Oops("No school was found with the specified id.", @@ -142,10 +153,10 @@ def get_school(school_id): if school.last_modified == since: return respond(code=304) #Not Modified - return respond(SchoolSchema().dump(school)) + return respond(SchoolSchema(exclude=('soft_deleted',)).dump(school)) -@blueprint.route("/school/", methods=['POST']) +@blueprint.route("/school", strict_slashes=False, methods=['POST']) @check_headers @requires_auth(permissions=[APIScopes.CREATE_SCHOOL]) @requires_admin @@ -192,10 +203,10 @@ def create_school(): #TODO: need to verify that the insert worked? - return respond(SchoolSchema().dump(new_object)) + return respond(SchoolSchema(exclude=('soft_deleted',)).dump(new_object)) -@blueprint.route("/school//", methods=['PATCH']) +@blueprint.route("/school/", strict_slashes=False, methods=['PATCH']) @check_headers @requires_auth(permissions=[APIScopes.EDIT_SCHOOL]) @requires_admin @@ -206,13 +217,17 @@ def update_school(school_id): security: - ApiKeyAuth: [] parameters: - - in: header - name: If-Unmodified-Since + - in: path + name: school_id schema: type: string - format: date - required: false - + length: 32 + required: true + - in: body + name: school + schema: + $ref: '#/definitions/School' + required: true """ data = get_request_body(request) @@ -220,7 +235,7 @@ def update_school(school_id): # if new_object.errors != {}: # return handle_marshmallow_errors(new_object.errors) - school = SchoolDB.query.filter_by(id=school_id).first() + school = SchoolDB.query.filter_by(id=school_id, soft_deleted=False).first() if school is None: raise Oops("No records could be updated because none were found", @@ -246,10 +261,10 @@ def update_school(school_id): db.session.commit() #TODO: need to verify that the update worked? - return respond(SchoolSchema().dump(school)) + return respond(SchoolSchema(exclude=('soft_deleted',)).dump(school)) -@blueprint.route("/school//", methods=['DELETE']) +@blueprint.route("/school/", strict_slashes=False, methods=['DELETE']) @check_headers @requires_auth(permissions=[APIScopes.DELETE_SCHOOL, APIScopes.DELETE_BELL_SCHEDULE]) @requires_admin @@ -275,7 +290,7 @@ def delete_school(school_id): """ - school = SchoolDB.query.filter_by(id=school_id).first() + school = SchoolDB.query.filter_by(id=school_id, soft_deleted=False).first() if school is None: raise Oops("No records could be deleted because none were found", 404, title="No Records Found") @@ -293,9 +308,31 @@ def delete_school(school_id): # should this just archive the school? or delete it and all related records? # sqlalchemy can be set to cascade deletes (i think). return None, 204 + +@blueprint.route("/bellschedules", strict_slashes=False, methods=['GET']) +@check_headers +@requires_auth#(permissions=[APIScopes.DELETE_SCHOOL, APIScopes.DELETE_BELL_SCHEDULE]) +@requires_admin +def list_owned_bellschedules(): + """ + gets a list of bell schedules that are part of schools that the current user owns + --- + security: + - ApiKeyAuth: [] + responses: + 200: + description: A list of bell schedules + schema: + $ref: '#/definitions/BellSchedule' + """ + #if get_api_user_id() not in school.owner_id + + schedules = BellScheduleDB.query.join(BellScheduleDB.school).filter(SchoolDB.owner_id==get_api_user_id(), SchoolDB.soft_deleted==False, BellScheduleDB.soft_deleted==False) + + return respond(BellScheduleSchema(exclude=('school_id','soft_deleted')).dump(schedules, many=True)) #TODO: add filtering for return values to reduce size of response. i.e. filter dates by after today, exclude meeting times if they havent changed -@blueprint.route("/bellschedules//", methods=['GET']) +@blueprint.route("/bellschedules/", strict_slashes=False, methods=['GET']) @check_headers def list_bellschedules(school_id): """ @@ -308,13 +345,22 @@ def list_bellschedules(school_id): type: string length: 32 required: true + responses: + 200: + description: A list of bell schedules + schema: + $ref: '#/definitions/BellSchedule' + """ - schedules = BellScheduleDB.query.filter_by(school_id=school_id) + schedules = BellScheduleDB.query.filter_by(school_id=school_id, soft_deleted=False) + + excluded_fields = exclude_unless_logged_in(['internal_description']) + excluded_fields.extend(('school_id',)) - return respond(BellScheduleSchema(exclude=('school_id',)).dump(schedules, many=True)) + return respond(BellScheduleSchema(exclude=excluded_fields).dump(schedules, many=True)) -@blueprint.route("/bellschedule//", methods=['GET']) +@blueprint.route("/bellschedule/", strict_slashes=False, methods=['GET']) @check_headers def get_bellschedule(bell_schedule_id): """ @@ -333,10 +379,15 @@ def get_bellschedule(bell_schedule_id): type: string format: date required: false + responses: + 200: + description: A single of bell schedule + schema: + $ref: '#/definitions/BellSchedule' """ schedule = BellScheduleDB.query.filter_by( - id=bell_schedule_id).first() + id=bell_schedule_id, soft_deleted=False).first() #double check this if schedule is None: @@ -349,33 +400,73 @@ def get_bellschedule(bell_schedule_id): if schedule.last_modified == since: return respond(code=304) #Not Modified - return respond(BellScheduleSchema().dump(schedule)) + excluded_fields = exclude_unless_logged_in(['internal_description']) + excluded_fields.extend(('soft_deleted',)) + + return respond(BellScheduleSchema(exclude=excluded_fields).dump(schedule)) -@blueprint.route("/bellschedule/", methods=['POST']) +@blueprint.route("/bellschedule", strict_slashes=False, methods=['POST']) @check_headers @requires_auth(permissions=[APIScopes.CREATE_BELL_SCHEDULE]) @requires_admin def create_bellschedule(): - + """ + Create a new bell schedule + --- + security: + - ApiKeyAuth: [] + parameters: + - in: body + name: schedule + schema: + $ref: '#/definitions/BellSchedule' + required: true + """ + request_data = get_request_body(request) + school_id= request_data["school_id"] # get school_id from a data parameter school = SchoolDB.query.filter_by(id=school_id).first() check_ownership(school) - new_schedule = BellScheduleSchema().load(get_request_body(request)).data + new_schedule = BellScheduleSchema().load(request_data, session=db.session) school.schedules.append(new_schedule) db.session.commit() - return respond(BellScheduleSchema(exclude=('school_id',)).dump(new_schedule)) + return respond(BellScheduleSchema(exclude=('school_id','soft_deleted')).dump(new_schedule)) -@blueprint.route("/bellschedule//", methods=['PATCH']) +@blueprint.route("/bellschedule/", strict_slashes=False, methods=['PATCH']) @check_headers @requires_auth(permissions=[APIScopes.EDIT_BELL_SCHEDULE]) @requires_admin def update_bellschedule(bell_schedule_id): + """ + Updates a bell schedule + --- + security: + - ApiKeyAuth: [] + parameters: + - in: path + name: bell_schedule_id + schema: + type: string + length: 32 + required: true + - in: body + name: schedule + schema: + $ref: '#/definitions/BellSchedule' + required: true + - in: header + name: If-Unmodified-Since + schema: + type: string + format: date + required: false + """ schedule = BellScheduleDB.query.filter_by(id=bell_schedule_id).first() school = SchoolDB.query.filter_by(id=schedule.school_id).first() @@ -401,7 +492,7 @@ def update_bellschedule(bell_schedule_id): db.session.commit() - return respond(BellScheduleSchema(exclude=('school_id',)).dump(schedule)) + return respond(BellScheduleSchema(exclude=('school_id','soft_deleted')).dump(schedule)) @blueprint.route("/bellschedule/", methods=['DELETE']) @@ -409,8 +500,27 @@ def update_bellschedule(bell_schedule_id): @requires_auth(permissions=[APIScopes.DELETE_BELL_SCHEDULE]) @requires_admin def delete_bellschedule(bell_schedule_id): + """ + deletes a bell schedule + --- + security: + - ApiKeyAuth: [] + parameters: + - in: path + name: bell_schedule_id + schema: + type: string + length: 32 + required: true + - in: header + name: If-Unmodified-Since + schema: + type: string + format: date + required: false + """ - schedule = BellScheduleDB.query.filter_by(id=bell_schedule_id).first() + schedule = BellScheduleDB.query.filter_by(id=bell_schedule_id, soft_deleted=False).first() school = SchoolDB.query.filter_by(id=schedule.school_id).first() if school is not None: check_ownership(school) @@ -419,9 +529,11 @@ def delete_bellschedule(bell_schedule_id): since = datetime.datetime.strptime(request.headers.get('If-Unmodified-Since'), HTTP_DATE_FORMAT) trap_object_modified_since(school.last_modified, since) - db.session.delete(schedule) + schedule.soft_deleted = True + # db.session.delete(schedule) + db.session.commit() - return None, 204 + return respond("success", code=204) # # Routes @@ -437,13 +549,17 @@ def delete_bellschedule(bell_schedule_id): @blueprint.before_request def before(): - current_app.logger.info( "Handling " + request.method + " Request for endpoint " + request.path + " from API user '" + get_api_user_id() + "' from address " + util.get_remote_address() ) - pass + current_app.logger.info(request.method + " " + request.path) @blueprint.after_request def after_request(response): response.headers['Content-Type'] = 'application/json' + if response.status_code != 200: + current_app.logger.info( "Handled request with HTTP status: " + str(response.status_code)) + + if response.status_code > 399: + current_app.logger.info(str(response.get_data())) return response # @@ -456,7 +572,8 @@ def after_request(response): # https://flask-limiter.readthedocs.io/en/stable/#custom-rate-limit-exceeded-responses @blueprint.errorhandler(429) def ratelimit_handler(e): - print(e) + current_app.logger.warning(e) + current_app.logger.warning("User at address " + get_remote_address() + "exceeded rate limit of " + e.description) return respond( make_error_object(429, title="Ratelimit Exceeded", message="ratelimit of " + e.description + " exceeded"), diff --git a/common/db_schema.py b/common/db_schema.py index 74cdbc6..c0bf213 100644 --- a/common/db_schema.py +++ b/common/db_schema.py @@ -22,7 +22,7 @@ class School(db.Model): primary_key=True, default=get_uuid) owner_id = db.Column('owner_id', db.VARCHAR(length=35)) full_name = db.Column('school_name', db.VARCHAR(length=75)) - schedules = db.relationship("BellSchedule") + schedules = db.relationship("BellSchedule",backref=db.backref("school")) acronym = db.Column( 'school_acronym', db.VARCHAR(length=75), nullable=True) alternate_freeperiod_name = db.Column( @@ -31,6 +31,7 @@ class School(db.Model): default=datetime.utcnow()) last_modified = db.Column('last_modified', db.DateTime, default=datetime.utcnow(), onupdate=datetime.utcnow) + soft_deleted = db.Column('soft_deleted', db.Boolean, nullable=False, default=False) class BellSchedule(db.Model): """ @@ -41,12 +42,15 @@ class BellSchedule(db.Model): primary_key=True, default=get_uuid) school_id = db.Column(HashColumn(length=32), ForeignKey(School.id)) full_name = db.Column('bell_schedule_name', db.VARCHAR(length=75)) - meeting_times = db.relationship("BellScheduleMeetingTime") + meeting_times = db.relationship("BellScheduleMeetingTime", cascade="save-update, merge,delete, delete-orphan") display_name = db.Column('bell_schedule_display_name', db.VARCHAR(length=75)) + audience = db.Column('audience', db.VARCHAR(length=75), nullable=False, server_default="everyone") + internal_description = db.Column('internal_description', db.VARCHAR(length=250), nullable=False, server_default="") creation_date = db.Column('creation_date', db.DateTime, default=datetime.utcnow()) last_modified = db.Column('last_modified', db.DateTime, default=datetime.utcnow(), onupdate=datetime.utcnow) + soft_deleted = db.Column('soft_deleted', db.Boolean, nullable=False, default=False) def get_uri(self, blueprint_name): # here the second time blueprint_name is called, it is acting like the api version number diff --git a/common/guid.py b/common/guid.py index ed95e8b..65736c2 100644 --- a/common/guid.py +++ b/common/guid.py @@ -1,5 +1,6 @@ from __future__ import absolute_import import uuid +from sqlalchemy.dialects.postgresql import UUID from sqlalchemy import types, func #https://docs.sqlalchemy.org/en/13/core/custom_types.html#backend-agnostic-guid-type @@ -16,8 +17,14 @@ def load_dialect_impl(self, dialect): def process_bind_param(self, value, dialect): if value is None: return value + elif dialect.name == 'postgresql': + return uuid.UUID(value).bytes else: - return uuid.UUID(hex=value).bytes + if not isinstance(value, uuid.UUID): + return uuid.UUID(value).bytes + else: + # bytes + return value.bytes def process_result_value(self, value, dialect): if value is None: diff --git a/common/helpers.py b/common/helpers.py index b723889..b5d8aeb 100644 --- a/common/helpers.py +++ b/common/helpers.py @@ -1,4 +1,4 @@ -from flask import _request_ctx_stack, request, url_for, make_response, jsonify +from flask import _request_ctx_stack, request, url_for, make_response, jsonify, current_app from werkzeug.wrappers import Response from functools import wraps from jose import jwt @@ -27,9 +27,10 @@ try: #TODO: remove dependency on setting up auth0 to test the API management_API = auth0management.Auth0ManagementService() -except: +except Exception as e: + current_app.logger.error(e) #TODO: need to implement better logging. - print('Auth0 is not configured correctly. Access control for requests will not be enforced.') + current_app.logger.warning('Auth0 is not configured correctly. Access control for requests will not be enforced.') management_API = None @@ -194,10 +195,6 @@ def get_request_body(request): """ return request.get_json() -def get_request_origin_identifier(): - user_id_parts = get_api_user_id().split("|") - return flask_limiter.util.get_remote_address() + get_api_client_id() + user_id_parts[1] if len(user_id_parts) > 2 else "" - def get_api_client_id(): """Returns a string to group API calls together for the purposes of ratelimiting @@ -213,6 +210,7 @@ def get_api_client_id(): def get_api_user_id(): """Returns the id of the user for whom data is being accessed on behalf of + This is only set for requests to endpoints using the @requires_auth decorator """ if hasattr(_request_ctx_stack.top, 'current_user'): raw_id = _request_ctx_stack.top.current_user["sub"] @@ -224,6 +222,13 @@ def get_api_user_id(): return "" +def exclude_unless_logged_in(fields: list): + is_admin = check_for_roles(["admin", "school admin"]) + if is_admin: + return [] + else: + return fields + def get_token_auth_header(): return get_valid_auth_header_of_type(AuthType.TOKEN) @@ -267,24 +272,42 @@ def check_permissions(user, permissions_to_check): raise AuthError( "You have not been granted the necessary permissions to access to this resource. You are missing the following permissions: " + perms_needed, 403) +def check_for_roles(roles:list, accept_any=True): + """Performs a simple, stupid, name-based check against the roles that a user has. + + This must be used after the @requires_auth decorator is applied -def check_for_role(role): + Args: + roles (list): a list of names of roles to check + accept_any (boolean): True to accept ANY of the provided roles. False to accept only ALL provided roles. Defaults to True. + + Returns: + bool: true if the user has any of the roles provided, false if the user has none of them, and None if there is no currently authenticated user + """ user_id = get_api_user_id() #TODO: make management API optional and check if it is present if management_API is None: #TODO: need to implement better logging. - print("Because Auth0 is not configured correctly, access control is not being enforced. All requests to check a users role will automatically pass.") + current_app.logger.warning("Because Auth0 is not configured correctly, access control is not being enforced. All requests to check a users role will automatically pass.") return True if user_id != "": - return role in management_API.get_roles_for_user(user_id) + roles_json = management_API.get_roles_for_user(user_id) + # current_app.logger.info(roles_json) + user_role_names = [r["name"].lower() for r in roles_json] + user_role_names = set(user_role_names) + requested_roles = set([r.lower() for r in roles]) + + required_threshold = 1 if accept_any else len(requested_roles) + + return len(requested_roles & user_role_names) >= required_threshold else: return None def check_ownership(school): if get_api_user_id() not in school.owner_id: - raise Oops("Authorizing user is not the owner of this school", 401) + raise Oops("Authorizing user does not have permission to access the requested school", 401) def list_owned_school_ids(cursor, school_id): @@ -313,7 +336,7 @@ def args_or_no(func): def decorated(*args, **kwargs): if not management_API: #TODO: need to implement better logging. - print("Because Auth0 is not configured correctly, access control is not being enforced. All requests to protected endpoints will automatically succeed.") + current_app.logger.warning("Because Auth0 is not configured correctly, access control is not being enforced. All requests to protected endpoints will automatically succeed.") return func(*args, **kwargs) token = get_token_auth_header() @@ -356,6 +379,8 @@ def decorated(*args, **kwargs): _request_ctx_stack.top.current_user = payload + current_app.logger.info( "Successfully authenticated user '" + get_api_user_id() + "'" ) + #this permissions check was added separately from the auth0 validation code if permissions is not None: check_permissions(payload, permissions) @@ -378,7 +403,7 @@ def requires_admin(f): @wraps(f) def decorated(*args, **kwargs): - is_admin = check_for_role("admin") + is_admin = check_for_roles(["admin", "school admin"]) if is_admin is None: raise Oops("There must be a user signed in to perform this action", 400, title="No User Authorization") diff --git a/common/services/auth0management.py b/common/services/auth0management.py index 9b1ed46..22adce6 100644 --- a/common/services/auth0management.py +++ b/common/services/auth0management.py @@ -1,49 +1,62 @@ -import http.client +import requests import json from os import environ as env - +import logging class Auth0ManagementService: base_path = "/api/v2" + base_url = "https://" + env.get("AUTH0_DOMAIN") + base_path headers = {"Content-Type": "application/json"} def __init__(self): - self.conn = http.client.HTTPSConnection("classclock.auth0.com") self.access_token = self.get_token() def get_user(self, user_id): - url = Auth0ManagementService.base_path + "/users/" + user_id + url = Auth0ManagementService.base_url + "/users/" + user_id heads = {**Auth0ManagementService.headers, ** {"Authorization": "Bearer " + self.access_token}} - self.conn.request("GET", url, "", heads) - - res = self.conn.getresponse() - data = res.read() + resp = requests.get(url, headers=heads) - return data.decode("utf-8") + data = resp.json() + return data # print(access_token) def get_roles_for_user(self, user_id): - url = Auth0ManagementService.base_path + "/users/" + user_id + "/roles" + url = Auth0ManagementService.base_url + "/users/" + user_id + "/roles" heads = {**Auth0ManagementService.headers, ** {"Authorization": "Bearer " + self.access_token}} - self.conn.request("GET", url, "", heads) + resp = requests.get(url, headers=heads) + data = resp.json() + if isinstance(data, list): + return data + elif (data.get("statusCode") == 401 and "xpired token" in data.get("message")): + self.access_token = self.get_token() + return self.get_roles_for_user(user_id) + else: + logging.error("encountered unexpected auth0 management API response") + logging.error(data) + return [] - res = self.conn.getresponse() - data = res.read() - - return data.decode("utf-8") def get_token(self): - payload = "{\"client_id\": \"" + env.get("AUTH0_CLIENT_ID") + "\" ,\"client_secret\": \"" + env.get( - "AUTH0_CLIENT_SECRET") + "\" ,\"audience\": \"https://classclock.auth0.com/api/v2/\",\"grant_type\": \"client_credentials\"}" - - self.conn.request("POST", "/oauth/token", payload, - Auth0ManagementService.headers) - res = self.conn.getresponse() - data = res.read() - return json.loads(data.decode("utf-8"))["access_token"] + # TODO: use the auth0 python SDK/lib for this https://github.com/auth0/auth0-python#management-sdk + # missing the trailing slash on the audience can cause problems: https://community.auth0.com/t/getting-service-not-enabled-within-domain-when-requesting-an-api-token/12634 + payload = { + "client_id": env.get("AUTH0_CLIENT_ID"), + "client_secret": env.get("AUTH0_CLIENT_SECRET"), + "audience": self.base_url + "/", + "grant_type": "client_credentials" + } + resp = requests.post("https://" + env.get("AUTH0_DOMAIN") + "/oauth/token", data=json.dumps(payload), headers=Auth0ManagementService.headers) + + data = resp.json() + if data.get("error"): + logging.error("failed to get auth0 management API access token") + logging.error(data) + return "" + else: + return data["access_token"] diff --git a/createdb.py b/createdb.py new file mode 100755 index 0000000..3c3e99e --- /dev/null +++ b/createdb.py @@ -0,0 +1,86 @@ +from common.db_schema import db +import argparse +from api import create_app +from flask_migrate import stamp + +parser = argparse.ArgumentParser(description='Set up a fresh ClassClock database.') +parser.add_argument('--demo', action='store_true', + help='Add some demo data to the database after creation') + +args = parser.parse_args() + + + +print("Beginning database creation...") +with create_app().app_context(): + print("Creating tables...") + db.create_all() + print("Committing...") + db.session.commit() + + + # then, load the migration configuration and generate the + # version table, "stamping" it with the most recent rev: + print("Stamping db version for future upgrades...") + stamp() + print("Done creating DB.") + + if args.demo: + print("Loading Demo Data...") + from common.db_schema import * + import datetime + import time + + + def today_plus(sch, num_days): + return BellScheduleDate(date=datetime.date.today() + datetime.timedelta(days=num_days)) + + s = School(owner_id="1234567890", full_name="Demonstration High School", acronym="DHS") + + + sc1 = BellSchedule( + full_name="Even Day", + display_name= "Even", + dates=[ + today_plus(s.id, 0), + today_plus(s.id, 2), + today_plus(s.id, 4) + ] + ) + + sc2 = BellSchedule( + full_name="Odd Day", + display_name="Odd", + dates=[ + today_plus(s.id, 1), + today_plus(s.id, 3) + ] + ) + + + s.schedules = [sc1,sc2] + + + sc1.meeting_times = [ + BellScheduleMeetingTime( + bell_schedule_id =sc1.id, + name="First Period", + start_time=datetime.time(hour=8, minute=25, second=0, microsecond=0), + end_time=datetime.time(9,25) + ) + ] + + sc2.meeting_times = [ + BellScheduleMeetingTime( + bell_schedule_id =sc2.id, + name="First Period", + start_time=datetime.time(8,45), + end_time=datetime.time(9,45) + ) + + ] + + db.session.add(s) + db.session.commit() + print("Done loading demo data.") + print("Done") \ No newline at end of file diff --git a/docs.py b/docs.py new file mode 100644 index 0000000..39314be --- /dev/null +++ b/docs.py @@ -0,0 +1,91 @@ +from flask import Blueprint, render_template, abort +from jinja2 import TemplateNotFound +from flasgger import APISpec, Schema, Swagger, fields +from apispec.ext.marshmallow import MarshmallowPlugin +from apispec_webframeworks.flask import FlaskPlugin +from common.schemas import * +from blueprints.v0 import * + + + +# docs = Blueprint('docs_pages', __name__, ) + +def create_docs(app): + + # Create an APISpec + # info: { + spec = APISpec( + title="ClassClock API", + version="0.3.3", + openapi_version='2.0', + + plugins=[ + FlaskPlugin(), + MarshmallowPlugin(), + ], + # options= { + # "description": "The development version of the ClassClock API", + # "servers": [ + # { + # "url": "https://api.classclock.app/v0", + # "description": "ClassClock API Server" + # }, + # { + # "url": "https://localhost:8000/v0", + # "description": "Dev server" + # } + # ], + # "externalDocs": { + # "description": "This API might loosely follow the JSON:API specofocation", + # "url": "https://jsonapi.org" + # }#, + # "contact": { + # # "responsibleOrganization": "ME", + # # "responsibleDeveloper": "Me", + # # "email": "me@me.com", + # # "url": "www.me.com", + # }, + # "termsOfService": "http://me.com/terms", + # } + ) + + + template = spec.to_flasgger( + app, + definitions=[SchoolSchema, BellScheduleSchema], + paths=[get_school, get_bellschedule ] + ) + + + # config={ + # "headers": [ + # ], + # "specs": [ + # { + # "endpoint": 'apispec_v1', + # "route": '/apispec_v1.json', + # "rule_filter": lambda rule: True, # all in + # "model_filter": lambda tag: True, # all in + # } + # ], + # "static_url_path": "/flasgger_static", + # # "static_folder": "static", # must be set by user + # "swagger_ui": True, + # # "specs_route": "/v0/docs/", + # "basePath": "/docs", + # "host": "api.classclock.app", # overrides localhost:500 + # "schemes": [ + # "https" + # ], + # "securityDefinitions": { + # "ApiKeyAuth": { + # "type": "apiKey", + # "in": "header", + # "name": "Authentication" + # } + # } + # }, + + # template["basePath"] = "/docs" + swagger = Swagger(app, template=template) + return swagger diff --git a/fly.toml b/fly.toml new file mode 100644 index 0000000..b313750 --- /dev/null +++ b/fly.toml @@ -0,0 +1,22 @@ +# fly.toml app configuration file generated for classclockapi on 2023-04-25T11:44:51-04:00 +# +# See https://fly.io/docs/reference/configuration/ for information about how to use this file. +# + +app = "classclockapi" +primary_region = "den" + +swap_size_mb = 512 + +[http_service] + internal_port = 8000 + force_https = true + auto_stop_machines = true + auto_start_machines = true + +[checks] + [checks.alive] + type = "tcp" + interval = "15s" + timeout = "2s" + grace_period = "5s" diff --git a/migrations/README b/migrations/README new file mode 100644 index 0000000..0e04844 --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 0000000..ec9d45c --- /dev/null +++ b/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..68feded --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,91 @@ +from __future__ import with_statement + +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option( + 'sqlalchemy.url', + str(current_app.extensions['migrate'].db.get_engine().url).replace( + '%', '%%')) +target_metadata = current_app.extensions['migrate'].db.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=target_metadata, literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + connectable = current_app.extensions['migrate'].db.get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=target_metadata, + process_revision_directives=process_revision_directives, + **current_app.extensions['migrate'].configure_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/2399c50496f7_add_soft_deletion.py b/migrations/versions/2399c50496f7_add_soft_deletion.py new file mode 100644 index 0000000..afd745b --- /dev/null +++ b/migrations/versions/2399c50496f7_add_soft_deletion.py @@ -0,0 +1,30 @@ +"""Add Soft Deletion + +Revision ID: 2399c50496f7 +Revises: 93d6649210e1 +Create Date: 2022-06-05 20:40:27.591102 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '2399c50496f7' +down_revision = '93d6649210e1' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('bellschedules', sa.Column('soft_deleted', sa.Boolean(), nullable=False, default=False)) + op.add_column('schools', sa.Column('soft_deleted', sa.Boolean(), nullable=False, default=False)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('schools', 'soft_deleted') + op.drop_column('bellschedules', 'soft_deleted') + # ### end Alembic commands ### diff --git a/migrations/versions/93d6649210e1_initial_migration.py b/migrations/versions/93d6649210e1_initial_migration.py new file mode 100644 index 0000000..87e0be1 --- /dev/null +++ b/migrations/versions/93d6649210e1_initial_migration.py @@ -0,0 +1,88 @@ +"""Initial migration. + +Revision ID: 93d6649210e1 +Revises: +Create Date: 2022-06-05 20:34:41.432858 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '93d6649210e1' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('bellscheduledates', 'creation_date', + existing_type=mysql.DATETIME(), + nullable=True) + op.drop_constraint('bellscheduledates_FK', 'bellscheduledates', type_='foreignkey') + op.drop_column('bellscheduledates', 'school_id') + op.alter_column('bellschedulemeetingtimes', 'creation_date', + existing_type=mysql.DATETIME(), + nullable=True) + op.drop_constraint('bellschedulemeetingtimes_FK', 'bellschedulemeetingtimes', type_='foreignkey') + op.drop_column('bellschedulemeetingtimes', 'school_id') + op.alter_column('bellschedules', 'bell_schedule_name', + existing_type=mysql.VARCHAR(charset='latin1', length=60), + nullable=True) + op.alter_column('bellschedules', 'creation_date', + existing_type=mysql.DATETIME(), + nullable=True) + op.alter_column('bellschedules', 'last_modified', + existing_type=mysql.DATETIME(), + nullable=True) + op.alter_column('schools', 'owner_id', + existing_type=mysql.VARCHAR(length=35), + nullable=True) + op.alter_column('schools', 'school_name', + existing_type=mysql.VARCHAR(charset='latin1', length=75), + nullable=True) + op.alter_column('schools', 'creation_date', + existing_type=mysql.DATETIME(), + nullable=True) + op.alter_column('schools', 'last_modified', + existing_type=mysql.DATETIME(), + nullable=True) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('schools', 'last_modified', + existing_type=mysql.DATETIME(), + nullable=False) + op.alter_column('schools', 'creation_date', + existing_type=mysql.DATETIME(), + nullable=False) + op.alter_column('schools', 'school_name', + existing_type=mysql.VARCHAR(charset='latin1', length=75), + nullable=False) + op.alter_column('schools', 'owner_id', + existing_type=mysql.VARCHAR(length=35), + nullable=False) + op.alter_column('bellschedules', 'last_modified', + existing_type=mysql.DATETIME(), + nullable=False) + op.alter_column('bellschedules', 'creation_date', + existing_type=mysql.DATETIME(), + nullable=False) + op.alter_column('bellschedules', 'bell_schedule_name', + existing_type=mysql.VARCHAR(charset='latin1', length=60), + nullable=False) + op.add_column('bellschedulemeetingtimes', sa.Column('school_id', sa.BINARY(length=16), nullable=False)) + op.create_foreign_key('bellschedulemeetingtimes_FK', 'bellschedulemeetingtimes', 'schools', ['school_id'], ['school_id']) + op.alter_column('bellschedulemeetingtimes', 'creation_date', + existing_type=mysql.DATETIME(), + nullable=False) + op.add_column('bellscheduledates', sa.Column('school_id', sa.BINARY(length=16), nullable=False)) + op.create_foreign_key('bellscheduledates_FK', 'bellscheduledates', 'schools', ['school_id'], ['school_id']) + op.alter_column('bellscheduledates', 'creation_date', + existing_type=mysql.DATETIME(), + nullable=False) + # ### end Alembic commands ### diff --git a/migrations/versions/9c3dd7f95cdc_add_audience_and_internal_description_.py b/migrations/versions/9c3dd7f95cdc_add_audience_and_internal_description_.py new file mode 100644 index 0000000..03fd596 --- /dev/null +++ b/migrations/versions/9c3dd7f95cdc_add_audience_and_internal_description_.py @@ -0,0 +1,30 @@ +"""add audience and internal description to bell schedule + +Revision ID: 9c3dd7f95cdc +Revises: 2399c50496f7 +Create Date: 2025-09-03 18:05:04.456307 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '9c3dd7f95cdc' +down_revision = '2399c50496f7' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('bellschedules', sa.Column('audience', sa.VARCHAR(length=75), server_default='everyone', nullable=False)) + op.add_column('bellschedules', sa.Column('internal_description', sa.VARCHAR(length=250), server_default='', nullable=False)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('bellschedules', 'internal_description') + op.drop_column('bellschedules', 'audience') + # ### end Alembic commands ### diff --git a/templates/home.html b/templates/home.html index 2b7d844..f6cb578 100644 --- a/templates/home.html +++ b/templates/home.html @@ -8,7 +8,7 @@

ClassClock API

This is the API for ClassClock.

If you came here looking for API docs, you can find them at the links below: