diff --git a/.github/workflows/build_docker_image.yml b/.github/workflows/build_docker_image.yml
index e1f784e..c1df7ee 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
@@ -16,6 +16,17 @@ jobs:
steps:
- uses: actions/checkout@v2
+ - 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: |
+ pip install cookiecutter
+ python tests/create_minimal_project.py
+
- name: Login to DockerHub
uses: docker/login-action@v1
with:
@@ -23,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: ./{{cookiecutter.project_name}}/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 8f2e8a7..abf7fbd 100644
--- a/.github/workflows/manual_build_docker_image.yml
+++ b/.github/workflows/manual_build_docker_image.yml
@@ -14,6 +14,17 @@ jobs:
steps:
- uses: actions/checkout@v2
+ - 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: |
+ pip install cookiecutter
+ python tests/create_minimal_project.py
+
- name: Login to DockerHub
uses: docker/login-action@v1
with:
@@ -21,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: ./{{cookiecutter.project_name}}/template_minimal
+ file: minimal_project/Dockerfile
+ context: minimal_project
push: true
tags: rafsaf/minimal-fastapi-postgres-template:${{ github.event.inputs.tag }}
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 24ce58a..e1186fb 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -2,94 +2,94 @@ name: Run tests
on:
push:
- pull_request:
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/README.md b/README.md
index faa7f97..da4ecd5 100644
--- a/README.md
+++ b/README.md
@@ -1,53 +1,50 @@
-
-
+
+
-
+
+
+
+
+
+
+
+
+
-## 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).
+# Minimal async FastAPI + PostgreSQL template
-:bangbang: | This repository contains two different templates to choose from.
-:---: | :---
+- [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)
-## Minimal async FastAPI + postgresql template
+## 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`
+
-- 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`
+_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._

-## 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`
-
-
-
-
## Quickstart
```bash
@@ -57,8 +54,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 +67,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` (uuid), `email` and `password_hash`, 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).
+
-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.
+## Step by step example - POST and GET endpoints
-## Step by step example for 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:
-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.)
+- `POST` endpoint `/pets/create` for creating `Pets` with relation to currently logged `User`
+- `GET` endpoint `/pets/me` for fetching all user's pets.
-### 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 the 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 ###
-# Somethig like `YYYY-MM-DD-....py` will appear in `/alembic/versions` folder
+# 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
+
+
+# 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) prefer less files than a lot of them for things like schemas.
-from pydantic import BaseModel
+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
-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),
+):
+ """Get list of pets for currently logged user."""
+
+ 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
```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"
+
+
+async def test_get_all_my_pets(
+ client: AsyncClient, default_user_headers, default_user: User, session: AsyncSession
+):
- puppy_name = "Sonia"
- puppy_age = 6
+ pet1 = Pet(default_user.id, "Pet_1")
+ pet2 = Pet(default_user.id, "Pet_2")
+ session.add(pet1)
+ session.add(pet2)
+ await session.commit()
- create_dog = await client.post(
- "/dogs/",
- json={"puppy_name": puppy_name, "puppy_age": puppy_age},
- headers={"Authorization": f"Bearer {access_token}"},
+ 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/).
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 92bc199..e50953d 100644
Binary files a/docs/template-minimal-openapi-example.png and b/docs/template-minimal-openapi-example.png differ
diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py
index c0a6c1c..d49ef24 100644
--- a/hooks/post_gen_project.py
+++ b/hooks/post_gen_project.py
@@ -8,7 +8,7 @@
from shutil import copytree, rmtree
PROJECT_NAME = "{{ cookiecutter.project_name }}"
-USE_FASTAPI_USERS = "{{ cookiecutter.use_fastapi_users }}"
+USE_FASTAPI_USERS = "{{ cookiecutter.experimental_fastapi_users_template }}"
TEMPLATES = ["template_fastapi_users", "template_minimal"]
@@ -38,7 +38,7 @@ def create_env_file_and_remove_env_template():
elif USE_FASTAPI_USERS in falsy:
used_template = "template_minimal"
else:
- raise ValueError(f"use_fastapi_users param must be in {truthy + falsy}")
+ raise ValueError(f"'experimental_fastapi_users_template' param must be in {truthy + falsy}")
copy_choosen_template_to_main_dir(used_template=used_template)
create_env_file_and_remove_env_template()
diff --git a/tests/create_fastapi_users_project.py b/tests/create_fastapi_users_project.py
index 2833c0c..b7640ec 100644
--- a/tests/create_fastapi_users_project.py
+++ b/tests/create_fastapi_users_project.py
@@ -15,7 +15,7 @@ def main():
no_input=True,
extra_context={
"project_name": "fastapi_users_project",
- "use_fastapi_users": True,
+ "experimental_fastapi_users_template": True,
},
)
diff --git a/tests/create_minimal_project.py b/tests/create_minimal_project.py
index b1b126d..52b8e29 100644
--- a/tests/create_minimal_project.py
+++ b/tests/create_minimal_project.py
@@ -15,7 +15,7 @@ def main():
no_input=True,
extra_context={
"project_name": "minimal_project",
- "use_fastapi_users": False,
+ "experimental_fastapi_users_template": False,
},
)
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..cc6bc98 100644
--- a/{{cookiecutter.project_name}}/template_minimal/.env.template
+++ b/{{cookiecutter.project_name}}/template_minimal/.env.template
@@ -2,19 +2,20 @@ SECRET_KEY={{ random_ascii_string(50) }}
ENVIRONMENT=DEV
ACCESS_TOKEN_EXPIRE_MINUTES=11520
REFRESH_TOKEN_EXPIRE_MINUTES=40320
-BACKEND_CORS_ORIGINS=http://localhost:3000,http://localhost:8001
+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/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..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_%%(hour).2d%%(minute).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/2021_11_09_1736_init__cefce371682e.py b/{{cookiecutter.project_name}}/template_minimal/alembic/versions/2021_11_09_1736_init__cefce371682e.py
deleted file mode 100644
index bae5d4b..0000000
--- a/{{cookiecutter.project_name}}/template_minimal/alembic/versions/2021_11_09_1736_init__cefce371682e.py
+++ /dev/null
@@ -1,38 +0,0 @@
-"""init
-
-Revision ID: cefce371682e
-Revises:
-Create Date: 2021-11-09 17:36:41.970204
-
-"""
-from alembic import op
-import sqlalchemy as sa
-
-
-# revision identifiers, used by Alembic.
-revision = 'cefce371682e'
-down_revision = None
-branch_labels = None
-depends_on = None
-
-
-def upgrade():
- # ### commands auto generated by Alembic - please adjust! ###
- op.create_table('user',
- sa.Column('id', sa.Integer(), nullable=False),
- sa.Column('full_name', sa.String(length=254), nullable=True),
- sa.Column('email', sa.String(length=254), nullable=False),
- sa.Column('hashed_password', sa.String(length=128), nullable=False),
- sa.PrimaryKeyConstraint('id')
- )
- op.create_index(op.f('ix_user_email'), 'user', ['email'], unique=True)
- op.create_index(op.f('ix_user_id'), 'user', ['id'], unique=False)
- # ### end Alembic commands ###
-
-
-def downgrade():
- # ### commands auto generated by Alembic - please adjust! ###
- op.drop_index(op.f('ix_user_id'), table_name='user')
- op.drop_index(op.f('ix_user_email'), table_name='user')
- op.drop_table('user')
- # ### end Alembic commands ###
diff --git a/{{cookiecutter.project_name}}/template_minimal/alembic/versions/2022050745_init_user_model_d1252175c146.py b/{{cookiecutter.project_name}}/template_minimal/alembic/versions/2022050745_init_user_model_d1252175c146.py
new file mode 100644
index 0000000..3418bca
--- /dev/null
+++ b/{{cookiecutter.project_name}}/template_minimal/alembic/versions/2022050745_init_user_model_d1252175c146.py
@@ -0,0 +1,36 @@
+"""init_user_model
+
+Revision ID: d1252175c146
+Revises:
+Create Date: 2022-05-07 15:45:09.674258
+
+"""
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects import postgresql
+
+# revision identifiers, used by Alembic.
+revision = "d1252175c146"
+down_revision = None
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table(
+ "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_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_model_email"), table_name="user_model")
+ op.drop_table("user_model")
+ # ### 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..c5de28e 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 @@
-from typing import AsyncGenerator, Optional
+import time
+from typing import AsyncGenerator
+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
-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")
@@ -26,18 +25,31 @@ 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.JWTError, ValidationError):
+ except (jwt.DecodeError):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Could not validate credentials",
+ 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="Could not validate credentials, cannot use refresh token",
+ )
+ now = int(time.time())
+ 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",
)
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")
+ 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..ddd2b11 100644
--- a/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/auth.py
+++ b/{{cookiecutter.project_name}}/template_minimal/app/api/endpoints/auth.py
@@ -1,91 +1,77 @@
-from typing import Optional
+import time
+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
-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)
+@router.post("/access-token", response_model=AccessTokenResponse)
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
- """
+ """OAuth2 compatible token, get an access token for future requests using username and password"""
+
result = await session.execute(select(User).where(User.email == form_data.username))
- user: Optional[User] = result.scalars().first()
+ 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): # 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)
- 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.generate_access_token_response(str(user.id))
-@router.post("/test-token", response_model=schemas.User)
-async def test_token(current_user: User = Depends(deps.get_current_user)):
- """
- Test access token for current user
- """
- return current_user
-
-@router.post("/refresh-token", response_model=schemas.Token)
+@router.post("/refresh-token", response_model=AccessTokenResponse)
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.ALGORITHM],
+ algorithms=[security.JWT_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, 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 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",
+ )
+
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.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 8b9bc09..d933fca 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,60 @@
-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)
+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)
+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) # type: ignore
- if user_update.full_name is not None:
- current_user.full_name = user_update.full_name # type: ignore
- if user_update.email is not None:
- current_user.email = user_update.email # type: ignore
+ """Delete current user"""
+ await session.execute(delete(User).where(User.id == current_user.id))
+ await session.commit()
+
+@router.post("/reset-password", response_model=UserResponse)
+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)
+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 06d259e..435f8ef 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, Union
+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"]
@@ -32,11 +34,12 @@
class Settings(BaseSettings):
# CORE SETTINGS
SECRET_KEY: str
- ENVIRONMENT: Literal["DEV", "PYTEST", "STAGE", "PRODUCTION"]
- ACCESS_TOKEN_EXPIRE_MINUTES: int
- SECURITY_BCRYPT_DEFAULT_ROUNDS: int = 12
- REFRESH_TOKEN_EXPIRE_MINUTES: int
- BACKEND_CORS_ORIGINS: Union[str, list[AnyHttpUrl]]
+ ENVIRONMENT: Literal["DEV", "PYTEST", "STG", "PRD"] = "DEV"
+ SECURITY_BCRYPT_ROUNDS: int = 12
+ 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
PROJECT_NAME: str = PYPROJECT_CONTENT["name"]
@@ -52,27 +55,20 @@ 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
FIRST_SUPERUSER_EMAIL: EmailStr
FIRST_SUPERUSER_PASSWORD: str
- # VALIDATORS
- @validator("BACKEND_CORS_ORIGINS")
- def _assemble_cors_origins(cls, cors_origins: Union[str, list[AnyHttpUrl]]):
- if isinstance(cors_origins, str):
- return [item.strip() for item in cors_origins.split(",")]
- return cors_origins
-
@validator("DEFAULT_SQLALCHEMY_DATABASE_URI")
def _assemble_default_db_connection(cls, v: str, values: dict[str, str]) -> str:
- return AnyUrl.build(
+ return PostgresDsn.build(
scheme="postgresql+asyncpg",
user=values["DEFAULT_DATABASE_USER"],
password=values["DEFAULT_DATABASE_PASSWORD"],
@@ -83,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 de362f6..62fb6dc 100644
--- a/{{cookiecutter.project_name}}/template_minimal/app/core/security.py
+++ b/{{cookiecutter.project_name}}/template_minimal/app/core/security.py
@@ -1,56 +1,89 @@
-"""
-Black-box security shortcuts to generate JWT tokens and password hash/verify
+"""Black-box security shortcuts to generate JWT tokens and password hashing and verifcation."""
-`subject` in access/refresh func may be antyhing unique to User account, `id` etc.
-"""
+import time
-from datetime import datetime, timedelta
-from typing import Any, Union
-
-from jose import jwt
+import jwt
from passlib.context import CryptContext
+from pydantic import BaseModel
from app.core import config
+from app.schemas.responses import AccessTokenResponse
-pwd_context = CryptContext(
+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",
- bcrypt__rounds=config.settings.SECURITY_BCRYPT_DEFAULT_ROUNDS,
+ bcrypt__rounds=config.settings.SECURITY_BCRYPT_ROUNDS,
)
-ALGORITHM = "HS256"
+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.
+ Args:
+ subject: anything unique to user, id or email etc.
+ exp_secs: expire time in seconds
+ refresh: if True, this is refresh token
+ """
-def create_access_token(subject: Union[str, Any]) -> tuple[str, datetime]:
- now = datetime.utcnow()
- expire = now + timedelta(minutes=config.settings.ACCESS_TOKEN_EXPIRE_MINUTES)
+ issued_at = int(time.time())
+ expires_at = issued_at + exp_secs
- to_encode = {"exp": expire, "sub": str(subject), "refresh": False}
- encoded_jwt: str = jwt.encode(
+ to_encode: dict[str, int | str | bool] = {
+ "issued_at": issued_at,
+ "expires_at": expires_at,
+ "sub": subject,
+ "refresh": refresh,
+ }
+ encoded_jwt = jwt.encode(
to_encode,
- config.settings.SECRET_KEY,
- algorithm=ALGORITHM,
+ key=config.settings.SECRET_KEY,
+ algorithm=JWT_ALGORITHM,
)
- return encoded_jwt, expire
+ return encoded_jwt, expires_at, issued_at
-def create_refresh_token(subject: Union[str, Any]) -> tuple[str, datetime]:
- now = datetime.utcnow()
- expire = now + timedelta(minutes=config.settings.REFRESH_TOKEN_EXPIRE_MINUTES)
-
- to_encode = {"exp": expire, "sub": str(subject), "refresh": True}
- encoded_jwt: str = jwt.encode(
- to_encode,
- config.settings.SECRET_KEY,
- algorithm=ALGORITHM,
+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_expires_at, refresh_issued_at = create_jwt_token(
+ subject, REFRESH_TOKEN_EXPIRE_SECS, refresh=True
+ )
+ 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,
)
- return encoded_jwt, expire
def verify_password(plain_password: str, hashed_password: str) -> bool:
- return pwd_context.verify(plain_password, hashed_password)
+ """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)
def get_password_hash(password: str) -> str:
- return pwd_context.hash(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)
diff --git a/{{cookiecutter.project_name}}/template_minimal/app/session.py b/{{cookiecutter.project_name}}/template_minimal/app/core/session.py
similarity index 70%
rename from {{cookiecutter.project_name}}/template_minimal/app/session.py
rename to {{cookiecutter.project_name}}/template_minimal/app/core/session.py
index 5b27e40..1154b50 100644
--- a/{{cookiecutter.project_name}}/template_minimal/app/session.py
+++ b/{{cookiecutter.project_name}}/template_minimal/app/core/session.py
@@ -1,3 +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
@@ -8,5 +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_session = sessionmaker(async_engine, expire_on_commit=False, class_=AsyncSession)
+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 227092d..52bb81d 100644
--- a/{{cookiecutter.project_name}}/template_minimal/app/initial_data.py
+++ b/{{cookiecutter.project_name}}/template_minimal/app/initial_data.py
@@ -6,23 +6,21 @@
"""
import asyncio
-from typing import Optional
from sqlalchemy import select
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:
-
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(
@@ -30,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 0892233..bfe7a21 100644
--- a/{{cookiecutter.project_name}}/template_minimal/app/main.py
+++ b/{{cookiecutter.project_name}}/template_minimal/app/main.py
@@ -1,9 +1,8 @@
-"""
-Main FastAPI app instance declaration
-"""
+"""Main FastAPI app instance declaration."""
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
@@ -15,15 +14,16 @@
openapi_url="/openapi.json",
docs_url="/",
)
+app.include_router(api_router)
-# Set all CORS enabled origins
-if config.settings.BACKEND_CORS_ORIGINS:
- app.add_middleware(
- CORSMiddleware,
- allow_origins=[str(origin) for origin in config.settings.BACKEND_CORS_ORIGINS],
- allow_credentials=True,
- allow_methods=["*"],
- allow_headers=["*"],
- )
+# 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=["*"],
+)
-app.include_router(api_router)
+# 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 2b7eb82..4a0f214 100644
--- a/{{cookiecutter.project_name}}/template_minimal/app/models.py
+++ b/{{cookiecutter.project_name}}/template_minimal/app/models.py
@@ -1,20 +1,41 @@
"""
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, imported by alembic migrations logic, see `alembic/env.py`
+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
"""
-from typing import Any, cast
+import uuid
+from dataclasses import dataclass, field
+
+from sqlalchemy import Column, String
+from sqlalchemy.dialects.postgresql import UUID
+from sqlalchemy.orm import registry
-from sqlalchemy import Column, Integer, String
-from sqlalchemy.orm.decl_api import declarative_base
+Base = registry()
-Base = cast(Any, declarative_base())
+@Base.mapped
+@dataclass
+class User:
+ __tablename__ = "user_model"
+ __sa_dataclass_metadata_key__ = "sa"
-class User(Base):
- __tablename__ = "user"
- id = Column(Integer, primary_key=True, index=True)
- full_name = Column(String(254), nullable=True)
- email = Column(String(254), unique=True, index=True, nullable=False)
- hashed_password = Column(String(128), nullable=False)
+ 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)})
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 9c81e3e..0000000
--- a/{{cookiecutter.project_name}}/template_minimal/app/schemas/token.py
+++ /dev/null
@@ -1,21 +0,0 @@
-from datetime import datetime
-from typing import Optional
-
-from pydantic import BaseModel
-
-
-class Token(BaseModel):
- token_type: str
- access_token: str
- expire_at: datetime
- refresh_token: str
- refresh_expire_at: datetime
-
-
-class TokenPayload(BaseModel):
- sub: Optional[int] = None
- refresh: Optional[bool] = None
-
-
-class TokenRefresh(BaseModel):
- refresh_token: str
diff --git a/{{cookiecutter.project_name}}/template_minimal/app/schemas/user.py b/{{cookiecutter.project_name}}/template_minimal/app/schemas/user.py
deleted file mode 100644
index 9e24055..0000000
--- a/{{cookiecutter.project_name}}/template_minimal/app/schemas/user.py
+++ /dev/null
@@ -1,26 +0,0 @@
-from typing import Optional
-
-from pydantic import BaseModel, EmailStr
-
-
-class BaseUser(BaseModel):
- class Config:
- orm_mode = True
-
-
-class User(BaseUser):
- id: int
- email: EmailStr
- full_name: str
-
-
-class UserUpdate(BaseUser):
- email: Optional[EmailStr]
- password: Optional[str]
- full_name: Optional[str]
-
-
-class UserCreate(BaseUser):
- email: EmailStr
- password: str
- full_name: str
diff --git a/{{cookiecutter.project_name}}/template_minimal/app/tests/conftest.py b/{{cookiecutter.project_name}}/template_minimal/app/tests/conftest.py
index af40fc0..66c081b 100644
--- a/{{cookiecutter.project_name}}/template_minimal/app/tests/conftest.py
+++ b/{{cookiecutter.project_name}}/template_minimal/app/tests/conftest.py
@@ -1,34 +1,36 @@
import asyncio
-from typing import AsyncGenerator, Optional
+from typing import AsyncGenerator
+from uuid import UUID
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
+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_hash = security.get_password_hash("geralt")
+default_user_password = "geralt"
+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")
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")
-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"
@@ -36,30 +38,48 @@ async def test_db_setup_sessionmaker():
# always drop and create test db tables between tests session
async with async_engine.begin() as conn:
-
await conn.run_sync(Base.metadata.drop_all)
await conn.run_sync(Base.metadata.create_all)
- return async_session
-@pytest.fixture
+@pytest_asyncio.fixture(autouse=True)
async def session(test_db_setup_sessionmaker) -> AsyncGenerator[AsyncSession, None]:
- async with test_db_setup_sessionmaker() as session:
+ 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.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()
- if user is None:
- new_user = User(
- email=default_user_email,
- hashed_password=default_user_hash,
- full_name="fullname",
+
+@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(test_db_setup_sessionmaker) -> 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.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 40a52eb..f408b19 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,64 @@
-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(
+ app.url_path_for("login_access_token"),
+ data={
+ "username": default_user_email,
+ "password": default_user_password,
+ },
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
+ )
+ assert response.status_code == 200
+ token = response.json()
+ assert token["token_type"] == "Bearer"
+ assert "access_token" in token
+ assert "expires_at" in token
+ assert "issued_at" in token
+ assert "refresh_token" in token
+ assert "refresh_token_expires_at" in token
+ assert "refresh_token_issued_at" in token
-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_fail_no_user(client: AsyncClient):
+ response = await client.post(
+ app.url_path_for("login_access_token"),
data={
- "username": "geralt@wiedzmin.pl",
- "password": "geralt",
+ "username": "xxx",
+ "password": "yyy",
},
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 == 400
+ assert response.json() == {"detail": "Incorrect email or password"}
- # 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_refresh_token(client: AsyncClient, default_user: User):
+ response = await client.post(
+ app.url_path_for("login_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"]
- assert get_new_token.status_code == 200
- new_token = get_new_token.json()
-
- assert "access_token" in new_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 "expires_at" in token
+ assert "issued_at" in token
+ assert "refresh_token" 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/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"
diff --git a/{{cookiecutter.project_name}}/template_minimal/docker-compose.dev.yml b/{{cookiecutter.project_name}}/template_minimal/docker-compose.dev.yml
new file mode 100644
index 0000000..8d008d2
--- /dev/null
+++ b/{{cookiecutter.project_name}}/template_minimal/docker-compose.dev.yml
@@ -0,0 +1,35 @@
+version: "3.7"
+
+# Database + Webserver (under http, for testing setup on localhost:80)
+#
+# docker-compose up -f docker-compose.dev.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..4db25e9 100644
--- a/{{cookiecutter.project_name}}/template_minimal/nginx-unit-config.json
+++ b/{{cookiecutter.project_name}}/template_minimal/nginx-unit-config.json
@@ -6,7 +6,8 @@
},
"applications": {
"fastapi": {
- "type": "python 3.9",
+ "type": "python 3.10",
+ "user": "root",
"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..23d6886 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.76.0"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
category = "main"
optional = false
@@ -260,13 +240,13 @@ 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,<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
@@ -713,7 +658,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
@@ -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 = "d5a61fe471977ad5ce64fd8fec4548ee4715d85f6e40f7a61ba8b318c6b2734d"
[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.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"},
@@ -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,68 +1201,64 @@ 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"},
- {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"},
]
toml = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
tomli = [
- {file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"},
- {file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"},
+ {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..c622f56 100644
--- a/{{cookiecutter.project_name}}/template_minimal/pyproject.toml
+++ b/{{cookiecutter.project_name}}/template_minimal/pyproject.toml
@@ -1,47 +1,47 @@
[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"}
-passlib = {extras = ["bcrypt"], version = "^1.7.4"}
-pydantic = {extras = ["email"], version = "^1.9.0"}
-alembic = "^1.7.5"
-python-multipart = "^0.0.5"
+alembic = "^1.7.7"
asyncpg = "^0.25.0"
+fastapi = "^0.76.0"
+passlib = {extras = ["bcrypt"], version = "^1.7.4"}
+pydantic = {extras = ["email", "dotenv"], version = "^1.9.0"}
+python-multipart = ">=0.0.5,<0.0.6"
+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 = []
+markers = ["pytest.mark.asyncio"]
+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..cd9f87e 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.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.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")
-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
+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"
+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..5f1bb30 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.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")
-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")
-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
+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"
+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"