From 835d9e29bc5728e1636ac58d39e0a82891412b54 Mon Sep 17 00:00:00 2001 From: rafsaf Date: Wed, 4 May 2022 00:36:51 +0200 Subject: [PATCH 01/20] move to python3.10, upgrades, refactor sqlalchemy models and Dockerfile --- .../template_minimal/Dockerfile | 22 +- .../template_minimal/alembic.ini | 2 +- ... 20220503_init_user_table_2dba36457121.py} | 14 +- .../template_minimal/app/api/deps.py | 13 +- .../app/api/endpoints/auth.py | 6 +- .../app/api/endpoints/users.py | 6 +- .../template_minimal/app/core/security.py | 45 +- .../template_minimal/app/initial_data.py | 2 + .../template_minimal/app/main.py | 4 +- .../template_minimal/app/models.py | 27 +- .../template_minimal/app/session.py | 6 +- .../template_minimal/app/tests/conftest.py | 13 +- .../template_minimal/docker-compose.local.yml | 35 + .../template_minimal/docker-compose.yml | 2 +- .../template_minimal/nginx-unit-config.json | 2 +- .../template_minimal/poetry.lock | 730 ++++++++---------- .../template_minimal/pyproject.toml | 47 +- .../template_minimal/requirements-dev.txt | 83 +- .../template_minimal/requirements.txt | 43 +- 19 files changed, 538 insertions(+), 564 deletions(-) rename {{cookiecutter.project_name}}/template_minimal/alembic/versions/{2021_11_09_1736_init__cefce371682e.py => 20220503_init_user_table_2dba36457121.py} (78%) create mode 100644 {{cookiecutter.project_name}}/template_minimal/docker-compose.local.yml diff --git a/{{cookiecutter.project_name}}/template_minimal/Dockerfile b/{{cookiecutter.project_name}}/template_minimal/Dockerfile index 1461692..f63310d 100644 --- a/{{cookiecutter.project_name}}/template_minimal/Dockerfile +++ b/{{cookiecutter.project_name}}/template_minimal/Dockerfile @@ -1,10 +1,10 @@ # See https://unit.nginx.org/installation/#docker-images -FROM nginx/unit:1.26.1-python3.9 +FROM nginx/unit:1.26.1-python3.10 ENV PYTHONUNBUFFERED 1 -RUN apt update && apt install -y python3-pip +RUN apt-get update && apt-get install -y python3-pip # Build folder for our app, only stuff that matters copied. RUN mkdir build @@ -13,17 +13,19 @@ 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 \ +RUN pip install -r requirements.txt + +RUN apt-get remove -y python3-pip \ + && apt-get 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 . +COPY app ./app +COPY alembic ./alembic +COPY alembic.ini . +COPY pyproject.toml . # 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 +COPY 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 index 61630d3..d88ed94 100644 --- a/{{cookiecutter.project_name}}/template_minimal/alembic.ini +++ b/{{cookiecutter.project_name}}/template_minimal/alembic.ini @@ -5,7 +5,7 @@ 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 +file_template = %%(year)d%%(month).2d%%(day).2d_%%(slug)s_%%(rev)s # sys.path path, will be prepended to sys.path if present. # defaults to the current working directory. 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/20220503_init_user_table_2dba36457121.py similarity index 78% rename from {{cookiecutter.project_name}}/template_minimal/alembic/versions/2021_11_09_1736_init__cefce371682e.py rename to {{cookiecutter.project_name}}/template_minimal/alembic/versions/20220503_init_user_table_2dba36457121.py index bae5d4b..f763773 100644 --- a/{{cookiecutter.project_name}}/template_minimal/alembic/versions/2021_11_09_1736_init__cefce371682e.py +++ b/{{cookiecutter.project_name}}/template_minimal/alembic/versions/20220503_init_user_table_2dba36457121.py @@ -1,8 +1,8 @@ -"""init +"""init_user_table -Revision ID: cefce371682e +Revision ID: 2dba36457121 Revises: -Create Date: 2021-11-09 17:36:41.970204 +Create Date: 2022-05-03 22:51:45.832789 """ from alembic import op @@ -10,7 +10,7 @@ # revision identifiers, used by Alembic. -revision = 'cefce371682e' +revision = '2dba36457121' down_revision = None branch_labels = None depends_on = None @@ -20,19 +20,17 @@ 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.Column('full_name', sa.String(length=254), nullable=True), 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) + op.create_index(op.f('ix_user_email'), 'user', ['email'], 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/api/deps.py b/{{cookiecutter.project_name}}/template_minimal/app/api/deps.py index 6a428fc..6548749 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/api/deps.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/api/deps.py @@ -1,8 +1,8 @@ from typing import AsyncGenerator, Optional +import jwt 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 @@ -29,15 +29,20 @@ async def get_current_user( token, config.settings.SECRET_KEY, algorithms=[security.ALGORITHM] ) token_data = schemas.TokenPayload(**payload) - except (jwt.JWTError, ValidationError): + except (jwt.DecodeError, ValidationError): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail="Could not validate credentials", + detail="Could not validate credentials.", + ) + if token_data.refresh: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Cannot use refresh token.", ) 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") + raise HTTPException(status_code=404, detail="User not found.") return user 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 287e663..082e1ec 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/auth.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/auth.py @@ -1,8 +1,8 @@ from typing import Optional +import jwt 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 @@ -28,7 +28,7 @@ async def login_access_token( 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 + if not security.verify_password(form_data.password, user.hashed_password): raise HTTPException(status_code=400, detail="Incorrect email or password") access_token, expire_at = security.create_access_token(user.id) @@ -64,7 +64,7 @@ async def refresh_token( algorithms=[security.ALGORITHM], ) token_data = schemas.TokenPayload(**payload) - except (jwt.JWTError, ValidationError): + except (jwt.DecodeError, ValidationError): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Could not validate credentials", diff --git a/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/users.py b/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/users.py index 8b9bc09..c26a616 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/users.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/users.py @@ -18,11 +18,11 @@ async def update_user_me( Update current user. """ if user_update.password is not None: - current_user.hashed_password = get_password_hash(user_update.password) # type: ignore + current_user.hashed_password = get_password_hash(user_update.password) if user_update.full_name is not None: - current_user.full_name = user_update.full_name # type: ignore + current_user.full_name = user_update.full_name if user_update.email is not None: - current_user.email = user_update.email # type: ignore + current_user.email = user_update.email session.add(current_user) await session.commit() diff --git a/{{cookiecutter.project_name}}/template_minimal/app/core/security.py b/{{cookiecutter.project_name}}/template_minimal/app/core/security.py index de362f6..07b8717 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/core/security.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/core/security.py @@ -1,13 +1,8 @@ -""" -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. -""" +"""Black-box security shortcuts to generate JWT tokens and password hashing and verifcation.""" from datetime import datetime, timedelta -from typing import Any, Union -from jose import jwt +import jwt from passlib.context import CryptContext from app.core import config @@ -22,35 +17,63 @@ ALGORITHM = "HS256" -def create_access_token(subject: Union[str, Any]) -> tuple[str, datetime]: +def create_access_token(subject: str | int) -> tuple[str, datetime]: + """Creates jwt access token for user. + + Returns tuple with generated token and expire datetime. + expire minutes may be set using ACCESS_TOKEN_EXPIRE_MINUTES. + + Args: + subject: anything unique to user, eg. id, email. + """ + 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, + 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: str | int) -> tuple[str, datetime]: + """Creates jwt refresh token for user. + + Returns tuple with generated token and expire datetime. + expire minutes may be set using REFRESH_TOKEN_EXPIRE_MINUTES. + + Args: + subject: anything unique to user, eg. id, email. + """ + 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, + key=config.settings.SECRET_KEY, algorithm=ALGORITHM, ) return encoded_jwt, expire def verify_password(plain_password: str, hashed_password: str) -> bool: + """Verifie plain and hashed password matches + + Applies passlib context based on bcrypt algorithm on plain passoword. + It takes about 0.3s for default 12 rounds of SECURITY_BCRYPT_DEFAULT_ROUNDS. + """ return pwd_context.verify(plain_password, hashed_password) def get_password_hash(password: str) -> str: + """Create hash from password + + Applies passlib context based on bcrypt algorithm on plain passoword. + It takes about 0.3s for default 12 rounds of SECURITY_BCRYPT_DEFAULT_ROUNDS. + """ 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 index 227092d..14da2e0 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/initial_data.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/initial_data.py @@ -9,6 +9,7 @@ from typing import Optional from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession from app.core import config, security from app.models import User @@ -18,6 +19,7 @@ async def main() -> None: print("Start initial data") async with async_session() as session: + session: AsyncSession # resolves type issues with async_session result = await session.execute( select(User).where(User.email == config.settings.FIRST_SUPERUSER_EMAIL) diff --git a/{{cookiecutter.project_name}}/template_minimal/app/main.py b/{{cookiecutter.project_name}}/template_minimal/app/main.py index 0892233..6a51dad 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/main.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/main.py @@ -1,6 +1,4 @@ -""" -Main FastAPI app instance declaration -""" +"""Main FastAPI app instance declaration.""" from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware diff --git a/{{cookiecutter.project_name}}/template_minimal/app/models.py b/{{cookiecutter.project_name}}/template_minimal/app/models.py index 2b7eb82..349e3e3 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/models.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/models.py @@ -1,20 +1,29 @@ """ SQL Alchemy models declaration. +https://docs.sqlalchemy.org/en/14/orm/declarative_styles.html#example-two-dataclasses-with-declarative-table +Dataclass style for powerful autocompletion support. + +Note, it is used by alembic migrations logic, see `alembic/env.py` -Note, imported by alembic migrations logic, see `alembic/env.py` """ -from typing import Any, cast +from dataclasses import dataclass, field from sqlalchemy import Column, Integer, String -from sqlalchemy.orm.decl_api import declarative_base +from sqlalchemy.orm import registry -Base = cast(Any, declarative_base()) +Base = registry() -class User(Base): +@Base.mapped +@dataclass +class User: __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) + __sa_dataclass_metadata_key__ = "sa" + + id: int = field(init=False, metadata={"sa": Column(Integer, primary_key=True)}) + email: str = field(metadata={"sa": Column(String(254), nullable=False, index=True)}) + hashed_password: str = field(metadata={"sa": Column(String(128), nullable=False)}) + full_name: str | None = field( + default=None, metadata={"sa": Column(String(254), nullable=True)} + ) diff --git a/{{cookiecutter.project_name}}/template_minimal/app/session.py b/{{cookiecutter.project_name}}/template_minimal/app/session.py index 5b27e40..86fd7f8 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/session.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/session.py @@ -1,3 +1,5 @@ +"""SQLAlchemy async engine and sessions tools""" + from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine from sqlalchemy.orm.session import sessionmaker @@ -9,4 +11,6 @@ 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) + + +async_session = sessionmaker(async_engine, expire_on_commit=False, class_=AsyncSession) # type: ignore diff --git a/{{cookiecutter.project_name}}/template_minimal/app/tests/conftest.py b/{{cookiecutter.project_name}}/template_minimal/app/tests/conftest.py index af40fc0..0755ad6 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/tests/conftest.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/tests/conftest.py @@ -1,7 +1,7 @@ import asyncio from typing import AsyncGenerator, Optional -import pytest +import pytest, pytest_asyncio from httpx import AsyncClient from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession @@ -17,18 +17,19 @@ @pytest.fixture(scope="session") def event_loop(): - loop = asyncio.get_event_loop() + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) yield loop loop.close() -@pytest.fixture(scope="session") +@pytest_asyncio.fixture() async def client(): async with AsyncClient(app=app, base_url="http://test") as client: yield client -@pytest.fixture(scope="session") +@pytest_asyncio.fixture(scope="session") async def test_db_setup_sessionmaker(): # assert if we use TEST_DB URL for 100% assert config.settings.ENVIRONMENT == "PYTEST" @@ -42,13 +43,13 @@ async def test_db_setup_sessionmaker(): return async_session -@pytest.fixture +@pytest_asyncio.fixture async def session(test_db_setup_sessionmaker) -> AsyncGenerator[AsyncSession, None]: async with test_db_setup_sessionmaker() as session: yield session -@pytest.fixture +@pytest_asyncio.fixture async def default_user(session: AsyncSession): result = await session.execute(select(User).where(User.email == default_user_email)) user: Optional[User] = result.scalars().first() diff --git a/{{cookiecutter.project_name}}/template_minimal/docker-compose.local.yml b/{{cookiecutter.project_name}}/template_minimal/docker-compose.local.yml new file mode 100644 index 0000000..998c19c --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/docker-compose.local.yml @@ -0,0 +1,35 @@ +version: "3.7" + +# Database + Webserver (under http, for testing setup) +# +# docker-compose up -f docker-compose.local.yml -d +# + +services: + postgres: + restart: unless-stopped + image: postgres:latest + volumes: + - postgres_data:/var/lib/postgresql/data + env_file: + - .env + environment: + - POSTGRES_DB=${DEFAULT_DATABASE_DB} + - POSTGRES_USER=${DEFAULT_DATABASE_USER} + - POSTGRES_PASSWORD=${DEFAULT_DATABASE_PASSWORD} + web: + depends_on: + - postgres + restart: "unless-stopped" + build: + context: ./ + dockerfile: Dockerfile + env_file: + - .env + environment: + - DEFAULT_DATABASE_HOSTNAME=postgres + ports: + - 80:80 + +volumes: + postgres_data: diff --git a/{{cookiecutter.project_name}}/template_minimal/docker-compose.yml b/{{cookiecutter.project_name}}/template_minimal/docker-compose.yml index 4f33f36..320b63c 100644 --- a/{{cookiecutter.project_name}}/template_minimal/docker-compose.yml +++ b/{{cookiecutter.project_name}}/template_minimal/docker-compose.yml @@ -1,4 +1,4 @@ -version: "3.3" +version: "3.7" # For local development, only database is running # diff --git a/{{cookiecutter.project_name}}/template_minimal/nginx-unit-config.json b/{{cookiecutter.project_name}}/template_minimal/nginx-unit-config.json index dda3653..968377f 100644 --- a/{{cookiecutter.project_name}}/template_minimal/nginx-unit-config.json +++ b/{{cookiecutter.project_name}}/template_minimal/nginx-unit-config.json @@ -6,7 +6,7 @@ }, "applications": { "fastapi": { - "type": "python 3.9", + "type": "python 3.10", "processes": 1, "threads": 1, "path": "/build/", diff --git a/{{cookiecutter.project_name}}/template_minimal/poetry.lock b/{{cookiecutter.project_name}}/template_minimal/poetry.lock index 91d42db..97c0a12 100644 --- a/{{cookiecutter.project_name}}/template_minimal/poetry.lock +++ b/{{cookiecutter.project_name}}/template_minimal/poetry.lock @@ -1,6 +1,6 @@ [[package]] name = "alembic" -version = "1.7.5" +version = "1.7.7" description = "A database migration tool for SQLAlchemy." category = "main" optional = false @@ -15,7 +15,7 @@ tz = ["python-dateutil"] [[package]] name = "anyio" -version = "3.4.0" +version = "3.5.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" category = "main" optional = false @@ -26,17 +26,17 @@ idna = ">=2.8" sniffio = ">=1.1" [package.extras] -doc = ["sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] +doc = ["packaging", "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" +version = "3.5.1" description = "ASGI specs, helper code, and adapters" -category = "main" +category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.extras] tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] @@ -89,7 +89,7 @@ pyflakes = ">=1.1.0" [[package]] name = "bcrypt" -version = "3.2.0" +version = "3.2.2" description = "Modern password hashing for your software and your servers" category = "main" optional = false @@ -97,7 +97,6 @@ python-versions = ">=3.6" [package.dependencies] cffi = ">=1.1" -six = ">=1.4.1" [package.extras] tests = ["pytest (>=3.2.1,!=3.3.0)"] @@ -105,35 +104,30 @@ typecheck = ["mypy"] [[package]] name = "black" -version = "21.12b0" +version = "22.3.0" description = "The uncompromising code formatter." category = "dev" optional = false python-versions = ">=3.6.2" [package.dependencies] -click = ">=7.1.2" +click = ">=8.0.0" mypy-extensions = ">=0.4.3" -pathspec = ">=0.9.0,<1" +pathspec = ">=0.9.0" platformdirs = ">=2" -tomli = ">=0.2.6,<2.0.0" -typing-extensions = [ - {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}, - {version = "!=3.10.0.1", markers = "python_version >= \"3.10\""}, -] +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] d = ["aiohttp (>=3.7.4)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -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" +category = "dev" optional = false python-versions = "*" @@ -150,9 +144,9 @@ pycparser = "*" [[package]] name = "charset-normalizer" -version = "2.0.10" +version = "2.0.12" 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" @@ -161,11 +155,11 @@ unicode_backport = ["unicodedata2"] [[package]] name = "click" -version = "8.0.3" +version = "8.1.3" description = "Composable command line interface toolkit" -category = "main" +category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} @@ -174,24 +168,24 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} 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.*" [[package]] name = "coverage" -version = "6.2" +version = "6.3.2" description = "Code coverage measurement for Python" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.extras] toml = ["tomli"] [[package]] name = "cryptography" -version = "36.0.1" +version = "37.0.1" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false @@ -206,42 +200,28 @@ docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling 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)"] +test = ["pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] [[package]] name = "dnspython" -version = "2.1.0" +version = "2.2.1" description = "DNS toolkit" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.6,<4.0" [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"] +dnssec = ["cryptography (>=2.6,<37.0)"] +curio = ["curio (>=1.2,<2.0)", "sniffio (>=1.1,<2.0)"] +doh = ["h2 (>=4.1.0)", "httpx (>=0.21.1)", "requests (>=2.23.0,<3.0.0)", "requests-toolbelt (>=0.9.1,<0.10.0)"] +idna = ["idna (>=2.1,<4.0)"] +trio = ["trio (>=0.14,<0.20)"] +wmi = ["wmi (>=1.5.1,<2.0.0)"] [[package]] name = "email-validator" -version = "1.1.3" -description = "A robust email syntax and deliverability validation library for Python 2.x/3.x." +version = "1.2.1" +description = "A robust email syntax and deliverability validation library." category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" @@ -252,7 +232,7 @@ idna = ">=2.0.0" [[package]] name = "fastapi" -version = "0.71.0" +version = "0.75.2" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" category = "main" optional = false @@ -263,10 +243,10 @@ pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1. 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)"] +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,<7.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)", "orjson (>=3.2.1,<4.0.0)", "email_validator (>=1.1.1,<2.0.0)", "uvicorn[standard] (>=0.12.0,<0.18.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.18.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 (>=0.4.1,<0.5.0)", "pyyaml (>=5.3.1,<7.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 (==22.3.0)", "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,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.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 (==4.2.1)", "types-orjson (==3.6.2)", "types-dataclasses (==0.6.5)"] [[package]] name = "flake8" @@ -296,13 +276,13 @@ 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" [[package]] name = "httpcore" -version = "0.14.4" +version = "0.14.7" description = "A minimal low-level HTTP client." category = "dev" optional = false @@ -316,10 +296,11 @@ sniffio = ">=1.0.0,<2.0.0" [package.extras] http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] [[package]] name = "httpx" -version = "0.21.3" +version = "0.22.0" description = "The next generation HTTP client." category = "dev" optional = false @@ -328,7 +309,7 @@ python-versions = ">=3.6" [package.dependencies] certifi = "*" charset-normalizer = "*" -httpcore = ">=0.14.0,<0.15.0" +httpcore = ">=0.14.5,<0.15.0" rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} sniffio = "*" @@ -336,6 +317,7 @@ sniffio = "*" 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)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] [[package]] name = "idna" @@ -369,11 +351,11 @@ 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." +version = "1.2.0" +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.*" +python-versions = ">=3.7" [package.dependencies] MarkupSafe = ">=0.9.2" @@ -381,14 +363,15 @@ MarkupSafe = ">=0.9.2" [package.extras] babel = ["babel"] lingua = ["lingua"] +testing = ["pytest"] [[package]] name = "markupsafe" -version = "2.0.1" +version = "2.1.1" description = "Safely add untrusted strings to HTML/XML markup." category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "mccabe" @@ -444,15 +427,15 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [[package]] name = "platformdirs" -version = "2.4.1" +version = "2.5.2" 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)"] +docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] +test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] [[package]] name = "pluggy" @@ -474,14 +457,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" @@ -508,6 +483,7 @@ python-versions = ">=3.6.1" [package.dependencies] email-validator = {version = ">=1.0.3", optional = true, markers = "extra == \"email\""} +python-dotenv = {version = ">=0.10.4", optional = true, markers = "extra == \"dotenv\""} typing-extensions = ">=3.7.4.3" [package.extras] @@ -522,24 +498,41 @@ 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.dependencies] +cryptography = {version = ">=3.3.1", optional = true, markers = "extra == \"crypto\""} + +[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" -description = "Python parsing module" +version = "3.0.8" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.6.8" [package.extras] -diagrams = ["jinja2", "railroad-diagrams"] +diagrams = ["railroad-diagrams", "jinja2"] [[package]] name = "pytest" -version = "6.2.5" +version = "7.1.2" description = "pytest: simple powerful testing with Python" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} @@ -549,28 +542,28 @@ iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" py = ">=1.8.2" -toml = "*" +tomli = ">=1.0.0" [package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] [[package]] name = "pytest-asyncio" -version = "0.16.0" -description = "Pytest support for asyncio." +version = "0.18.3" +description = "Pytest support for asyncio" category = "dev" optional = false -python-versions = ">= 3.6" +python-versions = ">=3.7" [package.dependencies] -pytest = ">=5.4.0" +pytest = ">=6.1.0" [package.extras] -testing = ["coverage", "hypothesis (>=5.7.1)"] +testing = ["coverage (==6.2)", "hypothesis (>=5.7.1)", "flaky (>=3.5.0)", "mypy (==0.931)", "pytest-trio (>=0.7.0)"] [[package]] name = "python-dotenv" -version = "0.19.2" +version = "0.20.0" description = "Read key-value pairs from a .env file and set them as environment variables" category = "main" optional = false @@ -579,25 +572,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" @@ -609,24 +583,6 @@ 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" @@ -641,17 +597,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" @@ -670,7 +615,7 @@ python-versions = ">=3.5" [[package]] name = "sqlalchemy" -version = "1.4.29" +version = "1.4.36" description = "Database Abstraction Library" category = "main" optional = false @@ -683,7 +628,7 @@ greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platfo 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)"] +asyncmy = ["greenlet (!=0.4.17)", "asyncmy (>=0.2.3,!=0.2.4)"] mariadb_connector = ["mariadb (>=1.0.1)"] mssql = ["pyodbc"] mssql_pymssql = ["pymssql"] @@ -702,7 +647,7 @@ sqlcipher = ["sqlcipher3-binary"] [[package]] name = "sqlalchemy2-stubs" -version = "0.0.2a19" +version = "0.0.2a22" description = "Typing Stubs for SQLAlchemy 1.4" category = "dev" optional = false @@ -729,46 +674,33 @@ full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests"] name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" +category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "tomli" -version = "1.2.3" +version = "2.0.1" description = "A lil' TOML parser" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "typing-extensions" -version = "4.0.1" -description = "Backported and Experimental Type Hints for Python 3.6+" +version = "4.2.0" +description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false -python-versions = ">=3.6" - -[[package]] -name = "urllib3" -version = "1.26.8" -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)"] +python-versions = ">=3.7" [[package]] name = "uvicorn" -version = "0.16.0" +version = "0.17.6" description = "The lightning-fast ASGI server." -category = "main" +category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.7" [package.dependencies] asgiref = ">=3.4.0" @@ -776,25 +708,25 @@ click = ">=7.0" h11 = ">=0.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)"] +standard = ["websockets (>=10.0)", "httptools (>=0.4.0)", "watchgod (>=0.6)", "python-dotenv (>=0.13)", "PyYAML (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "colorama (>=0.4)"] [metadata] lock-version = "1.1" -python-versions = "^3.9" -content-hash = "fb4d1a3aee0c00c3559d6367dd9094364793c22c17fc37524063a6a8ed56d69f" +python-versions = "^3.10" +content-hash = "ae84c41cab5ab2894820354794e37071b4fec21a2e3107d13c3ef88e030eb7ea" [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"}, + {file = "alembic-1.7.7-py3-none-any.whl", hash = "sha256:29be0856ec7591c39f4e1cb10f198045d890e6e2274cf8da80cb5e721a09642b"}, + {file = "alembic-1.7.7.tar.gz", hash = "sha256:4961248173ead7ce8a21efb3de378f13b8398e6630fab0eb258dc74a8af24c58"}, ] anyio = [ - {file = "anyio-3.4.0-py3-none-any.whl", hash = "sha256:2855a9423524abcdd652d942f8932fda1735210f77a6b392eafd9ff34d3fe020"}, - {file = "anyio-3.4.0.tar.gz", hash = "sha256:24adc69309fb5779bc1e06158e143e0b6d2c56b302a3ac3de3083c705a6ed39d"}, + {file = "anyio-3.5.0-py3-none-any.whl", hash = "sha256:b5fa16c5ff93fa1046f2eeb5bbff2dad4d3514d6cda61d02816dba34fa8c3c2e"}, + {file = "anyio-3.5.0.tar.gz", hash = "sha256:a0aeffe2fb1fdf374a8e4b471444f0f3ac4fb9f5a5b542b48824475e0042a5a6"}, ] asgiref = [ - {file = "asgiref-3.4.1-py3-none-any.whl", hash = "sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214"}, - {file = "asgiref-3.4.1.tar.gz", hash = "sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9"}, + {file = "asgiref-3.5.1-py3-none-any.whl", hash = "sha256:45a429524fba18aba9d512498b19d220c4d628e75b40cf5c627524dbaebc5cc1"}, + {file = "asgiref-3.5.1.tar.gz", hash = "sha256:fddeea3c53fa99d0cdb613c3941cc6e52d822491fc2753fba25768fb5bf4e865"}, ] asyncpg = [ {file = "asyncpg-0.25.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bf5e3408a14a17d480f36ebaf0401a12ff6ae5457fdf45e4e2775c51cc9517d3"}, @@ -836,17 +768,42 @@ 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"}, + {file = "bcrypt-3.2.2-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:7180d98a96f00b1050e93f5b0f556e658605dd9f524d0b0e68ae7944673f525e"}, + {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:61bae49580dce88095d669226d5076d0b9d927754cedbdf76c6c9f5099ad6f26"}, + {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88273d806ab3a50d06bc6a2fc7c87d737dd669b76ad955f449c43095389bc8fb"}, + {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6d2cb9d969bfca5bc08e45864137276e4c3d3d7de2b162171def3d188bf9d34a"}, + {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b02d6bfc6336d1094276f3f588aa1225a598e27f8e3388f4db9948cb707b521"}, + {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a2c46100e315c3a5b90fdc53e429c006c5f962529bc27e1dfd656292c20ccc40"}, + {file = "bcrypt-3.2.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7d9ba2e41e330d2af4af6b1b6ec9e6128e91343d0b4afb9282e54e5508f31baa"}, + {file = "bcrypt-3.2.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cd43303d6b8a165c29ec6756afd169faba9396a9472cdff753fe9f19b96ce2fa"}, + {file = "bcrypt-3.2.2-cp36-abi3-win32.whl", hash = "sha256:4e029cef560967fb0cf4a802bcf4d562d3d6b4b1bf81de5ec1abbe0f1adb027e"}, + {file = "bcrypt-3.2.2-cp36-abi3-win_amd64.whl", hash = "sha256:7ff2069240c6bbe49109fe84ca80508773a904f5a8cb960e02a977f7f519b129"}, + {file = "bcrypt-3.2.2.tar.gz", hash = "sha256:433c410c2177057705da2a9f2cd01dd157493b2a7ac14c8593a16b3dab6b6bfb"}, ] black = [ - {file = "black-21.12b0-py3-none-any.whl", hash = "sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f"}, - {file = "black-21.12b0.tar.gz", hash = "sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3"}, + {file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"}, + {file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"}, + {file = "black-22.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a"}, + {file = "black-22.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968"}, + {file = "black-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"}, + {file = "black-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce"}, + {file = "black-22.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82"}, + {file = "black-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b"}, + {file = "black-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015"}, + {file = "black-22.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b"}, + {file = "black-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a"}, + {file = "black-22.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163"}, + {file = "black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464"}, + {file = "black-22.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0"}, + {file = "black-22.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176"}, + {file = "black-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0"}, + {file = "black-22.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20"}, + {file = "black-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a"}, + {file = "black-22.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad"}, + {file = "black-22.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21"}, + {file = "black-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265"}, + {file = "black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72"}, + {file = "black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79"}, ] certifi = [ {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, @@ -905,103 +862,95 @@ cffi = [ {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"}, ] charset-normalizer = [ - {file = "charset-normalizer-2.0.10.tar.gz", hash = "sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd"}, - {file = "charset_normalizer-2.0.10-py3-none-any.whl", hash = "sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455"}, + {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, + {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, ] click = [ - {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, - {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"}, + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, ] 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"}, + {file = "coverage-6.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b27d894748475fa858f9597c0ee1d4829f44683f3813633aaf94b19cb5453cf"}, + {file = "coverage-6.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37d1141ad6b2466a7b53a22e08fe76994c2d35a5b6b469590424a9953155afac"}, + {file = "coverage-6.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9987b0354b06d4df0f4d3e0ec1ae76d7ce7cbca9a2f98c25041eb79eec766f1"}, + {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:26e2deacd414fc2f97dd9f7676ee3eaecd299ca751412d89f40bc01557a6b1b4"}, + {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dd8bafa458b5c7d061540f1ee9f18025a68e2d8471b3e858a9dad47c8d41903"}, + {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:46191097ebc381fbf89bdce207a6c107ac4ec0890d8d20f3360345ff5976155c"}, + {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6f89d05e028d274ce4fa1a86887b071ae1755082ef94a6740238cd7a8178804f"}, + {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:58303469e9a272b4abdb9e302a780072c0633cdcc0165db7eec0f9e32f901e05"}, + {file = "coverage-6.3.2-cp310-cp310-win32.whl", hash = "sha256:2fea046bfb455510e05be95e879f0e768d45c10c11509e20e06d8fcaa31d9e39"}, + {file = "coverage-6.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:a2a8b8bcc399edb4347a5ca8b9b87e7524c0967b335fbb08a83c8421489ddee1"}, + {file = "coverage-6.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f1555ea6d6da108e1999b2463ea1003fe03f29213e459145e70edbaf3e004aaa"}, + {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5f4e1edcf57ce94e5475fe09e5afa3e3145081318e5fd1a43a6b4539a97e518"}, + {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a15dc0a14008f1da3d1ebd44bdda3e357dbabdf5a0b5034d38fcde0b5c234b7"}, + {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21b7745788866028adeb1e0eca3bf1101109e2dc58456cb49d2d9b99a8c516e6"}, + {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8ce257cac556cb03be4a248d92ed36904a59a4a5ff55a994e92214cde15c5bad"}, + {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b0be84e5a6209858a1d3e8d1806c46214e867ce1b0fd32e4ea03f4bd8b2e3359"}, + {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:acf53bc2cf7282ab9b8ba346746afe703474004d9e566ad164c91a7a59f188a4"}, + {file = "coverage-6.3.2-cp37-cp37m-win32.whl", hash = "sha256:8bdde1177f2311ee552f47ae6e5aa7750c0e3291ca6b75f71f7ffe1f1dab3dca"}, + {file = "coverage-6.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b31651d018b23ec463e95cf10070d0b2c548aa950a03d0b559eaa11c7e5a6fa3"}, + {file = "coverage-6.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07e6db90cd9686c767dcc593dff16c8c09f9814f5e9c51034066cad3373b914d"}, + {file = "coverage-6.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2c6dbb42f3ad25760010c45191e9757e7dce981cbfb90e42feef301d71540059"}, + {file = "coverage-6.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c76aeef1b95aff3905fb2ae2d96e319caca5b76fa41d3470b19d4e4a3a313512"}, + {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cf5cfcb1521dc3255d845d9dca3ff204b3229401994ef8d1984b32746bb45ca"}, + {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fbbdc8d55990eac1b0919ca69eb5a988a802b854488c34b8f37f3e2025fa90d"}, + {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ec6bc7fe73a938933d4178c9b23c4e0568e43e220aef9472c4f6044bfc6dd0f0"}, + {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9baff2a45ae1f17c8078452e9e5962e518eab705e50a0aa8083733ea7d45f3a6"}, + {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd9e830e9d8d89b20ab1e5af09b32d33e1a08ef4c4e14411e559556fd788e6b2"}, + {file = "coverage-6.3.2-cp38-cp38-win32.whl", hash = "sha256:f7331dbf301b7289013175087636bbaf5b2405e57259dd2c42fdcc9fcc47325e"}, + {file = "coverage-6.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:68353fe7cdf91f109fc7d474461b46e7f1f14e533e911a2a2cbb8b0fc8613cf1"}, + {file = "coverage-6.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b78e5afb39941572209f71866aa0b206c12f0109835aa0d601e41552f9b3e620"}, + {file = "coverage-6.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4e21876082ed887baed0146fe222f861b5815455ada3b33b890f4105d806128d"}, + {file = "coverage-6.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34626a7eee2a3da12af0507780bb51eb52dca0e1751fd1471d0810539cefb536"}, + {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ebf730d2381158ecf3dfd4453fbca0613e16eaa547b4170e2450c9707665ce7"}, + {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd6fe30bd519694b356cbfcaca9bd5c1737cddd20778c6a581ae20dc8c04def2"}, + {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:96f8a1cb43ca1422f36492bebe63312d396491a9165ed3b9231e778d43a7fca4"}, + {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:dd035edafefee4d573140a76fdc785dc38829fe5a455c4bb12bac8c20cfc3d69"}, + {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ca5aeb4344b30d0bec47481536b8ba1181d50dbe783b0e4ad03c95dc1296684"}, + {file = "coverage-6.3.2-cp39-cp39-win32.whl", hash = "sha256:f5fa5803f47e095d7ad8443d28b01d48c0359484fec1b9d8606d0e3282084bc4"}, + {file = "coverage-6.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:9548f10d8be799551eb3a9c74bbf2b4934ddb330e08a73320123c07f95cc2d92"}, + {file = "coverage-6.3.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf"}, + {file = "coverage-6.3.2.tar.gz", hash = "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9"}, ] 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"}, + {file = "cryptography-37.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:74b55f67f4cf026cb84da7a1b04fc2a1d260193d4ad0ea5e9897c8b74c1e76ac"}, + {file = "cryptography-37.0.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:0db5cf21bd7d092baacb576482b0245102cea2d3cf09f09271ce9f69624ecb6f"}, + {file = "cryptography-37.0.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:faf0f5456c059c7b1c29441bdd5e988f0ba75bdc3eea776520d8dcb1e30e1b5c"}, + {file = "cryptography-37.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:06bfafa6e53ccbfb7a94be4687b211a025ce0625e3f3c60bb15cd048a18f3ed8"}, + {file = "cryptography-37.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf585476fcbcd37bed08072e8e2db3954ce1bfc68087a2dc9c19cfe0b90979ca"}, + {file = "cryptography-37.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d4daf890e674d191757d8d7d60dc3a29c58c72c7a76a05f1c0a326013f47e8b"}, + {file = "cryptography-37.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:ae1cd29fbe6b716855454e44f4bf743465152e15d2d317303fe3b58ee9e5af7a"}, + {file = "cryptography-37.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:451aaff8b8adf2dd0597cbb1fdcfc8a7d580f33f843b7cce75307a7f20112dd8"}, + {file = "cryptography-37.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:1858eff6246bb8bbc080eee78f3dd1528739e3f416cba5f9914e8631b8df9871"}, + {file = "cryptography-37.0.1-cp36-abi3-win32.whl", hash = "sha256:e69a0e36e62279120e648e787b76d79b41e0f9e86c1c636a4f38d415595c722e"}, + {file = "cryptography-37.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:a18ff4bfa9d64914a84d7b06c46eb86e0cc03113470b3c111255aceb6dcaf81d"}, + {file = "cryptography-37.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cce90609e01e1b192fae9e13665058ab46b2ea53a3c05a3ea74a3eb8c3af8857"}, + {file = "cryptography-37.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:c4a58eeafbd7409054be41a377e726a7904a17c26f45abf18125d21b1215b08b"}, + {file = "cryptography-37.0.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:618391152147a1221c87b1b0b7f792cafcfd4b5a685c5c72eeea2ddd29aeceff"}, + {file = "cryptography-37.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ceae26f876aabe193b13a0c36d1bb8e3e7e608d17351861b437bd882f617e9f"}, + {file = "cryptography-37.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:930b829e8a2abaf43a19f38277ae3c5e1ffcf547b936a927d2587769ae52c296"}, + {file = "cryptography-37.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:58021d6e9b1d88b1105269d0da5e60e778b37dfc0e824efc71343dd003726831"}, + {file = "cryptography-37.0.1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b1ee5c82cf03b30f6ae4e32d2bcb1e167ef74d6071cbb77c2af30f101d0b360b"}, + {file = "cryptography-37.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f095988548ec5095e3750cdb30e6962273d239b1998ba1aac66c0d5bee7111c1"}, + {file = "cryptography-37.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:125702572be12bcd318e3a14e9e70acd4be69a43664a75f0397e8650fe3c6cc3"}, + {file = "cryptography-37.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:315af6268de72bcfa0bb3401350ce7d921f216e6b60de12a363dad128d9d459f"}, + {file = "cryptography-37.0.1.tar.gz", hash = "sha256:d610d0ee14dd9109006215c7c0de15eee91230b70a9bce2263461cf7c3720b83"}, ] 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"}, + {file = "dnspython-2.2.1-py3-none-any.whl", hash = "sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f"}, + {file = "dnspython-2.2.1.tar.gz", hash = "sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e"}, ] 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"}, + {file = "email_validator-1.2.1-py2.py3-none-any.whl", hash = "sha256:c8589e691cf73eb99eed8d10ce0e9cbb05a0886ba920c8bcb7c82873f4c5789c"}, + {file = "email_validator-1.2.1.tar.gz", hash = "sha256:6757aea012d40516357c0ac2b1a4c31219ab2f899d26831334c5d069e8b6c3d8"}, ] 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.75.2-py3-none-any.whl", hash = "sha256:a70d31f4249b6b42dbe267667d22f83af645b2d857876c97f83ca9573215784f"}, + {file = "fastapi-0.75.2.tar.gz", hash = "sha256:b5dac161ee19d33346040d3f44d8b7a9ac09b37df9efff95891f5e7641fa482f"}, ] flake8 = [ {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, @@ -1019,6 +968,7 @@ greenlet = [ {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-musllinux_1_1_x86_64.whl", hash = "sha256:b336501a05e13b616ef81ce329c0e09ac5ed8c732d9ba7e3e983fcc1a9e86965"}, {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"}, @@ -1031,6 +981,7 @@ greenlet = [ {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-musllinux_1_1_x86_64.whl", hash = "sha256:b8c008de9d0daba7b6666aa5bbfdc23dcd78cafc33997c9b7741ff6353bafb7f"}, {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"}, @@ -1039,6 +990,7 @@ greenlet = [ {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-musllinux_1_1_x86_64.whl", hash = "sha256:8c5d5b35f789a030ebb95bff352f1d27a93d81069f2adb3182d99882e095cefe"}, {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"}, @@ -1047,6 +999,7 @@ greenlet = [ {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-musllinux_1_1_x86_64.whl", hash = "sha256:2bde6792f313f4e918caabc46532aa64aa27a0db05d75b20edfc5c6f46479de2"}, {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"}, @@ -1055,6 +1008,7 @@ greenlet = [ {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-musllinux_1_1_x86_64.whl", hash = "sha256:0051c6f1f27cb756ffc0ffbac7d2cd48cb0362ac1736871399a739b2885134d3"}, {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"}, @@ -1064,12 +1018,12 @@ h11 = [ {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"}, + {file = "httpcore-0.14.7-py3-none-any.whl", hash = "sha256:47d772f754359e56dd9d892d9593b6f9870a37aeb8ba51e9a88b09b3d68cfade"}, + {file = "httpcore-0.14.7.tar.gz", hash = "sha256:7503ec1c0f559066e7e39bc4003fd2ce023d01cf51793e3c173b864eb456ead1"}, ] httpx = [ - {file = "httpx-0.21.3-py3-none-any.whl", hash = "sha256:df9a0fd43fa79dbab411d83eb1ea6f7a525c96ad92e60c2d7f40388971b25777"}, - {file = "httpx-0.21.3.tar.gz", hash = "sha256:7a3eb67ef0b8abbd6d9402248ef2f84a76080fa1c839f8662e6eb385640e445a"}, + {file = "httpx-0.22.0-py3-none-any.whl", hash = "sha256:e35e83d1d2b9b2a609ef367cc4c1e66fd80b750348b20cc9e19d1952fc2ca3f6"}, + {file = "httpx-0.22.0.tar.gz", hash = "sha256:d8e778f76d9bbd46af49e7f062467e3157a5a3d2ae4876a4bbfd8a51ed9c9cb4"}, ] idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, @@ -1084,64 +1038,50 @@ isort = [ {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"}, + {file = "Mako-1.2.0-py3-none-any.whl", hash = "sha256:23aab11fdbbb0f1051b93793a58323ff937e98e34aece1c4219675122e57e4ba"}, + {file = "Mako-1.2.0.tar.gz", hash = "sha256:9a7c7e922b87db3686210cf49d5d767033a41d4010b284e747682c92bddd8b39"}, ] 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"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, + {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, ] mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, @@ -1164,8 +1104,8 @@ pathspec = [ {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"}, + {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, + {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, ] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, @@ -1175,21 +1115,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"}, @@ -1239,41 +1164,34 @@ 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"}, + {file = "pyparsing-3.0.8-py3-none-any.whl", hash = "sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06"}, + {file = "pyparsing-3.0.8.tar.gz", hash = "sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954"}, ] pytest = [ - {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, - {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, + {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"}, + {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"}, ] 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"}, + {file = "pytest-asyncio-0.18.3.tar.gz", hash = "sha256:7659bdb0a9eb9c6e3ef992eef11a2b3e69697800ad02fb06374a210d85b29f91"}, + {file = "pytest_asyncio-0.18.3-1-py3-none-any.whl", hash = "sha256:16cf40bdf2b4fb7fc8e4b82bd05ce3fbcd454cbf7b92afc445fe299dabb88213"}, + {file = "pytest_asyncio-0.18.3-py3-none-any.whl", hash = "sha256:8fafa6c52161addfd41ee7ab35f11836c5a16ec208f93ee388f752bea3493a84"}, ] 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"}, + {file = "python-dotenv-0.20.0.tar.gz", hash = "sha256:b7e3b04a59693c42c36f9ab1cc2acc46fa5df8c78e178fc33a8d4cd05c8d498f"}, + {file = "python_dotenv-0.20.0-py3-none-any.whl", hash = "sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938"}, ] 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"}, @@ -1283,46 +1201,46 @@ sniffio = [ {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"}, + {file = "SQLAlchemy-1.4.36-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:81e53bd383c2c33de9d578bfcc243f559bd3801a0e57f2bcc9a943c790662e0c"}, + {file = "SQLAlchemy-1.4.36-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6e1fe00ee85c768807f2a139b83469c1e52a9ffd58a6eb51aa7aeb524325ab18"}, + {file = "SQLAlchemy-1.4.36-cp27-cp27m-win32.whl", hash = "sha256:d57ac32f8dc731fddeb6f5d1358b4ca5456e72594e664769f0a9163f13df2a31"}, + {file = "SQLAlchemy-1.4.36-cp27-cp27m-win_amd64.whl", hash = "sha256:fca8322e04b2dde722fcb0558682740eebd3bd239bea7a0d0febbc190e99dc15"}, + {file = "SQLAlchemy-1.4.36-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:53d2d9ee93970c969bc4e3c78b1277d7129554642f6ffea039c282c7dc4577bc"}, + {file = "SQLAlchemy-1.4.36-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:f0394a3acfb8925db178f7728adb38c027ed7e303665b225906bfa8099dc1ce8"}, + {file = "SQLAlchemy-1.4.36-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09c606d8238feae2f360b8742ffbe67741937eb0a05b57f536948d198a3def96"}, + {file = "SQLAlchemy-1.4.36-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8d07fe2de0325d06e7e73281e9a9b5e259fbd7cbfbe398a0433cbb0082ad8fa7"}, + {file = "SQLAlchemy-1.4.36-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5041474dcab7973baa91ec1f3112049a9dd4652898d6a95a6a895ff5c58beb6b"}, + {file = "SQLAlchemy-1.4.36-cp310-cp310-win32.whl", hash = "sha256:be094460930087e50fd08297db9d7aadaed8408ad896baf758e9190c335632da"}, + {file = "SQLAlchemy-1.4.36-cp310-cp310-win_amd64.whl", hash = "sha256:64d796e9af522162f7f2bf7a3c5531a0a550764c426782797bbeed809d0646c5"}, + {file = "SQLAlchemy-1.4.36-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:a0ae3aa2e86a4613f2d4c49eb7da23da536e6ce80b2bfd60bbb2f55fc02b0b32"}, + {file = "SQLAlchemy-1.4.36-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d50cb71c1dbed70646d521a0975fb0f92b7c3f84c61fa59e07be23a1aaeecfc"}, + {file = "SQLAlchemy-1.4.36-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:16abf35af37a3d5af92725fc9ec507dd9e9183d261c2069b6606d60981ed1c6e"}, + {file = "SQLAlchemy-1.4.36-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5864a83bd345871ad9699ce466388f836db7572003d67d9392a71998092210e3"}, + {file = "SQLAlchemy-1.4.36-cp36-cp36m-win32.whl", hash = "sha256:fbf8c09fe9728168f8cc1b40c239eab10baf9c422c18be7f53213d70434dea43"}, + {file = "SQLAlchemy-1.4.36-cp36-cp36m-win_amd64.whl", hash = "sha256:6e859fa96605027bd50d8e966db1c4e1b03e7b3267abbc4b89ae658c99393c58"}, + {file = "SQLAlchemy-1.4.36-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:166a3887ec355f7d2f12738f7fa25dc8ac541867147a255f790f2f41f614cb44"}, + {file = "SQLAlchemy-1.4.36-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e885548da361aa3f8a9433db4cfb335b2107e533bf314359ae3952821d84b3e"}, + {file = "SQLAlchemy-1.4.36-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5c90ef955d429966d84326d772eb34333178737ebb669845f1d529eb00c75e72"}, + {file = "SQLAlchemy-1.4.36-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a052bd9f53004f8993c624c452dfad8ec600f572dd0ed0445fbe64b22f5570e"}, + {file = "SQLAlchemy-1.4.36-cp37-cp37m-win32.whl", hash = "sha256:dce3468bf1fc12374a1a732c9efd146ce034f91bb0482b602a9311cb6166a920"}, + {file = "SQLAlchemy-1.4.36-cp37-cp37m-win_amd64.whl", hash = "sha256:6cb4c4f57a20710cea277edf720d249d514e587f796b75785ad2c25e1c0fed26"}, + {file = "SQLAlchemy-1.4.36-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:e74ce103b81c375c3853b436297952ef8d7863d801dcffb6728d01544e5191b5"}, + {file = "SQLAlchemy-1.4.36-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b20c4178ead9bc398be479428568ff31b6c296eb22e75776273781a6551973f"}, + {file = "SQLAlchemy-1.4.36-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:af2587ae11400157753115612d6c6ad255143efba791406ad8a0cbcccf2edcb3"}, + {file = "SQLAlchemy-1.4.36-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83cf3077712be9f65c9aaa0b5bc47bc1a44789fd45053e2e3ecd59ff17c63fe9"}, + {file = "SQLAlchemy-1.4.36-cp38-cp38-win32.whl", hash = "sha256:ce20f5da141f8af26c123ebaa1b7771835ca6c161225ce728962a79054f528c3"}, + {file = "SQLAlchemy-1.4.36-cp38-cp38-win_amd64.whl", hash = "sha256:316c7e5304dda3e3ad711569ac5d02698bbc71299b168ac56a7076b86259f7ea"}, + {file = "SQLAlchemy-1.4.36-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:f522214f6749bc073262529c056f7dfd660f3b5ec4180c5354d985eb7219801e"}, + {file = "SQLAlchemy-1.4.36-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ecac4db8c1aa4a269f5829df7e706639a24b780d2ac46b3e485cbbd27ec0028"}, + {file = "SQLAlchemy-1.4.36-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b3db741beaa983d4cbf9087558620e7787106319f7e63a066990a70657dd6b35"}, + {file = "SQLAlchemy-1.4.36-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ec89bf98cc6a0f5d1e28e3ad28e9be6f3b4bdbd521a4053c7ae8d5e1289a8a1"}, + {file = "SQLAlchemy-1.4.36-cp39-cp39-win32.whl", hash = "sha256:e12532c4d3f614678623da5d852f038ace1f01869b89f003ed6fe8c793f0c6a3"}, + {file = "SQLAlchemy-1.4.36-cp39-cp39-win_amd64.whl", hash = "sha256:cb441ca461bf97d00877b607f132772644b623518b39ced54da433215adce691"}, + {file = "SQLAlchemy-1.4.36.tar.gz", hash = "sha256:64678ac321d64a45901ef2e24725ec5e783f1f4a588305e196431447e7ace243"}, ] 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"}, + {file = "sqlalchemy2-stubs-0.0.2a22.tar.gz", hash = "sha256:31288db647bbdd411ad1e22da39a10ebe211bdcfe2efef24bcebea05abc28dd4"}, + {file = "sqlalchemy2_stubs-0.0.2a22-py3-none-any.whl", hash = "sha256:b9b907c3555d0b11bb8d738b788be478ce3871174839171d0d49aba5d0785016"}, ] starlette = [ {file = "starlette-0.17.1-py3-none-any.whl", hash = "sha256:26a18cbda5e6b651c964c12c88b36d9898481cd428ed6e063f5f29c418f73050"}, @@ -1333,18 +1251,14 @@ toml = [ {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"}, + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] 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.8-py2.py3-none-any.whl", hash = "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"}, - {file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"}, + {file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"}, + {file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"}, ] uvicorn = [ - {file = "uvicorn-0.16.0-py3-none-any.whl", hash = "sha256:d8c839231f270adaa6d338d525e2652a0b4a5f4c2430b5c4ef6ae4d11776b0d2"}, - {file = "uvicorn-0.16.0.tar.gz", hash = "sha256:eacb66afa65e0648fcbce5e746b135d09722231ffffc61883d4fac2b62fbea8d"}, + {file = "uvicorn-0.17.6-py3-none-any.whl", hash = "sha256:19e2a0e96c9ac5581c01eb1a79a7d2f72bb479691acd2b8921fce48ed5b961a6"}, + {file = "uvicorn-0.17.6.tar.gz", hash = "sha256:5180f9d059611747d841a4a4c4ab675edf54c8489e97f96d0583ee90ac3bfc23"}, ] diff --git a/{{cookiecutter.project_name}}/template_minimal/pyproject.toml b/{{cookiecutter.project_name}}/template_minimal/pyproject.toml index a13fdac..c9a7292 100644 --- a/{{cookiecutter.project_name}}/template_minimal/pyproject.toml +++ b/{{cookiecutter.project_name}}/template_minimal/pyproject.toml @@ -1,47 +1,46 @@ [tool.poetry] +authors = ["admin "] +description = "FastAPI project generated using minimal-fastapi-postgres-template." 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" +python = "^3.10" + +PyJWT = {extras = ["crypto"], version = "^2.3.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"} +alembic = "^1.7.7" +asyncpg = "^0.25.0" +fastapi = "^0.75.2" passlib = {extras = ["bcrypt"], version = "^1.7.4"} -pydantic = {extras = ["email"], version = "^1.9.0"} -alembic = "^1.7.5" +pydantic = {extras = ["email", "dotenv"], version = "^1.9.0"} python-multipart = "^0.0.5" -asyncpg = "^0.25.0" +toml = "^0.10.2" [tool.poetry.dev-dependencies] -black = {version = "^21.12b0", allow-prereleases = true} autoflake = "^1.4" +black = "^22.3.0" +coverage = "^6.3.2" flake8 = "^4.0.1" +httpx = "^0.22.0" 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" +pytest = "^7.1.2" +pytest-asyncio = "^0.18.3" +sqlalchemy2-stubs = "^0.0.2-alpha.22" +uvicorn = "^0.17.6" [build-system] -requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" +requires = ["poetry-core>=1.0.0"] [tool.pytest.ini_options] -minversion = "6.0" addopts = "-v" -filterwarnings = [ - "ignore::passlib.exc.PasslibHashWarning" -] +asyncio_mode = "auto" +filterwarnings = [] +minversion = "6.0" testpaths = [ - "app/tests", + "app/tests", ] [tool.isort] -profile = "black" \ No newline at end of file +profile = "black" diff --git a/{{cookiecutter.project_name}}/template_minimal/requirements-dev.txt b/{{cookiecutter.project_name}}/template_minimal/requirements-dev.txt index 009f0d6..76002ba 100644 --- a/{{cookiecutter.project_name}}/template_minimal/requirements-dev.txt +++ b/{{cookiecutter.project_name}}/template_minimal/requirements-dev.txt @@ -1,62 +1,57 @@ -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" +alembic==1.7.7; python_version >= "3.6" +anyio==3.5.0; python_version >= "3.6" and python_full_version >= "3.6.2" +asgiref==3.5.1; python_version >= "3.7" 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.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" +atomicwrites==1.4.0; python_version >= "3.7" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.4.0" +attrs==21.4.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" autoflake==1.4 -bcrypt==3.2.0; python_version >= "3.6" -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" +bcrypt==3.2.2; python_version >= "3.6" +black==22.3.0; python_full_version >= "3.6.2" +certifi==2021.10.8; python_version >= "3.6" cffi==1.15.0 -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==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.71.0; python_full_version >= "3.6.1" +charset-normalizer==2.0.12; python_full_version >= "3.5.0" and python_version >= "3.6" +click==8.1.3; python_version >= "3.7" and python_full_version >= "3.6.2" +colorama==0.4.4; sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.6.2" and platform_system == "Windows" and (python_version >= "3.7" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.5.0") +coverage==6.3.2; python_version >= "3.7" +cryptography==37.0.1; python_version >= "3.6" +dnspython==2.2.1; python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.6.1" +email-validator==1.2.1; python_full_version >= "3.6.1" +fastapi==0.75.2; 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.14.4; python_version >= "3.6" -httpx==0.21.3; python_version >= "3.6" +h11==0.12.0; python_version >= "3.7" +httpcore==0.14.7; python_version >= "3.6" +httpx==0.22.0; python_version >= "3.6" idna==3.3 -iniconfig==1.1.1; python_version >= "3.6" +iniconfig==1.1.1; python_version >= "3.7" 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" +mako==1.2.0; python_version >= "3.7" +markupsafe==2.1.1; python_version >= "3.7" 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" +packaging==21.3; python_version >= "3.7" passlib==1.7.4 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" +platformdirs==2.5.2; python_version >= "3.7" and python_full_version >= "3.6.2" +pluggy==1.0.0; python_version >= "3.7" +py==1.11.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" 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" +pycparser==2.21 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 +pyjwt==2.3.0; python_version >= "3.6" +pyparsing==3.0.8; python_full_version >= "3.6.8" and python_version >= "3.7" +pytest-asyncio==0.18.3; python_version >= "3.7" +pytest==7.1.2; python_version >= "3.7" +python-dotenv==0.20.0; python_full_version >= "3.6.1" and 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") 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 >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" 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.29; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") +sqlalchemy2-stubs==0.0.2a22; python_version >= "3.6" +sqlalchemy==1.4.36; (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.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 +toml==0.10.2; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.3.0") +tomli==2.0.1; python_version < "3.11" and python_full_version >= "3.6.2" and python_version >= "3.7" +typing-extensions==4.2.0; python_version >= "3.7" and python_full_version >= "3.6.1" +uvicorn==0.17.6; python_version >= "3.7" diff --git a/{{cookiecutter.project_name}}/template_minimal/requirements.txt b/{{cookiecutter.project_name}}/template_minimal/requirements.txt index 6aaba0f..c1b9233 100644 --- a/{{cookiecutter.project_name}}/template_minimal/requirements.txt +++ b/{{cookiecutter.project_name}}/template_minimal/requirements.txt @@ -1,36 +1,25 @@ -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" +alembic==1.7.7; python_version >= "3.6" +anyio==3.5.0; python_version >= "3.6" and python_full_version >= "3.6.2" 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" +bcrypt==3.2.2; python_version >= "3.6" cffi==1.15.0 -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.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.71.0; python_full_version >= "3.6.1" +cryptography==37.0.1; python_version >= "3.6" +dnspython==2.2.1; python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.6.1" +email-validator==1.2.1; python_full_version >= "3.6.1" +fastapi==0.75.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.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" +mako==1.2.0; python_version >= "3.7" +markupsafe==2.1.1; python_version >= "3.7" 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" +pycparser==2.21 pydantic==1.9.0; python_full_version >= "3.6.1" -python-dotenv==0.19.2; python_version >= "3.5" -python-jose==3.3.0 +pyjwt==2.3.0; python_version >= "3.6" +python-dotenv==0.20.0; python_full_version >= "3.6.1" and 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") -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 >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" 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") +sqlalchemy==1.4.36; (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 +toml==0.10.2; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.3.0") +typing-extensions==4.2.0; python_version >= "3.7" and python_full_version >= "3.6.1" From 4354821e557528b60444ca1762d1746334e92c38 Mon Sep 17 00:00:00 2001 From: rafsaf Date: Thu, 5 May 2022 20:59:54 +0200 Subject: [PATCH 02/20] reformated tokens, added trusted hosts and rewritten tests --- ... 20220504_init_user_model_050a3f0604ae.py} | 12 ++-- .../template_minimal/app/api/deps.py | 6 +- .../app/api/endpoints/auth.py | 42 +++-------- .../template_minimal/app/core/config.py | 17 +++-- .../template_minimal/app/core/security.py | 69 +++++++++--------- .../template_minimal/app/initial_data.py | 3 +- .../template_minimal/app/main.py | 7 +- .../template_minimal/app/models.py | 23 +++++- .../template_minimal/app/schemas/token.py | 13 ++-- .../template_minimal/app/schemas/user.py | 12 ++-- .../template_minimal/app/tests/conftest.py | 52 +++++++++----- .../template_minimal/app/tests/test_auth.py | 70 ++++++++++++------- 12 files changed, 182 insertions(+), 144 deletions(-) rename {{cookiecutter.project_name}}/template_minimal/alembic/versions/{20220503_init_user_table_2dba36457121.py => 20220504_init_user_model_050a3f0604ae.py} (79%) diff --git a/{{cookiecutter.project_name}}/template_minimal/alembic/versions/20220503_init_user_table_2dba36457121.py b/{{cookiecutter.project_name}}/template_minimal/alembic/versions/20220504_init_user_model_050a3f0604ae.py similarity index 79% rename from {{cookiecutter.project_name}}/template_minimal/alembic/versions/20220503_init_user_table_2dba36457121.py rename to {{cookiecutter.project_name}}/template_minimal/alembic/versions/20220504_init_user_model_050a3f0604ae.py index f763773..3079547 100644 --- a/{{cookiecutter.project_name}}/template_minimal/alembic/versions/20220503_init_user_table_2dba36457121.py +++ b/{{cookiecutter.project_name}}/template_minimal/alembic/versions/20220504_init_user_model_050a3f0604ae.py @@ -1,16 +1,16 @@ -"""init_user_table +"""init_user_model -Revision ID: 2dba36457121 +Revision ID: 050a3f0604ae Revises: -Create Date: 2022-05-03 22:51:45.832789 +Create Date: 2022-05-04 21:28:33.553367 """ from alembic import op import sqlalchemy as sa - +from sqlalchemy.dialects import postgresql # revision identifiers, used by Alembic. -revision = '2dba36457121' +revision = '050a3f0604ae' down_revision = None branch_labels = None depends_on = None @@ -19,7 +19,7 @@ def upgrade(): # ### commands auto generated by Alembic - please adjust! ### op.create_table('user', - sa.Column('id', sa.Integer(), nullable=False), + sa.Column('id', postgresql.UUID(), nullable=False), sa.Column('email', sa.String(length=254), nullable=False), sa.Column('hashed_password', sa.String(length=128), nullable=False), sa.Column('full_name', sa.String(length=254), nullable=True), diff --git a/{{cookiecutter.project_name}}/template_minimal/app/api/deps.py b/{{cookiecutter.project_name}}/template_minimal/app/api/deps.py index 6548749..be16742 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/api/deps.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/api/deps.py @@ -1,4 +1,4 @@ -from typing import AsyncGenerator, Optional +from typing import AsyncGenerator import jwt from fastapi import Depends, HTTPException, status @@ -26,7 +26,7 @@ async def get_current_user( try: payload = jwt.decode( - token, config.settings.SECRET_KEY, algorithms=[security.ALGORITHM] + token, config.settings.SECRET_KEY, algorithms=[security.JWT_ALGORITHM] ) token_data = schemas.TokenPayload(**payload) except (jwt.DecodeError, ValidationError): @@ -41,7 +41,7 @@ async def get_current_user( ) result = await session.execute(select(User).where(User.id == token_data.sub)) - user: Optional[User] = result.scalars().first() + user: User | None = result.scalars().first() if not user: raise HTTPException(status_code=404, detail="User not found.") 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 082e1ec..ce845cd 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/auth.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/auth.py @@ -1,5 +1,3 @@ -from typing import Optional - import jwt from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordRequestForm @@ -15,7 +13,7 @@ router = APIRouter() -@router.post("/access-token", response_model=schemas.Token) +@router.post("/access-token", response_model=schemas.Token, name="access_token") async def login_access_token( session: AsyncSession = Depends(deps.get_session), form_data: OAuth2PasswordRequestForm = Depends(), @@ -23,34 +21,20 @@ async def login_access_token( """ OAuth2 compatible token, get an access token for future requests using username and password """ + # https://www.oauth.com/oauth2-servers/access-tokens/access-token-response/ + result = await session.execute(select(User).where(User.email == form_data.username)) - user: Optional[User] = result.scalars().first() + user: User | None = 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): 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 + return security.access_token_response(user.id) -@router.post("/refresh-token", response_model=schemas.Token) +@router.post("/refresh-token", response_model=schemas.Token, name="refresh_token") async def refresh_token( input: schemas.TokenRefresh, session: AsyncSession = Depends(deps.get_session) ): @@ -61,7 +45,7 @@ async def refresh_token( payload = jwt.decode( input.refresh_token, config.settings.SECRET_KEY, - algorithms=[security.ALGORITHM], + algorithms=[security.JWT_ALGORITHM], ) token_data = schemas.TokenPayload(**payload) except (jwt.DecodeError, ValidationError): @@ -75,17 +59,9 @@ async def refresh_token( detail="Could not validate credentials", ) result = await session.execute(select(User).where(User.id == token_data.sub)) - user: Optional[User] = result.scalars().first() + user: User | None = 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, - } + return security.access_token_response(user.id) diff --git a/{{cookiecutter.project_name}}/template_minimal/app/core/config.py b/{{cookiecutter.project_name}}/template_minimal/app/core/config.py index 06d259e..e3a6117 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/core/config.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/core/config.py @@ -20,7 +20,7 @@ """ from pathlib import Path -from typing import Literal, Union +from typing import Literal import toml from pydantic import AnyHttpUrl, AnyUrl, BaseSettings, EmailStr, validator @@ -32,11 +32,12 @@ class Settings(BaseSettings): # CORE SETTINGS SECRET_KEY: str - ENVIRONMENT: Literal["DEV", "PYTEST", "STAGE", "PRODUCTION"] - ACCESS_TOKEN_EXPIRE_MINUTES: int + ENVIRONMENT: Literal["DEV", "PYTEST", "STG", "PRD"] = "DEV" SECURITY_BCRYPT_DEFAULT_ROUNDS: int = 12 + ACCESS_TOKEN_EXPIRE_MINUTES: int REFRESH_TOKEN_EXPIRE_MINUTES: int - BACKEND_CORS_ORIGINS: Union[str, list[AnyHttpUrl]] + BACKEND_CORS_ORIGINS: str | list[AnyHttpUrl] + ALLOWED_HOSTS: str | list[str] = ["localhost"] # PROJECT NAME, VERSION AND DESCRIPTION PROJECT_NAME: str = PYPROJECT_CONTENT["name"] @@ -65,11 +66,17 @@ 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: str | list[AnyHttpUrl]): if isinstance(cors_origins, str): return [item.strip() for item in cors_origins.split(",")] return cors_origins + @validator("ALLOWED_HOSTS") + def _assemble_cors_allowed_hosts(cls, allowed_hosts: str | list[AnyHttpUrl]): + if isinstance(allowed_hosts, str): + return [item.strip() for item in allowed_hosts.split(",")] + return allowed_hosts + @validator("DEFAULT_SQLALCHEMY_DATABASE_URI") def _assemble_default_db_connection(cls, v: str, values: dict[str, str]) -> str: return AnyUrl.build( diff --git a/{{cookiecutter.project_name}}/template_minimal/app/core/security.py b/{{cookiecutter.project_name}}/template_minimal/app/core/security.py index 07b8717..dacf267 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/core/security.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/core/security.py @@ -1,12 +1,16 @@ """Black-box security shortcuts to generate JWT tokens and password hashing and verifcation.""" -from datetime import datetime, timedelta +from datetime import datetime import jwt from passlib.context import CryptContext from app.core import config +JWT_ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_SECS = config.settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60 +REFRESH_TOKEN_EXPIRE_SECS = config.settings.REFRESH_TOKEN_EXPIRE_MINUTES * 60 + pwd_context = CryptContext( schemes=["bcrypt"], deprecated="auto", @@ -14,51 +18,42 @@ ) -ALGORITHM = "HS256" - - -def create_access_token(subject: str | int) -> tuple[str, datetime]: - """Creates jwt access token for user. - - Returns tuple with generated token and expire datetime. - expire minutes may be set using ACCESS_TOKEN_EXPIRE_MINUTES. +def create_jwt_token(subject: str | int, exp_secs: int, refresh: bool): + """Creates jwt access or refresh token for user.""" - Args: - subject: anything unique to user, eg. id, email. - """ - - now = datetime.utcnow() - expire = now + timedelta(minutes=config.settings.ACCESS_TOKEN_EXPIRE_MINUTES) + iat = datetime.utcnow().timestamp() + exp = iat + exp_secs - to_encode = {"exp": expire, "sub": str(subject), "refresh": False} + to_encode = { + "iat": iat, + "exp": exp, + "sub": str(subject), + "refresh": refresh, + } encoded_jwt: str = jwt.encode( to_encode, key=config.settings.SECRET_KEY, - algorithm=ALGORITHM, + algorithm=JWT_ALGORITHM, ) - return encoded_jwt, expire - - -def create_refresh_token(subject: str | int) -> tuple[str, datetime]: - """Creates jwt refresh token for user. + return encoded_jwt, exp, iat - Returns tuple with generated token and expire datetime. - expire minutes may be set using REFRESH_TOKEN_EXPIRE_MINUTES. - Args: - subject: anything unique to user, eg. id, email. - """ - - 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, - key=config.settings.SECRET_KEY, - algorithm=ALGORITHM, +def access_token_response(subject: str | int): + access_token, exp, iat = create_jwt_token( + subject, ACCESS_TOKEN_EXPIRE_SECS, refresh=False + ) + refresh_token, refresh_exp, refresh_iat = create_jwt_token( + subject, REFRESH_TOKEN_EXPIRE_SECS, refresh=True ) - return encoded_jwt, expire + return { + "token_type": "Bearer", + "access_token": access_token, + "expires_at": exp, + "issued_at": iat, + "refresh_token": refresh_token, + "refresh_token_expires_at": refresh_exp, + "refresh_token_issued_at": refresh_iat, + } def verify_password(plain_password: str, hashed_password: str) -> bool: diff --git a/{{cookiecutter.project_name}}/template_minimal/app/initial_data.py b/{{cookiecutter.project_name}}/template_minimal/app/initial_data.py index 14da2e0..29a50c8 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/initial_data.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/initial_data.py @@ -6,7 +6,6 @@ """ import asyncio -from typing import Optional from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession @@ -24,7 +23,7 @@ async def main() -> None: result = await session.execute( select(User).where(User.email == config.settings.FIRST_SUPERUSER_EMAIL) ) - user: Optional[User] = result.scalars().first() + user: User | None = result.scalars().first() if user is None: new_superuser = User( diff --git a/{{cookiecutter.project_name}}/template_minimal/app/main.py b/{{cookiecutter.project_name}}/template_minimal/app/main.py index 6a51dad..32316bc 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/main.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/main.py @@ -2,6 +2,7 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware +from fastapi.middleware.trustedhost import TrustedHostMiddleware from app.api.api import api_router from app.core import config @@ -13,8 +14,7 @@ openapi_url="/openapi.json", docs_url="/", ) - -# Set all CORS enabled origins +# Set all CORS enabled origins and allowed trusted hosts if config.settings.BACKEND_CORS_ORIGINS: app.add_middleware( CORSMiddleware, @@ -23,5 +23,8 @@ allow_methods=["*"], allow_headers=["*"], ) + app.add_middleware( + TrustedHostMiddleware, allowed_hosts=config.settings.ALLOWED_HOSTS + ) 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 index 349e3e3..ac5e899 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/models.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/models.py @@ -3,25 +3,44 @@ https://docs.sqlalchemy.org/en/14/orm/declarative_styles.html#example-two-dataclasses-with-declarative-table Dataclass style for powerful autocompletion support. +https://alembic.sqlalchemy.org/en/latest/tutorial.html Note, it is used by alembic migrations logic, see `alembic/env.py` +Alembic shortcuts: +# create migration +alembic revision --autogenerate -m "migration_name" + +# apply all migrations +alembic upgrade head """ +import uuid from dataclasses import dataclass, field -from sqlalchemy import Column, Integer, String +from sqlalchemy import Column, String +from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import registry Base = registry() +def random_uuid(): + return str(uuid.uuid4()) + + @Base.mapped @dataclass class User: __tablename__ = "user" __sa_dataclass_metadata_key__ = "sa" - id: int = field(init=False, metadata={"sa": Column(Integer, primary_key=True)}) + id: str = field( + init=False, + default_factory=random_uuid, + metadata={ + "sa": Column(UUID(as_uuid=False), primary_key=True, default=random_uuid) + }, + ) email: str = field(metadata={"sa": Column(String(254), nullable=False, index=True)}) hashed_password: str = field(metadata={"sa": Column(String(128), nullable=False)}) full_name: str | None = field( diff --git a/{{cookiecutter.project_name}}/template_minimal/app/schemas/token.py b/{{cookiecutter.project_name}}/template_minimal/app/schemas/token.py index 9c81e3e..b14a9b6 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/schemas/token.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/schemas/token.py @@ -1,20 +1,19 @@ -from datetime import datetime -from typing import Optional - from pydantic import BaseModel class Token(BaseModel): token_type: str access_token: str - expire_at: datetime + expires_at: int refresh_token: str - refresh_expire_at: datetime + refresh_token_expires_at: int class TokenPayload(BaseModel): - sub: Optional[int] = None - refresh: Optional[bool] = None + sub: str | None = None + refresh: bool | None = None + iat: float + exp: float class TokenRefresh(BaseModel): diff --git a/{{cookiecutter.project_name}}/template_minimal/app/schemas/user.py b/{{cookiecutter.project_name}}/template_minimal/app/schemas/user.py index 9e24055..15af88b 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/schemas/user.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/schemas/user.py @@ -1,5 +1,3 @@ -from typing import Optional - from pydantic import BaseModel, EmailStr @@ -9,15 +7,15 @@ class Config: class User(BaseUser): - id: int + id: str email: EmailStr - full_name: str + full_name: str | None class UserUpdate(BaseUser): - email: Optional[EmailStr] - password: Optional[str] - full_name: Optional[str] + email: EmailStr | None + password: str | None + full_name: str | None class UserCreate(BaseUser): diff --git a/{{cookiecutter.project_name}}/template_minimal/app/tests/conftest.py b/{{cookiecutter.project_name}}/template_minimal/app/tests/conftest.py index 0755ad6..c3a0027 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/tests/conftest.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/tests/conftest.py @@ -1,9 +1,10 @@ import asyncio -from typing import AsyncGenerator, Optional +from typing import AsyncGenerator -import pytest, pytest_asyncio +import pytest +import pytest_asyncio from httpx import AsyncClient -from sqlalchemy import select +from sqlalchemy import delete, select from sqlalchemy.ext.asyncio import AsyncSession from app.core import config, security @@ -12,7 +13,8 @@ from app.session import async_engine, async_session default_user_email = "geralt@wiedzmin.pl" -default_user_hash = security.get_password_hash("geralt") +default_user_password = "geralt" +default_user_hash = security.get_password_hash(default_user_password) @pytest.fixture(scope="session") @@ -23,12 +25,6 @@ def event_loop(): loop.close() -@pytest_asyncio.fixture() -async def client(): - async with AsyncClient(app=app, base_url="http://test") as client: - yield client - - @pytest_asyncio.fixture(scope="session") async def test_db_setup_sessionmaker(): # assert if we use TEST_DB URL for 100% @@ -36,23 +32,34 @@ async def test_db_setup_sessionmaker(): 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: - + async with async_engine.connect() as conn: await conn.run_sync(Base.metadata.drop_all) await conn.run_sync(Base.metadata.create_all) return async_session -@pytest_asyncio.fixture +@pytest_asyncio.fixture(autouse=True) async def session(test_db_setup_sessionmaker) -> AsyncGenerator[AsyncSession, None]: async with test_db_setup_sessionmaker() as session: + session: AsyncSession + yield session + for name, table in Base.metadata.tables.items(): + await session.execute(delete(table)) + await session.commit() + -@pytest_asyncio.fixture -async def default_user(session: AsyncSession): +@pytest_asyncio.fixture() +async def client() -> AsyncGenerator[AsyncClient, None]: + async with AsyncClient(app=app, base_url="http://test") as client: + yield client + + +@pytest_asyncio.fixture() +async def default_user(session: AsyncSession) -> User: result = await session.execute(select(User).where(User.email == default_user_email)) - user: Optional[User] = result.scalars().first() + user: User | None = result.scalars().first() if user is None: new_user = User( email=default_user_email, @@ -64,3 +71,16 @@ async def default_user(session: AsyncSession): await session.refresh(new_user) return new_user return user + + +@pytest_asyncio.fixture() +async def default_user_headers(client: AsyncClient, default_user: User): + access_token = await client.post( + app.url_path_for("access_token"), + data={ + "username": default_user_email, + "password": default_user_password, + }, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) + return {"Authorization": f"Bearer {access_token.json()['access_token']}"} 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 40a52eb..231a683 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/tests/test_auth.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/tests/test_auth.py @@ -1,43 +1,65 @@ import pytest from httpx import AsyncClient +from app.main import app from app.models import User +from app.tests.conftest import default_user_email, default_user_password # 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", +async def test_auth_access_token(client: AsyncClient, default_user: User): + response = await client.post( + app.url_path_for("access_token"), data={ - "username": "geralt@wiedzmin.pl", - "password": "geralt", + "username": default_user_email, + "password": default_user_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"] + assert response.status_code == 200 + token = response.json() + assert token["token_type"] == "bearer" + assert "access_token" in token + assert "expire_at" in token + assert "refresh_token" in token + assert "refresh_expire_at" in 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} +async def test_auth_access_token_fail_no_user(client: AsyncClient): + response = await client.post( + app.url_path_for("access_token"), + data={ + "username": "xxx", + "password": "yyy", + }, + headers={"Content-Type": "application/x-www-form-urlencoded"}, ) - assert get_new_token.status_code == 200 - new_token = get_new_token.json() + assert response.status_code == 400 + assert response.json() == {"detail": "Incorrect email or password"} + - assert "access_token" in new_token +async def test_auth_refresh_token(client: AsyncClient, default_user: User): + response = await client.post( + app.url_path_for("access_token"), + data={ + "username": default_user_email, + "password": default_user_password, + }, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) + refresh_token = response.json()["refresh_token"] + + new_token_response = await client.post( + app.url_path_for("refresh_token"), json={"refresh_token": refresh_token} + ) + assert new_token_response.status_code == 200 + token = new_token_response.json() + assert token["token_type"] == "bearer" + assert "access_token" in token + assert "expire_at" in token + assert "refresh_token" in token + assert "refresh_expire_at" in token From c977021ef6249e209c4af47222135ef0ef72e567 Mon Sep 17 00:00:00 2001 From: rafsaf Date: Sat, 7 May 2022 09:18:38 +0200 Subject: [PATCH 03/20] Finished 3.0 refactoring of a template, cleanup requriements, changed user model, schemas, naming convention in jwt tokens and added more users endpoints and tests --- .../template_minimal/.env.example | 3 +- .../template_minimal/.env.template | 3 +- .../template_minimal/alembic.ini | 13 +- .../template_minimal/alembic/env.py | 7 +- .../20220504_init_user_model_050a3f0604ae.py | 36 --- ...2022050753_init_user_model_940215fe1ae8.py | 36 +++ .../template_minimal/app/api/deps.py | 19 +- .../app/api/endpoints/auth.py | 42 ++-- .../app/api/endpoints/users.py | 69 ++++-- .../template_minimal/app/core/config.py | 27 +-- .../template_minimal/app/core/security.py | 73 +++--- .../app/{ => core}/session.py | 8 +- .../template_minimal/app/initial_data.py | 6 +- .../template_minimal/app/main.py | 25 +-- .../template_minimal/app/models.py | 19 +- .../template_minimal/app/schemas/__init__.py | 2 - .../template_minimal/app/schemas/requests.py | 19 ++ .../template_minimal/app/schemas/responses.py | 24 ++ .../template_minimal/app/schemas/token.py | 20 -- .../template_minimal/app/schemas/user.py | 24 -- .../template_minimal/app/tests/conftest.py | 63 +++--- .../template_minimal/app/tests/test_auth.py | 21 +- .../template_minimal/app/tests/test_users.py | 66 ++++++ ...mpose.local.yml => docker-compose.dev.yml} | 4 +- .../template_minimal/poetry.lock | 210 +++++++++++++++++- .../template_minimal/pyproject.toml | 6 +- .../template_minimal/requirements-dev.txt | 28 ++- .../template_minimal/requirements.txt | 4 +- 28 files changed, 592 insertions(+), 285 deletions(-) delete mode 100644 {{cookiecutter.project_name}}/template_minimal/alembic/versions/20220504_init_user_model_050a3f0604ae.py create mode 100644 {{cookiecutter.project_name}}/template_minimal/alembic/versions/2022050753_init_user_model_940215fe1ae8.py rename {{cookiecutter.project_name}}/template_minimal/app/{ => core}/session.py (83%) create mode 100644 {{cookiecutter.project_name}}/template_minimal/app/schemas/requests.py create mode 100644 {{cookiecutter.project_name}}/template_minimal/app/schemas/responses.py delete mode 100644 {{cookiecutter.project_name}}/template_minimal/app/schemas/token.py delete mode 100644 {{cookiecutter.project_name}}/template_minimal/app/schemas/user.py create mode 100644 {{cookiecutter.project_name}}/template_minimal/app/tests/test_users.py rename {{cookiecutter.project_name}}/template_minimal/{docker-compose.local.yml => docker-compose.dev.yml} (83%) diff --git a/{{cookiecutter.project_name}}/template_minimal/.env.example b/{{cookiecutter.project_name}}/template_minimal/.env.example index 33e4ebb..a67ec5b 100644 --- a/{{cookiecutter.project_name}}/template_minimal/.env.example +++ b/{{cookiecutter.project_name}}/template_minimal/.env.example @@ -2,7 +2,8 @@ SECRET_KEY=DVnFmhwvjEhJZpuhndxjhlezxQPJmBIIkMDEmFREWQADPcUnrG ENVIRONMENT=DEV ACCESS_TOKEN_EXPIRE_MINUTES=11520 REFRESH_TOKEN_EXPIRE_MINUTES=40320 -BACKEND_CORS_ORIGINS=http://localhost:3000,http://localhost:8001 +BACKEND_CORS_ORIGINS=["http://localhost:3000","http://localhost:8001"] +ALLOWED_HOSTS=["localhost"] DEFAULT_DATABASE_HOSTNAME=localhost DEFAULT_DATABASE_USER=rDGJeEDqAz diff --git a/{{cookiecutter.project_name}}/template_minimal/.env.template b/{{cookiecutter.project_name}}/template_minimal/.env.template index b5bef22..16dce6e 100644 --- a/{{cookiecutter.project_name}}/template_minimal/.env.template +++ b/{{cookiecutter.project_name}}/template_minimal/.env.template @@ -2,7 +2,8 @@ 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 +BACKEND_CORS_ORIGINS=["http://localhost:3000","http://localhost:8001"] +ALLOWED_HOSTS=["localhost"] DEFAULT_DATABASE_HOSTNAME=localhost DEFAULT_DATABASE_USER={{ random_ascii_string(10) }} diff --git a/{{cookiecutter.project_name}}/template_minimal/alembic.ini b/{{cookiecutter.project_name}}/template_minimal/alembic.ini index d88ed94..cd05815 100644 --- a/{{cookiecutter.project_name}}/template_minimal/alembic.ini +++ b/{{cookiecutter.project_name}}/template_minimal/alembic.ini @@ -5,7 +5,7 @@ script_location = alembic # template used to generate migration files -file_template = %%(year)d%%(month).2d%%(day).2d_%%(slug)s_%%(rev)s +file_template = %%(year)d%%(month).2d%%(day).2d%%(minute).2d_%%(slug)s_%%(rev)s # sys.path path, will be prepended to sys.path if present. # defaults to the current working directory. @@ -21,7 +21,7 @@ prepend_sys_path = . # max length of characters to apply to the # "slug" field -# truncate_slug_length = 40 +truncate_slug_length = 40 # set to 'true' to run the environment during # the 'revision' command, regardless of autogenerate @@ -59,10 +59,11 @@ sqlalchemy.url = driver://user:pass@localhost/dbname # 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 +hooks = black + +black.type = console_scripts +black.entrypoint = black +black.options = REVISION_SCRIPT_FILENAME # Logging configuration [loggers] diff --git a/{{cookiecutter.project_name}}/template_minimal/alembic/env.py b/{{cookiecutter.project_name}}/template_minimal/alembic/env.py index 6e02264..d48071a 100644 --- a/{{cookiecutter.project_name}}/template_minimal/alembic/env.py +++ b/{{cookiecutter.project_name}}/template_minimal/alembic/env.py @@ -3,11 +3,11 @@ from sqlalchemy import engine_from_config from sqlalchemy import pool from sqlalchemy.ext.asyncio import AsyncEngine -from asyncio import get_event_loop - +import asyncio 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 @@ -53,6 +53,7 @@ def run_migrations_offline(): literal_binds=True, dialect_opts={"paramstyle": "named"}, compare_type=True, + compare_server_default=True, ) with context.begin_transaction(): @@ -93,4 +94,4 @@ async def run_migrations_online(): if context.is_offline_mode(): run_migrations_offline() else: - get_event_loop().run_until_complete(run_migrations_online()) + asyncio.run(run_migrations_online()) diff --git a/{{cookiecutter.project_name}}/template_minimal/alembic/versions/20220504_init_user_model_050a3f0604ae.py b/{{cookiecutter.project_name}}/template_minimal/alembic/versions/20220504_init_user_model_050a3f0604ae.py deleted file mode 100644 index 3079547..0000000 --- a/{{cookiecutter.project_name}}/template_minimal/alembic/versions/20220504_init_user_model_050a3f0604ae.py +++ /dev/null @@ -1,36 +0,0 @@ -"""init_user_model - -Revision ID: 050a3f0604ae -Revises: -Create Date: 2022-05-04 21:28:33.553367 - -""" -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - -# revision identifiers, used by Alembic. -revision = '050a3f0604ae' -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', postgresql.UUID(), nullable=False), - sa.Column('email', sa.String(length=254), nullable=False), - sa.Column('hashed_password', sa.String(length=128), nullable=False), - sa.Column('full_name', sa.String(length=254), nullable=True), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_user_email'), 'user', ['email'], unique=False) - # ### 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_minimal/alembic/versions/2022050753_init_user_model_940215fe1ae8.py b/{{cookiecutter.project_name}}/template_minimal/alembic/versions/2022050753_init_user_model_940215fe1ae8.py new file mode 100644 index 0000000..cd12b43 --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/alembic/versions/2022050753_init_user_model_940215fe1ae8.py @@ -0,0 +1,36 @@ +"""init_user_model + +Revision ID: 940215fe1ae8 +Revises: +Create Date: 2022-05-07 04:53:32.108714 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "940215fe1ae8" +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", postgresql.UUID(), nullable=False), + 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) + # ### 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_minimal/app/api/deps.py b/{{cookiecutter.project_name}}/template_minimal/app/api/deps.py index be16742..3145bd2 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/api/deps.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/api/deps.py @@ -1,16 +1,15 @@ +import time from typing import AsyncGenerator import jwt from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer -from pydantic import ValidationError from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from app import schemas from app.core import config, security +from app.core.session import async_session from app.models import User -from app.session import async_session reusable_oauth2 = OAuth2PasswordBearer(tokenUrl="auth/access-token") @@ -28,16 +27,24 @@ async def get_current_user( payload = jwt.decode( token, config.settings.SECRET_KEY, algorithms=[security.JWT_ALGORITHM] ) - token_data = schemas.TokenPayload(**payload) - except (jwt.DecodeError, ValidationError): + except (jwt.DecodeError): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Could not validate credentials.", ) + # JWT guarantees payload will be unchanged (and thus valid), no errors here + token_data = security.JWTTokenPayload(**payload) + if token_data.refresh: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail="Cannot use refresh token.", + detail="Could not validate credentials, cannot use refresh token", + ) + now = int(time.time()) + if not now >= token_data.issued_at and now <= token_data.expires_at: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Could not validate credentials, token expired or not yet valid", ) result = await session.execute(select(User).where(User.id == token_data.sub)) 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 ce845cd..7a5ce6d 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/auth.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/auth.py @@ -1,3 +1,5 @@ +import time + import jwt from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordRequestForm @@ -5,63 +7,71 @@ from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from app import schemas from app.api import deps from app.core import config, security from app.models import User +from app.schemas.requests import RefreshTokenRequest +from app.schemas.responses import AccessTokenResponse router = APIRouter() -@router.post("/access-token", response_model=schemas.Token, name="access_token") +@router.post("/access-token", response_model=AccessTokenResponse, name="access_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 - """ - # https://www.oauth.com/oauth2-servers/access-tokens/access-token-response/ + """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: User | None = 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): raise HTTPException(status_code=400, detail="Incorrect email or password") - return security.access_token_response(user.id) + return security.generate_access_token_response(str(user.id)) -@router.post("/refresh-token", response_model=schemas.Token, name="refresh_token") +@router.post("/refresh-token", response_model=AccessTokenResponse, name="refresh_token") async def refresh_token( - input: schemas.TokenRefresh, session: AsyncSession = Depends(deps.get_session) + input: RefreshTokenRequest, + session: AsyncSession = Depends(deps.get_session), ): - """ - OAuth2 compatible token, get an access token for future requests using refresh token - """ + """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.JWT_ALGORITHM], ) - token_data = schemas.TokenPayload(**payload) except (jwt.DecodeError, ValidationError): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail="Could not validate credentials", + detail="Could not validate credentials, unknown error", ) + + # JWT guarantees payload will be unchanged (and thus valid), no errors here + token_data = security.JWTTokenPayload(**payload) + if not token_data.refresh: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail="Could not validate credentials", + detail="Could not validate credentials, cannot use access token", ) + now = int(time.time()) + if not now >= token_data.issued_at and now <= token_data.expires_at: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Could not validate credentials, token expired or not yet valid", + ) + result = await session.execute(select(User).where(User.id == token_data.sub)) user: User | None = result.scalars().first() if user is None: raise HTTPException(status_code=404, detail="User not found") - return security.access_token_response(user.id) + return security.generate_access_token_response(str(user.id)) diff --git a/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/users.py b/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/users.py index c26a616..15deb49 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/users.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/users.py @@ -1,41 +1,62 @@ -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy import delete, select from sqlalchemy.ext.asyncio import AsyncSession -from app import models, schemas from app.api import deps from app.core.security import get_password_hash +from app.models import User +from app.schemas.requests import UserCreateRequest, UserUpdatePasswordRequest +from app.schemas.responses import UserResponse router = APIRouter() -@router.put("/me", response_model=schemas.User) -async def update_user_me( - user_update: schemas.UserUpdate, +@router.get("/me", response_model=UserResponse, name="read_current_user") +async def read_current_user( + current_user: User = Depends(deps.get_current_user), +): + """Get current user""" + return current_user + + +@router.delete("/me", status_code=204, name="delete_current_user") +async def delete_current_user( + current_user: User = Depends(deps.get_current_user), 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) - if user_update.full_name is not None: - current_user.full_name = user_update.full_name - if user_update.email is not None: - current_user.email = user_update.email + """Delete current user""" + await session.execute(delete(User).where(User.id == current_user.id)) + await session.commit() + +@router.post( + "/reset-password", response_model=UserResponse, name="reset_current_user_password" +) +async def reset_current_user_password( + user_update_password: UserUpdatePasswordRequest, + session: AsyncSession = Depends(deps.get_session), + current_user: User = Depends(deps.get_current_user), +): + """Update current user password""" + current_user.hashed_password = get_password_hash(user_update_password.password) 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), +@router.post("/register", response_model=UserResponse, name="register_new_user") +async def register_new_user( + new_user: UserCreateRequest, + session: AsyncSession = Depends(deps.get_session), ): - """ - Get current user. - """ - return current_user + """Create new user""" + result = await session.execute(select(User).where(User.email == new_user.email)) + if result.scalars().first() is not None: + raise HTTPException(status_code=400, detail="Cannot use this email address") + user = User( + email=new_user.email, + hashed_password=get_password_hash(new_user.password), + ) + session.add(user) + await session.commit() + return user diff --git a/{{cookiecutter.project_name}}/template_minimal/app/core/config.py b/{{cookiecutter.project_name}}/template_minimal/app/core/config.py index e3a6117..8718d63 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/core/config.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/core/config.py @@ -17,13 +17,15 @@ to databases to avoid typo bugs. See https://pydantic-docs.helpmanual.io/usage/settings/ + +Note, complex types like lists are read as json-encoded strings. """ from pathlib import Path from typing import Literal import toml -from pydantic import AnyHttpUrl, AnyUrl, BaseSettings, EmailStr, validator +from pydantic import AnyHttpUrl, BaseSettings, EmailStr, PostgresDsn, validator PROJECT_DIR = Path(__file__).parent.parent.parent PYPROJECT_CONTENT = toml.load(f"{PROJECT_DIR}/pyproject.toml")["tool"]["poetry"] @@ -33,11 +35,11 @@ class Settings(BaseSettings): # CORE SETTINGS SECRET_KEY: str ENVIRONMENT: Literal["DEV", "PYTEST", "STG", "PRD"] = "DEV" - SECURITY_BCRYPT_DEFAULT_ROUNDS: int = 12 + SECURITY_BCRYPT_ROUNDS: int = 12 ACCESS_TOKEN_EXPIRE_MINUTES: int REFRESH_TOKEN_EXPIRE_MINUTES: int - BACKEND_CORS_ORIGINS: str | list[AnyHttpUrl] - ALLOWED_HOSTS: str | list[str] = ["localhost"] + BACKEND_CORS_ORIGINS: list[AnyHttpUrl] + ALLOWED_HOSTS: list[str] = ["localhost"] # PROJECT NAME, VERSION AND DESCRIPTION PROJECT_NAME: str = PYPROJECT_CONTENT["name"] @@ -64,22 +66,9 @@ class Settings(BaseSettings): FIRST_SUPERUSER_EMAIL: EmailStr FIRST_SUPERUSER_PASSWORD: str - # VALIDATORS - @validator("BACKEND_CORS_ORIGINS") - def _assemble_cors_origins(cls, cors_origins: str | list[AnyHttpUrl]): - if isinstance(cors_origins, str): - return [item.strip() for item in cors_origins.split(",")] - return cors_origins - - @validator("ALLOWED_HOSTS") - def _assemble_cors_allowed_hosts(cls, allowed_hosts: str | list[AnyHttpUrl]): - if isinstance(allowed_hosts, str): - return [item.strip() for item in allowed_hosts.split(",")] - return allowed_hosts - @validator("DEFAULT_SQLALCHEMY_DATABASE_URI") def _assemble_default_db_connection(cls, v: str, values: dict[str, str]) -> str: - return AnyUrl.build( + return PostgresDsn.build( scheme="postgresql+asyncpg", user=values["DEFAULT_DATABASE_USER"], password=values["DEFAULT_DATABASE_PASSWORD"], @@ -90,7 +79,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: - return AnyUrl.build( + return PostgresDsn.build( scheme="postgresql+asyncpg", user=values["TEST_DATABASE_USER"], password=values["TEST_DATABASE_PASSWORD"], diff --git a/{{cookiecutter.project_name}}/template_minimal/app/core/security.py b/{{cookiecutter.project_name}}/template_minimal/app/core/security.py index dacf267..62fb6dc 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/core/security.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/core/security.py @@ -1,74 +1,89 @@ """Black-box security shortcuts to generate JWT tokens and password hashing and verifcation.""" -from datetime import datetime +import time import jwt from passlib.context import CryptContext +from pydantic import BaseModel from app.core import config +from app.schemas.responses import AccessTokenResponse JWT_ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_SECS = config.settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60 REFRESH_TOKEN_EXPIRE_SECS = config.settings.REFRESH_TOKEN_EXPIRE_MINUTES * 60 - -pwd_context = CryptContext( +PWD_CONTEXT = CryptContext( schemes=["bcrypt"], deprecated="auto", - bcrypt__rounds=config.settings.SECURITY_BCRYPT_DEFAULT_ROUNDS, + bcrypt__rounds=config.settings.SECURITY_BCRYPT_ROUNDS, ) +class JWTTokenPayload(BaseModel): + sub: str | int + refresh: bool + issued_at: int + expires_at: int + + def create_jwt_token(subject: str | int, exp_secs: int, refresh: bool): - """Creates jwt access or refresh token for user.""" + """Creates jwt access or refresh token for user. - iat = datetime.utcnow().timestamp() - exp = iat + exp_secs + Args: + subject: anything unique to user, id or email etc. + exp_secs: expire time in seconds + refresh: if True, this is refresh token + """ - to_encode = { - "iat": iat, - "exp": exp, - "sub": str(subject), + issued_at = int(time.time()) + expires_at = issued_at + exp_secs + + to_encode: dict[str, int | str | bool] = { + "issued_at": issued_at, + "expires_at": expires_at, + "sub": subject, "refresh": refresh, } - encoded_jwt: str = jwt.encode( + encoded_jwt = jwt.encode( to_encode, key=config.settings.SECRET_KEY, algorithm=JWT_ALGORITHM, ) - return encoded_jwt, exp, iat + return encoded_jwt, expires_at, issued_at -def access_token_response(subject: str | int): - access_token, exp, iat = create_jwt_token( +def generate_access_token_response(subject: str | int): + """Generate tokens and return AccessTokenResponse""" + access_token, expires_at, issued_at = create_jwt_token( subject, ACCESS_TOKEN_EXPIRE_SECS, refresh=False ) - refresh_token, refresh_exp, refresh_iat = create_jwt_token( + refresh_token, refresh_expires_at, refresh_issued_at = create_jwt_token( subject, REFRESH_TOKEN_EXPIRE_SECS, refresh=True ) - return { - "token_type": "Bearer", - "access_token": access_token, - "expires_at": exp, - "issued_at": iat, - "refresh_token": refresh_token, - "refresh_token_expires_at": refresh_exp, - "refresh_token_issued_at": refresh_iat, - } + return AccessTokenResponse( + token_type="Bearer", + access_token=access_token, + expires_at=expires_at, + issued_at=issued_at, + refresh_token=refresh_token, + refresh_token_expires_at=refresh_expires_at, + refresh_token_issued_at=refresh_issued_at, + ) def verify_password(plain_password: str, hashed_password: str) -> bool: - """Verifie plain and hashed password matches + """Verifies plain and hashed password matches Applies passlib context based on bcrypt algorithm on plain passoword. It takes about 0.3s for default 12 rounds of SECURITY_BCRYPT_DEFAULT_ROUNDS. """ - return pwd_context.verify(plain_password, hashed_password) + return PWD_CONTEXT.verify(plain_password, hashed_password) def get_password_hash(password: str) -> str: - """Create hash from password + """Creates hash from password Applies passlib context based on bcrypt algorithm on plain passoword. It takes about 0.3s for default 12 rounds of SECURITY_BCRYPT_DEFAULT_ROUNDS. """ - return pwd_context.hash(password) + return PWD_CONTEXT.hash(password) diff --git a/{{cookiecutter.project_name}}/template_minimal/app/session.py b/{{cookiecutter.project_name}}/template_minimal/app/core/session.py similarity index 83% rename from {{cookiecutter.project_name}}/template_minimal/app/session.py rename to {{cookiecutter.project_name}}/template_minimal/app/core/session.py index 86fd7f8..1154b50 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/session.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/core/session.py @@ -1,5 +1,7 @@ """SQLAlchemy async engine and sessions tools""" +from typing import TYPE_CHECKING + from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine from sqlalchemy.orm.session import sessionmaker @@ -10,7 +12,9 @@ else: sqlalchemy_database_uri = config.settings.DEFAULT_SQLALCHEMY_DATABASE_URI -async_engine = create_async_engine(sqlalchemy_database_uri, pool_pre_ping=True) - +async_engine = create_async_engine(sqlalchemy_database_uri, pool_pre_ping=True) async_session = sessionmaker(async_engine, expire_on_commit=False, class_=AsyncSession) # type: ignore + +if TYPE_CHECKING: + async_session: sessionmaker[AsyncSession] # type: ignore diff --git a/{{cookiecutter.project_name}}/template_minimal/app/initial_data.py b/{{cookiecutter.project_name}}/template_minimal/app/initial_data.py index 29a50c8..52bb81d 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/initial_data.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/initial_data.py @@ -8,18 +8,15 @@ import asyncio from sqlalchemy import select -from sqlalchemy.ext.asyncio import AsyncSession from app.core import config, security +from app.core.session import async_session 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: - session: AsyncSession # resolves type issues with async_session - result = await session.execute( select(User).where(User.email == config.settings.FIRST_SUPERUSER_EMAIL) ) @@ -31,7 +28,6 @@ async def main() -> None: 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() diff --git a/{{cookiecutter.project_name}}/template_minimal/app/main.py b/{{cookiecutter.project_name}}/template_minimal/app/main.py index 32316bc..bfe7a21 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/main.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/main.py @@ -14,17 +14,16 @@ openapi_url="/openapi.json", docs_url="/", ) -# Set all CORS enabled origins and allowed trusted hosts -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.add_middleware( - TrustedHostMiddleware, allowed_hosts=config.settings.ALLOWED_HOSTS - ) - app.include_router(api_router) + +# Sets all CORS enabled origins +app.add_middleware( + CORSMiddleware, + allow_origins=[str(origin) for origin in config.settings.BACKEND_CORS_ORIGINS], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Guards against HTTP Host Header attacks +app.add_middleware(TrustedHostMiddleware, allowed_hosts=config.settings.ALLOWED_HOSTS) diff --git a/{{cookiecutter.project_name}}/template_minimal/app/models.py b/{{cookiecutter.project_name}}/template_minimal/app/models.py index ac5e899..07f8c1a 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/models.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/models.py @@ -24,25 +24,18 @@ Base = registry() -def random_uuid(): - return str(uuid.uuid4()) - - @Base.mapped @dataclass class User: __tablename__ = "user" __sa_dataclass_metadata_key__ = "sa" - id: str = field( + id: uuid.UUID = field( init=False, - default_factory=random_uuid, - metadata={ - "sa": Column(UUID(as_uuid=False), primary_key=True, default=random_uuid) - }, + default_factory=uuid.uuid4, + metadata={"sa": Column(UUID(as_uuid=True), primary_key=True)}, ) - email: str = field(metadata={"sa": Column(String(254), nullable=False, index=True)}) - hashed_password: str = field(metadata={"sa": Column(String(128), nullable=False)}) - full_name: str | None = field( - default=None, metadata={"sa": Column(String(254), nullable=True)} + email: str = field( + metadata={"sa": Column(String(254), nullable=False, unique=True, index=True)} ) + hashed_password: str = field(metadata={"sa": 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 index e2ec5b2..e69de29 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/schemas/__init__.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/schemas/__init__.py @@ -1,2 +0,0 @@ -from .token import Token, TokenPayload, TokenRefresh -from .user import User, UserCreate, UserUpdate diff --git a/{{cookiecutter.project_name}}/template_minimal/app/schemas/requests.py b/{{cookiecutter.project_name}}/template_minimal/app/schemas/requests.py new file mode 100644 index 0000000..b0300eb --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/app/schemas/requests.py @@ -0,0 +1,19 @@ +from pydantic import BaseModel, EmailStr + + +class BaseRequest(BaseModel): + # may define additional fields or config shared across requests + pass + + +class RefreshTokenRequest(BaseRequest): + refresh_token: str + + +class UserUpdatePasswordRequest(BaseRequest): + password: str + + +class UserCreateRequest(BaseRequest): + email: EmailStr + password: str diff --git a/{{cookiecutter.project_name}}/template_minimal/app/schemas/responses.py b/{{cookiecutter.project_name}}/template_minimal/app/schemas/responses.py new file mode 100644 index 0000000..c3c3d87 --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/app/schemas/responses.py @@ -0,0 +1,24 @@ +import uuid + +from pydantic import BaseModel, EmailStr + + +class BaseResponse(BaseModel): + # may define additional fields or config shared across responses + class Config: + orm_mode = True + + +class AccessTokenResponse(BaseResponse): + token_type: str + access_token: str + expires_at: int + issued_at: int + refresh_token: str + refresh_token_expires_at: int + refresh_token_issued_at: int + + +class UserResponse(BaseResponse): + id: uuid.UUID + email: EmailStr diff --git a/{{cookiecutter.project_name}}/template_minimal/app/schemas/token.py b/{{cookiecutter.project_name}}/template_minimal/app/schemas/token.py deleted file mode 100644 index b14a9b6..0000000 --- a/{{cookiecutter.project_name}}/template_minimal/app/schemas/token.py +++ /dev/null @@ -1,20 +0,0 @@ -from pydantic import BaseModel - - -class Token(BaseModel): - token_type: str - access_token: str - expires_at: int - refresh_token: str - refresh_token_expires_at: int - - -class TokenPayload(BaseModel): - sub: str | None = None - refresh: bool | None = None - iat: float - exp: float - - -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 deleted file mode 100644 index 15af88b..0000000 --- a/{{cookiecutter.project_name}}/template_minimal/app/schemas/user.py +++ /dev/null @@ -1,24 +0,0 @@ -from pydantic import BaseModel, EmailStr - - -class BaseUser(BaseModel): - class Config: - orm_mode = True - - -class User(BaseUser): - id: str - email: EmailStr - full_name: str | None - - -class UserUpdate(BaseUser): - email: EmailStr | None - password: str | None - full_name: str | None - - -class UserCreate(BaseUser): - email: EmailStr - password: str - full_name: str diff --git a/{{cookiecutter.project_name}}/template_minimal/app/tests/conftest.py b/{{cookiecutter.project_name}}/template_minimal/app/tests/conftest.py index c3a0027..85a16a4 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/tests/conftest.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/tests/conftest.py @@ -1,5 +1,6 @@ import asyncio from typing import AsyncGenerator +from uuid import UUID import pytest import pytest_asyncio @@ -8,13 +9,17 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.core import config, security +from app.core.session import async_engine, async_session from app.main import app from app.models import Base, User -from app.session import async_engine, async_session +default_user_id = UUID("b75365d9-7bf9-4f54-add5-aeab333a087b") default_user_email = "geralt@wiedzmin.pl" default_user_password = "geralt" -default_user_hash = security.get_password_hash(default_user_password) +default_user_password_hash = security.get_password_hash(default_user_password) +default_user_access_token = security.create_jwt_token( + str(default_user_id), 60 * 60 * 24, refresh=False +)[0] @pytest.fixture(scope="session") @@ -32,55 +37,49 @@ async def test_db_setup_sessionmaker(): 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.connect() as conn: + 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_asyncio.fixture(autouse=True) -async def session(test_db_setup_sessionmaker) -> AsyncGenerator[AsyncSession, None]: - async with test_db_setup_sessionmaker() as session: - session: AsyncSession - +async def session() -> AsyncGenerator[AsyncSession, None]: + async with async_session() as session: yield session + # delete all data from all tables after test for name, table in Base.metadata.tables.items(): await session.execute(delete(table)) await session.commit() -@pytest_asyncio.fixture() +@pytest_asyncio.fixture(scope="session") async def client() -> AsyncGenerator[AsyncClient, None]: async with AsyncClient(app=app, base_url="http://test") as client: + client.headers.update({"Host": "localhost"}) yield client @pytest_asyncio.fixture() -async def default_user(session: AsyncSession) -> User: - result = await session.execute(select(User).where(User.email == default_user_email)) - user: User | None = result.scalars().first() - if user is None: - new_user = User( - email=default_user_email, - hashed_password=default_user_hash, - full_name="fullname", +async def default_user() -> User: + async with async_session() as session: + result = await session.execute( + select(User).where(User.email == default_user_email) ) - session.add(new_user) - await session.commit() - await session.refresh(new_user) - return new_user - return user + user: User | None = result.scalars().first() + if user is None: + new_user = User( + email=default_user_email, + hashed_password=default_user_password_hash, + ) + new_user.id = default_user_id + session.add(new_user) + await session.commit() + await session.refresh(new_user) + return new_user + return user @pytest_asyncio.fixture() -async def default_user_headers(client: AsyncClient, default_user: User): - access_token = await client.post( - app.url_path_for("access_token"), - data={ - "username": default_user_email, - "password": default_user_password, - }, - headers={"Content-Type": "application/x-www-form-urlencoded"}, - ) - return {"Authorization": f"Bearer {access_token.json()['access_token']}"} +async def default_user_headers(default_user: User): + return {"Authorization": f"Bearer {default_user_access_token}"} 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 231a683..56198eb 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/tests/test_auth.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/tests/test_auth.py @@ -1,13 +1,9 @@ -import pytest from httpx import AsyncClient from app.main import app from app.models import User from app.tests.conftest import default_user_email, default_user_password -# All test coroutines in file will be treated as marked (async allowed). -pytestmark = pytest.mark.asyncio - async def test_auth_access_token(client: AsyncClient, default_user: User): response = await client.post( @@ -18,14 +14,15 @@ async def test_auth_access_token(client: AsyncClient, default_user: User): }, headers={"Content-Type": "application/x-www-form-urlencoded"}, ) - assert response.status_code == 200 token = response.json() - assert token["token_type"] == "bearer" + assert token["token_type"] == "Bearer" assert "access_token" in token - assert "expire_at" in token + assert "expires_at" in token + assert "issued_at" in token assert "refresh_token" in token - assert "refresh_expire_at" in token + assert "refresh_token_expires_at" in token + assert "refresh_token_issued_at" in token async def test_auth_access_token_fail_no_user(client: AsyncClient): @@ -58,8 +55,10 @@ async def test_auth_refresh_token(client: AsyncClient, default_user: User): ) assert new_token_response.status_code == 200 token = new_token_response.json() - assert token["token_type"] == "bearer" + assert token["token_type"] == "Bearer" assert "access_token" in token - assert "expire_at" in token + assert "expires_at" in token + assert "issued_at" in token assert "refresh_token" in token - assert "refresh_expire_at" in token + assert "refresh_token_expires_at" in token + assert "refresh_token_issued_at" in token diff --git a/{{cookiecutter.project_name}}/template_minimal/app/tests/test_users.py b/{{cookiecutter.project_name}}/template_minimal/app/tests/test_users.py new file mode 100644 index 0000000..5c8bb1b --- /dev/null +++ b/{{cookiecutter.project_name}}/template_minimal/app/tests/test_users.py @@ -0,0 +1,66 @@ +from httpx import AsyncClient +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.main import app +from app.models import User +from app.tests.conftest import ( + default_user_email, + default_user_id, + default_user_password_hash, +) + + +async def test_read_current_user(client: AsyncClient, default_user_headers): + response = await client.get( + app.url_path_for("read_current_user"), headers=default_user_headers + ) + assert response.status_code == 200 + assert response.json() == { + "id": str(default_user_id), + "email": default_user_email, + } + + +async def test_delete_current_user( + client: AsyncClient, default_user_headers, session: AsyncSession +): + response = await client.delete( + app.url_path_for("delete_current_user"), headers=default_user_headers + ) + assert response.status_code == 204 + result = await session.execute(select(User).where(User.id == default_user_id)) + user = result.scalars().first() + assert user is None + + +async def test_reset_current_user_password( + client: AsyncClient, default_user_headers, session: AsyncSession +): + response = await client.post( + app.url_path_for("reset_current_user_password"), + headers=default_user_headers, + json={"password": "testxxxxxx"}, + ) + assert response.status_code == 200 + result = await session.execute(select(User).where(User.id == default_user_id)) + user: User | None = result.scalars().first() + assert user is not None + assert user.hashed_password != default_user_password_hash + + +async def test_register_new_user( + client: AsyncClient, default_user_headers, session: AsyncSession +): + response = await client.post( + app.url_path_for("register_new_user"), + headers=default_user_headers, + json={ + "email": "qwe@example.com", + "password": "asdasdasd", + }, + ) + assert response.status_code == 200 + result = await session.execute(select(User).where(User.email == "qwe@example.com")) + user: User | None = result.scalars().first() + assert user is not None diff --git a/{{cookiecutter.project_name}}/template_minimal/docker-compose.local.yml b/{{cookiecutter.project_name}}/template_minimal/docker-compose.dev.yml similarity index 83% rename from {{cookiecutter.project_name}}/template_minimal/docker-compose.local.yml rename to {{cookiecutter.project_name}}/template_minimal/docker-compose.dev.yml index 998c19c..8d008d2 100644 --- a/{{cookiecutter.project_name}}/template_minimal/docker-compose.local.yml +++ b/{{cookiecutter.project_name}}/template_minimal/docker-compose.dev.yml @@ -1,8 +1,8 @@ version: "3.7" -# Database + Webserver (under http, for testing setup) +# Database + Webserver (under http, for testing setup on localhost:80) # -# docker-compose up -f docker-compose.local.yml -d +# docker-compose up -f docker-compose.dev.yml -d # services: diff --git a/{{cookiecutter.project_name}}/template_minimal/poetry.lock b/{{cookiecutter.project_name}}/template_minimal/poetry.lock index 97c0a12..09ccaae 100644 --- a/{{cookiecutter.project_name}}/template_minimal/poetry.lock +++ b/{{cookiecutter.project_name}}/template_minimal/poetry.lock @@ -30,6 +30,17 @@ doc = ["packaging", "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 = "arrow" +version = "1.2.2" +description = "Better dates & times for Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +python-dateutil = ">=2.7.0" + [[package]] name = "asgiref" version = "3.5.1" @@ -102,6 +113,17 @@ cffi = ">=1.1" tests = ["pytest (>=3.2.1,!=3.3.0)"] typecheck = ["mypy"] +[[package]] +name = "binaryornot" +version = "0.4.4" +description = "Ultra-lightweight pure Python package to check if a file is binary or text." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +chardet = ">=3.0.2" + [[package]] name = "black" version = "22.3.0" @@ -142,6 +164,14 @@ python-versions = "*" [package.dependencies] pycparser = "*" +[[package]] +name = "chardet" +version = "4.0.0" +description = "Universal encoding detector for Python 2 and 3" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + [[package]] name = "charset-normalizer" version = "2.0.12" @@ -172,6 +202,24 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "cookiecutter" +version = "1.7.3" +description = "A command-line utility that creates projects from project templates, e.g. creating a Python package project from a Python package project template." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +binaryornot = ">=0.4.4" +click = ">=7.0" +Jinja2 = ">=2.7,<4.0.0" +jinja2-time = ">=0.2.0" +poyo = ">=0.5.0" +python-slugify = ">=4.0.0" +requests = ">=2.23.0" +six = ">=1.10" + [[package]] name = "coverage" version = "6.3.2" @@ -232,7 +280,7 @@ idna = ">=2.0.0" [[package]] name = "fastapi" -version = "0.75.2" +version = "0.76.0" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" category = "main" optional = false @@ -240,7 +288,7 @@ 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.18.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,<7.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)", "orjson (>=3.2.1,<4.0.0)", "email_validator (>=1.1.1,<2.0.0)", "uvicorn[standard] (>=0.12.0,<0.18.0)"] @@ -349,6 +397,32 @@ requirements_deprecated_finder = ["pipreqs", "pip-api"] colors = ["colorama (>=0.4.3,<0.5.0)"] plugins = ["setuptools"] +[[package]] +name = "jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "jinja2-time" +version = "0.2.0" +description = "Jinja2 Extension for Dates and Times" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +arrow = "*" +jinja2 = "*" + [[package]] name = "mako" version = "1.2.0" @@ -449,6 +523,14 @@ python-versions = ">=3.6" dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "poyo" +version = "0.5.0" +description = "A lightweight YAML Parser for Python. 🐓" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + [[package]] name = "py" version = "1.11.0" @@ -561,6 +643,17 @@ pytest = ">=6.1.0" [package.extras] testing = ["coverage (==6.2)", "hypothesis (>=5.7.1)", "flaky (>=3.5.0)", "mypy (==0.931)", "pytest-trio (>=0.7.0)"] +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" + +[package.dependencies] +six = ">=1.5" + [[package]] name = "python-dotenv" version = "0.20.0" @@ -583,6 +676,38 @@ python-versions = "*" [package.dependencies] six = ">=1.4.0" +[[package]] +name = "python-slugify" +version = "6.1.2" +description = "A Python slugify application that also handles Unicode" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.dependencies] +text-unidecode = ">=1.3" + +[package.extras] +unidecode = ["Unidecode (>=1.1.1)"] + +[[package]] +name = "requests" +version = "2.27.1" +description = "Python HTTP for Humans." +category = "dev" +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" @@ -658,7 +783,7 @@ typing-extensions = ">=3.7.4" [[package]] name = "starlette" -version = "0.17.1" +version = "0.18.0" description = "The little ASGI library that shines." category = "main" optional = false @@ -670,6 +795,14 @@ anyio = ">=3.0.0,<4" [package.extras] full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests"] +[[package]] +name = "text-unidecode" +version = "1.3" +description = "The most basic Text::Unidecode port" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "toml" version = "0.10.2" @@ -694,6 +827,19 @@ category = "main" optional = false python-versions = ">=3.7" +[[package]] +name = "urllib3" +version = "1.26.9" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "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.17.6" @@ -713,7 +859,7 @@ standard = ["websockets (>=10.0)", "httptools (>=0.4.0)", "watchgod (>=0.6)", "p [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "ae84c41cab5ab2894820354794e37071b4fec21a2e3107d13c3ef88e030eb7ea" +content-hash = "37e4257f3c9edb8b654b932061fa936beee3ea5e4eef486d2821153e543afdcd" [metadata.files] alembic = [ @@ -724,6 +870,10 @@ anyio = [ {file = "anyio-3.5.0-py3-none-any.whl", hash = "sha256:b5fa16c5ff93fa1046f2eeb5bbff2dad4d3514d6cda61d02816dba34fa8c3c2e"}, {file = "anyio-3.5.0.tar.gz", hash = "sha256:a0aeffe2fb1fdf374a8e4b471444f0f3ac4fb9f5a5b542b48824475e0042a5a6"}, ] +arrow = [ + {file = "arrow-1.2.2-py3-none-any.whl", hash = "sha256:d622c46ca681b5b3e3574fcb60a04e5cc81b9625112d5fb2b44220c36c892177"}, + {file = "arrow-1.2.2.tar.gz", hash = "sha256:05caf1fd3d9a11a1135b2b6f09887421153b94558e5ef4d090b567b47173ac2b"}, +] asgiref = [ {file = "asgiref-3.5.1-py3-none-any.whl", hash = "sha256:45a429524fba18aba9d512498b19d220c4d628e75b40cf5c627524dbaebc5cc1"}, {file = "asgiref-3.5.1.tar.gz", hash = "sha256:fddeea3c53fa99d0cdb613c3941cc6e52d822491fc2753fba25768fb5bf4e865"}, @@ -780,6 +930,10 @@ bcrypt = [ {file = "bcrypt-3.2.2-cp36-abi3-win_amd64.whl", hash = "sha256:7ff2069240c6bbe49109fe84ca80508773a904f5a8cb960e02a977f7f519b129"}, {file = "bcrypt-3.2.2.tar.gz", hash = "sha256:433c410c2177057705da2a9f2cd01dd157493b2a7ac14c8593a16b3dab6b6bfb"}, ] +binaryornot = [ + {file = "binaryornot-0.4.4-py2.py3-none-any.whl", hash = "sha256:b8b71173c917bddcd2c16070412e369c3ed7f0528926f70cac18a6c97fd563e4"}, + {file = "binaryornot-0.4.4.tar.gz", hash = "sha256:359501dfc9d40632edc9fac890e19542db1a287bbcfa58175b66658392018061"}, +] black = [ {file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"}, {file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"}, @@ -861,6 +1015,10 @@ cffi = [ {file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"}, {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"}, ] +chardet = [ + {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, + {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, +] charset-normalizer = [ {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, @@ -873,6 +1031,10 @@ colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] +cookiecutter = [ + {file = "cookiecutter-1.7.3-py2.py3-none-any.whl", hash = "sha256:f8671531fa96ab14339d0c59b4f662a4f12a2ecacd94a0f70a3500843da588e2"}, + {file = "cookiecutter-1.7.3.tar.gz", hash = "sha256:6b9a4d72882e243be077a7397d0f1f76fe66cf3df91f3115dbb5330e214fa457"}, +] coverage = [ {file = "coverage-6.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b27d894748475fa858f9597c0ee1d4829f44683f3813633aaf94b19cb5453cf"}, {file = "coverage-6.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37d1141ad6b2466a7b53a22e08fe76994c2d35a5b6b469590424a9953155afac"}, @@ -949,8 +1111,8 @@ email-validator = [ {file = "email_validator-1.2.1.tar.gz", hash = "sha256:6757aea012d40516357c0ac2b1a4c31219ab2f899d26831334c5d069e8b6c3d8"}, ] fastapi = [ - {file = "fastapi-0.75.2-py3-none-any.whl", hash = "sha256:a70d31f4249b6b42dbe267667d22f83af645b2d857876c97f83ca9573215784f"}, - {file = "fastapi-0.75.2.tar.gz", hash = "sha256:b5dac161ee19d33346040d3f44d8b7a9ac09b37df9efff95891f5e7641fa482f"}, + {file = "fastapi-0.76.0-py3-none-any.whl", hash = "sha256:1e05c868651e3935bd9b290c61a3661a54e37471d3a0700bc5e4380f9ed935ae"}, + {file = "fastapi-0.76.0.tar.gz", hash = "sha256:a5f99f6e827c7108a8efaf1d7f19d6cf2d735ad984f5e44d33ccec6ee88a7da1"}, ] flake8 = [ {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, @@ -1037,6 +1199,14 @@ isort = [ {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, ] +jinja2 = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] +jinja2-time = [ + {file = "jinja2-time-0.2.0.tar.gz", hash = "sha256:d14eaa4d315e7688daa4969f616f226614350c48730bfa1692d2caebd8c90d40"}, + {file = "jinja2_time-0.2.0-py2.py3-none-any.whl", hash = "sha256:d3eab6605e3ec8b7a0863df09cc1d23714908fa61aa6986a845c20ba488b4efa"}, +] mako = [ {file = "Mako-1.2.0-py3-none-any.whl", hash = "sha256:23aab11fdbbb0f1051b93793a58323ff937e98e34aece1c4219675122e57e4ba"}, {file = "Mako-1.2.0.tar.gz", hash = "sha256:9a7c7e922b87db3686210cf49d5d767033a41d4010b284e747682c92bddd8b39"}, @@ -1111,6 +1281,10 @@ pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] +poyo = [ + {file = "poyo-0.5.0-py2.py3-none-any.whl", hash = "sha256:3e2ca8e33fdc3c411cd101ca395668395dd5dc7ac775b8e809e3def9f9fe041a"}, + {file = "poyo-0.5.0.tar.gz", hash = "sha256:e26956aa780c45f011ca9886f044590e2d8fd8b61db7b1c1cf4e0869f48ed4dd"}, +] py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, @@ -1181,6 +1355,10 @@ pytest-asyncio = [ {file = "pytest_asyncio-0.18.3-1-py3-none-any.whl", hash = "sha256:16cf40bdf2b4fb7fc8e4b82bd05ce3fbcd454cbf7b92afc445fe299dabb88213"}, {file = "pytest_asyncio-0.18.3-py3-none-any.whl", hash = "sha256:8fafa6c52161addfd41ee7ab35f11836c5a16ec208f93ee388f752bea3493a84"}, ] +python-dateutil = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] python-dotenv = [ {file = "python-dotenv-0.20.0.tar.gz", hash = "sha256:b7e3b04a59693c42c36f9ab1cc2acc46fa5df8c78e178fc33a8d4cd05c8d498f"}, {file = "python_dotenv-0.20.0-py3-none-any.whl", hash = "sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938"}, @@ -1188,6 +1366,14 @@ python-dotenv = [ python-multipart = [ {file = "python-multipart-0.0.5.tar.gz", hash = "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"}, ] +python-slugify = [ + {file = "python-slugify-6.1.2.tar.gz", hash = "sha256:272d106cb31ab99b3496ba085e3fea0e9e76dcde967b5e9992500d1f785ce4e1"}, + {file = "python_slugify-6.1.2-py2.py3-none-any.whl", hash = "sha256:7b2c274c308b62f4269a9ba701aa69a797e9bca41aeee5b3a9e79e36b6656927"}, +] +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"}, @@ -1243,8 +1429,12 @@ sqlalchemy2-stubs = [ {file = "sqlalchemy2_stubs-0.0.2a22-py3-none-any.whl", hash = "sha256:b9b907c3555d0b11bb8d738b788be478ce3871174839171d0d49aba5d0785016"}, ] 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.18.0-py3-none-any.whl", hash = "sha256:377d64737a0e03560cb8eaa57604afee143cea5a4996933242798a7820e64f53"}, + {file = "starlette-0.18.0.tar.gz", hash = "sha256:b45c6e9a617ecb5caf7e6446bd8d767b0084d6217e8e1b08187ca5191e10f097"}, +] +text-unidecode = [ + {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, + {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, ] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, @@ -1258,6 +1448,10 @@ typing-extensions = [ {file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"}, {file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"}, ] +urllib3 = [ + {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, + {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, +] uvicorn = [ {file = "uvicorn-0.17.6-py3-none-any.whl", hash = "sha256:19e2a0e96c9ac5581c01eb1a79a7d2f72bb479691acd2b8921fce48ed5b961a6"}, {file = "uvicorn-0.17.6.tar.gz", hash = "sha256:5180f9d059611747d841a4a4c4ab675edf54c8489e97f96d0583ee90ac3bfc23"}, diff --git a/{{cookiecutter.project_name}}/template_minimal/pyproject.toml b/{{cookiecutter.project_name}}/template_minimal/pyproject.toml index c9a7292..45422b9 100644 --- a/{{cookiecutter.project_name}}/template_minimal/pyproject.toml +++ b/{{cookiecutter.project_name}}/template_minimal/pyproject.toml @@ -11,15 +11,16 @@ PyJWT = {extras = ["crypto"], version = "^2.3.0"} SQLAlchemy = {extras = ["asyncio"], version = "^1.4.29"} alembic = "^1.7.7" asyncpg = "^0.25.0" -fastapi = "^0.75.2" +fastapi = "^0.76.0" passlib = {extras = ["bcrypt"], version = "^1.7.4"} pydantic = {extras = ["email", "dotenv"], version = "^1.9.0"} -python-multipart = "^0.0.5" +python-multipart = ">=0.0.5,<0.0.6" toml = "^0.10.2" [tool.poetry.dev-dependencies] autoflake = "^1.4" black = "^22.3.0" +cookiecutter = "^1.7.3" coverage = "^6.3.2" flake8 = "^4.0.1" httpx = "^0.22.0" @@ -37,6 +38,7 @@ requires = ["poetry-core>=1.0.0"] addopts = "-v" asyncio_mode = "auto" filterwarnings = [] +markers = ["pytest.mark.asyncio"] minversion = "6.0" testpaths = [ "app/tests", diff --git a/{{cookiecutter.project_name}}/template_minimal/requirements-dev.txt b/{{cookiecutter.project_name}}/template_minimal/requirements-dev.txt index 76002ba..08d0ddd 100644 --- a/{{cookiecutter.project_name}}/template_minimal/requirements-dev.txt +++ b/{{cookiecutter.project_name}}/template_minimal/requirements-dev.txt @@ -1,22 +1,26 @@ alembic==1.7.7; python_version >= "3.6" anyio==3.5.0; python_version >= "3.6" and python_full_version >= "3.6.2" +arrow==1.2.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" asgiref==3.5.1; python_version >= "3.7" asyncpg==0.25.0; python_full_version >= "3.6.0" atomicwrites==1.4.0; python_version >= "3.7" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.4.0" attrs==21.4.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" autoflake==1.4 bcrypt==3.2.2; python_version >= "3.6" +binaryornot==0.4.4; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" black==22.3.0; python_full_version >= "3.6.2" -certifi==2021.10.8; python_version >= "3.6" +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.12; python_full_version >= "3.5.0" and python_version >= "3.6" -click==8.1.3; python_version >= "3.7" and python_full_version >= "3.6.2" -colorama==0.4.4; sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.6.2" and platform_system == "Windows" and (python_version >= "3.7" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.5.0") +chardet==4.0.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" +charset-normalizer==2.0.12; python_full_version >= "3.6.0" and python_version >= "3.6" +click==8.1.3; python_version >= "3.7" and python_full_version >= "3.6.2" and (python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7") +colorama==0.4.4; sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.6.2" and platform_system == "Windows" 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" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.5.0") +cookiecutter==1.7.3; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") coverage==6.3.2; python_version >= "3.7" cryptography==37.0.1; python_version >= "3.6" dnspython==2.2.1; python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.6.1" email-validator==1.2.1; python_full_version >= "3.6.1" -fastapi==0.75.2; python_full_version >= "3.6.1" +fastapi==0.76.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.7" @@ -25,8 +29,10 @@ httpx==0.22.0; python_version >= "3.6" idna==3.3 iniconfig==1.1.1; python_version >= "3.7" isort==5.10.1; python_full_version >= "3.6.1" and python_version < "4.0" +jinja2-time==0.2.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" +jinja2==3.1.2; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" mako==1.2.0; python_version >= "3.7" -markupsafe==2.1.1; python_version >= "3.7" +markupsafe==2.1.1; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" 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.7" @@ -34,6 +40,7 @@ passlib==1.7.4 pathspec==0.9.0; python_full_version >= "3.6.2" platformdirs==2.5.2; python_version >= "3.7" and python_full_version >= "3.6.2" pluggy==1.0.0; python_version >= "3.7" +poyo==0.5.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" py==1.11.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" 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 @@ -43,15 +50,20 @@ pyjwt==2.3.0; python_version >= "3.6" pyparsing==3.0.8; python_full_version >= "3.6.8" and python_version >= "3.7" pytest-asyncio==0.18.3; python_version >= "3.7" pytest==7.1.2; python_version >= "3.7" +python-dateutil==2.8.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" python-dotenv==0.20.0; python_full_version >= "3.6.1" and python_version >= "3.5" python-multipart==0.0.5 +python-slugify==6.1.2; 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" -six==1.16.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" +six==1.16.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" sniffio==1.2.0; python_version >= "3.6" and python_full_version >= "3.6.2" sqlalchemy2-stubs==0.0.2a22; python_version >= "3.6" sqlalchemy==1.4.36; (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" +starlette==0.18.0; python_version >= "3.6" and python_full_version >= "3.6.1" +text-unidecode==1.3; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" toml==0.10.2; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.3.0") tomli==2.0.1; python_version < "3.11" and python_full_version >= "3.6.2" and python_version >= "3.7" typing-extensions==4.2.0; python_version >= "3.7" and python_full_version >= "3.6.1" +urllib3==1.26.9; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version < "4" uvicorn==0.17.6; python_version >= "3.7" diff --git a/{{cookiecutter.project_name}}/template_minimal/requirements.txt b/{{cookiecutter.project_name}}/template_minimal/requirements.txt index c1b9233..5f1bb30 100644 --- a/{{cookiecutter.project_name}}/template_minimal/requirements.txt +++ b/{{cookiecutter.project_name}}/template_minimal/requirements.txt @@ -6,7 +6,7 @@ cffi==1.15.0 cryptography==37.0.1; python_version >= "3.6" dnspython==2.2.1; python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.6.1" email-validator==1.2.1; python_full_version >= "3.6.1" -fastapi==0.75.2; python_full_version >= "3.6.1" +fastapi==0.76.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") idna==3.3; python_full_version >= "3.6.2" and python_version >= "3.6" mako==1.2.0; python_version >= "3.7" @@ -20,6 +20,6 @@ python-multipart==0.0.5 six==1.16.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" sniffio==1.2.0; python_version >= "3.6" and python_full_version >= "3.6.2" sqlalchemy==1.4.36; (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" +starlette==0.18.0; python_version >= "3.6" and python_full_version >= "3.6.1" toml==0.10.2; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.3.0") typing-extensions==4.2.0; python_version >= "3.7" and python_full_version >= "3.6.1" From d2ec74513acb93d055b68eaa75e7f12ec51c45c3 Mon Sep 17 00:00:00 2001 From: rafsaf Date: Sat, 7 May 2022 10:27:28 +0200 Subject: [PATCH 04/20] updated tests.yml job, removed cookiecutter from template minimal dev requirements --- .github/workflows/tests.yml | 121 +++++------ .../template_minimal/.env.template | 8 +- .../template_minimal/poetry.lock | 196 +----------------- .../template_minimal/pyproject.toml | 1 - .../template_minimal/requirements-dev.txt | 24 +-- 5 files changed, 72 insertions(+), 278 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 24ce58a..0c2346b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,89 +7,90 @@ on: jobs: build: runs-on: ubuntu-20.04 - strategy: - matrix: - python-version: ["3.9", "3.10"] - services: postgres: image: postgres env: - POSTGRES_DB: test - POSTGRES_USER: test - POSTGRES_PASSWORD: test + POSTGRES_PASSWORD: postgres options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 ports: - - 30000:5432 + - 5432:5432 steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: - python-version: ${{ matrix.python-version }} - - name: Load cached venv1 - id: cached-poetry-dependencies1 - uses: actions/cache@v2 - with: - 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-dependencies1.outputs.cache-hit != 'true' + python-version: 3.10 + + - name: Generate projects from templates using cookiecutter + # fastapi_users_project folder + # minimal_project folder run: | - python -m venv .venv1 - source .venv1/bin/activate - pip install -r {{cookiecutter.project_name}}/template_minimal/requirements-dev.txt pip install cookiecutter - - name: Load cached venv2 - id: cached-poetry-dependencies2 + python tests/create_minimal_project.py + python tests/create_fastapi_users_project.py + + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: true + + ### template minimal ### + + - name: Load cached venv-1 + id: cached-poetry-dependencies-template-1 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_fastapi_users/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 + path: minimal_project/.venv + key: venv-${{ runner.os }}-${{ hashFiles('minimal_project/poetry.lock') }} + + - name: Install template minimal dependencies + if: steps.cached-poetry-dependencies-template-1.outputs.cache-hit != 'true' run: | - source .venv2/bin/activate - # stop the build if there are Python syntax errors or undefined names - cd \{\{cookiecutter.project_name\}\}/template_fastapi_users - flake8 app --count --exit-zero --statistics - - name: Test minimal project is passing pytest test + cd minimal_project + poetry install --no-interaction --no-root + + - name: Run template minimal flake8 and then tests + env: + TEST_DATABASE_HOSTNAME: localhost + TEST_DATABASE_PASSWORD: postgres + TEST_DATABASE_PORT: 5432 + TEST_DATABASE_USER: postgres + TEST_DATABASE_DB: postgres 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 + cd minimal_project + poetry run flake8 app --count --exit-zero --statistics + poetry run coverage run -m pytest - pytest minimal_project + ### template fastapi users ### - - name: Test fastapi_users project is passing pytest test + - name: Load cached venv-2 + id: cached-poetry-dependencies-template-2 + uses: actions/cache@v2 + with: + path: fastapi_users_project/.venv + key: venv-${{ runner.os }}-${{ hashFiles('fastapi_users_project/poetry.lock') }} + + - name: Install template fastapi users dependencies + if: steps.cached-poetry-dependencies-template-1.outputs.cache-hit != 'true' run: | - 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 + cd fastapi_users_project + poetry install --no-interaction --no-root - pytest fastapi_users_project + - name: Run template fastapi users flake8 and then tests + env: + TEST_DATABASE_HOSTNAME: localhost + TEST_DATABASE_PASSWORD: postgres + TEST_DATABASE_PORT: 5432 + TEST_DATABASE_USER: postgres + TEST_DATABASE_DB: postgres + run: | + cd fastapi_users_project + poetry run flake8 app --count --exit-zero --statistics + poetry run coverage run -m pytest diff --git a/{{cookiecutter.project_name}}/template_minimal/.env.template b/{{cookiecutter.project_name}}/template_minimal/.env.template index 16dce6e..cc6bc98 100644 --- a/{{cookiecutter.project_name}}/template_minimal/.env.template +++ b/{{cookiecutter.project_name}}/template_minimal/.env.template @@ -6,16 +6,16 @@ BACKEND_CORS_ORIGINS=["http://localhost:3000","http://localhost:8001"] ALLOWED_HOSTS=["localhost"] DEFAULT_DATABASE_HOSTNAME=localhost -DEFAULT_DATABASE_USER={{ random_ascii_string(10) }} +DEFAULT_DATABASE_USER=postgres DEFAULT_DATABASE_PASSWORD={{ random_ascii_string(50) }} DEFAULT_DATABASE_PORT={{ range(4000, 7000) | random }} -DEFAULT_DATABASE_DB=default_db +DEFAULT_DATABASE_DB=postgres TEST_DATABASE_HOSTNAME=localhost -TEST_DATABASE_USER=test +TEST_DATABASE_USER=postgres TEST_DATABASE_PASSWORD={{ random_ascii_string(50) }} TEST_DATABASE_PORT={{ range(30000, 40000) | random }} -TEST_DATABASE_DB=test_db +TEST_DATABASE_DB=postgres 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/poetry.lock b/{{cookiecutter.project_name}}/template_minimal/poetry.lock index 09ccaae..23d6886 100644 --- a/{{cookiecutter.project_name}}/template_minimal/poetry.lock +++ b/{{cookiecutter.project_name}}/template_minimal/poetry.lock @@ -30,17 +30,6 @@ doc = ["packaging", "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 = "arrow" -version = "1.2.2" -description = "Better dates & times for Python" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -python-dateutil = ">=2.7.0" - [[package]] name = "asgiref" version = "3.5.1" @@ -113,17 +102,6 @@ cffi = ">=1.1" tests = ["pytest (>=3.2.1,!=3.3.0)"] typecheck = ["mypy"] -[[package]] -name = "binaryornot" -version = "0.4.4" -description = "Ultra-lightweight pure Python package to check if a file is binary or text." -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -chardet = ">=3.0.2" - [[package]] name = "black" version = "22.3.0" @@ -164,14 +142,6 @@ python-versions = "*" [package.dependencies] pycparser = "*" -[[package]] -name = "chardet" -version = "4.0.0" -description = "Universal encoding detector for Python 2 and 3" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - [[package]] name = "charset-normalizer" version = "2.0.12" @@ -202,24 +172,6 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -[[package]] -name = "cookiecutter" -version = "1.7.3" -description = "A command-line utility that creates projects from project templates, e.g. creating a Python package project from a Python package project template." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[package.dependencies] -binaryornot = ">=0.4.4" -click = ">=7.0" -Jinja2 = ">=2.7,<4.0.0" -jinja2-time = ">=0.2.0" -poyo = ">=0.5.0" -python-slugify = ">=4.0.0" -requests = ">=2.23.0" -six = ">=1.10" - [[package]] name = "coverage" version = "6.3.2" @@ -397,32 +349,6 @@ requirements_deprecated_finder = ["pipreqs", "pip-api"] colors = ["colorama (>=0.4.3,<0.5.0)"] plugins = ["setuptools"] -[[package]] -name = "jinja2" -version = "3.1.2" -description = "A very fast and expressive template engine." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "jinja2-time" -version = "0.2.0" -description = "Jinja2 Extension for Dates and Times" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -arrow = "*" -jinja2 = "*" - [[package]] name = "mako" version = "1.2.0" @@ -523,14 +449,6 @@ python-versions = ">=3.6" dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] -[[package]] -name = "poyo" -version = "0.5.0" -description = "A lightweight YAML Parser for Python. 🐓" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - [[package]] name = "py" version = "1.11.0" @@ -643,17 +561,6 @@ pytest = ">=6.1.0" [package.extras] testing = ["coverage (==6.2)", "hypothesis (>=5.7.1)", "flaky (>=3.5.0)", "mypy (==0.931)", "pytest-trio (>=0.7.0)"] -[[package]] -name = "python-dateutil" -version = "2.8.2" -description = "Extensions to the standard Python datetime module" -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" - -[package.dependencies] -six = ">=1.5" - [[package]] name = "python-dotenv" version = "0.20.0" @@ -676,38 +583,6 @@ python-versions = "*" [package.dependencies] six = ">=1.4.0" -[[package]] -name = "python-slugify" -version = "6.1.2" -description = "A Python slugify application that also handles Unicode" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" - -[package.dependencies] -text-unidecode = ">=1.3" - -[package.extras] -unidecode = ["Unidecode (>=1.1.1)"] - -[[package]] -name = "requests" -version = "2.27.1" -description = "Python HTTP for Humans." -category = "dev" -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" @@ -795,14 +670,6 @@ anyio = ">=3.0.0,<4" [package.extras] full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests"] -[[package]] -name = "text-unidecode" -version = "1.3" -description = "The most basic Text::Unidecode port" -category = "dev" -optional = false -python-versions = "*" - [[package]] name = "toml" version = "0.10.2" @@ -827,19 +694,6 @@ category = "main" optional = false python-versions = ">=3.7" -[[package]] -name = "urllib3" -version = "1.26.9" -description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" - -[package.extras] -brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "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.17.6" @@ -859,7 +713,7 @@ standard = ["websockets (>=10.0)", "httptools (>=0.4.0)", "watchgod (>=0.6)", "p [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "37e4257f3c9edb8b654b932061fa936beee3ea5e4eef486d2821153e543afdcd" +content-hash = "d5a61fe471977ad5ce64fd8fec4548ee4715d85f6e40f7a61ba8b318c6b2734d" [metadata.files] alembic = [ @@ -870,10 +724,6 @@ anyio = [ {file = "anyio-3.5.0-py3-none-any.whl", hash = "sha256:b5fa16c5ff93fa1046f2eeb5bbff2dad4d3514d6cda61d02816dba34fa8c3c2e"}, {file = "anyio-3.5.0.tar.gz", hash = "sha256:a0aeffe2fb1fdf374a8e4b471444f0f3ac4fb9f5a5b542b48824475e0042a5a6"}, ] -arrow = [ - {file = "arrow-1.2.2-py3-none-any.whl", hash = "sha256:d622c46ca681b5b3e3574fcb60a04e5cc81b9625112d5fb2b44220c36c892177"}, - {file = "arrow-1.2.2.tar.gz", hash = "sha256:05caf1fd3d9a11a1135b2b6f09887421153b94558e5ef4d090b567b47173ac2b"}, -] asgiref = [ {file = "asgiref-3.5.1-py3-none-any.whl", hash = "sha256:45a429524fba18aba9d512498b19d220c4d628e75b40cf5c627524dbaebc5cc1"}, {file = "asgiref-3.5.1.tar.gz", hash = "sha256:fddeea3c53fa99d0cdb613c3941cc6e52d822491fc2753fba25768fb5bf4e865"}, @@ -930,10 +780,6 @@ bcrypt = [ {file = "bcrypt-3.2.2-cp36-abi3-win_amd64.whl", hash = "sha256:7ff2069240c6bbe49109fe84ca80508773a904f5a8cb960e02a977f7f519b129"}, {file = "bcrypt-3.2.2.tar.gz", hash = "sha256:433c410c2177057705da2a9f2cd01dd157493b2a7ac14c8593a16b3dab6b6bfb"}, ] -binaryornot = [ - {file = "binaryornot-0.4.4-py2.py3-none-any.whl", hash = "sha256:b8b71173c917bddcd2c16070412e369c3ed7f0528926f70cac18a6c97fd563e4"}, - {file = "binaryornot-0.4.4.tar.gz", hash = "sha256:359501dfc9d40632edc9fac890e19542db1a287bbcfa58175b66658392018061"}, -] black = [ {file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"}, {file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"}, @@ -1015,10 +861,6 @@ cffi = [ {file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"}, {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"}, ] -chardet = [ - {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, - {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, -] charset-normalizer = [ {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, @@ -1031,10 +873,6 @@ colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] -cookiecutter = [ - {file = "cookiecutter-1.7.3-py2.py3-none-any.whl", hash = "sha256:f8671531fa96ab14339d0c59b4f662a4f12a2ecacd94a0f70a3500843da588e2"}, - {file = "cookiecutter-1.7.3.tar.gz", hash = "sha256:6b9a4d72882e243be077a7397d0f1f76fe66cf3df91f3115dbb5330e214fa457"}, -] coverage = [ {file = "coverage-6.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b27d894748475fa858f9597c0ee1d4829f44683f3813633aaf94b19cb5453cf"}, {file = "coverage-6.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37d1141ad6b2466a7b53a22e08fe76994c2d35a5b6b469590424a9953155afac"}, @@ -1199,14 +1037,6 @@ isort = [ {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, ] -jinja2 = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, -] -jinja2-time = [ - {file = "jinja2-time-0.2.0.tar.gz", hash = "sha256:d14eaa4d315e7688daa4969f616f226614350c48730bfa1692d2caebd8c90d40"}, - {file = "jinja2_time-0.2.0-py2.py3-none-any.whl", hash = "sha256:d3eab6605e3ec8b7a0863df09cc1d23714908fa61aa6986a845c20ba488b4efa"}, -] mako = [ {file = "Mako-1.2.0-py3-none-any.whl", hash = "sha256:23aab11fdbbb0f1051b93793a58323ff937e98e34aece1c4219675122e57e4ba"}, {file = "Mako-1.2.0.tar.gz", hash = "sha256:9a7c7e922b87db3686210cf49d5d767033a41d4010b284e747682c92bddd8b39"}, @@ -1281,10 +1111,6 @@ pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] -poyo = [ - {file = "poyo-0.5.0-py2.py3-none-any.whl", hash = "sha256:3e2ca8e33fdc3c411cd101ca395668395dd5dc7ac775b8e809e3def9f9fe041a"}, - {file = "poyo-0.5.0.tar.gz", hash = "sha256:e26956aa780c45f011ca9886f044590e2d8fd8b61db7b1c1cf4e0869f48ed4dd"}, -] py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, @@ -1355,10 +1181,6 @@ pytest-asyncio = [ {file = "pytest_asyncio-0.18.3-1-py3-none-any.whl", hash = "sha256:16cf40bdf2b4fb7fc8e4b82bd05ce3fbcd454cbf7b92afc445fe299dabb88213"}, {file = "pytest_asyncio-0.18.3-py3-none-any.whl", hash = "sha256:8fafa6c52161addfd41ee7ab35f11836c5a16ec208f93ee388f752bea3493a84"}, ] -python-dateutil = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, -] python-dotenv = [ {file = "python-dotenv-0.20.0.tar.gz", hash = "sha256:b7e3b04a59693c42c36f9ab1cc2acc46fa5df8c78e178fc33a8d4cd05c8d498f"}, {file = "python_dotenv-0.20.0-py3-none-any.whl", hash = "sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938"}, @@ -1366,14 +1188,6 @@ python-dotenv = [ python-multipart = [ {file = "python-multipart-0.0.5.tar.gz", hash = "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"}, ] -python-slugify = [ - {file = "python-slugify-6.1.2.tar.gz", hash = "sha256:272d106cb31ab99b3496ba085e3fea0e9e76dcde967b5e9992500d1f785ce4e1"}, - {file = "python_slugify-6.1.2-py2.py3-none-any.whl", hash = "sha256:7b2c274c308b62f4269a9ba701aa69a797e9bca41aeee5b3a9e79e36b6656927"}, -] -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"}, @@ -1432,10 +1246,6 @@ starlette = [ {file = "starlette-0.18.0-py3-none-any.whl", hash = "sha256:377d64737a0e03560cb8eaa57604afee143cea5a4996933242798a7820e64f53"}, {file = "starlette-0.18.0.tar.gz", hash = "sha256:b45c6e9a617ecb5caf7e6446bd8d767b0084d6217e8e1b08187ca5191e10f097"}, ] -text-unidecode = [ - {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, - {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, -] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, @@ -1448,10 +1258,6 @@ typing-extensions = [ {file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"}, {file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"}, ] -urllib3 = [ - {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, - {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, -] uvicorn = [ {file = "uvicorn-0.17.6-py3-none-any.whl", hash = "sha256:19e2a0e96c9ac5581c01eb1a79a7d2f72bb479691acd2b8921fce48ed5b961a6"}, {file = "uvicorn-0.17.6.tar.gz", hash = "sha256:5180f9d059611747d841a4a4c4ab675edf54c8489e97f96d0583ee90ac3bfc23"}, diff --git a/{{cookiecutter.project_name}}/template_minimal/pyproject.toml b/{{cookiecutter.project_name}}/template_minimal/pyproject.toml index 45422b9..c622f56 100644 --- a/{{cookiecutter.project_name}}/template_minimal/pyproject.toml +++ b/{{cookiecutter.project_name}}/template_minimal/pyproject.toml @@ -20,7 +20,6 @@ toml = "^0.10.2" [tool.poetry.dev-dependencies] autoflake = "^1.4" black = "^22.3.0" -cookiecutter = "^1.7.3" coverage = "^6.3.2" flake8 = "^4.0.1" httpx = "^0.22.0" diff --git a/{{cookiecutter.project_name}}/template_minimal/requirements-dev.txt b/{{cookiecutter.project_name}}/template_minimal/requirements-dev.txt index 08d0ddd..cd9f87e 100644 --- a/{{cookiecutter.project_name}}/template_minimal/requirements-dev.txt +++ b/{{cookiecutter.project_name}}/template_minimal/requirements-dev.txt @@ -1,21 +1,17 @@ alembic==1.7.7; python_version >= "3.6" anyio==3.5.0; python_version >= "3.6" and python_full_version >= "3.6.2" -arrow==1.2.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" asgiref==3.5.1; python_version >= "3.7" asyncpg==0.25.0; python_full_version >= "3.6.0" atomicwrites==1.4.0; python_version >= "3.7" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.4.0" attrs==21.4.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" autoflake==1.4 bcrypt==3.2.2; python_version >= "3.6" -binaryornot==0.4.4; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" black==22.3.0; 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" +certifi==2021.10.8; python_version >= "3.6" cffi==1.15.0 -chardet==4.0.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" -charset-normalizer==2.0.12; python_full_version >= "3.6.0" and python_version >= "3.6" -click==8.1.3; python_version >= "3.7" and python_full_version >= "3.6.2" and (python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7") -colorama==0.4.4; sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.6.2" and platform_system == "Windows" 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" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.5.0") -cookiecutter==1.7.3; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") +charset-normalizer==2.0.12; python_full_version >= "3.5.0" and python_version >= "3.6" +click==8.1.3; python_version >= "3.7" and python_full_version >= "3.6.2" +colorama==0.4.4; sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.6.2" and platform_system == "Windows" and (python_version >= "3.7" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.5.0") coverage==6.3.2; python_version >= "3.7" cryptography==37.0.1; python_version >= "3.6" dnspython==2.2.1; python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.6.1" @@ -29,10 +25,8 @@ httpx==0.22.0; python_version >= "3.6" idna==3.3 iniconfig==1.1.1; python_version >= "3.7" isort==5.10.1; python_full_version >= "3.6.1" and python_version < "4.0" -jinja2-time==0.2.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" -jinja2==3.1.2; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" mako==1.2.0; python_version >= "3.7" -markupsafe==2.1.1; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" +markupsafe==2.1.1; python_version >= "3.7" 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.7" @@ -40,7 +34,6 @@ passlib==1.7.4 pathspec==0.9.0; python_full_version >= "3.6.2" platformdirs==2.5.2; python_version >= "3.7" and python_full_version >= "3.6.2" pluggy==1.0.0; python_version >= "3.7" -poyo==0.5.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" py==1.11.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" 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 @@ -50,20 +43,15 @@ pyjwt==2.3.0; python_version >= "3.6" pyparsing==3.0.8; python_full_version >= "3.6.8" and python_version >= "3.7" pytest-asyncio==0.18.3; python_version >= "3.7" pytest==7.1.2; python_version >= "3.7" -python-dateutil==2.8.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" python-dotenv==0.20.0; python_full_version >= "3.6.1" and python_version >= "3.5" python-multipart==0.0.5 -python-slugify==6.1.2; 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" -six==1.16.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" +six==1.16.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" sniffio==1.2.0; python_version >= "3.6" and python_full_version >= "3.6.2" sqlalchemy2-stubs==0.0.2a22; python_version >= "3.6" sqlalchemy==1.4.36; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") starlette==0.18.0; python_version >= "3.6" and python_full_version >= "3.6.1" -text-unidecode==1.3; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" toml==0.10.2; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.3.0") tomli==2.0.1; python_version < "3.11" and python_full_version >= "3.6.2" and python_version >= "3.7" typing-extensions==4.2.0; python_version >= "3.7" and python_full_version >= "3.6.1" -urllib3==1.26.9; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version < "4" uvicorn==0.17.6; python_version >= "3.7" From 3574ba50df9221d5aa9d6f838b6a83032f97035b Mon Sep 17 00:00:00 2001 From: rafsaf Date: Sat, 7 May 2022 10:53:34 +0200 Subject: [PATCH 05/20] Fix tests.yml python version typo --- .github/workflows/tests.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0c2346b..e1186fb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,7 +2,6 @@ name: Run tests on: push: - pull_request: jobs: build: @@ -25,7 +24,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: 3.10 + python-version: "3.10" - name: Generate projects from templates using cookiecutter # fastapi_users_project folder From 181430b2a2464d06386a31366dbedd4c5878f78a Mon Sep 17 00:00:00 2001 From: rafsaf Date: Sat, 7 May 2022 11:42:21 +0200 Subject: [PATCH 06/20] Fixed conftest issue with fixtures template minimal --- .../template_minimal/app/tests/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/{{cookiecutter.project_name}}/template_minimal/app/tests/conftest.py b/{{cookiecutter.project_name}}/template_minimal/app/tests/conftest.py index 85a16a4..8d67d79 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/tests/conftest.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/tests/conftest.py @@ -43,7 +43,7 @@ async def test_db_setup_sessionmaker(): @pytest_asyncio.fixture(autouse=True) -async def session() -> AsyncGenerator[AsyncSession, None]: +async def session(test_db_setup_sessionmaker) -> AsyncGenerator[AsyncSession, None]: async with async_session() as session: yield session @@ -61,7 +61,7 @@ async def client() -> AsyncGenerator[AsyncClient, None]: @pytest_asyncio.fixture() -async def default_user() -> User: +async def default_user(test_db_setup_sessionmaker) -> User: async with async_session() as session: result = await session.execute( select(User).where(User.email == default_user_email) From eb7507e7afbd71ae023de8dc7d346e561f637b3d Mon Sep 17 00:00:00 2001 From: rafsaf Date: Sat, 7 May 2022 12:36:37 +0200 Subject: [PATCH 07/20] Updated docs image, cookiecutter params and tests.yml file name --- cookiecutter.json | 4 ++-- docs/template-minimal-openapi-example.png | Bin 130457 -> 171365 bytes hooks/post_gen_project.py | 4 ++-- tests/create_fastapi_users_project.py | 2 +- tests/create_minimal_project.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cookiecutter.json b/cookiecutter.json index 7b410dc..5782100 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -1,4 +1,4 @@ { - "project_name": "Base Project", - "use_fastapi_users": false + "project_name": "my_project_name", + "experimental_fastapi_users_template": false } diff --git a/docs/template-minimal-openapi-example.png b/docs/template-minimal-openapi-example.png index 92bc199fa4e5028cea17e26008b51f1c9cfaa6e3..e50953d3379d9c160859fb774dd0853bbdd6144b 100644 GIT binary patch literal 171365 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 sxMUdcZz+>pHLV{G6ZjbDr0P+%(YU*(bb@ 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 eG0Pqlk^ Date: Sat, 7 May 2022 12:53:00 +0200 Subject: [PATCH 08/20] Fix typo in branch name in build_docker_image workflow --- .github/workflows/build_docker_image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_docker_image.yml b/.github/workflows/build_docker_image.yml index e1f784e..94d8981 100644 --- a/.github/workflows/build_docker_image.yml +++ b/.github/workflows/build_docker_image.yml @@ -5,7 +5,7 @@ on: workflows: - "Run tests" branches: - - master + - main types: - completed From fb0ada7eb0c23b3845af54959be745a2d06f3a63 Mon Sep 17 00:00:00 2001 From: rafsaf Date: Sat, 7 May 2022 12:59:28 +0200 Subject: [PATCH 09/20] try fix escape problem in build docker workflows with envs --- .github/workflows/build_docker_image.yml | 4 +++- .github/workflows/manual_build_docker_image.yml | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_docker_image.yml b/.github/workflows/build_docker_image.yml index 94d8981..2235b42 100644 --- a/.github/workflows/build_docker_image.yml +++ b/.github/workflows/build_docker_image.yml @@ -24,8 +24,10 @@ jobs: - name: Build and push image uses: docker/build-push-action@v2 + env: + cookiecutter_project_name: "{{cookiecutter_project_name}}" with: file: Dockerfile - context: ./{{cookiecutter.project_name}}/template_minimal + context: ./${{cookiecutter_project_name}}/template_minimal push: true tags: rafsaf/minimal-fastapi-postgres-template:latest diff --git a/.github/workflows/manual_build_docker_image.yml b/.github/workflows/manual_build_docker_image.yml index 8f2e8a7..0f1157d 100644 --- a/.github/workflows/manual_build_docker_image.yml +++ b/.github/workflows/manual_build_docker_image.yml @@ -22,8 +22,10 @@ jobs: - name: Build and push image uses: docker/build-push-action@v2 + env: + cookiecutter_project_name: "{{cookiecutter_project_name}}" with: file: Dockerfile - context: ./{{cookiecutter.project_name}}/template_minimal + context: ./${{cookiecutter_project_name}}/template_minimal push: true tags: rafsaf/minimal-fastapi-postgres-template:${{ github.event.inputs.tag }} From 55f411eb4bf98454fee2ba273cb2c81aac6d022a Mon Sep 17 00:00:00 2001 From: rafsaf Date: Sat, 7 May 2022 13:03:24 +0200 Subject: [PATCH 10/20] Final fix for build docker images workflow context --- .github/workflows/build_docker_image.yml | 6 +++--- .github/workflows/manual_build_docker_image.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build_docker_image.yml b/.github/workflows/build_docker_image.yml index 2235b42..8cec015 100644 --- a/.github/workflows/build_docker_image.yml +++ b/.github/workflows/build_docker_image.yml @@ -24,10 +24,10 @@ jobs: - name: Build and push image uses: docker/build-push-action@v2 - env: - cookiecutter_project_name: "{{cookiecutter_project_name}}" with: file: Dockerfile - context: ./${{cookiecutter_project_name}}/template_minimal + context: ./${{ secrets.cookiecutter_project_name }}/template_minimal + # workaround due to escape problems + # secrets.cookiecutter_project_name="{{cookiecutter.project_name}}" push: true tags: rafsaf/minimal-fastapi-postgres-template:latest diff --git a/.github/workflows/manual_build_docker_image.yml b/.github/workflows/manual_build_docker_image.yml index 0f1157d..a09c030 100644 --- a/.github/workflows/manual_build_docker_image.yml +++ b/.github/workflows/manual_build_docker_image.yml @@ -22,10 +22,10 @@ jobs: - name: Build and push image uses: docker/build-push-action@v2 - env: - cookiecutter_project_name: "{{cookiecutter_project_name}}" with: file: Dockerfile - context: ./${{cookiecutter_project_name}}/template_minimal + context: ./${{ secrets.cookiecutter_project_name }}/template_minimal + # workaround due to escape problems + # secrets.cookiecutter_project_name="{{cookiecutter.project_name}}" push: true tags: rafsaf/minimal-fastapi-postgres-template:${{ github.event.inputs.tag }} From 940c8a4eb38bc0fd976c9aff904f31998e553f43 Mon Sep 17 00:00:00 2001 From: rafsaf Date: Sat, 7 May 2022 13:07:17 +0200 Subject: [PATCH 11/20] Next try context docker build workflows --- .github/workflows/build_docker_image.yml | 6 +++--- .github/workflows/manual_build_docker_image.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build_docker_image.yml b/.github/workflows/build_docker_image.yml index 8cec015..06c2475 100644 --- a/.github/workflows/build_docker_image.yml +++ b/.github/workflows/build_docker_image.yml @@ -24,10 +24,10 @@ jobs: - name: Build and push image uses: docker/build-push-action@v2 + env: + cookiecutter_project_name: "{{cookiecutter.project_name}}" with: file: Dockerfile - context: ./${{ secrets.cookiecutter_project_name }}/template_minimal - # workaround due to escape problems - # secrets.cookiecutter_project_name="{{cookiecutter.project_name}}" + context: ./{{cookiecutter_project_name}}/template_minimal push: true tags: rafsaf/minimal-fastapi-postgres-template:latest diff --git a/.github/workflows/manual_build_docker_image.yml b/.github/workflows/manual_build_docker_image.yml index a09c030..c9b334a 100644 --- a/.github/workflows/manual_build_docker_image.yml +++ b/.github/workflows/manual_build_docker_image.yml @@ -22,10 +22,10 @@ jobs: - name: Build and push image uses: docker/build-push-action@v2 + env: + cookiecutter_project_name: "{{cookiecutter.project_name}}" with: file: Dockerfile - context: ./${{ secrets.cookiecutter_project_name }}/template_minimal - # workaround due to escape problems - # secrets.cookiecutter_project_name="{{cookiecutter.project_name}}" + context: ./{{cookiecutter_project_name}}/template_minimal push: true tags: rafsaf/minimal-fastapi-postgres-template:${{ github.event.inputs.tag }} From a41229ce4fe6324504806019a97af43d24b09d18 Mon Sep 17 00:00:00 2001 From: rafsaf Date: Sat, 7 May 2022 13:12:45 +0200 Subject: [PATCH 12/20] Final fix for invalid context in docker image workflows - use cp -r --- .github/workflows/build_docker_image.yml | 8 +++++--- .github/workflows/manual_build_docker_image.yml | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build_docker_image.yml b/.github/workflows/build_docker_image.yml index 06c2475..395a763 100644 --- a/.github/workflows/build_docker_image.yml +++ b/.github/workflows/build_docker_image.yml @@ -16,6 +16,10 @@ jobs: steps: - uses: actions/checkout@v2 + - name: copy minimal template to root folder + run: | + cp -r \{\{cookiecutter.project_name\}\}/template_minimal/ . + - name: Login to DockerHub uses: docker/login-action@v1 with: @@ -24,10 +28,8 @@ jobs: - name: Build and push image uses: docker/build-push-action@v2 - env: - cookiecutter_project_name: "{{cookiecutter.project_name}}" with: file: Dockerfile - context: ./{{cookiecutter_project_name}}/template_minimal + context: ./template_minimal push: true tags: rafsaf/minimal-fastapi-postgres-template:latest diff --git a/.github/workflows/manual_build_docker_image.yml b/.github/workflows/manual_build_docker_image.yml index c9b334a..dbed71e 100644 --- a/.github/workflows/manual_build_docker_image.yml +++ b/.github/workflows/manual_build_docker_image.yml @@ -14,6 +14,10 @@ jobs: steps: - uses: actions/checkout@v2 + - name: copy minimal template to root folder + run: | + cp -r \{\{cookiecutter.project_name\}\}/template_minimal/ . + - name: Login to DockerHub uses: docker/login-action@v1 with: @@ -22,10 +26,8 @@ jobs: - name: Build and push image uses: docker/build-push-action@v2 - env: - cookiecutter_project_name: "{{cookiecutter.project_name}}" with: file: Dockerfile - context: ./{{cookiecutter_project_name}}/template_minimal + context: ./template_minimal push: true tags: rafsaf/minimal-fastapi-postgres-template:${{ github.event.inputs.tag }} From d433f860e3fc43c7fdccc92416fadc7fdc7295fd Mon Sep 17 00:00:00 2001 From: rafsaf Date: Sat, 7 May 2022 13:30:49 +0200 Subject: [PATCH 13/20] Fix manual and build docker images workflows --- .github/workflows/build_docker_image.yml | 17 ++++++++++++----- .github/workflows/manual_build_docker_image.yml | 17 ++++++++++++----- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build_docker_image.yml b/.github/workflows/build_docker_image.yml index 395a763..c1df7ee 100644 --- a/.github/workflows/build_docker_image.yml +++ b/.github/workflows/build_docker_image.yml @@ -16,9 +16,16 @@ jobs: steps: - uses: actions/checkout@v2 - - name: copy minimal template to root folder + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: "3.10" + + - name: Generate projects from templates using cookiecutter + # minimal_project folder run: | - cp -r \{\{cookiecutter.project_name\}\}/template_minimal/ . + pip install cookiecutter + python tests/create_minimal_project.py - name: Login to DockerHub uses: docker/login-action@v1 @@ -27,9 +34,9 @@ jobs: password: ${{ secrets.DOCKER_PASS }} - name: Build and push image - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v3 with: - file: Dockerfile - context: ./template_minimal + file: minimal_project/Dockerfile + context: minimal_project push: true tags: rafsaf/minimal-fastapi-postgres-template:latest diff --git a/.github/workflows/manual_build_docker_image.yml b/.github/workflows/manual_build_docker_image.yml index dbed71e..abf7fbd 100644 --- a/.github/workflows/manual_build_docker_image.yml +++ b/.github/workflows/manual_build_docker_image.yml @@ -14,9 +14,16 @@ jobs: steps: - uses: actions/checkout@v2 - - name: copy minimal template to root folder + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: "3.10" + + - name: Generate projects from templates using cookiecutter + # minimal_project folder run: | - cp -r \{\{cookiecutter.project_name\}\}/template_minimal/ . + pip install cookiecutter + python tests/create_minimal_project.py - name: Login to DockerHub uses: docker/login-action@v1 @@ -25,9 +32,9 @@ jobs: password: ${{ secrets.DOCKER_PASS }} - name: Build and push image - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v3 with: - file: Dockerfile - context: ./template_minimal + file: minimal_project/Dockerfile + context: minimal_project push: true tags: rafsaf/minimal-fastapi-postgres-template:${{ github.event.inputs.tag }} From 2bac2afb030ef7da7d82eab9abac042d2f8ffad5 Mon Sep 17 00:00:00 2001 From: rafsaf Date: Sat, 7 May 2022 15:46:35 +0200 Subject: [PATCH 14/20] rename user table to user_model to avoid postgresql issues --- ...> 2022050745_init_user_model_d1252175c146.py} | 16 ++++++++-------- .../template_minimal/app/models.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) rename {{cookiecutter.project_name}}/template_minimal/alembic/versions/{2022050753_init_user_model_940215fe1ae8.py => 2022050745_init_user_model_d1252175c146.py} (64%) diff --git a/{{cookiecutter.project_name}}/template_minimal/alembic/versions/2022050753_init_user_model_940215fe1ae8.py b/{{cookiecutter.project_name}}/template_minimal/alembic/versions/2022050745_init_user_model_d1252175c146.py similarity index 64% rename from {{cookiecutter.project_name}}/template_minimal/alembic/versions/2022050753_init_user_model_940215fe1ae8.py rename to {{cookiecutter.project_name}}/template_minimal/alembic/versions/2022050745_init_user_model_d1252175c146.py index cd12b43..3418bca 100644 --- a/{{cookiecutter.project_name}}/template_minimal/alembic/versions/2022050753_init_user_model_940215fe1ae8.py +++ b/{{cookiecutter.project_name}}/template_minimal/alembic/versions/2022050745_init_user_model_d1252175c146.py @@ -1,8 +1,8 @@ """init_user_model -Revision ID: 940215fe1ae8 +Revision ID: d1252175c146 Revises: -Create Date: 2022-05-07 04:53:32.108714 +Create Date: 2022-05-07 15:45:09.674258 """ from alembic import op @@ -10,7 +10,7 @@ from sqlalchemy.dialects import postgresql # revision identifiers, used by Alembic. -revision = "940215fe1ae8" +revision = "d1252175c146" down_revision = None branch_labels = None depends_on = None @@ -19,18 +19,18 @@ def upgrade(): # ### commands auto generated by Alembic - please adjust! ### op.create_table( - "user", - sa.Column("id", postgresql.UUID(), nullable=False), + "user_model", + sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False), 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_model_email"), "user_model", ["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") + op.drop_index(op.f("ix_user_model_email"), table_name="user_model") + op.drop_table("user_model") # ### end Alembic commands ### diff --git a/{{cookiecutter.project_name}}/template_minimal/app/models.py b/{{cookiecutter.project_name}}/template_minimal/app/models.py index 07f8c1a..4a0f214 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/models.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/models.py @@ -27,7 +27,7 @@ @Base.mapped @dataclass class User: - __tablename__ = "user" + __tablename__ = "user_model" __sa_dataclass_metadata_key__ = "sa" id: uuid.UUID = field( From 55a8e5fb543d42ca3f4066b58dd4d0ec8af943de Mon Sep 17 00:00:00 2001 From: rafsaf Date: Sat, 7 May 2022 16:26:50 +0200 Subject: [PATCH 15/20] nginx unit config root user --- .../template_minimal/nginx-unit-config.json | 1 + 1 file changed, 1 insertion(+) diff --git a/{{cookiecutter.project_name}}/template_minimal/nginx-unit-config.json b/{{cookiecutter.project_name}}/template_minimal/nginx-unit-config.json index 968377f..4db25e9 100644 --- a/{{cookiecutter.project_name}}/template_minimal/nginx-unit-config.json +++ b/{{cookiecutter.project_name}}/template_minimal/nginx-unit-config.json @@ -7,6 +7,7 @@ "applications": { "fastapi": { "type": "python 3.10", + "user": "root", "processes": 1, "threads": 1, "path": "/build/", From 81b5287f49a1c2bf0646c5583519e2c73fee7bef Mon Sep 17 00:00:00 2001 From: rafsaf Date: Tue, 10 May 2022 16:29:28 +0200 Subject: [PATCH 16/20] Finish new Readme --- README.md | 365 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 234 insertions(+), 131 deletions(-) diff --git a/README.md b/README.md index faa7f97..84fdc05 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,44 @@ - - Test - - License +Python +Black + + Live example + + + Tests + -## Minimal FastAPI Postgres Template - -There are two templates to choose from, first **minimal template** 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. - -Seconds one, **fastapi-users template** is similar to minimal in structure, but based on popular library [FastAPI Users](https://fastapi-users.github.io/fastapi-users/) and very powerful (yet it require using it for user accounts management). - -:bangbang: | This repository contains two different templates to choose from. -:---: | :--- - -## Minimal async FastAPI + postgresql template - - - -- 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` +# Minimal async FastAPI + PostgreSQL template + +- [Feauters](#features) +- [Quickstart](#quickstart) +- [About](#about) +- [Step by step example - POST and GET endpoints](#step-by-step-example---post-and-get-endpoints) + - [1. Create SQLAlchemy model](#1-create-sqlalchemy-model) + - [2. Create and apply alembic migration](#2-create-and-apply-alembic-migration) + - [3. Create request and response schemas](#3-create-request-and-response-schemas) + - [4. Create endpoint](#4-create-endpoints) + - [5. Write tests](#5-write-tests) +- [Deployment strategies - via Docker image](#deployment-strategies---via-docker-image) + +## Features + +- [x] SQLAlchemy 1.4 using new 2.0 API, async queries, and dataclasses in SQLAlchemy models for best possible autocompletion support +- [x] Postgresql database under `asyncpg` +- [x] [Alembic](https://alembic.sqlalchemy.org/en/latest/) migrations +- [x] Very minimal project structure yet ready for quick start building new apps +- [x] Refresh token endpoint (not only access like in official template) +- [x] Two databases in docker-compose.yml (second one for tests) and ready to go Dockerfile with [Nginx Unit](https://unit.nginx.org/) webserver +- [x] [Poetry](https://python-poetry.org/docs/) and Python 3.10 based +- [x] `pre-push.sh` script with poetry export, autoflake, black, isort and flake8 +- [x] Rich setup for pytest async tests with few included and extensible `conftest.py` + +_Check out also online example: https://minimal-fastapi-postgres-template.rafsaf.pl, it's 100% code used in template with added domain and https only._ ![template-fastapi-minimal-openapi-example](./docs/template-minimal-openapi-example.png) -## Async FastAPI + postgresql template based on [fastapi-users](https://fastapi-users.github.io/fastapi-users/) - -- Extensible base user model -- Ready-to-use register, login, reset password and verify e-mail routes -- Ready-to-use social OAuth2 login flow -- 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 -- 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) - - ## Quickstart ```bash @@ -57,8 +48,9 @@ pip install cookiecutter # And cookiecutter this project :) cookiecutter https://github.com/rafsaf/minimal-fastapi-postgres-template -# decide if u want fastapi-users based template by -# changing "use_fastapi_users" in cookiecutter option to True, by default it is minimal template. +# if you want experimental fastapi-users template +# check "experimental_fastapi_users_template" +# to True in cookiecutter option cd project_name # Poetry install (and activate environment!) @@ -69,180 +61,291 @@ docker-compose up -d bash init.sh # And this is it: uvicorn app.main:app --reload + +# Optionally - use git init to initialize git repository ``` -tests: +### Running tests ```bash # Note, it will use second database declared in docker-compose.yml, not default one pytest +# collected 7 items + +# app/tests/test_auth.py::test_auth_access_token PASSED [ 14%] +# app/tests/test_auth.py::test_auth_access_token_fail_no_user PASSED [ 28%] +# app/tests/test_auth.py::test_auth_refresh_token PASSED [ 42%] +# app/tests/test_users.py::test_read_current_user PASSED [ 57%] +# app/tests/test_users.py::test_delete_current_user PASSED [ 71%] +# app/tests/test_users.py::test_reset_current_user_password PASSED [ 85%] +# app/tests/test_users.py::test_register_new_user PASSED [100%] +# +# ======================================================== 7 passed in 1.75s ======================================================== ``` ## About -This project is heavily base on official template https://github.com/tiangolo/full-stack-fastapi-postgresql (and on my previous work: [link1](https://github.com/rafsaf/fastapi-plan), [link2](https://github.com/rafsaf/docker-fastapi-projects)), but as it is now not too much up-to-date, it is much easier to create new one than change official. I didn't like some of conventions over there also (`crud` and `db` folders for example). +This project is heavily based on the official template https://github.com/tiangolo/full-stack-fastapi-postgresql (and on my previous work: [link1](https://github.com/rafsaf/fastapi-plan), [link2](https://github.com/rafsaf/docker-fastapi-projects)), but as it now not too much up-to-date, it is much easier to create new one than change official. I didn't like some of conventions over there also (`crud` and `db` folders for example or `schemas` with bunch of files). + +`2.0` style SQLAlchemy API is good enough so there is no need to write everything in `crud` and waste our time... The `core` folder was also rewritten. There is great base for writting tests in `tests`, but I didn't want to write hundreds of them, I noticed that usually after changes in the structure of the project, auto tests are useless and you have to write them from scratch anyway (delete old ones...), hence less than more. Similarly with the `User` model, it is very modest, with just id, email and password, because it will be adapted to the project anyway. + +
-`2.0` style SQLAlchemy API is good enough so there is no need to write everything in `crud` and waste our time... The `core` folder was also rewritten. There is great base for writting tests in `tests`, but I didn't want to write hundreds of them, I noticed that usually after changes in the structure of the project, auto tests are useless and you have to write them from scratch anyway (delete old ones...), hence less than more. Similarly with the `User` model, it is very modest, because it will be adapted to the project anyway (and there are no tests for these endpoints, you would remove them probably). +## Step by step example - POST and GET endpoints -On January 30 I added another template to this repository, that is based on [FastAPI Users](https://fastapi-users.github.io/fastapi-users/) project. The main assumptions have not changed and project structure is very similar. What it gives is not buggy, extensible, tested Users accounts managing system. This is a lot, if your project needs to manage accounts, this is definitely better than minimal template. +I always enjoy to have some kind of an example in templates (even if I don't like it much, _some_ parts may be useful and save my time...), so let's create two example endpoints: -## Step by step example for minimal template +- `POST` endpoint `/pets/create` for creating `Pets` with relation to currently logged `User` +- `GET` endpoint `/pets/me` for fetching all user's pets. -I always enjoy to to have some kind of example in templates (even if I don't like it much, _some_ parts may be useful and save my time...), so let's create `POST` endpoint for creating dogs. For second template, there may be some differences (imports etc.) +
-### 1. Add `HappyDog` model +### 1. Create SQLAlchemy model + +We will add Pet model to `app/models.py`. To keep things clear, below is full result of models.py file. ```python -# /app/models.py -(...) +# app/models.py + +import uuid +from dataclasses import dataclass, field + +from sqlalchemy import Column, ForeignKey, Integer, String +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import registry, relationship + +Base = registry() + + +@Base.mapped +@dataclass +class User: + __tablename__ = "user_model" + __sa_dataclass_metadata_key__ = "sa" + + id: uuid.UUID = field( + init=False, + default_factory=uuid.uuid4, + metadata={"sa": Column(UUID(as_uuid=True), primary_key=True)}, + ) + email: str = field( + metadata={"sa": Column(String(254), nullable=False, unique=True, index=True)} + ) + hashed_password: str = field(metadata={"sa": Column(String(128), nullable=False)}) + + +@Base.mapped +@dataclass +class Pet: + __tablename__ = "pets" + __sa_dataclass_metadata_key__ = "sa" + + id: int = field(init=False, metadata={"sa": Column(Integer, primary_key=True)}) + user_id: uuid.UUID = field( + metadata={"sa": Column(ForeignKey("user_model.id", ondelete="CASCADE"))}, + ) + pet_name: str = field( + metadata={"sa": Column(String(50), nullable=False)}, + ) + + -class HappyDog(Base): - __tablename__ = "happy_dog" - id = Column(Integer, primary_key=True, index=True) - puppy_name = Column(String(500)) - puppy_age = Column(Integer) ``` -### 2. Create and apply alembic migrations +Note, we are using super powerful SQLAlchemy feature here - you can read more about this fairy new syntax based on dataclasses [in this topic in docs](https://docs.sqlalchemy.org/en/14/orm/declarative_styles.html#example-two-dataclasses-with-declarative-table). + +
+ +### 2. Create and apply alembic migration ```bash -# Run -alembic revision --autogenerate -m "add_happy_dog" +### Use below commands in root folder in virtualenv ### + +# if you see FAILED: Target database is not up to date. +# first use alembic upgrade head + +# Create migration with alembic revision +alembic revision --autogenerate -m "create_pet_model" + + +# File similar to "2022050949_create_pet_model_44b7b689ea5f.py" should appear in `/alembic/versions` folder -# Somethig like `YYYY-MM-DD-....py` will appear in `/alembic/versions` folder +# Apply migration using alembic upgrade alembic upgrade head # (...) -# INFO [alembic.runtime.migration] Running upgrade cefce371682e -> 038f530b0e9b, add_happy_dog +# INFO [alembic.runtime.migration] Running upgrade d1252175c146 -> 44b7b689ea5f, create_pet_model ``` PS. Note, alembic is configured in a way that it work with async setup and also detects specific column changes. -### 3. Create schemas +
-```python -# /app/schemas/happy_dog.py +### 3. Create request and response schemas -from typing import Optional +Note, I personally lately (after seeing clear benefits at work) don't prefer less file than more for things like schemas. -from pydantic import BaseModel +Thats why there are only 2 files: `requests.py` and `responses.py` in `schemas` and I would keep it that way even for few dozen of endpoints. +```python +# app/schemas/requests.py -class BaseHappyDog(BaseModel): - puppy_name: str - puppy_age: Optional[int] +(...) -class CreateHappyDog(BaseHappyDog): - pass +class PetCreateRequest(BaseRequest): + pet_name: str +``` -class HappyDog(BaseHappyDog): - id: int +```python +# app/schemas/responses.py -``` +(...) -Then add it to schemas `__init__.py` -```python -# /app/schemas/__init__.py +class PetResponse(BaseResponse): + id: int + pet_name: str + user_id: uuid.UUID -from .token import Token, TokenPayload, TokenRefresh -from .user import User, UserCreate, UserUpdate -from .happy_dog import HappyDog, CreateHappyDog ``` -### 4. Create endpoint +
+ +### 4. Create endpoints ```python -# /app/api/endpoints/dogs.py +# /app/api/endpoints/pets.py -from typing import Any from fastapi import APIRouter, Depends +from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from app import models, schemas from app.api import deps +from app.models import Pet, User +from app.schemas.requests import PetCreateRequest +from app.schemas.responses import PetResponse router = APIRouter() -@router.post("/", response_model=schemas.HappyDog, status_code=201) -async def create_happy_dog( - dog_create: schemas.CreateHappyDog, +@router.post("/create", response_model=PetResponse, status_code=201) +async def create_new_pet( + new_pet: PetCreateRequest, session: AsyncSession = Depends(deps.get_session), - current_user: models.User = Depends(deps.get_current_active_user), -) -> Any: - """ - Creates new happy dog. Only for logged users. - """ - new_dog = models.HappyDog( - puppy_name=dog_create.puppy_name, puppy_age=dog_create.puppy_age - ) + current_user: User = Depends(deps.get_current_user), +): + """Creates new pet. Only for logged users.""" + + pet = Pet(user_id=current_user.id, pet_name=new_pet.pet_name) - session.add(new_dog) + session.add(pet) await session.commit() - await session.refresh(new_dog) + return pet - return new_dog + +@router.get("/me", response_model=list[PetResponse], status_code=200) +async def get_all_my_pets( + session: AsyncSession = Depends(deps.get_session), + current_user: User = Depends(deps.get_current_user), +): + """Creates new pet. Only for logged users.""" + + pets = await session.execute( + select(Pet) + .where( + Pet.user_id == current_user.id, + ) + .order_by(Pet.pet_name) + ) + return pets.scalars().all() ``` -Also, add it to router +Also, we need to add newly created endpoints to router. ```python # /app/api/api.py from fastapi import APIRouter -from app.api.endpoints import auth, users, dogs +from app.api.endpoints import auth, pets, users api_router = APIRouter() api_router.include_router(auth.router, prefix="/auth", tags=["auth"]) api_router.include_router(users.router, prefix="/users", tags=["users"]) -# new content below -api_router.include_router(dogs.router, prefix="/dogs", tags=["dogs"]) +api_router.include_router(pets.router, prefix="/pets", tags=["pets"]) ``` -### 5. Test it simply +
+ +### 5. Write tests + +No ```python -# /app/tests/test_dogs.py +# /app/tests/test_pets.py -import pytest from httpx import AsyncClient -from app.models import User +from sqlalchemy.ext.asyncio import AsyncSession -pytestmark = pytest.mark.asyncio +from app.main import app +from app.models import Pet, User -async def test_dog_endpoint(client: AsyncClient, default_user: User): - # better to create fixture auth_client or similar than repeat code with access_token - access_token = await client.post( - "/auth/access-token", - data={ - "username": "user@email.com", - "password": "password", - }, - headers={"Content-Type": "application/x-www-form-urlencoded"}, +async def test_create_new_pet( + client: AsyncClient, default_user_headers, default_user: User +): + response = await client.post( + app.url_path_for("create_new_pet"), + headers=default_user_headers, + json={"pet_name": "Tadeusz"}, ) - assert access_token.status_code == 200 - access_token = access_token.json()["access_token"] + assert response.status_code == 201 + result = response.json() + assert result["user_id"] == str(default_user.id) + assert result["pet_name"] == "Tadeusz" - puppy_name = "Sonia" - puppy_age = 6 - create_dog = await client.post( - "/dogs/", - json={"puppy_name": puppy_name, "puppy_age": puppy_age}, - headers={"Authorization": f"Bearer {access_token}"}, +async def test_get_all_my_pets( + client: AsyncClient, default_user_headers, default_user: User, session: AsyncSession +): + + pet1 = Pet(default_user.id, "Pet_1") + pet2 = Pet(default_user.id, "Pet_2") + session.add(pet1) + session.add(pet2) + await session.commit() + + response = await client.get( + app.url_path_for("get_all_my_pets"), + headers=default_user_headers, ) - assert create_dog.status_code == 201 - create_dog_json = create_dog.json() - assert create_dog_json["puppy_name"] == puppy_name - assert create_dog_json["puppy_age"] == puppy_age + assert response.status_code == 200 + + assert response.json() == [ + { + "user_id": str(pet1.user_id), + "pet_name": pet1.pet_name, + "id": pet1.id, + }, + { + "user_id": str(pet2.user_id), + "pet_name": pet2.pet_name, + "id": pet2.id, + }, + ] ``` + +## Deployment strategies - via Docker image + +This template has by default included `Dockerfile` with [Nginx Unit](https://unit.nginx.org/) webserver, that is my prefered choice, because of direct support for FastAPI and great ease of configuration. You should be able to run container(s) (over :80 port) and then need to setup the proxy, loadbalancer, with https enbaled, so the app stays behind it. + +`nginx-unit-config.json` file included in main folder has some default configuration options, runs app in single process and thread. More info about config file here https://unit.nginx.org/configuration/#python and about also read howto for FastAPI: https://unit.nginx.org/howto/fastapi/. + +If you prefer other webservers for FastAPI, check out [Daphne](https://github.com/django/daphne), [Hypercorn](https://pgjones.gitlab.io/hypercorn/index.html) or [Uvicorn](https://www.uvicorn.org/). From 48a2e921dd84f3ce57f6b65882258aa8e20c6f17 Mon Sep 17 00:00:00 2001 From: rafsaf Date: Tue, 10 May 2022 16:38:36 +0200 Subject: [PATCH 17/20] removed name param in endpoints, cleaner default_user_headers fixture and test utils --- .../template_minimal/app/api/endpoints/auth.py | 4 ++-- .../template_minimal/app/api/endpoints/users.py | 10 ++++------ .../template_minimal/app/tests/conftest.py | 6 +++--- .../template_minimal/app/tests/test_auth.py | 6 +++--- .../template_minimal/app/tests/utils.py | 10 ---------- 5 files changed, 12 insertions(+), 24 deletions(-) delete mode 100644 {{cookiecutter.project_name}}/template_minimal/app/tests/utils.py 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 7a5ce6d..2a35349 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/auth.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/auth.py @@ -16,7 +16,7 @@ router = APIRouter() -@router.post("/access-token", response_model=AccessTokenResponse, name="access_token") +@router.post("/access-token", response_model=AccessTokenResponse) async def login_access_token( session: AsyncSession = Depends(deps.get_session), form_data: OAuth2PasswordRequestForm = Depends(), @@ -35,7 +35,7 @@ async def login_access_token( return security.generate_access_token_response(str(user.id)) -@router.post("/refresh-token", response_model=AccessTokenResponse, name="refresh_token") +@router.post("/refresh-token", response_model=AccessTokenResponse) async def refresh_token( input: RefreshTokenRequest, session: AsyncSession = Depends(deps.get_session), diff --git a/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/users.py b/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/users.py index 15deb49..d933fca 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/users.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/users.py @@ -11,7 +11,7 @@ router = APIRouter() -@router.get("/me", response_model=UserResponse, name="read_current_user") +@router.get("/me", response_model=UserResponse) async def read_current_user( current_user: User = Depends(deps.get_current_user), ): @@ -19,7 +19,7 @@ async def read_current_user( return current_user -@router.delete("/me", status_code=204, name="delete_current_user") +@router.delete("/me", status_code=204) async def delete_current_user( current_user: User = Depends(deps.get_current_user), session: AsyncSession = Depends(deps.get_session), @@ -29,9 +29,7 @@ async def delete_current_user( await session.commit() -@router.post( - "/reset-password", response_model=UserResponse, name="reset_current_user_password" -) +@router.post("/reset-password", response_model=UserResponse) async def reset_current_user_password( user_update_password: UserUpdatePasswordRequest, session: AsyncSession = Depends(deps.get_session), @@ -44,7 +42,7 @@ async def reset_current_user_password( return current_user -@router.post("/register", response_model=UserResponse, name="register_new_user") +@router.post("/register", response_model=UserResponse) async def register_new_user( new_user: UserCreateRequest, session: AsyncSession = Depends(deps.get_session), diff --git a/{{cookiecutter.project_name}}/template_minimal/app/tests/conftest.py b/{{cookiecutter.project_name}}/template_minimal/app/tests/conftest.py index 8d67d79..66c081b 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/tests/conftest.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/tests/conftest.py @@ -60,7 +60,7 @@ async def client() -> AsyncGenerator[AsyncClient, None]: yield client -@pytest_asyncio.fixture() +@pytest_asyncio.fixture async def default_user(test_db_setup_sessionmaker) -> User: async with async_session() as session: result = await session.execute( @@ -80,6 +80,6 @@ async def default_user(test_db_setup_sessionmaker) -> User: return user -@pytest_asyncio.fixture() -async def default_user_headers(default_user: User): +@pytest.fixture +def default_user_headers(default_user: User): return {"Authorization": f"Bearer {default_user_access_token}"} 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 56198eb..f408b19 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/tests/test_auth.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/tests/test_auth.py @@ -7,7 +7,7 @@ async def test_auth_access_token(client: AsyncClient, default_user: User): response = await client.post( - app.url_path_for("access_token"), + app.url_path_for("login_access_token"), data={ "username": default_user_email, "password": default_user_password, @@ -27,7 +27,7 @@ async def test_auth_access_token(client: AsyncClient, default_user: User): async def test_auth_access_token_fail_no_user(client: AsyncClient): response = await client.post( - app.url_path_for("access_token"), + app.url_path_for("login_access_token"), data={ "username": "xxx", "password": "yyy", @@ -41,7 +41,7 @@ async def test_auth_access_token_fail_no_user(client: AsyncClient): async def test_auth_refresh_token(client: AsyncClient, default_user: User): response = await client.post( - app.url_path_for("access_token"), + app.url_path_for("login_access_token"), data={ "username": default_user_email, "password": default_user_password, diff --git a/{{cookiecutter.project_name}}/template_minimal/app/tests/utils.py b/{{cookiecutter.project_name}}/template_minimal/app/tests/utils.py deleted file mode 100644 index 7c14c07..0000000 --- a/{{cookiecutter.project_name}}/template_minimal/app/tests/utils.py +++ /dev/null @@ -1,10 +0,0 @@ -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" From e742590afdd0fc35b598fe7d6a9a7b6c73e1f264 Mon Sep 17 00:00:00 2001 From: rafsaf Date: Tue, 10 May 2022 16:44:45 +0200 Subject: [PATCH 18/20] Added more default values to core Settings - test database and tokens expire time --- .../template_minimal/app/core/config.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/{{cookiecutter.project_name}}/template_minimal/app/core/config.py b/{{cookiecutter.project_name}}/template_minimal/app/core/config.py index 8718d63..435f8ef 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/core/config.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/core/config.py @@ -36,9 +36,9 @@ class Settings(BaseSettings): SECRET_KEY: str ENVIRONMENT: Literal["DEV", "PYTEST", "STG", "PRD"] = "DEV" SECURITY_BCRYPT_ROUNDS: int = 12 - ACCESS_TOKEN_EXPIRE_MINUTES: int - REFRESH_TOKEN_EXPIRE_MINUTES: int - BACKEND_CORS_ORIGINS: list[AnyHttpUrl] + ACCESS_TOKEN_EXPIRE_MINUTES: int = 11520 # 8 days + REFRESH_TOKEN_EXPIRE_MINUTES: int = 40320 # 28 days + BACKEND_CORS_ORIGINS: list[AnyHttpUrl] = [] ALLOWED_HOSTS: list[str] = ["localhost"] # PROJECT NAME, VERSION AND DESCRIPTION @@ -55,11 +55,11 @@ class Settings(BaseSettings): 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_DATABASE_HOSTNAME: str = "postgres" + TEST_DATABASE_USER: str = "postgres" + TEST_DATABASE_PASSWORD: str = "postgres" + TEST_DATABASE_PORT: str = "5432" + TEST_DATABASE_DB: str = "postgres" TEST_SQLALCHEMY_DATABASE_URI: str = "" # FIRST SUPERUSER From df03b55c8ba85c002a0915d14276577348342391 Mon Sep 17 00:00:00 2001 From: rafsaf Date: Tue, 10 May 2022 16:48:51 +0200 Subject: [PATCH 19/20] Fix token data issued_at and expires_at validation --- {{cookiecutter.project_name}}/template_minimal/app/api/deps.py | 2 +- .../template_minimal/app/api/endpoints/auth.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/{{cookiecutter.project_name}}/template_minimal/app/api/deps.py b/{{cookiecutter.project_name}}/template_minimal/app/api/deps.py index 3145bd2..c5de28e 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/api/deps.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/api/deps.py @@ -41,7 +41,7 @@ async def get_current_user( detail="Could not validate credentials, cannot use refresh token", ) now = int(time.time()) - if not now >= token_data.issued_at and now <= token_data.expires_at: + if now < token_data.issued_at or now > token_data.expires_at: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Could not validate credentials, token expired or not yet valid", 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 2a35349..ddd2b11 100644 --- a/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/auth.py +++ b/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/auth.py @@ -62,7 +62,7 @@ async def refresh_token( detail="Could not validate credentials, cannot use access token", ) now = int(time.time()) - if not now >= token_data.issued_at and now <= token_data.expires_at: + if now < token_data.issued_at or now > token_data.expires_at: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Could not validate credentials, token expired or not yet valid", From b8d356f4baf077348e099353d0a1e89725af4d10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= <51059348+rafsaf@users.noreply.github.com> Date: Tue, 10 May 2022 17:02:33 +0200 Subject: [PATCH 20/20] Final update Readme - fix few typos --- README.md | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 84fdc05..da4ecd5 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,14 @@ + + Live example + License -Python -Black - - Live example + + Python + + + Black Tests @@ -35,6 +39,8 @@ - [x] `pre-push.sh` script with poetry export, autoflake, black, isort and flake8 - [x] Rich setup for pytest async tests with few included and extensible `conftest.py` +
+ _Check out also online example: https://minimal-fastapi-postgres-template.rafsaf.pl, it's 100% code used in template with added domain and https only._ ![template-fastapi-minimal-openapi-example](./docs/template-minimal-openapi-example.png) @@ -65,7 +71,7 @@ uvicorn app.main:app --reload # Optionally - use git init to initialize git repository ``` -### Running tests +#### Running tests ```bash # Note, it will use second database declared in docker-compose.yml, not default one @@ -84,11 +90,13 @@ pytest # ======================================================== 7 passed in 1.75s ======================================================== ``` +
+ ## About This project is heavily based on the official template https://github.com/tiangolo/full-stack-fastapi-postgresql (and on my previous work: [link1](https://github.com/rafsaf/fastapi-plan), [link2](https://github.com/rafsaf/docker-fastapi-projects)), but as it now not too much up-to-date, it is much easier to create new one than change official. I didn't like some of conventions over there also (`crud` and `db` folders for example or `schemas` with bunch of files). -`2.0` style SQLAlchemy API is good enough so there is no need to write everything in `crud` and waste our time... The `core` folder was also rewritten. There is great base for writting tests in `tests`, but I didn't want to write hundreds of them, I noticed that usually after changes in the structure of the project, auto tests are useless and you have to write them from scratch anyway (delete old ones...), hence less than more. Similarly with the `User` model, it is very modest, with just id, email and password, because it will be adapted to the project anyway. +`2.0` style SQLAlchemy API is good enough so there is no need to write everything in `crud` and waste our time... The `core` folder was also rewritten. There is great base for writting tests in `tests`, but I didn't want to write hundreds of them, I noticed that usually after changes in the structure of the project, auto tests are useless and you have to write them from scratch anyway (delete old ones...), hence less than more. Similarly with the `User` model, it is very modest, with just `id` (uuid), `email` and `password_hash`, because it will be adapted to the project anyway.
@@ -153,7 +161,7 @@ class Pet: ``` -Note, we are using super powerful SQLAlchemy feature here - you can read more about this fairy new syntax based on dataclasses [in this topic in docs](https://docs.sqlalchemy.org/en/14/orm/declarative_styles.html#example-two-dataclasses-with-declarative-table). +Note, we are using super powerful SQLAlchemy feature here - you can read more about this fairy new syntax based on dataclasses [in this topic in the docs](https://docs.sqlalchemy.org/en/14/orm/declarative_styles.html#example-two-dataclasses-with-declarative-table).
@@ -185,9 +193,9 @@ PS. Note, alembic is configured in a way that it work with async setup and also ### 3. Create request and response schemas -Note, I personally lately (after seeing clear benefits at work) don't prefer less file than more for things like schemas. +Note, I personally lately (after seeing clear benefits at work) prefer less files than a lot of them for things like schemas. -Thats why there are only 2 files: `requests.py` and `responses.py` in `schemas` and I would keep it that way even for few dozen of endpoints. +Thats why there are only 2 files: `requests.py` and `responses.py` in `schemas` folder and I would keep it that way even for few dozen of endpoints. ```python # app/schemas/requests.py @@ -252,7 +260,7 @@ async def get_all_my_pets( session: AsyncSession = Depends(deps.get_session), current_user: User = Depends(deps.get_current_user), ): - """Creates new pet. Only for logged users.""" + """Get list of pets for currently logged user.""" pets = await session.execute( select(Pet) @@ -285,8 +293,6 @@ api_router.include_router(pets.router, prefix="/pets", tags=["pets"]) ### 5. Write tests -No - ```python # /app/tests/test_pets.py