From 77b3e9c21f433df2a0fb484a9e20d2de64f6cfa2 Mon Sep 17 00:00:00 2001 From: rafsaf Date: Sun, 9 Jan 2022 18:58:05 +0100 Subject: [PATCH 01/11] initial folders, one for current version of template and another for new with fastapi-users --- .github/workflows/tests.yml | 2 +- .../nginx-unit-config.json | 15 - .../{ => template_fastapi_users}/.env.example | 0 .../.env.template | 0 .../{ => template_fastapi_users}/.flake8 | 0 .../{ => template_fastapi_users}/.gitignore | 0 .../{ => template_fastapi_users}/Dockerfile | 27 +- .../{ => template_fastapi_users}/alembic.ini | 0 .../alembic/README | 0 .../alembic/env.py | 4 +- .../alembic/script.py.mako | 0 .../2021_11_09_1736_init__cefce371682e.py | 0 .../app/__init__.py | 0 .../app/api/__init__.py | 0 .../app/api/api.py | 0 .../app/api/deps.py | 5 +- .../app/api/endpoints/__init__.py | 0 .../app/api/endpoints/auth.py | 7 +- .../app/api/endpoints/users.py | 0 .../app/conftest.py | 3 + .../app/core/__init__.py | 0 .../app/core/config.py | 19 +- .../app/core/security.py | 22 +- .../app/initial_data.py | 11 +- .../{ => template_fastapi_users}/app/main.py | 12 +- .../app/models.py | 2 +- .../app/schemas/__init__.py | 0 .../app/schemas/token.py | 0 .../app/schemas/user.py | 0 .../app/session.py | 8 +- .../app/tests/__init__.py | 0 .../app/tests/conftest.py | 9 +- .../app/tests/test_auth.py | 0 .../app/tests/utils.py | 0 .../docker-compose.yml | 0 .../{ => template_fastapi_users}/init.sh | 0 .../nginx-unit-config.json | 17 + .../{ => template_fastapi_users}/poetry.lock | 673 +++----- .../{ => template_fastapi_users}/pre-push.sh | 0 .../pyproject.toml | 33 +- .../requirements-dev.txt | 0 .../requirements.txt | 0 .../template_minimal/.env.example | 20 + .../template_minimal/.env.template | 20 + .../template_minimal/.flake8 | 11 + .../template_minimal/.gitignore | 136 ++ .../template_minimal/Dockerfile | 29 + .../template_minimal/alembic.ini | 100 ++ .../template_minimal/alembic/README | 1 + .../template_minimal/alembic/env.py | 96 ++ .../template_minimal/alembic/script.py.mako | 24 + .../2021_11_09_1736_init__cefce371682e.py | 38 + .../template_minimal/app/__init__.py | 0 .../template_minimal/app/api/__init__.py | 0 .../template_minimal/app/api/api.py | 7 + .../template_minimal/app/api/deps.py | 43 + .../app/api/endpoints/__init__.py | 0 .../app/api/endpoints/auth.py | 91 ++ .../app/api/endpoints/users.py | 41 + .../template_minimal/app/conftest.py | 13 + .../template_minimal/app/core/__init__.py | 0 .../template_minimal/app/core/config.py | 101 ++ .../template_minimal/app/core/security.py | 56 + .../template_minimal/app/initial_data.py | 45 + .../template_minimal/app/main.py | 29 + .../template_minimal/app/models.py | 20 + .../template_minimal/app/schemas/__init__.py | 2 + .../template_minimal/app/schemas/token.py | 21 + .../template_minimal/app/schemas/user.py | 26 + .../template_minimal/app/session.py | 12 + .../template_minimal/app/tests/__init__.py | 0 .../template_minimal/app/tests/conftest.py | 62 + .../template_minimal/app/tests/test_auth.py | 43 + .../template_minimal/app/tests/utils.py | 10 + .../template_minimal/docker-compose.yml | 36 + .../template_minimal/init.sh | 7 + .../template_minimal/nginx-unit-config.json | 17 + .../template_minimal/poetry.lock | 1453 +++++++++++++++++ .../template_minimal/pre-push.sh | 16 + .../template_minimal/pyproject.toml | 47 + .../template_minimal/requirements-dev.txt | 69 + .../template_minimal/requirements.txt | 37 + 82 files changed, 3150 insertions(+), 498 deletions(-) delete mode 100644 {{cookiecutter.project_name}}/nginx-unit-config.json rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/.env.example (100%) rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/.env.template (100%) rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/.flake8 (100%) rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/.gitignore (100%) rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/Dockerfile (77%) rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/alembic.ini (100%) rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/alembic/README (100%) rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/alembic/env.py (96%) rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/alembic/script.py.mako (100%) rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/alembic/versions/2021_11_09_1736_init__cefce371682e.py (100%) rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/app/__init__.py (100%) rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/app/api/__init__.py (100%) rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/app/api/api.py (100%) rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/app/api/deps.py (89%) rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/app/api/endpoints/__init__.py (100%) rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/app/api/endpoints/auth.py (95%) rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/app/api/endpoints/users.py (100%) rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/app/conftest.py (58%) rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/app/core/__init__.py (100%) rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/app/core/config.py (86%) rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/app/core/security.py (63%) rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/app/initial_data.py (75%) rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/app/main.py (61%) rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/app/models.py (90%) rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/app/schemas/__init__.py (100%) rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/app/schemas/token.py (100%) rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/app/schemas/user.py (100%) rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/app/session.py (56%) rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/app/tests/__init__.py (100%) rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/app/tests/conftest.py (85%) rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/app/tests/test_auth.py (100%) rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/app/tests/utils.py (100%) rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/docker-compose.yml (100%) rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/init.sh (100%) create mode 100644 {{cookiecutter.project_name}}/template_fastapi_users/nginx-unit-config.json rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/poetry.lock (55%) rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/pre-push.sh (100%) rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/pyproject.toml (53%) rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/requirements-dev.txt (100%) rename {{cookiecutter.project_name}}/{ => template_fastapi_users}/requirements.txt (100%) create mode 100644 {{cookiecutter.project_name}}/template_minimal/.env.example create mode 100644 {{cookiecutter.project_name}}/template_minimal/.env.template create mode 100644 {{cookiecutter.project_name}}/template_minimal/.flake8 create mode 100644 {{cookiecutter.project_name}}/template_minimal/.gitignore create mode 100644 {{cookiecutter.project_name}}/template_minimal/Dockerfile create mode 100644 {{cookiecutter.project_name}}/template_minimal/alembic.ini create mode 100644 {{cookiecutter.project_name}}/template_minimal/alembic/README create mode 100644 {{cookiecutter.project_name}}/template_minimal/alembic/env.py create mode 100644 {{cookiecutter.project_name}}/template_minimal/alembic/script.py.mako create mode 100644 {{cookiecutter.project_name}}/template_minimal/alembic/versions/2021_11_09_1736_init__cefce371682e.py create mode 100644 {{cookiecutter.project_name}}/template_minimal/app/__init__.py create mode 100644 {{cookiecutter.project_name}}/template_minimal/app/api/__init__.py create mode 100644 {{cookiecutter.project_name}}/template_minimal/app/api/api.py create mode 100644 {{cookiecutter.project_name}}/template_minimal/app/api/deps.py create mode 100644 {{cookiecutter.project_name}}/template_minimal/app/api/endpoints/__init__.py create mode 100644 {{cookiecutter.project_name}}/template_minimal/app/api/endpoints/auth.py create mode 100644 {{cookiecutter.project_name}}/template_minimal/app/api/endpoints/users.py create mode 100644 {{cookiecutter.project_name}}/template_minimal/app/conftest.py create mode 100644 {{cookiecutter.project_name}}/template_minimal/app/core/__init__.py create mode 100644 {{cookiecutter.project_name}}/template_minimal/app/core/config.py create mode 100644 {{cookiecutter.project_name}}/template_minimal/app/core/security.py create mode 100644 {{cookiecutter.project_name}}/template_minimal/app/initial_data.py create mode 100644 {{cookiecutter.project_name}}/template_minimal/app/main.py create mode 100644 {{cookiecutter.project_name}}/template_minimal/app/models.py create mode 100644 {{cookiecutter.project_name}}/template_minimal/app/schemas/__init__.py create mode 100644 {{cookiecutter.project_name}}/template_minimal/app/schemas/token.py create mode 100644 {{cookiecutter.project_name}}/template_minimal/app/schemas/user.py create mode 100644 {{cookiecutter.project_name}}/template_minimal/app/session.py create mode 100644 {{cookiecutter.project_name}}/template_minimal/app/tests/__init__.py create mode 100644 {{cookiecutter.project_name}}/template_minimal/app/tests/conftest.py create mode 100644 {{cookiecutter.project_name}}/template_minimal/app/tests/test_auth.py create mode 100644 {{cookiecutter.project_name}}/template_minimal/app/tests/utils.py create mode 100644 {{cookiecutter.project_name}}/template_minimal/docker-compose.yml create mode 100644 {{cookiecutter.project_name}}/template_minimal/init.sh create mode 100644 {{cookiecutter.project_name}}/template_minimal/nginx-unit-config.json create mode 100644 {{cookiecutter.project_name}}/template_minimal/poetry.lock create mode 100644 {{cookiecutter.project_name}}/template_minimal/pre-push.sh create mode 100644 {{cookiecutter.project_name}}/template_minimal/pyproject.toml create mode 100644 {{cookiecutter.project_name}}/template_minimal/requirements-dev.txt create mode 100644 {{cookiecutter.project_name}}/template_minimal/requirements.txt diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ced0463..08cae0d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - python-version: [3.7, 3.8, 3.9] + python-version: [3.9] services: postgres: diff --git a/{{cookiecutter.project_name}}/nginx-unit-config.json b/{{cookiecutter.project_name}}/nginx-unit-config.json deleted file mode 100644 index 75ae1c2..0000000 --- a/{{cookiecutter.project_name}}/nginx-unit-config.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "listeners": { - "*:80": { - "pass": "applications/fastapi" - } - }, - "applications": { - "fastapi": { - "type": "python 3.9", - "path": "/build/", - "module": "app.main", - "callable": "app" - } - } -} \ No newline at end of file diff --git a/{{cookiecutter.project_name}}/.env.example b/{{cookiecutter.project_name}}/template_fastapi_users/.env.example similarity index 100% rename from {{cookiecutter.project_name}}/.env.example rename to {{cookiecutter.project_name}}/template_fastapi_users/.env.example diff --git a/{{cookiecutter.project_name}}/.env.template b/{{cookiecutter.project_name}}/template_fastapi_users/.env.template similarity index 100% rename from {{cookiecutter.project_name}}/.env.template rename to {{cookiecutter.project_name}}/template_fastapi_users/.env.template diff --git a/{{cookiecutter.project_name}}/.flake8 b/{{cookiecutter.project_name}}/template_fastapi_users/.flake8 similarity index 100% rename from {{cookiecutter.project_name}}/.flake8 rename to {{cookiecutter.project_name}}/template_fastapi_users/.flake8 diff --git a/{{cookiecutter.project_name}}/.gitignore b/{{cookiecutter.project_name}}/template_fastapi_users/.gitignore similarity index 100% rename from {{cookiecutter.project_name}}/.gitignore rename to {{cookiecutter.project_name}}/template_fastapi_users/.gitignore diff --git a/{{cookiecutter.project_name}}/Dockerfile b/{{cookiecutter.project_name}}/template_fastapi_users/Dockerfile similarity index 77% rename from {{cookiecutter.project_name}}/Dockerfile rename to {{cookiecutter.project_name}}/template_fastapi_users/Dockerfile index 1a5fb4a..1461692 100644 --- a/{{cookiecutter.project_name}}/Dockerfile +++ b/{{cookiecutter.project_name}}/template_fastapi_users/Dockerfile @@ -1,26 +1,29 @@ # See https://unit.nginx.org/installation/#docker-images -FROM nginx/unit:1.25.0-python3.9 +FROM nginx/unit:1.26.1-python3.9 ENV PYTHONUNBUFFERED 1 -# Nginx unit config and init.sh will be consumed at container startup. -COPY ./app/init.sh /docker-entrypoint.d/init.sh -COPY ./nginx-unit-config.json /docker-entrypoint.d/config.json -RUN chmod +x /docker-entrypoint.d/init.sh +RUN apt update && apt install -y python3-pip # Build folder for our app, only stuff that matters copied. RUN mkdir build WORKDIR /build -COPY ./app ./app -COPY ./alembic ./alembic -COPY ./alembic.ini . +# Update, install requirements and then cleanup. COPY ./requirements.txt . -# Update, install requirements and then cleanup. -RUN apt update && apt install -y python3-pip \ - && pip3 install -r requirements.txt \ +RUN pip3 install -r requirements.txt \ && apt remove -y python3-pip \ && apt autoremove --purge -y \ - && rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/*.list \ No newline at end of file + && rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/*.list + +# Copy the rest of app +COPY ./app ./app +COPY ./alembic ./alembic +COPY ./alembic.ini . + +# Nginx unit config and init.sh will be consumed at container startup. +COPY ./app/init.sh /docker-entrypoint.d/init.sh +COPY ./nginx-unit-config.json /docker-entrypoint.d/config.json +RUN chmod a+x /docker-entrypoint.d/init.sh \ No newline at end of file diff --git a/{{cookiecutter.project_name}}/alembic.ini b/{{cookiecutter.project_name}}/template_fastapi_users/alembic.ini similarity index 100% rename from {{cookiecutter.project_name}}/alembic.ini rename to {{cookiecutter.project_name}}/template_fastapi_users/alembic.ini diff --git a/{{cookiecutter.project_name}}/alembic/README b/{{cookiecutter.project_name}}/template_fastapi_users/alembic/README similarity index 100% rename from {{cookiecutter.project_name}}/alembic/README rename to {{cookiecutter.project_name}}/template_fastapi_users/alembic/README diff --git a/{{cookiecutter.project_name}}/alembic/env.py b/{{cookiecutter.project_name}}/template_fastapi_users/alembic/env.py similarity index 96% rename from {{cookiecutter.project_name}}/alembic/env.py rename to {{cookiecutter.project_name}}/template_fastapi_users/alembic/env.py index 3601bbc..6e02264 100644 --- a/{{cookiecutter.project_name}}/alembic/env.py +++ b/{{cookiecutter.project_name}}/template_fastapi_users/alembic/env.py @@ -5,7 +5,7 @@ from sqlalchemy.ext.asyncio import AsyncEngine from asyncio import get_event_loop -from app.core.config import settings +from app.core import config as app_config from alembic import context # this is the Alembic Config object, which provides @@ -31,7 +31,7 @@ def get_database_uri(): - return settings.DEFAULT_SQLALCHEMY_DATABASE_URI + return app_config.settings.DEFAULT_SQLALCHEMY_DATABASE_URI def run_migrations_offline(): diff --git a/{{cookiecutter.project_name}}/alembic/script.py.mako b/{{cookiecutter.project_name}}/template_fastapi_users/alembic/script.py.mako similarity index 100% rename from {{cookiecutter.project_name}}/alembic/script.py.mako rename to {{cookiecutter.project_name}}/template_fastapi_users/alembic/script.py.mako diff --git a/{{cookiecutter.project_name}}/alembic/versions/2021_11_09_1736_init__cefce371682e.py b/{{cookiecutter.project_name}}/template_fastapi_users/alembic/versions/2021_11_09_1736_init__cefce371682e.py similarity index 100% rename from {{cookiecutter.project_name}}/alembic/versions/2021_11_09_1736_init__cefce371682e.py rename to {{cookiecutter.project_name}}/template_fastapi_users/alembic/versions/2021_11_09_1736_init__cefce371682e.py diff --git a/{{cookiecutter.project_name}}/app/__init__.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/__init__.py similarity index 100% rename from {{cookiecutter.project_name}}/app/__init__.py rename to {{cookiecutter.project_name}}/template_fastapi_users/app/__init__.py diff --git a/{{cookiecutter.project_name}}/app/api/__init__.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/api/__init__.py similarity index 100% rename from {{cookiecutter.project_name}}/app/api/__init__.py rename to {{cookiecutter.project_name}}/template_fastapi_users/app/api/__init__.py diff --git a/{{cookiecutter.project_name}}/app/api/api.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/api/api.py similarity index 100% rename from {{cookiecutter.project_name}}/app/api/api.py rename to {{cookiecutter.project_name}}/template_fastapi_users/app/api/api.py diff --git a/{{cookiecutter.project_name}}/app/api/deps.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/api/deps.py similarity index 89% rename from {{cookiecutter.project_name}}/app/api/deps.py rename to {{cookiecutter.project_name}}/template_fastapi_users/app/api/deps.py index b045afc..a8fc916 100644 --- a/{{cookiecutter.project_name}}/app/api/deps.py +++ b/{{cookiecutter.project_name}}/template_fastapi_users/app/api/deps.py @@ -8,8 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from app import schemas -from app.core import security -from app.core.config import settings +from app.core import security, config from app.models import User from app.session import async_session @@ -27,7 +26,7 @@ async def get_current_user( try: payload = jwt.decode( - token, settings.SECRET_KEY, algorithms=[security.ALGORITHM] + token, config.settings.SECRET_KEY, algorithms=[security.ALGORITHM] ) token_data = schemas.TokenPayload(**payload) except (jwt.JWTError, ValidationError): diff --git a/{{cookiecutter.project_name}}/app/api/endpoints/__init__.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/api/endpoints/__init__.py similarity index 100% rename from {{cookiecutter.project_name}}/app/api/endpoints/__init__.py rename to {{cookiecutter.project_name}}/template_fastapi_users/app/api/endpoints/__init__.py diff --git a/{{cookiecutter.project_name}}/app/api/endpoints/auth.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/api/endpoints/auth.py similarity index 95% rename from {{cookiecutter.project_name}}/app/api/endpoints/auth.py rename to {{cookiecutter.project_name}}/template_fastapi_users/app/api/endpoints/auth.py index bb0e528..90706bb 100644 --- a/{{cookiecutter.project_name}}/app/api/endpoints/auth.py +++ b/{{cookiecutter.project_name}}/template_fastapi_users/app/api/endpoints/auth.py @@ -9,8 +9,7 @@ from app import schemas from app.api import deps -from app.core import security -from app.core.config import settings +from app.core import security, config from app.models import User router = APIRouter() @@ -60,7 +59,9 @@ async def refresh_token( """ try: payload = jwt.decode( - input.refresh_token, settings.SECRET_KEY, algorithms=[security.ALGORITHM] + input.refresh_token, + config.settings.SECRET_KEY, + algorithms=[security.ALGORITHM], ) token_data = schemas.TokenPayload(**payload) except (jwt.JWTError, ValidationError): diff --git a/{{cookiecutter.project_name}}/app/api/endpoints/users.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/api/endpoints/users.py similarity index 100% rename from {{cookiecutter.project_name}}/app/api/endpoints/users.py rename to {{cookiecutter.project_name}}/template_fastapi_users/app/api/endpoints/users.py diff --git a/{{cookiecutter.project_name}}/app/conftest.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/conftest.py similarity index 58% rename from {{cookiecutter.project_name}}/app/conftest.py rename to {{cookiecutter.project_name}}/template_fastapi_users/app/conftest.py index b6ef2c3..ea7b91d 100644 --- a/{{cookiecutter.project_name}}/app/conftest.py +++ b/{{cookiecutter.project_name}}/template_fastapi_users/app/conftest.py @@ -7,4 +7,7 @@ import os +# This will ensure using test database os.environ["ENVIRONMENT"] = "PYTEST" +# This will change default 12 bcrypt rounds to only 1 so hashing password func will be short +os.environ["SECURITY_BCRYPT_DEFAULT_ROUNDS"] = "1" diff --git a/{{cookiecutter.project_name}}/app/core/__init__.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/core/__init__.py similarity index 100% rename from {{cookiecutter.project_name}}/app/core/__init__.py rename to {{cookiecutter.project_name}}/template_fastapi_users/app/core/__init__.py diff --git a/{{cookiecutter.project_name}}/app/core/config.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/core/config.py similarity index 86% rename from {{cookiecutter.project_name}}/app/core/config.py rename to {{cookiecutter.project_name}}/template_fastapi_users/app/core/config.py index 8aa3c2c..4f0a92e 100644 --- a/{{cookiecutter.project_name}}/app/core/config.py +++ b/{{cookiecutter.project_name}}/template_fastapi_users/app/core/config.py @@ -20,17 +20,11 @@ """ from pathlib import Path -from typing import Dict, List, Union +from typing import Union, Literal import toml from pydantic import AnyHttpUrl, AnyUrl, BaseSettings, EmailStr, validator -# Literal from typing_extensions for python 3.7 support, remove if not needed -try: - from typing import Literal -except ImportError: - from typing_extensions import Literal -# PROJECT_DIR = Path(__file__).parent.parent.parent pyproject_content = toml.load(f"{PROJECT_DIR}/pyproject.toml")["tool"]["poetry"] @@ -41,8 +35,9 @@ class Settings(BaseSettings): SECRET_KEY: str ENVIRONMENT: Literal["DEV", "PYTEST", "STAGE", "PRODUCTION"] ACCESS_TOKEN_EXPIRE_MINUTES: int + SECURITY_BCRYPT_DEFAULT_ROUNDS: int = 12 REFRESH_TOKEN_EXPIRE_MINUTES: int - BACKEND_CORS_ORIGINS: Union[str, List[AnyHttpUrl]] + BACKEND_CORS_ORIGINS: Union[str, list[AnyHttpUrl]] # PROJECT NAME, VERSION AND DESCRIPTION PROJECT_NAME: str = pyproject_content["name"] @@ -71,13 +66,13 @@ class Settings(BaseSettings): # VALIDATORS @validator("BACKEND_CORS_ORIGINS") - def _assemble_cors_origins(cls, cors_origins: Union[str, List[AnyHttpUrl]]): + def _assemble_cors_origins(cls, cors_origins: Union[str, list[AnyHttpUrl]]): if isinstance(cors_origins, str): return [item.strip() for item in cors_origins.split(",")] return cors_origins @validator("DEFAULT_SQLALCHEMY_DATABASE_URI") - def _assemble_default_db_connection(cls, v: str, values: Dict[str, str]) -> str: + def _assemble_default_db_connection(cls, v: str, values: dict[str, str]) -> str: return AnyUrl.build( scheme="postgresql+asyncpg", user=values["DEFAULT_DATABASE_USER"], @@ -88,7 +83,7 @@ def _assemble_default_db_connection(cls, v: str, values: Dict[str, str]) -> str: ) @validator("TEST_SQLALCHEMY_DATABASE_URI") - def _assemble_test_db_connection(cls, v: str, values: Dict[str, str]) -> str: + def _assemble_test_db_connection(cls, v: str, values: dict[str, str]) -> str: return AnyUrl.build( scheme="postgresql+asyncpg", user=values["TEST_DATABASE_USER"], @@ -103,4 +98,4 @@ class Config: case_sensitive = True -settings: Settings = Settings() +settings: Settings = Settings() # type: ignore diff --git a/{{cookiecutter.project_name}}/app/core/security.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/core/security.py similarity index 63% rename from {{cookiecutter.project_name}}/app/core/security.py rename to {{cookiecutter.project_name}}/template_fastapi_users/app/core/security.py index adec6aa..de362f6 100644 --- a/{{cookiecutter.project_name}}/app/core/security.py +++ b/{{cookiecutter.project_name}}/template_fastapi_users/app/core/security.py @@ -5,40 +5,44 @@ """ from datetime import datetime, timedelta -from typing import Any, Tuple, Union +from typing import Any, Union from jose import jwt from passlib.context import CryptContext -from app.core.config import settings +from app.core import config -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +pwd_context = CryptContext( + schemes=["bcrypt"], + deprecated="auto", + bcrypt__rounds=config.settings.SECURITY_BCRYPT_DEFAULT_ROUNDS, +) ALGORITHM = "HS256" -def create_access_token(subject: Union[str, Any]) -> Tuple[str, datetime]: +def create_access_token(subject: Union[str, Any]) -> tuple[str, datetime]: now = datetime.utcnow() - expire = now + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) + expire = now + timedelta(minutes=config.settings.ACCESS_TOKEN_EXPIRE_MINUTES) to_encode = {"exp": expire, "sub": str(subject), "refresh": False} encoded_jwt: str = jwt.encode( to_encode, - settings.SECRET_KEY, + config.settings.SECRET_KEY, algorithm=ALGORITHM, ) return encoded_jwt, expire -def create_refresh_token(subject: Union[str, Any]) -> Tuple[str, datetime]: +def create_refresh_token(subject: Union[str, Any]) -> tuple[str, datetime]: now = datetime.utcnow() - expire = now + timedelta(minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES) + expire = now + timedelta(minutes=config.settings.REFRESH_TOKEN_EXPIRE_MINUTES) to_encode = {"exp": expire, "sub": str(subject), "refresh": True} encoded_jwt: str = jwt.encode( to_encode, - settings.SECRET_KEY, + config.settings.SECRET_KEY, algorithm=ALGORITHM, ) return encoded_jwt, expire diff --git a/{{cookiecutter.project_name}}/app/initial_data.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/initial_data.py similarity index 75% rename from {{cookiecutter.project_name}}/app/initial_data.py rename to {{cookiecutter.project_name}}/template_fastapi_users/app/initial_data.py index 0fd073b..1f36897 100644 --- a/{{cookiecutter.project_name}}/app/initial_data.py +++ b/{{cookiecutter.project_name}}/template_fastapi_users/app/initial_data.py @@ -10,8 +10,7 @@ from sqlalchemy import select -from app.core import security -from app.core.config import settings +from app.core import security, config from app.models import User from app.session import async_session @@ -21,17 +20,17 @@ async def main() -> None: async with async_session() as session: result = await session.execute( - select(User).where(User.email == settings.FIRST_SUPERUSER_EMAIL) + select(User).where(User.email == config.settings.FIRST_SUPERUSER_EMAIL) ) user: Optional[User] = result.scalars().first() if user is None: new_superuser = User( - email=settings.FIRST_SUPERUSER_EMAIL, + email=config.settings.FIRST_SUPERUSER_EMAIL, hashed_password=security.get_password_hash( - settings.FIRST_SUPERUSER_PASSWORD + config.settings.FIRST_SUPERUSER_PASSWORD ), - full_name=settings.FIRST_SUPERUSER_EMAIL, + full_name=config.settings.FIRST_SUPERUSER_EMAIL, ) session.add(new_superuser) await session.commit() diff --git a/{{cookiecutter.project_name}}/app/main.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/main.py similarity index 61% rename from {{cookiecutter.project_name}}/app/main.py rename to {{cookiecutter.project_name}}/template_fastapi_users/app/main.py index 6d0760f..0892233 100644 --- a/{{cookiecutter.project_name}}/app/main.py +++ b/{{cookiecutter.project_name}}/template_fastapi_users/app/main.py @@ -6,21 +6,21 @@ from fastapi.middleware.cors import CORSMiddleware from app.api.api import api_router -from app.core.config import settings +from app.core import config app = FastAPI( - title=settings.PROJECT_NAME, - version=settings.VERSION, - description=settings.DESCRIPTION, + title=config.settings.PROJECT_NAME, + version=config.settings.VERSION, + description=config.settings.DESCRIPTION, openapi_url="/openapi.json", docs_url="/", ) # Set all CORS enabled origins -if settings.BACKEND_CORS_ORIGINS: +if config.settings.BACKEND_CORS_ORIGINS: app.add_middleware( CORSMiddleware, - allow_origins=[str(origin) for origin in settings.BACKEND_CORS_ORIGINS], + allow_origins=[str(origin) for origin in config.settings.BACKEND_CORS_ORIGINS], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], diff --git a/{{cookiecutter.project_name}}/app/models.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/models.py similarity index 90% rename from {{cookiecutter.project_name}}/app/models.py rename to {{cookiecutter.project_name}}/template_fastapi_users/app/models.py index edb4bde..2b7eb82 100644 --- a/{{cookiecutter.project_name}}/app/models.py +++ b/{{cookiecutter.project_name}}/template_fastapi_users/app/models.py @@ -7,7 +7,7 @@ from typing import Any, cast from sqlalchemy import Column, Integer, String -from sqlalchemy.orm import declarative_base +from sqlalchemy.orm.decl_api import declarative_base Base = cast(Any, declarative_base()) diff --git a/{{cookiecutter.project_name}}/app/schemas/__init__.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/schemas/__init__.py similarity index 100% rename from {{cookiecutter.project_name}}/app/schemas/__init__.py rename to {{cookiecutter.project_name}}/template_fastapi_users/app/schemas/__init__.py diff --git a/{{cookiecutter.project_name}}/app/schemas/token.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/schemas/token.py similarity index 100% rename from {{cookiecutter.project_name}}/app/schemas/token.py rename to {{cookiecutter.project_name}}/template_fastapi_users/app/schemas/token.py diff --git a/{{cookiecutter.project_name}}/app/schemas/user.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/schemas/user.py similarity index 100% rename from {{cookiecutter.project_name}}/app/schemas/user.py rename to {{cookiecutter.project_name}}/template_fastapi_users/app/schemas/user.py diff --git a/{{cookiecutter.project_name}}/app/session.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/session.py similarity index 56% rename from {{cookiecutter.project_name}}/app/session.py rename to {{cookiecutter.project_name}}/template_fastapi_users/app/session.py index 98282e4..5b27e40 100644 --- a/{{cookiecutter.project_name}}/app/session.py +++ b/{{cookiecutter.project_name}}/template_fastapi_users/app/session.py @@ -1,12 +1,12 @@ from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine from sqlalchemy.orm.session import sessionmaker -from app.core.config import settings +from app.core import config -if settings.ENVIRONMENT == "PYTEST": - sqlalchemy_database_uri = settings.TEST_SQLALCHEMY_DATABASE_URI +if config.settings.ENVIRONMENT == "PYTEST": + sqlalchemy_database_uri = config.settings.TEST_SQLALCHEMY_DATABASE_URI else: - sqlalchemy_database_uri = settings.DEFAULT_SQLALCHEMY_DATABASE_URI + sqlalchemy_database_uri = config.settings.DEFAULT_SQLALCHEMY_DATABASE_URI async_engine = create_async_engine(sqlalchemy_database_uri, pool_pre_ping=True) async_session = sessionmaker(async_engine, expire_on_commit=False, class_=AsyncSession) diff --git a/{{cookiecutter.project_name}}/app/tests/__init__.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/tests/__init__.py similarity index 100% rename from {{cookiecutter.project_name}}/app/tests/__init__.py rename to {{cookiecutter.project_name}}/template_fastapi_users/app/tests/__init__.py diff --git a/{{cookiecutter.project_name}}/app/tests/conftest.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/tests/conftest.py similarity index 85% rename from {{cookiecutter.project_name}}/app/tests/conftest.py rename to {{cookiecutter.project_name}}/template_fastapi_users/app/tests/conftest.py index 4125a1a..2707184 100644 --- a/{{cookiecutter.project_name}}/app/tests/conftest.py +++ b/{{cookiecutter.project_name}}/template_fastapi_users/app/tests/conftest.py @@ -6,8 +6,7 @@ from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from app.core.config import settings -from app.core.security import get_password_hash +from app.core import security, config from app.main import app from app.models import Base, User from app.session import async_engine, async_session @@ -29,8 +28,8 @@ async def client(): @pytest.fixture(scope="session") async def test_db_setup_sessionmaker(): # assert if we use TEST_DB URL for 100% - assert settings.ENVIRONMENT == "PYTEST" - assert str(async_engine.url) == settings.TEST_SQLALCHEMY_DATABASE_URI + assert config.settings.ENVIRONMENT == "PYTEST" + assert str(async_engine.url) == config.settings.TEST_SQLALCHEMY_DATABASE_URI # always drop and create test db tables between tests session async with async_engine.begin() as conn: @@ -53,7 +52,7 @@ async def default_user(session: AsyncSession): if user is None: new_user = User( email="user@email.com", - hashed_password=get_password_hash("password"), + hashed_password=security.get_password_hash("password"), full_name="fullname", ) session.add(new_user) diff --git a/{{cookiecutter.project_name}}/app/tests/test_auth.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/tests/test_auth.py similarity index 100% rename from {{cookiecutter.project_name}}/app/tests/test_auth.py rename to {{cookiecutter.project_name}}/template_fastapi_users/app/tests/test_auth.py diff --git a/{{cookiecutter.project_name}}/app/tests/utils.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/tests/utils.py similarity index 100% rename from {{cookiecutter.project_name}}/app/tests/utils.py rename to {{cookiecutter.project_name}}/template_fastapi_users/app/tests/utils.py diff --git a/{{cookiecutter.project_name}}/docker-compose.yml b/{{cookiecutter.project_name}}/template_fastapi_users/docker-compose.yml similarity index 100% rename from {{cookiecutter.project_name}}/docker-compose.yml rename to {{cookiecutter.project_name}}/template_fastapi_users/docker-compose.yml diff --git a/{{cookiecutter.project_name}}/init.sh b/{{cookiecutter.project_name}}/template_fastapi_users/init.sh similarity index 100% rename from {{cookiecutter.project_name}}/init.sh rename to {{cookiecutter.project_name}}/template_fastapi_users/init.sh diff --git a/{{cookiecutter.project_name}}/template_fastapi_users/nginx-unit-config.json b/{{cookiecutter.project_name}}/template_fastapi_users/nginx-unit-config.json new file mode 100644 index 0000000..dda3653 --- /dev/null +++ b/{{cookiecutter.project_name}}/template_fastapi_users/nginx-unit-config.json @@ -0,0 +1,17 @@ +{ + "listeners": { + "*:80": { + "pass": "applications/fastapi" + } + }, + "applications": { + "fastapi": { + "type": "python 3.9", + "processes": 1, + "threads": 1, + "path": "/build/", + "module": "app.main", + "callable": "app" + } + } +} diff --git a/{{cookiecutter.project_name}}/poetry.lock b/{{cookiecutter.project_name}}/template_fastapi_users/poetry.lock similarity index 55% rename from {{cookiecutter.project_name}}/poetry.lock rename to {{cookiecutter.project_name}}/template_fastapi_users/poetry.lock index 3d936b7..2062419 100644 --- a/{{cookiecutter.project_name}}/poetry.lock +++ b/{{cookiecutter.project_name}}/template_fastapi_users/poetry.lock @@ -19,7 +19,7 @@ tz = ["python-dateutil"] name = "anyio" version = "3.4.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" -category = "dev" +category = "main" optional = false python-versions = ">=3.6.2" @@ -49,7 +49,7 @@ tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] [[package]] name = "asyncpg" -version = "0.24.0" +version = "0.25.0" description = "An asyncio PostgreSQL driver" category = "main" optional = false @@ -73,17 +73,17 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "attrs" -version = "21.2.0" +version = "21.4.0" description = "Classes Without Boilerplate" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] [[package]] name = "autoflake" @@ -114,7 +114,7 @@ typecheck = ["mypy"] [[package]] name = "black" -version = "21.11b1" +version = "21.12b0" description = "The uncompromising code formatter." category = "dev" optional = false @@ -125,7 +125,6 @@ click = ">=7.1.2" mypy-extensions = ">=0.4.3" pathspec = ">=0.9.0,<1" platformdirs = ">=2" -regex = ">=2021.4.4" tomli = ">=0.2.6,<2.0.0" typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} typing-extensions = [ @@ -161,7 +160,7 @@ pycparser = "*" [[package]] name = "charset-normalizer" -version = "2.0.8" +version = "2.0.9" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false @@ -192,18 +191,18 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "coverage" -version = "5.5" +version = "6.2" description = "Code coverage measurement for Python" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +python-versions = ">=3.6" [package.extras] -toml = ["toml"] +toml = ["tomli"] [[package]] name = "cryptography" -version = "36.0.0" +version = "36.0.1" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false @@ -264,7 +263,7 @@ idna = ">=2.0.0" [[package]] name = "fastapi" -version = "0.68.2" +version = "0.71.0" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" category = "main" optional = false @@ -272,27 +271,27 @@ python-versions = ">=3.6.1" [package.dependencies] pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" -starlette = "0.14.2" +starlette = "0.17.1" [package.extras] -all = ["requests (>=2.24.0,<3.0.0)", "aiofiles (>=0.5.0,<0.8.0)", "jinja2 (>=2.11.2,<3.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "itsdangerous (>=1.1.0,<2.0.0)", "pyyaml (>=5.3.1,<6.0.0)", "graphene (>=2.1.8,<3.0.0)", "ujson (>=4.0.1,<5.0.0)", "orjson (>=3.2.1,<4.0.0)", "email_validator (>=1.1.1,<2.0.0)", "uvicorn[standard] (>=0.12.0,<0.16.0)", "async_exit_stack (>=1.0.1,<2.0.0)", "async_generator (>=1.10,<2.0.0)"] -dev = ["python-jose[cryptography] (>=3.3.0,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "uvicorn[standard] (>=0.12.0,<0.16.0)", "graphene (>=2.1.8,<3.0.0)"] -doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=7.1.9,<8.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "typer-cli (>=0.0.12,<0.0.13)", "pyyaml (>=5.3.1,<6.0.0)"] -test = ["pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "pytest-asyncio (>=0.14.0,<0.16.0)", "mypy (==0.910)", "flake8 (>=3.8.3,<4.0.0)", "black (==21.9b0)", "isort (>=5.0.6,<6.0.0)", "requests (>=2.24.0,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "email_validator (>=1.1.1,<2.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "peewee (>=3.13.3,<4.0.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "orjson (>=3.2.1,<4.0.0)", "ujson (>=4.0.1,<5.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "aiofiles (>=0.5.0,<0.8.0)", "flask (>=1.1.2,<2.0.0)", "async_exit_stack (>=1.0.1,<2.0.0)", "async_generator (>=1.10,<2.0.0)", "types-ujson (==0.1.1)", "types-orjson (==3.6.0)", "types-dataclasses (==0.1.7)"] +all = ["requests (>=2.24.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "itsdangerous (>=1.1.0,<3.0.0)", "pyyaml (>=5.3.1,<6.0.0)", "ujson (>=4.0.1,<5.0.0)", "orjson (>=3.2.1,<4.0.0)", "email_validator (>=1.1.1,<2.0.0)", "uvicorn[standard] (>=0.12.0,<0.16.0)"] +dev = ["python-jose[cryptography] (>=3.3.0,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "uvicorn[standard] (>=0.12.0,<0.16.0)"] +doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "typer-cli (>=0.0.12,<0.0.13)", "pyyaml (>=5.3.1,<6.0.0)"] +test = ["pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "mypy (==0.910)", "flake8 (>=3.8.3,<4.0.0)", "black (==21.9b0)", "isort (>=5.0.6,<6.0.0)", "requests (>=2.24.0,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "email_validator (>=1.1.1,<2.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "peewee (>=3.13.3,<4.0.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "orjson (>=3.2.1,<4.0.0)", "ujson (>=4.0.1,<5.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "flask (>=1.1.2,<3.0.0)", "anyio[trio] (>=3.2.1,<4.0.0)", "types-ujson (==0.1.1)", "types-orjson (==3.6.0)", "types-dataclasses (==0.1.7)"] [[package]] name = "flake8" -version = "3.9.2" +version = "4.0.1" description = "the modular source code checker: pep8 pyflakes and co" category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.6" [package.dependencies] -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +importlib-metadata = {version = "<4.3", markers = "python_version < \"3.8\""} mccabe = ">=0.6.0,<0.7.0" -pycodestyle = ">=2.7.0,<2.8.0" -pyflakes = ">=2.3.0,<2.4.0" +pycodestyle = ">=2.8.0,<2.9.0" +pyflakes = ">=2.4.0,<2.5.0" [[package]] name = "greenlet" @@ -315,7 +314,7 @@ python-versions = ">=3.6" [[package]] name = "httpcore" -version = "0.13.7" +version = "0.14.4" description = "A minimal low-level HTTP client." category = "dev" optional = false @@ -323,6 +322,7 @@ python-versions = ">=3.6" [package.dependencies] anyio = ">=3.0.0,<4.0.0" +certifi = "*" h11 = ">=0.11,<0.13" sniffio = ">=1.0.0,<2.0.0" @@ -331,7 +331,7 @@ http2 = ["h2 (>=3,<5)"] [[package]] name = "httpx" -version = "0.20.0" +version = "0.21.3" description = "The next generation HTTP client." category = "dev" optional = false @@ -340,7 +340,7 @@ python-versions = ">=3.6" [package.dependencies] certifi = "*" charset-normalizer = "*" -httpcore = ">=0.13.3,<0.14.0" +httpcore = ">=0.14.0,<0.15.0" rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} sniffio = "*" @@ -359,7 +359,7 @@ python-versions = ">=3.5" [[package]] name = "importlib-metadata" -version = "4.8.2" +version = "4.2.0" description = "Read metadata from Python packages" category = "main" optional = false @@ -371,8 +371,7 @@ zipp = ">=0.5" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -perf = ["ipython"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] [[package]] name = "importlib-resources" @@ -442,24 +441,6 @@ category = "dev" optional = false python-versions = "*" -[[package]] -name = "mypy" -version = "0.910" -description = "Optional static typing for Python" -category = "dev" -optional = false -python-versions = ">=3.5" - -[package.dependencies] -mypy-extensions = ">=0.4.3,<0.5.0" -toml = "*" -typed-ast = {version = ">=1.4.0,<1.5.0", markers = "python_version < \"3.8\""} -typing-extensions = ">=3.7.4" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -python2 = ["typed-ast (>=1.4.0,<1.5.0)"] - [[package]] name = "mypy-extensions" version = "0.4.3" @@ -506,11 +487,11 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [[package]] name = "platformdirs" -version = "2.4.0" +version = "2.4.1" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.extras] docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] @@ -549,11 +530,11 @@ python-versions = "*" [[package]] name = "pycodestyle" -version = "2.7.0" +version = "2.8.0" description = "Python style guide checker" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "pycparser" @@ -565,7 +546,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pydantic" -version = "1.8.2" +version = "1.9.0" description = "Data validation and settings management using python 3.6 type hinting" category = "main" optional = false @@ -581,7 +562,7 @@ email = ["email-validator (>=1.0.3)"] [[package]] name = "pyflakes" -version = "2.3.1" +version = "2.4.0" description = "passive checker of Python programs" category = "dev" optional = false @@ -675,17 +656,9 @@ python-versions = "*" [package.dependencies] six = ">=1.4.0" -[[package]] -name = "regex" -version = "2021.11.10" -description = "Alternative regular expression module, to replace re." -category = "dev" -optional = false -python-versions = "*" - [[package]] name = "requests" -version = "2.26.0" +version = "2.27.1" description = "Python HTTP for Humans." category = "main" optional = false @@ -738,13 +711,13 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" name = "sniffio" version = "1.2.0" description = "Sniff out which async library your code is running under" -category = "dev" +category = "main" optional = false python-versions = ">=3.5" [[package]] name = "sqlalchemy" -version = "1.4.27" +version = "1.4.29" description = "Database Abstraction Library" category = "main" optional = false @@ -775,18 +748,6 @@ postgresql_psycopg2cffi = ["psycopg2cffi"] pymysql = ["pymysql (<1)", "pymysql"] sqlcipher = ["sqlcipher3-binary"] -[[package]] -name = "sqlalchemy-stubs" -version = "0.4" -description = "SQLAlchemy stubs and mypy plugin" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -mypy = ">=0.790" -typing-extensions = ">=3.7.4" - [[package]] name = "sqlalchemy2-stubs" version = "0.0.2a19" @@ -800,14 +761,18 @@ typing-extensions = ">=3.7.4" [[package]] name = "starlette" -version = "0.14.2" +version = "0.17.1" description = "The little ASGI library that shines." category = "main" optional = false python-versions = ">=3.6" +[package.dependencies] +anyio = ">=3.0.0,<4" +typing-extensions = {version = "*", markers = "python_version < \"3.8\""} + [package.extras] -full = ["aiofiles", "graphene", "itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests"] +full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests"] [[package]] name = "toml" @@ -819,7 +784,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "tomli" -version = "1.2.2" +version = "1.2.3" description = "A lil' TOML parser" category = "dev" optional = false @@ -827,15 +792,15 @@ python-versions = ">=3.6" [[package]] name = "typed-ast" -version = "1.4.3" +version = "1.5.1" description = "a fork of Python 2 and 3 ast modules with type comment support" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.6" [[package]] name = "typing-extensions" -version = "4.0.0" +version = "4.0.1" description = "Backported and Experimental Type Hints for Python 3.6+" category = "main" optional = false @@ -856,7 +821,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "uvicorn" -version = "0.15.0" +version = "0.16.0" description = "The lightning-fast ASGI server." category = "main" optional = false @@ -869,24 +834,24 @@ h11 = ">=0.8" typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] -standard = ["websockets (>=9.1)", "httptools (>=0.2.0,<0.3.0)", "watchgod (>=0.6)", "python-dotenv (>=0.13)", "PyYAML (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "colorama (>=0.4)"] +standard = ["httptools (>=0.2.0,<0.4.0)", "watchgod (>=0.6)", "python-dotenv (>=0.13)", "PyYAML (>=5.1)", "websockets (>=9.1)", "websockets (>=10.0)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "colorama (>=0.4)"] [[package]] name = "zipp" -version = "3.6.0" +version = "3.7.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "f86a8d9859085df3cf5d7dfa83a28c2e863147fb420cc955f68247d7242075b9" +content-hash = "a00e6962598c9e04b8f260f70bb66492d5a520b72e65d4d1aa227a0a2a93f5cb" [metadata.files] alembic = [ @@ -902,27 +867,40 @@ asgiref = [ {file = "asgiref-3.4.1.tar.gz", hash = "sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9"}, ] asyncpg = [ - {file = "asyncpg-0.24.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c4fc0205fe4ddd5aeb3dfdc0f7bafd43411181e1f5650189608e5971cceacff1"}, - {file = "asyncpg-0.24.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a7095890c96ba36f9f668eb552bb020dddb44f8e73e932f8573efc613ee83843"}, - {file = "asyncpg-0.24.0-cp310-cp310-win_amd64.whl", hash = "sha256:8ff5073d4b654e34bd5eaadc01dc4d68b8a9609084d835acd364cd934190a08d"}, - {file = "asyncpg-0.24.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e36c6806883786b19551bb70a4882561f31135dc8105a59662e0376cf5b2cbc5"}, - {file = "asyncpg-0.24.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ddffcb85227bf39cd1bedd4603e0082b243cf3b14ced64dce506a15b05232b83"}, - {file = "asyncpg-0.24.0-cp37-cp37m-win_amd64.whl", hash = "sha256:41704c561d354bef01353835a7846e5606faabbeb846214dfcf666cf53319f18"}, - {file = "asyncpg-0.24.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:29ef6ae0a617fc13cc2ac5dc8e9b367bb83cba220614b437af9b67766f4b6b20"}, - {file = "asyncpg-0.24.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:eed43abc6ccf1dc02e0d0efc06ce46a411362f3358847c6b0ec9a43426f91ece"}, - {file = "asyncpg-0.24.0-cp38-cp38-win_amd64.whl", hash = "sha256:129d501f3d30616afd51eb8d3142ef51ba05374256bd5834cec3ef4956a9b317"}, - {file = "asyncpg-0.24.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a458fc69051fbb67d995fdda46d75a012b5d6200f91e17d23d4751482640ed4c"}, - {file = "asyncpg-0.24.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:556b0e92e2b75dc028b3c4bc9bd5162ddf0053b856437cf1f04c97f9c6837d03"}, - {file = "asyncpg-0.24.0-cp39-cp39-win_amd64.whl", hash = "sha256:a738f4807c853623d3f93f0fea11f61be6b0e5ca16ea8aeb42c2c7ee742aa853"}, - {file = "asyncpg-0.24.0.tar.gz", hash = "sha256:dd2fa063c3344823487d9ddccb40802f02622ddf8bf8a6cc53885ee7a2c1c0c6"}, + {file = "asyncpg-0.25.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bf5e3408a14a17d480f36ebaf0401a12ff6ae5457fdf45e4e2775c51cc9517d3"}, + {file = "asyncpg-0.25.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2bc197fc4aca2fd24f60241057998124012469d2e414aed3f992579db0c88e3a"}, + {file = "asyncpg-0.25.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a70783f6ffa34cc7dd2de20a873181414a34fd35a4a208a1f1a7f9f695e4ec4"}, + {file = "asyncpg-0.25.0-cp310-cp310-win32.whl", hash = "sha256:43cde84e996a3afe75f325a68300093425c2f47d340c0fc8912765cf24a1c095"}, + {file = "asyncpg-0.25.0-cp310-cp310-win_amd64.whl", hash = "sha256:56d88d7ef4341412cd9c68efba323a4519c916979ba91b95d4c08799d2ff0c09"}, + {file = "asyncpg-0.25.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a84d30e6f850bac0876990bcd207362778e2208df0bee8be8da9f1558255e634"}, + {file = "asyncpg-0.25.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:beaecc52ad39614f6ca2e48c3ca15d56e24a2c15cbfdcb764a4320cc45f02fd5"}, + {file = "asyncpg-0.25.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:6f8f5fc975246eda83da8031a14004b9197f510c41511018e7b1bedde6968e92"}, + {file = "asyncpg-0.25.0-cp36-cp36m-win32.whl", hash = "sha256:ddb4c3263a8d63dcde3d2c4ac1c25206bfeb31fa83bd70fd539e10f87739dee4"}, + {file = "asyncpg-0.25.0-cp36-cp36m-win_amd64.whl", hash = "sha256:bf6dc9b55b9113f39eaa2057337ce3f9ef7de99a053b8a16360395ce588925cd"}, + {file = "asyncpg-0.25.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:acb311722352152936e58a8ee3c5b8e791b24e84cd7d777c414ff05b3530ca68"}, + {file = "asyncpg-0.25.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0a61fb196ce4dae2f2fa26eb20a778db21bbee484d2e798cb3cc988de13bdd1b"}, + {file = "asyncpg-0.25.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2633331cbc8429030b4f20f712f8d0fbba57fa8555ee9b2f45f981b81328b256"}, + {file = "asyncpg-0.25.0-cp37-cp37m-win32.whl", hash = "sha256:863d36eba4a7caa853fd7d83fad5fd5306f050cc2fe6e54fbe10cdb30420e5e9"}, + {file = "asyncpg-0.25.0-cp37-cp37m-win_amd64.whl", hash = "sha256:fe471ccd915b739ca65e2e4dbd92a11b44a5b37f2e38f70827a1c147dafe0fa8"}, + {file = "asyncpg-0.25.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:72a1e12ea0cf7c1e02794b697e3ca967b2360eaa2ce5d4bfdd8604ec2d6b774b"}, + {file = "asyncpg-0.25.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4327f691b1bdb222df27841938b3e04c14068166b3a97491bec2cb982f49f03e"}, + {file = "asyncpg-0.25.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:739bbd7f89a2b2f6bc44cb8bf967dab12c5bc714fcbe96e68d512be45ecdf962"}, + {file = "asyncpg-0.25.0-cp38-cp38-win32.whl", hash = "sha256:18d49e2d93a7139a2fdbd113e320cc47075049997268a61bfbe0dde680c55471"}, + {file = "asyncpg-0.25.0-cp38-cp38-win_amd64.whl", hash = "sha256:191fe6341385b7fdea7dbdcf47fd6db3fd198827dcc1f2b228476d13c05a03c6"}, + {file = "asyncpg-0.25.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:52fab7f1b2c29e187dd8781fce896249500cf055b63471ad66332e537e9b5f7e"}, + {file = "asyncpg-0.25.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a738f1b2876f30d710d3dc1e7858160a0afe1603ba16bf5f391f5316eb0ed855"}, + {file = "asyncpg-0.25.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4105f57ad1e8fbc8b1e535d8fcefa6ce6c71081228f08680c6dea24384ff0e"}, + {file = "asyncpg-0.25.0-cp39-cp39-win32.whl", hash = "sha256:f55918ded7b85723a5eaeb34e86e7b9280d4474be67df853ab5a7fa0cc7c6bf2"}, + {file = "asyncpg-0.25.0-cp39-cp39-win_amd64.whl", hash = "sha256:649e2966d98cc48d0646d9a4e29abecd8b59d38d55c256d5c857f6b27b7407ac"}, + {file = "asyncpg-0.25.0.tar.gz", hash = "sha256:63f8e6a69733b285497c2855464a34de657f2cccd25aeaeeb5071872e9382540"}, ] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, ] attrs = [ - {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, - {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, + {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, + {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, ] autoflake = [ {file = "autoflake-1.4.tar.gz", hash = "sha256:61a353012cff6ab94ca062823d1fb2f692c4acda51c76ff83a8d77915fba51ea"}, @@ -937,8 +915,8 @@ bcrypt = [ {file = "bcrypt-3.2.0.tar.gz", hash = "sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29"}, ] black = [ - {file = "black-21.11b1-py3-none-any.whl", hash = "sha256:802c6c30b637b28645b7fde282ed2569c0cd777dbe493a41b6a03c1d903f99ac"}, - {file = "black-21.11b1.tar.gz", hash = "sha256:a042adbb18b3262faad5aff4e834ff186bb893f95ba3a8013f09de1e5569def2"}, + {file = "black-21.12b0-py3-none-any.whl", hash = "sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f"}, + {file = "black-21.12b0.tar.gz", hash = "sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3"}, ] certifi = [ {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, @@ -997,8 +975,8 @@ cffi = [ {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"}, ] charset-normalizer = [ - {file = "charset-normalizer-2.0.8.tar.gz", hash = "sha256:735e240d9a8506778cd7a453d97e817e536bb1fc29f4f6961ce297b9c7a917b0"}, - {file = "charset_normalizer-2.0.8-py3-none-any.whl", hash = "sha256:83fcdeb225499d6344c8f7f34684c2981270beacc32ede2e669e94f7fa544405"}, + {file = "charset-normalizer-2.0.9.tar.gz", hash = "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c"}, + {file = "charset_normalizer-2.0.9-py3-none-any.whl", hash = "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721"}, ] click = [ {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, @@ -1009,81 +987,75 @@ colorama = [ {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] coverage = [ - {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, - {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"}, - {file = "coverage-5.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669"}, - {file = "coverage-5.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90"}, - {file = "coverage-5.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c"}, - {file = "coverage-5.5-cp27-cp27m-win32.whl", hash = "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a"}, - {file = "coverage-5.5-cp27-cp27m-win_amd64.whl", hash = "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82"}, - {file = "coverage-5.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905"}, - {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"}, - {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"}, - {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"}, - {file = "coverage-5.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6"}, - {file = "coverage-5.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0"}, - {file = "coverage-5.5-cp310-cp310-win_amd64.whl", hash = "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae"}, - {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"}, - {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"}, - {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"}, - {file = "coverage-5.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701"}, - {file = "coverage-5.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793"}, - {file = "coverage-5.5-cp35-cp35m-win32.whl", hash = "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e"}, - {file = "coverage-5.5-cp35-cp35m-win_amd64.whl", hash = "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3"}, - {file = "coverage-5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066"}, - {file = "coverage-5.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a"}, - {file = "coverage-5.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465"}, - {file = "coverage-5.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb"}, - {file = "coverage-5.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821"}, - {file = "coverage-5.5-cp36-cp36m-win32.whl", hash = "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45"}, - {file = "coverage-5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184"}, - {file = "coverage-5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a"}, - {file = "coverage-5.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53"}, - {file = "coverage-5.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d"}, - {file = "coverage-5.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638"}, - {file = "coverage-5.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3"}, - {file = "coverage-5.5-cp37-cp37m-win32.whl", hash = "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a"}, - {file = "coverage-5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a"}, - {file = "coverage-5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6"}, - {file = "coverage-5.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2"}, - {file = "coverage-5.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759"}, - {file = "coverage-5.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873"}, - {file = "coverage-5.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a"}, - {file = "coverage-5.5-cp38-cp38-win32.whl", hash = "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6"}, - {file = "coverage-5.5-cp38-cp38-win_amd64.whl", hash = "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502"}, - {file = "coverage-5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b"}, - {file = "coverage-5.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529"}, - {file = "coverage-5.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b"}, - {file = "coverage-5.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff"}, - {file = "coverage-5.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b"}, - {file = "coverage-5.5-cp39-cp39-win32.whl", hash = "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6"}, - {file = "coverage-5.5-cp39-cp39-win_amd64.whl", hash = "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03"}, - {file = "coverage-5.5-pp36-none-any.whl", hash = "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079"}, - {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, - {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, + {file = "coverage-6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b"}, + {file = "coverage-6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0"}, + {file = "coverage-6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da"}, + {file = "coverage-6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d"}, + {file = "coverage-6.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739"}, + {file = "coverage-6.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971"}, + {file = "coverage-6.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840"}, + {file = "coverage-6.2-cp310-cp310-win32.whl", hash = "sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c"}, + {file = "coverage-6.2-cp310-cp310-win_amd64.whl", hash = "sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f"}, + {file = "coverage-6.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76"}, + {file = "coverage-6.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47"}, + {file = "coverage-6.2-cp311-cp311-win_amd64.whl", hash = "sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64"}, + {file = "coverage-6.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9"}, + {file = "coverage-6.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d"}, + {file = "coverage-6.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48"}, + {file = "coverage-6.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e"}, + {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d"}, + {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17"}, + {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781"}, + {file = "coverage-6.2-cp36-cp36m-win32.whl", hash = "sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a"}, + {file = "coverage-6.2-cp36-cp36m-win_amd64.whl", hash = "sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0"}, + {file = "coverage-6.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49"}, + {file = "coverage-6.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521"}, + {file = "coverage-6.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884"}, + {file = "coverage-6.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa"}, + {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64"}, + {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617"}, + {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8"}, + {file = "coverage-6.2-cp37-cp37m-win32.whl", hash = "sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4"}, + {file = "coverage-6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74"}, + {file = "coverage-6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e"}, + {file = "coverage-6.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58"}, + {file = "coverage-6.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc"}, + {file = "coverage-6.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd"}, + {file = "coverage-6.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953"}, + {file = "coverage-6.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475"}, + {file = "coverage-6.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57"}, + {file = "coverage-6.2-cp38-cp38-win32.whl", hash = "sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c"}, + {file = "coverage-6.2-cp38-cp38-win_amd64.whl", hash = "sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2"}, + {file = "coverage-6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd"}, + {file = "coverage-6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685"}, + {file = "coverage-6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c"}, + {file = "coverage-6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3"}, + {file = "coverage-6.2-cp39-cp39-win32.whl", hash = "sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282"}, + {file = "coverage-6.2-cp39-cp39-win_amd64.whl", hash = "sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644"}, + {file = "coverage-6.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de"}, + {file = "coverage-6.2.tar.gz", hash = "sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8"}, ] cryptography = [ - {file = "cryptography-36.0.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:9511416e85e449fe1de73f7f99b21b3aa04fba4c4d335d30c486ba3756e3a2a6"}, - {file = "cryptography-36.0.0-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:97199a13b772e74cdcdb03760c32109c808aff7cd49c29e9cf4b7754bb725d1d"}, - {file = "cryptography-36.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:494106e9cd945c2cadfce5374fa44c94cfadf01d4566a3b13bb487d2e6c7959e"}, - {file = "cryptography-36.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6fbbbb8aab4053fa018984bb0e95a16faeb051dd8cca15add2a27e267ba02b58"}, - {file = "cryptography-36.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:684993ff6f67000a56454b41bdc7e015429732d65a52d06385b6e9de6181c71e"}, - {file = "cryptography-36.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c702855cd3174666ef0d2d13dcc879090aa9c6c38f5578896407a7028f75b9f"}, - {file = "cryptography-36.0.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d91bc9f535599bed58f6d2e21a2724cb0c3895bf41c6403fe881391d29096f1d"}, - {file = "cryptography-36.0.0-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:b17d83b3d1610e571fedac21b2eb36b816654d6f7496004d6a0d32f99d1d8120"}, - {file = "cryptography-36.0.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8982c19bb90a4fa2aad3d635c6d71814e38b643649b4000a8419f8691f20ac44"}, - {file = "cryptography-36.0.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:24469d9d33217ffd0ce4582dfcf2a76671af115663a95328f63c99ec7ece61a4"}, - {file = "cryptography-36.0.0-cp36-abi3-win32.whl", hash = "sha256:f6a5a85beb33e57998dc605b9dbe7deaa806385fdf5c4810fb849fcd04640c81"}, - {file = "cryptography-36.0.0-cp36-abi3-win_amd64.whl", hash = "sha256:2deab5ec05d83ddcf9b0916319674d3dae88b0e7ee18f8962642d3cde0496568"}, - {file = "cryptography-36.0.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2049f8b87f449fc6190350de443ee0c1dd631f2ce4fa99efad2984de81031681"}, - {file = "cryptography-36.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a776bae1629c8d7198396fd93ec0265f8dd2341c553dc32b976168aaf0e6a636"}, - {file = "cryptography-36.0.0-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:aa94d617a4cd4cdf4af9b5af65100c036bce22280ebb15d8b5262e8273ebc6ba"}, - {file = "cryptography-36.0.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:5c49c9e8fb26a567a2b3fa0343c89f5d325447956cc2fc7231c943b29a973712"}, - {file = "cryptography-36.0.0-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ef216d13ac8d24d9cd851776662f75f8d29c9f2d05cdcc2d34a18d32463a9b0b"}, - {file = "cryptography-36.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:231c4a69b11f6af79c1495a0e5a85909686ea8db946935224b7825cfb53827ed"}, - {file = "cryptography-36.0.0-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:f92556f94e476c1b616e6daec5f7ddded2c082efa7cee7f31c7aeda615906ed8"}, - {file = "cryptography-36.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d73e3a96c38173e0aa5646c31bf8473bc3564837977dd480f5cbeacf1d7ef3a3"}, - {file = "cryptography-36.0.0.tar.gz", hash = "sha256:52f769ecb4ef39865719aedc67b4b7eae167bafa48dbc2a26dd36fa56460507f"}, + {file = "cryptography-36.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:73bc2d3f2444bcfeac67dd130ff2ea598ea5f20b40e36d19821b4df8c9c5037b"}, + {file = "cryptography-36.0.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:2d87cdcb378d3cfed944dac30596da1968f88fb96d7fc34fdae30a99054b2e31"}, + {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74d6c7e80609c0f4c2434b97b80c7f8fdfaa072ca4baab7e239a15d6d70ed73a"}, + {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:6c0c021f35b421ebf5976abf2daacc47e235f8b6082d3396a2fe3ccd537ab173"}, + {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d59a9d55027a8b88fd9fd2826c4392bd487d74bf628bb9d39beecc62a644c12"}, + {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a817b961b46894c5ca8a66b599c745b9a3d9f822725221f0e0fe49dc043a3a3"}, + {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:94ae132f0e40fe48f310bba63f477f14a43116f05ddb69d6fa31e93f05848ae2"}, + {file = "cryptography-36.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7be0eec337359c155df191d6ae00a5e8bbb63933883f4f5dffc439dac5348c3f"}, + {file = "cryptography-36.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e0344c14c9cb89e76eb6a060e67980c9e35b3f36691e15e1b7a9e58a0a6c6dc3"}, + {file = "cryptography-36.0.1-cp36-abi3-win32.whl", hash = "sha256:4caa4b893d8fad33cf1964d3e51842cd78ba87401ab1d2e44556826df849a8ca"}, + {file = "cryptography-36.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:391432971a66cfaf94b21c24ab465a4cc3e8bf4a939c1ca5c3e3a6e0abebdbcf"}, + {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bb5829d027ff82aa872d76158919045a7c1e91fbf241aec32cb07956e9ebd3c9"}, + {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebc15b1c22e55c4d5566e3ca4db8689470a0ca2babef8e3a9ee057a8b82ce4b1"}, + {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:596f3cd67e1b950bc372c33f1a28a0692080625592ea6392987dba7f09f17a94"}, + {file = "cryptography-36.0.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:30ee1eb3ebe1644d1c3f183d115a8c04e4e603ed6ce8e394ed39eea4a98469ac"}, + {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec63da4e7e4a5f924b90af42eddf20b698a70e58d86a72d943857c4c6045b3ee"}, + {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca238ceb7ba0bdf6ce88c1b74a87bffcee5afbfa1e41e173b1ceb095b39add46"}, + {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:ca28641954f767f9822c24e927ad894d45d5a1e501767599647259cbf030b903"}, + {file = "cryptography-36.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:39bdf8e70eee6b1c7b289ec6e5d84d49a6bfa11f8b8646b5b3dfe41219153316"}, + {file = "cryptography-36.0.1.tar.gz", hash = "sha256:53e5c1dc3d7a953de055d77bef2ff607ceef7a2aac0353b5d630ab67f7423638"}, ] dnspython = [ {file = "dnspython-2.1.0-py3-none-any.whl", hash = "sha256:95d12f6ef0317118d2a1a6fc49aac65ffec7eb8087474158f42f26a639135216"}, @@ -1098,12 +1070,12 @@ email-validator = [ {file = "email_validator-1.1.3.tar.gz", hash = "sha256:aa237a65f6f4da067119b7df3f13e89c25c051327b2b5b66dc075f33d62480d7"}, ] fastapi = [ - {file = "fastapi-0.68.2-py3-none-any.whl", hash = "sha256:36bcdd3dbea87c586061005e4a40b9bd0145afd766655b4e0ec1d8870b32555c"}, - {file = "fastapi-0.68.2.tar.gz", hash = "sha256:38526fc46bda73f7ec92033952677323c16061e70a91d15c95f18b11895da494"}, + {file = "fastapi-0.71.0-py3-none-any.whl", hash = "sha256:a78eca6b084de9667f2d5f37e2ae297270e5a119cd01c2f04815795da92fc87f"}, + {file = "fastapi-0.71.0.tar.gz", hash = "sha256:2b5ac0ae89c80b40d1dd4b2ea0bb1f78d7c4affd3644d080bf050f084759fff2"}, ] flake8 = [ - {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, - {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, + {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, + {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, ] greenlet = [ {file = "greenlet-1.1.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6"}, @@ -1162,20 +1134,20 @@ h11 = [ {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, ] httpcore = [ - {file = "httpcore-0.13.7-py3-none-any.whl", hash = "sha256:369aa481b014cf046f7067fddd67d00560f2f00426e79569d99cb11245134af0"}, - {file = "httpcore-0.13.7.tar.gz", hash = "sha256:036f960468759e633574d7c121afba48af6419615d36ab8ede979f1ad6276fa3"}, + {file = "httpcore-0.14.4-py3-none-any.whl", hash = "sha256:9410fe352bea732311f2b2bee0555c8cc5e62b9a73b9d3272fe125a2aa6eb28e"}, + {file = "httpcore-0.14.4.tar.gz", hash = "sha256:d4305811f604d3c2e22869147392f134796976ff946c96a8cfba87f4e0171d83"}, ] httpx = [ - {file = "httpx-0.20.0-py3-none-any.whl", hash = "sha256:33af5aad9bdc82ef1fc89219c1e36f5693bf9cd0ebe330884df563445682c0f8"}, - {file = "httpx-0.20.0.tar.gz", hash = "sha256:09606d630f070d07f9ff28104fbcea429ea0014c1e89ac90b4d8de8286c40e7b"}, + {file = "httpx-0.21.3-py3-none-any.whl", hash = "sha256:df9a0fd43fa79dbab411d83eb1ea6f7a525c96ad92e60c2d7f40388971b25777"}, + {file = "httpx-0.21.3.tar.gz", hash = "sha256:7a3eb67ef0b8abbd6d9402248ef2f84a76080fa1c839f8662e6eb385640e445a"}, ] idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.8.2-py3-none-any.whl", hash = "sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100"}, - {file = "importlib_metadata-4.8.2.tar.gz", hash = "sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb"}, + {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, + {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, ] importlib-resources = [ {file = "importlib_resources-5.4.0-py3-none-any.whl", hash = "sha256:33a95faed5fc19b4bc16b29a6eeae248a3fe69dd55d4d229d2b480e23eeaad45"}, @@ -1253,31 +1225,6 @@ mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] -mypy = [ - {file = "mypy-0.910-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457"}, - {file = "mypy-0.910-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb"}, - {file = "mypy-0.910-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:088cd9c7904b4ad80bec811053272986611b84221835e079be5bcad029e79dd9"}, - {file = "mypy-0.910-cp35-cp35m-win_amd64.whl", hash = "sha256:adaeee09bfde366d2c13fe6093a7df5df83c9a2ba98638c7d76b010694db760e"}, - {file = "mypy-0.910-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ecd2c3fe726758037234c93df7e98deb257fd15c24c9180dacf1ef829da5f921"}, - {file = "mypy-0.910-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d9dd839eb0dc1bbe866a288ba3c1afc33a202015d2ad83b31e875b5905a079b6"}, - {file = "mypy-0.910-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3e382b29f8e0ccf19a2df2b29a167591245df90c0b5a2542249873b5c1d78212"}, - {file = "mypy-0.910-cp36-cp36m-win_amd64.whl", hash = "sha256:53fd2eb27a8ee2892614370896956af2ff61254c275aaee4c230ae771cadd885"}, - {file = "mypy-0.910-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b6fb13123aeef4a3abbcfd7e71773ff3ff1526a7d3dc538f3929a49b42be03f0"}, - {file = "mypy-0.910-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e4dab234478e3bd3ce83bac4193b2ecd9cf94e720ddd95ce69840273bf44f6de"}, - {file = "mypy-0.910-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:7df1ead20c81371ccd6091fa3e2878559b5c4d4caadaf1a484cf88d93ca06703"}, - {file = "mypy-0.910-cp37-cp37m-win_amd64.whl", hash = "sha256:0aadfb2d3935988ec3815952e44058a3100499f5be5b28c34ac9d79f002a4a9a"}, - {file = "mypy-0.910-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ec4e0cd079db280b6bdabdc807047ff3e199f334050db5cbb91ba3e959a67504"}, - {file = "mypy-0.910-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:119bed3832d961f3a880787bf621634ba042cb8dc850a7429f643508eeac97b9"}, - {file = "mypy-0.910-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:866c41f28cee548475f146aa4d39a51cf3b6a84246969f3759cb3e9c742fc072"}, - {file = "mypy-0.910-cp38-cp38-win_amd64.whl", hash = "sha256:ceb6e0a6e27fb364fb3853389607cf7eb3a126ad335790fa1e14ed02fba50811"}, - {file = "mypy-0.910-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a85e280d4d217150ce8cb1a6dddffd14e753a4e0c3cf90baabb32cefa41b59e"}, - {file = "mypy-0.910-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:42c266ced41b65ed40a282c575705325fa7991af370036d3f134518336636f5b"}, - {file = "mypy-0.910-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:3c4b8ca36877fc75339253721f69603a9c7fdb5d4d5a95a1a1b899d8b86a4de2"}, - {file = "mypy-0.910-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:c0df2d30ed496a08de5daed2a9ea807d07c21ae0ab23acf541ab88c24b26ab97"}, - {file = "mypy-0.910-cp39-cp39-win_amd64.whl", hash = "sha256:c6c2602dffb74867498f86e6129fd52a2770c48b7cd3ece77ada4fa38f94eba8"}, - {file = "mypy-0.910-py3-none-any.whl", hash = "sha256:ef565033fa5a958e62796867b1df10c40263ea9ded87164d67572834e57a174d"}, - {file = "mypy-0.910.tar.gz", hash = "sha256:704098302473cb31a218f1775a873b376b30b4c18229421e9e9dc8916fd16150"}, -] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, @@ -1295,8 +1242,8 @@ pathspec = [ {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, ] platformdirs = [ - {file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"}, - {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"}, + {file = "platformdirs-2.4.1-py3-none-any.whl", hash = "sha256:1d7385c7db91728b83efd0ca99a5afb296cab9d0ed8313a45ed8ba17967ecfca"}, + {file = "platformdirs-2.4.1.tar.gz", hash = "sha256:440633ddfebcc36264232365d7840a970e75e1018d15b4327d11f91909045fda"}, ] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, @@ -1322,40 +1269,53 @@ pyasn1 = [ {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, ] pycodestyle = [ - {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, - {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, + {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, + {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, ] pycparser = [ {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] pydantic = [ - {file = "pydantic-1.8.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739"}, - {file = "pydantic-1.8.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4"}, - {file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:589eb6cd6361e8ac341db97602eb7f354551482368a37f4fd086c0733548308e"}, - {file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:10e5622224245941efc193ad1d159887872776df7a8fd592ed746aa25d071840"}, - {file = "pydantic-1.8.2-cp36-cp36m-win_amd64.whl", hash = "sha256:99a9fc39470010c45c161a1dc584997f1feb13f689ecf645f59bb4ba623e586b"}, - {file = "pydantic-1.8.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a83db7205f60c6a86f2c44a61791d993dff4b73135df1973ecd9eed5ea0bda20"}, - {file = "pydantic-1.8.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:41b542c0b3c42dc17da70554bc6f38cbc30d7066d2c2815a94499b5684582ecb"}, - {file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:ea5cb40a3b23b3265f6325727ddfc45141b08ed665458be8c6285e7b85bd73a1"}, - {file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:18b5ea242dd3e62dbf89b2b0ec9ba6c7b5abaf6af85b95a97b00279f65845a23"}, - {file = "pydantic-1.8.2-cp37-cp37m-win_amd64.whl", hash = "sha256:234a6c19f1c14e25e362cb05c68afb7f183eb931dd3cd4605eafff055ebbf287"}, - {file = "pydantic-1.8.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:021ea0e4133e8c824775a0cfe098677acf6fa5a3cbf9206a376eed3fc09302cd"}, - {file = "pydantic-1.8.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e710876437bc07bd414ff453ac8ec63d219e7690128d925c6e82889d674bb505"}, - {file = "pydantic-1.8.2-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:ac8eed4ca3bd3aadc58a13c2aa93cd8a884bcf21cb019f8cfecaae3b6ce3746e"}, - {file = "pydantic-1.8.2-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:4a03cbbe743e9c7247ceae6f0d8898f7a64bb65800a45cbdc52d65e370570820"}, - {file = "pydantic-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:8621559dcf5afacf0069ed194278f35c255dc1a1385c28b32dd6c110fd6531b3"}, - {file = "pydantic-1.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8b223557f9510cf0bfd8b01316bf6dd281cf41826607eada99662f5e4963f316"}, - {file = "pydantic-1.8.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:244ad78eeb388a43b0c927e74d3af78008e944074b7d0f4f696ddd5b2af43c62"}, - {file = "pydantic-1.8.2-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:05ef5246a7ffd2ce12a619cbb29f3307b7c4509307b1b49f456657b43529dc6f"}, - {file = "pydantic-1.8.2-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:54cd5121383f4a461ff7644c7ca20c0419d58052db70d8791eacbbe31528916b"}, - {file = "pydantic-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:4be75bebf676a5f0f87937c6ddb061fa39cbea067240d98e298508c1bda6f3f3"}, - {file = "pydantic-1.8.2-py3-none-any.whl", hash = "sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833"}, - {file = "pydantic-1.8.2.tar.gz", hash = "sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b"}, + {file = "pydantic-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cb23bcc093697cdea2708baae4f9ba0e972960a835af22560f6ae4e7e47d33f5"}, + {file = "pydantic-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1d5278bd9f0eee04a44c712982343103bba63507480bfd2fc2790fa70cd64cf4"}, + {file = "pydantic-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab624700dc145aa809e6f3ec93fb8e7d0f99d9023b713f6a953637429b437d37"}, + {file = "pydantic-1.9.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c8d7da6f1c1049eefb718d43d99ad73100c958a5367d30b9321b092771e96c25"}, + {file = "pydantic-1.9.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3c3b035103bd4e2e4a28da9da7ef2fa47b00ee4a9cf4f1a735214c1bcd05e0f6"}, + {file = "pydantic-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3011b975c973819883842c5ab925a4e4298dffccf7782c55ec3580ed17dc464c"}, + {file = "pydantic-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:086254884d10d3ba16da0588604ffdc5aab3f7f09557b998373e885c690dd398"}, + {file = "pydantic-1.9.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0fe476769acaa7fcddd17cadd172b156b53546ec3614a4d880e5d29ea5fbce65"}, + {file = "pydantic-1.9.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8e9dcf1ac499679aceedac7e7ca6d8641f0193c591a2d090282aaf8e9445a46"}, + {file = "pydantic-1.9.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1e4c28f30e767fd07f2ddc6f74f41f034d1dd6bc526cd59e63a82fe8bb9ef4c"}, + {file = "pydantic-1.9.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:c86229333cabaaa8c51cf971496f10318c4734cf7b641f08af0a6fbf17ca3054"}, + {file = "pydantic-1.9.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:c0727bda6e38144d464daec31dff936a82917f431d9c39c39c60a26567eae3ed"}, + {file = "pydantic-1.9.0-cp36-cp36m-win_amd64.whl", hash = "sha256:dee5ef83a76ac31ab0c78c10bd7d5437bfdb6358c95b91f1ba7ff7b76f9996a1"}, + {file = "pydantic-1.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9c9bdb3af48e242838f9f6e6127de9be7063aad17b32215ccc36a09c5cf1070"}, + {file = "pydantic-1.9.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ee7e3209db1e468341ef41fe263eb655f67f5c5a76c924044314e139a1103a2"}, + {file = "pydantic-1.9.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b6037175234850ffd094ca77bf60fb54b08b5b22bc85865331dd3bda7a02fa1"}, + {file = "pydantic-1.9.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b2571db88c636d862b35090ccf92bf24004393f85c8870a37f42d9f23d13e032"}, + {file = "pydantic-1.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8b5ac0f1c83d31b324e57a273da59197c83d1bb18171e512908fe5dc7278a1d6"}, + {file = "pydantic-1.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:bbbc94d0c94dd80b3340fc4f04fd4d701f4b038ebad72c39693c794fd3bc2d9d"}, + {file = "pydantic-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e0896200b6a40197405af18828da49f067c2fa1f821491bc8f5bde241ef3f7d7"}, + {file = "pydantic-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bdfdadb5994b44bd5579cfa7c9b0e1b0e540c952d56f627eb227851cda9db77"}, + {file = "pydantic-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:574936363cd4b9eed8acdd6b80d0143162f2eb654d96cb3a8ee91d3e64bf4cf9"}, + {file = "pydantic-1.9.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c556695b699f648c58373b542534308922c46a1cda06ea47bc9ca45ef5b39ae6"}, + {file = "pydantic-1.9.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f947352c3434e8b937e3aa8f96f47bdfe6d92779e44bb3f41e4c213ba6a32145"}, + {file = "pydantic-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5e48ef4a8b8c066c4a31409d91d7ca372a774d0212da2787c0d32f8045b1e034"}, + {file = "pydantic-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:96f240bce182ca7fe045c76bcebfa0b0534a1bf402ed05914a6f1dadff91877f"}, + {file = "pydantic-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:815ddebb2792efd4bba5488bc8fde09c29e8ca3227d27cf1c6990fc830fd292b"}, + {file = "pydantic-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c5b77947b9e85a54848343928b597b4f74fc364b70926b3c4441ff52620640c"}, + {file = "pydantic-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c68c3bc88dbda2a6805e9a142ce84782d3930f8fdd9655430d8576315ad97ce"}, + {file = "pydantic-1.9.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a79330f8571faf71bf93667d3ee054609816f10a259a109a0738dac983b23c3"}, + {file = "pydantic-1.9.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f5a64b64ddf4c99fe201ac2724daada8595ada0d102ab96d019c1555c2d6441d"}, + {file = "pydantic-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a733965f1a2b4090a5238d40d983dcd78f3ecea221c7af1497b845a9709c1721"}, + {file = "pydantic-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cc6a4cb8a118ffec2ca5fcb47afbacb4f16d0ab8b7350ddea5e8ef7bcc53a16"}, + {file = "pydantic-1.9.0-py3-none-any.whl", hash = "sha256:085ca1de245782e9b46cefcf99deecc67d418737a1fd3f6a4f511344b613a5b3"}, + {file = "pydantic-1.9.0.tar.gz", hash = "sha256:742645059757a56ecd886faf4ed2441b9c0cd406079c2b4bee51bcc3fbcd510a"}, ] pyflakes = [ - {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, - {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, + {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, + {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, ] pyparsing = [ {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, @@ -1380,85 +1340,9 @@ python-jose = [ python-multipart = [ {file = "python-multipart-0.0.5.tar.gz", hash = "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"}, ] -regex = [ - {file = "regex-2021.11.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9345b6f7ee578bad8e475129ed40123d265464c4cfead6c261fd60fc9de00bcf"}, - {file = "regex-2021.11.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:416c5f1a188c91e3eb41e9c8787288e707f7d2ebe66e0a6563af280d9b68478f"}, - {file = "regex-2021.11.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0538c43565ee6e703d3a7c3bdfe4037a5209250e8502c98f20fea6f5fdf2965"}, - {file = "regex-2021.11.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee1227cf08b6716c85504aebc49ac827eb88fcc6e51564f010f11a406c0a667"}, - {file = "regex-2021.11.10-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6650f16365f1924d6014d2ea770bde8555b4a39dc9576abb95e3cd1ff0263b36"}, - {file = "regex-2021.11.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30ab804ea73972049b7a2a5c62d97687d69b5a60a67adca07eb73a0ddbc9e29f"}, - {file = "regex-2021.11.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68a067c11463de2a37157930d8b153005085e42bcb7ad9ca562d77ba7d1404e0"}, - {file = "regex-2021.11.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:162abfd74e88001d20cb73ceaffbfe601469923e875caf9118333b1a4aaafdc4"}, - {file = "regex-2021.11.10-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9ed0b1e5e0759d6b7f8e2f143894b2a7f3edd313f38cf44e1e15d360e11749b"}, - {file = "regex-2021.11.10-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:473e67837f786404570eae33c3b64a4b9635ae9f00145250851a1292f484c063"}, - {file = "regex-2021.11.10-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2fee3ed82a011184807d2127f1733b4f6b2ff6ec7151d83ef3477f3b96a13d03"}, - {file = "regex-2021.11.10-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:d5fd67df77bab0d3f4ea1d7afca9ef15c2ee35dfb348c7b57ffb9782a6e4db6e"}, - {file = "regex-2021.11.10-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5d408a642a5484b9b4d11dea15a489ea0928c7e410c7525cd892f4d04f2f617b"}, - {file = "regex-2021.11.10-cp310-cp310-win32.whl", hash = "sha256:98ba568e8ae26beb726aeea2273053c717641933836568c2a0278a84987b2a1a"}, - {file = "regex-2021.11.10-cp310-cp310-win_amd64.whl", hash = "sha256:780b48456a0f0ba4d390e8b5f7c661fdd218934388cde1a974010a965e200e12"}, - {file = "regex-2021.11.10-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:dba70f30fd81f8ce6d32ddeef37d91c8948e5d5a4c63242d16a2b2df8143aafc"}, - {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1f54b9b4b6c53369f40028d2dd07a8c374583417ee6ec0ea304e710a20f80a0"}, - {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fbb9dc00e39f3e6c0ef48edee202f9520dafb233e8b51b06b8428cfcb92abd30"}, - {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666abff54e474d28ff42756d94544cdfd42e2ee97065857413b72e8a2d6a6345"}, - {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5537f71b6d646f7f5f340562ec4c77b6e1c915f8baae822ea0b7e46c1f09b733"}, - {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2e07c6a26ed4bea91b897ee2b0835c21716d9a469a96c3e878dc5f8c55bb23"}, - {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ca5f18a75e1256ce07494e245cdb146f5a9267d3c702ebf9b65c7f8bd843431e"}, - {file = "regex-2021.11.10-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:74cbeac0451f27d4f50e6e8a8f3a52ca074b5e2da9f7b505c4201a57a8ed6286"}, - {file = "regex-2021.11.10-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:3598893bde43091ee5ca0a6ad20f08a0435e93a69255eeb5f81b85e81e329264"}, - {file = "regex-2021.11.10-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:50a7ddf3d131dc5633dccdb51417e2d1910d25cbcf842115a3a5893509140a3a"}, - {file = "regex-2021.11.10-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:61600a7ca4bcf78a96a68a27c2ae9389763b5b94b63943d5158f2a377e09d29a"}, - {file = "regex-2021.11.10-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:563d5f9354e15e048465061509403f68424fef37d5add3064038c2511c8f5e00"}, - {file = "regex-2021.11.10-cp36-cp36m-win32.whl", hash = "sha256:93a5051fcf5fad72de73b96f07d30bc29665697fb8ecdfbc474f3452c78adcf4"}, - {file = "regex-2021.11.10-cp36-cp36m-win_amd64.whl", hash = "sha256:b483c9d00a565633c87abd0aaf27eb5016de23fed952e054ecc19ce32f6a9e7e"}, - {file = "regex-2021.11.10-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fff55f3ce50a3ff63ec8e2a8d3dd924f1941b250b0aac3d3d42b687eeff07a8e"}, - {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e32d2a2b02ccbef10145df9135751abea1f9f076e67a4e261b05f24b94219e36"}, - {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53db2c6be8a2710b359bfd3d3aa17ba38f8aa72a82309a12ae99d3c0c3dcd74d"}, - {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2207ae4f64ad3af399e2d30dde66f0b36ae5c3129b52885f1bffc2f05ec505c8"}, - {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5ca078bb666c4a9d1287a379fe617a6dccd18c3e8a7e6c7e1eb8974330c626a"}, - {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd33eb9bdcfbabab3459c9ee651d94c842bc8a05fabc95edf4ee0c15a072495e"}, - {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05b7d6d7e64efe309972adab77fc2af8907bb93217ec60aa9fe12a0dad35874f"}, - {file = "regex-2021.11.10-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:42b50fa6666b0d50c30a990527127334d6b96dd969011e843e726a64011485da"}, - {file = "regex-2021.11.10-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6e1d2cc79e8dae442b3fa4a26c5794428b98f81389af90623ffcc650ce9f6732"}, - {file = "regex-2021.11.10-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:0416f7399e918c4b0e074a0f66e5191077ee2ca32a0f99d4c187a62beb47aa05"}, - {file = "regex-2021.11.10-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:ce298e3d0c65bd03fa65ffcc6db0e2b578e8f626d468db64fdf8457731052942"}, - {file = "regex-2021.11.10-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dc07f021ee80510f3cd3af2cad5b6a3b3a10b057521d9e6aaeb621730d320c5a"}, - {file = "regex-2021.11.10-cp37-cp37m-win32.whl", hash = "sha256:e71255ba42567d34a13c03968736c5d39bb4a97ce98188fafb27ce981115beec"}, - {file = "regex-2021.11.10-cp37-cp37m-win_amd64.whl", hash = "sha256:07856afef5ffcc052e7eccf3213317fbb94e4a5cd8177a2caa69c980657b3cb4"}, - {file = "regex-2021.11.10-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba05430e819e58544e840a68b03b28b6d328aff2e41579037e8bab7653b37d83"}, - {file = "regex-2021.11.10-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7f301b11b9d214f83ddaf689181051e7f48905568b0c7017c04c06dfd065e244"}, - {file = "regex-2021.11.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aaa4e0705ef2b73dd8e36eeb4c868f80f8393f5f4d855e94025ce7ad8525f50"}, - {file = "regex-2021.11.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:788aef3549f1924d5c38263104dae7395bf020a42776d5ec5ea2b0d3d85d6646"}, - {file = "regex-2021.11.10-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f8af619e3be812a2059b212064ea7a640aff0568d972cd1b9e920837469eb3cb"}, - {file = "regex-2021.11.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85bfa6a5413be0ee6c5c4a663668a2cad2cbecdee367630d097d7823041bdeec"}, - {file = "regex-2021.11.10-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f23222527b307970e383433daec128d769ff778d9b29343fb3496472dc20dabe"}, - {file = "regex-2021.11.10-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:da1a90c1ddb7531b1d5ff1e171b4ee61f6345119be7351104b67ff413843fe94"}, - {file = "regex-2021.11.10-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f5be7805e53dafe94d295399cfbe5227f39995a997f4fd8539bf3cbdc8f47ca8"}, - {file = "regex-2021.11.10-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a955b747d620a50408b7fdf948e04359d6e762ff8a85f5775d907ceced715129"}, - {file = "regex-2021.11.10-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:139a23d1f5d30db2cc6c7fd9c6d6497872a672db22c4ae1910be22d4f4b2068a"}, - {file = "regex-2021.11.10-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:ca49e1ab99593438b204e00f3970e7a5f70d045267051dfa6b5f4304fcfa1dbf"}, - {file = "regex-2021.11.10-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:96fc32c16ea6d60d3ca7f63397bff5c75c5a562f7db6dec7d412f7c4d2e78ec0"}, - {file = "regex-2021.11.10-cp38-cp38-win32.whl", hash = "sha256:0617383e2fe465732af4509e61648b77cbe3aee68b6ac8c0b6fe934db90be5cc"}, - {file = "regex-2021.11.10-cp38-cp38-win_amd64.whl", hash = "sha256:a3feefd5e95871872673b08636f96b61ebef62971eab044f5124fb4dea39919d"}, - {file = "regex-2021.11.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f7f325be2804246a75a4f45c72d4ce80d2443ab815063cdf70ee8fb2ca59ee1b"}, - {file = "regex-2021.11.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:537ca6a3586931b16a85ac38c08cc48f10fc870a5b25e51794c74df843e9966d"}, - {file = "regex-2021.11.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eef2afb0fd1747f33f1ee3e209bce1ed582d1896b240ccc5e2697e3275f037c7"}, - {file = "regex-2021.11.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:432bd15d40ed835a51617521d60d0125867f7b88acf653e4ed994a1f8e4995dc"}, - {file = "regex-2021.11.10-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b43c2b8a330a490daaef5a47ab114935002b13b3f9dc5da56d5322ff218eeadb"}, - {file = "regex-2021.11.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:962b9a917dd7ceacbe5cd424556914cb0d636001e393b43dc886ba31d2a1e449"}, - {file = "regex-2021.11.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fa8c626d6441e2d04b6ee703ef2d1e17608ad44c7cb75258c09dd42bacdfc64b"}, - {file = "regex-2021.11.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3c5fb32cc6077abad3bbf0323067636d93307c9fa93e072771cf9a64d1c0f3ef"}, - {file = "regex-2021.11.10-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cd410a1cbb2d297c67d8521759ab2ee3f1d66206d2e4328502a487589a2cb21b"}, - {file = "regex-2021.11.10-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e6096b0688e6e14af6a1b10eaad86b4ff17935c49aa774eac7c95a57a4e8c296"}, - {file = "regex-2021.11.10-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:529801a0d58809b60b3531ee804d3e3be4b412c94b5d267daa3de7fadef00f49"}, - {file = "regex-2021.11.10-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0f594b96fe2e0821d026365f72ac7b4f0b487487fb3d4aaf10dd9d97d88a9737"}, - {file = "regex-2021.11.10-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2409b5c9cef7054dde93a9803156b411b677affc84fca69e908b1cb2c540025d"}, - {file = "regex-2021.11.10-cp39-cp39-win32.whl", hash = "sha256:3b5df18db1fccd66de15aa59c41e4f853b5df7550723d26aa6cb7f40e5d9da5a"}, - {file = "regex-2021.11.10-cp39-cp39-win_amd64.whl", hash = "sha256:83ee89483672b11f8952b158640d0c0ff02dc43d9cb1b70c1564b49abe92ce29"}, - {file = "regex-2021.11.10.tar.gz", hash = "sha256:f341ee2df0999bfdf7a95e448075effe0db212a59387de1a70690e4acb03d4c6"}, -] requests = [ - {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, - {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, + {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, + {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, ] rfc3986 = [ {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, @@ -1477,108 +1361,93 @@ sniffio = [ {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"}, ] sqlalchemy = [ - {file = "SQLAlchemy-1.4.27-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:6afa9e4e63f066e0fd90a21db7e95e988d96127f52bfb298a0e9bec6999357a9"}, - {file = "SQLAlchemy-1.4.27-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ec1c908fa721f2c5684900cc8ff75555b1a5a2ae4f5a5694eb0e37a5263cea44"}, - {file = "SQLAlchemy-1.4.27-cp27-cp27m-win32.whl", hash = "sha256:0438bccc16349db2d5203598be6073175ce16d4e53b592d6e6cef880c197333e"}, - {file = "SQLAlchemy-1.4.27-cp27-cp27m-win_amd64.whl", hash = "sha256:435b1980c1333ffe3ab386ad28d7b209590b0fa83ea8544d853e7a22f957331b"}, - {file = "SQLAlchemy-1.4.27-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:486f7916ef77213103467924ef25f5ea1055ae901f385fe4d707604095fdf6a9"}, - {file = "SQLAlchemy-1.4.27-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:d81c84c9d2523b3ea20f8e3aceea68615768a7464c0f9a9899600ce6592ec570"}, - {file = "SQLAlchemy-1.4.27-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5881644fc51af7b232ab8d64f75c0f32295dfe88c2ee188023795cdbd4cf99b"}, - {file = "SQLAlchemy-1.4.27-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:24828c5e74882cf41516740c0b150702bee4c6817d87d5c3d3bafef2e6896f80"}, - {file = "SQLAlchemy-1.4.27-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7d0a1b1258efff7d7f2e6cfa56df580d09ba29d35a1e3f604f867e1f685feb2"}, - {file = "SQLAlchemy-1.4.27-cp310-cp310-win32.whl", hash = "sha256:aadc6d1e58e14010ae4764d1ba1fd0928dbb9423b27a382ea3a1444f903f4084"}, - {file = "SQLAlchemy-1.4.27-cp310-cp310-win_amd64.whl", hash = "sha256:9134e5810262203388b203c2022bbcbf1a22e89861eef9340e772a73dd9076fa"}, - {file = "SQLAlchemy-1.4.27-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:fa52534076394af7315306a8701b726a6521b591d95e8f4e5121c82f94790e8d"}, - {file = "SQLAlchemy-1.4.27-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2717ceae35e71de1f58b0d1ee7e773d3aab5c403c6e79e8d262277c7f7f95269"}, - {file = "SQLAlchemy-1.4.27-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2e93624d186ea7a738ada47314701c8830e0e4b021a6bce7fbe6f39b87ee1516"}, - {file = "SQLAlchemy-1.4.27-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:987fe2f84ceaf744fa0e48805152abe485a9d7002c9923b18a4b2529c7bff218"}, - {file = "SQLAlchemy-1.4.27-cp36-cp36m-win32.whl", hash = "sha256:2146ef996181e3d4dd20eaf1d7325eb62d6c8aa4dc1677c1872ddfa8561a47d9"}, - {file = "SQLAlchemy-1.4.27-cp36-cp36m-win_amd64.whl", hash = "sha256:ad8ec6b69d03e395db48df8991aa15fce3cd23e378b73e01d46a26a6efd5c26d"}, - {file = "SQLAlchemy-1.4.27-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:52f23a76544ed29573c0f3ee41f0ca1aedbab3a453102b60b540cc6fa55448ad"}, - {file = "SQLAlchemy-1.4.27-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd421a14edf73cfe01e8f51ed8966294ee3b3db8da921cacc88e497fd6e977af"}, - {file = "SQLAlchemy-1.4.27-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:10230364479429437f1b819a8839f1edc5744c018bfeb8d01320930f97695bc9"}, - {file = "SQLAlchemy-1.4.27-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78943451ab3ffd0e27876f9cea2b883317518b418f06b90dadf19394534637e9"}, - {file = "SQLAlchemy-1.4.27-cp37-cp37m-win32.whl", hash = "sha256:a81e40dfa50ed3c472494adadba097640bfcf43db160ed783132045eb2093cb1"}, - {file = "SQLAlchemy-1.4.27-cp37-cp37m-win_amd64.whl", hash = "sha256:015511c52c650eebf1059ed8a21674d9d4ae567ebfd80fc73f8252faccd71864"}, - {file = "SQLAlchemy-1.4.27-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:cc49fb8ff103900c20e4a9c53766c82a7ebbc183377fb357a8298bad216e9cdd"}, - {file = "SQLAlchemy-1.4.27-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9369f927f4d19b58322cfea8a51710a3f7c47a0e7f3398d94a4632760ecd74f6"}, - {file = "SQLAlchemy-1.4.27-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6510f4a5029643301bdfe56b61e806093af2101d347d485c42a5535847d2c699"}, - {file = "SQLAlchemy-1.4.27-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:771eca9872b47a629010665ff92de1c248a6979b8d1603daced37773d6f6e365"}, - {file = "SQLAlchemy-1.4.27-cp38-cp38-win32.whl", hash = "sha256:4d1d707b752137e6bf45720648e1b828d5e4881d690df79cca07f7217ea06365"}, - {file = "SQLAlchemy-1.4.27-cp38-cp38-win_amd64.whl", hash = "sha256:c035184af4e58e154b0977eea52131edd096e0754a88f7d5a847e7ccb3510772"}, - {file = "SQLAlchemy-1.4.27-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:bac949be7579fed824887eed6672f44b7c4318abbfb2004b2c6968818b535a2f"}, - {file = "SQLAlchemy-1.4.27-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ac8306e04275d382d6393e557047b0a9d7ddf9f7ca5da9b3edbd9323ea75bd9"}, - {file = "SQLAlchemy-1.4.27-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8327e468b1775c0dfabc3d01f39f440585bf4d398508fcbbe2f0d931c502337d"}, - {file = "SQLAlchemy-1.4.27-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b02eee1577976acb4053f83d32b7826424f8b9f70809fa756529a52c6537eda4"}, - {file = "SQLAlchemy-1.4.27-cp39-cp39-win32.whl", hash = "sha256:5beeff18b4e894f6cb73c8daf2c0d8768844ef40d97032bb187d75b1ec8de24b"}, - {file = "SQLAlchemy-1.4.27-cp39-cp39-win_amd64.whl", hash = "sha256:8dbe5f639e6d035778ebf700be6d573f82a13662c3c2c3aa0f1dba303b942806"}, - {file = "SQLAlchemy-1.4.27.tar.gz", hash = "sha256:d768359daeb3a86644f3854c6659e4496a3e6bba2b4651ecc87ce7ad415b320c"}, -] -sqlalchemy-stubs = [ - {file = "sqlalchemy-stubs-0.4.tar.gz", hash = "sha256:c665d6dd4482ef642f01027fa06c3d5e91befabb219dc71fc2a09e7d7695f7ae"}, - {file = "sqlalchemy_stubs-0.4-py3-none-any.whl", hash = "sha256:5eec7aa110adf9b957b631799a72fef396b23ff99fe296df726645d01e312aa5"}, + {file = "SQLAlchemy-1.4.29-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:da64423c05256f4ab8c0058b90202053b201cbe3a081f3a43eb590cd554395ab"}, + {file = "SQLAlchemy-1.4.29-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0fc4eec2f46b40bdd42112b3be3fbbf88e194bcf02950fbb88bcdc1b32f07dc7"}, + {file = "SQLAlchemy-1.4.29-cp27-cp27m-win32.whl", hash = "sha256:101d2e100ba9182c9039699588e0b2d833c54b3bad46c67c192159876c9f27ea"}, + {file = "SQLAlchemy-1.4.29-cp27-cp27m-win_amd64.whl", hash = "sha256:ceac84dd9abbbe115e8be0c817bed85d9fa639b4d294e7817f9e61162d5f766c"}, + {file = "SQLAlchemy-1.4.29-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:15b65887b6c324cad638c7671cb95985817b733242a7eb69edd7cdf6953be1e0"}, + {file = "SQLAlchemy-1.4.29-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:78abc507d17753ed434b6cc0c0693126279723d5656d9775bfcac966a99a899b"}, + {file = "SQLAlchemy-1.4.29-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb8c993706e86178ce15a6b86a335a2064f52254b640e7f53365e716423d33f4"}, + {file = "SQLAlchemy-1.4.29-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:804e22d5b6165a4f3f019dd9c94bec5687de985a9c54286b93ded9f7846b8c82"}, + {file = "SQLAlchemy-1.4.29-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56d9d62021946263d4478c9ca012fbd1805f10994cb615c88e7bfd1ae14604d8"}, + {file = "SQLAlchemy-1.4.29-cp310-cp310-win32.whl", hash = "sha256:027f356c727db24f3c75828c7feb426f87ce1241242d08958e454bd025810660"}, + {file = "SQLAlchemy-1.4.29-cp310-cp310-win_amd64.whl", hash = "sha256:debaf09a823061f88a8dee04949814cf7e82fb394c5bca22c780cb03172ca23b"}, + {file = "SQLAlchemy-1.4.29-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:dc27dcc6c72eb38be7f144e9c2c4372d35a3684d3a6dd43bd98c1238358ee17c"}, + {file = "SQLAlchemy-1.4.29-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4ddd4f2e247128c58bb3dd4489922874afce157d2cff0b2295d67fcd0f22494"}, + {file = "SQLAlchemy-1.4.29-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9ce960a1dc60524136cf6f75621588e2508a117e04a6e3eedb0968bd13b8c824"}, + {file = "SQLAlchemy-1.4.29-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5919e647e1d4805867ea556ed4967c68b4d8b266059fa35020dbaed8ffdd60f3"}, + {file = "SQLAlchemy-1.4.29-cp36-cp36m-win32.whl", hash = "sha256:886359f734b95ad1ef443b13bb4518bcade4db4f9553c9ce33d6d04ebda8d44e"}, + {file = "SQLAlchemy-1.4.29-cp36-cp36m-win_amd64.whl", hash = "sha256:e9cc6d844e24c307c3272677982a9b33816aeb45e4977791c3bdd47637a8d810"}, + {file = "SQLAlchemy-1.4.29-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:5e9cd33459afa69c88fa648e803d1f1245e3caa60bfe8b80a9595e5edd3bda9c"}, + {file = "SQLAlchemy-1.4.29-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eeaebceb24b46e884c4ad3c04f37feb178b81f6ce720af19bfa2592ca32fdef7"}, + {file = "SQLAlchemy-1.4.29-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e89347d3bd2ef873832b47e85f4bbd810a5e626c5e749d90a07638da100eb1c8"}, + {file = "SQLAlchemy-1.4.29-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a717c2e70fd1bb477161c4cc85258e41d978584fbe5522613618195f7e87d9b"}, + {file = "SQLAlchemy-1.4.29-cp37-cp37m-win32.whl", hash = "sha256:f74d6c05d2d163464adbdfbc1ab85048cc15462ff7d134b8aed22bd521e1faa5"}, + {file = "SQLAlchemy-1.4.29-cp37-cp37m-win_amd64.whl", hash = "sha256:621854dbb4d2413c759a5571564170de45ef37299df52e78e62b42e2880192e1"}, + {file = "SQLAlchemy-1.4.29-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:f3909194751bb6cb7c5511dd18bcf77e6e3f0b31604ed4004dffa9461f71e737"}, + {file = "SQLAlchemy-1.4.29-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd49d21d1f03c81fbec9080ecdc4486d5ddda67e7fbb75ebf48294465c022cdc"}, + {file = "SQLAlchemy-1.4.29-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e5f6959466a42b6569774c257e55f9cd85200d5b0ba09f0f5d8b5845349c5822"}, + {file = "SQLAlchemy-1.4.29-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0072f9887aabe66db23f818bbe950cfa1b6127c5cb769b00bcc07935b3adb0ad"}, + {file = "SQLAlchemy-1.4.29-cp38-cp38-win32.whl", hash = "sha256:ad618d687d26d4cbfa9c6fa6141d59e05bcdfc60cb6e1f1d3baa18d8c62fef5f"}, + {file = "SQLAlchemy-1.4.29-cp38-cp38-win_amd64.whl", hash = "sha256:878daecb6405e786b07f97e1c77a9cfbbbec17432e8a90c487967e32cfdecb33"}, + {file = "SQLAlchemy-1.4.29-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:e027bdf0a4cf6bd0a3ad3b998643ea374d7991bd117b90bf9982e41ceb742941"}, + {file = "SQLAlchemy-1.4.29-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5de7adfb91d351f44062b8dedf29f49d4af7cb765be65816e79223a4e31062b"}, + {file = "SQLAlchemy-1.4.29-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fbc6e63e481fa323036f305ada96a3362e1d60dd2bfa026cac10c3553e6880e9"}, + {file = "SQLAlchemy-1.4.29-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7dd0502cb091660ad0d89c5e95a29825f37cde2a5249957838e975871fbffaad"}, + {file = "SQLAlchemy-1.4.29-cp39-cp39-win32.whl", hash = "sha256:37b46bfc4af3dc226acb6fa28ecd2e1fd223433dc5e15a2bad62bf0a0cbb4e8b"}, + {file = "SQLAlchemy-1.4.29-cp39-cp39-win_amd64.whl", hash = "sha256:08cfd35eecaba79be930c9bfd2e1f0c67a7e1314355d83a378f9a512b1cf7587"}, + {file = "SQLAlchemy-1.4.29.tar.gz", hash = "sha256:fa2bad14e1474ba649cfc969c1d2ec915dd3e79677f346bbfe08e93ef9020b39"}, ] sqlalchemy2-stubs = [ {file = "sqlalchemy2-stubs-0.0.2a19.tar.gz", hash = "sha256:2117c48ce5acfe33bf9c9bfce2a981632d931949e68fa313aa5c2a3bc980ca7a"}, {file = "sqlalchemy2_stubs-0.0.2a19-py3-none-any.whl", hash = "sha256:aac7dca77a2c49e5f0934976421d5e25ae4dc5e27db48c01e055f81caa1e3ead"}, ] starlette = [ - {file = "starlette-0.14.2-py3-none-any.whl", hash = "sha256:3c8e48e52736b3161e34c9f0e8153b4f32ec5d8995a3ee1d59410d92f75162ed"}, - {file = "starlette-0.14.2.tar.gz", hash = "sha256:7d49f4a27f8742262ef1470608c59ddbc66baf37c148e938c7038e6bc7a998aa"}, + {file = "starlette-0.17.1-py3-none-any.whl", hash = "sha256:26a18cbda5e6b651c964c12c88b36d9898481cd428ed6e063f5f29c418f73050"}, + {file = "starlette-0.17.1.tar.gz", hash = "sha256:57eab3cc975a28af62f6faec94d355a410634940f10b30d68d31cb5ec1b44ae8"}, ] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] tomli = [ - {file = "tomli-1.2.2-py3-none-any.whl", hash = "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade"}, - {file = "tomli-1.2.2.tar.gz", hash = "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee"}, + {file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"}, + {file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"}, ] typed-ast = [ - {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"}, - {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"}, - {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"}, - {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"}, - {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"}, - {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"}, - {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"}, - {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"}, - {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"}, - {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"}, - {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"}, - {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"}, - {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"}, - {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"}, - {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"}, - {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"}, - {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"}, - {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"}, - {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"}, - {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"}, - {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"}, - {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"}, - {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"}, - {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"}, - {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"}, - {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"}, - {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"}, - {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"}, - {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, - {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, + {file = "typed_ast-1.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d8314c92414ce7481eee7ad42b353943679cf6f30237b5ecbf7d835519e1212"}, + {file = "typed_ast-1.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b53ae5de5500529c76225d18eeb060efbcec90ad5e030713fe8dab0fb4531631"}, + {file = "typed_ast-1.5.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:24058827d8f5d633f97223f5148a7d22628099a3d2efe06654ce872f46f07cdb"}, + {file = "typed_ast-1.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:a6d495c1ef572519a7bac9534dbf6d94c40e5b6a608ef41136133377bba4aa08"}, + {file = "typed_ast-1.5.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:de4ecae89c7d8b56169473e08f6bfd2df7f95015591f43126e4ea7865928677e"}, + {file = "typed_ast-1.5.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:256115a5bc7ea9e665c6314ed6671ee2c08ca380f9d5f130bd4d2c1f5848d695"}, + {file = "typed_ast-1.5.1-cp36-cp36m-win_amd64.whl", hash = "sha256:7c42707ab981b6cf4b73490c16e9d17fcd5227039720ca14abe415d39a173a30"}, + {file = "typed_ast-1.5.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:71dcda943a471d826ea930dd449ac7e76db7be778fcd722deb63642bab32ea3f"}, + {file = "typed_ast-1.5.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4f30a2bcd8e68adbb791ce1567fdb897357506f7ea6716f6bbdd3053ac4d9471"}, + {file = "typed_ast-1.5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ca9e8300d8ba0b66d140820cf463438c8e7b4cdc6fd710c059bfcfb1531d03fb"}, + {file = "typed_ast-1.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9caaf2b440efb39ecbc45e2fabde809cbe56272719131a6318fd9bf08b58e2cb"}, + {file = "typed_ast-1.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c9bcad65d66d594bffab8575f39420fe0ee96f66e23c4d927ebb4e24354ec1af"}, + {file = "typed_ast-1.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:591bc04e507595887160ed7aa8d6785867fb86c5793911be79ccede61ae96f4d"}, + {file = "typed_ast-1.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:a80d84f535642420dd17e16ae25bb46c7f4c16ee231105e7f3eb43976a89670a"}, + {file = "typed_ast-1.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:38cf5c642fa808300bae1281460d4f9b7617cf864d4e383054a5ef336e344d32"}, + {file = "typed_ast-1.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b6ab14c56bc9c7e3c30228a0a0b54b915b1579613f6e463ba6f4eb1382e7fd4"}, + {file = "typed_ast-1.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a2b8d7007f6280e36fa42652df47087ac7b0a7d7f09f9468f07792ba646aac2d"}, + {file = "typed_ast-1.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:b6d17f37f6edd879141e64a5db17b67488cfeffeedad8c5cec0392305e9bc775"}, + {file = "typed_ast-1.5.1.tar.gz", hash = "sha256:484137cab8ecf47e137260daa20bafbba5f4e3ec7fda1c1e69ab299b75fa81c5"}, ] typing-extensions = [ - {file = "typing_extensions-4.0.0-py3-none-any.whl", hash = "sha256:829704698b22e13ec9eaf959122315eabb370b0884400e9818334d8b677023d9"}, - {file = "typing_extensions-4.0.0.tar.gz", hash = "sha256:2cdf80e4e04866a9b3689a51869016d36db0814d84b8d8a568d22781d45d27ed"}, + {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, + {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, ] urllib3 = [ {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"}, ] uvicorn = [ - {file = "uvicorn-0.15.0-py3-none-any.whl", hash = "sha256:17f898c64c71a2640514d4089da2689e5db1ce5d4086c2d53699bf99513421c1"}, - {file = "uvicorn-0.15.0.tar.gz", hash = "sha256:d9a3c0dd1ca86728d3e235182683b4cf94cd53a867c288eaeca80ee781b2caff"}, + {file = "uvicorn-0.16.0-py3-none-any.whl", hash = "sha256:d8c839231f270adaa6d338d525e2652a0b4a5f4c2430b5c4ef6ae4d11776b0d2"}, + {file = "uvicorn-0.16.0.tar.gz", hash = "sha256:eacb66afa65e0648fcbce5e746b135d09722231ffffc61883d4fac2b62fbea8d"}, ] zipp = [ - {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"}, - {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"}, + {file = "zipp-3.7.0-py3-none-any.whl", hash = "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"}, + {file = "zipp-3.7.0.tar.gz", hash = "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d"}, ] diff --git a/{{cookiecutter.project_name}}/pre-push.sh b/{{cookiecutter.project_name}}/template_fastapi_users/pre-push.sh similarity index 100% rename from {{cookiecutter.project_name}}/pre-push.sh rename to {{cookiecutter.project_name}}/template_fastapi_users/pre-push.sh diff --git a/{{cookiecutter.project_name}}/pyproject.toml b/{{cookiecutter.project_name}}/template_fastapi_users/pyproject.toml similarity index 53% rename from {{cookiecutter.project_name}}/pyproject.toml rename to {{cookiecutter.project_name}}/template_fastapi_users/pyproject.toml index 5d866a8..c084bba 100644 --- a/{{cookiecutter.project_name}}/pyproject.toml +++ b/{{cookiecutter.project_name}}/template_fastapi_users/pyproject.toml @@ -5,29 +5,28 @@ description = "FastAPI project generated using minimal-fastapi-postgres-template authors = ["admin "] [tool.poetry.dependencies] -python = "^3.7" -fastapi = "^0.68.1" -SQLAlchemy = {extras = ["asyncio"], version = "^1.4.26"} -uvicorn = "^0.15.0" -python-dotenv = "^0.19.0" -requests = "^2.26.0" -python-jose = {version = "^3.3.0", extras = ["cryptography"]} -passlib = {version = "^1.7.4", extras = ["bcrypt"]} -pydantic = {version = "^1.8.2", extras = ["email"]} -alembic = "^1.7.3" +python = "^3.9" +fastapi = "^0.70.1,<0.71.0" +SQLAlchemy = {extras = ["asyncio"], version = "^1.4.29"} +uvicorn = "^0.16.0" +python-dotenv = "^0.19.2" +requests = "^2.27.1" +python-jose = {extras = ["cryptography"], version = "^3.3.0"} +passlib = {extras = ["bcrypt"], version = "^1.7.4"} +pydantic = {extras = ["email"], version = "^1.9.0"} +alembic = "^1.7.5" python-multipart = "^0.0.5" -asyncpg = "^0.24.0" +asyncpg = "^0.25.0" [tool.poetry.dev-dependencies] -black = {version = "^21.9b0", python = ">=3.6.2,<4.0.0"} +black = {version = "^21.12b0", allow-prereleases = true} autoflake = "^1.4" -flake8 = "^3.9.2" -isort = "^5.9.3" -coverage = "^5.5" +flake8 = "^4.0.1" +isort = "^5.10.1" +coverage = "^6.2" pytest = "^6.2.5" pytest-asyncio = "^0.16.0" -httpx = "^0.20.0" -sqlalchemy-stubs = "^0.4" +httpx = "^0.21.3" sqlalchemy2-stubs = "^0.0.2-alpha.19" [build-system] diff --git a/{{cookiecutter.project_name}}/requirements-dev.txt b/{{cookiecutter.project_name}}/template_fastapi_users/requirements-dev.txt similarity index 100% rename from {{cookiecutter.project_name}}/requirements-dev.txt rename to {{cookiecutter.project_name}}/template_fastapi_users/requirements-dev.txt diff --git a/{{cookiecutter.project_name}}/requirements.txt b/{{cookiecutter.project_name}}/template_fastapi_users/requirements.txt similarity index 100% rename from {{cookiecutter.project_name}}/requirements.txt rename to {{cookiecutter.project_name}}/template_fastapi_users/requirements.txt diff --git a/{{cookiecutter.project_name}}/template_minimal/.env.example b/{{cookiecutter.project_name}}/template_minimal/.env.example new file mode 100644 index 0000000..6442612 --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/.env.example @@ -0,0 +1,20 @@ +SECRET_KEY="DVnFmhwvjEhJZpuhndxjhlezxQPJmBIIkMDEmFREWQADPcUnrG" +ENVIRONMENT="DEV" +ACCESS_TOKEN_EXPIRE_MINUTES="11520" +REFRESH_TOKEN_EXPIRE_MINUTES="40320" +BACKEND_CORS_ORIGINS="http://localhost:3000,http://localhost:8001" + +DEFAULT_DATABASE_HOSTNAME="localhost" +DEFAULT_DATABASE_USER="rDGJeEDqAz" +DEFAULT_DATABASE_PASSWORD="XsPQhCoEfOQZueDjsILetLDUvbvSxAMnrVtgVZpmdcSssUgbvs" +DEFAULT_DATABASE_PORT="5387" +DEFAULT_DATABASE_DB="default_db" + +TEST_DATABASE_HOSTNAME="localhost" +TEST_DATABASE_USER="test" +TEST_DATABASE_PASSWORD="ywRCUjJijmQoBmWxIfLldOoITPzajPSNvTvHyugQoSqGwNcvQE" +TEST_DATABASE_PORT="37270" +TEST_DATABASE_DB="test_db" + +FIRST_SUPERUSER_EMAIL="example@example.com" +FIRST_SUPERUSER_PASSWORD="OdLknKQJMUwuhpAVHvRC" \ No newline at end of file diff --git a/{{cookiecutter.project_name}}/template_minimal/.env.template b/{{cookiecutter.project_name}}/template_minimal/.env.template new file mode 100644 index 0000000..f392a2f --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/.env.template @@ -0,0 +1,20 @@ +SECRET_KEY="{{ random_ascii_string(50) }}" +ENVIRONMENT="DEV" +ACCESS_TOKEN_EXPIRE_MINUTES="11520" +REFRESH_TOKEN_EXPIRE_MINUTES="40320" +BACKEND_CORS_ORIGINS="http://localhost:3000,http://localhost:8001" + +DEFAULT_DATABASE_HOSTNAME="localhost" +DEFAULT_DATABASE_USER="{{ random_ascii_string(10) }}" +DEFAULT_DATABASE_PASSWORD="{{ random_ascii_string(50) }}" +DEFAULT_DATABASE_PORT="{{ range(4000, 7000) | random }}" +DEFAULT_DATABASE_DB="default_db" + +TEST_DATABASE_HOSTNAME="localhost" +TEST_DATABASE_USER="test" +TEST_DATABASE_PASSWORD="{{ random_ascii_string(50) }}" +TEST_DATABASE_PORT="{{ range(30000, 40000) | random }}" +TEST_DATABASE_DB="test_db" + +FIRST_SUPERUSER_EMAIL="example@example.com" +FIRST_SUPERUSER_PASSWORD="{{ random_ascii_string(20) }}" \ No newline at end of file diff --git a/{{cookiecutter.project_name}}/template_minimal/.flake8 b/{{cookiecutter.project_name}}/template_minimal/.flake8 new file mode 100644 index 0000000..6b8856f --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/.flake8 @@ -0,0 +1,11 @@ +[flake8] +max-line-length = 88 +select = C,E,F,W,B,B9 +ignore = E203, E501, W503 +exclude = + __init__.py, + .venv, + venv, + __pycache__, + .github, + .vscode, diff --git a/{{cookiecutter.project_name}}/template_minimal/.gitignore b/{{cookiecutter.project_name}}/template_minimal/.gitignore new file mode 100644 index 0000000..32562ce --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/.gitignore @@ -0,0 +1,136 @@ + +# postgresql +data +default_database_data +test_database_data + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class +.env + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +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/ + +# 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 +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.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 + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments + +.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/ \ No newline at end of file diff --git a/{{cookiecutter.project_name}}/template_minimal/Dockerfile b/{{cookiecutter.project_name}}/template_minimal/Dockerfile new file mode 100644 index 0000000..1461692 --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/Dockerfile @@ -0,0 +1,29 @@ +# See https://unit.nginx.org/installation/#docker-images + +FROM nginx/unit:1.26.1-python3.9 + +ENV PYTHONUNBUFFERED 1 + +RUN apt update && apt install -y python3-pip + +# Build folder for our app, only stuff that matters copied. +RUN mkdir build +WORKDIR /build + +# Update, install requirements and then cleanup. +COPY ./requirements.txt . + +RUN pip3 install -r requirements.txt \ + && apt remove -y python3-pip \ + && apt autoremove --purge -y \ + && rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/*.list + +# Copy the rest of app +COPY ./app ./app +COPY ./alembic ./alembic +COPY ./alembic.ini . + +# Nginx unit config and init.sh will be consumed at container startup. +COPY ./app/init.sh /docker-entrypoint.d/init.sh +COPY ./nginx-unit-config.json /docker-entrypoint.d/config.json +RUN chmod a+x /docker-entrypoint.d/init.sh \ No newline at end of file diff --git a/{{cookiecutter.project_name}}/template_minimal/alembic.ini b/{{cookiecutter.project_name}}/template_minimal/alembic.ini new file mode 100644 index 0000000..61630d3 --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/alembic.ini @@ -0,0 +1,100 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = alembic + +# template used to generate migration files +file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d_%%(slug)s__%%(rev)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python-dateutil library that can be +# installed by adding `alembic[tz]` to the pip requirements +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" +# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. Valid values are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +version_path_separator = os # default: use os.pathsep + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = driver://user:pass@localhost/dbname + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[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 + +[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/{{cookiecutter.project_name}}/template_minimal/alembic/README b/{{cookiecutter.project_name}}/template_minimal/alembic/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/{{cookiecutter.project_name}}/template_minimal/alembic/env.py b/{{cookiecutter.project_name}}/template_minimal/alembic/env.py new file mode 100644 index 0000000..6e02264 --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/alembic/env.py @@ -0,0 +1,96 @@ +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool +from sqlalchemy.ext.asyncio import AsyncEngine +from asyncio import get_event_loop + +from app.core import config as app_config +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) # type: ignore + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +from app.models import Base + +target_metadata = Base.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 get_database_uri(): + return app_config.settings.DEFAULT_SQLALCHEMY_DATABASE_URI + + +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 = get_database_uri() + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + compare_type=True, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def do_run_migrations(connection): + context.configure( + connection=connection, target_metadata=target_metadata, compare_type=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +async 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. + + """ + configuration = config.get_section(config.config_ini_section) + assert configuration + configuration["sqlalchemy.url"] = get_database_uri() + connectable = AsyncEngine( + engine_from_config( + configuration, + prefix="sqlalchemy.", + poolclass=pool.NullPool, + future=True, + ) # type: ignore + ) + async with connectable.connect() as connection: + await connection.run_sync(do_run_migrations) + + +if context.is_offline_mode(): + run_migrations_offline() +else: + get_event_loop().run_until_complete(run_migrations_online()) diff --git a/{{cookiecutter.project_name}}/template_minimal/alembic/script.py.mako b/{{cookiecutter.project_name}}/template_minimal/alembic/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/alembic/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/{{cookiecutter.project_name}}/template_minimal/alembic/versions/2021_11_09_1736_init__cefce371682e.py b/{{cookiecutter.project_name}}/template_minimal/alembic/versions/2021_11_09_1736_init__cefce371682e.py new file mode 100644 index 0000000..bae5d4b --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/alembic/versions/2021_11_09_1736_init__cefce371682e.py @@ -0,0 +1,38 @@ +"""init + +Revision ID: cefce371682e +Revises: +Create Date: 2021-11-09 17:36:41.970204 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'cefce371682e' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('user', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('full_name', sa.String(length=254), nullable=True), + sa.Column('email', sa.String(length=254), nullable=False), + sa.Column('hashed_password', sa.String(length=128), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_user_email'), 'user', ['email'], unique=True) + op.create_index(op.f('ix_user_id'), 'user', ['id'], unique=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_user_id'), table_name='user') + op.drop_index(op.f('ix_user_email'), table_name='user') + op.drop_table('user') + # ### end Alembic commands ### diff --git a/{{cookiecutter.project_name}}/template_minimal/app/__init__.py b/{{cookiecutter.project_name}}/template_minimal/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/{{cookiecutter.project_name}}/template_minimal/app/api/__init__.py b/{{cookiecutter.project_name}}/template_minimal/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/{{cookiecutter.project_name}}/template_minimal/app/api/api.py b/{{cookiecutter.project_name}}/template_minimal/app/api/api.py new file mode 100644 index 0000000..79b9497 --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/app/api/api.py @@ -0,0 +1,7 @@ +from fastapi import APIRouter + +from app.api.endpoints import auth, users + +api_router = APIRouter() +api_router.include_router(auth.router, prefix="/auth", tags=["auth"]) +api_router.include_router(users.router, prefix="/users", tags=["users"]) diff --git a/{{cookiecutter.project_name}}/template_minimal/app/api/deps.py b/{{cookiecutter.project_name}}/template_minimal/app/api/deps.py new file mode 100644 index 0000000..a8fc916 --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/app/api/deps.py @@ -0,0 +1,43 @@ +from typing import AsyncGenerator, Optional + +from fastapi import Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer +from jose import jwt +from pydantic import ValidationError +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app import schemas +from app.core import security, config +from app.models import User +from app.session import async_session + +reusable_oauth2 = OAuth2PasswordBearer(tokenUrl="auth/access-token") + + +async def get_session() -> AsyncGenerator[AsyncSession, None]: + async with async_session() as session: + yield session + + +async def get_current_user( + session: AsyncSession = Depends(get_session), token: str = Depends(reusable_oauth2) +) -> User: + + try: + payload = jwt.decode( + token, config.settings.SECRET_KEY, algorithms=[security.ALGORITHM] + ) + token_data = schemas.TokenPayload(**payload) + except (jwt.JWTError, ValidationError): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Could not validate credentials", + ) + + result = await session.execute(select(User).where(User.id == token_data.sub)) + user: Optional[User] = result.scalars().first() + + if not user: + raise HTTPException(status_code=404, detail="User not found") + return user diff --git a/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/__init__.py b/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/auth.py b/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/auth.py new file mode 100644 index 0000000..90706bb --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/auth.py @@ -0,0 +1,91 @@ +from typing import Optional + +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.security import OAuth2PasswordRequestForm +from jose import jwt +from pydantic import ValidationError +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app import schemas +from app.api import deps +from app.core import security, config +from app.models import User + +router = APIRouter() + + +@router.post("/access-token", response_model=schemas.Token) +async def login_access_token( + session: AsyncSession = Depends(deps.get_session), + form_data: OAuth2PasswordRequestForm = Depends(), +): + """ + OAuth2 compatible token, get an access token for future requests using username and password + """ + result = await session.execute(select(User).where(User.email == form_data.username)) + user: Optional[User] = result.scalars().first() + if user is None: + raise HTTPException(status_code=400, detail="Incorrect email or password") + + if not security.verify_password(form_data.password, user.hashed_password): # type: ignore + raise HTTPException(status_code=400, detail="Incorrect email or password") + + access_token, expire_at = security.create_access_token(user.id) + refresh_token, refresh_expire_at = security.create_refresh_token(user.id) + return { + "token_type": "bearer", + "access_token": access_token, + "expire_at": expire_at, + "refresh_token": refresh_token, + "refresh_expire_at": refresh_expire_at, + } + + +@router.post("/test-token", response_model=schemas.User) +async def test_token(current_user: User = Depends(deps.get_current_user)): + """ + Test access token for current user + """ + return current_user + + +@router.post("/refresh-token", response_model=schemas.Token) +async def refresh_token( + input: schemas.TokenRefresh, session: AsyncSession = Depends(deps.get_session) +): + """ + OAuth2 compatible token, get an access token for future requests using refresh token + """ + try: + payload = jwt.decode( + input.refresh_token, + config.settings.SECRET_KEY, + algorithms=[security.ALGORITHM], + ) + token_data = schemas.TokenPayload(**payload) + except (jwt.JWTError, ValidationError): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Could not validate credentials", + ) + if not token_data.refresh: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Could not validate credentials", + ) + result = await session.execute(select(User).where(User.id == token_data.sub)) + user: Optional[User] = result.scalars().first() + + if user is None: + raise HTTPException(status_code=404, detail="User not found") + + access_token, expire_at = security.create_access_token(user.id) + refresh_token, refresh_expire_at = security.create_refresh_token(user.id) + return { + "token_type": "bearer", + "access_token": access_token, + "expire_at": expire_at, + "refresh_token": refresh_token, + "refresh_expire_at": refresh_expire_at, + } diff --git a/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/users.py b/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/users.py new file mode 100644 index 0000000..8b9bc09 --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/users.py @@ -0,0 +1,41 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.ext.asyncio import AsyncSession + +from app import models, schemas +from app.api import deps +from app.core.security import get_password_hash + +router = APIRouter() + + +@router.put("/me", response_model=schemas.User) +async def update_user_me( + user_update: schemas.UserUpdate, + session: AsyncSession = Depends(deps.get_session), + current_user: models.User = Depends(deps.get_current_user), +): + """ + Update current user. + """ + if user_update.password is not None: + current_user.hashed_password = get_password_hash(user_update.password) # type: ignore + if user_update.full_name is not None: + current_user.full_name = user_update.full_name # type: ignore + if user_update.email is not None: + current_user.email = user_update.email # type: ignore + + session.add(current_user) + await session.commit() + await session.refresh(current_user) + + return current_user + + +@router.get("/me", response_model=schemas.User) +async def read_user_me( + current_user: models.User = Depends(deps.get_current_user), +): + """ + Get current user. + """ + return current_user diff --git a/{{cookiecutter.project_name}}/template_minimal/app/conftest.py b/{{cookiecutter.project_name}}/template_minimal/app/conftest.py new file mode 100644 index 0000000..ea7b91d --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/app/conftest.py @@ -0,0 +1,13 @@ +""" +Used to execute code before running tests, in this case we want to use test database. +We don't want to mess up dev database. + +Put here any Pytest related code (it will be executed before `app/tests/...`) +""" + +import os + +# This will ensure using test database +os.environ["ENVIRONMENT"] = "PYTEST" +# This will change default 12 bcrypt rounds to only 1 so hashing password func will be short +os.environ["SECURITY_BCRYPT_DEFAULT_ROUNDS"] = "1" diff --git a/{{cookiecutter.project_name}}/template_minimal/app/core/__init__.py b/{{cookiecutter.project_name}}/template_minimal/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/{{cookiecutter.project_name}}/template_minimal/app/core/config.py b/{{cookiecutter.project_name}}/template_minimal/app/core/config.py new file mode 100644 index 0000000..4f0a92e --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/app/core/config.py @@ -0,0 +1,101 @@ +""" +File with environment variables and general configuration logic. +`SECRET_KEY`, `ENVIRONMENT` etc. map to env variables with the same names. + +Pydantic priority ordering: + +1. (Most important, will overwrite everything) - environment variables +2. `.env` file in root folder of project +3. Default values + +For project name, version, description we use pyproject.toml +For the rest, we use file `.env` (gitignored), see `.env.example` + +`DEFAULT_SQLALCHEMY_DATABASE_URI` and `TEST_SQLALCHEMY_DATABASE_URI`: +Both are ment to be validated at the runtime, do not change unless you know +what are you doing. All the two validators do is to build full URI (TCP protocol) +to databases to avoid typo bugs. + +See https://pydantic-docs.helpmanual.io/usage/settings/ +""" + +from pathlib import Path +from typing import Union, Literal + +import toml +from pydantic import AnyHttpUrl, AnyUrl, BaseSettings, EmailStr, validator + + +PROJECT_DIR = Path(__file__).parent.parent.parent +pyproject_content = toml.load(f"{PROJECT_DIR}/pyproject.toml")["tool"]["poetry"] + + +class Settings(BaseSettings): + # CORE SETTINGS + SECRET_KEY: str + ENVIRONMENT: Literal["DEV", "PYTEST", "STAGE", "PRODUCTION"] + ACCESS_TOKEN_EXPIRE_MINUTES: int + SECURITY_BCRYPT_DEFAULT_ROUNDS: int = 12 + REFRESH_TOKEN_EXPIRE_MINUTES: int + BACKEND_CORS_ORIGINS: Union[str, list[AnyHttpUrl]] + + # PROJECT NAME, VERSION AND DESCRIPTION + PROJECT_NAME: str = pyproject_content["name"] + VERSION: str = pyproject_content["version"] + DESCRIPTION: str = pyproject_content["description"] + + # POSTGRESQL DEFAULT DATABASE + DEFAULT_DATABASE_HOSTNAME: str + DEFAULT_DATABASE_USER: str + DEFAULT_DATABASE_PASSWORD: str + DEFAULT_DATABASE_PORT: str + DEFAULT_DATABASE_DB: str + DEFAULT_SQLALCHEMY_DATABASE_URI: str = "" + + # POSTGRESQL TEST DATABASE + TEST_DATABASE_HOSTNAME: str + TEST_DATABASE_USER: str + TEST_DATABASE_PASSWORD: str + TEST_DATABASE_PORT: str + TEST_DATABASE_DB: str + TEST_SQLALCHEMY_DATABASE_URI: str = "" + + # FIRST SUPERUSER + FIRST_SUPERUSER_EMAIL: EmailStr + FIRST_SUPERUSER_PASSWORD: str + + # VALIDATORS + @validator("BACKEND_CORS_ORIGINS") + def _assemble_cors_origins(cls, cors_origins: Union[str, list[AnyHttpUrl]]): + if isinstance(cors_origins, str): + return [item.strip() for item in cors_origins.split(",")] + return cors_origins + + @validator("DEFAULT_SQLALCHEMY_DATABASE_URI") + def _assemble_default_db_connection(cls, v: str, values: dict[str, str]) -> str: + return AnyUrl.build( + scheme="postgresql+asyncpg", + user=values["DEFAULT_DATABASE_USER"], + password=values["DEFAULT_DATABASE_PASSWORD"], + host=values["DEFAULT_DATABASE_HOSTNAME"], + port=values["DEFAULT_DATABASE_PORT"], + path=f"/{values['DEFAULT_DATABASE_DB']}", + ) + + @validator("TEST_SQLALCHEMY_DATABASE_URI") + def _assemble_test_db_connection(cls, v: str, values: dict[str, str]) -> str: + return AnyUrl.build( + scheme="postgresql+asyncpg", + user=values["TEST_DATABASE_USER"], + password=values["TEST_DATABASE_PASSWORD"], + host=values["TEST_DATABASE_HOSTNAME"], + port=values["TEST_DATABASE_PORT"], + path=f"/{values['TEST_DATABASE_DB']}", + ) + + class Config: + env_file = f"{PROJECT_DIR}/.env" + case_sensitive = True + + +settings: Settings = Settings() # type: ignore diff --git a/{{cookiecutter.project_name}}/template_minimal/app/core/security.py b/{{cookiecutter.project_name}}/template_minimal/app/core/security.py new file mode 100644 index 0000000..de362f6 --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/app/core/security.py @@ -0,0 +1,56 @@ +""" +Black-box security shortcuts to generate JWT tokens and password hash/verify + +`subject` in access/refresh func may be antyhing unique to User account, `id` etc. +""" + +from datetime import datetime, timedelta +from typing import Any, Union + +from jose import jwt +from passlib.context import CryptContext + +from app.core import config + +pwd_context = CryptContext( + schemes=["bcrypt"], + deprecated="auto", + bcrypt__rounds=config.settings.SECURITY_BCRYPT_DEFAULT_ROUNDS, +) + + +ALGORITHM = "HS256" + + +def create_access_token(subject: Union[str, Any]) -> tuple[str, datetime]: + now = datetime.utcnow() + expire = now + timedelta(minutes=config.settings.ACCESS_TOKEN_EXPIRE_MINUTES) + + to_encode = {"exp": expire, "sub": str(subject), "refresh": False} + encoded_jwt: str = jwt.encode( + to_encode, + config.settings.SECRET_KEY, + algorithm=ALGORITHM, + ) + return encoded_jwt, expire + + +def create_refresh_token(subject: Union[str, Any]) -> tuple[str, datetime]: + now = datetime.utcnow() + expire = now + timedelta(minutes=config.settings.REFRESH_TOKEN_EXPIRE_MINUTES) + + to_encode = {"exp": expire, "sub": str(subject), "refresh": True} + encoded_jwt: str = jwt.encode( + to_encode, + config.settings.SECRET_KEY, + algorithm=ALGORITHM, + ) + return encoded_jwt, expire + + +def verify_password(plain_password: str, hashed_password: str) -> bool: + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password: str) -> str: + return pwd_context.hash(password) diff --git a/{{cookiecutter.project_name}}/template_minimal/app/initial_data.py b/{{cookiecutter.project_name}}/template_minimal/app/initial_data.py new file mode 100644 index 0000000..1f36897 --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/app/initial_data.py @@ -0,0 +1,45 @@ +""" +Put here any Python code that must be runned before application startup. +It is included in `init.sh` script. + +By defualt `main` create a superuser if not exists +""" + +import asyncio +from typing import Optional + +from sqlalchemy import select + +from app.core import security, config +from app.models import User +from app.session import async_session + + +async def main() -> None: + print("Start initial data") + async with async_session() as session: + + result = await session.execute( + select(User).where(User.email == config.settings.FIRST_SUPERUSER_EMAIL) + ) + user: Optional[User] = result.scalars().first() + + if user is None: + new_superuser = User( + email=config.settings.FIRST_SUPERUSER_EMAIL, + hashed_password=security.get_password_hash( + config.settings.FIRST_SUPERUSER_PASSWORD + ), + full_name=config.settings.FIRST_SUPERUSER_EMAIL, + ) + session.add(new_superuser) + await session.commit() + print("Superuser was created") + else: + print("Superuser already exists in database") + + print("Initial data created") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/{{cookiecutter.project_name}}/template_minimal/app/main.py b/{{cookiecutter.project_name}}/template_minimal/app/main.py new file mode 100644 index 0000000..0892233 --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/app/main.py @@ -0,0 +1,29 @@ +""" +Main FastAPI app instance declaration +""" + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from app.api.api import api_router +from app.core import config + +app = FastAPI( + title=config.settings.PROJECT_NAME, + version=config.settings.VERSION, + description=config.settings.DESCRIPTION, + openapi_url="/openapi.json", + docs_url="/", +) + +# Set all CORS enabled origins +if config.settings.BACKEND_CORS_ORIGINS: + app.add_middleware( + CORSMiddleware, + allow_origins=[str(origin) for origin in config.settings.BACKEND_CORS_ORIGINS], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + +app.include_router(api_router) diff --git a/{{cookiecutter.project_name}}/template_minimal/app/models.py b/{{cookiecutter.project_name}}/template_minimal/app/models.py new file mode 100644 index 0000000..2b7eb82 --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/app/models.py @@ -0,0 +1,20 @@ +""" +SQL Alchemy models declaration. + +Note, imported by alembic migrations logic, see `alembic/env.py` +""" + +from typing import Any, cast + +from sqlalchemy import Column, Integer, String +from sqlalchemy.orm.decl_api import declarative_base + +Base = cast(Any, declarative_base()) + + +class User(Base): + __tablename__ = "user" + id = Column(Integer, primary_key=True, index=True) + full_name = Column(String(254), nullable=True) + email = Column(String(254), unique=True, index=True, nullable=False) + hashed_password = Column(String(128), nullable=False) diff --git a/{{cookiecutter.project_name}}/template_minimal/app/schemas/__init__.py b/{{cookiecutter.project_name}}/template_minimal/app/schemas/__init__.py new file mode 100644 index 0000000..e2ec5b2 --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/app/schemas/__init__.py @@ -0,0 +1,2 @@ +from .token import Token, TokenPayload, TokenRefresh +from .user import User, UserCreate, UserUpdate diff --git a/{{cookiecutter.project_name}}/template_minimal/app/schemas/token.py b/{{cookiecutter.project_name}}/template_minimal/app/schemas/token.py new file mode 100644 index 0000000..9c81e3e --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/app/schemas/token.py @@ -0,0 +1,21 @@ +from datetime import datetime +from typing import Optional + +from pydantic import BaseModel + + +class Token(BaseModel): + token_type: str + access_token: str + expire_at: datetime + refresh_token: str + refresh_expire_at: datetime + + +class TokenPayload(BaseModel): + sub: Optional[int] = None + refresh: Optional[bool] = None + + +class TokenRefresh(BaseModel): + refresh_token: str diff --git a/{{cookiecutter.project_name}}/template_minimal/app/schemas/user.py b/{{cookiecutter.project_name}}/template_minimal/app/schemas/user.py new file mode 100644 index 0000000..9e24055 --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/app/schemas/user.py @@ -0,0 +1,26 @@ +from typing import Optional + +from pydantic import BaseModel, EmailStr + + +class BaseUser(BaseModel): + class Config: + orm_mode = True + + +class User(BaseUser): + id: int + email: EmailStr + full_name: str + + +class UserUpdate(BaseUser): + email: Optional[EmailStr] + password: Optional[str] + full_name: Optional[str] + + +class UserCreate(BaseUser): + email: EmailStr + password: str + full_name: str diff --git a/{{cookiecutter.project_name}}/template_minimal/app/session.py b/{{cookiecutter.project_name}}/template_minimal/app/session.py new file mode 100644 index 0000000..5b27e40 --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/app/session.py @@ -0,0 +1,12 @@ +from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine +from sqlalchemy.orm.session import sessionmaker + +from app.core import config + +if config.settings.ENVIRONMENT == "PYTEST": + sqlalchemy_database_uri = config.settings.TEST_SQLALCHEMY_DATABASE_URI +else: + sqlalchemy_database_uri = config.settings.DEFAULT_SQLALCHEMY_DATABASE_URI + +async_engine = create_async_engine(sqlalchemy_database_uri, pool_pre_ping=True) +async_session = sessionmaker(async_engine, expire_on_commit=False, class_=AsyncSession) diff --git a/{{cookiecutter.project_name}}/template_minimal/app/tests/__init__.py b/{{cookiecutter.project_name}}/template_minimal/app/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/{{cookiecutter.project_name}}/template_minimal/app/tests/conftest.py b/{{cookiecutter.project_name}}/template_minimal/app/tests/conftest.py new file mode 100644 index 0000000..2707184 --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/app/tests/conftest.py @@ -0,0 +1,62 @@ +import asyncio +from typing import AsyncGenerator, Optional + +import pytest +from httpx import AsyncClient +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core import security, config +from app.main import app +from app.models import Base, User +from app.session import async_engine, async_session + + +@pytest.fixture(scope="session") +def event_loop(): + loop = asyncio.get_event_loop() + yield loop + loop.close() + + +@pytest.fixture(scope="session") +async def client(): + async with AsyncClient(app=app, base_url="http://test") as client: + yield client + + +@pytest.fixture(scope="session") +async def test_db_setup_sessionmaker(): + # assert if we use TEST_DB URL for 100% + assert config.settings.ENVIRONMENT == "PYTEST" + assert str(async_engine.url) == config.settings.TEST_SQLALCHEMY_DATABASE_URI + + # always drop and create test db tables between tests session + async with async_engine.begin() as conn: + + await conn.run_sync(Base.metadata.drop_all) + await conn.run_sync(Base.metadata.create_all) + return async_session + + +@pytest.fixture +async def session(test_db_setup_sessionmaker) -> AsyncGenerator[AsyncSession, None]: + async with test_db_setup_sessionmaker() as session: + yield session + + +@pytest.fixture +async def default_user(session: AsyncSession): + result = await session.execute(select(User).where(User.email == "user@email.com")) + user: Optional[User] = result.scalars().first() + if user is None: + new_user = User( + email="user@email.com", + hashed_password=security.get_password_hash("password"), + full_name="fullname", + ) + session.add(new_user) + await session.commit() + await session.refresh(new_user) + return new_user + return user diff --git a/{{cookiecutter.project_name}}/template_minimal/app/tests/test_auth.py b/{{cookiecutter.project_name}}/template_minimal/app/tests/test_auth.py new file mode 100644 index 0000000..ccfaa18 --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/app/tests/test_auth.py @@ -0,0 +1,43 @@ +import pytest +from httpx import AsyncClient + +from app.models import User + +# All test coroutines in file will be treated as marked (async allowed). +pytestmark = pytest.mark.asyncio + + +async def test_login_endpoints(client: AsyncClient, default_user: User): + + # access-token endpoint + access_token = await client.post( + "/auth/access-token", + data={ + "username": "user@email.com", + "password": "password", + }, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) + assert access_token.status_code == 200 + token = access_token.json() + + access_token = token["access_token"] + refresh_token = token["refresh_token"] + + # test-token endpoint + test_token = await client.post( + "/auth/test-token", headers={"Authorization": f"Bearer {access_token}"} + ) + assert test_token.status_code == 200 + response_user = test_token.json() + assert response_user["email"] == default_user.email + + # refresh-token endpoint + get_new_token = await client.post( + "/auth/refresh-token", json={"refresh_token": refresh_token} + ) + + assert get_new_token.status_code == 200 + new_token = get_new_token.json() + + assert "access_token" in new_token diff --git a/{{cookiecutter.project_name}}/template_minimal/app/tests/utils.py b/{{cookiecutter.project_name}}/template_minimal/app/tests/utils.py new file mode 100644 index 0000000..7c14c07 --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/app/tests/utils.py @@ -0,0 +1,10 @@ +import random +import string + + +def random_lower_string(length: int = 32) -> str: + return "".join(random.choices(string.ascii_lowercase, k=length)) + + +def random_email(length: int = 10) -> str: + return f"{random_lower_string(length)}@{random_lower_string(length)}.com" diff --git a/{{cookiecutter.project_name}}/template_minimal/docker-compose.yml b/{{cookiecutter.project_name}}/template_minimal/docker-compose.yml new file mode 100644 index 0000000..4f33f36 --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/docker-compose.yml @@ -0,0 +1,36 @@ +version: "3.3" + +# For local development, only database is running +# +# docker-compose up -d +# uvicorn app.main:app --reload +# + +services: + default_database: + restart: unless-stopped + image: postgres:latest + volumes: + - ./default_database_data/db:/var/lib/postgresql/data + environment: + - POSTGRES_DB=${DEFAULT_DATABASE_DB} + - POSTGRES_USER=${DEFAULT_DATABASE_USER} + - POSTGRES_PASSWORD=${DEFAULT_DATABASE_PASSWORD} + env_file: + - .env + ports: + - "${DEFAULT_DATABASE_PORT}:5432" + + test_database: + restart: unless-stopped + image: postgres:latest + volumes: + - ./test_database_data/db:/var/lib/postgresql/data + environment: + - POSTGRES_DB=${TEST_DATABASE_DB} + - POSTGRES_USER=${TEST_DATABASE_USER} + - POSTGRES_PASSWORD=${TEST_DATABASE_PASSWORD} + env_file: + - .env + ports: + - "${TEST_DATABASE_PORT}:5432" diff --git a/{{cookiecutter.project_name}}/template_minimal/init.sh b/{{cookiecutter.project_name}}/template_minimal/init.sh new file mode 100644 index 0000000..5811845 --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/init.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +echo "Run migrations" +alembic upgrade head + +echo "Create initial data in DB" +python -m app.initial_data diff --git a/{{cookiecutter.project_name}}/template_minimal/nginx-unit-config.json b/{{cookiecutter.project_name}}/template_minimal/nginx-unit-config.json new file mode 100644 index 0000000..dda3653 --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/nginx-unit-config.json @@ -0,0 +1,17 @@ +{ + "listeners": { + "*:80": { + "pass": "applications/fastapi" + } + }, + "applications": { + "fastapi": { + "type": "python 3.9", + "processes": 1, + "threads": 1, + "path": "/build/", + "module": "app.main", + "callable": "app" + } + } +} diff --git a/{{cookiecutter.project_name}}/template_minimal/poetry.lock b/{{cookiecutter.project_name}}/template_minimal/poetry.lock new file mode 100644 index 0000000..2062419 --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/poetry.lock @@ -0,0 +1,1453 @@ +[[package]] +name = "alembic" +version = "1.7.5" +description = "A database migration tool for SQLAlchemy." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +importlib-metadata = {version = "*", markers = "python_version < \"3.9\""} +importlib-resources = {version = "*", markers = "python_version < \"3.9\""} +Mako = "*" +SQLAlchemy = ">=1.3.0" + +[package.extras] +tz = ["python-dateutil"] + +[[package]] +name = "anyio" +version = "3.4.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +category = "main" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" +typing-extensions = {version = "*", markers = "python_version < \"3.8\""} + +[package.extras] +doc = ["sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] +test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=6.0)", "pytest-mock (>=3.6.1)", "trustme", "contextlib2", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"] +trio = ["trio (>=0.16)"] + +[[package]] +name = "asgiref" +version = "3.4.1" +description = "ASGI specs, helper code, and adapters" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing-extensions = {version = "*", markers = "python_version < \"3.8\""} + +[package.extras] +tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] + +[[package]] +name = "asyncpg" +version = "0.25.0" +description = "An asyncio PostgreSQL driver" +category = "main" +optional = false +python-versions = ">=3.6.0" + +[package.dependencies] +typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\""} + +[package.extras] +dev = ["Cython (>=0.29.24,<0.30.0)", "pytest (>=6.0)", "Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "pycodestyle (>=2.7.0,<2.8.0)", "flake8 (>=3.9.2,<3.10.0)", "uvloop (>=0.15.3)"] +docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)"] +test = ["pycodestyle (>=2.7.0,<2.8.0)", "flake8 (>=3.9.2,<3.10.0)", "uvloop (>=0.15.3)"] + +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "21.4.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] + +[[package]] +name = "autoflake" +version = "1.4" +description = "Removes unused imports and unused variables" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +pyflakes = ">=1.1.0" + +[[package]] +name = "bcrypt" +version = "3.2.0" +description = "Modern password hashing for your software and your servers" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.1" +six = ">=1.4.1" + +[package.extras] +tests = ["pytest (>=3.2.1,!=3.3.0)"] +typecheck = ["mypy"] + +[[package]] +name = "black" +version = "21.12b0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +click = ">=7.1.2" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.9.0,<1" +platformdirs = ">=2" +tomli = ">=0.2.6,<2.0.0" +typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} +typing-extensions = [ + {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}, + {version = "!=3.10.0.1", markers = "python_version >= \"3.10\""}, +] + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +python2 = ["typed-ast (>=1.4.3)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "certifi" +version = "2021.10.8" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "cffi" +version = "1.15.0" +description = "Foreign Function Interface for Python calling C code." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "charset-normalizer" +version = "2.0.9" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.5.0" + +[package.extras] +unicode_backport = ["unicodedata2"] + +[[package]] +name = "click" +version = "8.0.3" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "coverage" +version = "6.2" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "cryptography" +version = "36.0.1" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] +docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] +pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] +sdist = ["setuptools_rust (>=0.11.4)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pytest (>=6.2.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] + +[[package]] +name = "dnspython" +version = "2.1.0" +description = "DNS toolkit" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +dnssec = ["cryptography (>=2.6)"] +doh = ["requests", "requests-toolbelt"] +idna = ["idna (>=2.1)"] +curio = ["curio (>=1.2)", "sniffio (>=1.1)"] +trio = ["trio (>=0.14.0)", "sniffio (>=1.1)"] + +[[package]] +name = "ecdsa" +version = "0.17.0" +description = "ECDSA cryptographic signature library (pure python)" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[package.dependencies] +six = ">=1.9.0" + +[package.extras] +gmpy = ["gmpy"] +gmpy2 = ["gmpy2"] + +[[package]] +name = "email-validator" +version = "1.1.3" +description = "A robust email syntax and deliverability validation library for Python 2.x/3.x." +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.dependencies] +dnspython = ">=1.15.0" +idna = ">=2.0.0" + +[[package]] +name = "fastapi" +version = "0.71.0" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" +starlette = "0.17.1" + +[package.extras] +all = ["requests (>=2.24.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "itsdangerous (>=1.1.0,<3.0.0)", "pyyaml (>=5.3.1,<6.0.0)", "ujson (>=4.0.1,<5.0.0)", "orjson (>=3.2.1,<4.0.0)", "email_validator (>=1.1.1,<2.0.0)", "uvicorn[standard] (>=0.12.0,<0.16.0)"] +dev = ["python-jose[cryptography] (>=3.3.0,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "uvicorn[standard] (>=0.12.0,<0.16.0)"] +doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "typer-cli (>=0.0.12,<0.0.13)", "pyyaml (>=5.3.1,<6.0.0)"] +test = ["pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "mypy (==0.910)", "flake8 (>=3.8.3,<4.0.0)", "black (==21.9b0)", "isort (>=5.0.6,<6.0.0)", "requests (>=2.24.0,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "email_validator (>=1.1.1,<2.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "peewee (>=3.13.3,<4.0.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "orjson (>=3.2.1,<4.0.0)", "ujson (>=4.0.1,<5.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "flask (>=1.1.2,<3.0.0)", "anyio[trio] (>=3.2.1,<4.0.0)", "types-ujson (==0.1.1)", "types-orjson (==3.6.0)", "types-dataclasses (==0.1.7)"] + +[[package]] +name = "flake8" +version = "4.0.1" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +importlib-metadata = {version = "<4.3", markers = "python_version < \"3.8\""} +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.8.0,<2.9.0" +pyflakes = ">=2.4.0,<2.5.0" + +[[package]] +name = "greenlet" +version = "1.1.2" +description = "Lightweight in-process concurrent programming" +category = "main" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" + +[package.extras] +docs = ["sphinx"] + +[[package]] +name = "h11" +version = "0.12.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "httpcore" +version = "0.14.4" +description = "A minimal low-level HTTP client." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +anyio = ">=3.0.0,<4.0.0" +certifi = "*" +h11 = ">=0.11,<0.13" +sniffio = ">=1.0.0,<2.0.0" + +[package.extras] +http2 = ["h2 (>=3,<5)"] + +[[package]] +name = "httpx" +version = "0.21.3" +description = "The next generation HTTP client." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +certifi = "*" +charset-normalizer = "*" +httpcore = ">=0.14.0,<0.15.0" +rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} +sniffio = "*" + +[package.extras] +brotli = ["brotlicffi", "brotli"] +cli = ["click (>=8.0.0,<9.0.0)", "rich (>=10.0.0,<11.0.0)", "pygments (>=2.0.0,<3.0.0)"] +http2 = ["h2 (>=3,<5)"] + +[[package]] +name = "idna" +version = "3.3" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "importlib-metadata" +version = "4.2.0" +description = "Read metadata from Python packages" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] + +[[package]] +name = "importlib-resources" +version = "5.4.0" +description = "Read resources from Python packages" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-black (>=0.3.7)", "pytest-mypy"] + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "isort" +version = "5.10.1" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.6.1,<4.0" + +[package.extras] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +requirements_deprecated_finder = ["pipreqs", "pip-api"] +colors = ["colorama (>=0.4.3,<0.5.0)"] +plugins = ["setuptools"] + +[[package]] +name = "mako" +version = "1.1.6" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +MarkupSafe = ">=0.9.2" + +[package.extras] +babel = ["babel"] +lingua = ["lingua"] + +[[package]] +name = "markupsafe" +version = "2.0.1" +description = "Safely add untrusted strings to HTML/XML markup." +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "packaging" +version = "21.3" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" + +[[package]] +name = "passlib" +version = "1.7.4" +description = "comprehensive password hashing framework supporting over 30 schemes" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +bcrypt = {version = ">=3.1.0", optional = true, markers = "extra == \"bcrypt\""} + +[package.extras] +argon2 = ["argon2-cffi (>=18.2.0)"] +bcrypt = ["bcrypt (>=3.1.0)"] +build_docs = ["sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)", "cloud-sptheme (>=1.10.1)"] +totp = ["cryptography"] + +[[package]] +name = "pathspec" +version = "0.9.0" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[[package]] +name = "platformdirs" +version = "2.4.1" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] +test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pyasn1" +version = "0.4.8" +description = "ASN.1 types and codecs" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "pycodestyle" +version = "2.8.0" +description = "Python style guide checker" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pydantic" +version = "1.9.0" +description = "Data validation and settings management using python 3.6 type hinting" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +email-validator = {version = ">=1.0.3", optional = true, markers = "extra == \"email\""} +typing-extensions = ">=3.7.4.3" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + +[[package]] +name = "pyflakes" +version = "2.4.0" +description = "passive checker of Python programs" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pyparsing" +version = "3.0.6" +description = "Python parsing module" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pytest" +version = "6.2.5" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +toml = "*" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.16.0" +description = "Pytest support for asyncio." +category = "dev" +optional = false +python-versions = ">= 3.6" + +[package.dependencies] +pytest = ">=5.4.0" + +[package.extras] +testing = ["coverage", "hypothesis (>=5.7.1)"] + +[[package]] +name = "python-dotenv" +version = "0.19.2" +description = "Read key-value pairs from a .env file and set them as environment variables" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "python-jose" +version = "3.3.0" +description = "JOSE implementation in Python" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"cryptography\""} +ecdsa = "!=0.15" +pyasn1 = "*" +rsa = "*" + +[package.extras] +cryptography = ["cryptography (>=3.4.0)"] +pycrypto = ["pycrypto (>=2.6.0,<2.7.0)", "pyasn1"] +pycryptodome = ["pycryptodome (>=3.3.1,<4.0.0)", "pyasn1"] + +[[package]] +name = "python-multipart" +version = "0.0.5" +description = "A streaming multipart parser for Python" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +six = ">=1.4.0" + +[[package]] +name = "requests" +version = "2.27.1" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} +idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] + +[[package]] +name = "rfc3986" +version = "1.5.0" +description = "Validating URI References per RFC 3986" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} + +[package.extras] +idna2008 = ["idna"] + +[[package]] +name = "rsa" +version = "4.8" +description = "Pure-Python RSA implementation" +category = "main" +optional = false +python-versions = ">=3.6,<4" + +[package.dependencies] +pyasn1 = ">=0.1.3" + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "sniffio" +version = "1.2.0" +description = "Sniff out which async library your code is running under" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "sqlalchemy" +version = "1.4.29" +description = "Database Abstraction Library" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" + +[package.dependencies] +greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + +[package.extras] +aiomysql = ["greenlet (!=0.4.17)", "aiomysql"] +aiosqlite = ["typing_extensions (!=3.10.0.1)", "greenlet (!=0.4.17)", "aiosqlite"] +asyncio = ["greenlet (!=0.4.17)"] +asyncmy = ["greenlet (!=0.4.17)", "asyncmy (>=0.2.3)"] +mariadb_connector = ["mariadb (>=1.0.1)"] +mssql = ["pyodbc"] +mssql_pymssql = ["pymssql"] +mssql_pyodbc = ["pyodbc"] +mypy = ["sqlalchemy2-stubs", "mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0,<2)", "mysqlclient (>=1.4.0)"] +mysql_connector = ["mysql-connector-python"] +oracle = ["cx_oracle (>=7,<8)", "cx_oracle (>=7)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql_asyncpg = ["greenlet (!=0.4.17)", "asyncpg"] +postgresql_pg8000 = ["pg8000 (>=1.16.6)"] +postgresql_psycopg2binary = ["psycopg2-binary"] +postgresql_psycopg2cffi = ["psycopg2cffi"] +pymysql = ["pymysql (<1)", "pymysql"] +sqlcipher = ["sqlcipher3-binary"] + +[[package]] +name = "sqlalchemy2-stubs" +version = "0.0.2a19" +description = "Typing Stubs for SQLAlchemy 1.4" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing-extensions = ">=3.7.4" + +[[package]] +name = "starlette" +version = "0.17.1" +description = "The little ASGI library that shines." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +anyio = ">=3.0.0,<4" +typing-extensions = {version = "*", markers = "python_version < \"3.8\""} + +[package.extras] +full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests"] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "tomli" +version = "1.2.3" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "typed-ast" +version = "1.5.1" +description = "a fork of Python 2 and 3 ast modules with type comment support" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "typing-extensions" +version = "4.0.1" +description = "Backported and Experimental Type Hints for Python 3.6+" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "urllib3" +version = "1.26.7" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "uvicorn" +version = "0.16.0" +description = "The lightning-fast ASGI server." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +asgiref = ">=3.4.0" +click = ">=7.0" +h11 = ">=0.8" +typing-extensions = {version = "*", markers = "python_version < \"3.8\""} + +[package.extras] +standard = ["httptools (>=0.2.0,<0.4.0)", "watchgod (>=0.6)", "python-dotenv (>=0.13)", "PyYAML (>=5.1)", "websockets (>=9.1)", "websockets (>=10.0)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "colorama (>=0.4)"] + +[[package]] +name = "zipp" +version = "3.7.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.7" +content-hash = "a00e6962598c9e04b8f260f70bb66492d5a520b72e65d4d1aa227a0a2a93f5cb" + +[metadata.files] +alembic = [ + {file = "alembic-1.7.5-py3-none-any.whl", hash = "sha256:a9dde941534e3d7573d9644e8ea62a2953541e27bc1793e166f60b777ae098b4"}, + {file = "alembic-1.7.5.tar.gz", hash = "sha256:7c328694a2e68f03ee971e63c3bd885846470373a5b532cf2c9f1601c413b153"}, +] +anyio = [ + {file = "anyio-3.4.0-py3-none-any.whl", hash = "sha256:2855a9423524abcdd652d942f8932fda1735210f77a6b392eafd9ff34d3fe020"}, + {file = "anyio-3.4.0.tar.gz", hash = "sha256:24adc69309fb5779bc1e06158e143e0b6d2c56b302a3ac3de3083c705a6ed39d"}, +] +asgiref = [ + {file = "asgiref-3.4.1-py3-none-any.whl", hash = "sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214"}, + {file = "asgiref-3.4.1.tar.gz", hash = "sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9"}, +] +asyncpg = [ + {file = "asyncpg-0.25.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bf5e3408a14a17d480f36ebaf0401a12ff6ae5457fdf45e4e2775c51cc9517d3"}, + {file = "asyncpg-0.25.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2bc197fc4aca2fd24f60241057998124012469d2e414aed3f992579db0c88e3a"}, + {file = "asyncpg-0.25.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a70783f6ffa34cc7dd2de20a873181414a34fd35a4a208a1f1a7f9f695e4ec4"}, + {file = "asyncpg-0.25.0-cp310-cp310-win32.whl", hash = "sha256:43cde84e996a3afe75f325a68300093425c2f47d340c0fc8912765cf24a1c095"}, + {file = "asyncpg-0.25.0-cp310-cp310-win_amd64.whl", hash = "sha256:56d88d7ef4341412cd9c68efba323a4519c916979ba91b95d4c08799d2ff0c09"}, + {file = "asyncpg-0.25.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a84d30e6f850bac0876990bcd207362778e2208df0bee8be8da9f1558255e634"}, + {file = "asyncpg-0.25.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:beaecc52ad39614f6ca2e48c3ca15d56e24a2c15cbfdcb764a4320cc45f02fd5"}, + {file = "asyncpg-0.25.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:6f8f5fc975246eda83da8031a14004b9197f510c41511018e7b1bedde6968e92"}, + {file = "asyncpg-0.25.0-cp36-cp36m-win32.whl", hash = "sha256:ddb4c3263a8d63dcde3d2c4ac1c25206bfeb31fa83bd70fd539e10f87739dee4"}, + {file = "asyncpg-0.25.0-cp36-cp36m-win_amd64.whl", hash = "sha256:bf6dc9b55b9113f39eaa2057337ce3f9ef7de99a053b8a16360395ce588925cd"}, + {file = "asyncpg-0.25.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:acb311722352152936e58a8ee3c5b8e791b24e84cd7d777c414ff05b3530ca68"}, + {file = "asyncpg-0.25.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0a61fb196ce4dae2f2fa26eb20a778db21bbee484d2e798cb3cc988de13bdd1b"}, + {file = "asyncpg-0.25.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2633331cbc8429030b4f20f712f8d0fbba57fa8555ee9b2f45f981b81328b256"}, + {file = "asyncpg-0.25.0-cp37-cp37m-win32.whl", hash = "sha256:863d36eba4a7caa853fd7d83fad5fd5306f050cc2fe6e54fbe10cdb30420e5e9"}, + {file = "asyncpg-0.25.0-cp37-cp37m-win_amd64.whl", hash = "sha256:fe471ccd915b739ca65e2e4dbd92a11b44a5b37f2e38f70827a1c147dafe0fa8"}, + {file = "asyncpg-0.25.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:72a1e12ea0cf7c1e02794b697e3ca967b2360eaa2ce5d4bfdd8604ec2d6b774b"}, + {file = "asyncpg-0.25.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4327f691b1bdb222df27841938b3e04c14068166b3a97491bec2cb982f49f03e"}, + {file = "asyncpg-0.25.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:739bbd7f89a2b2f6bc44cb8bf967dab12c5bc714fcbe96e68d512be45ecdf962"}, + {file = "asyncpg-0.25.0-cp38-cp38-win32.whl", hash = "sha256:18d49e2d93a7139a2fdbd113e320cc47075049997268a61bfbe0dde680c55471"}, + {file = "asyncpg-0.25.0-cp38-cp38-win_amd64.whl", hash = "sha256:191fe6341385b7fdea7dbdcf47fd6db3fd198827dcc1f2b228476d13c05a03c6"}, + {file = "asyncpg-0.25.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:52fab7f1b2c29e187dd8781fce896249500cf055b63471ad66332e537e9b5f7e"}, + {file = "asyncpg-0.25.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a738f1b2876f30d710d3dc1e7858160a0afe1603ba16bf5f391f5316eb0ed855"}, + {file = "asyncpg-0.25.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4105f57ad1e8fbc8b1e535d8fcefa6ce6c71081228f08680c6dea24384ff0e"}, + {file = "asyncpg-0.25.0-cp39-cp39-win32.whl", hash = "sha256:f55918ded7b85723a5eaeb34e86e7b9280d4474be67df853ab5a7fa0cc7c6bf2"}, + {file = "asyncpg-0.25.0-cp39-cp39-win_amd64.whl", hash = "sha256:649e2966d98cc48d0646d9a4e29abecd8b59d38d55c256d5c857f6b27b7407ac"}, + {file = "asyncpg-0.25.0.tar.gz", hash = "sha256:63f8e6a69733b285497c2855464a34de657f2cccd25aeaeeb5071872e9382540"}, +] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, + {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, +] +autoflake = [ + {file = "autoflake-1.4.tar.gz", hash = "sha256:61a353012cff6ab94ca062823d1fb2f692c4acda51c76ff83a8d77915fba51ea"}, +] +bcrypt = [ + {file = "bcrypt-3.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:63d4e3ff96188e5898779b6057878fecf3f11cfe6ec3b313ea09955d587ec7a7"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:cd1ea2ff3038509ea95f687256c46b79f5fc382ad0aa3664d200047546d511d1"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:cdcdcb3972027f83fe24a48b1e90ea4b584d35f1cc279d76de6fc4b13376239d"}, + {file = "bcrypt-3.2.0-cp36-abi3-win32.whl", hash = "sha256:a67fb841b35c28a59cebed05fbd3e80eea26e6d75851f0574a9273c80f3e9b55"}, + {file = "bcrypt-3.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:81fec756feff5b6818ea7ab031205e1d323d8943d237303baca2c5f9c7846f34"}, + {file = "bcrypt-3.2.0.tar.gz", hash = "sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29"}, +] +black = [ + {file = "black-21.12b0-py3-none-any.whl", hash = "sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f"}, + {file = "black-21.12b0.tar.gz", hash = "sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3"}, +] +certifi = [ + {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, + {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, +] +cffi = [ + {file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"}, + {file = "cffi-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0"}, + {file = "cffi-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14"}, + {file = "cffi-1.15.0-cp27-cp27m-win32.whl", hash = "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474"}, + {file = "cffi-1.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6"}, + {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27"}, + {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023"}, + {file = "cffi-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2"}, + {file = "cffi-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382"}, + {file = "cffi-1.15.0-cp310-cp310-win32.whl", hash = "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55"}, + {file = "cffi-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0"}, + {file = "cffi-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605"}, + {file = "cffi-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e"}, + {file = "cffi-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc"}, + {file = "cffi-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7"}, + {file = "cffi-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66"}, + {file = "cffi-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029"}, + {file = "cffi-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6"}, + {file = "cffi-1.15.0-cp38-cp38-win32.whl", hash = "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c"}, + {file = "cffi-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443"}, + {file = "cffi-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a"}, + {file = "cffi-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8"}, + {file = "cffi-1.15.0-cp39-cp39-win32.whl", hash = "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a"}, + {file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"}, + {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"}, +] +charset-normalizer = [ + {file = "charset-normalizer-2.0.9.tar.gz", hash = "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c"}, + {file = "charset_normalizer-2.0.9-py3-none-any.whl", hash = "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721"}, +] +click = [ + {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, + {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +coverage = [ + {file = "coverage-6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b"}, + {file = "coverage-6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0"}, + {file = "coverage-6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da"}, + {file = "coverage-6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d"}, + {file = "coverage-6.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739"}, + {file = "coverage-6.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971"}, + {file = "coverage-6.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840"}, + {file = "coverage-6.2-cp310-cp310-win32.whl", hash = "sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c"}, + {file = "coverage-6.2-cp310-cp310-win_amd64.whl", hash = "sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f"}, + {file = "coverage-6.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76"}, + {file = "coverage-6.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47"}, + {file = "coverage-6.2-cp311-cp311-win_amd64.whl", hash = "sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64"}, + {file = "coverage-6.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9"}, + {file = "coverage-6.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d"}, + {file = "coverage-6.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48"}, + {file = "coverage-6.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e"}, + {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d"}, + {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17"}, + {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781"}, + {file = "coverage-6.2-cp36-cp36m-win32.whl", hash = "sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a"}, + {file = "coverage-6.2-cp36-cp36m-win_amd64.whl", hash = "sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0"}, + {file = "coverage-6.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49"}, + {file = "coverage-6.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521"}, + {file = "coverage-6.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884"}, + {file = "coverage-6.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa"}, + {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64"}, + {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617"}, + {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8"}, + {file = "coverage-6.2-cp37-cp37m-win32.whl", hash = "sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4"}, + {file = "coverage-6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74"}, + {file = "coverage-6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e"}, + {file = "coverage-6.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58"}, + {file = "coverage-6.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc"}, + {file = "coverage-6.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd"}, + {file = "coverage-6.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953"}, + {file = "coverage-6.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475"}, + {file = "coverage-6.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57"}, + {file = "coverage-6.2-cp38-cp38-win32.whl", hash = "sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c"}, + {file = "coverage-6.2-cp38-cp38-win_amd64.whl", hash = "sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2"}, + {file = "coverage-6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd"}, + {file = "coverage-6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685"}, + {file = "coverage-6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c"}, + {file = "coverage-6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3"}, + {file = "coverage-6.2-cp39-cp39-win32.whl", hash = "sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282"}, + {file = "coverage-6.2-cp39-cp39-win_amd64.whl", hash = "sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644"}, + {file = "coverage-6.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de"}, + {file = "coverage-6.2.tar.gz", hash = "sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8"}, +] +cryptography = [ + {file = "cryptography-36.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:73bc2d3f2444bcfeac67dd130ff2ea598ea5f20b40e36d19821b4df8c9c5037b"}, + {file = "cryptography-36.0.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:2d87cdcb378d3cfed944dac30596da1968f88fb96d7fc34fdae30a99054b2e31"}, + {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74d6c7e80609c0f4c2434b97b80c7f8fdfaa072ca4baab7e239a15d6d70ed73a"}, + {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:6c0c021f35b421ebf5976abf2daacc47e235f8b6082d3396a2fe3ccd537ab173"}, + {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d59a9d55027a8b88fd9fd2826c4392bd487d74bf628bb9d39beecc62a644c12"}, + {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a817b961b46894c5ca8a66b599c745b9a3d9f822725221f0e0fe49dc043a3a3"}, + {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:94ae132f0e40fe48f310bba63f477f14a43116f05ddb69d6fa31e93f05848ae2"}, + {file = "cryptography-36.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7be0eec337359c155df191d6ae00a5e8bbb63933883f4f5dffc439dac5348c3f"}, + {file = "cryptography-36.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e0344c14c9cb89e76eb6a060e67980c9e35b3f36691e15e1b7a9e58a0a6c6dc3"}, + {file = "cryptography-36.0.1-cp36-abi3-win32.whl", hash = "sha256:4caa4b893d8fad33cf1964d3e51842cd78ba87401ab1d2e44556826df849a8ca"}, + {file = "cryptography-36.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:391432971a66cfaf94b21c24ab465a4cc3e8bf4a939c1ca5c3e3a6e0abebdbcf"}, + {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bb5829d027ff82aa872d76158919045a7c1e91fbf241aec32cb07956e9ebd3c9"}, + {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebc15b1c22e55c4d5566e3ca4db8689470a0ca2babef8e3a9ee057a8b82ce4b1"}, + {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:596f3cd67e1b950bc372c33f1a28a0692080625592ea6392987dba7f09f17a94"}, + {file = "cryptography-36.0.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:30ee1eb3ebe1644d1c3f183d115a8c04e4e603ed6ce8e394ed39eea4a98469ac"}, + {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec63da4e7e4a5f924b90af42eddf20b698a70e58d86a72d943857c4c6045b3ee"}, + {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca238ceb7ba0bdf6ce88c1b74a87bffcee5afbfa1e41e173b1ceb095b39add46"}, + {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:ca28641954f767f9822c24e927ad894d45d5a1e501767599647259cbf030b903"}, + {file = "cryptography-36.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:39bdf8e70eee6b1c7b289ec6e5d84d49a6bfa11f8b8646b5b3dfe41219153316"}, + {file = "cryptography-36.0.1.tar.gz", hash = "sha256:53e5c1dc3d7a953de055d77bef2ff607ceef7a2aac0353b5d630ab67f7423638"}, +] +dnspython = [ + {file = "dnspython-2.1.0-py3-none-any.whl", hash = "sha256:95d12f6ef0317118d2a1a6fc49aac65ffec7eb8087474158f42f26a639135216"}, + {file = "dnspython-2.1.0.zip", hash = "sha256:e4a87f0b573201a0f3727fa18a516b055fd1107e0e5477cded4a2de497df1dd4"}, +] +ecdsa = [ + {file = "ecdsa-0.17.0-py2.py3-none-any.whl", hash = "sha256:5cf31d5b33743abe0dfc28999036c849a69d548f994b535e527ee3cb7f3ef676"}, + {file = "ecdsa-0.17.0.tar.gz", hash = "sha256:b9f500bb439e4153d0330610f5d26baaf18d17b8ced1bc54410d189385ea68aa"}, +] +email-validator = [ + {file = "email_validator-1.1.3-py2.py3-none-any.whl", hash = "sha256:5675c8ceb7106a37e40e2698a57c056756bf3f272cfa8682a4f87ebd95d8440b"}, + {file = "email_validator-1.1.3.tar.gz", hash = "sha256:aa237a65f6f4da067119b7df3f13e89c25c051327b2b5b66dc075f33d62480d7"}, +] +fastapi = [ + {file = "fastapi-0.71.0-py3-none-any.whl", hash = "sha256:a78eca6b084de9667f2d5f37e2ae297270e5a119cd01c2f04815795da92fc87f"}, + {file = "fastapi-0.71.0.tar.gz", hash = "sha256:2b5ac0ae89c80b40d1dd4b2ea0bb1f78d7c4affd3644d080bf050f084759fff2"}, +] +flake8 = [ + {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, + {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, +] +greenlet = [ + {file = "greenlet-1.1.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6"}, + {file = "greenlet-1.1.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:aec52725173bd3a7b56fe91bc56eccb26fbdff1386ef123abb63c84c5b43b63a"}, + {file = "greenlet-1.1.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:833e1551925ed51e6b44c800e71e77dacd7e49181fdc9ac9a0bf3714d515785d"}, + {file = "greenlet-1.1.2-cp27-cp27m-win32.whl", hash = "sha256:aa5b467f15e78b82257319aebc78dd2915e4c1436c3c0d1ad6f53e47ba6e2713"}, + {file = "greenlet-1.1.2-cp27-cp27m-win_amd64.whl", hash = "sha256:40b951f601af999a8bf2ce8c71e8aaa4e8c6f78ff8afae7b808aae2dc50d4c40"}, + {file = "greenlet-1.1.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:95e69877983ea39b7303570fa6760f81a3eec23d0e3ab2021b7144b94d06202d"}, + {file = "greenlet-1.1.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:356b3576ad078c89a6107caa9c50cc14e98e3a6c4874a37c3e0273e4baf33de8"}, + {file = "greenlet-1.1.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8639cadfda96737427330a094476d4c7a56ac03de7265622fcf4cfe57c8ae18d"}, + {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97e5306482182170ade15c4b0d8386ded995a07d7cc2ca8f27958d34d6736497"}, + {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6a36bb9474218c7a5b27ae476035497a6990e21d04c279884eb10d9b290f1b1"}, + {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abb7a75ed8b968f3061327c433a0fbd17b729947b400747c334a9c29a9af6c58"}, + {file = "greenlet-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:14d4f3cd4e8b524ae9b8aa567858beed70c392fdec26dbdb0a8a418392e71708"}, + {file = "greenlet-1.1.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:17ff94e7a83aa8671a25bf5b59326ec26da379ace2ebc4411d690d80a7fbcf23"}, + {file = "greenlet-1.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9f3cba480d3deb69f6ee2c1825060177a22c7826431458c697df88e6aeb3caee"}, + {file = "greenlet-1.1.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:fa877ca7f6b48054f847b61d6fa7bed5cebb663ebc55e018fda12db09dcc664c"}, + {file = "greenlet-1.1.2-cp35-cp35m-win32.whl", hash = "sha256:7cbd7574ce8e138bda9df4efc6bf2ab8572c9aff640d8ecfece1b006b68da963"}, + {file = "greenlet-1.1.2-cp35-cp35m-win_amd64.whl", hash = "sha256:903bbd302a2378f984aef528f76d4c9b1748f318fe1294961c072bdc7f2ffa3e"}, + {file = "greenlet-1.1.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:049fe7579230e44daef03a259faa24511d10ebfa44f69411d99e6a184fe68073"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:dd0b1e9e891f69e7675ba5c92e28b90eaa045f6ab134ffe70b52e948aa175b3c"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7418b6bfc7fe3331541b84bb2141c9baf1ec7132a7ecd9f375912eca810e714e"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9d29ca8a77117315101425ec7ec2a47a22ccf59f5593378fc4077ac5b754fce"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21915eb821a6b3d9d8eefdaf57d6c345b970ad722f856cd71739493ce003ad08"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eff9d20417ff9dcb0d25e2defc2574d10b491bf2e693b4e491914738b7908168"}, + {file = "greenlet-1.1.2-cp36-cp36m-win32.whl", hash = "sha256:32ca72bbc673adbcfecb935bb3fb1b74e663d10a4b241aaa2f5a75fe1d1f90aa"}, + {file = "greenlet-1.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f0214eb2a23b85528310dad848ad2ac58e735612929c8072f6093f3585fd342d"}, + {file = "greenlet-1.1.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:b92e29e58bef6d9cfd340c72b04d74c4b4e9f70c9fa7c78b674d1fec18896dc4"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fdcec0b8399108577ec290f55551d926d9a1fa6cad45882093a7a07ac5ec147b"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:93f81b134a165cc17123626ab8da2e30c0455441d4ab5576eed73a64c025b25c"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e12bdc622676ce47ae9abbf455c189e442afdde8818d9da983085df6312e7a1"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c790abda465726cfb8bb08bd4ca9a5d0a7bd77c7ac1ca1b839ad823b948ea28"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f276df9830dba7a333544bd41070e8175762a7ac20350786b322b714b0e654f5"}, + {file = "greenlet-1.1.2-cp37-cp37m-win32.whl", hash = "sha256:64e6175c2e53195278d7388c454e0b30997573f3f4bd63697f88d855f7a6a1fc"}, + {file = "greenlet-1.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b11548073a2213d950c3f671aa88e6f83cda6e2fb97a8b6317b1b5b33d850e06"}, + {file = "greenlet-1.1.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:9633b3034d3d901f0a46b7939f8c4d64427dfba6bbc5a36b1a67364cf148a1b0"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:eb6ea6da4c787111adf40f697b4e58732ee0942b5d3bd8f435277643329ba627"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:f3acda1924472472ddd60c29e5b9db0cec629fbe3c5c5accb74d6d6d14773478"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e859fcb4cbe93504ea18008d1df98dee4f7766db66c435e4882ab35cf70cac43"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00e44c8afdbe5467e4f7b5851be223be68adb4272f44696ee71fe46b7036a711"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec8c433b3ab0419100bd45b47c9c8551248a5aee30ca5e9d399a0b57ac04651b"}, + {file = "greenlet-1.1.2-cp38-cp38-win32.whl", hash = "sha256:288c6a76705dc54fba69fbcb59904ae4ad768b4c768839b8ca5fdadec6dd8cfd"}, + {file = "greenlet-1.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:8d2f1fb53a421b410751887eb4ff21386d119ef9cde3797bf5e7ed49fb51a3b3"}, + {file = "greenlet-1.1.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:166eac03e48784a6a6e0e5f041cfebb1ab400b394db188c48b3a84737f505b67"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:572e1787d1460da79590bf44304abbc0a2da944ea64ec549188fa84d89bba7ab"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:be5f425ff1f5f4b3c1e33ad64ab994eed12fc284a6ea71c5243fd564502ecbe5"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1692f7d6bc45e3200844be0dba153612103db241691088626a33ff1f24a0d88"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7227b47e73dedaa513cdebb98469705ef0d66eb5a1250144468e9c3097d6b59b"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ff61ff178250f9bb3cd89752df0f1dd0e27316a8bd1465351652b1b4a4cdfd3"}, + {file = "greenlet-1.1.2-cp39-cp39-win32.whl", hash = "sha256:f70a9e237bb792c7cc7e44c531fd48f5897961701cdaa06cf22fc14965c496cf"}, + {file = "greenlet-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd"}, + {file = "greenlet-1.1.2.tar.gz", hash = "sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a"}, +] +h11 = [ + {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, + {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, +] +httpcore = [ + {file = "httpcore-0.14.4-py3-none-any.whl", hash = "sha256:9410fe352bea732311f2b2bee0555c8cc5e62b9a73b9d3272fe125a2aa6eb28e"}, + {file = "httpcore-0.14.4.tar.gz", hash = "sha256:d4305811f604d3c2e22869147392f134796976ff946c96a8cfba87f4e0171d83"}, +] +httpx = [ + {file = "httpx-0.21.3-py3-none-any.whl", hash = "sha256:df9a0fd43fa79dbab411d83eb1ea6f7a525c96ad92e60c2d7f40388971b25777"}, + {file = "httpx-0.21.3.tar.gz", hash = "sha256:7a3eb67ef0b8abbd6d9402248ef2f84a76080fa1c839f8662e6eb385640e445a"}, +] +idna = [ + {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, + {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, +] +importlib-metadata = [ + {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, + {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, +] +importlib-resources = [ + {file = "importlib_resources-5.4.0-py3-none-any.whl", hash = "sha256:33a95faed5fc19b4bc16b29a6eeae248a3fe69dd55d4d229d2b480e23eeaad45"}, + {file = "importlib_resources-5.4.0.tar.gz", hash = "sha256:d756e2f85dd4de2ba89be0b21dba2a3bbec2e871a42a3a16719258a11f87506b"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +isort = [ + {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, + {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, +] +mako = [ + {file = "Mako-1.1.6-py2.py3-none-any.whl", hash = "sha256:afaf8e515d075b22fad7d7b8b30e4a1c90624ff2f3733a06ec125f5a5f043a57"}, + {file = "Mako-1.1.6.tar.gz", hash = "sha256:4e9e345a41924a954251b95b4b28e14a301145b544901332e658907a7464b6b2"}, +] +markupsafe = [ + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, + {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +packaging = [ + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, +] +passlib = [ + {file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"}, + {file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"}, +] +pathspec = [ + {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, + {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, +] +platformdirs = [ + {file = "platformdirs-2.4.1-py3-none-any.whl", hash = "sha256:1d7385c7db91728b83efd0ca99a5afb296cab9d0ed8313a45ed8ba17967ecfca"}, + {file = "platformdirs-2.4.1.tar.gz", hash = "sha256:440633ddfebcc36264232365d7840a970e75e1018d15b4327d11f91909045fda"}, +] +pluggy = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] +py = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] +pyasn1 = [ + {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, + {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"}, + {file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"}, + {file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"}, + {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, + {file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"}, + {file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"}, + {file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"}, + {file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"}, + {file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"}, + {file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"}, + {file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"}, + {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, +] +pycodestyle = [ + {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, + {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, +] +pycparser = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] +pydantic = [ + {file = "pydantic-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cb23bcc093697cdea2708baae4f9ba0e972960a835af22560f6ae4e7e47d33f5"}, + {file = "pydantic-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1d5278bd9f0eee04a44c712982343103bba63507480bfd2fc2790fa70cd64cf4"}, + {file = "pydantic-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab624700dc145aa809e6f3ec93fb8e7d0f99d9023b713f6a953637429b437d37"}, + {file = "pydantic-1.9.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c8d7da6f1c1049eefb718d43d99ad73100c958a5367d30b9321b092771e96c25"}, + {file = "pydantic-1.9.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3c3b035103bd4e2e4a28da9da7ef2fa47b00ee4a9cf4f1a735214c1bcd05e0f6"}, + {file = "pydantic-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3011b975c973819883842c5ab925a4e4298dffccf7782c55ec3580ed17dc464c"}, + {file = "pydantic-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:086254884d10d3ba16da0588604ffdc5aab3f7f09557b998373e885c690dd398"}, + {file = "pydantic-1.9.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0fe476769acaa7fcddd17cadd172b156b53546ec3614a4d880e5d29ea5fbce65"}, + {file = "pydantic-1.9.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8e9dcf1ac499679aceedac7e7ca6d8641f0193c591a2d090282aaf8e9445a46"}, + {file = "pydantic-1.9.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1e4c28f30e767fd07f2ddc6f74f41f034d1dd6bc526cd59e63a82fe8bb9ef4c"}, + {file = "pydantic-1.9.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:c86229333cabaaa8c51cf971496f10318c4734cf7b641f08af0a6fbf17ca3054"}, + {file = "pydantic-1.9.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:c0727bda6e38144d464daec31dff936a82917f431d9c39c39c60a26567eae3ed"}, + {file = "pydantic-1.9.0-cp36-cp36m-win_amd64.whl", hash = "sha256:dee5ef83a76ac31ab0c78c10bd7d5437bfdb6358c95b91f1ba7ff7b76f9996a1"}, + {file = "pydantic-1.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9c9bdb3af48e242838f9f6e6127de9be7063aad17b32215ccc36a09c5cf1070"}, + {file = "pydantic-1.9.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ee7e3209db1e468341ef41fe263eb655f67f5c5a76c924044314e139a1103a2"}, + {file = "pydantic-1.9.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b6037175234850ffd094ca77bf60fb54b08b5b22bc85865331dd3bda7a02fa1"}, + {file = "pydantic-1.9.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b2571db88c636d862b35090ccf92bf24004393f85c8870a37f42d9f23d13e032"}, + {file = "pydantic-1.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8b5ac0f1c83d31b324e57a273da59197c83d1bb18171e512908fe5dc7278a1d6"}, + {file = "pydantic-1.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:bbbc94d0c94dd80b3340fc4f04fd4d701f4b038ebad72c39693c794fd3bc2d9d"}, + {file = "pydantic-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e0896200b6a40197405af18828da49f067c2fa1f821491bc8f5bde241ef3f7d7"}, + {file = "pydantic-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bdfdadb5994b44bd5579cfa7c9b0e1b0e540c952d56f627eb227851cda9db77"}, + {file = "pydantic-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:574936363cd4b9eed8acdd6b80d0143162f2eb654d96cb3a8ee91d3e64bf4cf9"}, + {file = "pydantic-1.9.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c556695b699f648c58373b542534308922c46a1cda06ea47bc9ca45ef5b39ae6"}, + {file = "pydantic-1.9.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f947352c3434e8b937e3aa8f96f47bdfe6d92779e44bb3f41e4c213ba6a32145"}, + {file = "pydantic-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5e48ef4a8b8c066c4a31409d91d7ca372a774d0212da2787c0d32f8045b1e034"}, + {file = "pydantic-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:96f240bce182ca7fe045c76bcebfa0b0534a1bf402ed05914a6f1dadff91877f"}, + {file = "pydantic-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:815ddebb2792efd4bba5488bc8fde09c29e8ca3227d27cf1c6990fc830fd292b"}, + {file = "pydantic-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c5b77947b9e85a54848343928b597b4f74fc364b70926b3c4441ff52620640c"}, + {file = "pydantic-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c68c3bc88dbda2a6805e9a142ce84782d3930f8fdd9655430d8576315ad97ce"}, + {file = "pydantic-1.9.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a79330f8571faf71bf93667d3ee054609816f10a259a109a0738dac983b23c3"}, + {file = "pydantic-1.9.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f5a64b64ddf4c99fe201ac2724daada8595ada0d102ab96d019c1555c2d6441d"}, + {file = "pydantic-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a733965f1a2b4090a5238d40d983dcd78f3ecea221c7af1497b845a9709c1721"}, + {file = "pydantic-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cc6a4cb8a118ffec2ca5fcb47afbacb4f16d0ab8b7350ddea5e8ef7bcc53a16"}, + {file = "pydantic-1.9.0-py3-none-any.whl", hash = "sha256:085ca1de245782e9b46cefcf99deecc67d418737a1fd3f6a4f511344b613a5b3"}, + {file = "pydantic-1.9.0.tar.gz", hash = "sha256:742645059757a56ecd886faf4ed2441b9c0cd406079c2b4bee51bcc3fbcd510a"}, +] +pyflakes = [ + {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, + {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, +] +pyparsing = [ + {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, + {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"}, +] +pytest = [ + {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, + {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, +] +pytest-asyncio = [ + {file = "pytest-asyncio-0.16.0.tar.gz", hash = "sha256:7496c5977ce88c34379df64a66459fe395cd05543f0a2f837016e7144391fcfb"}, + {file = "pytest_asyncio-0.16.0-py3-none-any.whl", hash = "sha256:5f2a21273c47b331ae6aa5b36087047b4899e40f03f18397c0e65fa5cca54e9b"}, +] +python-dotenv = [ + {file = "python-dotenv-0.19.2.tar.gz", hash = "sha256:a5de49a31e953b45ff2d2fd434bbc2670e8db5273606c1e737cc6b93eff3655f"}, + {file = "python_dotenv-0.19.2-py2.py3-none-any.whl", hash = "sha256:32b2bdc1873fd3a3c346da1c6db83d0053c3c62f28f1f38516070c4c8971b1d3"}, +] +python-jose = [ + {file = "python-jose-3.3.0.tar.gz", hash = "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a"}, + {file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"}, +] +python-multipart = [ + {file = "python-multipart-0.0.5.tar.gz", hash = "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"}, +] +requests = [ + {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, + {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, +] +rfc3986 = [ + {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, + {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, +] +rsa = [ + {file = "rsa-4.8-py3-none-any.whl", hash = "sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb"}, + {file = "rsa-4.8.tar.gz", hash = "sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17"}, +] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] +sniffio = [ + {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"}, + {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"}, +] +sqlalchemy = [ + {file = "SQLAlchemy-1.4.29-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:da64423c05256f4ab8c0058b90202053b201cbe3a081f3a43eb590cd554395ab"}, + {file = "SQLAlchemy-1.4.29-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0fc4eec2f46b40bdd42112b3be3fbbf88e194bcf02950fbb88bcdc1b32f07dc7"}, + {file = "SQLAlchemy-1.4.29-cp27-cp27m-win32.whl", hash = "sha256:101d2e100ba9182c9039699588e0b2d833c54b3bad46c67c192159876c9f27ea"}, + {file = "SQLAlchemy-1.4.29-cp27-cp27m-win_amd64.whl", hash = "sha256:ceac84dd9abbbe115e8be0c817bed85d9fa639b4d294e7817f9e61162d5f766c"}, + {file = "SQLAlchemy-1.4.29-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:15b65887b6c324cad638c7671cb95985817b733242a7eb69edd7cdf6953be1e0"}, + {file = "SQLAlchemy-1.4.29-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:78abc507d17753ed434b6cc0c0693126279723d5656d9775bfcac966a99a899b"}, + {file = "SQLAlchemy-1.4.29-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb8c993706e86178ce15a6b86a335a2064f52254b640e7f53365e716423d33f4"}, + {file = "SQLAlchemy-1.4.29-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:804e22d5b6165a4f3f019dd9c94bec5687de985a9c54286b93ded9f7846b8c82"}, + {file = "SQLAlchemy-1.4.29-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56d9d62021946263d4478c9ca012fbd1805f10994cb615c88e7bfd1ae14604d8"}, + {file = "SQLAlchemy-1.4.29-cp310-cp310-win32.whl", hash = "sha256:027f356c727db24f3c75828c7feb426f87ce1241242d08958e454bd025810660"}, + {file = "SQLAlchemy-1.4.29-cp310-cp310-win_amd64.whl", hash = "sha256:debaf09a823061f88a8dee04949814cf7e82fb394c5bca22c780cb03172ca23b"}, + {file = "SQLAlchemy-1.4.29-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:dc27dcc6c72eb38be7f144e9c2c4372d35a3684d3a6dd43bd98c1238358ee17c"}, + {file = "SQLAlchemy-1.4.29-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4ddd4f2e247128c58bb3dd4489922874afce157d2cff0b2295d67fcd0f22494"}, + {file = "SQLAlchemy-1.4.29-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9ce960a1dc60524136cf6f75621588e2508a117e04a6e3eedb0968bd13b8c824"}, + {file = "SQLAlchemy-1.4.29-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5919e647e1d4805867ea556ed4967c68b4d8b266059fa35020dbaed8ffdd60f3"}, + {file = "SQLAlchemy-1.4.29-cp36-cp36m-win32.whl", hash = "sha256:886359f734b95ad1ef443b13bb4518bcade4db4f9553c9ce33d6d04ebda8d44e"}, + {file = "SQLAlchemy-1.4.29-cp36-cp36m-win_amd64.whl", hash = "sha256:e9cc6d844e24c307c3272677982a9b33816aeb45e4977791c3bdd47637a8d810"}, + {file = "SQLAlchemy-1.4.29-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:5e9cd33459afa69c88fa648e803d1f1245e3caa60bfe8b80a9595e5edd3bda9c"}, + {file = "SQLAlchemy-1.4.29-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eeaebceb24b46e884c4ad3c04f37feb178b81f6ce720af19bfa2592ca32fdef7"}, + {file = "SQLAlchemy-1.4.29-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e89347d3bd2ef873832b47e85f4bbd810a5e626c5e749d90a07638da100eb1c8"}, + {file = "SQLAlchemy-1.4.29-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a717c2e70fd1bb477161c4cc85258e41d978584fbe5522613618195f7e87d9b"}, + {file = "SQLAlchemy-1.4.29-cp37-cp37m-win32.whl", hash = "sha256:f74d6c05d2d163464adbdfbc1ab85048cc15462ff7d134b8aed22bd521e1faa5"}, + {file = "SQLAlchemy-1.4.29-cp37-cp37m-win_amd64.whl", hash = "sha256:621854dbb4d2413c759a5571564170de45ef37299df52e78e62b42e2880192e1"}, + {file = "SQLAlchemy-1.4.29-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:f3909194751bb6cb7c5511dd18bcf77e6e3f0b31604ed4004dffa9461f71e737"}, + {file = "SQLAlchemy-1.4.29-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd49d21d1f03c81fbec9080ecdc4486d5ddda67e7fbb75ebf48294465c022cdc"}, + {file = "SQLAlchemy-1.4.29-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e5f6959466a42b6569774c257e55f9cd85200d5b0ba09f0f5d8b5845349c5822"}, + {file = "SQLAlchemy-1.4.29-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0072f9887aabe66db23f818bbe950cfa1b6127c5cb769b00bcc07935b3adb0ad"}, + {file = "SQLAlchemy-1.4.29-cp38-cp38-win32.whl", hash = "sha256:ad618d687d26d4cbfa9c6fa6141d59e05bcdfc60cb6e1f1d3baa18d8c62fef5f"}, + {file = "SQLAlchemy-1.4.29-cp38-cp38-win_amd64.whl", hash = "sha256:878daecb6405e786b07f97e1c77a9cfbbbec17432e8a90c487967e32cfdecb33"}, + {file = "SQLAlchemy-1.4.29-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:e027bdf0a4cf6bd0a3ad3b998643ea374d7991bd117b90bf9982e41ceb742941"}, + {file = "SQLAlchemy-1.4.29-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5de7adfb91d351f44062b8dedf29f49d4af7cb765be65816e79223a4e31062b"}, + {file = "SQLAlchemy-1.4.29-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fbc6e63e481fa323036f305ada96a3362e1d60dd2bfa026cac10c3553e6880e9"}, + {file = "SQLAlchemy-1.4.29-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7dd0502cb091660ad0d89c5e95a29825f37cde2a5249957838e975871fbffaad"}, + {file = "SQLAlchemy-1.4.29-cp39-cp39-win32.whl", hash = "sha256:37b46bfc4af3dc226acb6fa28ecd2e1fd223433dc5e15a2bad62bf0a0cbb4e8b"}, + {file = "SQLAlchemy-1.4.29-cp39-cp39-win_amd64.whl", hash = "sha256:08cfd35eecaba79be930c9bfd2e1f0c67a7e1314355d83a378f9a512b1cf7587"}, + {file = "SQLAlchemy-1.4.29.tar.gz", hash = "sha256:fa2bad14e1474ba649cfc969c1d2ec915dd3e79677f346bbfe08e93ef9020b39"}, +] +sqlalchemy2-stubs = [ + {file = "sqlalchemy2-stubs-0.0.2a19.tar.gz", hash = "sha256:2117c48ce5acfe33bf9c9bfce2a981632d931949e68fa313aa5c2a3bc980ca7a"}, + {file = "sqlalchemy2_stubs-0.0.2a19-py3-none-any.whl", hash = "sha256:aac7dca77a2c49e5f0934976421d5e25ae4dc5e27db48c01e055f81caa1e3ead"}, +] +starlette = [ + {file = "starlette-0.17.1-py3-none-any.whl", hash = "sha256:26a18cbda5e6b651c964c12c88b36d9898481cd428ed6e063f5f29c418f73050"}, + {file = "starlette-0.17.1.tar.gz", hash = "sha256:57eab3cc975a28af62f6faec94d355a410634940f10b30d68d31cb5ec1b44ae8"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +tomli = [ + {file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"}, + {file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"}, +] +typed-ast = [ + {file = "typed_ast-1.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d8314c92414ce7481eee7ad42b353943679cf6f30237b5ecbf7d835519e1212"}, + {file = "typed_ast-1.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b53ae5de5500529c76225d18eeb060efbcec90ad5e030713fe8dab0fb4531631"}, + {file = "typed_ast-1.5.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:24058827d8f5d633f97223f5148a7d22628099a3d2efe06654ce872f46f07cdb"}, + {file = "typed_ast-1.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:a6d495c1ef572519a7bac9534dbf6d94c40e5b6a608ef41136133377bba4aa08"}, + {file = "typed_ast-1.5.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:de4ecae89c7d8b56169473e08f6bfd2df7f95015591f43126e4ea7865928677e"}, + {file = "typed_ast-1.5.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:256115a5bc7ea9e665c6314ed6671ee2c08ca380f9d5f130bd4d2c1f5848d695"}, + {file = "typed_ast-1.5.1-cp36-cp36m-win_amd64.whl", hash = "sha256:7c42707ab981b6cf4b73490c16e9d17fcd5227039720ca14abe415d39a173a30"}, + {file = "typed_ast-1.5.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:71dcda943a471d826ea930dd449ac7e76db7be778fcd722deb63642bab32ea3f"}, + {file = "typed_ast-1.5.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4f30a2bcd8e68adbb791ce1567fdb897357506f7ea6716f6bbdd3053ac4d9471"}, + {file = "typed_ast-1.5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ca9e8300d8ba0b66d140820cf463438c8e7b4cdc6fd710c059bfcfb1531d03fb"}, + {file = "typed_ast-1.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9caaf2b440efb39ecbc45e2fabde809cbe56272719131a6318fd9bf08b58e2cb"}, + {file = "typed_ast-1.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c9bcad65d66d594bffab8575f39420fe0ee96f66e23c4d927ebb4e24354ec1af"}, + {file = "typed_ast-1.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:591bc04e507595887160ed7aa8d6785867fb86c5793911be79ccede61ae96f4d"}, + {file = "typed_ast-1.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:a80d84f535642420dd17e16ae25bb46c7f4c16ee231105e7f3eb43976a89670a"}, + {file = "typed_ast-1.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:38cf5c642fa808300bae1281460d4f9b7617cf864d4e383054a5ef336e344d32"}, + {file = "typed_ast-1.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b6ab14c56bc9c7e3c30228a0a0b54b915b1579613f6e463ba6f4eb1382e7fd4"}, + {file = "typed_ast-1.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a2b8d7007f6280e36fa42652df47087ac7b0a7d7f09f9468f07792ba646aac2d"}, + {file = "typed_ast-1.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:b6d17f37f6edd879141e64a5db17b67488cfeffeedad8c5cec0392305e9bc775"}, + {file = "typed_ast-1.5.1.tar.gz", hash = "sha256:484137cab8ecf47e137260daa20bafbba5f4e3ec7fda1c1e69ab299b75fa81c5"}, +] +typing-extensions = [ + {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, + {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, +] +urllib3 = [ + {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, + {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"}, +] +uvicorn = [ + {file = "uvicorn-0.16.0-py3-none-any.whl", hash = "sha256:d8c839231f270adaa6d338d525e2652a0b4a5f4c2430b5c4ef6ae4d11776b0d2"}, + {file = "uvicorn-0.16.0.tar.gz", hash = "sha256:eacb66afa65e0648fcbce5e746b135d09722231ffffc61883d4fac2b62fbea8d"}, +] +zipp = [ + {file = "zipp-3.7.0-py3-none-any.whl", hash = "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"}, + {file = "zipp-3.7.0.tar.gz", hash = "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d"}, +] diff --git a/{{cookiecutter.project_name}}/template_minimal/pre-push.sh b/{{cookiecutter.project_name}}/template_minimal/pre-push.sh new file mode 100644 index 0000000..8631794 --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/pre-push.sh @@ -0,0 +1,16 @@ +echo "export requirements.txt" +poetry export -o requirements.txt --without-hashes +poetry export -o requirements-dev.txt --dev --without-hashes +echo "autoflake" +autoflake --recursive --in-place \ + --remove-unused-variables \ + --remove-all-unused-imports \ + --ignore-init-module-imports \ + app +echo "black" +black app +echo "isort" +isort app +echo "flake8" +flake8 app --count --statistics +echo "OK" \ No newline at end of file diff --git a/{{cookiecutter.project_name}}/template_minimal/pyproject.toml b/{{cookiecutter.project_name}}/template_minimal/pyproject.toml new file mode 100644 index 0000000..a13fdac --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/pyproject.toml @@ -0,0 +1,47 @@ +[tool.poetry] +name = "{{cookiecutter.project_name}}" +version = "0.1.0-alpha" +description = "FastAPI project generated using minimal-fastapi-postgres-template." +authors = ["admin "] + +[tool.poetry.dependencies] +python = "^3.9" +fastapi = "^0.71.0" +SQLAlchemy = {extras = ["asyncio"], version = "^1.4.29"} +uvicorn = "^0.16.0" +python-dotenv = "^0.19.2" +requests = "^2.27.1" +python-jose = {extras = ["cryptography"], version = "^3.3.0"} +passlib = {extras = ["bcrypt"], version = "^1.7.4"} +pydantic = {extras = ["email"], version = "^1.9.0"} +alembic = "^1.7.5" +python-multipart = "^0.0.5" +asyncpg = "^0.25.0" + +[tool.poetry.dev-dependencies] +black = {version = "^21.12b0", allow-prereleases = true} +autoflake = "^1.4" +flake8 = "^4.0.1" +isort = "^5.10.1" +coverage = "^6.2" +pytest = "^6.2.5" +pytest-asyncio = "^0.16.0" +httpx = "^0.21.3" +sqlalchemy2-stubs = "^0.0.2-alpha.19" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.pytest.ini_options] +minversion = "6.0" +addopts = "-v" +filterwarnings = [ + "ignore::passlib.exc.PasslibHashWarning" +] +testpaths = [ + "app/tests", +] + +[tool.isort] +profile = "black" \ No newline at end of file diff --git a/{{cookiecutter.project_name}}/template_minimal/requirements-dev.txt b/{{cookiecutter.project_name}}/template_minimal/requirements-dev.txt new file mode 100644 index 0000000..e983a91 --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/requirements-dev.txt @@ -0,0 +1,69 @@ +alembic==1.7.5; python_version >= "3.6" +anyio==3.4.0; python_full_version >= "3.6.2" and python_version >= "3.6" +asgiref==3.4.1; python_version >= "3.6" +asyncpg==0.24.0; python_full_version >= "3.6.0" +atomicwrites==1.4.0; python_version >= "3.6" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.6" and python_full_version >= "3.4.0" +attrs==21.2.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" +autoflake==1.4 +bcrypt==3.2.0; python_version >= "3.6" +black==21.11b1; python_full_version >= "3.6.2" and python_full_version < "4.0.0" +certifi==2021.10.8; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6" +cffi==1.15.0 +charset-normalizer==2.0.8; python_full_version >= "3.6.0" and python_version >= "3.6" +click==8.0.3; python_full_version >= "3.6.2" and python_full_version < "4.0.0" and python_version >= "3.6" +colorama==0.4.4; python_version >= "3.6" and python_full_version < "3.0.0" and sys_platform == "win32" and platform_system == "Windows" or sys_platform == "win32" and python_version >= "3.6" and python_full_version >= "3.5.0" and platform_system == "Windows" +coverage==5.5; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0" and python_version < "4") +cryptography==36.0.0; python_version >= "3.6" +dnspython==2.1.0; python_full_version >= "3.6.1" and python_version >= "3.6" +ecdsa==0.17.0; python_version >= "2.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" +email-validator==1.1.3; python_full_version >= "3.6.1" +fastapi==0.68.2; python_full_version >= "3.6.1" +flake8==3.9.2; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") +greenlet==1.1.2; python_version >= "3" and python_full_version < "3.0.0" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32") and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0") and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6") or python_version >= "3" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32") and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0") and python_full_version >= "3.5.0" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6") +h11==0.12.0; python_version >= "3.6" +httpcore==0.13.7; python_version >= "3.6" +httpx==0.20.0; python_version >= "3.6" +idna==3.3 +importlib-metadata==4.8.2; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.8" or python_full_version >= "3.6.0" and python_version < "3.8" and python_version >= "3.6" +importlib-resources==5.4.0; python_version < "3.9" and python_version >= "3.6" +iniconfig==1.1.1; python_version >= "3.6" +isort==5.10.1; python_full_version >= "3.6.1" and python_version < "4.0" +mako==1.1.6; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" +markupsafe==2.0.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" +mccabe==0.6.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" +mypy-extensions==0.4.3; python_full_version >= "3.6.2" and python_full_version < "4.0.0" and python_version >= "3.5" +mypy==0.910; python_version >= "3.5" +packaging==21.3; python_version >= "3.6" +passlib==1.7.4 +pathspec==0.9.0; python_full_version >= "3.6.2" and python_full_version < "4.0.0" +platformdirs==2.4.0; python_full_version >= "3.6.2" and python_full_version < "4.0.0" and python_version >= "3.6" +pluggy==1.0.0; python_version >= "3.6" +py==1.11.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" +pyasn1==0.4.8; python_version >= "3.6" and python_version < "4" +pycodestyle==2.7.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" +pycparser==2.21; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.4.0" +pydantic==1.8.2; python_full_version >= "3.6.1" +pyflakes==2.3.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" +pyparsing==3.0.6; python_version >= "3.6" +pytest-asyncio==0.16.0; python_version >= "3.6" +pytest==6.2.5; python_version >= "3.6" +python-dotenv==0.19.2; python_version >= "3.5" +python-jose==3.3.0 +python-multipart==0.0.5 +regex==2021.11.10; python_full_version >= "3.6.2" and python_full_version < "4.0.0" +requests==2.26.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") +rfc3986==1.5.0; python_version >= "3.6" +rsa==4.8; python_version >= "3.6" and python_version < "4" +six==1.16.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.3.0" +sniffio==1.2.0; python_full_version >= "3.6.2" and python_version >= "3.6" +sqlalchemy-stubs==0.4 +sqlalchemy2-stubs==0.0.2a19; python_version >= "3.6" +sqlalchemy==1.4.27; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") +starlette==0.14.2; python_version >= "3.6" and python_full_version >= "3.6.1" +toml==0.10.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6" +tomli==1.2.2; python_full_version >= "3.6.2" and python_full_version < "4.0.0" and python_version >= "3.6" +typed-ast==1.4.3; python_version < "3.8" and implementation_name == "cpython" and python_full_version >= "3.6.2" and python_full_version < "4.0.0" and python_version >= "3.5" +typing-extensions==4.0.0 +urllib3==1.26.7; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version < "4" +uvicorn==0.15.0 +zipp==3.6.0; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.8" or python_full_version >= "3.6.0" and python_version < "3.8" and python_version >= "3.6" diff --git a/{{cookiecutter.project_name}}/template_minimal/requirements.txt b/{{cookiecutter.project_name}}/template_minimal/requirements.txt new file mode 100644 index 0000000..ed2b5d3 --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/requirements.txt @@ -0,0 +1,37 @@ +alembic==1.7.5; python_version >= "3.6" +asgiref==3.4.1; python_version >= "3.6" +asyncpg==0.24.0; python_full_version >= "3.6.0" +bcrypt==3.2.0; python_version >= "3.6" +certifi==2021.10.8; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" +cffi==1.15.0 +charset-normalizer==2.0.8; python_full_version >= "3.6.0" and python_version >= "3" +click==8.0.3; python_version >= "3.6" +colorama==0.4.4; python_version >= "3.6" and python_full_version < "3.0.0" and platform_system == "Windows" or platform_system == "Windows" and python_version >= "3.6" and python_full_version >= "3.5.0" +cryptography==36.0.0; python_version >= "3.6" +dnspython==2.1.0; python_full_version >= "3.6.1" and python_version >= "3.6" +ecdsa==0.17.0; python_version >= "2.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" +email-validator==1.1.3; python_full_version >= "3.6.1" +fastapi==0.68.2; python_full_version >= "3.6.1" +greenlet==1.1.2; python_version >= "3" and python_full_version < "3.0.0" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32") and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0") and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6") or python_version >= "3" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32") and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0") and python_full_version >= "3.5.0" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6") +h11==0.12.0; python_version >= "3.6" +idna==3.3; python_full_version >= "3.6.1" and python_version >= "3.5" +importlib-metadata==4.8.2; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.8" or python_full_version >= "3.6.0" and python_version < "3.8" and python_version >= "3.6" +importlib-resources==5.4.0; python_version < "3.9" and python_version >= "3.6" +mako==1.1.6; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" +markupsafe==2.0.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" +passlib==1.7.4 +pyasn1==0.4.8; python_version >= "3.6" and python_version < "4" +pycparser==2.21; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.4.0" +pydantic==1.8.2; python_full_version >= "3.6.1" +python-dotenv==0.19.2; python_version >= "3.5" +python-jose==3.3.0 +python-multipart==0.0.5 +requests==2.26.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") +rsa==4.8; python_version >= "3.6" and python_version < "4" +six==1.16.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.3.0" +sqlalchemy==1.4.27; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") +starlette==0.14.2; python_version >= "3.6" and python_full_version >= "3.6.1" +typing-extensions==4.0.0; python_version >= "3.6" and python_version < "3.8" and python_full_version >= "3.6.1" and (python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.8" or python_full_version >= "3.6.0" and python_version < "3.8" and python_version >= "3.6") +urllib3==1.26.7; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version < "4" +uvicorn==0.15.0 +zipp==3.6.0; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.8" or python_full_version >= "3.6.0" and python_version < "3.8" and python_version >= "3.6" From 0ed3ae8842493f795592dfe2a8acb08d9ac72f5a Mon Sep 17 00:00:00 2001 From: rafsaf Date: Sun, 16 Jan 2022 01:59:28 +0100 Subject: [PATCH 02/11] added template_fastapi_users, changes in project structure --- .../2021_11_09_1736_init__cefce371682e.py | 38 -- ...itial_fastapi_users_model__57b460a916d4.py | 39 ++ .../template_fastapi_users/app/api/api.py | 30 +- .../template_fastapi_users/app/api/deps.py | 49 ++- .../app/api/endpoints/auth.py | 91 ----- .../app/api/endpoints/users.py | 41 -- .../template_fastapi_users/app/core/config.py | 12 +- .../app/core/security.py | 84 +++-- .../app/initial_data.py | 33 +- .../template_fastapi_users/app/main.py | 2 +- .../template_fastapi_users/app/models.py | 10 +- .../app/schemas/__init__.py | 3 +- .../app/schemas/token.py | 21 -- .../app/schemas/user.py | 42 ++- .../app/tests/conftest.py | 38 +- .../app/tests/test_auth.py | 22 +- .../template_fastapi_users/app/tests/utils.py | 28 ++ .../template_fastapi_users/poetry.lock | 352 +++++------------- .../template_fastapi_users/pyproject.toml | 9 +- .../requirements-dev.txt | 78 ++-- .../template_fastapi_users/requirements.txt | 48 ++- .../template_minimal/app/api/api.py | 2 +- .../app/api/endpoints/auth.py | 41 ++ .../template_minimal/app/core/config.py | 8 +- .../template_minimal/poetry.lock | 119 +----- 25 files changed, 471 insertions(+), 769 deletions(-) delete mode 100644 {{cookiecutter.project_name}}/template_fastapi_users/alembic/versions/2021_11_09_1736_init__cefce371682e.py create mode 100644 {{cookiecutter.project_name}}/template_fastapi_users/alembic/versions/2022_01_09_1952_initial_fastapi_users_model__57b460a916d4.py delete mode 100644 {{cookiecutter.project_name}}/template_fastapi_users/app/api/endpoints/auth.py delete mode 100644 {{cookiecutter.project_name}}/template_fastapi_users/app/api/endpoints/users.py delete mode 100644 {{cookiecutter.project_name}}/template_fastapi_users/app/schemas/token.py diff --git a/{{cookiecutter.project_name}}/template_fastapi_users/alembic/versions/2021_11_09_1736_init__cefce371682e.py b/{{cookiecutter.project_name}}/template_fastapi_users/alembic/versions/2021_11_09_1736_init__cefce371682e.py deleted file mode 100644 index bae5d4b..0000000 --- a/{{cookiecutter.project_name}}/template_fastapi_users/alembic/versions/2021_11_09_1736_init__cefce371682e.py +++ /dev/null @@ -1,38 +0,0 @@ -"""init - -Revision ID: cefce371682e -Revises: -Create Date: 2021-11-09 17:36:41.970204 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = 'cefce371682e' -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('user', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('full_name', sa.String(length=254), nullable=True), - sa.Column('email', sa.String(length=254), nullable=False), - sa.Column('hashed_password', sa.String(length=128), nullable=False), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_user_email'), 'user', ['email'], unique=True) - op.create_index(op.f('ix_user_id'), 'user', ['id'], unique=False) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_index(op.f('ix_user_id'), table_name='user') - op.drop_index(op.f('ix_user_email'), table_name='user') - op.drop_table('user') - # ### end Alembic commands ### diff --git a/{{cookiecutter.project_name}}/template_fastapi_users/alembic/versions/2022_01_09_1952_initial_fastapi_users_model__57b460a916d4.py b/{{cookiecutter.project_name}}/template_fastapi_users/alembic/versions/2022_01_09_1952_initial_fastapi_users_model__57b460a916d4.py new file mode 100644 index 0000000..aa186f6 --- /dev/null +++ b/{{cookiecutter.project_name}}/template_fastapi_users/alembic/versions/2022_01_09_1952_initial_fastapi_users_model__57b460a916d4.py @@ -0,0 +1,39 @@ +"""initial_fastapi_users_model + +Revision ID: 57b460a916d4 +Revises: +Create Date: 2022-01-09 19:52:37.810574 + +""" +from alembic import op +import sqlalchemy as sa +from fastapi_users_db_sqlalchemy import guid + +# revision identifiers, used by Alembic. +revision = "57b460a916d4" +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "user", + sa.Column("id", guid.GUID(), nullable=False), + sa.Column("email", sa.String(length=320), nullable=False), + sa.Column("hashed_password", sa.String(length=72), nullable=False), + sa.Column("is_active", sa.Boolean(), nullable=False), + sa.Column("is_superuser", sa.Boolean(), nullable=False), + sa.Column("is_verified", sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_user_email"), "user", ["email"], unique=True) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f("ix_user_email"), table_name="user") + op.drop_table("user") + # ### end Alembic commands ### diff --git a/{{cookiecutter.project_name}}/template_fastapi_users/app/api/api.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/api/api.py index 79b9497..2d91f3c 100644 --- a/{{cookiecutter.project_name}}/template_fastapi_users/app/api/api.py +++ b/{{cookiecutter.project_name}}/template_fastapi_users/app/api/api.py @@ -1,7 +1,31 @@ +""" +Users and auth routers 'for free' from FastAPI Users. +https://fastapi-users.github.io/fastapi-users/configuration/routers/ + +You can include more of them + oauth login endpoints. + +fastapi_users in defined in deps, because it also +includes useful dependencies. +""" + from fastapi import APIRouter -from app.api.endpoints import auth, users +from app.api.deps import fastapi_users +from app.core import security api_router = APIRouter() -api_router.include_router(auth.router, prefix="/auth", tags=["auth"]) -api_router.include_router(users.router, prefix="/users", tags=["users"]) +api_router.include_router( + fastapi_users.get_auth_router(security.AUTH_BACKEND), + prefix="/auth/jwt", + tags=["auth"], +) +api_router.include_router( + fastapi_users.get_register_router(), + prefix="/auth", + tags=["auth"], +) +api_router.include_router( + fastapi_users.get_users_router(), + prefix="/users", + tags=["users"], +) diff --git a/{{cookiecutter.project_name}}/template_fastapi_users/app/api/deps.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/api/deps.py index a8fc916..7dc3e17 100644 --- a/{{cookiecutter.project_name}}/template_fastapi_users/app/api/deps.py +++ b/{{cookiecutter.project_name}}/template_fastapi_users/app/api/deps.py @@ -1,15 +1,14 @@ -from typing import AsyncGenerator, Optional +from typing import AsyncGenerator -from fastapi import Depends, HTTPException, status +from fastapi import Depends from fastapi.security import OAuth2PasswordBearer -from jose import jwt -from pydantic import ValidationError -from sqlalchemy import select +from fastapi_users.fastapi_users import FastAPIUsers +from fastapi_users_db_sqlalchemy import SQLAlchemyUserDatabase from sqlalchemy.ext.asyncio import AsyncSession from app import schemas -from app.core import security, config -from app.models import User +from app.core import security +from app.models import UserTable from app.session import async_session reusable_oauth2 = OAuth2PasswordBearer(tokenUrl="auth/access-token") @@ -20,24 +19,24 @@ async def get_session() -> AsyncGenerator[AsyncSession, None]: yield session -async def get_current_user( - session: AsyncSession = Depends(get_session), token: str = Depends(reusable_oauth2) -) -> User: +async def get_user_db(session: AsyncSession = Depends(get_session)): + yield SQLAlchemyUserDatabase(schemas.UserDB, session, UserTable) - try: - payload = jwt.decode( - token, config.settings.SECRET_KEY, algorithms=[security.ALGORITHM] - ) - token_data = schemas.TokenPayload(**payload) - except (jwt.JWTError, ValidationError): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Could not validate credentials", - ) - result = await session.execute(select(User).where(User.id == token_data.sub)) - user: Optional[User] = result.scalars().first() +async def get_user_manager(user_db: SQLAlchemyUserDatabase = Depends(get_user_db)): + yield security.UserManager(user_db) - if not user: - raise HTTPException(status_code=404, detail="User not found") - return user + +fastapi_users = FastAPIUsers( + get_user_manager, # type: ignore + [security.AUTH_BACKEND], + schemas.User, + schemas.UserCreate, + schemas.UserUpdate, + schemas.UserDB, +) + + +get_current_user = fastapi_users.current_user() +get_current_active_user = fastapi_users.current_user(active=True) +get_current_superuser = fastapi_users.current_user(active=True, superuser=True) diff --git a/{{cookiecutter.project_name}}/template_fastapi_users/app/api/endpoints/auth.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/api/endpoints/auth.py deleted file mode 100644 index 90706bb..0000000 --- a/{{cookiecutter.project_name}}/template_fastapi_users/app/api/endpoints/auth.py +++ /dev/null @@ -1,91 +0,0 @@ -from typing import Optional - -from fastapi import APIRouter, Depends, HTTPException, status -from fastapi.security import OAuth2PasswordRequestForm -from jose import jwt -from pydantic import ValidationError -from sqlalchemy import select -from sqlalchemy.ext.asyncio import AsyncSession - -from app import schemas -from app.api import deps -from app.core import security, config -from app.models import User - -router = APIRouter() - - -@router.post("/access-token", response_model=schemas.Token) -async def login_access_token( - session: AsyncSession = Depends(deps.get_session), - form_data: OAuth2PasswordRequestForm = Depends(), -): - """ - OAuth2 compatible token, get an access token for future requests using username and password - """ - result = await session.execute(select(User).where(User.email == form_data.username)) - user: Optional[User] = result.scalars().first() - if user is None: - raise HTTPException(status_code=400, detail="Incorrect email or password") - - if not security.verify_password(form_data.password, user.hashed_password): # type: ignore - raise HTTPException(status_code=400, detail="Incorrect email or password") - - access_token, expire_at = security.create_access_token(user.id) - refresh_token, refresh_expire_at = security.create_refresh_token(user.id) - return { - "token_type": "bearer", - "access_token": access_token, - "expire_at": expire_at, - "refresh_token": refresh_token, - "refresh_expire_at": refresh_expire_at, - } - - -@router.post("/test-token", response_model=schemas.User) -async def test_token(current_user: User = Depends(deps.get_current_user)): - """ - Test access token for current user - """ - return current_user - - -@router.post("/refresh-token", response_model=schemas.Token) -async def refresh_token( - input: schemas.TokenRefresh, session: AsyncSession = Depends(deps.get_session) -): - """ - OAuth2 compatible token, get an access token for future requests using refresh token - """ - try: - payload = jwt.decode( - input.refresh_token, - config.settings.SECRET_KEY, - algorithms=[security.ALGORITHM], - ) - token_data = schemas.TokenPayload(**payload) - except (jwt.JWTError, ValidationError): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Could not validate credentials", - ) - if not token_data.refresh: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Could not validate credentials", - ) - result = await session.execute(select(User).where(User.id == token_data.sub)) - user: Optional[User] = result.scalars().first() - - if user is None: - raise HTTPException(status_code=404, detail="User not found") - - access_token, expire_at = security.create_access_token(user.id) - refresh_token, refresh_expire_at = security.create_refresh_token(user.id) - return { - "token_type": "bearer", - "access_token": access_token, - "expire_at": expire_at, - "refresh_token": refresh_token, - "refresh_expire_at": refresh_expire_at, - } diff --git a/{{cookiecutter.project_name}}/template_fastapi_users/app/api/endpoints/users.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/api/endpoints/users.py deleted file mode 100644 index 8b9bc09..0000000 --- a/{{cookiecutter.project_name}}/template_fastapi_users/app/api/endpoints/users.py +++ /dev/null @@ -1,41 +0,0 @@ -from fastapi import APIRouter, Depends -from sqlalchemy.ext.asyncio import AsyncSession - -from app import models, schemas -from app.api import deps -from app.core.security import get_password_hash - -router = APIRouter() - - -@router.put("/me", response_model=schemas.User) -async def update_user_me( - user_update: schemas.UserUpdate, - session: AsyncSession = Depends(deps.get_session), - current_user: models.User = Depends(deps.get_current_user), -): - """ - Update current user. - """ - if user_update.password is not None: - current_user.hashed_password = get_password_hash(user_update.password) # type: ignore - if user_update.full_name is not None: - current_user.full_name = user_update.full_name # type: ignore - if user_update.email is not None: - current_user.email = user_update.email # type: ignore - - session.add(current_user) - await session.commit() - await session.refresh(current_user) - - return current_user - - -@router.get("/me", response_model=schemas.User) -async def read_user_me( - current_user: models.User = Depends(deps.get_current_user), -): - """ - Get current user. - """ - return current_user diff --git a/{{cookiecutter.project_name}}/template_fastapi_users/app/core/config.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/core/config.py index 4f0a92e..bcef1de 100644 --- a/{{cookiecutter.project_name}}/template_fastapi_users/app/core/config.py +++ b/{{cookiecutter.project_name}}/template_fastapi_users/app/core/config.py @@ -20,14 +20,13 @@ """ from pathlib import Path -from typing import Union, Literal +from typing import Literal, Union import toml from pydantic import AnyHttpUrl, AnyUrl, BaseSettings, EmailStr, validator - PROJECT_DIR = Path(__file__).parent.parent.parent -pyproject_content = toml.load(f"{PROJECT_DIR}/pyproject.toml")["tool"]["poetry"] +PYPROJECT_CONTENT = toml.load(f"{PROJECT_DIR}/pyproject.toml")["tool"]["poetry"] class Settings(BaseSettings): @@ -35,14 +34,13 @@ class Settings(BaseSettings): SECRET_KEY: str ENVIRONMENT: Literal["DEV", "PYTEST", "STAGE", "PRODUCTION"] ACCESS_TOKEN_EXPIRE_MINUTES: int - SECURITY_BCRYPT_DEFAULT_ROUNDS: int = 12 REFRESH_TOKEN_EXPIRE_MINUTES: int BACKEND_CORS_ORIGINS: Union[str, list[AnyHttpUrl]] # PROJECT NAME, VERSION AND DESCRIPTION - PROJECT_NAME: str = pyproject_content["name"] - VERSION: str = pyproject_content["version"] - DESCRIPTION: str = pyproject_content["description"] + PROJECT_NAME: str = PYPROJECT_CONTENT["name"] + VERSION: str = PYPROJECT_CONTENT["version"] + DESCRIPTION: str = PYPROJECT_CONTENT["description"] # POSTGRESQL DEFAULT DATABASE DEFAULT_DATABASE_HOSTNAME: str diff --git a/{{cookiecutter.project_name}}/template_fastapi_users/app/core/security.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/core/security.py index de362f6..448aacf 100644 --- a/{{cookiecutter.project_name}}/template_fastapi_users/app/core/security.py +++ b/{{cookiecutter.project_name}}/template_fastapi_users/app/core/security.py @@ -1,56 +1,64 @@ """ -Black-box security shortcuts to generate JWT tokens and password hash/verify +You can have several authentication methods, e.g. a cookie +authentication for browser-based queries and a JWT token authentication for pure API queries. -`subject` in access/refresh func may be antyhing unique to User account, `id` etc. -""" - -from datetime import datetime, timedelta -from typing import Any, Union +In this template, token will be sent through Bearer header +{"Authorization": "Bearer xyz"} +using JWT tokens. -from jose import jwt -from passlib.context import CryptContext +There are more option to consider, refer to +https://fastapi-users.github.io/fastapi-users/configuration/authentication/ -from app.core import config +UserManager class is core fastapi users class with customizable attrs and methods +https://fastapi-users.github.io/fastapi-users/configuration/user-manager/ +""" -pwd_context = CryptContext( - schemes=["bcrypt"], - deprecated="auto", - bcrypt__rounds=config.settings.SECURITY_BCRYPT_DEFAULT_ROUNDS, -) +from typing import Optional -ALGORITHM = "HS256" +from fastapi import Request +from fastapi_users.authentication import ( + AuthenticationBackend, + BearerTransport, + JWTStrategy, +) +from fastapi_users.manager import BaseUserManager +from app import schemas +from app.core import config -def create_access_token(subject: Union[str, Any]) -> tuple[str, datetime]: - now = datetime.utcnow() - expire = now + timedelta(minutes=config.settings.ACCESS_TOKEN_EXPIRE_MINUTES) - to_encode = {"exp": expire, "sub": str(subject), "refresh": False} - encoded_jwt: str = jwt.encode( - to_encode, - config.settings.SECRET_KEY, - algorithm=ALGORITHM, +def get_jwt_strategy() -> JWTStrategy: + return JWTStrategy( + secret=config.settings.SECRET_KEY, + lifetime_seconds=config.settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60, ) - return encoded_jwt, expire -def create_refresh_token(subject: Union[str, Any]) -> tuple[str, datetime]: - now = datetime.utcnow() - expire = now + timedelta(minutes=config.settings.REFRESH_TOKEN_EXPIRE_MINUTES) +BEARER_TRANSPORT = BearerTransport(tokenUrl="auth/jwt/login") +AUTH_BACKEND = AuthenticationBackend( + name="jwt", + transport=BEARER_TRANSPORT, + get_strategy=get_jwt_strategy, +) - to_encode = {"exp": expire, "sub": str(subject), "refresh": True} - encoded_jwt: str = jwt.encode( - to_encode, - config.settings.SECRET_KEY, - algorithm=ALGORITHM, - ) - return encoded_jwt, expire +class UserManager(BaseUserManager[schemas.UserCreate, schemas.UserDB]): + user_db_model = schemas.UserDB + reset_password_token_secret = config.settings.SECRET_KEY + verification_token_secret = config.settings.SECRET_KEY -def verify_password(plain_password: str, hashed_password: str) -> bool: - return pwd_context.verify(plain_password, hashed_password) + async def on_after_register( + self, user: schemas.UserDB, request: Optional[Request] = None + ): + print(f"User {user.id} has registered.") + async def on_after_forgot_password( + self, user: schemas.UserDB, token: str, request: Optional[Request] = None + ): + print(f"User {user.id} has forgot their password. Reset token: {token}") -def get_password_hash(password: str) -> str: - return pwd_context.hash(password) + async def on_after_request_verify( + self, user: schemas.UserDB, token: str, request: Optional[Request] = None + ): + print(f"Verification requested for user {user.id}. Verification token: {token}") diff --git a/{{cookiecutter.project_name}}/template_fastapi_users/app/initial_data.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/initial_data.py index 1f36897..4577040 100644 --- a/{{cookiecutter.project_name}}/template_fastapi_users/app/initial_data.py +++ b/{{cookiecutter.project_name}}/template_fastapi_users/app/initial_data.py @@ -2,38 +2,43 @@ Put here any Python code that must be runned before application startup. It is included in `init.sh` script. -By defualt `main` create a superuser if not exists +By defualt `main` create a superuser if it does not exist. """ import asyncio from typing import Optional +from fastapi_users.password import get_password_hash +from fastapi_users_db_sqlalchemy import SQLAlchemyUserDatabase from sqlalchemy import select -from app.core import security, config -from app.models import User +from app import schemas +from app.core import config +from app.models import UserTable from app.session import async_session async def main() -> None: print("Start initial data") async with async_session() as session: - result = await session.execute( - select(User).where(User.email == config.settings.FIRST_SUPERUSER_EMAIL) + select(UserTable).where( + UserTable.email == config.settings.FIRST_SUPERUSER_EMAIL + ) ) - user: Optional[User] = result.scalars().first() + user: Optional[UserTable] = result.scalars().first() if user is None: - new_superuser = User( - email=config.settings.FIRST_SUPERUSER_EMAIL, - hashed_password=security.get_password_hash( - config.settings.FIRST_SUPERUSER_PASSWORD - ), - full_name=config.settings.FIRST_SUPERUSER_EMAIL, + await SQLAlchemyUserDatabase(schemas.UserDB, session, UserTable).create( + schemas.UserDB( + email=config.settings.FIRST_SUPERUSER_EMAIL, + is_superuser=True, + is_verified=True, + hashed_password=get_password_hash( + config.settings.FIRST_SUPERUSER_PASSWORD + ), + ) ) - session.add(new_superuser) - await session.commit() print("Superuser was created") else: print("Superuser already exists in database") diff --git a/{{cookiecutter.project_name}}/template_fastapi_users/app/main.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/main.py index 0892233..abac223 100644 --- a/{{cookiecutter.project_name}}/template_fastapi_users/app/main.py +++ b/{{cookiecutter.project_name}}/template_fastapi_users/app/main.py @@ -5,8 +5,8 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from app.api.api import api_router from app.core import config +from app.api.api import api_router app = FastAPI( title=config.settings.PROJECT_NAME, diff --git a/{{cookiecutter.project_name}}/template_fastapi_users/app/models.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/models.py index 2b7eb82..7316b29 100644 --- a/{{cookiecutter.project_name}}/template_fastapi_users/app/models.py +++ b/{{cookiecutter.project_name}}/template_fastapi_users/app/models.py @@ -6,15 +6,11 @@ from typing import Any, cast -from sqlalchemy import Column, Integer, String +from fastapi_users_db_sqlalchemy import SQLAlchemyBaseUserTable from sqlalchemy.orm.decl_api import declarative_base Base = cast(Any, declarative_base()) -class User(Base): - __tablename__ = "user" - id = Column(Integer, primary_key=True, index=True) - full_name = Column(String(254), nullable=True) - email = Column(String(254), unique=True, index=True, nullable=False) - hashed_password = Column(String(128), nullable=False) +class UserTable(Base, SQLAlchemyBaseUserTable): + pass diff --git a/{{cookiecutter.project_name}}/template_fastapi_users/app/schemas/__init__.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/schemas/__init__.py index e2ec5b2..6037db2 100644 --- a/{{cookiecutter.project_name}}/template_fastapi_users/app/schemas/__init__.py +++ b/{{cookiecutter.project_name}}/template_fastapi_users/app/schemas/__init__.py @@ -1,2 +1 @@ -from .token import Token, TokenPayload, TokenRefresh -from .user import User, UserCreate, UserUpdate +from .user import User, UserCreate, UserDB, UserUpdate diff --git a/{{cookiecutter.project_name}}/template_fastapi_users/app/schemas/token.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/schemas/token.py deleted file mode 100644 index 9c81e3e..0000000 --- a/{{cookiecutter.project_name}}/template_fastapi_users/app/schemas/token.py +++ /dev/null @@ -1,21 +0,0 @@ -from datetime import datetime -from typing import Optional - -from pydantic import BaseModel - - -class Token(BaseModel): - token_type: str - access_token: str - expire_at: datetime - refresh_token: str - refresh_expire_at: datetime - - -class TokenPayload(BaseModel): - sub: Optional[int] = None - refresh: Optional[bool] = None - - -class TokenRefresh(BaseModel): - refresh_token: str diff --git a/{{cookiecutter.project_name}}/template_fastapi_users/app/schemas/user.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/schemas/user.py index 9e24055..8d02126 100644 --- a/{{cookiecutter.project_name}}/template_fastapi_users/app/schemas/user.py +++ b/{{cookiecutter.project_name}}/template_fastapi_users/app/schemas/user.py @@ -1,26 +1,42 @@ +""" +All fields in schemas are defaults from FastAPI Users, repeated below for easier view +""" + +from fastapi_users import models +import uuid from typing import Optional -from pydantic import BaseModel, EmailStr +from pydantic import UUID4, EmailStr, Field -class BaseUser(BaseModel): - class Config: - orm_mode = True +class User(models.BaseUser): + id: UUID4 = Field(default_factory=uuid.uuid4) + email: EmailStr + is_active: bool = True + is_superuser: bool = False + is_verified: bool = False -class User(BaseUser): - id: int +class UserCreate(models.BaseUserCreate): email: EmailStr - full_name: str + password: str + is_active: Optional[bool] = True + is_superuser: Optional[bool] = False + is_verified: Optional[bool] = False -class UserUpdate(BaseUser): - email: Optional[EmailStr] +class UserUpdate(models.BaseUserUpdate): password: Optional[str] - full_name: Optional[str] + email: Optional[EmailStr] + is_active: Optional[bool] + is_superuser: Optional[bool] + is_verified: Optional[bool] -class UserCreate(BaseUser): +class UserDB(User, models.BaseUserDB): + id: UUID4 = Field(default_factory=uuid.uuid4) email: EmailStr - password: str - full_name: str + is_active: bool = True + is_superuser: bool = False + is_verified: bool = False + hashed_password: str diff --git a/{{cookiecutter.project_name}}/template_fastapi_users/app/tests/conftest.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/tests/conftest.py index 2707184..9e92e71 100644 --- a/{{cookiecutter.project_name}}/template_fastapi_users/app/tests/conftest.py +++ b/{{cookiecutter.project_name}}/template_fastapi_users/app/tests/conftest.py @@ -1,15 +1,20 @@ import asyncio -from typing import AsyncGenerator, Optional +from typing import AsyncGenerator import pytest from httpx import AsyncClient -from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession - -from app.core import security, config +from fastapi_users.password import get_password_hash +from app.core import config from app.main import app -from app.models import Base, User +from app.models import Base from app.session import async_engine, async_session +from app.tests import utils + +default_user_email = "geralt@wiedzmin.pl" +default_user_hash = get_password_hash("geralt") +superuser_user_email = "yennefer@wiedzmin.pl" +superuser_user_hash = get_password_hash("yennefer") @pytest.fixture(scope="session") @@ -21,7 +26,7 @@ def event_loop(): @pytest.fixture(scope="session") async def client(): - async with AsyncClient(app=app, base_url="http://test") as client: + async with AsyncClient(app=app, base_url="http://doesnt.matter") as client: yield client @@ -47,16 +52,11 @@ async def session(test_db_setup_sessionmaker) -> AsyncGenerator[AsyncSession, No @pytest.fixture async def default_user(session: AsyncSession): - result = await session.execute(select(User).where(User.email == "user@email.com")) - user: Optional[User] = result.scalars().first() - if user is None: - new_user = User( - email="user@email.com", - hashed_password=security.get_password_hash("password"), - full_name="fullname", - ) - session.add(new_user) - await session.commit() - await session.refresh(new_user) - return new_user - return user + return await utils.create_db_user(default_user_email, default_user_hash, session) + + +@pytest.fixture +async def superuser_user(session: AsyncSession): + return await utils.create_db_user( + superuser_user_email, superuser_user_hash, session, is_superuser=True + ) diff --git a/{{cookiecutter.project_name}}/template_fastapi_users/app/tests/test_auth.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/tests/test_auth.py index ccfaa18..d2bb8c3 100644 --- a/{{cookiecutter.project_name}}/template_fastapi_users/app/tests/test_auth.py +++ b/{{cookiecutter.project_name}}/template_fastapi_users/app/tests/test_auth.py @@ -1,32 +1,36 @@ +""" +Testing FastAPI Users makes no sense, its just an example to remove. +""" + import pytest from httpx import AsyncClient +from app.schemas import UserDB -from app.models import User # All test coroutines in file will be treated as marked (async allowed). pytestmark = pytest.mark.asyncio -async def test_login_endpoints(client: AsyncClient, default_user: User): +async def test_login_endpoints(client: AsyncClient, default_user: UserDB): # access-token endpoint - access_token = await client.post( - "/auth/access-token", + access_token_res = await client.post( + "/auth/jwt/login", data={ - "username": "user@email.com", - "password": "password", + "username": "geralt@wiedzmin.pl", + "password": "geralt", }, headers={"Content-Type": "application/x-www-form-urlencoded"}, ) - assert access_token.status_code == 200 - token = access_token.json() + assert access_token_res.status_code == 200 + token = access_token_res.json() access_token = token["access_token"] refresh_token = token["refresh_token"] # test-token endpoint test_token = await client.post( - "/auth/test-token", headers={"Authorization": f"Bearer {access_token}"} + "/users/me", headers={"Authorization": f"Bearer {access_token}"} ) assert test_token.status_code == 200 response_user = test_token.json() diff --git a/{{cookiecutter.project_name}}/template_fastapi_users/app/tests/utils.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/tests/utils.py index 7c14c07..0f9fd72 100644 --- a/{{cookiecutter.project_name}}/template_fastapi_users/app/tests/utils.py +++ b/{{cookiecutter.project_name}}/template_fastapi_users/app/tests/utils.py @@ -1,5 +1,14 @@ import random import string +from fastapi_users import password +from pydantic.networks import EmailStr +from sqlalchemy.ext.asyncio import AsyncSession + +from fastapi_users_db_sqlalchemy import SQLAlchemyUserDatabase + +from app import schemas +from app.core import config +from app.models import UserTable def random_lower_string(length: int = 32) -> str: @@ -8,3 +17,22 @@ def random_lower_string(length: int = 32) -> str: def random_email(length: int = 10) -> str: return f"{random_lower_string(length)}@{random_lower_string(length)}.com" + + +async def create_db_user( + email: str, + hashed_password: str, + session: AsyncSession, + is_superuser: bool = False, + is_verified: bool = True, +) -> schemas.UserDB: + + new_user = await SQLAlchemyUserDatabase(schemas.UserDB, session, UserTable).create( + schemas.UserDB( + email=EmailStr(email), + is_superuser=is_superuser, + is_verified=is_verified, + hashed_password=hashed_password, + ) + ) + return new_user diff --git a/{{cookiecutter.project_name}}/template_fastapi_users/poetry.lock b/{{cookiecutter.project_name}}/template_fastapi_users/poetry.lock index 2062419..cb9bf80 100644 --- a/{{cookiecutter.project_name}}/template_fastapi_users/poetry.lock +++ b/{{cookiecutter.project_name}}/template_fastapi_users/poetry.lock @@ -7,8 +7,6 @@ optional = false python-versions = ">=3.6" [package.dependencies] -importlib-metadata = {version = "*", markers = "python_version < \"3.9\""} -importlib-resources = {version = "*", markers = "python_version < \"3.9\""} Mako = "*" SQLAlchemy = ">=1.3.0" @@ -26,7 +24,6 @@ python-versions = ">=3.6.2" [package.dependencies] idna = ">=2.8" sniffio = ">=1.1" -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] doc = ["sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] @@ -37,13 +34,10 @@ trio = ["trio (>=0.16)"] name = "asgiref" version = "3.4.1" description = "ASGI specs, helper code, and adapters" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" -[package.dependencies] -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} - [package.extras] tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] @@ -55,9 +49,6 @@ category = "main" optional = false python-versions = ">=3.6.0" -[package.dependencies] -typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\""} - [package.extras] dev = ["Cython (>=0.29.24,<0.30.0)", "pytest (>=6.0)", "Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "pycodestyle (>=2.7.0,<2.8.0)", "flake8 (>=3.9.2,<3.10.0)", "uvloop (>=0.15.3)"] docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)"] @@ -126,7 +117,6 @@ mypy-extensions = ">=0.4.3" pathspec = ">=0.9.0,<1" platformdirs = ">=2" tomli = ">=0.2.6,<2.0.0" -typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} typing-extensions = [ {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}, {version = "!=3.10.0.1", markers = "python_version >= \"3.10\""}, @@ -143,7 +133,7 @@ uvloop = ["uvloop (>=0.15.2)"] name = "certifi" version = "2021.10.8" description = "Python package for providing Mozilla's CA Bundle." -category = "main" +category = "dev" optional = false python-versions = "*" @@ -160,9 +150,9 @@ pycparser = "*" [[package]] name = "charset-normalizer" -version = "2.0.9" +version = "2.0.10" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" +category = "dev" optional = false python-versions = ">=3.5.0" @@ -173,19 +163,18 @@ unicode_backport = ["unicodedata2"] name = "click" version = "8.0.3" description = "Composable command line interface toolkit" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "colorama" version = "0.4.4" description = "Cross-platform colored terminal text." -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -200,25 +189,6 @@ python-versions = ">=3.6" [package.extras] toml = ["tomli"] -[[package]] -name = "cryptography" -version = "36.0.1" -description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -cffi = ">=1.12" - -[package.extras] -docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] -docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] -pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] -sdist = ["setuptools_rust (>=0.11.4)"] -ssh = ["bcrypt (>=3.1.5)"] -test = ["pytest (>=6.2.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] - [[package]] name = "dnspython" version = "2.1.0" @@ -234,21 +204,6 @@ idna = ["idna (>=2.1)"] curio = ["curio (>=1.2)", "sniffio (>=1.1)"] trio = ["trio (>=0.14.0)", "sniffio (>=1.1)"] -[[package]] -name = "ecdsa" -version = "0.17.0" -description = "ECDSA cryptographic signature library (pure python)" -category = "main" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" - -[package.dependencies] -six = ">=1.9.0" - -[package.extras] -gmpy = ["gmpy"] -gmpy2 = ["gmpy2"] - [[package]] name = "email-validator" version = "1.1.3" @@ -263,7 +218,7 @@ idna = ">=2.0.0" [[package]] name = "fastapi" -version = "0.71.0" +version = "0.70.1" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" category = "main" optional = false @@ -271,14 +226,52 @@ python-versions = ">=3.6.1" [package.dependencies] pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" -starlette = "0.17.1" +starlette = "0.16.0" [package.extras] all = ["requests (>=2.24.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "itsdangerous (>=1.1.0,<3.0.0)", "pyyaml (>=5.3.1,<6.0.0)", "ujson (>=4.0.1,<5.0.0)", "orjson (>=3.2.1,<4.0.0)", "email_validator (>=1.1.1,<2.0.0)", "uvicorn[standard] (>=0.12.0,<0.16.0)"] dev = ["python-jose[cryptography] (>=3.3.0,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "uvicorn[standard] (>=0.12.0,<0.16.0)"] -doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "typer-cli (>=0.0.12,<0.0.13)", "pyyaml (>=5.3.1,<6.0.0)"] +doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=7.1.9,<8.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "typer-cli (>=0.0.12,<0.0.13)", "pyyaml (>=5.3.1,<6.0.0)"] test = ["pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "mypy (==0.910)", "flake8 (>=3.8.3,<4.0.0)", "black (==21.9b0)", "isort (>=5.0.6,<6.0.0)", "requests (>=2.24.0,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "email_validator (>=1.1.1,<2.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "peewee (>=3.13.3,<4.0.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "orjson (>=3.2.1,<4.0.0)", "ujson (>=4.0.1,<5.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "flask (>=1.1.2,<3.0.0)", "anyio[trio] (>=3.2.1,<4.0.0)", "types-ujson (==0.1.1)", "types-orjson (==3.6.0)", "types-dataclasses (==0.1.7)"] +[[package]] +name = "fastapi-users" +version = "9.2.0" +description = "Ready-to-use and customizable users management for FastAPI." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +email-validator = ">=1.1.0,<1.2" +fastapi = ">=0.65.2,<0.71.0" +fastapi-users-db-sqlalchemy = {version = ">=2.0.0", optional = true, markers = "extra == \"sqlalchemy2\""} +makefun = ">=1.9.2,<1.13" +passlib = {version = "1.7.4", extras = ["bcrypt"]} +pyjwt = "2.3.0" +python-multipart = "0.0.5" + +[package.extras] +mongodb = ["fastapi-users-db-mongodb (>=1.1.0)"] +oauth = ["httpx-oauth (>=0.4,<0.5)"] +ormar = ["fastapi-users-db-ormar (>=1.0.0)"] +redis = ["aioredis (>=2.0.1,<2.1.0)", "hiredis (>=2.0.0,<2.1.0)"] +sqlalchemy = ["fastapi-users-db-sqlalchemy (>=1.1.0,<2.0.0)"] +sqlalchemy2 = ["fastapi-users-db-sqlalchemy (>=2.0.0)"] +tortoise-orm = ["fastapi-users-db-tortoise (>=1.1.0)"] + +[[package]] +name = "fastapi-users-db-sqlalchemy" +version = "2.0.4" +description = "FastAPI Users database adapter for SQLAlchemy." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +fastapi-users = ">=9.1.0" +sqlalchemy = ">=1.4" + [[package]] name = "flake8" version = "4.0.1" @@ -288,7 +281,6 @@ optional = false python-versions = ">=3.6" [package.dependencies] -importlib-metadata = {version = "<4.3", markers = "python_version < \"3.8\""} mccabe = ">=0.6.0,<0.7.0" pycodestyle = ">=2.8.0,<2.9.0" pyflakes = ">=2.4.0,<2.5.0" @@ -308,7 +300,7 @@ docs = ["sphinx"] name = "h11" version = "0.12.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" @@ -357,37 +349,6 @@ category = "main" optional = false python-versions = ">=3.5" -[[package]] -name = "importlib-metadata" -version = "4.2.0" -description = "Read metadata from Python packages" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} -zipp = ">=0.5" - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] - -[[package]] -name = "importlib-resources" -version = "5.4.0" -description = "Read resources from Python packages" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-black (>=0.3.7)", "pytest-mypy"] - [[package]] name = "iniconfig" version = "1.1.1" @@ -410,6 +371,14 @@ requirements_deprecated_finder = ["pipreqs", "pip-api"] colors = ["colorama (>=0.4.3,<0.5.0)"] plugins = ["setuptools"] +[[package]] +name = "makefun" +version = "1.12.1" +description = "Small library to dynamically create python functions." +category = "main" +optional = false +python-versions = "*" + [[package]] name = "mako" version = "1.1.6" @@ -505,9 +474,6 @@ category = "dev" optional = false python-versions = ">=3.6" -[package.dependencies] -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} - [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] @@ -520,14 +486,6 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -[[package]] -name = "pyasn1" -version = "0.4.8" -description = "ASN.1 types and codecs" -category = "main" -optional = false -python-versions = "*" - [[package]] name = "pycodestyle" version = "2.8.0" @@ -568,6 +526,20 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[[package]] +name = "pyjwt" +version = "2.3.0" +description = "JSON Web Token implementation in Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +crypto = ["cryptography (>=3.3.1)"] +dev = ["sphinx", "sphinx-rtd-theme", "zope.interface", "cryptography (>=3.3.1)", "pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)", "mypy", "pre-commit"] +docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] +tests = ["pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)"] + [[package]] name = "pyparsing" version = "3.0.6" @@ -591,7 +563,6 @@ python-versions = ">=3.6" atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" @@ -626,25 +597,6 @@ python-versions = ">=3.5" [package.extras] cli = ["click (>=5.0)"] -[[package]] -name = "python-jose" -version = "3.3.0" -description = "JOSE implementation in Python" -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"cryptography\""} -ecdsa = "!=0.15" -pyasn1 = "*" -rsa = "*" - -[package.extras] -cryptography = ["cryptography (>=3.4.0)"] -pycrypto = ["pycrypto (>=2.6.0,<2.7.0)", "pyasn1"] -pycryptodome = ["pycryptodome (>=3.3.1,<4.0.0)", "pyasn1"] - [[package]] name = "python-multipart" version = "0.0.5" @@ -660,7 +612,7 @@ six = ">=1.4.0" name = "requests" version = "2.27.1" description = "Python HTTP for Humans." -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" @@ -688,17 +640,6 @@ idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} [package.extras] idna2008 = ["idna"] -[[package]] -name = "rsa" -version = "4.8" -description = "Pure-Python RSA implementation" -category = "main" -optional = false -python-versions = ">=3.6,<4" - -[package.dependencies] -pyasn1 = ">=0.1.3" - [[package]] name = "six" version = "1.16.0" @@ -725,7 +666,6 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" [package.dependencies] greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [package.extras] aiomysql = ["greenlet (!=0.4.17)", "aiomysql"] @@ -761,7 +701,7 @@ typing-extensions = ">=3.7.4" [[package]] name = "starlette" -version = "0.17.1" +version = "0.16.0" description = "The little ASGI library that shines." category = "main" optional = false @@ -769,10 +709,9 @@ python-versions = ">=3.6" [package.dependencies] anyio = ">=3.0.0,<4" -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] -full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests"] +full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests", "graphene"] [[package]] name = "toml" @@ -790,14 +729,6 @@ category = "dev" optional = false python-versions = ">=3.6" -[[package]] -name = "typed-ast" -version = "1.5.1" -description = "a fork of Python 2 and 3 ast modules with type comment support" -category = "dev" -optional = false -python-versions = ">=3.6" - [[package]] name = "typing-extensions" version = "4.0.1" @@ -808,9 +739,9 @@ python-versions = ">=3.6" [[package]] name = "urllib3" -version = "1.26.7" +version = "1.26.8" description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" @@ -823,7 +754,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] name = "uvicorn" version = "0.16.0" description = "The lightning-fast ASGI server." -category = "main" +category = "dev" optional = false python-versions = "*" @@ -831,27 +762,14 @@ python-versions = "*" asgiref = ">=3.4.0" click = ">=7.0" h11 = ">=0.8" -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] standard = ["httptools (>=0.2.0,<0.4.0)", "watchgod (>=0.6)", "python-dotenv (>=0.13)", "PyYAML (>=5.1)", "websockets (>=9.1)", "websockets (>=10.0)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "colorama (>=0.4)"] -[[package]] -name = "zipp" -version = "3.7.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] - [metadata] lock-version = "1.1" -python-versions = "^3.7" -content-hash = "a00e6962598c9e04b8f260f70bb66492d5a520b72e65d4d1aa227a0a2a93f5cb" +python-versions = "^3.9" +content-hash = "0f47109307eb4b11a2898d492ccca09b9d5562dff15bfb9f7b71ef0366da122f" [metadata.files] alembic = [ @@ -975,8 +893,8 @@ cffi = [ {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"}, ] charset-normalizer = [ - {file = "charset-normalizer-2.0.9.tar.gz", hash = "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c"}, - {file = "charset_normalizer-2.0.9-py3-none-any.whl", hash = "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721"}, + {file = "charset-normalizer-2.0.10.tar.gz", hash = "sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd"}, + {file = "charset_normalizer-2.0.10-py3-none-any.whl", hash = "sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455"}, ] click = [ {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, @@ -1035,43 +953,25 @@ coverage = [ {file = "coverage-6.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de"}, {file = "coverage-6.2.tar.gz", hash = "sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8"}, ] -cryptography = [ - {file = "cryptography-36.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:73bc2d3f2444bcfeac67dd130ff2ea598ea5f20b40e36d19821b4df8c9c5037b"}, - {file = "cryptography-36.0.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:2d87cdcb378d3cfed944dac30596da1968f88fb96d7fc34fdae30a99054b2e31"}, - {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74d6c7e80609c0f4c2434b97b80c7f8fdfaa072ca4baab7e239a15d6d70ed73a"}, - {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:6c0c021f35b421ebf5976abf2daacc47e235f8b6082d3396a2fe3ccd537ab173"}, - {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d59a9d55027a8b88fd9fd2826c4392bd487d74bf628bb9d39beecc62a644c12"}, - {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a817b961b46894c5ca8a66b599c745b9a3d9f822725221f0e0fe49dc043a3a3"}, - {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:94ae132f0e40fe48f310bba63f477f14a43116f05ddb69d6fa31e93f05848ae2"}, - {file = "cryptography-36.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7be0eec337359c155df191d6ae00a5e8bbb63933883f4f5dffc439dac5348c3f"}, - {file = "cryptography-36.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e0344c14c9cb89e76eb6a060e67980c9e35b3f36691e15e1b7a9e58a0a6c6dc3"}, - {file = "cryptography-36.0.1-cp36-abi3-win32.whl", hash = "sha256:4caa4b893d8fad33cf1964d3e51842cd78ba87401ab1d2e44556826df849a8ca"}, - {file = "cryptography-36.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:391432971a66cfaf94b21c24ab465a4cc3e8bf4a939c1ca5c3e3a6e0abebdbcf"}, - {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bb5829d027ff82aa872d76158919045a7c1e91fbf241aec32cb07956e9ebd3c9"}, - {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebc15b1c22e55c4d5566e3ca4db8689470a0ca2babef8e3a9ee057a8b82ce4b1"}, - {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:596f3cd67e1b950bc372c33f1a28a0692080625592ea6392987dba7f09f17a94"}, - {file = "cryptography-36.0.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:30ee1eb3ebe1644d1c3f183d115a8c04e4e603ed6ce8e394ed39eea4a98469ac"}, - {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec63da4e7e4a5f924b90af42eddf20b698a70e58d86a72d943857c4c6045b3ee"}, - {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca238ceb7ba0bdf6ce88c1b74a87bffcee5afbfa1e41e173b1ceb095b39add46"}, - {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:ca28641954f767f9822c24e927ad894d45d5a1e501767599647259cbf030b903"}, - {file = "cryptography-36.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:39bdf8e70eee6b1c7b289ec6e5d84d49a6bfa11f8b8646b5b3dfe41219153316"}, - {file = "cryptography-36.0.1.tar.gz", hash = "sha256:53e5c1dc3d7a953de055d77bef2ff607ceef7a2aac0353b5d630ab67f7423638"}, -] dnspython = [ {file = "dnspython-2.1.0-py3-none-any.whl", hash = "sha256:95d12f6ef0317118d2a1a6fc49aac65ffec7eb8087474158f42f26a639135216"}, {file = "dnspython-2.1.0.zip", hash = "sha256:e4a87f0b573201a0f3727fa18a516b055fd1107e0e5477cded4a2de497df1dd4"}, ] -ecdsa = [ - {file = "ecdsa-0.17.0-py2.py3-none-any.whl", hash = "sha256:5cf31d5b33743abe0dfc28999036c849a69d548f994b535e527ee3cb7f3ef676"}, - {file = "ecdsa-0.17.0.tar.gz", hash = "sha256:b9f500bb439e4153d0330610f5d26baaf18d17b8ced1bc54410d189385ea68aa"}, -] email-validator = [ {file = "email_validator-1.1.3-py2.py3-none-any.whl", hash = "sha256:5675c8ceb7106a37e40e2698a57c056756bf3f272cfa8682a4f87ebd95d8440b"}, {file = "email_validator-1.1.3.tar.gz", hash = "sha256:aa237a65f6f4da067119b7df3f13e89c25c051327b2b5b66dc075f33d62480d7"}, ] fastapi = [ - {file = "fastapi-0.71.0-py3-none-any.whl", hash = "sha256:a78eca6b084de9667f2d5f37e2ae297270e5a119cd01c2f04815795da92fc87f"}, - {file = "fastapi-0.71.0.tar.gz", hash = "sha256:2b5ac0ae89c80b40d1dd4b2ea0bb1f78d7c4affd3644d080bf050f084759fff2"}, + {file = "fastapi-0.70.1-py3-none-any.whl", hash = "sha256:5367226c7bcd7bfb2e17edaf225fd9a983095b1372281e9a3eb661336fb93748"}, + {file = "fastapi-0.70.1.tar.gz", hash = "sha256:21d03979b5336375c66fa5d1f3126c6beca650d5d2166fbb78345a30d33c8d06"}, +] +fastapi-users = [ + {file = "fastapi-users-9.2.0.tar.gz", hash = "sha256:49d452ce785134aea64428be3747785d1cae1cfd1fd546eefc248088d806ff06"}, + {file = "fastapi_users-9.2.0-py3-none-any.whl", hash = "sha256:67d0c7fdcf5bbaf46849b79c12fd3188627746753389f732b4c459b279944253"}, +] +fastapi-users-db-sqlalchemy = [ + {file = "fastapi-users-db-sqlalchemy-2.0.4.tar.gz", hash = "sha256:93bd5ba48f7a7e9fe43b62a48ad7c5ec5993c8230c6dafb61c341ceee811ebf1"}, + {file = "fastapi_users_db_sqlalchemy-2.0.4-py3-none-any.whl", hash = "sha256:4d246157c18335353111bfdf47dca081a6663a337e2f7e9670eb9606730399bb"}, ] flake8 = [ {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, @@ -1145,14 +1045,6 @@ idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, ] -importlib-metadata = [ - {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, - {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, -] -importlib-resources = [ - {file = "importlib_resources-5.4.0-py3-none-any.whl", hash = "sha256:33a95faed5fc19b4bc16b29a6eeae248a3fe69dd55d4d229d2b480e23eeaad45"}, - {file = "importlib_resources-5.4.0.tar.gz", hash = "sha256:d756e2f85dd4de2ba89be0b21dba2a3bbec2e871a42a3a16719258a11f87506b"}, -] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, @@ -1161,6 +1053,10 @@ isort = [ {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, ] +makefun = [ + {file = "makefun-1.12.1-py2.py3-none-any.whl", hash = "sha256:639ac01a68ddd76ab156ca435bcd89dab25c717fb09af23f1563dc26362b4dcc"}, + {file = "makefun-1.12.1.tar.gz", hash = "sha256:4d0e90ca3fdbdeb6a4a0891e2da7d4b8e80386e19e6db91ce29b8aa5c876ecfe"}, +] mako = [ {file = "Mako-1.1.6-py2.py3-none-any.whl", hash = "sha256:afaf8e515d075b22fad7d7b8b30e4a1c90624ff2f3733a06ec125f5a5f043a57"}, {file = "Mako-1.1.6.tar.gz", hash = "sha256:4e9e345a41924a954251b95b4b28e14a301145b544901332e658907a7464b6b2"}, @@ -1253,21 +1149,6 @@ py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] -pyasn1 = [ - {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, - {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"}, - {file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"}, - {file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"}, - {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, - {file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"}, - {file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"}, - {file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"}, - {file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"}, - {file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"}, - {file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"}, - {file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"}, - {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, -] pycodestyle = [ {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, @@ -1317,6 +1198,10 @@ pyflakes = [ {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, ] +pyjwt = [ + {file = "PyJWT-2.3.0-py3-none-any.whl", hash = "sha256:e0c4bb8d9f0af0c7f5b1ec4c5036309617d03d56932877f2f7a0beeb5318322f"}, + {file = "PyJWT-2.3.0.tar.gz", hash = "sha256:b888b4d56f06f6dcd777210c334e69c737be74755d3e5e9ee3fe67dc18a0ee41"}, +] pyparsing = [ {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"}, @@ -1333,10 +1218,6 @@ python-dotenv = [ {file = "python-dotenv-0.19.2.tar.gz", hash = "sha256:a5de49a31e953b45ff2d2fd434bbc2670e8db5273606c1e737cc6b93eff3655f"}, {file = "python_dotenv-0.19.2-py2.py3-none-any.whl", hash = "sha256:32b2bdc1873fd3a3c346da1c6db83d0053c3c62f28f1f38516070c4c8971b1d3"}, ] -python-jose = [ - {file = "python-jose-3.3.0.tar.gz", hash = "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a"}, - {file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"}, -] python-multipart = [ {file = "python-multipart-0.0.5.tar.gz", hash = "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"}, ] @@ -1348,10 +1229,6 @@ rfc3986 = [ {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, ] -rsa = [ - {file = "rsa-4.8-py3-none-any.whl", hash = "sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb"}, - {file = "rsa-4.8.tar.gz", hash = "sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17"}, -] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -1403,8 +1280,8 @@ sqlalchemy2-stubs = [ {file = "sqlalchemy2_stubs-0.0.2a19-py3-none-any.whl", hash = "sha256:aac7dca77a2c49e5f0934976421d5e25ae4dc5e27db48c01e055f81caa1e3ead"}, ] starlette = [ - {file = "starlette-0.17.1-py3-none-any.whl", hash = "sha256:26a18cbda5e6b651c964c12c88b36d9898481cd428ed6e063f5f29c418f73050"}, - {file = "starlette-0.17.1.tar.gz", hash = "sha256:57eab3cc975a28af62f6faec94d355a410634940f10b30d68d31cb5ec1b44ae8"}, + {file = "starlette-0.16.0-py3-none-any.whl", hash = "sha256:38eb24bf705a2c317e15868e384c1b8a12ca396e5a3c3a003db7e667c43f939f"}, + {file = "starlette-0.16.0.tar.gz", hash = "sha256:e1904b5d0007aee24bdd3c43994be9b3b729f4f58e740200de1d623f8c3a8870"}, ] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, @@ -1414,40 +1291,15 @@ tomli = [ {file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"}, {file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"}, ] -typed-ast = [ - {file = "typed_ast-1.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d8314c92414ce7481eee7ad42b353943679cf6f30237b5ecbf7d835519e1212"}, - {file = "typed_ast-1.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b53ae5de5500529c76225d18eeb060efbcec90ad5e030713fe8dab0fb4531631"}, - {file = "typed_ast-1.5.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:24058827d8f5d633f97223f5148a7d22628099a3d2efe06654ce872f46f07cdb"}, - {file = "typed_ast-1.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:a6d495c1ef572519a7bac9534dbf6d94c40e5b6a608ef41136133377bba4aa08"}, - {file = "typed_ast-1.5.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:de4ecae89c7d8b56169473e08f6bfd2df7f95015591f43126e4ea7865928677e"}, - {file = "typed_ast-1.5.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:256115a5bc7ea9e665c6314ed6671ee2c08ca380f9d5f130bd4d2c1f5848d695"}, - {file = "typed_ast-1.5.1-cp36-cp36m-win_amd64.whl", hash = "sha256:7c42707ab981b6cf4b73490c16e9d17fcd5227039720ca14abe415d39a173a30"}, - {file = "typed_ast-1.5.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:71dcda943a471d826ea930dd449ac7e76db7be778fcd722deb63642bab32ea3f"}, - {file = "typed_ast-1.5.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4f30a2bcd8e68adbb791ce1567fdb897357506f7ea6716f6bbdd3053ac4d9471"}, - {file = "typed_ast-1.5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ca9e8300d8ba0b66d140820cf463438c8e7b4cdc6fd710c059bfcfb1531d03fb"}, - {file = "typed_ast-1.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9caaf2b440efb39ecbc45e2fabde809cbe56272719131a6318fd9bf08b58e2cb"}, - {file = "typed_ast-1.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c9bcad65d66d594bffab8575f39420fe0ee96f66e23c4d927ebb4e24354ec1af"}, - {file = "typed_ast-1.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:591bc04e507595887160ed7aa8d6785867fb86c5793911be79ccede61ae96f4d"}, - {file = "typed_ast-1.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:a80d84f535642420dd17e16ae25bb46c7f4c16ee231105e7f3eb43976a89670a"}, - {file = "typed_ast-1.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:38cf5c642fa808300bae1281460d4f9b7617cf864d4e383054a5ef336e344d32"}, - {file = "typed_ast-1.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b6ab14c56bc9c7e3c30228a0a0b54b915b1579613f6e463ba6f4eb1382e7fd4"}, - {file = "typed_ast-1.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a2b8d7007f6280e36fa42652df47087ac7b0a7d7f09f9468f07792ba646aac2d"}, - {file = "typed_ast-1.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:b6d17f37f6edd879141e64a5db17b67488cfeffeedad8c5cec0392305e9bc775"}, - {file = "typed_ast-1.5.1.tar.gz", hash = "sha256:484137cab8ecf47e137260daa20bafbba5f4e3ec7fda1c1e69ab299b75fa81c5"}, -] typing-extensions = [ {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, ] urllib3 = [ - {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, - {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"}, + {file = "urllib3-1.26.8-py2.py3-none-any.whl", hash = "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"}, + {file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"}, ] uvicorn = [ {file = "uvicorn-0.16.0-py3-none-any.whl", hash = "sha256:d8c839231f270adaa6d338d525e2652a0b4a5f4c2430b5c4ef6ae4d11776b0d2"}, {file = "uvicorn-0.16.0.tar.gz", hash = "sha256:eacb66afa65e0648fcbce5e746b135d09722231ffffc61883d4fac2b62fbea8d"}, ] -zipp = [ - {file = "zipp-3.7.0-py3-none-any.whl", hash = "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"}, - {file = "zipp-3.7.0.tar.gz", hash = "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d"}, -] diff --git a/{{cookiecutter.project_name}}/template_fastapi_users/pyproject.toml b/{{cookiecutter.project_name}}/template_fastapi_users/pyproject.toml index c084bba..e8cd804 100644 --- a/{{cookiecutter.project_name}}/template_fastapi_users/pyproject.toml +++ b/{{cookiecutter.project_name}}/template_fastapi_users/pyproject.toml @@ -6,23 +6,20 @@ authors = ["admin "] [tool.poetry.dependencies] python = "^3.9" -fastapi = "^0.70.1,<0.71.0" SQLAlchemy = {extras = ["asyncio"], version = "^1.4.29"} -uvicorn = "^0.16.0" python-dotenv = "^0.19.2" -requests = "^2.27.1" -python-jose = {extras = ["cryptography"], version = "^3.3.0"} -passlib = {extras = ["bcrypt"], version = "^1.7.4"} pydantic = {extras = ["email"], version = "^1.9.0"} alembic = "^1.7.5" -python-multipart = "^0.0.5" asyncpg = "^0.25.0" +fastapi-users = {extras = ["sqlalchemy2"], version = "^9.2.0"} [tool.poetry.dev-dependencies] black = {version = "^21.12b0", allow-prereleases = true} autoflake = "^1.4" +uvicorn = "^0.16.0" flake8 = "^4.0.1" isort = "^5.10.1" +requests = "^2.27.1" coverage = "^6.2" pytest = "^6.2.5" pytest-asyncio = "^0.16.0" diff --git a/{{cookiecutter.project_name}}/template_fastapi_users/requirements-dev.txt b/{{cookiecutter.project_name}}/template_fastapi_users/requirements-dev.txt index e983a91..6662e93 100644 --- a/{{cookiecutter.project_name}}/template_fastapi_users/requirements-dev.txt +++ b/{{cookiecutter.project_name}}/template_fastapi_users/requirements-dev.txt @@ -1,69 +1,61 @@ alembic==1.7.5; python_version >= "3.6" -anyio==3.4.0; python_full_version >= "3.6.2" and python_version >= "3.6" +anyio==3.4.0; python_full_version >= "3.6.2" and python_version >= "3.7" asgiref==3.4.1; python_version >= "3.6" -asyncpg==0.24.0; python_full_version >= "3.6.0" +asyncpg==0.25.0; python_full_version >= "3.6.0" atomicwrites==1.4.0; python_version >= "3.6" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.6" and python_full_version >= "3.4.0" -attrs==21.2.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" +attrs==21.4.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" autoflake==1.4 -bcrypt==3.2.0; python_version >= "3.6" -black==21.11b1; python_full_version >= "3.6.2" and python_full_version < "4.0.0" +bcrypt==3.2.0; python_version >= "3.7" +black==21.12b0; python_full_version >= "3.6.2" certifi==2021.10.8; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6" -cffi==1.15.0 -charset-normalizer==2.0.8; python_full_version >= "3.6.0" and python_version >= "3.6" -click==8.0.3; python_full_version >= "3.6.2" and python_full_version < "4.0.0" and python_version >= "3.6" +cffi==1.15.0; python_version >= "3.7" +charset-normalizer==2.0.10; python_full_version >= "3.6.0" and python_version >= "3.6" +click==8.0.3; python_version >= "3.6" and python_full_version >= "3.6.2" colorama==0.4.4; python_version >= "3.6" and python_full_version < "3.0.0" and sys_platform == "win32" and platform_system == "Windows" or sys_platform == "win32" and python_version >= "3.6" and python_full_version >= "3.5.0" and platform_system == "Windows" -coverage==5.5; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0" and python_version < "4") -cryptography==36.0.0; python_version >= "3.6" +coverage==6.2; python_version >= "3.6" dnspython==2.1.0; python_full_version >= "3.6.1" and python_version >= "3.6" -ecdsa==0.17.0; python_version >= "2.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" -email-validator==1.1.3; python_full_version >= "3.6.1" -fastapi==0.68.2; python_full_version >= "3.6.1" -flake8==3.9.2; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") +email-validator==1.1.3; python_full_version >= "3.6.1" and (python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7") and python_version >= "3.7" +fastapi-users-db-sqlalchemy==2.0.4; python_version >= "3.7" +fastapi-users==9.2.0; python_version >= "3.7" +fastapi==0.70.1; python_full_version >= "3.6.1" and python_version >= "3.7" +flake8==4.0.1; python_version >= "3.6" greenlet==1.1.2; python_version >= "3" and python_full_version < "3.0.0" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32") and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0") and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6") or python_version >= "3" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32") and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0") and python_full_version >= "3.5.0" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6") h11==0.12.0; python_version >= "3.6" -httpcore==0.13.7; python_version >= "3.6" -httpx==0.20.0; python_version >= "3.6" +httpcore==0.14.4; python_version >= "3.6" +httpx==0.21.3; python_version >= "3.6" idna==3.3 -importlib-metadata==4.8.2; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.8" or python_full_version >= "3.6.0" and python_version < "3.8" and python_version >= "3.6" -importlib-resources==5.4.0; python_version < "3.9" and python_version >= "3.6" iniconfig==1.1.1; python_version >= "3.6" isort==5.10.1; python_full_version >= "3.6.1" and python_version < "4.0" +makefun==1.12.1; python_version >= "3.7" mako==1.1.6; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" markupsafe==2.0.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" -mccabe==0.6.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" -mypy-extensions==0.4.3; python_full_version >= "3.6.2" and python_full_version < "4.0.0" and python_version >= "3.5" -mypy==0.910; python_version >= "3.5" +mccabe==0.6.1; python_version >= "3.6" +mypy-extensions==0.4.3; python_full_version >= "3.6.2" packaging==21.3; python_version >= "3.6" -passlib==1.7.4 -pathspec==0.9.0; python_full_version >= "3.6.2" and python_full_version < "4.0.0" -platformdirs==2.4.0; python_full_version >= "3.6.2" and python_full_version < "4.0.0" and python_version >= "3.6" +passlib==1.7.4; python_version >= "3.7" +pathspec==0.9.0; python_full_version >= "3.6.2" +platformdirs==2.4.1; python_version >= "3.7" and python_full_version >= "3.6.2" pluggy==1.0.0; python_version >= "3.6" py==1.11.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" -pyasn1==0.4.8; python_version >= "3.6" and python_version < "4" -pycodestyle==2.7.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" -pycparser==2.21; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.4.0" -pydantic==1.8.2; python_full_version >= "3.6.1" -pyflakes==2.3.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" +pycodestyle==2.8.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" +pycparser==2.21; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version >= "3.4.0" +pydantic==1.9.0; python_full_version >= "3.6.1" +pyflakes==2.4.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" +pyjwt==2.3.0; python_version >= "3.7" pyparsing==3.0.6; python_version >= "3.6" pytest-asyncio==0.16.0; python_version >= "3.6" pytest==6.2.5; python_version >= "3.6" python-dotenv==0.19.2; python_version >= "3.5" -python-jose==3.3.0 python-multipart==0.0.5 -regex==2021.11.10; python_full_version >= "3.6.2" and python_full_version < "4.0.0" -requests==2.26.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") +requests==2.27.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") rfc3986==1.5.0; python_version >= "3.6" -rsa==4.8; python_version >= "3.6" and python_version < "4" -six==1.16.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.3.0" +six==1.16.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.7" sniffio==1.2.0; python_full_version >= "3.6.2" and python_version >= "3.6" -sqlalchemy-stubs==0.4 sqlalchemy2-stubs==0.0.2a19; python_version >= "3.6" -sqlalchemy==1.4.27; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") -starlette==0.14.2; python_version >= "3.6" and python_full_version >= "3.6.1" +sqlalchemy==1.4.29; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") +starlette==0.16.0; python_full_version >= "3.6.1" and python_version >= "3.7" toml==0.10.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6" -tomli==1.2.2; python_full_version >= "3.6.2" and python_full_version < "4.0.0" and python_version >= "3.6" -typed-ast==1.4.3; python_version < "3.8" and implementation_name == "cpython" and python_full_version >= "3.6.2" and python_full_version < "4.0.0" and python_version >= "3.5" -typing-extensions==4.0.0 -urllib3==1.26.7; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version < "4" -uvicorn==0.15.0 -zipp==3.6.0; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.8" or python_full_version >= "3.6.0" and python_version < "3.8" and python_version >= "3.6" +tomli==1.2.3; python_version >= "3.6" and python_full_version >= "3.6.2" +typing-extensions==4.0.1 +urllib3==1.26.8; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version < "4" +uvicorn==0.16.0 diff --git a/{{cookiecutter.project_name}}/template_fastapi_users/requirements.txt b/{{cookiecutter.project_name}}/template_fastapi_users/requirements.txt index ed2b5d3..0bd6626 100644 --- a/{{cookiecutter.project_name}}/template_fastapi_users/requirements.txt +++ b/{{cookiecutter.project_name}}/template_fastapi_users/requirements.txt @@ -1,37 +1,35 @@ alembic==1.7.5; python_version >= "3.6" +anyio==3.4.0; python_full_version >= "3.6.2" and python_version >= "3.7" asgiref==3.4.1; python_version >= "3.6" -asyncpg==0.24.0; python_full_version >= "3.6.0" -bcrypt==3.2.0; python_version >= "3.6" +asyncpg==0.25.0; python_full_version >= "3.6.0" +bcrypt==3.2.0; python_version >= "3.7" certifi==2021.10.8; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" -cffi==1.15.0 -charset-normalizer==2.0.8; python_full_version >= "3.6.0" and python_version >= "3" +cffi==1.15.0; python_version >= "3.7" +charset-normalizer==2.0.10; python_full_version >= "3.6.0" and python_version >= "3" click==8.0.3; python_version >= "3.6" colorama==0.4.4; python_version >= "3.6" and python_full_version < "3.0.0" and platform_system == "Windows" or platform_system == "Windows" and python_version >= "3.6" and python_full_version >= "3.5.0" -cryptography==36.0.0; python_version >= "3.6" dnspython==2.1.0; python_full_version >= "3.6.1" and python_version >= "3.6" -ecdsa==0.17.0; python_version >= "2.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" -email-validator==1.1.3; python_full_version >= "3.6.1" -fastapi==0.68.2; python_full_version >= "3.6.1" +email-validator==1.1.3; python_full_version >= "3.6.1" and (python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7") and python_version >= "3.7" +fastapi-users-db-sqlalchemy==2.0.4; python_version >= "3.7" +fastapi-users==9.2.0; python_version >= "3.7" +fastapi==0.70.1; python_full_version >= "3.6.1" and python_version >= "3.7" greenlet==1.1.2; python_version >= "3" and python_full_version < "3.0.0" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32") and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0") and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6") or python_version >= "3" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32") and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0") and python_full_version >= "3.5.0" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6") h11==0.12.0; python_version >= "3.6" -idna==3.3; python_full_version >= "3.6.1" and python_version >= "3.5" -importlib-metadata==4.8.2; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.8" or python_full_version >= "3.6.0" and python_version < "3.8" and python_version >= "3.6" -importlib-resources==5.4.0; python_version < "3.9" and python_version >= "3.6" +idna==3.3; python_full_version >= "3.6.2" and python_version >= "3.7" +makefun==1.12.1; python_version >= "3.7" mako==1.1.6; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" markupsafe==2.0.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" -passlib==1.7.4 -pyasn1==0.4.8; python_version >= "3.6" and python_version < "4" -pycparser==2.21; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.4.0" -pydantic==1.8.2; python_full_version >= "3.6.1" +passlib==1.7.4; python_version >= "3.7" +pycparser==2.21; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version >= "3.4.0" +pydantic==1.9.0; python_full_version >= "3.6.1" +pyjwt==2.3.0; python_version >= "3.7" python-dotenv==0.19.2; python_version >= "3.5" -python-jose==3.3.0 python-multipart==0.0.5 -requests==2.26.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") -rsa==4.8; python_version >= "3.6" and python_version < "4" -six==1.16.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.3.0" -sqlalchemy==1.4.27; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") -starlette==0.14.2; python_version >= "3.6" and python_full_version >= "3.6.1" -typing-extensions==4.0.0; python_version >= "3.6" and python_version < "3.8" and python_full_version >= "3.6.1" and (python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.8" or python_full_version >= "3.6.0" and python_version < "3.8" and python_version >= "3.6") -urllib3==1.26.7; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version < "4" -uvicorn==0.15.0 -zipp==3.6.0; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.8" or python_full_version >= "3.6.0" and python_version < "3.8" and python_version >= "3.6" +requests==2.27.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") +six==1.16.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.7" +sniffio==1.2.0; python_full_version >= "3.6.2" and python_version >= "3.7" +sqlalchemy==1.4.29; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") +starlette==0.16.0; python_full_version >= "3.6.1" and python_version >= "3.7" +typing-extensions==4.0.1; python_version >= "3.7" and python_full_version >= "3.6.1" +urllib3==1.26.8; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version < "4" +uvicorn==0.16.0 diff --git a/{{cookiecutter.project_name}}/template_minimal/app/api/api.py b/{{cookiecutter.project_name}}/template_minimal/app/api/api.py index 79b9497..58528de 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/api/api.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/api/api.py @@ -2,6 +2,6 @@ from app.api.endpoints import auth, users -api_router = APIRouter() +api_router = APIRouter(prefix="/test") api_router.include_router(auth.router, prefix="/auth", tags=["auth"]) api_router.include_router(users.router, prefix="/users", tags=["users"]) diff --git a/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/auth.py b/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/auth.py index 90706bb..a234e36 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/auth.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/auth.py @@ -89,3 +89,44 @@ async def refresh_token( "refresh_token": refresh_token, "refresh_expire_at": refresh_expire_at, } + + +@router.post("/refresh-token", response_model=schemas.UserCreate) +async def refresh_token2( + input: schemas.TokenRefresh, session: AsyncSession = Depends(deps.get_session) +): + """ + OAuth2 compatible token, get an access token for future requests using refresh token + """ + try: + payload = jwt.decode( + input.refresh_token, + config.settings.SECRET_KEY, + algorithms=[security.ALGORITHM], + ) + token_data = schemas.TokenPayload(**payload) + except (jwt.JWTError, ValidationError): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Could not validate credentials", + ) + if not token_data.refresh: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Could not validate credentials", + ) + result = await session.execute(select(User).where(User.id == token_data.sub)) + user: Optional[User] = result.scalars().first() + + if user is None: + raise HTTPException(status_code=404, detail="User not found") + + access_token, expire_at = security.create_access_token(user.id) + refresh_token, refresh_expire_at = security.create_refresh_token(user.id) + return { + "token_type": "bearer", + "access_token": access_token, + "expire_at": expire_at, + "refresh_token": refresh_token, + "refresh_expire_at": refresh_expire_at, + } diff --git a/{{cookiecutter.project_name}}/template_minimal/app/core/config.py b/{{cookiecutter.project_name}}/template_minimal/app/core/config.py index 4f0a92e..79177a2 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/core/config.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/core/config.py @@ -27,7 +27,7 @@ PROJECT_DIR = Path(__file__).parent.parent.parent -pyproject_content = toml.load(f"{PROJECT_DIR}/pyproject.toml")["tool"]["poetry"] +PYPROJECT_CONTENT = toml.load(f"{PROJECT_DIR}/pyproject.toml")["tool"]["poetry"] class Settings(BaseSettings): @@ -40,9 +40,9 @@ class Settings(BaseSettings): BACKEND_CORS_ORIGINS: Union[str, list[AnyHttpUrl]] # PROJECT NAME, VERSION AND DESCRIPTION - PROJECT_NAME: str = pyproject_content["name"] - VERSION: str = pyproject_content["version"] - DESCRIPTION: str = pyproject_content["description"] + PROJECT_NAME: str = PYPROJECT_CONTENT["name"] + VERSION: str = PYPROJECT_CONTENT["version"] + DESCRIPTION: str = PYPROJECT_CONTENT["description"] # POSTGRESQL DEFAULT DATABASE DEFAULT_DATABASE_HOSTNAME: str diff --git a/{{cookiecutter.project_name}}/template_minimal/poetry.lock b/{{cookiecutter.project_name}}/template_minimal/poetry.lock index 2062419..91d42db 100644 --- a/{{cookiecutter.project_name}}/template_minimal/poetry.lock +++ b/{{cookiecutter.project_name}}/template_minimal/poetry.lock @@ -7,8 +7,6 @@ optional = false python-versions = ">=3.6" [package.dependencies] -importlib-metadata = {version = "*", markers = "python_version < \"3.9\""} -importlib-resources = {version = "*", markers = "python_version < \"3.9\""} Mako = "*" SQLAlchemy = ">=1.3.0" @@ -26,7 +24,6 @@ python-versions = ">=3.6.2" [package.dependencies] idna = ">=2.8" sniffio = ">=1.1" -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] doc = ["sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] @@ -41,9 +38,6 @@ category = "main" optional = false python-versions = ">=3.6" -[package.dependencies] -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} - [package.extras] tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] @@ -55,9 +49,6 @@ category = "main" optional = false python-versions = ">=3.6.0" -[package.dependencies] -typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\""} - [package.extras] dev = ["Cython (>=0.29.24,<0.30.0)", "pytest (>=6.0)", "Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "pycodestyle (>=2.7.0,<2.8.0)", "flake8 (>=3.9.2,<3.10.0)", "uvloop (>=0.15.3)"] docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)"] @@ -126,7 +117,6 @@ mypy-extensions = ">=0.4.3" pathspec = ">=0.9.0,<1" platformdirs = ">=2" tomli = ">=0.2.6,<2.0.0" -typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} typing-extensions = [ {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}, {version = "!=3.10.0.1", markers = "python_version >= \"3.10\""}, @@ -160,7 +150,7 @@ pycparser = "*" [[package]] name = "charset-normalizer" -version = "2.0.9" +version = "2.0.10" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false @@ -179,7 +169,6 @@ python-versions = ">=3.6" [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "colorama" @@ -288,7 +277,6 @@ optional = false python-versions = ">=3.6" [package.dependencies] -importlib-metadata = {version = "<4.3", markers = "python_version < \"3.8\""} mccabe = ">=0.6.0,<0.7.0" pycodestyle = ">=2.8.0,<2.9.0" pyflakes = ">=2.4.0,<2.5.0" @@ -357,37 +345,6 @@ category = "main" optional = false python-versions = ">=3.5" -[[package]] -name = "importlib-metadata" -version = "4.2.0" -description = "Read metadata from Python packages" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} -zipp = ">=0.5" - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] - -[[package]] -name = "importlib-resources" -version = "5.4.0" -description = "Read resources from Python packages" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-black (>=0.3.7)", "pytest-mypy"] - [[package]] name = "iniconfig" version = "1.1.1" @@ -505,9 +462,6 @@ category = "dev" optional = false python-versions = ">=3.6" -[package.dependencies] -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} - [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] @@ -591,7 +545,6 @@ python-versions = ">=3.6" atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" @@ -725,7 +678,6 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" [package.dependencies] greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [package.extras] aiomysql = ["greenlet (!=0.4.17)", "aiomysql"] @@ -769,7 +721,6 @@ python-versions = ">=3.6" [package.dependencies] anyio = ">=3.0.0,<4" -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests"] @@ -790,14 +741,6 @@ category = "dev" optional = false python-versions = ">=3.6" -[[package]] -name = "typed-ast" -version = "1.5.1" -description = "a fork of Python 2 and 3 ast modules with type comment support" -category = "dev" -optional = false -python-versions = ">=3.6" - [[package]] name = "typing-extensions" version = "4.0.1" @@ -808,7 +751,7 @@ python-versions = ">=3.6" [[package]] name = "urllib3" -version = "1.26.7" +version = "1.26.8" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false @@ -831,27 +774,14 @@ python-versions = "*" asgiref = ">=3.4.0" click = ">=7.0" h11 = ">=0.8" -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] standard = ["httptools (>=0.2.0,<0.4.0)", "watchgod (>=0.6)", "python-dotenv (>=0.13)", "PyYAML (>=5.1)", "websockets (>=9.1)", "websockets (>=10.0)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "colorama (>=0.4)"] -[[package]] -name = "zipp" -version = "3.7.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] - [metadata] lock-version = "1.1" -python-versions = "^3.7" -content-hash = "a00e6962598c9e04b8f260f70bb66492d5a520b72e65d4d1aa227a0a2a93f5cb" +python-versions = "^3.9" +content-hash = "fb4d1a3aee0c00c3559d6367dd9094364793c22c17fc37524063a6a8ed56d69f" [metadata.files] alembic = [ @@ -975,8 +905,8 @@ cffi = [ {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"}, ] charset-normalizer = [ - {file = "charset-normalizer-2.0.9.tar.gz", hash = "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c"}, - {file = "charset_normalizer-2.0.9-py3-none-any.whl", hash = "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721"}, + {file = "charset-normalizer-2.0.10.tar.gz", hash = "sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd"}, + {file = "charset_normalizer-2.0.10-py3-none-any.whl", hash = "sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455"}, ] click = [ {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, @@ -1145,14 +1075,6 @@ idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, ] -importlib-metadata = [ - {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, - {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, -] -importlib-resources = [ - {file = "importlib_resources-5.4.0-py3-none-any.whl", hash = "sha256:33a95faed5fc19b4bc16b29a6eeae248a3fe69dd55d4d229d2b480e23eeaad45"}, - {file = "importlib_resources-5.4.0.tar.gz", hash = "sha256:d756e2f85dd4de2ba89be0b21dba2a3bbec2e871a42a3a16719258a11f87506b"}, -] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, @@ -1414,40 +1336,15 @@ tomli = [ {file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"}, {file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"}, ] -typed-ast = [ - {file = "typed_ast-1.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d8314c92414ce7481eee7ad42b353943679cf6f30237b5ecbf7d835519e1212"}, - {file = "typed_ast-1.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b53ae5de5500529c76225d18eeb060efbcec90ad5e030713fe8dab0fb4531631"}, - {file = "typed_ast-1.5.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:24058827d8f5d633f97223f5148a7d22628099a3d2efe06654ce872f46f07cdb"}, - {file = "typed_ast-1.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:a6d495c1ef572519a7bac9534dbf6d94c40e5b6a608ef41136133377bba4aa08"}, - {file = "typed_ast-1.5.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:de4ecae89c7d8b56169473e08f6bfd2df7f95015591f43126e4ea7865928677e"}, - {file = "typed_ast-1.5.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:256115a5bc7ea9e665c6314ed6671ee2c08ca380f9d5f130bd4d2c1f5848d695"}, - {file = "typed_ast-1.5.1-cp36-cp36m-win_amd64.whl", hash = "sha256:7c42707ab981b6cf4b73490c16e9d17fcd5227039720ca14abe415d39a173a30"}, - {file = "typed_ast-1.5.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:71dcda943a471d826ea930dd449ac7e76db7be778fcd722deb63642bab32ea3f"}, - {file = "typed_ast-1.5.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4f30a2bcd8e68adbb791ce1567fdb897357506f7ea6716f6bbdd3053ac4d9471"}, - {file = "typed_ast-1.5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ca9e8300d8ba0b66d140820cf463438c8e7b4cdc6fd710c059bfcfb1531d03fb"}, - {file = "typed_ast-1.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9caaf2b440efb39ecbc45e2fabde809cbe56272719131a6318fd9bf08b58e2cb"}, - {file = "typed_ast-1.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c9bcad65d66d594bffab8575f39420fe0ee96f66e23c4d927ebb4e24354ec1af"}, - {file = "typed_ast-1.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:591bc04e507595887160ed7aa8d6785867fb86c5793911be79ccede61ae96f4d"}, - {file = "typed_ast-1.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:a80d84f535642420dd17e16ae25bb46c7f4c16ee231105e7f3eb43976a89670a"}, - {file = "typed_ast-1.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:38cf5c642fa808300bae1281460d4f9b7617cf864d4e383054a5ef336e344d32"}, - {file = "typed_ast-1.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b6ab14c56bc9c7e3c30228a0a0b54b915b1579613f6e463ba6f4eb1382e7fd4"}, - {file = "typed_ast-1.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a2b8d7007f6280e36fa42652df47087ac7b0a7d7f09f9468f07792ba646aac2d"}, - {file = "typed_ast-1.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:b6d17f37f6edd879141e64a5db17b67488cfeffeedad8c5cec0392305e9bc775"}, - {file = "typed_ast-1.5.1.tar.gz", hash = "sha256:484137cab8ecf47e137260daa20bafbba5f4e3ec7fda1c1e69ab299b75fa81c5"}, -] typing-extensions = [ {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, ] urllib3 = [ - {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, - {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"}, + {file = "urllib3-1.26.8-py2.py3-none-any.whl", hash = "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"}, + {file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"}, ] uvicorn = [ {file = "uvicorn-0.16.0-py3-none-any.whl", hash = "sha256:d8c839231f270adaa6d338d525e2652a0b4a5f4c2430b5c4ef6ae4d11776b0d2"}, {file = "uvicorn-0.16.0.tar.gz", hash = "sha256:eacb66afa65e0648fcbce5e746b135d09722231ffffc61883d4fac2b62fbea8d"}, ] -zipp = [ - {file = "zipp-3.7.0-py3-none-any.whl", hash = "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"}, - {file = "zipp-3.7.0.tar.gz", hash = "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d"}, -] From 4b367dbb44bda1d1d1838a78e65e3e817d3f5bfb Mon Sep 17 00:00:00 2001 From: rafsaf Date: Sun, 30 Jan 2022 00:57:52 +0100 Subject: [PATCH 03/11] add cookiecutter option to choose template, update tests for templates and github workflow, run formatting --- .github/workflows/tests.yml | 61 ++++++++++++++----- .gitignore | 6 +- cookiecutter.json | 5 +- hooks/post_gen_project.py | 40 ++++++++++-- tests/create_fastapi_users_project.py | 24 ++++++++ ...t_project.py => create_minimal_project.py} | 5 +- .../template_fastapi_users/.env.example | 1 - .../template_fastapi_users/.env.template | 1 - .../template_fastapi_users/app/core/config.py | 1 - .../app/core/security.py | 4 +- .../template_fastapi_users/app/main.py | 2 +- .../app/schemas/user.py | 2 +- .../app/tests/conftest.py | 3 +- .../app/tests/test_auth.py | 15 +---- .../template_fastapi_users/app/tests/utils.py | 6 +- .../requirements-dev.txt | 4 +- .../template_fastapi_users/requirements.txt | 11 +--- .../template_minimal/app/api/api.py | 2 +- .../template_minimal/app/api/deps.py | 2 +- .../app/api/endpoints/auth.py | 2 +- .../template_minimal/app/conftest.py | 2 - .../template_minimal/app/core/config.py | 3 +- .../template_minimal/app/initial_data.py | 2 +- .../template_minimal/app/tests/conftest.py | 11 ++-- .../template_minimal/app/tests/test_auth.py | 4 +- .../template_minimal/requirements-dev.txt | 61 ++++++++----------- .../template_minimal/requirements.txt | 29 +++++---- 27 files changed, 183 insertions(+), 126 deletions(-) create mode 100644 tests/create_fastapi_users_project.py rename tests/{create_test_project.py => create_minimal_project.py} (65%) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 08cae0d..9914a63 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -32,33 +32,64 @@ jobs: uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - - name: Load cached venv - id: cached-poetry-dependencies + - name: Load cached venv1 + id: cached-poetry-dependencies1 uses: actions/cache@v2 with: - path: .venv - key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }} + path: .venv1 + key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('{{cookiecutter.project_name}}/template_minimal/poetry.lock') }} - name: Install dependencies and actiavte virtualenv - if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + if: steps.cached-poetry-dependencies1.outputs.cache-hit != 'true' run: | - python -m venv .venv - source .venv/bin/activate - pip install -r {{cookiecutter.project_name}}/requirements-dev.txt + python -m venv .venv1 + source .venv1/bin/activate + pip install -r {{cookiecutter.project_name}}/template_minimal/requirements-dev.txt pip install cookiecutter - - name: Lint with flake8 + - name: Load cached venv2 + id: cached-poetry-dependencies2 + uses: actions/cache@v2 + with: + path: .venv2 + key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('{{cookiecutter.project_name}}/template_fastapi_users/poetry.lock') }} + - name: Install dependencies and actiavte virtualenv + if: steps.cached-poetry-dependencies2.outputs.cache-hit != 'true' + run: | + python -m venv .venv2 + source .venv2/bin/activate + pip install -r {{cookiecutter.project_name}}/template_minimal/requirements-dev.txt + pip install cookiecutter + - name: Lint with flake8 minimal project + run: | + source .venv1/bin/activate + # stop the build if there are Python syntax errors or undefined names + cd \{\{cookiecutter.project_name\}\}/template_minimal + flake8 app --count --exit-zero --statistics + - name: Lint with flake8 fastapi_users project run: | - source .venv/bin/activate + source .venv2/bin/activate # stop the build if there are Python syntax errors or undefined names - cd \{\{cookiecutter.project_name\}\}/ + cd \{\{cookiecutter.project_name\}\}/template_fastapi_users flake8 app --count --exit-zero --statistics - - name: Test new cookiecuttered project is passing pytest test + - name: Test minimal project is passing pytest test + run: | + source .venv1/bin/activate + python tests/create_minimal_project.py + export TEST_DATABASE_HOSTNAME=localhost + export TEST_DATABASE_USER=test + export TEST_DATABASE_PASSWORD=test + export TEST_DATABASE_PORT=30000 + export TEST_DATABASE_DB=test + + pytest minimal_project + + - name: Test fastapi_users project is passing pytest test run: | - source .venv/bin/activate - python tests/create_test_project.py + source .venv2/bin/activate + python tests/create_fastapi_users_project.py export TEST_DATABASE_HOSTNAME=localhost export TEST_DATABASE_USER=test export TEST_DATABASE_PASSWORD=test export TEST_DATABASE_PORT=30000 export TEST_DATABASE_DB=test - pytest generated_project_for_tests + pytest fastapi_users_project diff --git a/.gitignore b/.gitignore index 600d2d3..562d81d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ -.vscode \ No newline at end of file +.vscode +.venv +venv +.venv1 +.venv2 \ No newline at end of file diff --git a/cookiecutter.json b/cookiecutter.json index 6dbf3b7..7b410dc 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -1,3 +1,4 @@ { - "project_name": "Base Project" -} \ No newline at end of file + "project_name": "Base Project", + "use_fastapi_users": false +} diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py index bd71b11..1d2a070 100644 --- a/hooks/post_gen_project.py +++ b/hooks/post_gen_project.py @@ -1,12 +1,40 @@ """ +Copy template choosen from TEMPLATES to main folder. + Copy content from env template (autogenerated passwords etc) -to .env which is gitignored, then remove template env file as it is redundant +to .env which is gitignored, then remove template env file as it is redundant. """ - from pathlib import Path +from shutil import copytree, rmtree + +PROJECT_NAME = "{{ cookiecutter.module_name }}" +USE_FASTAPI_USERS = bool("{{ cookiecutter.use_fastapi_users }}") +TEMPLATES = ["template_fastapi_users", "template_minimal"] + + +def copy_choosen_template_to_main_dir(used_template: str): + if used_template not in TEMPLATES: + raise ValueError(f"{used_template} not in {TEMPLATES}") + + copytree(used_template, "./") + + for template in TEMPLATES: + rmtree(Path(template)) + + +def create_env_file_and_remove_env_template(): + env_template_file = Path(".env.template") + env_file = Path(".env") + + env_file.write_text(env_template_file.read_text()) + env_template_file.unlink() + -env_template_file = Path("./.env.template") -env_file = Path("./.env") +if __name__ == "__main__": + if USE_FASTAPI_USERS: + used_template = "template_fastapi_users" + else: + used_template = "template_minimal" -env_file.write_text(env_template_file.read_text()) -env_template_file.unlink() + copy_choosen_template_to_main_dir(used_template=used_template) + create_env_file_and_remove_env_template() diff --git a/tests/create_fastapi_users_project.py b/tests/create_fastapi_users_project.py new file mode 100644 index 0000000..2833c0c --- /dev/null +++ b/tests/create_fastapi_users_project.py @@ -0,0 +1,24 @@ +""" +Creates fastapi_users template project in root folder with default values +""" + +from pathlib import Path + +from cookiecutter.main import cookiecutter + +ROOT_FOLDER = Path(__file__).parent.parent + + +def main(): + cookiecutter( + template=str(ROOT_FOLDER), + no_input=True, + extra_context={ + "project_name": "fastapi_users_project", + "use_fastapi_users": True, + }, + ) + + +if __name__ == "__main__": + main() diff --git a/tests/create_test_project.py b/tests/create_minimal_project.py similarity index 65% rename from tests/create_test_project.py rename to tests/create_minimal_project.py index 9c3554f..b1b126d 100644 --- a/tests/create_test_project.py +++ b/tests/create_minimal_project.py @@ -1,5 +1,5 @@ """ -Creates template project in root folder with default values +Creates minimal template project in root folder with default values """ from pathlib import Path @@ -14,7 +14,8 @@ def main(): template=str(ROOT_FOLDER), no_input=True, extra_context={ - "project_name": "generated_project_for_tests", + "project_name": "minimal_project", + "use_fastapi_users": False, }, ) diff --git a/{{cookiecutter.project_name}}/template_fastapi_users/.env.example b/{{cookiecutter.project_name}}/template_fastapi_users/.env.example index 6442612..db9fd91 100644 --- a/{{cookiecutter.project_name}}/template_fastapi_users/.env.example +++ b/{{cookiecutter.project_name}}/template_fastapi_users/.env.example @@ -1,7 +1,6 @@ SECRET_KEY="DVnFmhwvjEhJZpuhndxjhlezxQPJmBIIkMDEmFREWQADPcUnrG" ENVIRONMENT="DEV" ACCESS_TOKEN_EXPIRE_MINUTES="11520" -REFRESH_TOKEN_EXPIRE_MINUTES="40320" BACKEND_CORS_ORIGINS="http://localhost:3000,http://localhost:8001" DEFAULT_DATABASE_HOSTNAME="localhost" diff --git a/{{cookiecutter.project_name}}/template_fastapi_users/.env.template b/{{cookiecutter.project_name}}/template_fastapi_users/.env.template index f392a2f..a69ae3f 100644 --- a/{{cookiecutter.project_name}}/template_fastapi_users/.env.template +++ b/{{cookiecutter.project_name}}/template_fastapi_users/.env.template @@ -1,7 +1,6 @@ SECRET_KEY="{{ random_ascii_string(50) }}" ENVIRONMENT="DEV" ACCESS_TOKEN_EXPIRE_MINUTES="11520" -REFRESH_TOKEN_EXPIRE_MINUTES="40320" BACKEND_CORS_ORIGINS="http://localhost:3000,http://localhost:8001" DEFAULT_DATABASE_HOSTNAME="localhost" diff --git a/{{cookiecutter.project_name}}/template_fastapi_users/app/core/config.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/core/config.py index bcef1de..a86a202 100644 --- a/{{cookiecutter.project_name}}/template_fastapi_users/app/core/config.py +++ b/{{cookiecutter.project_name}}/template_fastapi_users/app/core/config.py @@ -34,7 +34,6 @@ class Settings(BaseSettings): SECRET_KEY: str ENVIRONMENT: Literal["DEV", "PYTEST", "STAGE", "PRODUCTION"] ACCESS_TOKEN_EXPIRE_MINUTES: int - REFRESH_TOKEN_EXPIRE_MINUTES: int BACKEND_CORS_ORIGINS: Union[str, list[AnyHttpUrl]] # PROJECT NAME, VERSION AND DESCRIPTION diff --git a/{{cookiecutter.project_name}}/template_fastapi_users/app/core/security.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/core/security.py index 448aacf..4ceb025 100644 --- a/{{cookiecutter.project_name}}/template_fastapi_users/app/core/security.py +++ b/{{cookiecutter.project_name}}/template_fastapi_users/app/core/security.py @@ -1,5 +1,5 @@ """ -You can have several authentication methods, e.g. a cookie +You can have several authentication methods, e.g. a cookie authentication for browser-based queries and a JWT token authentication for pure API queries. In this template, token will be sent through Bearer header @@ -7,7 +7,7 @@ using JWT tokens. There are more option to consider, refer to -https://fastapi-users.github.io/fastapi-users/configuration/authentication/ +https://fastapi-users.github.io/fastapi-users/configuration/authentication/ UserManager class is core fastapi users class with customizable attrs and methods https://fastapi-users.github.io/fastapi-users/configuration/user-manager/ diff --git a/{{cookiecutter.project_name}}/template_fastapi_users/app/main.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/main.py index abac223..0892233 100644 --- a/{{cookiecutter.project_name}}/template_fastapi_users/app/main.py +++ b/{{cookiecutter.project_name}}/template_fastapi_users/app/main.py @@ -5,8 +5,8 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from app.core import config from app.api.api import api_router +from app.core import config app = FastAPI( title=config.settings.PROJECT_NAME, diff --git a/{{cookiecutter.project_name}}/template_fastapi_users/app/schemas/user.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/schemas/user.py index 8d02126..ff08ab2 100644 --- a/{{cookiecutter.project_name}}/template_fastapi_users/app/schemas/user.py +++ b/{{cookiecutter.project_name}}/template_fastapi_users/app/schemas/user.py @@ -2,10 +2,10 @@ All fields in schemas are defaults from FastAPI Users, repeated below for easier view """ -from fastapi_users import models import uuid from typing import Optional +from fastapi_users import models from pydantic import UUID4, EmailStr, Field diff --git a/{{cookiecutter.project_name}}/template_fastapi_users/app/tests/conftest.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/tests/conftest.py index 9e92e71..4c11514 100644 --- a/{{cookiecutter.project_name}}/template_fastapi_users/app/tests/conftest.py +++ b/{{cookiecutter.project_name}}/template_fastapi_users/app/tests/conftest.py @@ -2,9 +2,10 @@ from typing import AsyncGenerator import pytest +from fastapi_users.password import get_password_hash from httpx import AsyncClient from sqlalchemy.ext.asyncio import AsyncSession -from fastapi_users.password import get_password_hash + from app.core import config from app.main import app from app.models import Base diff --git a/{{cookiecutter.project_name}}/template_fastapi_users/app/tests/test_auth.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/tests/test_auth.py index d2bb8c3..25b6a15 100644 --- a/{{cookiecutter.project_name}}/template_fastapi_users/app/tests/test_auth.py +++ b/{{cookiecutter.project_name}}/template_fastapi_users/app/tests/test_auth.py @@ -4,8 +4,8 @@ import pytest from httpx import AsyncClient -from app.schemas import UserDB +from app.schemas import UserDB # All test coroutines in file will be treated as marked (async allowed). pytestmark = pytest.mark.asyncio @@ -26,22 +26,11 @@ async def test_login_endpoints(client: AsyncClient, default_user: UserDB): token = access_token_res.json() access_token = token["access_token"] - refresh_token = token["refresh_token"] # test-token endpoint - test_token = await client.post( + test_token = await client.get( "/users/me", headers={"Authorization": f"Bearer {access_token}"} ) assert test_token.status_code == 200 response_user = test_token.json() assert response_user["email"] == default_user.email - - # refresh-token endpoint - get_new_token = await client.post( - "/auth/refresh-token", json={"refresh_token": refresh_token} - ) - - assert get_new_token.status_code == 200 - new_token = get_new_token.json() - - assert "access_token" in new_token diff --git a/{{cookiecutter.project_name}}/template_fastapi_users/app/tests/utils.py b/{{cookiecutter.project_name}}/template_fastapi_users/app/tests/utils.py index 0f9fd72..aacc1c5 100644 --- a/{{cookiecutter.project_name}}/template_fastapi_users/app/tests/utils.py +++ b/{{cookiecutter.project_name}}/template_fastapi_users/app/tests/utils.py @@ -1,13 +1,11 @@ import random import string -from fastapi_users import password -from pydantic.networks import EmailStr -from sqlalchemy.ext.asyncio import AsyncSession from fastapi_users_db_sqlalchemy import SQLAlchemyUserDatabase +from pydantic.networks import EmailStr +from sqlalchemy.ext.asyncio import AsyncSession from app import schemas -from app.core import config from app.models import UserTable diff --git a/{{cookiecutter.project_name}}/template_fastapi_users/requirements-dev.txt b/{{cookiecutter.project_name}}/template_fastapi_users/requirements-dev.txt index 6662e93..7a33726 100644 --- a/{{cookiecutter.project_name}}/template_fastapi_users/requirements-dev.txt +++ b/{{cookiecutter.project_name}}/template_fastapi_users/requirements-dev.txt @@ -11,7 +11,7 @@ certifi==2021.10.8; python_version >= "3.6" and python_full_version < "3.0.0" or cffi==1.15.0; python_version >= "3.7" charset-normalizer==2.0.10; python_full_version >= "3.6.0" and python_version >= "3.6" click==8.0.3; python_version >= "3.6" and python_full_version >= "3.6.2" -colorama==0.4.4; python_version >= "3.6" and python_full_version < "3.0.0" and sys_platform == "win32" and platform_system == "Windows" or sys_platform == "win32" and python_version >= "3.6" and python_full_version >= "3.5.0" and platform_system == "Windows" +colorama==0.4.4; sys_platform == "win32" and python_version >= "3.6" and python_full_version >= "3.6.2" and platform_system == "Windows" and (python_version >= "3.6" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.6" and python_full_version >= "3.5.0") coverage==6.2; python_version >= "3.6" dnspython==2.1.0; python_full_version >= "3.6.1" and python_version >= "3.6" email-validator==1.1.3; python_full_version >= "3.6.1" and (python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7") and python_version >= "3.7" @@ -46,7 +46,7 @@ pyparsing==3.0.6; python_version >= "3.6" pytest-asyncio==0.16.0; python_version >= "3.6" pytest==6.2.5; python_version >= "3.6" python-dotenv==0.19.2; python_version >= "3.5" -python-multipart==0.0.5 +python-multipart==0.0.5; python_version >= "3.7" requests==2.27.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") rfc3986==1.5.0; python_version >= "3.6" six==1.16.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.7" diff --git a/{{cookiecutter.project_name}}/template_fastapi_users/requirements.txt b/{{cookiecutter.project_name}}/template_fastapi_users/requirements.txt index 0bd6626..e1fc98b 100644 --- a/{{cookiecutter.project_name}}/template_fastapi_users/requirements.txt +++ b/{{cookiecutter.project_name}}/template_fastapi_users/requirements.txt @@ -1,20 +1,14 @@ alembic==1.7.5; python_version >= "3.6" anyio==3.4.0; python_full_version >= "3.6.2" and python_version >= "3.7" -asgiref==3.4.1; python_version >= "3.6" asyncpg==0.25.0; python_full_version >= "3.6.0" bcrypt==3.2.0; python_version >= "3.7" -certifi==2021.10.8; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" cffi==1.15.0; python_version >= "3.7" -charset-normalizer==2.0.10; python_full_version >= "3.6.0" and python_version >= "3" -click==8.0.3; python_version >= "3.6" -colorama==0.4.4; python_version >= "3.6" and python_full_version < "3.0.0" and platform_system == "Windows" or platform_system == "Windows" and python_version >= "3.6" and python_full_version >= "3.5.0" dnspython==2.1.0; python_full_version >= "3.6.1" and python_version >= "3.6" email-validator==1.1.3; python_full_version >= "3.6.1" and (python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7") and python_version >= "3.7" fastapi-users-db-sqlalchemy==2.0.4; python_version >= "3.7" fastapi-users==9.2.0; python_version >= "3.7" fastapi==0.70.1; python_full_version >= "3.6.1" and python_version >= "3.7" greenlet==1.1.2; python_version >= "3" and python_full_version < "3.0.0" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32") and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0") and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6") or python_version >= "3" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32") and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0") and python_full_version >= "3.5.0" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6") -h11==0.12.0; python_version >= "3.6" idna==3.3; python_full_version >= "3.6.2" and python_version >= "3.7" makefun==1.12.1; python_version >= "3.7" mako==1.1.6; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" @@ -24,12 +18,9 @@ pycparser==2.21; python_version >= "3.7" and python_full_version < "3.0.0" or py pydantic==1.9.0; python_full_version >= "3.6.1" pyjwt==2.3.0; python_version >= "3.7" python-dotenv==0.19.2; python_version >= "3.5" -python-multipart==0.0.5 -requests==2.27.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") +python-multipart==0.0.5; python_version >= "3.7" six==1.16.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.7" sniffio==1.2.0; python_full_version >= "3.6.2" and python_version >= "3.7" sqlalchemy==1.4.29; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") starlette==0.16.0; python_full_version >= "3.6.1" and python_version >= "3.7" typing-extensions==4.0.1; python_version >= "3.7" and python_full_version >= "3.6.1" -urllib3==1.26.8; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version < "4" -uvicorn==0.16.0 diff --git a/{{cookiecutter.project_name}}/template_minimal/app/api/api.py b/{{cookiecutter.project_name}}/template_minimal/app/api/api.py index 58528de..79b9497 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/api/api.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/api/api.py @@ -2,6 +2,6 @@ from app.api.endpoints import auth, users -api_router = APIRouter(prefix="/test") +api_router = APIRouter() api_router.include_router(auth.router, prefix="/auth", tags=["auth"]) api_router.include_router(users.router, prefix="/users", tags=["users"]) diff --git a/{{cookiecutter.project_name}}/template_minimal/app/api/deps.py b/{{cookiecutter.project_name}}/template_minimal/app/api/deps.py index a8fc916..6a428fc 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/api/deps.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/api/deps.py @@ -8,7 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from app import schemas -from app.core import security, config +from app.core import config, security from app.models import User from app.session import async_session diff --git a/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/auth.py b/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/auth.py index a234e36..bc8c9ad 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/auth.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/auth.py @@ -9,7 +9,7 @@ from app import schemas from app.api import deps -from app.core import security, config +from app.core import config, security from app.models import User router = APIRouter() diff --git a/{{cookiecutter.project_name}}/template_minimal/app/conftest.py b/{{cookiecutter.project_name}}/template_minimal/app/conftest.py index ea7b91d..9d48255 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/conftest.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/conftest.py @@ -9,5 +9,3 @@ # This will ensure using test database os.environ["ENVIRONMENT"] = "PYTEST" -# This will change default 12 bcrypt rounds to only 1 so hashing password func will be short -os.environ["SECURITY_BCRYPT_DEFAULT_ROUNDS"] = "1" diff --git a/{{cookiecutter.project_name}}/template_minimal/app/core/config.py b/{{cookiecutter.project_name}}/template_minimal/app/core/config.py index 79177a2..06d259e 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/core/config.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/core/config.py @@ -20,12 +20,11 @@ """ from pathlib import Path -from typing import Union, Literal +from typing import Literal, Union import toml from pydantic import AnyHttpUrl, AnyUrl, BaseSettings, EmailStr, validator - PROJECT_DIR = Path(__file__).parent.parent.parent PYPROJECT_CONTENT = toml.load(f"{PROJECT_DIR}/pyproject.toml")["tool"]["poetry"] diff --git a/{{cookiecutter.project_name}}/template_minimal/app/initial_data.py b/{{cookiecutter.project_name}}/template_minimal/app/initial_data.py index 1f36897..227092d 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/initial_data.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/initial_data.py @@ -10,7 +10,7 @@ from sqlalchemy import select -from app.core import security, config +from app.core import config, security from app.models import User from app.session import async_session diff --git a/{{cookiecutter.project_name}}/template_minimal/app/tests/conftest.py b/{{cookiecutter.project_name}}/template_minimal/app/tests/conftest.py index 2707184..af40fc0 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/tests/conftest.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/tests/conftest.py @@ -6,11 +6,14 @@ from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from app.core import security, config +from app.core import config, security from app.main import app from app.models import Base, User from app.session import async_engine, async_session +default_user_email = "geralt@wiedzmin.pl" +default_user_hash = security.get_password_hash("geralt") + @pytest.fixture(scope="session") def event_loop(): @@ -47,12 +50,12 @@ async def session(test_db_setup_sessionmaker) -> AsyncGenerator[AsyncSession, No @pytest.fixture async def default_user(session: AsyncSession): - result = await session.execute(select(User).where(User.email == "user@email.com")) + result = await session.execute(select(User).where(User.email == default_user_email)) user: Optional[User] = result.scalars().first() if user is None: new_user = User( - email="user@email.com", - hashed_password=security.get_password_hash("password"), + email=default_user_email, + hashed_password=default_user_hash, full_name="fullname", ) session.add(new_user) diff --git a/{{cookiecutter.project_name}}/template_minimal/app/tests/test_auth.py b/{{cookiecutter.project_name}}/template_minimal/app/tests/test_auth.py index ccfaa18..40a52eb 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/tests/test_auth.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/tests/test_auth.py @@ -13,8 +13,8 @@ async def test_login_endpoints(client: AsyncClient, default_user: User): access_token = await client.post( "/auth/access-token", data={ - "username": "user@email.com", - "password": "password", + "username": "geralt@wiedzmin.pl", + "password": "geralt", }, headers={"Content-Type": "application/x-www-form-urlencoded"}, ) diff --git a/{{cookiecutter.project_name}}/template_minimal/requirements-dev.txt b/{{cookiecutter.project_name}}/template_minimal/requirements-dev.txt index e983a91..009f0d6 100644 --- a/{{cookiecutter.project_name}}/template_minimal/requirements-dev.txt +++ b/{{cookiecutter.project_name}}/template_minimal/requirements-dev.txt @@ -1,69 +1,62 @@ alembic==1.7.5; python_version >= "3.6" -anyio==3.4.0; python_full_version >= "3.6.2" and python_version >= "3.6" +anyio==3.4.0; python_version >= "3.6" and python_full_version >= "3.6.2" asgiref==3.4.1; python_version >= "3.6" -asyncpg==0.24.0; python_full_version >= "3.6.0" +asyncpg==0.25.0; python_full_version >= "3.6.0" atomicwrites==1.4.0; python_version >= "3.6" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.6" and python_full_version >= "3.4.0" -attrs==21.2.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" +attrs==21.4.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" autoflake==1.4 bcrypt==3.2.0; python_version >= "3.6" -black==21.11b1; python_full_version >= "3.6.2" and python_full_version < "4.0.0" +black==21.12b0; python_full_version >= "3.6.2" certifi==2021.10.8; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6" cffi==1.15.0 -charset-normalizer==2.0.8; python_full_version >= "3.6.0" and python_version >= "3.6" -click==8.0.3; python_full_version >= "3.6.2" and python_full_version < "4.0.0" and python_version >= "3.6" +charset-normalizer==2.0.10; python_full_version >= "3.6.0" and python_version >= "3.6" +click==8.0.3; python_version >= "3.6" and python_full_version >= "3.6.2" colorama==0.4.4; python_version >= "3.6" and python_full_version < "3.0.0" and sys_platform == "win32" and platform_system == "Windows" or sys_platform == "win32" and python_version >= "3.6" and python_full_version >= "3.5.0" and platform_system == "Windows" -coverage==5.5; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0" and python_version < "4") -cryptography==36.0.0; python_version >= "3.6" +coverage==6.2; python_version >= "3.6" +cryptography==36.0.1; python_version >= "3.6" dnspython==2.1.0; python_full_version >= "3.6.1" and python_version >= "3.6" ecdsa==0.17.0; python_version >= "2.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" email-validator==1.1.3; python_full_version >= "3.6.1" -fastapi==0.68.2; python_full_version >= "3.6.1" -flake8==3.9.2; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") +fastapi==0.71.0; python_full_version >= "3.6.1" +flake8==4.0.1; python_version >= "3.6" greenlet==1.1.2; python_version >= "3" and python_full_version < "3.0.0" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32") and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0") and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6") or python_version >= "3" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32") and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0") and python_full_version >= "3.5.0" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6") h11==0.12.0; python_version >= "3.6" -httpcore==0.13.7; python_version >= "3.6" -httpx==0.20.0; python_version >= "3.6" +httpcore==0.14.4; python_version >= "3.6" +httpx==0.21.3; python_version >= "3.6" idna==3.3 -importlib-metadata==4.8.2; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.8" or python_full_version >= "3.6.0" and python_version < "3.8" and python_version >= "3.6" -importlib-resources==5.4.0; python_version < "3.9" and python_version >= "3.6" iniconfig==1.1.1; python_version >= "3.6" isort==5.10.1; python_full_version >= "3.6.1" and python_version < "4.0" mako==1.1.6; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" markupsafe==2.0.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" -mccabe==0.6.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" -mypy-extensions==0.4.3; python_full_version >= "3.6.2" and python_full_version < "4.0.0" and python_version >= "3.5" -mypy==0.910; python_version >= "3.5" +mccabe==0.6.1; python_version >= "3.6" +mypy-extensions==0.4.3; python_full_version >= "3.6.2" packaging==21.3; python_version >= "3.6" passlib==1.7.4 -pathspec==0.9.0; python_full_version >= "3.6.2" and python_full_version < "4.0.0" -platformdirs==2.4.0; python_full_version >= "3.6.2" and python_full_version < "4.0.0" and python_version >= "3.6" +pathspec==0.9.0; python_full_version >= "3.6.2" +platformdirs==2.4.1; python_version >= "3.7" and python_full_version >= "3.6.2" pluggy==1.0.0; python_version >= "3.6" py==1.11.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" pyasn1==0.4.8; python_version >= "3.6" and python_version < "4" -pycodestyle==2.7.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" +pycodestyle==2.8.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" pycparser==2.21; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.4.0" -pydantic==1.8.2; python_full_version >= "3.6.1" -pyflakes==2.3.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" +pydantic==1.9.0; python_full_version >= "3.6.1" +pyflakes==2.4.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" pyparsing==3.0.6; python_version >= "3.6" pytest-asyncio==0.16.0; python_version >= "3.6" pytest==6.2.5; python_version >= "3.6" python-dotenv==0.19.2; python_version >= "3.5" python-jose==3.3.0 python-multipart==0.0.5 -regex==2021.11.10; python_full_version >= "3.6.2" and python_full_version < "4.0.0" -requests==2.26.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") +requests==2.27.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") rfc3986==1.5.0; python_version >= "3.6" rsa==4.8; python_version >= "3.6" and python_version < "4" six==1.16.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.3.0" -sniffio==1.2.0; python_full_version >= "3.6.2" and python_version >= "3.6" -sqlalchemy-stubs==0.4 +sniffio==1.2.0; python_version >= "3.6" and python_full_version >= "3.6.2" sqlalchemy2-stubs==0.0.2a19; python_version >= "3.6" -sqlalchemy==1.4.27; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") -starlette==0.14.2; python_version >= "3.6" and python_full_version >= "3.6.1" +sqlalchemy==1.4.29; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") +starlette==0.17.1; python_version >= "3.6" and python_full_version >= "3.6.1" toml==0.10.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6" -tomli==1.2.2; python_full_version >= "3.6.2" and python_full_version < "4.0.0" and python_version >= "3.6" -typed-ast==1.4.3; python_version < "3.8" and implementation_name == "cpython" and python_full_version >= "3.6.2" and python_full_version < "4.0.0" and python_version >= "3.5" -typing-extensions==4.0.0 -urllib3==1.26.7; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version < "4" -uvicorn==0.15.0 -zipp==3.6.0; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.8" or python_full_version >= "3.6.0" and python_version < "3.8" and python_version >= "3.6" +tomli==1.2.3; python_version >= "3.6" and python_full_version >= "3.6.2" +typing-extensions==4.0.1 +urllib3==1.26.8; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version < "4" +uvicorn==0.16.0 diff --git a/{{cookiecutter.project_name}}/template_minimal/requirements.txt b/{{cookiecutter.project_name}}/template_minimal/requirements.txt index ed2b5d3..6aaba0f 100644 --- a/{{cookiecutter.project_name}}/template_minimal/requirements.txt +++ b/{{cookiecutter.project_name}}/template_minimal/requirements.txt @@ -1,37 +1,36 @@ alembic==1.7.5; python_version >= "3.6" +anyio==3.4.0; python_version >= "3.6" and python_full_version >= "3.6.2" asgiref==3.4.1; python_version >= "3.6" -asyncpg==0.24.0; python_full_version >= "3.6.0" +asyncpg==0.25.0; python_full_version >= "3.6.0" bcrypt==3.2.0; python_version >= "3.6" certifi==2021.10.8; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" cffi==1.15.0 -charset-normalizer==2.0.8; python_full_version >= "3.6.0" and python_version >= "3" +charset-normalizer==2.0.10; python_full_version >= "3.6.0" and python_version >= "3" click==8.0.3; python_version >= "3.6" colorama==0.4.4; python_version >= "3.6" and python_full_version < "3.0.0" and platform_system == "Windows" or platform_system == "Windows" and python_version >= "3.6" and python_full_version >= "3.5.0" -cryptography==36.0.0; python_version >= "3.6" +cryptography==36.0.1; python_version >= "3.6" dnspython==2.1.0; python_full_version >= "3.6.1" and python_version >= "3.6" ecdsa==0.17.0; python_version >= "2.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" email-validator==1.1.3; python_full_version >= "3.6.1" -fastapi==0.68.2; python_full_version >= "3.6.1" +fastapi==0.71.0; python_full_version >= "3.6.1" greenlet==1.1.2; python_version >= "3" and python_full_version < "3.0.0" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32") and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0") and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6") or python_version >= "3" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32") and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0") and python_full_version >= "3.5.0" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6") h11==0.12.0; python_version >= "3.6" -idna==3.3; python_full_version >= "3.6.1" and python_version >= "3.5" -importlib-metadata==4.8.2; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.8" or python_full_version >= "3.6.0" and python_version < "3.8" and python_version >= "3.6" -importlib-resources==5.4.0; python_version < "3.9" and python_version >= "3.6" +idna==3.3; python_full_version >= "3.6.2" and python_version >= "3.6" mako==1.1.6; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" markupsafe==2.0.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" passlib==1.7.4 pyasn1==0.4.8; python_version >= "3.6" and python_version < "4" pycparser==2.21; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.4.0" -pydantic==1.8.2; python_full_version >= "3.6.1" +pydantic==1.9.0; python_full_version >= "3.6.1" python-dotenv==0.19.2; python_version >= "3.5" python-jose==3.3.0 python-multipart==0.0.5 -requests==2.26.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") +requests==2.27.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") rsa==4.8; python_version >= "3.6" and python_version < "4" six==1.16.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.3.0" -sqlalchemy==1.4.27; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") -starlette==0.14.2; python_version >= "3.6" and python_full_version >= "3.6.1" -typing-extensions==4.0.0; python_version >= "3.6" and python_version < "3.8" and python_full_version >= "3.6.1" and (python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.8" or python_full_version >= "3.6.0" and python_version < "3.8" and python_version >= "3.6") -urllib3==1.26.7; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version < "4" -uvicorn==0.15.0 -zipp==3.6.0; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.8" or python_full_version >= "3.6.0" and python_version < "3.8" and python_version >= "3.6" +sniffio==1.2.0; python_version >= "3.6" and python_full_version >= "3.6.2" +sqlalchemy==1.4.29; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") +starlette==0.17.1; python_version >= "3.6" and python_full_version >= "3.6.1" +typing-extensions==4.0.1; python_version >= "3.6" and python_full_version >= "3.6.1" +urllib3==1.26.8; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version < "4" +uvicorn==0.16.0 From 09aaa6bf8469a323f9f05044b4a9ddc84183147a Mon Sep 17 00:00:00 2001 From: rafsaf Date: Sun, 30 Jan 2022 01:01:51 +0100 Subject: [PATCH 04/11] fix typo in post_gen_project --- hooks/post_gen_project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py index 1d2a070..4f77d6a 100644 --- a/hooks/post_gen_project.py +++ b/hooks/post_gen_project.py @@ -7,7 +7,7 @@ from pathlib import Path from shutil import copytree, rmtree -PROJECT_NAME = "{{ cookiecutter.module_name }}" +PROJECT_NAME = "{{ cookiecutter.project_name }}" USE_FASTAPI_USERS = bool("{{ cookiecutter.use_fastapi_users }}") TEMPLATES = ["template_fastapi_users", "template_minimal"] From b26097e14addc5d7c18d5cbe649f6671aa4c6266 Mon Sep 17 00:00:00 2001 From: rafsaf Date: Sun, 30 Jan 2022 01:02:12 +0100 Subject: [PATCH 05/11] add python 3.10 to test workflows --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9914a63..048b6fc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - python-version: [3.9] + python-version: [3.9, 3.10] services: postgres: From ac98a959240b24a7cff8968a5f81d41d87415bd1 Mon Sep 17 00:00:00 2001 From: rafsaf Date: Sun, 30 Jan 2022 01:11:00 +0100 Subject: [PATCH 06/11] fix copytree in post_gen hook --- hooks/post_gen_project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py index 4f77d6a..e9463d6 100644 --- a/hooks/post_gen_project.py +++ b/hooks/post_gen_project.py @@ -16,7 +16,7 @@ def copy_choosen_template_to_main_dir(used_template: str): if used_template not in TEMPLATES: raise ValueError(f"{used_template} not in {TEMPLATES}") - copytree(used_template, "./") + copytree(used_template, "./", dirs_exist_ok=True) for template in TEMPLATES: rmtree(Path(template)) From c600d3aa284565ab452f75baaa96a243979fb268 Mon Sep 17 00:00:00 2001 From: rafsaf Date: Sun, 30 Jan 2022 01:15:58 +0100 Subject: [PATCH 07/11] change python-version in workflow --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 048b6fc..295b924 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - python-version: [3.9, 3.10] + python-version: ["3.9", "3.10"] services: postgres: From 064fdd2ad6dd62fbb2c683d0eafbe0109307ddb2 Mon Sep 17 00:00:00 2001 From: rafsaf Date: Sun, 30 Jan 2022 01:30:53 +0100 Subject: [PATCH 08/11] fix workflow typo --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 295b924..fedd6c2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -56,7 +56,7 @@ jobs: run: | python -m venv .venv2 source .venv2/bin/activate - pip install -r {{cookiecutter.project_name}}/template_minimal/requirements-dev.txt + pip install -r {{cookiecutter.project_name}}/template_fastapi_users/requirements-dev.txt pip install cookiecutter - name: Lint with flake8 minimal project run: | From 2984b463644087bce32cc84d525a67a02e4af52c Mon Sep 17 00:00:00 2001 From: rafsaf Date: Sun, 30 Jan 2022 13:30:01 +0100 Subject: [PATCH 09/11] fix post_gen_project hook --- hooks/post_gen_project.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py index e9463d6..c0a6c1c 100644 --- a/hooks/post_gen_project.py +++ b/hooks/post_gen_project.py @@ -8,7 +8,7 @@ from shutil import copytree, rmtree PROJECT_NAME = "{{ cookiecutter.project_name }}" -USE_FASTAPI_USERS = bool("{{ cookiecutter.use_fastapi_users }}") +USE_FASTAPI_USERS = "{{ cookiecutter.use_fastapi_users }}" TEMPLATES = ["template_fastapi_users", "template_minimal"] @@ -31,10 +31,14 @@ def create_env_file_and_remove_env_template(): if __name__ == "__main__": - if USE_FASTAPI_USERS: + truthy = ["T", "t", "true", "True", 1] + falsy = ["F", "f", "false", "False", 0] + if USE_FASTAPI_USERS in truthy: used_template = "template_fastapi_users" - else: + elif USE_FASTAPI_USERS in falsy: used_template = "template_minimal" + else: + raise ValueError(f"use_fastapi_users param must be in {truthy + falsy}") copy_choosen_template_to_main_dir(used_template=used_template) create_env_file_and_remove_env_template() From af6cee21e404c76505f4a70cb50cae55104416d8 Mon Sep 17 00:00:00 2001 From: rafsaf Date: Sun, 30 Jan 2022 14:50:44 +0100 Subject: [PATCH 10/11] some change in readme, added new images, small fix in conftest in fastapi-user template --- README.md | 22 +++++++++++++++--- docs/OpenAPI_example.png | Bin 56570 -> 0 bytes ...template-fastapi-users-openapi-example.png | Bin 0 -> 160836 bytes docs/template-minimal-openapi-example.png | Bin 0 -> 130457 bytes .../template_fastapi_users/app/conftest.py | 2 -- 5 files changed, 19 insertions(+), 5 deletions(-) delete mode 100644 docs/OpenAPI_example.png create mode 100644 docs/template-fastapi-users-openapi-example.png create mode 100644 docs/template-minimal-openapi-example.png diff --git a/README.md b/README.md index f082bf1..17a8b4c 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,15 @@ Test - - License + + License +:information_source: This repository contains two different templates to choose from. + ## Minimal async FastAPI + postgresql template -![OpenAPIexample](./docs/OpenAPI_example.png) +![template-fastapi-minimal-openapi-example](./docs/template-minimal-openapi-example.png) - SQLAlchemy using new 2.0 API + async queries - Postgresql database under `asyncpg` @@ -20,6 +22,20 @@ - `pre-push.sh` script with poetry export, autoflake, black, isort and flake8 - Setup for async tests, one func test for token flow and very extensible `conftest.py` +## Async FastAPI + postgresql template based on [fastapi-users](https://fastapi-users.github.io/fastapi-users/) + +- SQLAlchemy using new 2.0 API + async queries +- Postgresql database under `asyncpg` +- Alembic migrations +- Very minimal project structure yet ready for quick start building new api +- Refresh token endpoint (not only access like in official template) +- Two databases in docker-compose.yml (second one for tests) +- poetry +- `pre-push.sh` script with poetry export, autoflake, black, isort and flake8 +- Setup for async tests, one func test for token flow and very extensible `conftest.py` + +![template-fastapi-users-openapi-example](./docs/template-fastapi-users-openapi-example.png) + ## What this repo is This is a minimal template for FastAPI backend + postgresql db as of 2021.11, `async` style for database sessions, endpoints and tests. It provides basic codebase that almost every application has, but nothing more. diff --git a/docs/OpenAPI_example.png b/docs/OpenAPI_example.png deleted file mode 100644 index 9add209d8a07228c4ebe63cf2b251fa36a86dbf3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 56570 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 z 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