From 89c884d7645605f3779826606640ecec036d9af0 Mon Sep 17 00:00:00 2001 From: Pavel Kirilin <win10@list.ru> Date: Wed, 18 Aug 2021 12:50:37 +0400 Subject: [PATCH] Updated template. Signed-off-by: Pavel Kirilin <win10@list.ru> --- README.md | 64 +- fastapi_template/__main__.py | 37 ++ fastapi_template/cli.py | 172 ++++++ fastapi_template/input_model.py | 42 ++ fastapi_template/main.py | 24 - fastapi_template/template/cookiecutter.json | 46 +- .../template/hooks/post_gen_project.py | 20 +- .../.dockerignore | 143 +++++ .../.editorconfig | 31 + .../{{cookiecutter.project_name}}/.env | 1 + .../{{cookiecutter.project_name}}/.flake8 | 89 +++ .../{{cookiecutter.project_name}}/.gitignore | 6 +- .../.gitlab-ci.yml | 72 ++- .../.pre-commit-config.yaml | 94 +-- .../{{cookiecutter.project_name}}/Dockerfile | 105 ---- .../{{cookiecutter.project_name}}/README.md | 66 +-- .../{{cookiecutter.project_name}}/alembic.ini | 54 +- .../conditional_files.json | 81 ++- .../{{cookiecutter.project_name}}/deploy.sh | 14 - .../deploy/Dockerfile | 32 + .../deploy/docker-compose.yml | 45 ++ .../deploy/kube/app.yml | 57 ++ .../deploy/kube/db.yml | 33 ++ .../deploy/kube/redis.yml | 33 ++ .../docker-compose.prod.yml | 82 --- .../docker-compose.yml | 90 --- .../{{cookiecutter.project_name}}/envs/.env | 17 - .../envs/example.env | 18 - .../envs/test.env | 18 - .../{{cookiecutter.project_name}}/main.py | 45 -- .../migrations/env.py | 96 --- .../7ae297ab5ac1_created_dummy_model.py | 42 -- .../pyproject.toml | 97 +++- .../{{cookiecutter.project_name}}/pytest.ini | 4 - .../scheduler.py | 19 - .../src/api/__init__.py | 28 - .../src/api/dummy_db/__init__.py | 3 - .../src/api/dummy_db/routes.py | 88 --- .../src/api/dummy_db/schema.py | 44 -- .../src/api/httpbin/__init__.py | 3 - .../src/api/httpbin/routes.py | 17 - .../src/api/httpbin/schema.py | 0 .../src/api/redis_api/__init__.py | 3 - .../src/api/redis_api/routes.py | 32 - .../src/api/redis_api/schema.py | 11 - .../src/exceptions.py | 23 - .../src/models/__init__.py | 5 - .../src/models/dummy_db_model.py | 76 --- .../src/server.py | 70 --- .../src/services/__init__.py | 0 .../src/services/db/__init__.py | 5 - .../src/services/db/base.py | 105 ---- .../src/services/db/db_meta.py | 3 - .../src/services/db/engine.py | 41 -- .../src/services/db/session.py | 61 -- .../src/services/elastic/__init__.py | 6 - .../src/services/elastic/client.py | 4 - .../src/services/elastic/mixin.py | 115 ---- .../src/services/elastic/schema.py | 15 - .../src/services/httpbin/__init__.py | 0 .../src/services/httpbin/client.py | 60 -- .../src/services/httpbin/schema.py | 20 - .../src/services/redis.py | 50 -- .../src/settings.py | 44 -- .../src/utils.py | 0 ...iecutter.project_name }}_scheduler.service | 15 - ...okiecutter.project_name }}_service.service | 15 - .../tests/__init__.py | 13 - .../tests/conftest.py | 93 --- .../tests/dummy_db_test.py | 39 -- .../{{cookiecutter.project_name}}/__init__.py | 1 + .../{{cookiecutter.project_name}}/__main__.py | 19 + .../{{cookiecutter.project_name}}/conftest.py | 54 ++ .../{{cookiecutter.project_name}}/db/base.py | 20 + .../db/dao/__init__.py | 1 + .../db/dao/dummy_dao.py | 35 ++ .../db/dependencies.py | 19 + .../{{cookiecutter.project_name}}/db/meta.py | 3 + .../db/migrations/README.md} | 0 .../db/migrations/__init__.py | 1 + .../db/migrations/env.py | 85 +++ .../db}/migrations/script.py.mako | 4 +- .../versions/2021-08-16-16-53_819cbf6e030b.py | 22 + .../versions/2021-08-16-16-55_2b7380507a71.py | 32 + .../db/migrations/versions}/__init__.py | 0 .../db/models/__init__.py | 14 + .../db/models/dummy_model.py | 13 + .../services/redis/dependency.py | 20 + .../{{cookiecutter.project_name}}/settings.py | 94 +++ .../tests/__init__.py | 1 + .../tests/test_fastapitestproject.py | 8 + .../web/api/__init__.py | 1 + .../web/api/dummy/__init__.py | 4 + .../web/api/dummy/schema.py | 21 + .../web/api/dummy/views.py | 41 ++ .../web/api/echo/__init__.py | 4 + .../web/api/echo/schema.py | 7 + .../web/api/echo/views.py | 18 + .../web/api/redis/__init__.py | 4 + .../web/api/redis/schema.py | 10 + .../web/api/redis/views.py | 41 ++ .../web/api/router.py | 19 + .../web/application.py | 24 + .../web/lifetime.py | 94 +++ images/ui-example.png | Bin 0 -> 23550 bytes poetry.lock | 549 +++++++++++------- pyproject.toml | 12 +- 107 files changed, 2108 insertions(+), 2183 deletions(-) create mode 100644 fastapi_template/__main__.py create mode 100644 fastapi_template/cli.py create mode 100644 fastapi_template/input_model.py delete mode 100644 fastapi_template/main.py create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/.dockerignore create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/.editorconfig create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/.env create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/.flake8 delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/Dockerfile delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/deploy.sh create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/deploy/Dockerfile create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/deploy/docker-compose.yml create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/deploy/kube/app.yml create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/deploy/kube/db.yml create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/deploy/kube/redis.yml delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/docker-compose.prod.yml delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/docker-compose.yml delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/envs/.env delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/envs/example.env delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/envs/test.env delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/main.py delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/migrations/env.py delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/migrations/versions/7ae297ab5ac1_created_dummy_model.py delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/pytest.ini delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/scheduler.py delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/src/api/__init__.py delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/src/api/dummy_db/__init__.py delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/src/api/dummy_db/routes.py delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/src/api/dummy_db/schema.py delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/src/api/httpbin/__init__.py delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/src/api/httpbin/routes.py delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/src/api/httpbin/schema.py delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/src/api/redis_api/__init__.py delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/src/api/redis_api/routes.py delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/src/api/redis_api/schema.py delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/src/exceptions.py delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/src/models/__init__.py delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/src/models/dummy_db_model.py delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/src/server.py delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/src/services/__init__.py delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/src/services/db/__init__.py delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/src/services/db/base.py delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/src/services/db/db_meta.py delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/src/services/db/engine.py delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/src/services/db/session.py delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/src/services/elastic/__init__.py delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/src/services/elastic/client.py delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/src/services/elastic/mixin.py delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/src/services/elastic/schema.py delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/src/services/httpbin/__init__.py delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/src/services/httpbin/client.py delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/src/services/httpbin/schema.py delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/src/services/redis.py delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/src/settings.py delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/src/utils.py delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/systemd/{{ cookiecutter.project_name }}_scheduler.service delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/systemd/{{ cookiecutter.project_name }}_service.service delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/tests/__init__.py delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/tests/conftest.py delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/tests/dummy_db_test.py create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/__init__.py create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/__main__.py create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/conftest.py create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/base.py create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/dao/__init__.py create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/dao/dummy_dao.py create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/dependencies.py create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/meta.py rename fastapi_template/template/{{cookiecutter.project_name}}/{migrations/__init__.py => {{cookiecutter.project_name}}/db/migrations/README.md} (100%) create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/migrations/__init__.py create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/migrations/env.py rename fastapi_template/template/{{cookiecutter.project_name}}/{ => {{cookiecutter.project_name}}/db}/migrations/script.py.mako (90%) create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/migrations/versions/2021-08-16-16-53_819cbf6e030b.py create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/migrations/versions/2021-08-16-16-55_2b7380507a71.py rename fastapi_template/template/{{cookiecutter.project_name}}/{src => {{cookiecutter.project_name}}/db/migrations/versions}/__init__.py (100%) create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/models/__init__.py create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/models/dummy_model.py create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/services/redis/dependency.py create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/settings.py create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/tests/__init__.py create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/tests/test_fastapitestproject.py create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/__init__.py create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/dummy/__init__.py create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/dummy/schema.py create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/dummy/views.py create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/echo/__init__.py create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/echo/schema.py create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/echo/views.py create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/redis/__init__.py create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/redis/schema.py create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/redis/views.py create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/router.py create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/application.py create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/lifetime.py create mode 100644 images/ui-example.png diff --git a/README.md b/README.md index ac64d67..b6a4493 100644 --- a/README.md +++ b/README.md @@ -5,24 +5,66 @@ <div><i>Fast and flexible general-purpose template for your API.</i></div> </div> +<div align="center"> +<img src="https://raw.githubusercontent.com/s3rius/FastAPI-template/master/images/ui-example.png" width=700> +<div><i>With text user interface.</i></div> +</div> ## Usage -âš ï¸ [Git](https://git-scm.com/downloads), [Python](https://www.python.org/), and [Docker-compose](https://docs.docker.com/compose/install/) must be installed and accessible âš ï¸ + +âš ï¸ [Git](https://git-scm.com/downloads), [Python](https://www.python.org/) and [Poetry](https://python-poetry.org/) must be installed and accessible âš ï¸ ```bash python3 -m pip install fastapi_template -fastapi_template -# Answer prompts questions -# ??? +python3 -m fastapi_template +# or fastapi_template +# Answer all the questions # 🪠Enjoy your new project 🪠cd new_project -docker-compose up --build +docker-compose -f deploy/docker-compose.yml --project-directory . up --build +``` + +If you want to install in from sources then try this: +```shell +python3 -m pip install poetry +python3 -m pip install . +python3 -m fastapi_template ``` ## Features -Currently supported features: -- redis -- systemd units -- Example (dummy) SQLAlchemy model -- Elastic Search support -- Scheduler support + +Template is made with SQLAlchemy14 and uses sqlalchemy orm and sessions, +instead of raw drivers. + +It has minimum to start new excellent project. + +Generator features: +- Different databases to choose from. +- Alembic integration; +- redis support; +- CI\CD (Currently only gitlab-ci); +- Kubernetes config. + +This project can handle arguments passed through command line. + +```shell +$ python -m fastapi_template --help + +usage: FastAPI template [-h] [--name PROJECT_NAME] + [--description PROJECT_DESCRIPTION] + [--db {none,sqlite,mysql,postgresql}] [--ci] [--redis] + [--alembic] [--kube] [--force] + +optional arguments: + -h, --help show this help message and exit + --name PROJECT_NAME Name of your awesome project + --description PROJECT_DESCRIPTION + Project description + --db {none,sqlite,mysql,postgresql} + Database + --ci Add CI/CD support + --redis Add redis support + --alembic Add alembic support + --kube Add kubernetes configs + --force Owerrite directory if exists +``` diff --git a/fastapi_template/__main__.py b/fastapi_template/__main__.py new file mode 100644 index 0000000..8e52abe --- /dev/null +++ b/fastapi_template/__main__.py @@ -0,0 +1,37 @@ +from pathlib import Path + +from cookiecutter.exceptions import FailedHookException, OutputDirExistsException +from cookiecutter.main import cookiecutter +from termcolor import cprint + +from fastapi_template.cli import get_context +from fastapi_template.input_model import BuilderContext + +script_dir = Path(__file__).parent + + +def main(): + try: + context = get_context() + except KeyboardInterrupt: + print("Goodbye!") + return + try: + cookiecutter( + template=f"{script_dir}/template", + extra_context=context.dict(), + default_config=BuilderContext().dict(), + no_input=True, + overwrite_if_exists=context.force, + ) + except (FailedHookException, OutputDirExistsException) as exc: + if isinstance(exc, OutputDirExistsException): + cprint("Directory with such name already exists!", "red") + return + cprint( + "Project successfully generated. You can read information about usage in README.md" + ) + + +if __name__ == "__main__": + main() diff --git a/fastapi_template/cli.py b/fastapi_template/cli.py new file mode 100644 index 0000000..fc9054d --- /dev/null +++ b/fastapi_template/cli.py @@ -0,0 +1,172 @@ +import re +from argparse import ArgumentParser +from operator import attrgetter + +from prompt_toolkit import prompt +from prompt_toolkit.document import Document +from prompt_toolkit.shortcuts import checkboxlist_dialog, radiolist_dialog +from prompt_toolkit.validation import ValidationError, Validator + +from fastapi_template.input_model import BuilderContext, Database, DatabaseType + + +class SnakeCaseValidator(Validator): + def validate(self, document: Document): + text = document.text + if not text or re.fullmatch(r"[a-zA-Z][\w\_\d]*", text) is None: + raise ValidationError(message="Must be a valid snake_case name.") + + +DB_INFO = { + DatabaseType.none: Database( + name="none", + image="none", + driver=None, + port=None, + ), + DatabaseType.postgresql: Database( + name=DatabaseType.postgresql.value, + image="postgres:13.4-buster", + driver="postgresql+asyncpg", + port=5432, + ), + DatabaseType.mysql: Database( + name=DatabaseType.mysql.value, + image="bitnami/mysql:8.0.26", + driver="mysql+aiomysql", + port=3306, + ), + DatabaseType.sqlite: Database( + name=DatabaseType.sqlite.value, image=None, driver="sqlite+aiosqlite", port=None + ), +} + + +def parse_args(): + parser = ArgumentParser( + prog="FastAPI template", + ) + parser.add_argument( + "--name", + type=str, + dest="project_name", + help="Name of your awesome project", + ) + parser.add_argument( + "--description", + type=str, + dest="project_description", + help="Project description", + ) + parser.add_argument( + "--db", + help="Database", + type=DatabaseType, + choices=list(DatabaseType), + default=None, + dest="db", + ) + parser.add_argument( + "--ci", + help="Add CI/CD support", + action="store_true", + default=None, + dest="enable_ci", + ) + parser.add_argument( + "--redis", + help="Add redis support", + action="store_true", + default=None, + dest="enable_redis", + ) + parser.add_argument( + "--alembic", + help="Add alembic support", + action="store_true", + default=None, + dest="enable_alembic", + ) + parser.add_argument( + "--kube", + help="Add kubernetes configs", + action="store_true", + default=None, + dest="enable_kube", + ) + parser.add_argument( + "--force", + help="Owerrite directory if it exists", + action="store_true", + default=False, + dest="force", + ) + + return parser.parse_args() + + +def ask_features(current_context: BuilderContext) -> BuilderContext: + features = { + "Redis support": { + "name": "enable_redis", + "value": current_context.enable_redis, + }, + "CI/CD": { + "name": "enable_ci", + "value": current_context.enable_ci, + }, + "Kubernetes": { + "name": "enable_kube", + "value": current_context.enable_kube, + }, + } + if current_context.db != DatabaseType.none: + features["Alembic migrations"] = { + "name": "enable_alembic", + "value": current_context.enable_alembic, + } + checkbox_values = [] + for feature_name, feature in features.items(): + if feature["value"] is None: + setattr(current_context, feature["name"], False) + checkbox_values.append((feature["name"], feature_name)) + if checkbox_values: + results = checkboxlist_dialog( + title="Features", + text="What features do you wanna add?", + values=checkbox_values, + ).run() + if results is None: + raise KeyboardInterrupt() + for feature in results: + setattr(current_context, feature, True) + return current_context + + +def read_user_input(current_context: BuilderContext) -> BuilderContext: + if current_context.project_name is None: + current_context.project_name = prompt( + "Project name: ", validator=SnakeCaseValidator() + ) + if current_context.project_description is None: + current_context.project_description = prompt("Project description: ") + if current_context.db is None: + current_context.db = radiolist_dialog( + "Databases", + text="Which database do you want?", + values=[(db, db.value) for db in list(DatabaseType)], + ).run() + if current_context.db is None: + raise KeyboardInterrupt() + if current_context.db == DatabaseType.none: + current_context.enable_alembic = False + ask_features(current_context) + return current_context + + +def get_context() -> BuilderContext: + args = parse_args() + context = BuilderContext.from_orm(args) + context = read_user_input(context) + context.db_info = DB_INFO[context.db] + return context diff --git a/fastapi_template/input_model.py b/fastapi_template/input_model.py new file mode 100644 index 0000000..1d2eb93 --- /dev/null +++ b/fastapi_template/input_model.py @@ -0,0 +1,42 @@ +import enum +from typing import Optional + +from pydantic import BaseModel + + +@enum.unique +class DatabaseType(enum.Enum): + none = "none" + sqlite = "sqlite" + mysql = "mysql" + postgresql = "postgresql" + + +@enum.unique +class CIType(enum.Enum): + none = "none" + gitlab_ci = "gitlab" + + +class Database(BaseModel): + name: str + image: Optional[str] + driver: Optional[str] + port: Optional[int] + + +class BuilderContext(BaseModel): + """Options for project generation.""" + + project_name: Optional[str] + project_description: Optional[str] + db: Optional[DatabaseType] + db_info: Optional[Database] + enable_redis: Optional[bool] + enable_ci: Optional[bool] + enable_alembic: Optional[bool] + enable_kube: Optional[bool] + force: bool = False + + class Config: + orm_mode = True diff --git a/fastapi_template/main.py b/fastapi_template/main.py deleted file mode 100644 index b6b3063..0000000 --- a/fastapi_template/main.py +++ /dev/null @@ -1,24 +0,0 @@ -from pathlib import Path - -from cookiecutter.exceptions import FailedHookException, OutputDirExistsException -from cookiecutter.main import cookiecutter -from termcolor import cprint - -script_dir = Path(__file__).parent - - -def main(): - try: - cookiecutter(template=f"{script_dir}/template") - except (FailedHookException, OutputDirExistsException) as exc: - if isinstance(exc, OutputDirExistsException): - cprint( - "Directory with such name already exists!", - "red" - ) - return - cprint("Project successfully generated. You can read information about usage in README.md") - - -if __name__ == "__main__": - main() diff --git a/fastapi_template/template/cookiecutter.json b/fastapi_template/template/cookiecutter.json index 6bc0e64..469ca15 100644 --- a/fastapi_template/template/cookiecutter.json +++ b/fastapi_template/template/cookiecutter.json @@ -1,29 +1,25 @@ { - "project_name": "new_fastapi_project", - "full_name": "Your name", - "email": "win10@list.ru", - "project_description": "", - "default_port": 8401, - "add_redis": [ - true, - false - ], - "add_systemd": [ - true, - false - ], - "add_dummy_model": [ - true, - false - ], - "add_elastic_search": [ - true, - false - ], - "add_scheduler": [ - true, - false - ], + "project_name": { + "type": "string" + }, + "project_description": { + "type": "string" + }, + "db_info": { + "type": "dict" + }, + "enable_redis": { + "type": "bool" + }, + "enable_ci": { + "type": "bool" + }, + "enable_alembic": { + "type": "bool" + }, + "enable_kube": { + "type": "bool" + }, "_extensions": [ "cookiecutter.extensions.RandomStringExtension" ] diff --git a/fastapi_template/template/hooks/post_gen_project.py b/fastapi_template/template/hooks/post_gen_project.py index ccb10c5..a9bf5aa 100644 --- a/fastapi_template/template/hooks/post_gen_project.py +++ b/fastapi_template/template/hooks/post_gen_project.py @@ -2,12 +2,13 @@ import json import os import shutil +import subprocess from argparse import Namespace import pre_commit.constants as pre_commit_constants import pre_commit.main as pre_commit from pygit2 import init_repository -from termcolor import cprint +from termcolor import cprint, colored MANIFEST = "conditional_files.json" @@ -24,13 +25,17 @@ def delete_resource(resource): def delete_resources_for_disabled_features(): with open(MANIFEST) as manifest_file: manifest = json.load(manifest_file) - for feature in manifest['features']: - if not feature['enabled'] == "true": - print("removing resources for disabled feature {}...".format(feature['name'])) + for feature_name, feature in manifest.items(): + if feature['enabled'].lower() != "true": + text = "{} resources for disabled feature {}...".format( + colored("Removing", color="red"), + colored(feature_name, color="magenta", attrs=['underline']) + ) + print(text) for resource in feature['resources']: delete_resource(resource) - print("cleanup complete, removing manifest...") delete_resource(MANIFEST) + cprint("cleanup complete!", color="green") def init_repo(): @@ -48,7 +53,7 @@ def init_repo(): overwrite=False ) cprint("pre-commit installed.", "green") - run_namespace = Namespace( + pre_commit_args = Namespace( all_files=True, files=[], hook_stage='commit', @@ -62,10 +67,11 @@ def init_repo(): show_diff_on_failure=False, is_squash_merge=False, ) + subprocess.run(["poetry", "install", "-n"]) pre_commit.run( config_file=pre_commit_constants.CONFIG_FILE, store=store, - args=run_namespace + args=pre_commit_args ) repo.index.add_all() repo.index.write() diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/.dockerignore b/fastapi_template/template/{{cookiecutter.project_name}}/.dockerignore new file mode 100644 index 0000000..741c3d3 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/.dockerignore @@ -0,0 +1,143 @@ +### Python template + +deploy/ +.idea/ +.vscode/ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/.editorconfig b/fastapi_template/template/{{cookiecutter.project_name}}/.editorconfig new file mode 100644 index 0000000..eb831fe --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/.editorconfig @@ -0,0 +1,31 @@ +root = true + +[*] +tab_width = 4 +end_of_line = lf +max_line_length = 88 +ij_visual_guides = 88 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{js,py,html}] +charset = utf-8 + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +[Makefile] +indent_style = tab + +[.flake8] +indent_style = space +indent_size = 2 + +[*.py] +indent_style = space +indent_size = 4 +ij_python_from_import_parentheses_force_if_multiline = true diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/.env b/fastapi_template/template/{{cookiecutter.project_name}}/.env new file mode 100644 index 0000000..8d89e98 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/.env @@ -0,0 +1 @@ +{{cookiecutter.project_name | upper}}_RELOAD=True diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/.flake8 b/fastapi_template/template/{{cookiecutter.project_name}}/.flake8 new file mode 100644 index 0000000..6572ef1 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/.flake8 @@ -0,0 +1,89 @@ +[flake8] +max-complexity = 6 +inline-quotes = double +max-line-length = 88 +extend-ignore = E203 +docstring_style=sphinx + +ignore = + ; Found `f` string + WPS305, + ; Missing docstring in public module + D100, + ; Missing docstring in magic method + D105, + ; Missing docstring in __init__ + D107, + ; Found class without a base class + WPS306, + ; Missing docstring in public nested class + D106, + ; First line should be in imperative mood + D401, + ; Found `__init__.py` module with logic + WPS412, + ; Found implicit string concatenation + WPS326, + ; Found string constant over-use + WPS226, + ; Found upper-case constant in a class + WPS115, + ; Found nested function + WPS430, + ; Found using `@staticmethod` + WPS602, + ; Found method without arguments + WPS605, + ; Found overused expression + WPS204, + ; Found too many module members + WPS202, + ; Found too high module cognitive complexity + WPS232, + ; line break before binary operator + W503, + ; Found module with too many imports + WPS201, + ; Found vague import that may cause confusion: X + WPS347, + ; Inline strong start-string without end-string. + RST210, + ; Found nested class + WPS431, + ; Found wrong module name + WPS100, + ; Found too many methods + WPS214, + ; Found too long ``try`` body + WPS229, + ; Found unpythonic getter or setter + WPS615, + ; Found a line that starts with a dot + WPS348, + ; Found complex default value (for dependency injection) + WPS404, + ; not perform function calls in argument defaults (for dependency injection) + B008, + + ; all init files + __init__.py: + ; ignore not used imports + F401, + ; ignore import with wildcard + F403, + ; Found wrong metadata variable + WPS410, + +per-file-ignores = + ; all tests + test_*.py,tests.py,tests_*.py,*/tests/*: + ; Use of assert detected + S101, + ; Found outer scope names shadowing + WPS442, + +exclude = + ./.git, + ./venv, + migrations, + ./var, diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/.gitignore b/fastapi_template/template/{{cookiecutter.project_name}}/.gitignore index 334abdf..ded1f6c 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/.gitignore +++ b/fastapi_template/template/{{cookiecutter.project_name}}/.gitignore @@ -1,5 +1,7 @@ -.idea/ +### Python template +.idea/ +.vscode/ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -107,7 +109,7 @@ celerybeat.pid *.sage.py # Environments -envs/.env +.env .venv env/ venv/ diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/.gitlab-ci.yml b/fastapi_template/template/{{cookiecutter.project_name}}/.gitlab-ci.yml index 07c268b..87f801c 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/.gitlab-ci.yml +++ b/fastapi_template/template/{{cookiecutter.project_name}}/.gitlab-ci.yml @@ -1,33 +1,53 @@ -variables: - GIT_STRATEGY: clone - stages: - - test - - deploy - -.install-deps-template: &install-deps - before_script: - - echo "Installing deps" - - python3 -m pip install poetry - - poetry install -vv + - "test" -run_tests: - <<: *install-deps +.test-template: stage: test + image: python:3.9.6-slim-buster tags: - - testing + - kubernetes-runner + - docker-runner + except: + - tags + before_script: + - pip install poetry==1.1.7 + - poetry config virtualenvs.create false + - poetry install + +flake8: + extends: + - .test-template script: - - poetry run pre-commit run --all-files - - poetry run dotenv -f envs/test.env run pytest -vv --cov=src + - flake8 --count . -deploy_app: - <<: *install-deps - stage: deploy - only: ['master'] - tags: - - production +pytest: + extends: + - .test-template + {%- if cookiecutter.db_info.name != "none" %} + {%- if cookiecutter.db_info.name != "mysql" %} + services: + - name: {{ cookiecutter.db_info.image }} + {%- endif %} + variables: + {%- if cookiecutter.db_info.name == "postgresql" %} + {{ cookiecutter.project_name | upper }}_DB_HOST: localhost + POSTGRES_PASSWORD: {{ cookiecutter.project_name }} + POSTGRES_USER: {{ cookiecutter.project_name }} + POSTGRES_DB: {{ cookiecutter.project_name }} + {%- endif %} + {%- if cookiecutter.db_info.name == "mysql" %} + {{ cookiecutter.project_name | upper }}_DB_HOST: localhost + MYSQL_PASSWORD: {{ cookiecutter.project_name }} + MYSQL_USER: {{ cookiecutter.project_name }} + MYSQL_DB: {{ cookiecutter.project_name }} + {%- endif %} + {%- endif %} + script: + - pytest -vv --cov="{{cookiecutter.project_name}}" . + +black: + extends: + - .test-template script: - - poetry run dotenv -f "${APP_ENV_FILE}" run alembic upgrade head; - - sh deploy.sh - environment: - name: production \ No newline at end of file + - black --check . + diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/.pre-commit-config.yaml b/fastapi_template/template/{{cookiecutter.project_name}}/.pre-commit-config.yaml index 2ed6b76..294b534 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/.pre-commit-config.yaml +++ b/fastapi_template/template/{{cookiecutter.project_name}}/.pre-commit-config.yaml @@ -6,54 +6,58 @@ repos: hooks: - id: check-ast - id: trailing-whitespace + - id: check-toml + - id: end-of-file-fixer - - repo: https://github.com/psf/black - rev: 20.8b0 + - repo: https://github.com/asottile/add-trailing-comma + rev: v2.1.0 hooks: - - id: black + - id: add-trailing-comma - - repo: https://gitlab.com/PyCQA/flake8.git - rev: 3.8.3 + - repo: local hooks: + - id: black + name: Format with Black + entry: poetry run black + language: system + types: [python] + + - id: autoflake + name: autoflake + entry: poetry run autoflake + language: system + types: [ python ] + args: [ --in-place, --remove-all-unused-imports, --remove-duplicate-keys ] + + - id: isort + name: isort + entry: poetry run isort + language: system + types: [ python ] + - id: flake8 - args: - - "--max-line-length=88" - - "--extend-ignore=E203" - exclude: > - (?x)^( - migrations/.+ - )$ - - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.782 - hooks: + name: Check with Flake8 + entry: poetry run flake8 + language: system + pass_filenames: false + types: [ python ] + args: [--count, .] + - id: mypy - args: - - "--strict" - - "--ignore-missing-imports" - - "--allow-subclassing-any" - - "--allow-untyped-calls" - - "--pretty" - - "--show-error-codes" - - "--no-warn-return-any" - - "--implicit-reexport" - - "--allow-untyped-decorators" - exclude: > - (?x)^( - migrations/.+ - )$ - - - repo: https://github.com/timothycrosley/isort - rev: 5.4.2 - hooks: - - id: isort - args: - - "-l=88" - - "--multi-line=3" - - "--tc" - - "--up" - - "--force-grid-wrap=0" - exclude: > - (?x)^( - migrations/.+ - )$ + name: Validate types with MyPy + entry: poetry run mypy + language: system + types: [ python ] + + - id: yesqa + name: Remove usless noqa + entry: poetry run yesqa + language: system + types: [ python ] + + - id: pytest + name: pytest + entry: poetry run pytest + language: system + pass_filenames: false + types: [ python ] diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/Dockerfile b/fastapi_template/template/{{cookiecutter.project_name}}/Dockerfile deleted file mode 100644 index 4631950..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/Dockerfile +++ /dev/null @@ -1,105 +0,0 @@ -# `python-base` sets up all our shared environment variables -FROM python:3.8.5-slim as python-base - - # python -ENV PYTHONUNBUFFERED=1 \ - # prevents python creating .pyc files - PYTHONDONTWRITEBYTECODE=1 \ - \ - # pip - PIP_NO_CACHE_DIR=off \ - PIP_DISABLE_PIP_VERSION_CHECK=on \ - PIP_DEFAULT_TIMEOUT=100 \ - \ - # poetry - # https://python-poetry.org/docs/configuration/#using-environment-variables - POETRY_VERSION=1.0.10 \ - # make poetry install to this location - POETRY_HOME="/opt/poetry" \ - # make poetry create the virtual environment in the project's root - # it gets named `.venv` - POETRY_VIRTUALENVS_IN_PROJECT=true \ - # do not ask any interactive question - POETRY_NO_INTERACTION=1 \ - \ - # paths - # this is where our requirements + virtual environment will live - PYSETUP_PATH="/opt/pysetup" \ - VENV_PATH="/opt/pysetup/.venv" - -RUN apt-get update \ - && apt-get install --no-install-recommends -y \ - # deps for installing poetry - postgresql=11+200+deb10u4 \ - libpq-dev=11.9-0+deb10u1 \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -# prepend poetry and venv to path -ENV PATH="$POETRY_HOME/bin:$VENV_PATH/bin:$PATH" - - -# `builder-base` stage is used to build deps + create our virtual environment -FROM python-base as builder-base -RUN apt-get update \ - && apt-get install --no-install-recommends -y \ - # deps for installing poetry - curl=7.64.0-4+deb10u1 \ - # deps for building python deps - build-essential=12.6 \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -# This is DL3008 -SHELL ["/bin/bash", "-o", "pipefail", "-c"] -# install poetry - respects $POETRY_VERSION & $POETRY_HOME -RUN curl -sSL https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py | python - -# copy project requirement files here to ensure they will be cached. -WORKDIR $PYSETUP_PATH -COPY poetry.lock* pyproject.toml ./ - -# install runtime deps - uses $POETRY_VIRTUALENVS_IN_PROJECT internally -RUN poetry install --no-dev - -# `development` image is used during development / testing -FROM python-base as development -ENV FASTAPI_ENV=development -WORKDIR $PYSETUP_PATH - -# copy in our built poetry + venv -COPY --from=builder-base $POETRY_HOME $POETRY_HOME -COPY --from=builder-base $PYSETUP_PATH $PYSETUP_PATH - -# quicker install as runtime deps are already installed -RUN poetry install - -VOLUME /app -# will become mountpoint of our code -WORKDIR /app - -EXPOSE 8000 -ENV LOG_LEVEL=DEBUG - -CMD [ "uvicorn", "--reload", "--access-log", "--log-level", "debug", "--host", "0.0.0.0", "--port", "8000", "src.server:app" ] - -{% if cookiecutter.add_scheduler == "True" -%} -FROM python-base as scheduler -WORKDIR $PYSETUP_PATH - -# copy in our built poetry + venv -COPY --from=builder-base $POETRY_HOME $POETRY_HOME -COPY --from=builder-base $PYSETUP_PATH $PYSETUP_PATH - -VOLUME /app -WORKDIR /app - -CMD ["python", "scheduler.py"] -{% endif %} -# `production` image used for runtime -FROM python-base as production -ENV FASTAPI_ENV=production -COPY --from=builder-base $PYSETUP_PATH $PYSETUP_PATH -COPY . /app/ -WORKDIR /app -CMD ["gunicorn", "-k", "uvicorn.workers.UvicornWorker", "src.server:app"] diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/README.md b/fastapi_template/template/{{cookiecutter.project_name}}/README.md index 7d9052d..621d8f8 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/README.md +++ b/fastapi_template/template/{{cookiecutter.project_name}}/README.md @@ -1,69 +1,7 @@ # {{cookiecutter.project_name}} -{{cookiecutter.project_description}} +Start a project with: -## For developers - -To run project locally you can just type `docker-compose up` - -### Needed environment variables for CI\CD -* APP_ENV_FILE - path to .env file on server - -## How to run the application - -At first, you need to install all dependencies with poetry - -```bash -poetry install -``` - -For tests you need to set all needed environment variables. -All needed variables shown in `envs/.env.example`. - -If all new environment parameters are stored in .env file, then you better to start testing/application using `dotenv` program, or set `EnvironmentFile` in your systemd-module. - -### Applying migrations and startup -Before running the application don't forget to apply pending migrations. To do so run the following command in the application directory: -```bash -dotenv -f ".env.file" run alembic upgrade head; -``` - -To test and start the application with `dotenv` run following commands: -```bash -# Testing -dotenv -f envs/test.env run pytest -# Startup -dotenv -f envs/.env run uvicorn --access-log --log-level debug --host 0.0.0.0 --port 8100 src.server:app -``` - -If all environment variables already set: -```bash -# Testing -pytest -# Startup -uvicorn --access-log --log-level debug --host 0.0.0.0 --port 8100 src.server:app ``` - -Example of configured systemd module for application shown in [systemd/{{cookiecutter.project_name}}_service.service](systemd/{{ cookiecutter.project_name }}_service.service) file. - - -## Task scheduler startup -Task scheduler is a script that runs background tasks periodically. -You can read more about scheduler at [aioschedule docs](https://pypi.org/project/aioschedule/) - -```bash -# Command to run scheduler script with existing environment variables: -python scheduler.py - -# Command to run sceduler with .env file: -dotenv -f envs/.env run python scheduler.py +docker-compose -f deploy/docker-compose.yml --project-directory . up ``` - -Example of configured systemd module for scheduler shown in [systemd/{{cookiecutter.project_name}}_scheduler.service](systemd/{{ cookiecutter.project_name }}_scheduler.service) - -## About systemd files - -To use systemd files on different machine you may need to change following parameters: -* EnvironmentFile - absolute path to .env file to use during execution; -* WorkingDirectory - absolute path to project directory on your machine or server; -* ExecStart - command to start the application. \ No newline at end of file diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/alembic.ini b/fastapi_template/template/{{cookiecutter.project_name}}/alembic.ini index 97a52b1..588ed01 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/alembic.ini +++ b/fastapi_template/template/{{cookiecutter.project_name}}/alembic.ini @@ -1,52 +1,22 @@ -# A generic, single database configuration. - [alembic] -# path to migration scripts -script_location = migrations - -# template used to generate migration files -# file_template = %%(rev)s_%%(slug)s - -# timezone to use when rendering the date -# within the migration file as well as the filename. -# string value is passed to dateutil.tz.gettz() -# leave blank for localtime -# timezone = - -# max length of characters to apply to the -# "slug" field +script_location = {{cookiecutter.project_name}}/db/migrations +file_template = %%(year)d-%%(month).2d-%%(day).2d-%%(hour).2d-%%(minute).2d_%%(rev)s +prepend_sys_path = . +output_encoding = utf-8 # truncate_slug_length = 40 -# set to 'true' to run the environment during -# the 'revision' command, regardless of autogenerate -# revision_environment = false - -# set to 'true' to allow .pyc and .pyo files without -# a source .py file to be detected as revisions in the -# versions/ directory -# sourceless = false - -# version location specification; this defaults -# to src/alembic/versions. When using multiple version -# directories, initial revisions must be specified with --version-path -# version_locations = %(here)s/bar %(here)s/bat src/alembic/versions - -# the output encoding used when revision files -# are written from script.py.mako -# output_encoding = utf-8 +[post_write_hooks] +hooks = black,autoflake,isort +black.type = console_scripts +black.entrypoint = black -[post_write_hooks] -# post_write_hooks defines scripts or Python functions that are run -# on newly generated revision scripts. See the documentation for further -# detail and examples +autoflake.type = console_scripts +autoflake.entrypoint = autoflake -# 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 +isort.type = console_scripts +isort.entrypoint = isort # Logging configuration [loggers] diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/conditional_files.json b/fastapi_template/template/{{cookiecutter.project_name}}/conditional_files.json index ce4d22d..3f5ca14 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/conditional_files.json +++ b/fastapi_template/template/{{cookiecutter.project_name}}/conditional_files.json @@ -1,44 +1,39 @@ { - "features": [ - { - "name": "Redis support", - "enabled": "{{cookiecutter.add_redis|lower}}", - "resources": [ - "src/services/redis.py", - "src/api/redis_api" - ] - }, - { - "name": "Task scheduler", - "enabled": "{{cookiecutter.add_scheduler|lower}}", - "resources": [ - "scheduler.py", - "systemd/{{ cookiecutter.project_name }}_scheduler.service" - ] - }, - { - "name": "Systemd support", - "enabled": "{{cookiecutter.add_systemd|lower}}", - "resources": [ - "systemd" - ] - }, - { - "name": "Dummy DB model", - "enabled": "{{cookiecutter.add_dummy_model|lower}}", - "resources": [ - "src/models/dummy_db_model.py", - "migrations/versions/7ae297ab5ac1_created_dummy_model.py", - "tests/dummy_db_test.py", - "src/api/dummy_db" - ] - }, - { - "name": "Elastic search", - "enabled": "{{cookiecutter.add_elastic_search|lower}}", - "resources": [ - "src/services/elastic" - ] - } - ] -} \ No newline at end of file + "Redis": { + "enabled": "{{cookiecutter.enable_redis}}", + "resources": [ + "{{cookiecutter.project_name}}/web/api/redis", + "{{cookiecutter.project_name}}/services/redis", + "deploy/kube/redis.yml" + ] + }, + "Kubernetes": { + "enabled": "{{cookiecutter.enable_kube}}", + "resources": [ + "deploy/kube" + ] + }, + "Database support": { + "enabled": "{{cookiecutter.db_info.name != 'none'}}", + "resources": [ + "{{cookiecutter.project_name}}/web/api/dummy", + "{{cookiecutter.project_name}}/db", + "{{cookiecutter.project_name}}/conftest.py", + "deploy/kube/db.yml", + "alembic.ini" + ] + }, + "Alembic": { + "enabled": "{{cookiecutter.enable_alembic}}", + "resources": [ + "alembic.ini", + "{{cookiecutter.project_name}}/db/migrations" + ] + }, + "CI support": { + "enabled": "{{cookiecutter.enable_ci}}", + "resources": [ + ".gitlab-ci.yml" + ] + } +} diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/deploy.sh b/fastapi_template/template/{{cookiecutter.project_name}}/deploy.sh deleted file mode 100644 index baf38a9..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/deploy.sh +++ /dev/null @@ -1,14 +0,0 @@ -echo "Your deploy steps must run here" - -{% if cookiecutter.add_systemd == "True" -%} -# Function to replace stub values in service files -function replace_stubs() { - sed -i "s#WorkingDirectory=.*#WorkingDirectory=${APP_DIR:?}#" "$1" - sed -i "s#EnvironmentFile=ENV_FILE*#EnvironmentFile=${ENV_FILE_PATH:?}#" "$1" -} - -replace_stubs "./systemd/{{ cookiecutter.project_name }}_service.service" -{% if cookiecutter.add_scheduler == "True" -%} -replace_stubs "./systemd/{{ cookiecutter.project_name }}_scheduler.service" -{% endif %} -{% endif %} \ No newline at end of file diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/deploy/Dockerfile b/fastapi_template/template/{{cookiecutter.project_name}}/deploy/Dockerfile new file mode 100644 index 0000000..b40f5eb --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/deploy/Dockerfile @@ -0,0 +1,32 @@ +FROM python:3.9.6-slim-buster + +RUN useradd -m {{cookiecutter.project_name}} + +USER {{cookiecutter.project_name}} + +ENV PATH="${PATH}:/home/{{cookiecutter.project_name}}/.poetry/bin:/home/{{cookiecutter.project_name}}/.local/bin" + +RUN pip install poetry==1.1.7 + +# Installing requirements +RUN poetry config virtualenvs.create false + +COPY pyproject.toml poetry.lock /home/{{cookiecutter.project_name}}/app/ +WORKDIR /home/{{cookiecutter.project_name}}/app/ + +RUN poetry install --no-dev + +# Copying actuall application +COPY . /home/{{cookiecutter.project_name}}/app/src/ +WORKDIR /home/{{cookiecutter.project_name}}/app/src/ +RUN pip install --use-feature=in-tree-build . + +WORKDIR /home/{{cookiecutter.project_name}}/app + +USER root +RUN rm -rf /home/{{cookiecutter.project_name}}/app/src +RUN chown -R {{cookiecutter.project_name}} /home/{{cookiecutter.project_name}} +RUN chmod -R 700 /home/{{cookiecutter.project_name}} +USER {{cookiecutter.project_name}} + +CMD python -m {{cookiecutter.project_name}} diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/deploy/docker-compose.yml b/fastapi_template/template/{{cookiecutter.project_name}}/deploy/docker-compose.yml new file mode 100644 index 0000000..3a0be7c --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/deploy/docker-compose.yml @@ -0,0 +1,45 @@ +version: '3.7' + +services: + api: + build: + context: . + dockerfile: ./deploy/Dockerfile + env_file: + - .env + environment: + - {{cookiecutter.project_name | upper }}_VAR=test # stub + {%- if cookiecutter.db_info.name == "postgresql" %} + - {{cookiecutter.project_name | upper }}_DB_PORT=5432 + {%- endif %} + {%- if cookiecutter.db_info.name == "mysql" %} + - {{cookiecutter.project_name | upper }}_DB_PORT=3306 + {%- endif %} + + {%- if cookiecutter.db_info.name == "postgresql" %} + db: + image: postgres:13.4-buster + hostname: {{cookiecutter.project_name}}-db + environment: + - POSTGRES_PASSWORD={{cookiecutter.project_name}} + - POSTGRES_USER={{cookiecutter.project_name}} + - POSTGRES_DB={{cookiecutter.project_name}} + {%- endif %} + + {%- if cookiecutter.db_info.name == "mysql" %} + db: + image: bitnami/mysql:8.0.26 + hostname: {{cookiecutter.project_name}}-db + environment: + - MYSQL_PASSWORD={{cookiecutter.project_name}} + - MYSQL_USER={{cookiecutter.project_name}} + - MYSQL_DATABASE={{cookiecutter.project_name}} + {%- endif %} + + {%- if cookiecutter.enable_redis == "True" %} + redis: + image: bitnami/redis:6.2.5 + hostname: {{cookiecutter.project_name}}-redis + environment: + - ALLOW_EMPTY_PASSWORD=yes + {%- endif %} diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/deploy/kube/app.yml b/fastapi_template/template/{{cookiecutter.project_name}}/deploy/kube/app.yml new file mode 100644 index 0000000..8d9330c --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/deploy/kube/app.yml @@ -0,0 +1,57 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{cookiecutter.project_name}} +spec: + selector: + matchLabels: + app: {{cookiecutter.project_name}} + template: + metadata: + labels: + app: {{cookiecutter.project_name}} + spec: + containers: + - name: {{cookiecutter.project_name}} + image: {{cookiecutter.project_name}}:latest + imagePullPolicy: Always + envFrom: + - configMapRef: + name: {{cookiecutter.project_name}}-env + resources: + limits: + memory: "300Mi" + cpu: "200m" + ports: + - containerPort: 8000 +--- +apiVersion: v1 +kind: Service +metadata: + name: {{cookiecutter.project_name}} +spec: + selector: + app: {{cookiecutter.project_name}} + ports: + - port: 80 + targetPort: 8000 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{cookiecutter.project_name}} + labels: + name: {{cookiecutter.project_name}} +spec: + rules: + - host: {{cookiecutter.project_name}}.local + http: + paths: + - pathType: Prefix + path: "/" + backend: + service: + name: {{cookiecutter.project_name}} + port: + number: 80 diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/deploy/kube/db.yml b/fastapi_template/template/{{cookiecutter.project_name}}/deploy/kube/db.yml new file mode 100644 index 0000000..5fe6ed1 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/deploy/kube/db.yml @@ -0,0 +1,33 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{cookiecutter.project_name}}-db +spec: + selector: + matchLabels: + app: {{cookiecutter.project_name}}-db + template: + metadata: + labels: + app: {{cookiecutter.project_name}}-db + spec: + containers: + - name: database + image: {{cookiecutter.db_info.image}} + resources: + limits: + memory: "300Mi" + cpu: "200m" + ports: + - containerPort: 5432 +--- +apiVersion: v1 +kind: Service +metadata: + name: "{{cookiecutter.project_name}}-db" +spec: + selector: + app: {{cookiecutter.project_name}}-db + ports: + - port: {{cookiecutter.db_info.port}} + targetPort: {{cookiecutter.db_info.port}} diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/deploy/kube/redis.yml b/fastapi_template/template/{{cookiecutter.project_name}}/deploy/kube/redis.yml new file mode 100644 index 0000000..4e1e304 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/deploy/kube/redis.yml @@ -0,0 +1,33 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{cookiecutter.project_name}}-redis +spec: + selector: + matchLabels: + app: {{cookiecutter.project_name}}-redis + template: + metadata: + labels: + app: {{cookiecutter.project_name}}-redis + spec: + containers: + - name: redis + image: bitnami/redis:6.2.5 + resources: + limits: + memory: "300Mi" + cpu: "200m" + ports: + - containerPort: 6379 +--- +apiVersion: v1 +kind: Service +metadata: + name: "{{cookiecutter.project_name}}-redis" +spec: + selector: + app: {{cookiecutter.project_name}}-redis + ports: + - port: 6379 + targetPort: 6379 diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/docker-compose.prod.yml b/fastapi_template/template/{{cookiecutter.project_name}}/docker-compose.prod.yml deleted file mode 100644 index 54c42e8..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/docker-compose.prod.yml +++ /dev/null @@ -1,82 +0,0 @@ -# Local docker-compose configuration - -version: '3.7' - -services: - back: - container_name: {{ cookiecutter.project_name }}_backend - build: - context: . - target: production - labels: - {{ cookiecutter.project_name }}.description: {{ cookiecutter.project_description }} - env_file: - - envs/.env - depends_on: - - db - networks: - - {{ cookiecutter.project_name }}_network - - db: - container_name: {{ cookiecutter.project_name }}_db - image: postgres:12.4 - volumes: - - db_data:/var/lib/postgresql/data - networks: - - {{ cookiecutter.project_name }}_network - env_file: - - envs/.env - - {% if cookiecutter.add_redis == "True" -%} - redis: - container_name: {{ cookiecutter.project_name }}_redis - image: bitnami/redis:6.0.7 - volumes: - - redis_data:/bitnami/redis/data - env_file: - - envs/.env - networks: - - {{ cookiecutter.project_name }}_network - {% endif %} - - {% if cookiecutter.add_scheduler == "True" -%} - scheduler: - container_name: {{ cookiecutter.project_name }}_scheduler - build: - context: . - target: scheduler - labels: - scheduler.description: "{{ cookiecutter.project_name }} scheduler image" - env_file: - - envs/.env - volumes: - - ./:/app - depends_on: - - db - networks: - - {{ cookiecutter.project_name }}_network - {% endif %} - - {% if cookiecutter.add_elastic_search == "True" -%} - es: - restart: always - container_name: {{ cookiecutter.project_name }}_es_dev - image: elasticsearch:7.3.0 - volumes: - - es_data_dev:/usr/share/elasticsearch/data - environment: - - discovery.type=single-node - - bootstrap.memory_lock=true - - "ES_JAVA_OPTS=-Xms512m -Xmx512m" - networks: - - {{ cookiecutter.project_name }}_network_dev - {% endif %} - -volumes: - db_data: - redis_data: - es_data_dev: - -networks: - {{ cookiecutter.project_name }}_network: - name: {{ cookiecutter.project_name }}_network \ No newline at end of file diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/docker-compose.yml b/fastapi_template/template/{{cookiecutter.project_name}}/docker-compose.yml deleted file mode 100644 index 3fe9d8c..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/docker-compose.yml +++ /dev/null @@ -1,90 +0,0 @@ -# Local docker-compose configuration - -version: '3.7' - -services: - back: &back - container_name: {{ cookiecutter.project_name }}_backend_dev - build: - context: . - target: development - labels: - {{ cookiecutter.project_name }}.description: {{ cookiecutter.project_description }} - env_file: - - envs/.env - ports: - - {{ cookiecutter.default_port }}:8000 - volumes: - - ./:/app - depends_on: - - db - networks: - - {{ cookiecutter.project_name }}_network_dev - - db: - container_name: {{ cookiecutter.project_name }}_db_dev - image: postgres:12.4 - volumes: - - db_data_dev:/var/lib/postgresql/data - ports: - - 5432:5432 - networks: - - {{ cookiecutter.project_name }}_network_dev - env_file: - - envs/.env - - {% if cookiecutter.add_redis == "True" -%} - redis: - container_name: {{ cookiecutter.project_name }}_redis_dev - image: bitnami/redis:6.0.7 - volumes: - - redis_data_dev:/bitnami/redis/data - env_file: - - envs/.env - ports: - - 6379:6379 - networks: - - {{ cookiecutter.project_name }}_network_dev - {% endif %} - - {% if cookiecutter.add_scheduler == "True" -%} - scheduler: - container_name: {{ cookiecutter.project_name }}_scheduler_dev - build: - context: . - target: scheduler - labels: - scheduler.description: "{{ cookiecutter.project_name }} scheduler image" - env_file: - - envs/.env - volumes: - - ./:/app - depends_on: - - db - networks: - - {{ cookiecutter.project_name }}_network_dev - {% endif %} - - {% if cookiecutter.add_elastic_search == "True" -%} - es: - restart: always - container_name: {{ cookiecutter.project_name }}_es_dev - image: elasticsearch:7.3.0 - volumes: - - es_data_dev:/usr/share/elasticsearch/data - environment: - - discovery.type=single-node - - bootstrap.memory_lock=true - - "ES_JAVA_OPTS=-Xms512m -Xmx512m" - networks: - - {{ cookiecutter.project_name }}_network_dev - {% endif %} - -volumes: - db_data_dev: - redis_data_dev: - es_data_dev: - -networks: - {{ cookiecutter.project_name }}_network_dev: - name: {{ cookiecutter.project_name }}_network_dev \ No newline at end of file diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/envs/.env b/fastapi_template/template/{{cookiecutter.project_name}}/envs/.env deleted file mode 100644 index 4c2e415..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/envs/.env +++ /dev/null @@ -1,17 +0,0 @@ -LOG_LEVEL=Debug -POSTGRES_HOST=db -POSTGRES_PORT=5432 -POSTGRES_DB={{cookiecutter.project_name}}_db -POSTGRES_USER={{cookiecutter.project_name}} -POSTGRES_PASSWORD={{cookiecutter.postgres_password}} -{% if cookiecutter.add_redis == "True" -%} -REDIS_PASSWORD={{cookiecutter.redis_password}} -REDIS_HOST=redis -REDIS_PORT=6379 -{% endif %} -{% if cookiecutter.add_scheduler == "True" -%} -SCHEDULE_TIMER=20 -{% endif %} -{% if cookiecutter.add_elastic_search == "True" -%} -ELASTIC_HOST=http://es:9200 -{% endif %} \ No newline at end of file diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/envs/example.env b/fastapi_template/template/{{cookiecutter.project_name}}/envs/example.env deleted file mode 100644 index f4ffb55..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/envs/example.env +++ /dev/null @@ -1,18 +0,0 @@ -POSTGRES_HOST=db -POSTGRES_PORT=5432 -POSTGRES_DB=secret_db -POSTGRES_USER=secret_user -POSTGRES_PASSWORD=SomeSecretRedisPassword -LOG_LEVEL=Debug -{% if cookiecutter.add_redis == "True" -%} -REDIS_PASSWORD=SomeSecretRedisPassword -REDIS_HOST=redis -REDIS_PORT=6379 -{% endif %} -HTTPBIN_HOST=https://httpbin.org/ -{% if cookiecutter.add_scheduler == "True" -%} -SCHEDULE_TIMER=20 -{% endif %} -{% if cookiecutter.add_elastic_search == "True" -%} -ELASTIC_HOST=http://es:9200 -{% endif %} \ No newline at end of file diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/envs/test.env b/fastapi_template/template/{{cookiecutter.project_name}}/envs/test.env deleted file mode 100644 index 96a9cb7..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/envs/test.env +++ /dev/null @@ -1,18 +0,0 @@ -POSTGRES_HOST=localhost -POSTGRES_PORT=5432 -POSTGRES_DB={{cookiecutter.project_name}}_db_test -POSTGRES_USER={{cookiecutter.project_name}} -POSTGRES_PASSWORD={{cookiecutter.postgres_password}} -DB_ECHO=True -{% if cookiecutter.add_redis == "True" -%} -REDIS_PASSWORD={{cookiecutter.redis_password}} -REDIS_HOST=localhost -REDIS_PORT=6379 -{% endif %} -HTTPBIN_HOST=https://httpbin.org/ -{% if cookiecutter.add_scheduler == "True" -%} -SCHEDULE_TIMER=20 -{% endif %} -{% if cookiecutter.add_elastic_search == "True" -%} -ELASTIC_HOST=http://es:9200 -{% endif %} \ No newline at end of file diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/main.py b/fastapi_template/template/{{cookiecutter.project_name}}/main.py deleted file mode 100644 index 90b6168..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/main.py +++ /dev/null @@ -1,45 +0,0 @@ -import argparse -from typing import Any, Dict, Optional - -from fastapi import FastAPI -from gunicorn.app.base import BaseApplication - -from src.server import app - - -class StandaloneApplication(BaseApplication): - def __init__(self, application_instance: FastAPI, run_options: Optional[Dict[str, Any]] = None): - self.options = run_options or {} - self.application = application_instance - super().__init__() - - def load_config(self) -> None: - config = { - key: value - for key, value in self.options.items() - if key in self.cfg.settings and value is not None - } - for key, value in config.items(): - self.cfg.set(key.lower(), value) - - def load(self) -> FastAPI: - return self.application - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser() - parser.add_argument("--host", type=str, default="0.0.0.0") - parser.add_argument("--port", type=int, default=8000) - parser.add_argument("--pid-file", type=str, default="/tmp/{{cookiecutter.project_name}}.pid") - return parser.parse_args() - - -if __name__ == "__main__": - args = parse_args() - options = { - "bind": f"{args.host}:{args.port}", - "workers": 4, - "worker_class": "uvicorn.workers.UvicornWorker", - "pidfile": args.pid_file, - } - StandaloneApplication(app, options).run() diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/migrations/env.py b/fastapi_template/template/{{cookiecutter.project_name}}/migrations/env.py deleted file mode 100644 index 9df59ac..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/migrations/env.py +++ /dev/null @@ -1,96 +0,0 @@ -import os -import sys -from logging.config import fileConfig - -from alembic import context -from sqlalchemy import create_engine -from sqlalchemy.engine.url import URL, make_url - -sys.path.insert(0, os.path.abspath("")) -# this is the Alembic Config object, which provides -# access to the values within the .ini file in use. -# noqa -from src.settings import settings - -config = context.config - -# Interpret the config file for Python logging. -# This line sets up loggers basically. -fileConfig(config.config_file_name) - -# add your model's MetaData object here -# for 'autogenerate' support -# from myapp import mymodel -# target_metadata = mymodel.Base.metadata -# noqa -from src.services.db import db_meta as target_metadata -from src.models import * # isort:skip - - -def run_migrations_offline(): - """Run migrations in 'offline' mode. - - This configures the context with just a URL - and not an Engine, though an Engine is acceptable - here as well. By skipping the Engine creation - we don't even need a DBAPI to be available. - - Calls to context.execute() here emit the given string to the - script output. - - """ - url = make_url( - URL( - drivername=settings.db_driver, - username=settings.postgres_user, - password=settings.postgres_password, - host=settings.postgres_host, - port=settings.postgres_port, - database=settings.postgres_db, - ) - ) - - context.configure( - url=url, - target_metadata=target_metadata, - literal_binds=True, - dialect_opts={"paramstyle": "named"}, - ) - - with context.begin_transaction(): - context.run_migrations() - - -def run_migrations_online(): - """Run migrations in 'online' mode. - - In this scenario we need to create an Engine - and associate a connection with the context. - - """ - url = make_url( - URL( - drivername=settings.db_driver, - username=settings.postgres_user, - password=settings.postgres_password, - host=settings.postgres_host, - port=settings.postgres_port, - database=settings.postgres_db, - ) - ) - - connectable = create_engine( - str(url), - ) - - with connectable.connect() as connection: - context.configure(connection=connection, target_metadata=target_metadata) - - with context.begin_transaction(): - context.run_migrations() - - -if context.is_offline_mode(): - run_migrations_offline() -else: - run_migrations_online() diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/migrations/versions/7ae297ab5ac1_created_dummy_model.py b/fastapi_template/template/{{cookiecutter.project_name}}/migrations/versions/7ae297ab5ac1_created_dummy_model.py deleted file mode 100644 index 3b1e426..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/migrations/versions/7ae297ab5ac1_created_dummy_model.py +++ /dev/null @@ -1,42 +0,0 @@ -"""Added dummy model - -Revision ID: f9c5d2dba936 -Revises: -Create Date: 2020-10-05 23:56:58.658606 - -""" -import sqlalchemy as sa -from alembic import op -from sqlalchemy.dialects import postgresql - -# revision identifiers, used by Alembic. -revision = 'f9c5d2dba936' -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('dummydbmodel', - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('name', sa.String(), nullable=False), - sa.Column('surname', sa.String(), nullable=False), - {% if cookiecutter.add_elastic_search == "True" -%}sa.Column('tags', sa.String(), nullable=False, default=""),{% endif %} - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('clock_timestamp()'), - nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('clock_timestamp()'), - nullable=False), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_dummydbmodel_name'), 'dummydbmodel', ['name'], unique=False) - op.create_index(op.f('ix_dummydbmodel_surname'), 'dummydbmodel', ['surname'], unique=False) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_index(op.f('ix_dummydbmodel_surname'), table_name='dummydbmodel') - op.drop_index(op.f('ix_dummydbmodel_name'), table_name='dummydbmodel') - op.drop_table('dummydbmodel') - # ### end Alembic commands ### diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/pyproject.toml b/fastapi_template/template/{{cookiecutter.project_name}}/pyproject.toml index 811eab5..b93dc40 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/pyproject.toml +++ b/fastapi_template/template/{{cookiecutter.project_name}}/pyproject.toml @@ -2,39 +2,76 @@ name = "{{cookiecutter.project_name}}" version = "0.1.0" description = "{{cookiecutter.project_description}}" -authors = ["{{cookiecutter.full_name}} <{{cookiecutter.email}}>"] +authors = [ + +] +maintainers = [ + +] +readme = "README.md" [tool.poetry.dependencies] -python = "^3.8" -uvicorn = "^0.11.8" -fastapi = "^0.61.1" -sqlalchemy = "^1.3.19" -loguru = "^0.5.2" -alembic = "^1.4.3" -httpx = "^0.14.3" -ujson = "^4.0.1" -gunicorn = "^20.0.4" -httptools = "^0.1.1" -aiopg = "^1.0.0" -{% if cookiecutter.add_redis == "True" -%} -aioredis = "^1.3.1" -{% endif %} -{% if cookiecutter.add_elastic_search == "True" -%} -elasticsearch-dsl = "^7.3.0" -elasticsearch = {extras = ["async"], version = "^7.9.1"} -{% endif %} -{% if cookiecutter.add_scheduler == "True" -%} -aioschedule = "^0.5.2" -{% endif %} +python = "^3.9" +fastapi = "^0.68.0" +uvicorn = "^0.15.0" +pydantic = {version = "^1.8.2", extras = ["dotenv"]} +yarl = "^1.6.3" +{%- if cookiecutter.db_info.name != "none" %} +SQLAlchemy = {version = ">=1.4,<1.4.23", extras = ["mypy"]} +greenlet = "^1.1.1" +{%- endif %} +{% if cookiecutter.enable_alembic == "True" %} +alembic = "^1.6.5" +{%- endif %} +{%- if cookiecutter.db_info.name == "postgresql" %} +asyncpg = {version = "^0.24.0", extras = ["sa"]} +{%- endif %} +{%- if cookiecutter.db_info.name == "sqlite" %} +aiosqlite = "^0.17.0" +{%- endif %} +{%- if cookiecutter.db_info.name == "mysql" %} +aiomysql = "^0.0.21" +{%- endif %} +{%- if cookiecutter.enable_redis == "True" %} +aioredis = {version = "^2.0.0", extras = ["hiredis"]} +{%- endif %} [tool.poetry.dev-dependencies] -pytest = "^6.0.2" -requests = "^2.24.0" -pytest-asyncio = "^0.14.0" -pre-commit = "^2.7.1" -pytest-cov = "^2.10.1" -python-dotenv = "^0.14.0" +pytest = "^6.0" +flake8 = "^3.9.2" +mypy = "^0.910" +isort = "^5.9.3" +yesqa = "^1.2.3" +pre-commit = "^2.11.0" +wemake-python-styleguide = "^0.15.3" +black = "^21.7b0" +{%- if cookiecutter.db_info.name != "none" %} +pytest-async-sqlalchemy = "^0.1.3" +{%- endif %} +autoflake = "^1.4" +{%- if cookiecutter.enable_alembic %} +pytest-alembic = "^0.3.3" +{%- endif %} +pytest-cov = "^2.12.1" +pytest-asyncio = "^0.15.1" + +[tool.isort] +profile = "black" +multi_line_output = 3 + +[tool.mypy] +strict = true +ignore_missing_imports = true +allow_subclassing_any = true +allow_untyped_calls = true +pretty = true +show_error_codes = true +implicit_reexport = true +allow_untyped_decorators = true +{%- if cookiecutter.db_info.name != "none" %} +plugins = ["sqlalchemy.ext.mypy.plugin"] +{%- endif %} [build-system] -requires = ["poetry>=0.12"] -build-backend = "poetry.masonry.api" \ No newline at end of file +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/pytest.ini b/fastapi_template/template/{{cookiecutter.project_name}}/pytest.ini deleted file mode 100644 index 69ecb57..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/pytest.ini +++ /dev/null @@ -1,4 +0,0 @@ -[pytest] -minversion = 6.0 -testpaths = - tests \ No newline at end of file diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/scheduler.py b/fastapi_template/template/{{cookiecutter.project_name}}/scheduler.py deleted file mode 100644 index 513db02..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/scheduler.py +++ /dev/null @@ -1,19 +0,0 @@ -import asyncio - -import aioschedule as schedule -from loguru import logger - -from src.settings import settings - - -async def scheduled_task() -> None: - logger.info("I was scheduled to run now") - - -schedule.every(settings.schedule_timer).seconds.do(scheduled_task) - -if __name__ == "__main__": - logger.debug("Started scheduler") - loop = asyncio.get_event_loop() - while True: - loop.run_until_complete(schedule.run_pending()) \ No newline at end of file diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/src/api/__init__.py b/fastapi_template/template/{{cookiecutter.project_name}}/src/api/__init__.py deleted file mode 100644 index f4114f6..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/src/api/__init__.py +++ /dev/null @@ -1,28 +0,0 @@ -from fastapi import APIRouter - -from src.api.httpbin import URL_PREFIX as httpbin_prefix -from src.api.httpbin import router as httpbin_router -{% if cookiecutter.add_redis == "True" -%} -from src.api.redis_api import URL_PREFIX as redis_prefix -from src.api.redis_api import router as redis_router -{% endif %} -{% if cookiecutter.add_dummy_model == "True" -%} -from src.api.dummy_db import URL_PREFIX as dummy_db_prefix -from src.api.dummy_db import router as dummy_db_router -{% endif %} - -api_router = APIRouter() - -api_router.include_router( - router=httpbin_router, prefix=httpbin_prefix, tags=['HTTPBin'] -) -{% if cookiecutter.add_redis == "True" -%} -api_router.include_router( - router=redis_router, prefix=redis_prefix, tags=["Redis"] -) -{% endif %} -{% if cookiecutter.add_dummy_model == "True" -%} -api_router.include_router( - router=dummy_db_router, prefix=dummy_db_prefix, tags=["Dummy db object"] -) -{% endif %} \ No newline at end of file diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/src/api/dummy_db/__init__.py b/fastapi_template/template/{{cookiecutter.project_name}}/src/api/dummy_db/__init__.py deleted file mode 100644 index df53d3a..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/src/api/dummy_db/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from src.api.dummy_db.routes import URL_PREFIX, router - -__all__ = ['router', 'URL_PREFIX'] diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/src/api/dummy_db/routes.py b/fastapi_template/template/{{cookiecutter.project_name}}/src/api/dummy_db/routes.py deleted file mode 100644 index 886ed07..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/src/api/dummy_db/routes.py +++ /dev/null @@ -1,88 +0,0 @@ -import uuid -from typing import Optional - -from fastapi import APIRouter, Depends - -from src.api.dummy_db.schema import ( - UpdateDummyModel, - GetDummyResponse, -) -{% if cookiecutter.add_elastic_search == "True" -%} -from src.api.dummy_db.schema import DummyElasticResponse, ElasticAdd -from src.services.elastic.schema import ElasticFilterModel -{%else%} -from src.api.dummy_db.schema import BaseDummyModel -{% endif %} - -from src.models import DummyDBModel -from src.services.db.session import Session, db_session - -router = APIRouter() -URL_PREFIX = "/dummy_db_obj" - - -@router.put("/") -async def create_dummy(dummy_obj: {% if cookiecutter.add_elastic_search == "True" -%}ElasticAdd{% else %}BaseDummyModel{% endif %}, session: Session = Depends(db_session)) -> None: - """ - Add dummy object in database. - If you have elastic search feature enabled it will be added in your index. - """ - {% if cookiecutter.add_elastic_search == "True" -%} - insert_query = DummyDBModel.create(**dummy_obj.dict()).returning(DummyDBModel.id) - model_id = await session.scalar(insert_query) - await DummyDBModel.elastic_add( - model_id=model_id, - **dummy_obj.dict() - ) - {% else %} - await session.execute(DummyDBModel.create(**dummy_obj.dict())) - {% endif %} - -@router.post("/{dummy_id}") -async def update_dummy_model( - dummy_id: uuid.UUID, - new_values: UpdateDummyModel, - session: Session = Depends(db_session) -) -> None: - await session.execute(DummyDBModel.update(dummy_id, **new_values.dict())) - {% if cookiecutter.add_elastic_search == "True" -%} - await DummyDBModel.elastic_update( - model_id=dummy_id, - **new_values.dict(exclude_unset=True) - ) - {% endif %} - - -@router.delete("/{dummy_id}") -async def delete_dummy_model(dummy_id: uuid.UUID, session: Session = Depends(db_session)) -> None: - await session.execute(DummyDBModel.delete(dummy_id)) - {% if cookiecutter.add_elastic_search == "True" -%} - await DummyDBModel.elastic_delete(model_id=dummy_id) - {% endif %} - - -@router.get("/", response_model=GetDummyResponse) -async def filter_dummy_models( - dummy_id: Optional[uuid.UUID] = None, - name: Optional[str] = None, - surname: Optional[str] = None, - session: Session = Depends(db_session) -) -> GetDummyResponse: - filter_query = DummyDBModel.filter( - dummy_id=dummy_id, - name=name, - surname=surname - ) - results = await session.fetchall(filter_query) - return GetDummyResponse( - results=results - ) - -{% if cookiecutter.add_elastic_search == "True" -%} -@router.get("/elastic", response_model=DummyElasticResponse) -async def dummy_elastic_filter(query: ElasticFilterModel = Depends(ElasticFilterModel)) -> DummyElasticResponse: - results = await DummyDBModel.elastic_filter(**query.dict()) - return DummyElasticResponse( - results=results - ) -{% endif %} diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/src/api/dummy_db/schema.py b/fastapi_template/template/{{cookiecutter.project_name}}/src/api/dummy_db/schema.py deleted file mode 100644 index aec457a..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/src/api/dummy_db/schema.py +++ /dev/null @@ -1,44 +0,0 @@ -import uuid -from datetime import datetime -from typing import Optional, List -from src.models.dummy_db_model import DummyElasticFilter -from pydantic import Field, BaseConfig -from pydantic.main import BaseModel - - -class BaseDummyModel(BaseModel): - name: str = Field(example="Dummy name") - surname: str = Field(example="Dummy surname") - - -class ReturnDummyModel(BaseDummyModel): - id: uuid.UUID - created_at: datetime - updated_at: datetime - - class Config(BaseConfig): - orm_mode = True - - -class GetDummyResponse(BaseModel): - results: List[ReturnDummyModel] - -{% if cookiecutter.add_elastic_search == "True" -%} -class DummyElasticResponse(BaseModel): - results: List[DummyElasticFilter] - -class ElasticAdd(BaseDummyModel): - tags: str = Field(default="") -{% endif %} - -class UpdateDummyModel(BaseModel): - name: Optional[str] = Field(default=None, example="New name") - surname: Optional[str] = Field(default=None, example="New surname") - {% if cookiecutter.add_elastic_search == "True" -%} - tags: Optional[str] = Field(default=None, example="tag1,tag2") - {% endif %} - -class DummyFiltersModel(BaseModel): - dummy_id: Optional[uuid.UUID] = Field(default=None) - name: Optional[str] = Field(default=None) - surname: Optional[str] = Field(default=None) diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/src/api/httpbin/__init__.py b/fastapi_template/template/{{cookiecutter.project_name}}/src/api/httpbin/__init__.py deleted file mode 100644 index 279f5aa..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/src/api/httpbin/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from src.api.httpbin.routes import URL_PREFIX, router - -__all__ = ['router', 'URL_PREFIX'] diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/src/api/httpbin/routes.py b/fastapi_template/template/{{cookiecutter.project_name}}/src/api/httpbin/routes.py deleted file mode 100644 index f7425c7..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/src/api/httpbin/routes.py +++ /dev/null @@ -1,17 +0,0 @@ -from fastapi import APIRouter - -from src.services.httpbin.client import http_bin_service -from src.services.httpbin.schema import DummyHttpBinObj, HTTPBinResponse - -router = APIRouter() -URL_PREFIX = "/http_bin" - - -@router.put("/", response_model=HTTPBinResponse) -async def create_dummy_obj(dummy_obj: DummyHttpBinObj) -> HTTPBinResponse: - return await http_bin_service.put_req(dummy_obj) - - -@router.delete("/{obj_id}", response_model=HTTPBinResponse) -async def delete_dummy_obj(obj_id: str) -> HTTPBinResponse: - return await http_bin_service.delete_req(obj_id) diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/src/api/httpbin/schema.py b/fastapi_template/template/{{cookiecutter.project_name}}/src/api/httpbin/schema.py deleted file mode 100644 index e69de29..0000000 diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/src/api/redis_api/__init__.py b/fastapi_template/template/{{cookiecutter.project_name}}/src/api/redis_api/__init__.py deleted file mode 100644 index a78ab98..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/src/api/redis_api/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from src.api.redis_api.routes import router, URL_PREFIX - -__all__ = ["router", 'URL_PREFIX'] diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/src/api/redis_api/routes.py b/fastapi_template/template/{{cookiecutter.project_name}}/src/api/redis_api/routes.py deleted file mode 100644 index 06a264e..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/src/api/redis_api/routes.py +++ /dev/null @@ -1,32 +0,0 @@ -from fastapi import APIRouter, HTTPException - -from src.api.redis_api.schema import RedisSetModel, RedisValueModel -from src.services.redis import redis - -router = APIRouter() -URL_PREFIX = "/redis" - - -@router.put("/set") -async def set_redis_value( - redis_target: RedisSetModel, -) -> None: - set_key = await redis.client.set(redis_target.key, redis_target.value, expire=redis_target.expire) - if not set_key: - raise HTTPException( - status_code=400, - detail="No, you can't", - ) - - -@router.get("/get", response_model=RedisValueModel) -async def get_redis_value( - key: str, -) -> RedisValueModel: - value = await redis.client.get(key, encoding='utf-8') - if not value: - raise HTTPException( - status_code=400, - detail="I can't get it", - ) - return RedisValueModel(value=value) diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/src/api/redis_api/schema.py b/fastapi_template/template/{{cookiecutter.project_name}}/src/api/redis_api/schema.py deleted file mode 100644 index 5940df5..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/src/api/redis_api/schema.py +++ /dev/null @@ -1,11 +0,0 @@ -from pydantic import BaseModel, Field - - -class RedisSetModel(BaseModel): - key: str - value: str - expire: int = Field(default=0) - - -class RedisValueModel(BaseModel): - value: str diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/src/exceptions.py b/fastapi_template/template/{{cookiecutter.project_name}}/src/exceptions.py deleted file mode 100644 index ec7a868..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/src/exceptions.py +++ /dev/null @@ -1,23 +0,0 @@ -from fastapi import HTTPException -from httpx import Response - - -class ServiceError(HTTPException): - """ - This exception can be returned from handler as is. - If this exception is raised you can skip handling. - - Your application will return normal response - with status_code=400 and human-readable message. - """ - def __init__(self, action: str, service_response: Response) -> None: - try: - error_data = service_response.json() - except LookupError: - error_data = service_response.text - self.detail = ( - f"Can't {action}. Getter respond: {error_data}" - f" (status code: {service_response.status_code})" - ) - self.response = service_response - self.status_code = 400 diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/src/models/__init__.py b/fastapi_template/template/{{cookiecutter.project_name}}/src/models/__init__.py deleted file mode 100644 index 932064b..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/src/models/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -{% if cookiecutter.add_dummy_model == "True" -%} -from src.models.dummy_db_model import DummyDBModel - -__all__ = ["DummyDBModel"] -{% endif %} \ No newline at end of file diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/src/models/dummy_db_model.py b/fastapi_template/template/{{cookiecutter.project_name}}/src/models/dummy_db_model.py deleted file mode 100644 index b49086f..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/src/models/dummy_db_model.py +++ /dev/null @@ -1,76 +0,0 @@ -import uuid -from typing import Optional - -from sqlalchemy import Column, String, sql -{% if cookiecutter.add_elastic_search == "True" -%} -from src.services.elastic import ElasticModelMixin -from src.services.elastic.schema import ESReturnModel -{% endif %} -from src.services.db import Base - -{% if cookiecutter.add_elastic_search == "True" -%} -class DummyElasticFilter(ESReturnModel): - name: str - surname: str -{% endif %} - -class DummyDBModel(Base{% if cookiecutter.add_elastic_search == "True" -%}, ElasticModelMixin[DummyElasticFilter]{% endif %}): - name = Column(String, nullable=False, index=True) - surname = Column(String, nullable=False, index=True) - {% if cookiecutter.add_elastic_search == "True" -%} - tags = Column(String, nullable=False, default="") - - __es_index_name = "dummy_elastic_index" - __es_search_fields = ["name", "surname"] - __es_search_type = DummyElasticFilter - {% endif %} - - @classmethod - def create( - cls, - *, - name: str, - surname: str, - {% if cookiecutter.add_elastic_search == "True" -%}tags: str = "",{% endif %} - ) -> sql.Insert: - return cls.insert_query( - name=name, - surname=surname, - {% if cookiecutter.add_elastic_search == "True" -%}tags=tags,{% endif %} - ) - - @classmethod - def delete(cls, dummy_id: uuid.UUID) -> sql.Delete: - return cls.delete_query().where(cls.id == dummy_id) - - @classmethod - def update(cls, - dummy_id: uuid.UUID, - *, - name: Optional[str] = None, - surname: Optional[str] = None, - {% if cookiecutter.add_elastic_search == "True" -%}tags: Optional[str] = None,{% endif %} - ) -> sql.Update: - new_values = {} - if name: - new_values[cls.name] = name - if surname: - new_values[cls.surname] = surname - if tags is not None: - new_values[cls.tags] = tags - return cls.update_query().where(cls.id == dummy_id).values(new_values) - - @classmethod - def filter(cls, *, - dummy_id: Optional[uuid.UUID] = None, - name: Optional[str] = None, - surname: Optional[str] = None - ) -> sql.Select: - query = cls.select_query() - if dummy_id: - query = query.where(cls.id == dummy_id) - if name: - query = query.where(cls.name == name) - if surname: - query = query.where(cls.surname == surname) - return query diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/src/server.py b/fastapi_template/template/{{cookiecutter.project_name}}/src/server.py deleted file mode 100644 index 316edfb..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/src/server.py +++ /dev/null @@ -1,70 +0,0 @@ -import sys -from typing import Any, Callable - -import alembic.config -from fastapi import FastAPI -from loguru import logger -from starlette.requests import Request -from starlette.responses import UJSONResponse - -from src.api import api_router -from src.services.db import db_engine -{% if cookiecutter.add_redis == "True" -%} -from src.services.redis import redis -{% endif %} -from src.settings import settings - -logger.configure( - handlers=[{ - "sink": sys.stderr, - "backtrace": False, - "diagnose": settings.is_dev, - "catch": False, - "colorize": settings.is_dev, - }] -) - -app = FastAPI( - title="{{cookiecutter.project_name}}", - description="{{cookiecutter.project_description}}", - default_response_class=UJSONResponse -) - -app.include_router(prefix="", router=api_router) - - -@app.on_event("startup") -async def startup() -> None: - {% if cookiecutter.add_redis == "True" -%} - await redis.create_pool() - {% endif %} - await db_engine.connect() - - -@app.on_event("shutdown") -async def shutdown() -> None: - {% if cookiecutter.add_redis == "True" -%} - await redis.shutdown() - {% endif %} - await db_engine.close() - - - -@app.middleware("http") -async def exception_logger(request: Request, call_next: Callable[..., Any]) -> Any: - """ - Just to log all exception with godlike logger from loguru. - """ - target_func = logger.catch()(call_next) - response = await target_func(request) - return response - - -if settings.is_dev: - alembic.config.main( - [ - "--raiseerr", - "upgrade", - "head", - ] - ) diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/src/services/__init__.py b/fastapi_template/template/{{cookiecutter.project_name}}/src/services/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/src/services/db/__init__.py b/fastapi_template/template/{{cookiecutter.project_name}}/src/services/db/__init__.py deleted file mode 100644 index 11f3cf1..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/src/services/db/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from src.services.db.base import Base -from src.services.db.db_meta import meta -from src.services.db.engine import db_engine, db_url - -__all__ = ["db_engine", "db_url", "meta", "Base"] diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/src/services/db/base.py b/fastapi_template/template/{{cookiecutter.project_name}}/src/services/db/base.py deleted file mode 100644 index 6584a3c..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/src/services/db/base.py +++ /dev/null @@ -1,105 +0,0 @@ -import uuid -from typing import Any, Dict, Optional, Tuple, Type, Union - -import sqlalchemy as sa -from sqlalchemy.dialects.postgresql import UUID -from sqlalchemy.ext.declarative import as_declarative, declared_attr -from sqlalchemy.orm.attributes import InstrumentedAttribute -from src.services.db.db_meta import meta - - -@as_declarative(metadata=meta) -class Base: - """ - Base class for all models - - It has some very cool methods which allows you - to ship autocompletion and type verification to SQLAlchemy models. - - >>> class Model(Base): - >>> name = sa.Column(sa.String()) - >>> - >>> @classmethod - >>> def get_by_name(cls, name: str) -> sa.sql.Select: - >>> return Model.select_query(cls.id).where(cls.name == name) - ... - >>> session.fetchall(Model.get_by_name("random_name")) - - `id`, `created_at` and `updated_at` columns are created - automatically for all models. - - Basic settings form models such as `__name__`, `__table__` and `__table_args__` - defined with types to allow mypy verify this data. - - `__tablename__` generated automatically based on class name. - """ - - __name__: str - __table__: sa.Table - __table_args__: Tuple[Any, ...] - - @declared_attr - def __tablename__(self) -> str: - return self.__name__.lower() - - @declared_attr - def id(self) -> Any: - return sa.Column( - UUID(as_uuid=True), - primary_key=True, - default=uuid.uuid4 - ) - - @declared_attr - def created_at(self) -> Any: - return sa.Column( - sa.DateTime(timezone=True), - server_default=sa.text("clock_timestamp()"), - nullable=False, - ) - - @declared_attr - def updated_at(self) -> Any: - return sa.Column( - sa.DateTime(timezone=True), - server_default=sa.text("clock_timestamp()"), - onupdate=sa.text("clock_timestamp()"), - nullable=False, - ) - - @classmethod - def get( - cls, pk: Union[uuid.UUID, str], *fields: InstrumentedAttribute - ) -> Optional[Any]: - return cls.select_query(*fields).where(cls.id == pk) - - @classmethod - def exists(cls, pk: Union[uuid.UUID, str]) -> sa.sql.expression.Exists: - return sa.exists().where(cls.id == pk) - - @classmethod - def delete(cls, pk: uuid.UUID) -> sa.sql.Delete: - return cls.delete_query().where(cls.id == pk) - - @classmethod - def select_query( - cls, - *columns: Union[InstrumentedAttribute, Type["Base"]], - use_labels: bool = False, - ) -> sa.sql.Select: - return sa.select(columns or [cls], use_labels=use_labels) - - @classmethod - def insert_query(cls, **values: Any) -> sa.sql.Insert: - return cls.__table__.insert().values(**values) - - @classmethod - def update_query(cls) -> sa.sql.Update: - return cls.__table__.update() - - @classmethod - def delete_query(cls) -> sa.sql.Delete: - return cls.__table__.delete() - - def as_dict(self) -> Dict[str, Any]: - return {c.name: getattr(self, c.key) for c in self.__table__.columns} diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/src/services/db/db_meta.py b/fastapi_template/template/{{cookiecutter.project_name}}/src/services/db/db_meta.py deleted file mode 100644 index 5d24aa1..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/src/services/db/db_meta.py +++ /dev/null @@ -1,3 +0,0 @@ -from sqlalchemy import MetaData - -meta = MetaData() diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/src/services/db/engine.py b/fastapi_template/template/{{cookiecutter.project_name}}/src/services/db/engine.py deleted file mode 100644 index 17c9b02..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/src/services/db/engine.py +++ /dev/null @@ -1,41 +0,0 @@ -from typing import Optional - -from aiopg.sa import Engine, create_engine -from sqlalchemy.engine.url import URL, make_url - -from src.settings import settings - -db_url = make_url( - str(URL( - drivername=settings.db_driver, - username=settings.postgres_user, - password=settings.postgres_password, - host=settings.postgres_host, - port=settings.postgres_port, - database=settings.postgres_db, - )) -) - - -class DBEngine: - def __init__(self, connection_url: str) -> None: - self.dsn = connection_url - self.engine: Optional[Engine] = None - - async def connect(self) -> None: - self.engine = await create_engine(dsn=self.dsn) - - @property - def client(self) -> Engine: - if self.engine is None: - raise ValueError("Not connected to database") - return self.engine - - async def close(self) -> None: - if self.engine: - self.engine.close() - await self.engine.wait_closed() - raise Exception("Not connected to database") - - -db_engine = DBEngine(str(db_url)) diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/src/services/db/session.py b/fastapi_template/template/{{cookiecutter.project_name}}/src/services/db/session.py deleted file mode 100644 index 45b9fbe..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/src/services/db/session.py +++ /dev/null @@ -1,61 +0,0 @@ -from typing import Any, List, AsyncGenerator - -from aiopg.sa import SAConnection - -from src.services.db import db_engine - - -class Session: - """ - Database session object. - It used to acquire connection from pool - and execute many queries using one connection. - """ - def __init__(self, connection: SAConnection): - self.connection = connection - - async def execute(self, query: Any) -> Any: - """ - Simply execute SQLAlchemy generated query - or string query and return raw results. - """ - return await self.connection.execute(query) - - async def fetchone(self, query: Any) -> Any: - """ - Get one object from database by query. - """ - cursor = await self.connection.execute(query) - return await cursor.fetchone() - - async def scalar(self, query: Any) -> Any: - """ - Scalar returns first column of the first result. - It's convenient fot such things as getting function result. - - >>> session = Session(connection) - >>> session.scalar(Model.insert_query(**values).returning(Model.id)) - UUID('7a9ffbfe-f871-42fb-a371-7bd29227a9ff') - """ - result = await self.fetchone(query) - return result[0] - - async def fetchall(self, query: Any) -> List[Any]: - """ - Get all matching objects from database by query. - """ - cursor = await self.connection.execute(query) - return await cursor.fetchall() - - -async def db_session() -> AsyncGenerator[Session, None]: - """ - Dependency to acquire connection from database pool - and close it when handler function is proceeds. - """ - connection = await db_engine.client.acquire() - session = Session(connection) - try: - yield session - finally: - await connection.close() diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/src/services/elastic/__init__.py b/fastapi_template/template/{{cookiecutter.project_name}}/src/services/elastic/__init__.py deleted file mode 100644 index 9c2b1be..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/src/services/elastic/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from src.services.elastic.mixin import ElasticModelMixin - -__all__ = [ - 'ElasticModelMixin', - 'schema' -] diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/src/services/elastic/client.py b/fastapi_template/template/{{cookiecutter.project_name}}/src/services/elastic/client.py deleted file mode 100644 index 8f89602..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/src/services/elastic/client.py +++ /dev/null @@ -1,4 +0,0 @@ -from elasticsearch import AsyncElasticsearch -from src.settings import settings - -elastic_client = AsyncElasticsearch(hosts=[settings.elastic_host]) diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/src/services/elastic/mixin.py b/fastapi_template/template/{{cookiecutter.project_name}}/src/services/elastic/mixin.py deleted file mode 100644 index 0b217f9..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/src/services/elastic/mixin.py +++ /dev/null @@ -1,115 +0,0 @@ -import uuid -from typing import TypeVar, List, Optional, Any, Generic, Union, Dict - -from elasticsearch import NotFoundError -from elasticsearch_dsl import Search -from elasticsearch_dsl.query import MultiMatch -from loguru import logger - -from src.services.elastic.client import elastic_client -from src.services.elastic.schema import ESReturnModel - -SearchModel = TypeVar("SearchModel", covariant=True, bound=ESReturnModel) - - -class ElasticModelMixin(Generic[SearchModel]): - """ - Mixin to simplify ElasticSearch integration with SQLAlchemy models. - - It has three main variables to override in model. - - 1) __es_index_name - it's a name of the index in ElasticSearch where - data for model would be stored. - 2) __es_search_fields - the list of fields use in search query. - 3) __es_search_type - covariant of `SearchModel` to return from `elastic_filter`. - If you won't define it `elastic_filter` will return dict. - """ - __es_index_name: str = "default_index" - __es_search_fields: List[str] = [] - __es_search_type: Optional[SearchModel] = None - - @classmethod - async def elastic_add( - cls, model_id: uuid.UUID, *, tags: Optional[str] = "", **kwargs: Any - ) -> None: - """ - Add object to elastic index. - - >>> await Model.elastic_index(uuid.uuid4(), name="La", surname="Tex") - """ - ret = await elastic_client.index( - index=cls.__es_index_name, - body={"tags": tags.split(",") if tags else [], **kwargs}, - id=str(model_id), - ) - result = ret["result"] - logger.debug(f"{result} '{cls.__name__}:{model_id}' in elastic") - - @classmethod - async def elastic_filter( - cls, *, query: str, offset: int, limit: int - ) -> List[Union[SearchModel, Dict[str, Any]]]: - """ - Filter existing models in ElasticSearch by string query. - This function uses __es_search_fields as fields for phrase_prefix query. - - >>> await Model.elastic_filter(query="La") - """ - elastic_query = Search() - if query: - elastic_query = elastic_query.query( - MultiMatch( - type="phrase_prefix", query=query, fields=cls.__es_search_fields - ) - ) - elastic_query = elastic_query[offset : offset + limit] - search_res = await elastic_client.search(elastic_query.to_dict()) - hits = search_res.get("hits", {}).get("hits", []) - results = [] - constructor = cls.__es_search_type or dict - for hit in hits: - logger.debug(hit) - results.append(constructor(id=hit.get("_id"), **hit.get("_source", {}))) - return results - - @classmethod - async def elastic_update( - cls, - model_id: uuid.UUID, - **body: Any, - ) -> None: - """ - Update model in ElasticSearch by id. - """ - tags = body.get("tags", None) - if tags is not None: - body["tags"] = tags.split(",") if tags else [] - logger.debug(f"Updating '{cls.__name__}:{model_id}' in ES") - try: - await elastic_client.update( - index=cls.__es_index_name, - id=str(model_id), - body={"doc": body}, - refresh=True, - ) - except NotFoundError: - logger.debug(f"Can't update unknown {cls.__name__} in es") - - @classmethod - async def elastic_delete(cls, model_id: uuid.UUID) -> None: - """ - Delete object from ElasticSearch index. - """ - try: - await elastic_client.delete(index=cls.__es_index_name, id=str(model_id)) - except NotFoundError: - logger.debug(f"'{cls.__name__}:{model_id}' was not found in ES") - - @classmethod - async def elastic_create_index(cls) -> None: - """ - Create index if it's not exists. - """ - if not await elastic_client.indices.exists(cls.__es_index_name): - logger.debug(f"Creating elastic index {cls.__es_index_name}") - await elastic_client.indices.create(cls.__es_index_name) diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/src/services/elastic/schema.py b/fastapi_template/template/{{cookiecutter.project_name}}/src/services/elastic/schema.py deleted file mode 100644 index 1b70c8d..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/src/services/elastic/schema.py +++ /dev/null @@ -1,15 +0,0 @@ -import uuid -from typing import Optional, List - -from pydantic import Field, BaseModel - - -class ESReturnModel(BaseModel): - id: uuid.UUID - tags: Optional[List[str]] - - -class ElasticFilterModel(BaseModel): - query: str = Field(default="") - limit: int = Field(default=100, lt=400) - offset: int = Field(default=0) diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/src/services/httpbin/__init__.py b/fastapi_template/template/{{cookiecutter.project_name}}/src/services/httpbin/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/src/services/httpbin/client.py b/fastapi_template/template/{{cookiecutter.project_name}}/src/services/httpbin/client.py deleted file mode 100644 index 6676d83..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/src/services/httpbin/client.py +++ /dev/null @@ -1,60 +0,0 @@ -from httpx import AsyncClient, Response -from loguru import logger - -from src.exceptions import ServiceError -from src.services.httpbin.schema import DummyHttpBinObj, HTTPBinResponse -from src.settings import settings - - -class HttpBinService: - def __init__(self, http_bin_host: str) -> None: - self.host = http_bin_host - - @property - def client(self) -> AsyncClient: - """ - Generate a client with base_url - :return: async http client - """ - return AsyncClient( - base_url=self.host - ) - - @classmethod - def catch_response(cls, response: Response, action: str, *codes: int) -> None: - logger.debug("Response got") - logger.debug(response.url) - logger.debug(response.status_code) - logger.debug(response.headers) - logger.debug(response.text) - if response.status_code not in codes: - raise ServiceError(action, response) - - async def put_req(self, dummy_obj: DummyHttpBinObj) -> HTTPBinResponse: - """ - Create dummy object in HTTPBin API. - :param dummy_obj: target to create - :return: None if success, raise an error if status code is not 201 - """ - async with self.client as http: - response = await http.put("/put", json=dummy_obj.dict()) - self.catch_response(response, "Create dummy object", 200) - return HTTPBinResponse(**response.json()) - - async def delete_req(self, dummy_id: str) -> HTTPBinResponse: - """ - Delete dummy_obj from HTTPBin API. - :param dummy_id: target to delete - :return: None if success, raise an error if status code is not 200 - """ - async with self.client as http: - response = await http.delete("/delete", params={ - "dummy_id": dummy_id - }) - self.catch_response(response, "delete bucket", 200) - return HTTPBinResponse(**response.json()) - - -http_bin_service = HttpBinService( - settings.httpbin_host -) diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/src/services/httpbin/schema.py b/fastapi_template/template/{{cookiecutter.project_name}}/src/services/httpbin/schema.py deleted file mode 100644 index 0bbe197..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/src/services/httpbin/schema.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import Any, Dict, Optional - -from pydantic import BaseModel, Field - - -class DummyHttpBinObj(BaseModel): - id: int - name: str - surname: str - - -class HTTPBinResponse(BaseModel): - args: Dict[str, Any] - data: str - files: Dict[str, Any] - form: Dict[str, str] - headers: Dict[str, str] - json_data: Optional[Dict[str, Any]] = Field(alias="json") - origin: str - url: str diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/src/services/redis.py b/fastapi_template/template/{{cookiecutter.project_name}}/src/services/redis.py deleted file mode 100644 index 33dfd60..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/src/services/redis.py +++ /dev/null @@ -1,50 +0,0 @@ -from typing import Optional, Tuple, Union - -import aioredis -from aioredis import Redis -from loguru import logger -from src.settings import settings - - -class RedisClient: - """ - Super simple redis client. - """ - def __init__(self, address: Union[str, Tuple[str, int]], password: str): - self.address = address - self.password = password - self._client: Optional[Redis] = None - - async def create_pool(self) -> None: - logger.debug("Creating redis pool") - self._client = await aioredis.create_redis_pool( - self.address, password=self.password - ) - logger.debug("Redis pool created") - - @property - def client(self) -> Redis: - """ - Get actual Redis client. - :return: - """ - if self._client is None: - raise Exception("Not connected to redis") - return self._client - - async def shutdown(self) -> None: - """ - Shutdown session for redis. - Close redis pool. - """ - logger.debug("Shutting down redis") - if self._client is None: - return - self._client.close() - await self._client.wait_closed() - logger.debug("Redis connection pool closed") - - -redis = RedisClient( - address=(settings.redis_host, settings.redis_port), password=settings.redis_password -) diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/src/settings.py b/fastapi_template/template/{{cookiecutter.project_name}}/src/settings.py deleted file mode 100644 index 73121a3..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/src/settings.py +++ /dev/null @@ -1,44 +0,0 @@ -from enum import Enum - -from pydantic import BaseSettings, Field - - -class AppMode(Enum): - DEV = "development" - PROD = "production" - - -class Settings(BaseSettings): - # Application settings - fastapi_env: AppMode = Field(default=AppMode.PROD) - log_level: str = Field(default="INFO") - # Auxiliary database settings - db_echo: bool = Field(default=False) - db_driver: str = Field(default="postgresql") - # Postgres connection settings - postgres_db: str = Field(...) - postgres_host: str = Field(...) - postgres_port: str = Field(...) - postgres_user: str = Field(...) - postgres_password: str = Field(...) - {% if cookiecutter.add_redis == "True" -%} - # Redis connection settings - redis_host: str = Field(...) - redis_port: int = Field(default=6379) - redis_password: str = Field(...) - {% endif %} - {% if cookiecutter.add_scheduler == "True" -%} - schedule_timer: int = Field(default=20) - {% endif %} - # httpbin client settings - httpbin_host: str = Field(default="https://httpbin.org/") - {% if cookiecutter.add_elastic_search == "True" -%} - elastic_host: str = Field(...) - {% endif %} - - @property - def is_dev(self) -> bool: - return self.fastapi_env == AppMode.DEV - - -settings = Settings() \ No newline at end of file diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/src/utils.py b/fastapi_template/template/{{cookiecutter.project_name}}/src/utils.py deleted file mode 100644 index e69de29..0000000 diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/systemd/{{ cookiecutter.project_name }}_scheduler.service b/fastapi_template/template/{{cookiecutter.project_name}}/systemd/{{ cookiecutter.project_name }}_scheduler.service deleted file mode 100644 index 5da9aa8..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/systemd/{{ cookiecutter.project_name }}_scheduler.service +++ /dev/null @@ -1,15 +0,0 @@ -[Unit] -Description={{cookiecutter.project_description}} -After=network.target - -[Service] -Type=simple -User=gitlab-runner -EnvironmentFile=ENV_FILE -WorkingDirectory=DIR -ExecStart=/bin/bash -c 'source /opt/rh/rh-python38/enable && poetry install --no-dev && poetry run python scheduler.py' -ExecStop=/usr/bin/kill -15 $MAINPID -Restart=on-abort - -[Install] -WantedBy=multi-user.target diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/systemd/{{ cookiecutter.project_name }}_service.service b/fastapi_template/template/{{cookiecutter.project_name}}/systemd/{{ cookiecutter.project_name }}_service.service deleted file mode 100644 index 9b624ec..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/systemd/{{ cookiecutter.project_name }}_service.service +++ /dev/null @@ -1,15 +0,0 @@ -[Unit] -Description={{cookiecutter.project_description}} -After=network.target - -[Service] -Type=simple -User=gitlab-runner -EnvironmentFile=ENV_FILE -WorkingDirectory=DIR -ExecStart=/bin/bash -c 'source /opt/rh/rh-python38/enable && poetry install --no-dev && poetry run uvicorn --workers=4 --access-log --log-level debug --host 0.0.0.0 --port 8100 src.server:app' -ExecStop=/usr/bin/kill -15 $MAINPID -Restart=on-abort - -[Install] -WantedBy=multi-user.target diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/tests/__init__.py b/fastapi_template/template/{{cookiecutter.project_name}}/tests/__init__.py deleted file mode 100644 index 9982356..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/tests/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -from typing import Any, Dict, Optional - -from pydantic import Field -from pydantic.main import BaseModel - - -class TestSettings(BaseModel): - __test__ = False - - request_data: Dict[str, Any] - response_code: Optional[int] - response_json: Optional[Dict[Any, Any]] = Field(None) - validation_data: Optional[Dict[Any, Any]] = Field(None) diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/tests/conftest.py b/fastapi_template/template/{{cookiecutter.project_name}}/tests/conftest.py deleted file mode 100644 index 7c2596e..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/tests/conftest.py +++ /dev/null @@ -1,93 +0,0 @@ -import warnings -from typing import Generator - -import alembic.config -import pytest -from fastapi.testclient import TestClient -from sqlalchemy import create_engine -from sqlalchemy.engine import Connection, Engine -from sqlalchemy.engine.url import URL, make_url -from sqlalchemy.exc import ProgrammingError - -from src.services.db import db_url -from src.settings import settings - -warnings.filterwarnings("ignore", category=DeprecationWarning) - - -def get_engine() -> Engine: - pg_db_url = make_url( - str(URL( - drivername=settings.db_driver, - username=settings.postgres_user, - password=settings.postgres_password, - host=settings.postgres_host, - port=settings.postgres_port, - database="postgres", - )) - ) - engine = create_engine(str(pg_db_url)) - return engine - - -def run_psql_without_transaction(command: str) -> None: - engine = get_engine() - connection = engine.connect() - connection.connection.set_isolation_level(0) - connection.execute(command) - connection.connection.set_isolation_level(1) - connection.close() - - -@pytest.fixture(scope="session") -def create_database() -> None: - try: - run_psql_without_transaction(f"CREATE DATABASE {settings.postgres_db}") - except ProgrammingError: - pass - - -def prepare_db() -> None: - alembic.config.main( - [ - "--raiseerr", - "upgrade", - "head", - ] - ) - - -def drop_db() -> None: - alembic.config.main( - [ - "--raiseerr", - "downgrade", - "base", - ] - ) - - -@pytest.fixture(scope="function") -def create_db(create_database: None) -> Generator[None, None, None]: - prepare_db() - yield - drop_db() - - -@pytest.fixture(scope="function") -def app_fixture(create_db: None) -> TestClient: - from src.server import app - - api_client = TestClient(app) - - return api_client - - -@pytest.fixture(scope="function") -def pg_conn() -> Generator[Connection, None, None]: - engine = create_engine(str(db_url), pool_size=0, echo=True) - conn = engine.connect() - - yield conn - - conn.close() diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/tests/dummy_db_test.py b/fastapi_template/template/{{cookiecutter.project_name}}/tests/dummy_db_test.py deleted file mode 100644 index 89bb371..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/tests/dummy_db_test.py +++ /dev/null @@ -1,39 +0,0 @@ -import pytest -import sqlalchemy as sa -from sqlalchemy.engine import Connection -from starlette.testclient import TestClient - -from src.models import DummyDBModel -from tests import TestSettings - -test_data = [ - TestSettings( - request_data=dict( - dummy_id="b4439fc8-8fde-4079-ab2c-d61e46e22f25", - json=dict( - name="test dummy object", - surname="Cesar" - ), - ), - ), -] - - -@pytest.mark.parametrize("test_conf", test_data) -def test_delete_dummy_obj( - pg_conn: Connection, test_conf: TestSettings, app_fixture: TestClient -) -> None: - with app_fixture as client: - put_result = client.put( - "/dummy_db_obj/", - json=test_conf.request_data["json"], - ) - assert put_result.status_code == 200 - with pg_conn.begin(): - result = pg_conn.execute(DummyDBModel.select_query(DummyDBModel.id)).first() - assert len(result) == 1 - result = client.delete(f"/dummy_db_obj/{result[0]}") - assert result.status_code == 200 - with pg_conn.begin(): - result = pg_conn.execute(sa.func.count(DummyDBModel.id)).first() - assert result[0] == 0 diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/__init__.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/__init__.py new file mode 100644 index 0000000..8b08592 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/__init__.py @@ -0,0 +1 @@ +"""{{cookiecutter.project_name}} package.""" diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/__main__.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/__main__.py new file mode 100644 index 0000000..cdb27c1 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/__main__.py @@ -0,0 +1,19 @@ +import uvicorn + +from {{cookiecutter.project_name}}.settings import settings + + +def main() -> None: + """Entrypoint of the application.""" + uvicorn.run( + "{{cookiecutter.project_name}}.web.application:get_app", + workers=settings.workers_count, + host=settings.host, + port=settings.port, + reload=settings.reload, + factory=True, + ) + + +if __name__ == "__main__": + main() diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/conftest.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/conftest.py new file mode 100644 index 0000000..86b6683 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/conftest.py @@ -0,0 +1,54 @@ +import asyncio +import sys +from typing import Any, Generator + +import pytest + +from {{cookiecutter.project_name}}.settings import settings + + +@pytest.fixture(scope="session") +def event_loop() -> Generator[asyncio.AbstractEventLoop, None, None]: + """ + Create an instance of event loop for tests. + + This hack is required in order to get `dbsession` fixture to work. + Because default fixture `event_loop` is function scoped, + but dbsession requires session scoped `event_loop` fixture. + + :yields: event loop. + """ + python_version = sys.version_info[:2] + if sys.platform.startswith("win") and python_version >= (3, 8): + # Avoid "RuntimeError: Event loop is closed" on Windows when tearing down tests + # https://github.com/encode/httpx/issues/914 + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + + loop = asyncio.new_event_loop() + yield loop + loop.close() + + +@pytest.fixture(scope="session") +def database_url() -> str: + """ + Возвращает ÑÑылку на теÑтовю базу данных. + + :return: URL. + """ + return str(settings.db_url) + + +@pytest.fixture(scope="session") +def init_database() -> Any: + """ + Create a database for tests. + + :return: ничего + """ + from {{cookiecutter.project_name}}.db.meta import meta # noqa: WPS433 + from {{cookiecutter.project_name}}.db.models import load_all_models # noqa: WPS433 + + load_all_models() + + return meta.create_all diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/base.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/base.py new file mode 100644 index 0000000..3689d8e --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/base.py @@ -0,0 +1,20 @@ +from typing import Any, Tuple + +from sqlalchemy import Table +from sqlalchemy.orm import as_declarative + +from {{cookiecutter.project_name}}.db.meta import meta + + +@as_declarative(metadata=meta) +class Base: + """ + Base for all models. + + It has some type definitions to + enhance autocompletion. + """ + + __tablename__: str + __table__: Table + __table_args__: Tuple[Any, ...] diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/dao/__init__.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/dao/__init__.py new file mode 100644 index 0000000..db62a0a --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/dao/__init__.py @@ -0,0 +1 @@ +"""DAO classes.""" diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/dao/dummy_dao.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/dao/dummy_dao.py new file mode 100644 index 0000000..2180ffd --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/dao/dummy_dao.py @@ -0,0 +1,35 @@ +from fastapi import Depends +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncScalarResult, AsyncSession + +from {{cookiecutter.project_name}}.db.dependencies import get_db_session +from {{cookiecutter.project_name}}.db.models.dummy_model import DummyModel + + +class DummyDAO: + """Class for accessing dummy table.""" + + def __init__(self, session: AsyncSession = Depends(get_db_session)): + self.session = session + + async def create_dummy_model(self, name: str) -> None: + """ + Add single dummy to session. + + :param name: name of a dummy. + """ + self.session.add(DummyModel(name=name)) + + async def get_all_dummies(self, limit: int, offset: int) -> AsyncScalarResult: + """ + Get all dummy models with limit/offset pagination. + + :param limit: limit of dummies. + :param offset: offset of dummies. + :return: stream of dummies. + """ + raw_stream = await self.session.stream( + select(DummyModel).limit(limit).offset(offset), + ) + + return raw_stream.scalars() diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/dependencies.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/dependencies.py new file mode 100644 index 0000000..9e511d7 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/dependencies.py @@ -0,0 +1,19 @@ +from typing import AsyncGenerator + +from sqlalchemy.ext.asyncio import AsyncSession +from starlette.requests import Request + + +async def get_db_session(request: Request) -> AsyncGenerator[AsyncSession, None]: + """ + Create and get database session. + + :param request: current request. + :yield: database session. + """ + session: AsyncSession = request.app.state.db_session_factory() + + yield session + + await session.commit() + await session.close() diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/meta.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/meta.py new file mode 100644 index 0000000..4c95254 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/meta.py @@ -0,0 +1,3 @@ +import sqlalchemy as sa + +meta = sa.MetaData() diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/migrations/__init__.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/migrations/README.md similarity index 100% rename from fastapi_template/template/{{cookiecutter.project_name}}/migrations/__init__.py rename to fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/migrations/README.md diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/migrations/__init__.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/migrations/__init__.py new file mode 100644 index 0000000..6dccb95 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/migrations/__init__.py @@ -0,0 +1 @@ +"""Alembic migraions.""" diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/migrations/env.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/migrations/env.py new file mode 100644 index 0000000..6507aa0 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/migrations/env.py @@ -0,0 +1,85 @@ +import asyncio +from logging.config import fileConfig + +from alembic import context +from sqlalchemy.ext.asyncio.engine import create_async_engine +from sqlalchemy.future import Connection + +from {{cookiecutter.project_name}}.db.meta import meta +from {{cookiecutter.project_name}}.db.models import load_all_models +from {{cookiecutter.project_name}}.settings import settings + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + + +load_all_models() +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = meta + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + context.configure( + url=str(settings.db_url), + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def do_run_migrations(connection: Connection) -> None: + """ + Run actual sync migrations. + + :param connection: connection to the database. + """ + context.configure(connection=connection, target_metadata=target_metadata) + + with context.begin_transaction(): + context.run_migrations() + + +async def run_migrations_online() -> None: + """ + Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + """ + connectable = create_async_engine(str(settings.db_url)) + + async with connectable.connect() as connection: + await connection.run_sync(do_run_migrations) + + +if context.is_offline_mode(): + run_migrations_offline() +else: + asyncio.run(run_migrations_online()) diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/migrations/script.py.mako b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/migrations/script.py.mako similarity index 90% rename from fastapi_template/template/{{cookiecutter.project_name}}/migrations/script.py.mako rename to fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/migrations/script.py.mako index 2c01563..55df286 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/migrations/script.py.mako +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/migrations/script.py.mako @@ -16,9 +16,9 @@ branch_labels = ${repr(branch_labels)} depends_on = ${repr(depends_on)} -def upgrade(): +def upgrade() -> None: ${upgrades if upgrades else "pass"} -def downgrade(): +def downgrade() -> None: ${downgrades if downgrades else "pass"} diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/migrations/versions/2021-08-16-16-53_819cbf6e030b.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/migrations/versions/2021-08-16-16-53_819cbf6e030b.py new file mode 100644 index 0000000..3b22742 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/migrations/versions/2021-08-16-16-53_819cbf6e030b.py @@ -0,0 +1,22 @@ +"""Initial migration. + +Revision ID: 819cbf6e030b +Revises: +Create Date: 2021-08-16 16:53:05.484024 + +""" + + +# revision identifiers, used by Alembic. +revision = "819cbf6e030b" +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade() -> None: + pass + + +def downgrade() -> None: + pass diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/migrations/versions/2021-08-16-16-55_2b7380507a71.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/migrations/versions/2021-08-16-16-55_2b7380507a71.py new file mode 100644 index 0000000..53cfe12 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/migrations/versions/2021-08-16-16-55_2b7380507a71.py @@ -0,0 +1,32 @@ +"""Created Dummy Model. + +Revision ID: 2b7380507a71 +Revises: 819cbf6e030b +Create Date: 2021-08-16 16:55:25.157309 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "2b7380507a71" +down_revision = "819cbf6e030b" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "dummy_model", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("name", sa.String(length=200), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("dummy_model") + # ### end Alembic commands ### diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/src/__init__.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/migrations/versions/__init__.py similarity index 100% rename from fastapi_template/template/{{cookiecutter.project_name}}/src/__init__.py rename to fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/migrations/versions/__init__.py diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/models/__init__.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/models/__init__.py new file mode 100644 index 0000000..490649f --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/models/__init__.py @@ -0,0 +1,14 @@ +"""{{cookiecutter.project_name}} models.""" +import pkgutil +from pathlib import Path + + +def load_all_models() -> None: + """Load all models from this folder.""" + package_dir = Path(__file__).resolve().parent + modules = pkgutil.walk_packages( + path=[str(package_dir)], + prefix="{{cookiecutter.project_name}}.db.models.", + ) + for module in modules: + __import__(module.name) # noqa: WPS421 diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/models/dummy_model.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/models/dummy_model.py new file mode 100644 index 0000000..15d56f5 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/models/dummy_model.py @@ -0,0 +1,13 @@ +from sqlalchemy.sql.schema import Column +from sqlalchemy.sql.sqltypes import Integer, String + +from {{cookiecutter.project_name}}.db.base import Base + + +class DummyModel(Base): + """Model for demo purpose.""" + + __tablename__ = "dummy_model" + + id = Column(Integer(), primary_key=True, autoincrement=True) + name = Column(String(length=200)) # noqa: WPS432 diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/services/redis/dependency.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/services/redis/dependency.py new file mode 100644 index 0000000..986d336 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/services/redis/dependency.py @@ -0,0 +1,20 @@ +from typing import AsyncGenerator + +from aioredis import Redis +from starlette.requests import Request + + +async def get_redis_connection(request: Request) -> AsyncGenerator[Redis, None]: + """ + Get redis client. + + This dependency aquires connection from pool. + + :param request: current request. + :yield: redis client. + """ + redis_client = Redis(connection_pool=request.app.state.redis_pool) + + yield redis_client + + await redis_client.close() diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/settings.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/settings.py new file mode 100644 index 0000000..03e2368 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/settings.py @@ -0,0 +1,94 @@ +from pathlib import Path +from tempfile import gettempdir +from typing import Optional + +from pydantic import BaseSettings +from yarl import URL + +TEMP_DIR = Path(gettempdir()) + + +class Settings(BaseSettings): + """Application settings.""" + + host: str = "127.0.0.1" + port: int = 8000 + # quantity of workers for uvicorn + workers_count: int = 1 + # Enable uvicorn reloading + reload: bool = False + + {%- if cookiecutter.db_info.name != "none" %} + {%- if cookiecutter.db_info.name == "sqlite" %} + db_file: Path = TEMP_DIR / "db.sqlite3" + {% else %} + db_host: str = "{{cookiecutter.project_name}}-db" + db_port: int = {{cookiecutter.db_info.port}} + db_user: str = "{{cookiecutter.project_name}}" + db_pass: str = "{{cookiecutter.project_name}}" + db_base: str = "{{cookiecutter.project_name}}" + db_echo: bool = False + {%- endif %} + {%- endif %} + + {%- if cookiecutter.enable_redis == "True" %} + redis_host: str = "{{cookiecutter.project_name}}-redis" + redis_port: int = 6379 + redis_user: Optional[str] = None + redis_pass: Optional[str] = None + redis_base: Optional[int] = None + {% endif %} + + {%- if cookiecutter.db_info.name != "none" %} + @property + def db_url(self) -> URL: + """ + Assemble database URL from settings. + + :return: database URL. + """ + {%- if cookiecutter.db_info.name == "sqlite" %} + return URL.build( + scheme="{{cookiecutter.db_info.driver}}", + path=f"///{self.db_file}" + ) + {% else %} + return URL.build( + scheme="{{cookiecutter.db_info.driver}}", + host=self.db_host, + port=self.db_port, + user=self.db_user, + password=self.db_pass, + path=f"/{self.db_base}", + ) + {%- endif %} + {%- endif %} + + {%- if cookiecutter.enable_redis == "True" %} + @property + def redis_url(self) -> URL: + """ + Assemble REDIS URL from settings. + + :return: redis URL. + """ + path = "" + if self.redis_base is not None: + path = f"/{self.redis_base}" + return URL.build( + scheme="redis", + host=self.redis_host, + port=self.redis_port, + user=self.redis_user, + password=self.redis_pass, + path=path, + ) + {%- endif %} + + class Config: + env_file = ".env" + env_prefix = "{{cookiecutter.project_name | upper }}_" + env_file_encoding = "utf-8" + + +settings = Settings() diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/tests/__init__.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/tests/__init__.py new file mode 100644 index 0000000..36120ba --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for {{cookiecutter.project_name}}.""" diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/tests/test_fastapitestproject.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/tests/test_fastapitestproject.py new file mode 100644 index 0000000..23a09e9 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/tests/test_fastapitestproject.py @@ -0,0 +1,8 @@ +def test_stub() -> None: + """ + Test stub. + + It must be removed in real applications. + """ + test_val = 3 + assert test_val == 3 diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/__init__.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/__init__.py new file mode 100644 index 0000000..35232ba --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/__init__.py @@ -0,0 +1 @@ +"""{{cookiecutter.project_name}} API package.""" diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/dummy/__init__.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/dummy/__init__.py new file mode 100644 index 0000000..5b22f56 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/dummy/__init__.py @@ -0,0 +1,4 @@ +"""Dummy model API.""" +from {{cookiecutter.project_name}}.web.api.dummy.views import router + +__all__ = ['router'] diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/dummy/schema.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/dummy/schema.py new file mode 100644 index 0000000..129518d --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/dummy/schema.py @@ -0,0 +1,21 @@ +from pydantic import BaseModel + + +class DummyModelDTO(BaseModel): + """ + DTO for dummy models. + + It returned when accessing dummy models from the API. + """ + + id: int + name: str + + class Config: + orm_mode = True + + +class DummyModelInputDTO(BaseModel): + """DTO for creating new dummy model.""" + + name: str diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/dummy/views.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/dummy/views.py new file mode 100644 index 0000000..955c9c6 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/dummy/views.py @@ -0,0 +1,41 @@ +from typing import Any, List + +from fastapi import APIRouter +from fastapi.param_functions import Depends + +from {{cookiecutter.project_name}}.db.dao.dummy_dao import DummyDAO +from {{cookiecutter.project_name}}.web.api.dummy.schema import DummyModelDTO, DummyModelInputDTO + +router = APIRouter() + + +@router.get("/", response_model=List[DummyModelDTO]) +async def get_dummy_models( + limit: int = 10, + offset: int = 0, + dummy_dao: DummyDAO = Depends(), +) -> Any: + """ + Retrieve all dummy objects from database. + + :param limit: limit of dummy objects, defaults to 10. + :param offset: offset of dummy objects, defaults to 0. + :param dummy_dao: DAO for dummy models. + :return: list of dummy obbjects from database. + """ + dummies = await dummy_dao.get_all_dummies(limit=limit, offset=offset) + return await dummies.fetchall() + + +@router.put("/") +async def create_dummy_model( + new_dummy_object: DummyModelInputDTO, + dummy_dao: DummyDAO = Depends(), +) -> None: + """ + Create dummy model in database. + + :param new_dummy_object: new dummy model item. + :param dummy_dao: DAO for dummy models. + """ + await dummy_dao.create_dummy_model(**new_dummy_object.dict()) diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/echo/__init__.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/echo/__init__.py new file mode 100644 index 0000000..06767af --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/echo/__init__.py @@ -0,0 +1,4 @@ +"""Echo API.""" +from {{cookiecutter.project_name}}.web.api.echo.views import router + +__all__ = ['router'] diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/echo/schema.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/echo/schema.py new file mode 100644 index 0000000..f408bc2 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/echo/schema.py @@ -0,0 +1,7 @@ +from pydantic import BaseModel + + +class Message(BaseModel): + """Simple message model.""" + + message: str diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/echo/views.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/echo/views.py new file mode 100644 index 0000000..52d90c6 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/echo/views.py @@ -0,0 +1,18 @@ +from fastapi import APIRouter + +from {{cookiecutter.project_name}}.web.api.echo.schema import Message + +router = APIRouter() + + +@router.post("/", response_model=Message) +async def send_echo_message( + incoming_message: Message, +) -> Message: + """ + Send echo back to user. + + :param incoming_message: incoming message. + :returns: message same as the incoming. + """ + return incoming_message diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/redis/__init__.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/redis/__init__.py new file mode 100644 index 0000000..6c1178b --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/redis/__init__.py @@ -0,0 +1,4 @@ +"""Redis API.""" +from {{cookiecutter.project_name}}.web.api.redis.views import router + +__all__ = ['router'] diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/redis/schema.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/redis/schema.py new file mode 100644 index 0000000..e7e025d --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/redis/schema.py @@ -0,0 +1,10 @@ +from typing import Optional + +from pydantic import BaseModel + + +class RedisValueDTO(BaseModel): + """DTO for redis values.""" + + key: str + value: Optional[str] # noqa: WPS110 diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/redis/views.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/redis/views.py new file mode 100644 index 0000000..aeb4d0d --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/redis/views.py @@ -0,0 +1,41 @@ +from aioredis import Redis +from fastapi import APIRouter +from fastapi.param_functions import Depends + +from {{cookiecutter.project_name}}.services.redis.dependency import get_redis_connection +from {{cookiecutter.project_name}}.web.api.redis.schema import RedisValueDTO + +router = APIRouter() + + +@router.get("/", response_model=RedisValueDTO) +async def get_value( + key: str, + redis: Redis = Depends(get_redis_connection), +) -> RedisValueDTO: + """ + Get value from redis. + + :param key: redis key, to get data from. + :param redis: redis connection. + :returns: information from redis. + """ + redis_value = await redis.get(key) + return RedisValueDTO( + key=key, + value=redis_value, + ) + + +@router.put("/") +async def set_value( + redis_value: RedisValueDTO, + redis: Redis = Depends(get_redis_connection), +) -> None: + """ + Set value in redis. + + :param redis_value: new value data. + :param redis: redis connection. + """ + await redis.set(name=redis_value.key, value=redis_value.value) diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/router.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/router.py new file mode 100644 index 0000000..58cb9c2 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/router.py @@ -0,0 +1,19 @@ +from fastapi.routing import APIRouter + +from {{cookiecutter.project_name}}.web.api import echo + +{%- if cookiecutter.db_info.name != "none" %} +from {{cookiecutter.project_name}}.web.api import dummy +{%- endif %} +{%- if cookiecutter.enable_redis == "True" %} +from {{cookiecutter.project_name}}.web.api import redis +{%- endif %} + +api_router = APIRouter() +api_router.include_router(echo.router, prefix="/echo", tags=["echo"]) +{%- if cookiecutter.db_info.name != "none" %} +api_router.include_router(dummy.router, prefix="/dummy", tags=["dummy"]) +{%- endif %} +{%- if cookiecutter.enable_redis == "True" %} +api_router.include_router(redis.router, prefix="/redis", tags=["redis"]) +{%- endif %} diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/application.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/application.py new file mode 100644 index 0000000..8de1571 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/application.py @@ -0,0 +1,24 @@ +from fastapi import FastAPI + +from {{cookiecutter.project_name}}.web.api.router import api_router +from {{cookiecutter.project_name}}.web.lifetime import shutdown, startup + + +def get_app() -> FastAPI: + """ + Get FastAPI application. + + This is the main constructor of an application. + + :return: application. + """ + app = FastAPI( + title="{{cookiecutter.project_name}}", + ) + + app.on_event("startup")(startup(app)) + app.on_event("shutdown")(shutdown(app)) + + app.include_router(router=api_router, prefix="/api") + + return app diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/lifetime.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/lifetime.py new file mode 100644 index 0000000..15a4ea8 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/lifetime.py @@ -0,0 +1,94 @@ +from typing import Awaitable, Callable + +from fastapi import FastAPI + +from {{cookiecutter.project_name}}.settings import settings + +{%- if cookiecutter.enable_redis == "True" %} +import aioredis +{%- endif %} + +{%- if cookiecutter.db_info.name != "none" %} +from asyncio import current_task +from sqlalchemy.ext.asyncio import ( # type: ignore + AsyncSession, + async_scoped_session, + create_async_engine, +) +from sqlalchemy.orm import sessionmaker +{%- endif %} + + +{% if cookiecutter.db_info.name != "none" %} +def _setup_db(app: FastAPI) -> None: + """ + Create connection to the database. + + This function creates SQLAlchemy engine instance, + session_factory for creating sessions + and stores them in the application's state property. + + :param app: fastAPI application. + """ + engine = create_async_engine(str(settings.db_url), echo=settings.db_echo) + session_factory = async_scoped_session( + sessionmaker( + engine, + expire_on_commit=False, + class_=AsyncSession, + ), + scopefunc=current_task, + ) + app.state.db_engine = engine + app.state.db_session_factory = session_factory +{%- endif %} + +{% if cookiecutter.enable_redis == "True" %} +def _setup_redis(app: FastAPI) -> None: + app.state.redis_pool = aioredis.ConnectionPool.from_url( + str(settings.redis_url), + ) +{%- endif %} + + +def startup(app: FastAPI) -> Callable[[], Awaitable[None]]: + """ + Actions to run on application startup. + + This function use fastAPI app to store data, + such as db_engine. + + :param app: the fastAPI application. + :return: function that actually performs actions. + """ + + async def _startup() -> None: + {%- if cookiecutter.db_info.name != "none" %} + _setup_db(app) + {%- endif %} + {%- if cookiecutter.enable_redis == "True" %} + _setup_redis(app) + {%- endif %} + pass # noqa: WPS420 + + return _startup + + +def shutdown(app: FastAPI) -> Callable[[], Awaitable[None]]: + """ + Actions to run on application's shutdown. + + :param app: fastAPI application. + :return: function that actually performs actions. + """ + + async def _shutdown() -> None: + {%- if cookiecutter.db_info.name != "none" %} + await app.state.db_engine.dispose() + {%- endif %} + {%- if cookiecutter.enable_redis == "True" %} + await app.state.redis_pool.disconnect() + {%- endif %} + pass # noqa: WPS420 + + return _shutdown diff --git a/images/ui-example.png b/images/ui-example.png new file mode 100644 index 0000000000000000000000000000000000000000..7d2a6c08766a6f418073b6d0a5a2eb8d38d264e0 GIT binary patch literal 23550 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 literal 0 HcmV?d00001 diff --git a/poetry.lock b/poetry.lock index 9f69da8..17bcbf8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,22 +1,26 @@ -[[package]] -name = "appdirs" -version = "1.4.4" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "main" -optional = false -python-versions = "*" - [[package]] name = "arrow" -version = "0.17.0" +version = "1.1.1" description = "Better dates & times for Python" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.6" [package.dependencies] python-dateutil = ">=2.7.0" +[[package]] +name = "backports.entry-points-selectable" +version = "1.1.0" +description = "Compatibility shim providing selectable entry points for older implementations" +category = "main" +optional = false +python-versions = ">=2.7" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "pytest-mypy", "pytest-checkdocs (>=2.4)", "pytest-enabler (>=1.0.1)"] + [[package]] name = "binaryornot" version = "0.4.4" @@ -28,17 +32,9 @@ python-versions = "*" [package.dependencies] chardet = ">=3.0.2" -[[package]] -name = "cached-property" -version = "1.5.2" -description = "A decorator for caching properties in classes." -category = "main" -optional = false -python-versions = "*" - [[package]] name = "certifi" -version = "2020.11.8" +version = "2021.5.30" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false @@ -46,7 +42,7 @@ python-versions = "*" [[package]] name = "cffi" -version = "1.14.3" +version = "1.14.6" description = "Foreign Function Interface for Python calling C code." category = "main" optional = false @@ -57,7 +53,7 @@ pycparser = "*" [[package]] name = "cfgv" -version = "3.2.0" +version = "3.3.0" description = "Validate configuration and produce human readable error messages." category = "main" optional = false @@ -65,23 +61,45 @@ python-versions = ">=3.6.1" [[package]] name = "chardet" -version = "3.0.4" +version = "4.0.0" description = "Universal encoding detector for Python 2 and 3" category = "main" optional = false -python-versions = "*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "charset-normalizer" +version = "2.0.4" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.5.0" + +[package.extras] +unicode_backport = ["unicodedata2"] [[package]] name = "click" -version = "7.1.2" +version = "8.0.1" description = "Composable command line interface toolkit" category = "main" optional = false +python-versions = ">=3.6" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "main" +optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "cookiecutter" -version = "1.7.2" +version = "1.7.3" description = "A command-line utility that creates projects from project templates, e.g. creating a Python package project from a Python package project template." category = "main" optional = false @@ -90,9 +108,8 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.dependencies] binaryornot = ">=0.4.4" click = ">=7.0" -Jinja2 = "<3.0.0" +Jinja2 = ">=2.7,<4.0.0" jinja2-time = ">=0.2.0" -MarkupSafe = "<2.0.0" poyo = ">=0.5.0" python-slugify = ">=4.0.0" requests = ">=2.23.0" @@ -100,7 +117,7 @@ six = ">=1.10" [[package]] name = "distlib" -version = "0.3.1" +version = "0.3.2" description = "Distribution utilities" category = "main" optional = false @@ -116,36 +133,36 @@ python-versions = "*" [[package]] name = "identify" -version = "1.5.9" +version = "2.2.13" description = "File identification library for Python" category = "main" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +python-versions = ">=3.6.1" [package.extras] -license = ["editdistance"] +license = ["editdistance-s"] [[package]] name = "idna" -version = "2.10" +version = "3.2" description = "Internationalized Domain Names in Applications (IDNA)" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.5" [[package]] name = "jinja2" -version = "2.11.2" +version = "3.0.1" description = "A very fast and expressive template engine." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.6" [package.dependencies] -MarkupSafe = ">=0.23" +MarkupSafe = ">=2.0" [package.extras] -i18n = ["Babel (>=0.8)"] +i18n = ["Babel (>=2.7)"] [[package]] name = "jinja2-time" @@ -161,20 +178,32 @@ jinja2 = "*" [[package]] name = "markupsafe" -version = "1.1.1" +version = "2.0.1" description = "Safely add untrusted strings to HTML/XML markup." category = "main" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +python-versions = ">=3.6" [[package]] name = "nodeenv" -version = "1.5.0" +version = "1.6.0" description = "Node.js virtual environment builder" category = "main" optional = false python-versions = "*" +[[package]] +name = "platformdirs" +version = "2.2.0" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] +test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] + [[package]] name = "poyo" version = "0.5.0" @@ -185,7 +214,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pre-commit" -version = "2.8.2" +version = "2.14.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." category = "main" optional = false @@ -199,6 +228,17 @@ pyyaml = ">=5.1" toml = "*" virtualenv = ">=20.0.8" +[[package]] +name = "prompt-toolkit" +version = "3.0.19" +description = "Library for building powerful interactive command lines in Python" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +wcwidth = "*" + [[package]] name = "pycparser" version = "2.20" @@ -207,21 +247,35 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[[package]] +name = "pydantic" +version = "1.8.2" +description = "Data validation and settings management using python 3.6 type hinting" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +typing-extensions = ">=3.7.4.3" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + [[package]] name = "pygit2" -version = "1.4.0" +version = "1.6.1" description = "Python bindings for libgit2." category = "main" optional = false python-versions = "*" [package.dependencies] -cached-property = "*" cffi = ">=1.4.0" [[package]] name = "python-dateutil" -version = "2.8.1" +version = "2.8.2" description = "Extensions to the standard Python datetime module" category = "main" optional = false @@ -232,11 +286,11 @@ six = ">=1.5" [[package]] name = "python-slugify" -version = "4.0.1" +version = "5.0.2" description = "A Python Slugify application that handles Unicode" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.6" [package.dependencies] text-unidecode = ">=1.3" @@ -246,33 +300,33 @@ unidecode = ["Unidecode (>=1.1.1)"] [[package]] name = "pyyaml" -version = "5.3.1" +version = "5.4.1" description = "YAML parser and emitter for Python" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [[package]] name = "requests" -version = "2.25.0" +version = "2.26.0" description = "Python HTTP for Humans." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [package.dependencies] certifi = ">=2017.4.17" -chardet = ">=3.0.2,<4" -idna = ">=2.5,<3" +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] -security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] [[package]] name = "six" -version = "1.15.0" +version = "1.16.0" description = "Python 2 and 3 compatibility utilities" category = "main" optional = false @@ -302,9 +356,17 @@ category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +[[package]] +name = "typing-extensions" +version = "3.10.0.0" +description = "Backported and Experimental Type Hints for Python 3.5+" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "urllib3" -version = "1.26.2" +version = "1.26.6" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false @@ -317,223 +379,301 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.1.0" +version = "20.7.2" description = "Virtual Python Environment builder" category = "main" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [package.dependencies] -appdirs = ">=1.4.3,<2" +"backports.entry-points-selectable" = ">=1.0.4" distlib = ">=0.3.1,<1" filelock = ">=3.0.0,<4" +platformdirs = ">=2,<3" six = ">=1.9.0,<2" [package.extras] docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"] -testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "pytest-xdist (>=1.31.0)", "packaging (>=20.0)", "xonsh (>=0.9.16)"] +testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] + +[[package]] +name = "wcwidth" +version = "0.2.5" +description = "Measures the displayed width of unicode strings in a terminal" +category = "main" +optional = false +python-versions = "*" [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "33b8d26680a3c261f42bc2a403e6fad6800e544608213bf58e35f8aec070e6d6" +content-hash = "f75e0608a162229e1e7cf160a4adcbbe2e1cb11621073400604d5d5086504c71" [metadata.files] -appdirs = [ - {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, - {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, -] arrow = [ - {file = "arrow-0.17.0-py2.py3-none-any.whl", hash = "sha256:e098abbd9af3665aea81bdd6c869e93af4feb078e98468dd351c383af187aac5"}, - {file = "arrow-0.17.0.tar.gz", hash = "sha256:ff08d10cda1d36c68657d6ad20d74fbea493d980f8b2d45344e00d6ed2bf6ed4"}, + {file = "arrow-1.1.1-py3-none-any.whl", hash = "sha256:77a60a4db5766d900a2085ce9074c5c7b8e2c99afeaa98ad627637ff6f292510"}, + {file = "arrow-1.1.1.tar.gz", hash = "sha256:dee7602f6c60e3ec510095b5e301441bc56288cb8f51def14dcb3079f623823a"}, +] +"backports.entry-points-selectable" = [ + {file = "backports.entry_points_selectable-1.1.0-py2.py3-none-any.whl", hash = "sha256:a6d9a871cde5e15b4c4a53e3d43ba890cc6861ec1332c9c2428c92f977192acc"}, + {file = "backports.entry_points_selectable-1.1.0.tar.gz", hash = "sha256:988468260ec1c196dab6ae1149260e2f5472c9110334e5d51adcb77867361f6a"}, ] binaryornot = [ {file = "binaryornot-0.4.4-py2.py3-none-any.whl", hash = "sha256:b8b71173c917bddcd2c16070412e369c3ed7f0528926f70cac18a6c97fd563e4"}, {file = "binaryornot-0.4.4.tar.gz", hash = "sha256:359501dfc9d40632edc9fac890e19542db1a287bbcfa58175b66658392018061"}, ] -cached-property = [ - {file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"}, - {file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"}, -] certifi = [ - {file = "certifi-2020.11.8-py2.py3-none-any.whl", hash = "sha256:1f422849db327d534e3d0c5f02a263458c3955ec0aae4ff09b95f195c59f4edd"}, - {file = "certifi-2020.11.8.tar.gz", hash = "sha256:f05def092c44fbf25834a51509ef6e631dc19765ab8a57b4e7ab85531f0a9cf4"}, + {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, + {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, ] cffi = [ - {file = "cffi-1.14.3-2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3eeeb0405fd145e714f7633a5173318bd88d8bbfc3dd0a5751f8c4f70ae629bc"}, - {file = "cffi-1.14.3-2-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:cb763ceceae04803adcc4e2d80d611ef201c73da32d8f2722e9d0ab0c7f10768"}, - {file = "cffi-1.14.3-2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:44f60519595eaca110f248e5017363d751b12782a6f2bd6a7041cba275215f5d"}, - {file = "cffi-1.14.3-2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c53af463f4a40de78c58b8b2710ade243c81cbca641e34debf3396a9640d6ec1"}, - {file = "cffi-1.14.3-2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:33c6cdc071ba5cd6d96769c8969a0531be2d08c2628a0143a10a7dcffa9719ca"}, - {file = "cffi-1.14.3-2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c11579638288e53fc94ad60022ff1b67865363e730ee41ad5e6f0a17188b327a"}, - {file = "cffi-1.14.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:3cb3e1b9ec43256c4e0f8d2837267a70b0e1ca8c4f456685508ae6106b1f504c"}, - {file = "cffi-1.14.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f0620511387790860b249b9241c2f13c3a80e21a73e0b861a2df24e9d6f56730"}, - {file = "cffi-1.14.3-cp27-cp27m-win32.whl", hash = "sha256:005f2bfe11b6745d726dbb07ace4d53f057de66e336ff92d61b8c7e9c8f4777d"}, - {file = "cffi-1.14.3-cp27-cp27m-win_amd64.whl", hash = "sha256:2f9674623ca39c9ebe38afa3da402e9326c245f0f5ceff0623dccdac15023e05"}, - {file = "cffi-1.14.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:09e96138280241bd355cd585148dec04dbbedb4f46128f340d696eaafc82dd7b"}, - {file = "cffi-1.14.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:3363e77a6176afb8823b6e06db78c46dbc4c7813b00a41300a4873b6ba63b171"}, - {file = "cffi-1.14.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0ef488305fdce2580c8b2708f22d7785ae222d9825d3094ab073e22e93dfe51f"}, - {file = "cffi-1.14.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:0b1ad452cc824665ddc682400b62c9e4f5b64736a2ba99110712fdee5f2505c4"}, - {file = "cffi-1.14.3-cp35-cp35m-win32.whl", hash = "sha256:85ba797e1de5b48aa5a8427b6ba62cf69607c18c5d4eb747604b7302f1ec382d"}, - {file = "cffi-1.14.3-cp35-cp35m-win_amd64.whl", hash = "sha256:e66399cf0fc07de4dce4f588fc25bfe84a6d1285cc544e67987d22663393926d"}, - {file = "cffi-1.14.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:15f351bed09897fbda218e4db5a3d5c06328862f6198d4fb385f3e14e19decb3"}, - {file = "cffi-1.14.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4d7c26bfc1ea9f92084a1d75e11999e97b62d63128bcc90c3624d07813c52808"}, - {file = "cffi-1.14.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:23e5d2040367322824605bc29ae8ee9175200b92cb5483ac7d466927a9b3d537"}, - {file = "cffi-1.14.3-cp36-cp36m-win32.whl", hash = "sha256:a624fae282e81ad2e4871bdb767e2c914d0539708c0f078b5b355258293c98b0"}, - {file = "cffi-1.14.3-cp36-cp36m-win_amd64.whl", hash = "sha256:de31b5164d44ef4943db155b3e8e17929707cac1e5bd2f363e67a56e3af4af6e"}, - {file = "cffi-1.14.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f92cdecb618e5fa4658aeb97d5eb3d2f47aa94ac6477c6daf0f306c5a3b9e6b1"}, - {file = "cffi-1.14.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:22399ff4870fb4c7ef19fff6eeb20a8bbf15571913c181c78cb361024d574579"}, - {file = "cffi-1.14.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:f4eae045e6ab2bb54ca279733fe4eb85f1effda392666308250714e01907f394"}, - {file = "cffi-1.14.3-cp37-cp37m-win32.whl", hash = "sha256:b0358e6fefc74a16f745afa366acc89f979040e0cbc4eec55ab26ad1f6a9bfbc"}, - {file = "cffi-1.14.3-cp37-cp37m-win_amd64.whl", hash = "sha256:6642f15ad963b5092d65aed022d033c77763515fdc07095208f15d3563003869"}, - {file = "cffi-1.14.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:2791f68edc5749024b4722500e86303a10d342527e1e3bcac47f35fbd25b764e"}, - {file = "cffi-1.14.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:529c4ed2e10437c205f38f3691a68be66c39197d01062618c55f74294a4a4828"}, - {file = "cffi-1.14.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8f0f1e499e4000c4c347a124fa6a27d37608ced4fe9f7d45070563b7c4c370c9"}, - {file = "cffi-1.14.3-cp38-cp38-win32.whl", hash = "sha256:3b8eaf915ddc0709779889c472e553f0d3e8b7bdf62dab764c8921b09bf94522"}, - {file = "cffi-1.14.3-cp38-cp38-win_amd64.whl", hash = "sha256:bbd2f4dfee1079f76943767fce837ade3087b578aeb9f69aec7857d5bf25db15"}, - {file = "cffi-1.14.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:cc75f58cdaf043fe6a7a6c04b3b5a0e694c6a9e24050967747251fb80d7bce0d"}, - {file = "cffi-1.14.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:bf39a9e19ce7298f1bd6a9758fa99707e9e5b1ebe5e90f2c3913a47bc548747c"}, - {file = "cffi-1.14.3-cp39-cp39-win32.whl", hash = "sha256:d80998ed59176e8cba74028762fbd9b9153b9afc71ea118e63bbf5d4d0f9552b"}, - {file = "cffi-1.14.3-cp39-cp39-win_amd64.whl", hash = "sha256:c150eaa3dadbb2b5339675b88d4573c1be3cb6f2c33a6c83387e10cc0bf05bd3"}, - {file = "cffi-1.14.3.tar.gz", hash = "sha256:f92f789e4f9241cd262ad7a555ca2c648a98178a953af117ef7fad46aa1d5591"}, + {file = "cffi-1.14.6-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:22b9c3c320171c108e903d61a3723b51e37aaa8c81255b5e7ce102775bd01e2c"}, + {file = "cffi-1.14.6-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:f0c5d1acbfca6ebdd6b1e3eded8d261affb6ddcf2186205518f1428b8569bb99"}, + {file = "cffi-1.14.6-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99f27fefe34c37ba9875f224a8f36e31d744d8083e00f520f133cab79ad5e819"}, + {file = "cffi-1.14.6-cp27-cp27m-win32.whl", hash = "sha256:55af55e32ae468e9946f741a5d51f9896da6b9bf0bbdd326843fec05c730eb20"}, + {file = "cffi-1.14.6-cp27-cp27m-win_amd64.whl", hash = "sha256:7bcac9a2b4fdbed2c16fa5681356d7121ecabf041f18d97ed5b8e0dd38a80224"}, + {file = "cffi-1.14.6-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ed38b924ce794e505647f7c331b22a693bee1538fdf46b0222c4717b42f744e7"}, + {file = "cffi-1.14.6-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e22dcb48709fc51a7b58a927391b23ab37eb3737a98ac4338e2448bef8559b33"}, + {file = "cffi-1.14.6-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:aedb15f0a5a5949ecb129a82b72b19df97bbbca024081ed2ef88bd5c0a610534"}, + {file = "cffi-1.14.6-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:48916e459c54c4a70e52745639f1db524542140433599e13911b2f329834276a"}, + {file = "cffi-1.14.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f627688813d0a4140153ff532537fbe4afea5a3dffce1f9deb7f91f848a832b5"}, + {file = "cffi-1.14.6-cp35-cp35m-win32.whl", hash = "sha256:f0010c6f9d1a4011e429109fda55a225921e3206e7f62a0c22a35344bfd13cca"}, + {file = "cffi-1.14.6-cp35-cp35m-win_amd64.whl", hash = "sha256:57e555a9feb4a8460415f1aac331a2dc833b1115284f7ded7278b54afc5bd218"}, + {file = "cffi-1.14.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e8c6a99be100371dbb046880e7a282152aa5d6127ae01783e37662ef73850d8f"}, + {file = "cffi-1.14.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:19ca0dbdeda3b2615421d54bef8985f72af6e0c47082a8d26122adac81a95872"}, + {file = "cffi-1.14.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d950695ae4381ecd856bcaf2b1e866720e4ab9a1498cba61c602e56630ca7195"}, + {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9dc245e3ac69c92ee4c167fbdd7428ec1956d4e754223124991ef29eb57a09d"}, + {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8661b2ce9694ca01c529bfa204dbb144b275a31685a075ce123f12331be790b"}, + {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b315d709717a99f4b27b59b021e6207c64620790ca3e0bde636a6c7f14618abb"}, + {file = "cffi-1.14.6-cp36-cp36m-win32.whl", hash = "sha256:80b06212075346b5546b0417b9f2bf467fea3bfe7352f781ffc05a8ab24ba14a"}, + {file = "cffi-1.14.6-cp36-cp36m-win_amd64.whl", hash = "sha256:a9da7010cec5a12193d1af9872a00888f396aba3dc79186604a09ea3ee7c029e"}, + {file = "cffi-1.14.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4373612d59c404baeb7cbd788a18b2b2a8331abcc84c3ba40051fcd18b17a4d5"}, + {file = "cffi-1.14.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f10afb1004f102c7868ebfe91c28f4a712227fe4cb24974350ace1f90e1febbf"}, + {file = "cffi-1.14.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fd4305f86f53dfd8cd3522269ed7fc34856a8ee3709a5e28b2836b2db9d4cd69"}, + {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d6169cb3c6c2ad50db5b868db6491a790300ade1ed5d1da29289d73bbe40b56"}, + {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d4b68e216fc65e9fe4f524c177b54964af043dde734807586cf5435af84045c"}, + {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33791e8a2dc2953f28b8d8d300dde42dd929ac28f974c4b4c6272cb2955cb762"}, + {file = "cffi-1.14.6-cp37-cp37m-win32.whl", hash = "sha256:0c0591bee64e438883b0c92a7bed78f6290d40bf02e54c5bf0978eaf36061771"}, + {file = "cffi-1.14.6-cp37-cp37m-win_amd64.whl", hash = "sha256:8eb687582ed7cd8c4bdbff3df6c0da443eb89c3c72e6e5dcdd9c81729712791a"}, + {file = "cffi-1.14.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba6f2b3f452e150945d58f4badd92310449876c4c954836cfb1803bdd7b422f0"}, + {file = "cffi-1.14.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:64fda793737bc4037521d4899be780534b9aea552eb673b9833b01f945904c2e"}, + {file = "cffi-1.14.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9f3e33c28cd39d1b655ed1ba7247133b6f7fc16fa16887b120c0c670e35ce346"}, + {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26bb2549b72708c833f5abe62b756176022a7b9a7f689b571e74c8478ead51dc"}, + {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb687a11f0a7a1839719edd80f41e459cc5366857ecbed383ff376c4e3cc6afd"}, + {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2ad4d668a5c0645d281dcd17aff2be3212bc109b33814bbb15c4939f44181cc"}, + {file = "cffi-1.14.6-cp38-cp38-win32.whl", hash = "sha256:487d63e1454627c8e47dd230025780e91869cfba4c753a74fda196a1f6ad6548"}, + {file = "cffi-1.14.6-cp38-cp38-win_amd64.whl", hash = "sha256:c33d18eb6e6bc36f09d793c0dc58b0211fccc6ae5149b808da4a62660678b156"}, + {file = "cffi-1.14.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:06c54a68935738d206570b20da5ef2b6b6d92b38ef3ec45c5422c0ebaf338d4d"}, + {file = "cffi-1.14.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:f174135f5609428cc6e1b9090f9268f5c8935fddb1b25ccb8255a2d50de6789e"}, + {file = "cffi-1.14.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f3ebe6e73c319340830a9b2825d32eb6d8475c1dac020b4f0aa774ee3b898d1c"}, + {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c8d896becff2fa653dc4438b54a5a25a971d1f4110b32bd3068db3722c80202"}, + {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4922cd707b25e623b902c86188aca466d3620892db76c0bdd7b99a3d5e61d35f"}, + {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9e005e9bd57bc987764c32a1bee4364c44fdc11a3cc20a40b93b444984f2b87"}, + {file = "cffi-1.14.6-cp39-cp39-win32.whl", hash = "sha256:eb9e2a346c5238a30a746893f23a9535e700f8192a68c07c0258e7ece6ff3728"}, + {file = "cffi-1.14.6-cp39-cp39-win_amd64.whl", hash = "sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2"}, + {file = "cffi-1.14.6.tar.gz", hash = "sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd"}, ] cfgv = [ - {file = "cfgv-3.2.0-py2.py3-none-any.whl", hash = "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d"}, - {file = "cfgv-3.2.0.tar.gz", hash = "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1"}, + {file = "cfgv-3.3.0-py2.py3-none-any.whl", hash = "sha256:b449c9c6118fe8cca7fa5e00b9ec60ba08145d281d52164230a69211c5d597a1"}, + {file = "cfgv-3.3.0.tar.gz", hash = "sha256:9e600479b3b99e8af981ecdfc80a0296104ee610cab48a5ae4ffd0b668650eb1"}, ] chardet = [ - {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, - {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, + {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, + {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, +] +charset-normalizer = [ + {file = "charset-normalizer-2.0.4.tar.gz", hash = "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"}, + {file = "charset_normalizer-2.0.4-py3-none-any.whl", hash = "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b"}, ] click = [ - {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, - {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, + {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, + {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] cookiecutter = [ - {file = "cookiecutter-1.7.2-py2.py3-none-any.whl", hash = "sha256:430eb882d028afb6102c084bab6cf41f6559a77ce9b18dc6802e3bc0cc5f4a30"}, - {file = "cookiecutter-1.7.2.tar.gz", hash = "sha256:efb6b2d4780feda8908a873e38f0e61778c23f6a2ea58215723bcceb5b515dac"}, + {file = "cookiecutter-1.7.3-py2.py3-none-any.whl", hash = "sha256:f8671531fa96ab14339d0c59b4f662a4f12a2ecacd94a0f70a3500843da588e2"}, + {file = "cookiecutter-1.7.3.tar.gz", hash = "sha256:6b9a4d72882e243be077a7397d0f1f76fe66cf3df91f3115dbb5330e214fa457"}, ] distlib = [ - {file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"}, - {file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"}, + {file = "distlib-0.3.2-py2.py3-none-any.whl", hash = "sha256:23e223426b28491b1ced97dc3bbe183027419dfc7982b4fa2f05d5f3ff10711c"}, + {file = "distlib-0.3.2.zip", hash = "sha256:106fef6dc37dd8c0e2c0a60d3fca3e77460a48907f335fa28420463a6f799736"}, ] filelock = [ {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, ] identify = [ - {file = "identify-1.5.9-py2.py3-none-any.whl", hash = "sha256:5dd84ac64a9a115b8e0b27d1756b244b882ad264c3c423f42af8235a6e71ca12"}, - {file = "identify-1.5.9.tar.gz", hash = "sha256:c9504ba6a043ee2db0a9d69e43246bc138034895f6338d5aed1b41e4a73b1513"}, + {file = "identify-2.2.13-py2.py3-none-any.whl", hash = "sha256:7199679b5be13a6b40e6e19ea473e789b11b4e3b60986499b1f589ffb03c217c"}, + {file = "identify-2.2.13.tar.gz", hash = "sha256:7bc6e829392bd017236531963d2d937d66fc27cadc643ac0aba2ce9f26157c79"}, ] idna = [ - {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, - {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, + {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, + {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"}, ] jinja2 = [ - {file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"}, - {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"}, + {file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"}, + {file = "Jinja2-3.0.1.tar.gz", hash = "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"}, ] jinja2-time = [ {file = "jinja2-time-0.2.0.tar.gz", hash = "sha256:d14eaa4d315e7688daa4969f616f226614350c48730bfa1692d2caebd8c90d40"}, {file = "jinja2_time-0.2.0-py2.py3-none-any.whl", hash = "sha256:d3eab6605e3ec8b7a0863df09cc1d23714908fa61aa6986a845c20ba488b4efa"}, ] markupsafe = [ - {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, - {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, - {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, - {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, + {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-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-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_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-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-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"}, ] nodeenv = [ - {file = "nodeenv-1.5.0-py2.py3-none-any.whl", hash = "sha256:5304d424c529c997bc888453aeaa6362d242b6b4631e90f3d4bf1b290f1c84a9"}, - {file = "nodeenv-1.5.0.tar.gz", hash = "sha256:ab45090ae383b716c4ef89e690c41ff8c2b257b85b309f01f3654df3d084bd7c"}, + {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"}, + {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"}, +] +platformdirs = [ + {file = "platformdirs-2.2.0-py3-none-any.whl", hash = "sha256:4666d822218db6a262bdfdc9c39d21f23b4cfdb08af331a81e92751daf6c866c"}, + {file = "platformdirs-2.2.0.tar.gz", hash = "sha256:632daad3ab546bd8e6af0537d09805cec458dce201bccfe23012df73332e181e"}, ] poyo = [ {file = "poyo-0.5.0-py2.py3-none-any.whl", hash = "sha256:3e2ca8e33fdc3c411cd101ca395668395dd5dc7ac775b8e809e3def9f9fe041a"}, {file = "poyo-0.5.0.tar.gz", hash = "sha256:e26956aa780c45f011ca9886f044590e2d8fd8b61db7b1c1cf4e0869f48ed4dd"}, ] pre-commit = [ - {file = "pre_commit-2.8.2-py2.py3-none-any.whl", hash = "sha256:22e6aa3bd571debb01eb7d34483f11c01b65237be4eebbf30c3d4fb65762d315"}, - {file = "pre_commit-2.8.2.tar.gz", hash = "sha256:905ebc9b534b991baec87e934431f2d0606ba27f2b90f7f652985f5a5b8b6ae6"}, + {file = "pre_commit-2.14.0-py2.py3-none-any.whl", hash = "sha256:ec3045ae62e1aa2eecfb8e86fa3025c2e3698f77394ef8d2011ce0aedd85b2d4"}, + {file = "pre_commit-2.14.0.tar.gz", hash = "sha256:2386eeb4cf6633712c7cc9ede83684d53c8cafca6b59f79c738098b51c6d206c"}, +] +prompt-toolkit = [ + {file = "prompt_toolkit-3.0.19-py3-none-any.whl", hash = "sha256:7089d8d2938043508aa9420ec18ce0922885304cddae87fb96eebca942299f88"}, + {file = "prompt_toolkit-3.0.19.tar.gz", hash = "sha256:08360ee3a3148bdb5163621709ee322ec34fc4375099afa4bbf751e9b7b7fa4f"}, ] pycparser = [ {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, ] +pydantic = [ + {file = "pydantic-1.8.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739"}, + {file = "pydantic-1.8.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4"}, + {file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:589eb6cd6361e8ac341db97602eb7f354551482368a37f4fd086c0733548308e"}, + {file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:10e5622224245941efc193ad1d159887872776df7a8fd592ed746aa25d071840"}, + {file = "pydantic-1.8.2-cp36-cp36m-win_amd64.whl", hash = "sha256:99a9fc39470010c45c161a1dc584997f1feb13f689ecf645f59bb4ba623e586b"}, + {file = "pydantic-1.8.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a83db7205f60c6a86f2c44a61791d993dff4b73135df1973ecd9eed5ea0bda20"}, + {file = "pydantic-1.8.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:41b542c0b3c42dc17da70554bc6f38cbc30d7066d2c2815a94499b5684582ecb"}, + {file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:ea5cb40a3b23b3265f6325727ddfc45141b08ed665458be8c6285e7b85bd73a1"}, + {file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:18b5ea242dd3e62dbf89b2b0ec9ba6c7b5abaf6af85b95a97b00279f65845a23"}, + {file = "pydantic-1.8.2-cp37-cp37m-win_amd64.whl", hash = "sha256:234a6c19f1c14e25e362cb05c68afb7f183eb931dd3cd4605eafff055ebbf287"}, + {file = "pydantic-1.8.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:021ea0e4133e8c824775a0cfe098677acf6fa5a3cbf9206a376eed3fc09302cd"}, + {file = "pydantic-1.8.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e710876437bc07bd414ff453ac8ec63d219e7690128d925c6e82889d674bb505"}, + {file = "pydantic-1.8.2-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:ac8eed4ca3bd3aadc58a13c2aa93cd8a884bcf21cb019f8cfecaae3b6ce3746e"}, + {file = "pydantic-1.8.2-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:4a03cbbe743e9c7247ceae6f0d8898f7a64bb65800a45cbdc52d65e370570820"}, + {file = "pydantic-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:8621559dcf5afacf0069ed194278f35c255dc1a1385c28b32dd6c110fd6531b3"}, + {file = "pydantic-1.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8b223557f9510cf0bfd8b01316bf6dd281cf41826607eada99662f5e4963f316"}, + {file = "pydantic-1.8.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:244ad78eeb388a43b0c927e74d3af78008e944074b7d0f4f696ddd5b2af43c62"}, + {file = "pydantic-1.8.2-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:05ef5246a7ffd2ce12a619cbb29f3307b7c4509307b1b49f456657b43529dc6f"}, + {file = "pydantic-1.8.2-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:54cd5121383f4a461ff7644c7ca20c0419d58052db70d8791eacbbe31528916b"}, + {file = "pydantic-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:4be75bebf676a5f0f87937c6ddb061fa39cbea067240d98e298508c1bda6f3f3"}, + {file = "pydantic-1.8.2-py3-none-any.whl", hash = "sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833"}, + {file = "pydantic-1.8.2.tar.gz", hash = "sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b"}, +] pygit2 = [ - {file = "pygit2-1.4.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b422e417739def0a136a6355723dfe8a5ffc83db5098076f28a14f1d139779c1"}, - {file = "pygit2-1.4.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:32eb863d6651d4890ced318505ea8dc229bd9637deaf29c898de1ab574d727a0"}, - {file = "pygit2-1.4.0-cp36-cp36m-win32.whl", hash = "sha256:0d298098e286eeda000e49ca7e1b41f87300e10dd8b9d06b32b008bd61f50b83"}, - {file = "pygit2-1.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:9c8d5881eb709e2e2e13000b507a131bd5fb91a879581030088d0ddffbcd19af"}, - {file = "pygit2-1.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ddb7a1f6d38063e8724abfa1cfdfb0f9b25014b8bca0546274b7a84b873a3888"}, - {file = "pygit2-1.4.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:ec5c0365a9bdfcac1609d20868507b28685ec5ea7cc3a2c903c9b62ef2e0bbc0"}, - {file = "pygit2-1.4.0-cp37-cp37m-win32.whl", hash = "sha256:818c91b582109d90580c5da74af783738838353f15eb12eeb734d80a974b05a3"}, - {file = "pygit2-1.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9c2f2d9ef59513007b66f6534b000792b614de3faf60313a0a68f6b8571aea85"}, - {file = "pygit2-1.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8306a302487dac67df7af6a064bb37e8a8eb4138958f9560ff49ff162e185dab"}, - {file = "pygit2-1.4.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d0d889144e9487d926fecea947c3f39ce5f477e521d7d467d2e66907e4cd657d"}, - {file = "pygit2-1.4.0-cp38-cp38-win32.whl", hash = "sha256:41204b6f3406d9f53147710f3cc485d77181ba67f57c34d36b7c86de1c14a18c"}, - {file = "pygit2-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:cf00481ddf053e549a6edd0216bdc267b292d261eae02a67bb3737de920cbf88"}, - {file = "pygit2-1.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37d6d7d6d7804c42a0fe23425c72e38093488525092fc5e51a05684e63503ce7"}, - {file = "pygit2-1.4.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:e9037a7d810750fe23c9f5641ef14a0af2525ff03e14752cd4f73e1870ecfcb0"}, - {file = "pygit2-1.4.0-cp39-cp39-win32.whl", hash = "sha256:fdd8ba30cda277290e000322f505132f590cf89bd7d31829b45a3cb57447ec32"}, - {file = "pygit2-1.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:0ee135eb2cd8b07ce1374f3596cc5c3213472d6389bad6a4c5d87d8e267e93e9"}, - {file = "pygit2-1.4.0.tar.gz", hash = "sha256:cbeb38ab1df9b5d8896548a11e63aae8a064763ab5f1eabe4475e6b8a78ee1c8"}, + {file = "pygit2-1.6.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:547429774c11f5bc9d20a49aa86e4bd13c90a55140504ef05f55cf424470ee34"}, + {file = "pygit2-1.6.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e75865d7b6fc161d93b16f10365eaad353cd546e302a98f2de2097ddea1066b"}, + {file = "pygit2-1.6.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4a64b6090308ffd1c82e2dd4316cb79483715387b13818156d516134a5b17c"}, + {file = "pygit2-1.6.1-cp36-cp36m-win32.whl", hash = "sha256:2666a3970b2ea1222a9f0463b466f98c8d564f29ec84cf0a58d9b0d3865dbaaf"}, + {file = "pygit2-1.6.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2de12ca2d3b7eb86106223b40b2edc0c61103c71e7962e53092c6ddef71a194"}, + {file = "pygit2-1.6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9c1d96c66fb6e69ec710078a73c19edff420bc1db430caa9e03a825eede3f25c"}, + {file = "pygit2-1.6.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:454d42550fa6a6cd0e6a6ad9ab3f3262135fd157f57bad245ce156c36ee93370"}, + {file = "pygit2-1.6.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce0827b77dd2f8a3465bdc181c4e65f27dd12dbd92635c038e58030cc90c2de0"}, + {file = "pygit2-1.6.1-cp37-cp37m-win32.whl", hash = "sha256:b0161a141888d450eb821472fdcdadd14a072ddeda841fee9984956d34d3e19d"}, + {file = "pygit2-1.6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:af2fa259b6f7899227611ab978c600695724e85965836cb607d8b1e70cfea9b3"}, + {file = "pygit2-1.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0e1e02c28983ddc004c0f54063f3e46fca388225d468e32e16689cfb750e0bd6"}, + {file = "pygit2-1.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5dadc4844feb76cde5cc9a37656326a361dd8b5c8e8f8674dcd4a5ecf395db3"}, + {file = "pygit2-1.6.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef07458e4172a31318663295083b43f957d611145738ff56aa76db593542a6e8"}, + {file = "pygit2-1.6.1-cp38-cp38-win32.whl", hash = "sha256:7a0c0a1f11fd41f57e8c6c64d903cc7fa4ec95d15592270be3217ed7f78eb023"}, + {file = "pygit2-1.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:2fd5c1b2d84dc6084f1bda836607afe37e95186a53a5a827a69083415e57fe4f"}, + {file = "pygit2-1.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b9b88b7e9a5286a71be0b6c307f0523c9606aeedff6b61eb9c440e18817fa641"}, + {file = "pygit2-1.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac12d32b714c3383ebccffee5eb6aff0b69a2542a40a664fd5ad370afcb28ee7"}, + {file = "pygit2-1.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe682ed6afd2ab31127f6a502cf3e002dc1cc8d26c36a5d49dfd180250351eb6"}, + {file = "pygit2-1.6.1-cp39-cp39-win32.whl", hash = "sha256:dbbf66a23860aa899949068ac9b503b4bc21e6063e8f53870440adbdc909405e"}, + {file = "pygit2-1.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:f90775afb11f69376e2af21ab56fcfbb52f6bc84117059ddf0355f81e5e36352"}, + {file = "pygit2-1.6.1.tar.gz", hash = "sha256:c3303776f774d3e0115c1c4f6e1fc35470d15f113a7ae9401a0b90acfa1661ac"}, ] python-dateutil = [ - {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, - {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, ] python-slugify = [ - {file = "python-slugify-4.0.1.tar.gz", hash = "sha256:69a517766e00c1268e5bbfc0d010a0a8508de0b18d30ad5a1ff357f8ae724270"}, + {file = "python-slugify-5.0.2.tar.gz", hash = "sha256:f13383a0b9fcbe649a1892b9c8eb4f8eab1d6d84b84bb7a624317afa98159cab"}, + {file = "python_slugify-5.0.2-py2.py3-none-any.whl", hash = "sha256:6d8c5df75cd4a7c3a2d21e257633de53f52ab0265cd2d1dc62a730e8194a7380"}, ] pyyaml = [ - {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, - {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, - {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"}, - {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"}, - {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"}, - {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"}, - {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"}, - {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, - {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, - {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, - {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, + {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, + {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, + {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"}, + {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, + {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, + {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, + {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"}, + {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"}, + {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, + {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, + {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, + {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, + {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"}, + {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"}, + {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, + {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, + {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, + {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, + {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"}, + {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"}, + {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, + {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, + {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, + {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, + {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"}, + {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"}, + {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, + {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, + {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, ] requests = [ - {file = "requests-2.25.0-py2.py3-none-any.whl", hash = "sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998"}, - {file = "requests-2.25.0.tar.gz", hash = "sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8"}, + {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, + {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, ] six = [ - {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, - {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] termcolor = [ {file = "termcolor-1.1.0.tar.gz", hash = "sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b"}, @@ -546,11 +686,20 @@ toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] +typing-extensions = [ + {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, + {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, + {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, +] urllib3 = [ - {file = "urllib3-1.26.2-py2.py3-none-any.whl", hash = "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473"}, - {file = "urllib3-1.26.2.tar.gz", hash = "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08"}, + {file = "urllib3-1.26.6-py2.py3-none-any.whl", hash = "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4"}, + {file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"}, ] virtualenv = [ - {file = "virtualenv-20.1.0-py2.py3-none-any.whl", hash = "sha256:b0011228208944ce71052987437d3843e05690b2f23d1c7da4263fde104c97a2"}, - {file = "virtualenv-20.1.0.tar.gz", hash = "sha256:b8d6110f493af256a40d65e29846c69340a947669eec8ce784fcf3dd3af28380"}, + {file = "virtualenv-20.7.2-py2.py3-none-any.whl", hash = "sha256:e4670891b3a03eb071748c569a87cceaefbf643c5bac46d996c5a45c34aa0f06"}, + {file = "virtualenv-20.7.2.tar.gz", hash = "sha256:9ef4e8ee4710826e98ff3075c9a4739e2cb1040de6a2a8d35db0055840dc96a0"}, +] +wcwidth = [ + {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, + {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, ] diff --git a/pyproject.toml b/pyproject.toml index e2b0738..03035b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "fastapi_template" -version = "1.2.3" +version = "2.0.0" description = "Feature-rich robust FastAPI template" authors = ["Pavel Kirilin <win10@list.ru>"] packages = [ @@ -18,15 +18,17 @@ keywords = [ [tool.poetry.dependencies] python = "^3.8" -cookiecutter = "^1.7.2" -pre-commit = "^2.8.2" +cookiecutter = "^1.7.3" +pre-commit = "^2.14.0" termcolor = "^1.1.0" -pygit2 = "^1.4.0" +pygit2 = "^1.6.0" +pydantic = "^1.8.2" +prompt-toolkit = "^3.0.19" [tool.poetry.dev-dependencies] [tool.poetry.scripts] -fastapi_template = "fastapi_template.main:main" +fastapi_template = "fastapi_template.__main__:main" [build-system] requires = ["poetry-core>=1.0.0"] -- GitLab