diff --git a/README.md b/README.md index 74ee8f3338bba5e7a3f8bfe83e0dc4f3f2588e10..3b58385d83ede6c2e6887a9397a6fe12551558ad 100644 --- a/README.md +++ b/README.md @@ -30,42 +30,50 @@ python3 -m fastapi_template ## Features -Template is made with SQLAlchemy1.4 and uses sqlalchemy orm and sessions, -instead of raw drivers. +One of the coolest features is that this project is extremely small and handy. +You can choose between different databases and even ORMs. +Currently SQLAlchemy1.4 and TortoiseORM are supported. -It has minimum to start new excellent project. - -Pre-commit integrations and excellent code documentation. +TUI and CLI and excellent code documentation. Generator features: -- Different databases to choose from. -- Alembic integration; +- Different databases support; +- Different ORMs support; +- Optional migrations for each ORM; - redis support; -- different CI\CD templates; -- Kubernetes config generation. +- different CI\CD; +- Kubernetes config generation; +- Demo routers and models; +- Pre-commit integrations; +- Generated tests; +- Tests for the generator itself. This project can handle arguments passed through command line. ```shell $ python -m fastapi_template --help -usage: FastAPI template [-h] [--name PROJECT_NAME] +usage: FastAPI template [-h] [--version] [--name PROJECT_NAME] [--description PROJECT_DESCRIPTION] [--db {none,sqlite,mysql,postgresql}] - [--ci {none,gitlab,github}] [--redis] [--alembic] + [--orm {sqlalchemy,tortoise}] + [--ci {none,gitlab,github}] [--redis] [--migrations] [--kube] [--dummy] [--routers] [--swagger] [--force] optional arguments: -h, --help show this help message and exit + --version, -V Prints current version --name PROJECT_NAME Name of your awesome project --description PROJECT_DESCRIPTION Project description --db {none,sqlite,mysql,postgresql} Database + --orm {sqlalchemy,tortoise} + ORM --ci {none,gitlab,github} Choose CI support --redis Add redis support - --alembic Add alembic support + --migrations Add migrations support --kube Add kubernetes configs --dummy, --dummy-model Add dummy model diff --git a/fastapi_template/__main__.py b/fastapi_template/__main__.py index 818ed6eb28291e4530794fd58488d3945b6d6ac1..1ae52d796881b88e30f4fb2586c5563ce688b54c 100644 --- a/fastapi_template/__main__.py +++ b/fastapi_template/__main__.py @@ -1,3 +1,4 @@ +import json from pathlib import Path from cookiecutter.exceptions import FailedHookException, OutputDirExistsException @@ -19,7 +20,7 @@ def generate_project(context: BuilderContext) -> None: try: cookiecutter( template=f"{script_dir}/template", - extra_context=context.dict(), + extra_context=json.loads(context.json()), default_config=BuilderContext().dict(), no_input=True, overwrite_if_exists=context.force, @@ -42,5 +43,6 @@ def main() -> None: return generate_project(context) + if __name__ == "__main__": main() diff --git a/fastapi_template/cli.py b/fastapi_template/cli.py index ff9715fe5dc84ebcb7424df87c51d1a89c82304a..ac215033f19f0a02dcb43bb6bf3f3dd6241d2341 100644 --- a/fastapi_template/cli.py +++ b/fastapi_template/cli.py @@ -7,7 +7,14 @@ 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, DB_INFO, DatabaseType, CIType +from fastapi_template.input_model import ( + ORM, + BuilderContext, + DB_INFO, + DatabaseType, + CIType, +) +from importlib.metadata import version class SnakeCaseValidator(Validator): @@ -21,6 +28,9 @@ def parse_args(): parser = ArgumentParser( prog="FastAPI template", ) + parser.add_argument( + "--version", "-V", action="store_true", help="Prints current version" + ) parser.add_argument( "--name", type=str, @@ -42,6 +52,14 @@ def parse_args(): default=None, dest="db", ) + parser.add_argument( + "--orm", + help="ORM", + type=str, + choices=list(map(attrgetter("value"), ORM)), + default=None, + dest="orm", + ) parser.add_argument( "--ci", help="Choose CI support", @@ -58,11 +76,11 @@ def parse_args(): dest="enable_redis", ) parser.add_argument( - "--alembic", - help="Add alembic support", + "--migrations", + help="Add migrations support", action="store_true", default=None, - dest="enable_alembic", + dest="enable_migrations", ) parser.add_argument( "--kube", @@ -122,16 +140,15 @@ def ask_features(current_context: BuilderContext) -> BuilderContext: "name": "self_hosted_swagger", "value": current_context.self_hosted_swagger, }, - } if current_context.db != DatabaseType.none: - features["Alembic migrations"] = { - "name": "enable_alembic", - "value": current_context.enable_alembic, + features["Migrations support"] = { + "name": "enable_migrations", + "value": current_context.enable_migrations, } features["Add dummy model"] = { "name": "add_dummy", - "value": current_context.add_dummy + "value": current_context.add_dummy, } checkbox_values = [] for feature_name, feature in features.items(): @@ -156,7 +173,7 @@ def read_user_input(current_context: BuilderContext) -> BuilderContext: current_context.project_name = prompt( "Project name: ", validator=SnakeCaseValidator() ) - current_context.kube_name = current_context.project_name.replace('_', '-') + current_context.kube_name = current_context.project_name.replace("_", "-") if current_context.project_description is None: current_context.project_description = prompt("Project description: ") if current_context.db is None: @@ -168,8 +185,16 @@ def read_user_input(current_context: BuilderContext) -> BuilderContext: if current_context.db is None: raise KeyboardInterrupt() if current_context.db == DatabaseType.none: - current_context.enable_alembic = False + current_context.enable_migrations = False current_context.add_dummy = False + elif current_context.orm is None: + current_context.orm = radiolist_dialog( + "ORM", + text="Which ORM do you want?", + values=[(orm, orm.value) for orm in list(ORM)], + ).run() + if current_context.orm is None: + raise KeyboardInterrupt() if current_context.ci_type is None: current_context.ci_type = radiolist_dialog( "CI", @@ -184,6 +209,9 @@ def read_user_input(current_context: BuilderContext) -> BuilderContext: def get_context() -> BuilderContext: args = parse_args() + if args.version: + print(version("fastapi_template")) + exit(0) context = BuilderContext.from_orm(args) context = read_user_input(context) context.db_info = DB_INFO[context.db] diff --git a/fastapi_template/input_model.py b/fastapi_template/input_model.py index edd41cd032508a50648d3a76e29d94f695c717c9..3c5c3699a9059dfe2aaa042e28e5919a6979eb0e 100644 --- a/fastapi_template/input_model.py +++ b/fastapi_template/input_model.py @@ -18,37 +18,52 @@ class CIType(enum.Enum): gitlab_ci = "gitlab" github = "github" +@enum.unique +class ORM(enum.Enum): + sqlalchemy = "sqlalchemy" + tortoise = "tortoise" + class Database(BaseModel): name: str image: Optional[str] driver: Optional[str] + async_driver: Optional[str] port: Optional[int] + DB_INFO = { DatabaseType.none: Database( name="none", image=None, driver=None, + async_driver=None, port=None, ), DatabaseType.postgresql: Database( name=DatabaseType.postgresql.value, image="postgres:13.4-buster", - driver="postgresql+asyncpg", + async_driver="postgresql+asyncpg", + driver="postgres", port=5432, ), DatabaseType.mysql: Database( name=DatabaseType.mysql.value, image="bitnami/mysql:8.0.26", - driver="mysql+aiomysql", + async_driver="mysql+aiomysql", + driver="mysql", port=3306, ), DatabaseType.sqlite: Database( - name=DatabaseType.sqlite.value, image=None, driver="sqlite+aiosqlite", port=None + name=DatabaseType.sqlite.value, + image=None, + async_driver="sqlite+aiosqlite", + driver="sqlite", + port=None, ), } + class BuilderContext(BaseModel): """Options for project generation.""" @@ -59,7 +74,8 @@ class BuilderContext(BaseModel): db_info: Optional[Database] enable_redis: Optional[bool] ci_type: Optional[CIType] - enable_alembic: Optional[bool] + orm: Optional[ORM] + enable_migrations: Optional[bool] enable_kube: Optional[bool] enable_routers: Optional[bool] add_dummy: Optional[bool] = False diff --git a/fastapi_template/template/cookiecutter.json b/fastapi_template/template/cookiecutter.json index 6bdfe0fc1588b23c75a7b6222bf645800cbeb722..c1ea080328a38fb1621f01a418b96f3632bd2711 100644 --- a/fastapi_template/template/cookiecutter.json +++ b/fastapi_template/template/cookiecutter.json @@ -14,7 +14,7 @@ "ci_type": { "type": "string" }, - "enable_alembic": { + "enable_migrations": { "type": "bool" }, "enable_kube": { @@ -29,6 +29,9 @@ "add_dummy": { "type": "bool" }, + "orm": { + "type": "str" + }, "self_hosted_swagger": { "type": "bool" }, diff --git a/fastapi_template/template/hooks/post_gen_project.py b/fastapi_template/template/hooks/post_gen_project.py index 6165a08967e0b2fa4f26ef0565dba569394a2f27..76f2399dbd03c38208de675c7afac7fa425aa89a 100644 --- a/fastapi_template/template/hooks/post_gen_project.py +++ b/fastapi_template/template/hooks/post_gen_project.py @@ -6,16 +6,16 @@ import subprocess from pygit2 import init_repository from termcolor import cprint, colored +from pathlib import Path MANIFEST = "conditional_files.json" +REPLACE_MANIFEST = "replaceable_files.json" def delete_resource(resource): if os.path.isfile(resource): - print("removing file: {}".format(resource)) os.remove(resource) elif os.path.isdir(resource): - print("removing directory: {}".format(resource)) shutil.rmtree(resource) @@ -23,18 +23,39 @@ def delete_resources_for_disabled_features(): with open(MANIFEST) as manifest_file: manifest = json.load(manifest_file) for feature_name, feature in manifest.items(): - if feature['enabled'].lower() != "true": + if feature["enabled"].lower() != "true": text = "{} resources for disabled feature {}...".format( colored("Removing", color="red"), - colored(feature_name, color="magenta", attrs=['underline']) + colored(feature_name, color="magenta", attrs=["underline"]), ) print(text) - for resource in feature['resources']: + for resource in feature["resources"]: delete_resource(resource) delete_resource(MANIFEST) cprint("cleanup complete!", color="green") +def replace_resources(): + print( + "â Placing {} nicely in your {} â".format( + colored("resources", color="green"), colored("new project", color="blue") + ) + ) + with open(REPLACE_MANIFEST) as replace_manifest: + manifest = json.load(replace_manifest) + for target, replaces in manifest.items(): + target_path = Path(target) + delete_resource(target_path) + for src_file in map(Path, replaces): + if src_file.exists(): + shutil.move(src_file, target_path) + print( + "Resources are happy to be where {}.".format( + colored("they are needed the most", color="green", attrs=["underline"]) + ) + ) + + def init_repo(): repo_path = os.getcwd() repo = init_repository(repo_path) @@ -52,4 +73,5 @@ def init_repo(): if __name__ == "__main__": delete_resources_for_disabled_features() + replace_resources() init_repo() diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/.flake8 b/fastapi_template/template/{{cookiecutter.project_name}}/.flake8 index a4b9f062b448b9899cd0a55a17db83f2454d5351..e724cd56411e5ca7b8ccc630bad1aad59de94022 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/.flake8 +++ b/fastapi_template/template/{{cookiecutter.project_name}}/.flake8 @@ -72,6 +72,8 @@ per-file-ignores = WPS210, ; Found magic number WPS432, + ; Missing parameter(s) in Docstring + DAR101, ; all init files __init__.py: diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/README.md b/fastapi_template/template/{{cookiecutter.project_name}}/README.md index 8357b6c749e8e74d2e247bb664271b442f0346a8..1145b5ad12e6d3f2a14c63dd7da12523ddd046d1 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/README.md +++ b/fastapi_template/template/{{cookiecutter.project_name}}/README.md @@ -31,7 +31,7 @@ docker save --output {{cookiecutter.project_name}}.tar {{cookiecutter.project_na ``` {%- endif %} -{%- if cookiecutter.enable_alembic == 'True' %} +{%- if cookiecutter.enable_migrations == 'True' %} ## Migrations diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/aerich.ini b/fastapi_template/template/{{cookiecutter.project_name}}/aerich.ini new file mode 100644 index 0000000000000000000000000000000000000000..3ee2b52555c766f8aa17f13cbd5063903fedb411 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/aerich.ini @@ -0,0 +1,4 @@ +[aerich] +tortoise_orm = {{cookiecutter.project_name}}.db.config.TORTOISE_CONFIG +location = ./{{cookiecutter.project_name}}/db/migrations +src_folder = ./{{cookiecutter.project_name}} diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/conditional_files.json b/fastapi_template/template/{{cookiecutter.project_name}}/conditional_files.json index 0b3b44389f1a74731102dec5d0c0f8149b97f2d3..78ebd23812f7c9bde90a800095003ae7176912d0 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/conditional_files.json +++ b/fastapi_template/template/{{cookiecutter.project_name}}/conditional_files.json @@ -17,11 +17,12 @@ "Database support": { "enabled": "{{cookiecutter.db_info.name != 'none'}}", "resources": [ + "aerich.ini", + "alembic.ini", "{{cookiecutter.project_name}}/web/api/dummy", - "{{cookiecutter.project_name}}/db", + "{{cookiecutter.project_name}}/db_sa", "{{cookiecutter.project_name}}/tests/test_dummy.py", - "deploy/kube/db.yml", - "alembic.ini" + "deploy/kube/db.yml" ] }, "Postgres and MySQL support": { @@ -30,21 +31,23 @@ "deploy/kube/db.yml" ] }, - "Alembic": { - "enabled": "{{cookiecutter.enable_alembic}}", + "Migrations": { + "enabled": "{{cookiecutter.enable_migrations}}", "resources": [ + "aerich.ini", "alembic.ini", - "{{cookiecutter.project_name}}/db/migrations" + "{{cookiecutter.project_name}}/db_sa/migrations", + "{{cookiecutter.project_name}}/db_tortoise/migrations" ] }, "Gitlab CI": { - "enabled": "{{cookiecutter.ci_type == 'CIType.gitlab_ci'}}", + "enabled": "{{cookiecutter.ci_type == 'gitlab_ci'}}", "resources": [ ".gitlab-ci.yml" ] }, "Github CI": { - "enabled": "{{cookiecutter.ci_type == 'CIType.github'}}", + "enabled": "{{cookiecutter.ci_type == 'github'}}", "resources": [ ".github" ] @@ -64,10 +67,15 @@ "enabled": "{{cookiecutter.add_dummy}}", "resources": [ "{{cookiecutter.project_name}}/web/api/dummy", - "{{cookiecutter.project_name}}/db/dao", - "{{cookiecutter.project_name}}/db/models/dummy_model.py", + "{{cookiecutter.project_name}}/db_sa/dao", + "{{cookiecutter.project_name}}/db_sa/models/dummy_model.py", + "{{cookiecutter.project_name}}/db_tortoise/dao", + "{{cookiecutter.project_name}}/db_tortoise/models/dummy_model.py", "{{cookiecutter.project_name}}/tests/test_dummy.py", - "{{cookiecutter.project_name}}/db/migrations/versions/2021-08-16-16-55_2b7380507a71.py" + "{{cookiecutter.project_name}}/db_sa/migrations/versions/2021-08-16-16-55_2b7380507a71.py", + "{{cookiecutter.project_name}}/db_tortoise/migrations/models/1_20210928165300_init_dummy_pg.sql", + "{{cookiecutter.project_name}}/db_tortoise/migrations/models/1_20210928165300_init_dummy_mysql.sql", + "{{cookiecutter.project_name}}/db_tortoise/migrations/models/1_20210928165300_init_dummy_sqlite.sql" ] }, "Self-hosted swagger": { @@ -76,5 +84,40 @@ "{{cookiecutter.project_name}}/web/static", "{{cookiecutter.project_name}}/web/api/docs" ] + }, + "SQLAlchemy ORM": { + "enabled": "{{cookiecutter.orm == 'sqlalchemy'}}", + "resources": [ + "alembic.ini", + "{{cookiecutter.project_name}}/db_sa" + ] + }, + "Tortoise ORM": { + "enabled": "{{cookiecutter.orm == 'tortoise'}}", + "resources": [ + "aerich.ini", + "{{cookiecutter.project_name}}/db_tortoise" + ] + }, + "Postgresql DB": { + "enabled": "{{cookiecutter.db_info.name == 'postgresql'}}", + "resources": [ + "{{cookiecutter.project_name}}/db_tortoise/migrations/models/0_20210928165300_init_pg.sql", + "{{cookiecutter.project_name}}/db_tortoise/migrations/models/1_20210928165300_init_dummy_pg.sql" + ] + }, + "MySQL DB": { + "enabled": "{{cookiecutter.db_info.name == 'mysql'}}", + "resources": [ + "{{cookiecutter.project_name}}/db_tortoise/migrations/models/0_20210928165300_init_mysql.sql", + "{{cookiecutter.project_name}}/db_tortoise/migrations/models/1_20210928165300_init_dummy_mysql.sql" + ] + }, + "SQLite DB": { + "enabled": "{{cookiecutter.db_info.name == 'sqlite'}}", + "resources": [ + "{{cookiecutter.project_name}}/db_tortoise/migrations/models/0_20210928165300_init_sqlite.sql", + "{{cookiecutter.project_name}}/db_tortoise/migrations/models/1_20210928165300_init_dummy_sqlite.sql" + ] } } diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/deploy/docker-compose.yml b/fastapi_template/template/{{cookiecutter.project_name}}/deploy/docker-compose.yml index 80d1bf2634774e285f8a8bd69ab3001e0814efc6..c93d33a74d3e7656a52edbf77d32f5714028630b 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/deploy/docker-compose.yml +++ b/fastapi_template/template/{{cookiecutter.project_name}}/deploy/docker-compose.yml @@ -76,17 +76,21 @@ services: - "-p{{cookiecutter.project_name}}" - "-e" - "SELECT 1" - interval: 2s + interval: 3s timeout: 3s retries: 40 volumes: - {{cookiecutter.project_name}}-db-data:/bitnami/mysql/data {%- endif %} - {% if cookiecutter.enable_alembic == 'True' -%} + {% if cookiecutter.enable_migrations == 'True' -%} migrator: image: {{cookiecutter.project_name}}:{{"${" }}{{cookiecutter.project_name | upper }}_VERSION:-latest{{"}"}} + {%- if cookiecutter.orm == 'sqlalchemy' %} command: alembic upgrade head + {%- elif cookiecutter.orm == 'tortoise' %} + command: aerich upgrade + {%- endif %} {%- if cookiecutter.db_info.name == "sqlite" %} environment: {{cookiecutter.project_name | upper }}_DB_FILE: /db_data/db.sqlite3 diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/deploy/kube/db.yml b/fastapi_template/template/{{cookiecutter.project_name}}/deploy/kube/db.yml index cd337fbdaa8250f135c10f07564dc0f7883269fb..e488897fbce9eba8cfdb79abda87ec0530dc215d 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/deploy/kube/db.yml +++ b/fastapi_template/template/{{cookiecutter.project_name}}/deploy/kube/db.yml @@ -68,9 +68,14 @@ spec: - "180" - "{{cookiecutter.kube_name}}-db-service:{{cookiecutter.db_info.port}}" - "--" + {%- if cookiecutter.orm == 'sqlalchemy' %} - "alembic" - "upgrade" - "head" + {% elif cookiecutter.orm == 'tortoise' %} + - "aerich" + - "upgrade" + {%- endif %} env: - name: {{cookiecutter.project_name | upper }}_DB_HOST value: {{cookiecutter.kube_name}}-db-service diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/pyproject.toml b/fastapi_template/template/{{cookiecutter.project_name}}/pyproject.toml index 12490b7c7ab007dcaec55d7b18c5f90c2fc77e6f..42060ce7919d271f64784c272d3b85f1d2a85995 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/pyproject.toml +++ b/fastapi_template/template/{{cookiecutter.project_name}}/pyproject.toml @@ -16,20 +16,33 @@ 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" %} +{%- if cookiecutter.orm == "sqlalchemy" %} SQLAlchemy = {version = "^1.4", extras = ["mypy", "asyncio"]} +{%- elif cookiecutter.orm == "tortoise" %} +tortoise-orm = "^0.17.7" {%- endif %} -{%- if cookiecutter.enable_alembic == "True" %} +{%- if cookiecutter.enable_migrations == "True" %} +{%- if cookiecutter.orm == "sqlalchemy" %} alembic = "^1.6.5" +{%- elif cookiecutter.orm == "tortoise" %} +aerich = "^0.5.8" +{%- endif %} {%- endif %} {%- if cookiecutter.db_info.name == "postgresql" %} +{%- if cookiecutter.orm == "sqlalchemy" %} asyncpg = {version = "^0.24.0", extras = ["sa"]} +{%- elif cookiecutter.orm == "tortoise" %} +asyncpg = {version = "^0.24.0"} +{%- endif %} {%- endif %} {%- if cookiecutter.db_info.name == "sqlite" %} aiosqlite = "^0.17.0" {%- endif %} {%- if cookiecutter.db_info.name == "mysql" %} aiomysql = "^0.0.21" +{%- if cookiecutter.orm == "tortoise" %} +cryptography = "^3.4.8" +{%- endif %} {%- endif %} {%- if cookiecutter.enable_redis == "True" %} aioredis = {version = "^2.0.0", extras = ["hiredis"]} @@ -51,7 +64,7 @@ pre-commit = "^2.11.0" wemake-python-styleguide = "^0.15.3" black = "^21.7b0" autoflake = "^1.4" -{%- if cookiecutter.db_info.name != "none" %} +{%- if cookiecutter.orm == "sqlalchemy" %} SQLAlchemy = {version = "^1.4", extras = ["mypy"]} {%- endif %} pytest-cov = "^2.12.1" @@ -62,6 +75,10 @@ pytest-env = "^0.6.2" fakeredis = "^1.6.1" {%- endif %} requests = "^2.26.0" +{%- if cookiecutter.orm == "tortoise" %} +asynctest = "^0.13.0" +{%- endif %} + [tool.isort] profile = "black" @@ -76,11 +93,16 @@ pretty = true show_error_codes = true implicit_reexport = true allow_untyped_decorators = true -{%- if cookiecutter.db_info.name != "none" %} +warn_return_any = false +{%- if cookiecutter.orm == "sqlalchemy" %} plugins = ["sqlalchemy.ext.mypy.plugin"] {%- endif %} [tool.pytest.ini_options] +filterwarnings = [ + "error", + "ignore::DeprecationWarning", +] {%- if cookiecutter.db_info.name != "none" %} env = [ {%- if cookiecutter.db_info.name == "sqlite" %} diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/replaceable_files.json b/fastapi_template/template/{{cookiecutter.project_name}}/replaceable_files.json new file mode 100644 index 0000000000000000000000000000000000000000..81b49bae4d227d1bfdc86b195395acfa56f94606 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/replaceable_files.json @@ -0,0 +1,6 @@ +{ + "{{cookiecutter.project_name}}/db": [ + "{{cookiecutter.project_name}}/db_sa", + "{{cookiecutter.project_name}}/db_tortoise" + ] +} 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 index 61da6c4206b6e88a425c147574ce6985bfe8f126..8ab5548c6b685250b9afc5297cb40ecd6adbba6f 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/conftest.py +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/conftest.py @@ -1,6 +1,7 @@ import asyncio +from asyncio.events import AbstractEventLoop import sys -from typing import Any, Generator, AsyncGenerator +from typing import Generator, AsyncGenerator import pytest from fastapi import FastAPI @@ -11,14 +12,22 @@ from {{cookiecutter.project_name}}.services.redis.dependency import get_redis_co {%- endif %} from {{cookiecutter.project_name}}.settings import settings +from {{cookiecutter.project_name}}.web.application import get_app + {%- if cookiecutter.db_info.name != "none" %} +{%- if cookiecutter.orm == "sqlalchemy" %} from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, AsyncEngine, AsyncConnection from sqlalchemy.orm import sessionmaker from {{cookiecutter.project_name}}.db.dependencies import get_db_session from {{cookiecutter.project_name}}.db.utils import create_database, drop_database +{%- elif cookiecutter.orm == "tortoise" %} +from tortoise.contrib.test import finalizer, initializer, _restore_default # noqa: WPS450 +from tortoise import Tortoise +from {{cookiecutter.project_name}}.db.config import MODELS_MODULES {%- endif %} -from {{cookiecutter.project_name}}.web.application import get_app +{%- endif %} + import nest_asyncio nest_asyncio.apply() @@ -44,8 +53,8 @@ def event_loop() -> Generator[asyncio.AbstractEventLoop, None, None]: yield loop loop.close() - {%- if cookiecutter.db_info.name != "none" %} +{%- if cookiecutter.orm == "sqlalchemy" %} @pytest.fixture(scope="session") @pytest.mark.asyncio async def _engine() -> AsyncGenerator[AsyncEngine, None]: @@ -113,6 +122,47 @@ async def transaction(_engine: AsyncEngine) -> AsyncGenerator[AsyncConnection, N yield conn finally: await conn.rollback() +{%- elif cookiecutter.orm == "tortoise" %} + +@pytest.fixture(scope="session", autouse=True) +def initialize_db(event_loop: AbstractEventLoop) -> Generator[None, None, None]: + """ + Initialize models and database. + + :param event_loop: Session-wide event loop. + :yields: Nothing. + """ + initializer( + MODELS_MODULES, + db_url=str(settings.db_url), + app_label="models", + loop=event_loop, + ) + + yield + + finalizer() + +@pytest.fixture(autouse=True) +@pytest.mark.asyncio +async def clean_db() -> AsyncGenerator[None, None]: + """ + Removes all data from database after test. + + :yields: Nothing. + """ + yield + + _restore_default() + for app in Tortoise.apps.values(): + for model in app.values(): + meta = model._meta # noqa: WPS437 + quote_char = meta.db.query_class._builder().QUOTE_CHAR # noqa: WPS437 + await meta.db.execute_script( + f"DELETE FROM {quote_char}{meta.db_table}{quote_char}" # noqa: S608 + ) + +{%- endif %} {%- endif %} @@ -129,7 +179,7 @@ def fake_redis() -> FakeRedis: @pytest.fixture() def fastapi_app( - {%- if cookiecutter.db_info.name != "none" %} + {%- if cookiecutter.db_info.name != "none" and cookiecutter.orm == "sqlalchemy" %} dbsession: AsyncSession, {%- endif %} {% if cookiecutter.enable_redis == "True" -%} @@ -139,16 +189,10 @@ def fastapi_app( """ Fixture for creating FastAPI app. - {% if cookiecutter.db_info.name != "none" -%} - :param dbsession: test db session. - {% endif -%} - {% if cookiecutter.enable_redis == "True" -%} - :param fake_redis: Fake redis instance. - {% endif -%} :return: fastapi app with mocked dependencies. """ application = get_app() - {%- if cookiecutter.db_info.name != "none" %} + {% if cookiecutter.db_info.name != "none" and cookiecutter.orm == "sqlalchemy" -%} application.dependency_overrides[get_db_session] = lambda: dbsession {%- endif %} {%- if cookiecutter.enable_redis == "True" %} 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_sa/base.py similarity index 100% rename from fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/base.py rename to fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_sa/base.py 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_sa/dao/__init__.py similarity index 100% rename from fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/dao/__init__.py rename to fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_sa/dao/__init__.py 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_sa/dao/dummy_dao.py similarity index 85% rename from fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/dao/dummy_dao.py rename to fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_sa/dao/dummy_dao.py index 03a51db95cf89500a8c3d34608b1b512b5479753..4bc6ca5dcfe2acdc1883c77eab92bcdca2dc1097 100644 --- 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_sa/dao/dummy_dao.py @@ -1,7 +1,7 @@ from typing import List, Optional from fastapi import Depends from sqlalchemy import select -from sqlalchemy.ext.asyncio import AsyncScalarResult, AsyncSession +from sqlalchemy.ext.asyncio import AsyncSession from {{cookiecutter.project_name}}.db.dependencies import get_db_session from {{cookiecutter.project_name}}.db.models.dummy_model import DummyModel @@ -13,7 +13,7 @@ class DummyDAO: def __init__(self, session: AsyncSession = Depends(get_db_session)): self.session = session - def create_dummy_model(self, name: str) -> None: + async def create_dummy_model(self, name: str) -> None: """ Add single dummy to session. @@ -21,7 +21,7 @@ class DummyDAO: """ self.session.add(DummyModel(name=name)) - async def get_all_dummies(self, limit: int, offset: int) -> AsyncScalarResult: + async def get_all_dummies(self, limit: int, offset: int) -> List[DummyModel]: """ Get all dummy models with limit/offset pagination. @@ -29,11 +29,11 @@ class DummyDAO: :param offset: offset of dummies. :return: stream of dummies. """ - raw_stream = await self.session.stream( + raw_dummies = await self.session.execute( select(DummyModel).limit(limit).offset(offset), ) - return raw_stream.scalars() + return raw_dummies.scalars().fetchall() async def filter( self, 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_sa/dependencies.py similarity index 100% rename from fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/dependencies.py rename to fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_sa/dependencies.py 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_sa/meta.py similarity index 100% rename from fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/meta.py rename to fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_sa/meta.py 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_sa/migrations/__init__.py similarity index 100% rename from fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/migrations/__init__.py rename to fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_sa/migrations/__init__.py 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_sa/migrations/env.py similarity index 100% rename from fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/migrations/env.py rename to fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_sa/migrations/env.py diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/migrations/script.py.mako b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_sa/migrations/script.py.mako similarity index 100% rename from fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/migrations/script.py.mako rename to fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_sa/migrations/script.py.mako 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_sa/migrations/versions/2021-08-16-16-53_819cbf6e030b.py similarity index 100% rename from fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/migrations/versions/2021-08-16-16-53_819cbf6e030b.py rename to fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_sa/migrations/versions/2021-08-16-16-53_819cbf6e030b.py 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_sa/migrations/versions/2021-08-16-16-55_2b7380507a71.py similarity index 100% rename from fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/migrations/versions/2021-08-16-16-55_2b7380507a71.py rename to fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_sa/migrations/versions/2021-08-16-16-55_2b7380507a71.py diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/migrations/versions/__init__.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_sa/migrations/versions/__init__.py similarity index 100% rename from fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/migrations/versions/__init__.py rename to fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_sa/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_sa/models/__init__.py similarity index 100% rename from fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/models/__init__.py rename to fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_sa/models/__init__.py 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_sa/models/dummy_model.py similarity index 100% rename from fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/models/dummy_model.py rename to fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_sa/models/dummy_model.py diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/utils.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_sa/utils.py similarity index 100% rename from fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/utils.py rename to fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_sa/utils.py diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_tortoise/config.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_tortoise/config.py new file mode 100644 index 0000000000000000000000000000000000000000..e7e37372c4c579e61873687728bebc2d38156d0c --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_tortoise/config.py @@ -0,0 +1,16 @@ +from typing import List +from {{cookiecutter.project_name}}.settings import settings + +MODELS_MODULES: List[str] = [{%- if cookiecutter.add_dummy == 'True' %}"{{cookiecutter.project_name}}.db.models.dummy_model"{%- endif %}] # noqa: WPS407 + +TORTOISE_CONFIG = { # noqa: WPS407 + "connections": { + "default": str(settings.db_url), + }, + "apps": { + "models": { + "models": ["aerich.models"] + MODELS_MODULES, + "default_connection": "default", + }, + }, +} diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_tortoise/dao/__init__.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_tortoise/dao/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..db62a0afb2db870dce3368e05380185d0e71bbdc --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_tortoise/dao/__init__.py @@ -0,0 +1 @@ +"""DAO classes.""" diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_tortoise/dao/dummy_dao.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_tortoise/dao/dummy_dao.py new file mode 100644 index 0000000000000000000000000000000000000000..7190725d9dfbc92584db532fb7cbe61bd9fff5d4 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_tortoise/dao/dummy_dao.py @@ -0,0 +1,38 @@ +from {{cookiecutter.project_name}}.db.models.dummy_model import DummyModel +from typing import List, Optional + + +class DummyDAO: + """Class for accessing dummy table.""" + + async def create_dummy_model(self, name: str) -> None: + """ + Add single dummy to session. + + :param name: name of a dummy. + """ + await DummyModel.create(name=name) + + async def get_all_dummies(self, limit: int, offset: int) -> List[DummyModel]: + """ + Get all dummy models with limit/offset pagination. + + :param limit: limit of dummies. + :param offset: offset of dummies. + :return: stream of dummies. + """ + return ( + await DummyModel.all().offset(offset).limit(limit) + ) + + async def filter(self, name: Optional[str] = None) -> List[DummyModel]: + """ + Get specific dummy model. + + :param name: name of dummy instance. + :return: dummy models. + """ + query = DummyModel.all() + if name: + query = query.filter(name=name) + return await query diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_tortoise/migrations/models/0_20210928165300_init_mysql.sql b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_tortoise/migrations/models/0_20210928165300_init_mysql.sql new file mode 100644 index 0000000000000000000000000000000000000000..5bc531c8d7044c27129d18bd1af02b5908fd35a4 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_tortoise/migrations/models/0_20210928165300_init_mysql.sql @@ -0,0 +1,7 @@ +-- upgrade -- +CREATE TABLE IF NOT EXISTS `aerich` ( + `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT, + `version` VARCHAR(255) NOT NULL, + `app` VARCHAR(20) NOT NULL, + `content` JSON NOT NULL +) CHARACTER SET utf8mb4; diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_tortoise/migrations/models/0_20210928165300_init_pg.sql b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_tortoise/migrations/models/0_20210928165300_init_pg.sql new file mode 100644 index 0000000000000000000000000000000000000000..6b1ec932a049be5f9eec6e3274ea7490ac364ea3 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_tortoise/migrations/models/0_20210928165300_init_pg.sql @@ -0,0 +1,7 @@ +-- upgrade -- +CREATE TABLE IF NOT EXISTS "aerich" ( + "id" SERIAL NOT NULL PRIMARY KEY, + "version" VARCHAR(255) NOT NULL, + "app" VARCHAR(20) NOT NULL, + "content" JSONB NOT NULL +); diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_tortoise/migrations/models/0_20210928165300_init_sqlite.sql b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_tortoise/migrations/models/0_20210928165300_init_sqlite.sql new file mode 100644 index 0000000000000000000000000000000000000000..864443340fdb76b2264b0502c97e20b7530a1ca9 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_tortoise/migrations/models/0_20210928165300_init_sqlite.sql @@ -0,0 +1,7 @@ +-- upgrade -- +CREATE TABLE IF NOT EXISTS "aerich" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + "version" VARCHAR(255) NOT NULL, + "app" VARCHAR(20) NOT NULL, + "content" JSON NOT NULL +); diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_tortoise/migrations/models/1_20210928165300_init_dummy_mysql.sql b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_tortoise/migrations/models/1_20210928165300_init_dummy_mysql.sql new file mode 100644 index 0000000000000000000000000000000000000000..ea5fa9efd741c44173f8312497fa3f1b4ce3cf60 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_tortoise/migrations/models/1_20210928165300_init_dummy_mysql.sql @@ -0,0 +1,5 @@ +-- upgrade -- +CREATE TABLE IF NOT EXISTS `dummymodel` ( + `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT, + `name` VARCHAR(200) NOT NULL +) CHARACTER SET utf8mb4 COMMENT='Model for demo purpose.'; diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_tortoise/migrations/models/1_20210928165300_init_dummy_pg.sql b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_tortoise/migrations/models/1_20210928165300_init_dummy_pg.sql new file mode 100644 index 0000000000000000000000000000000000000000..b84401de7104ec7335ec4af92dd5c118b4e42fc0 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_tortoise/migrations/models/1_20210928165300_init_dummy_pg.sql @@ -0,0 +1,6 @@ +-- upgrade -- +CREATE TABLE IF NOT EXISTS "dummymodel" ( + "id" SERIAL NOT NULL PRIMARY KEY, + "name" VARCHAR(200) NOT NULL +); +COMMENT ON TABLE "dummymodel" IS 'Model for demo purpose.'; diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_tortoise/migrations/models/1_20210928165300_init_dummy_sqlite.sql b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_tortoise/migrations/models/1_20210928165300_init_dummy_sqlite.sql new file mode 100644 index 0000000000000000000000000000000000000000..23fc3e57924bf4b2aff832fbe80c8362bca86a96 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_tortoise/migrations/models/1_20210928165300_init_dummy_sqlite.sql @@ -0,0 +1,5 @@ +-- upgrade -- +CREATE TABLE IF NOT EXISTS "dummymodel" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + "name" VARCHAR(200) NOT NULL +) /* Model for demo purpose. */; diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_tortoise/models/__init__.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_tortoise/models/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f115875461f131eedb1e3258331b68e5ae79cb6e --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_tortoise/models/__init__.py @@ -0,0 +1 @@ +"""Models for {{cookiecutter.project_name}}.""" diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_tortoise/models/dummy_model.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_tortoise/models/dummy_model.py new file mode 100644 index 0000000000000000000000000000000000000000..495cb8eb7157de1463ef12db2882e9c70d732b32 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_tortoise/models/dummy_model.py @@ -0,0 +1,11 @@ +from tortoise import models, fields + + +class DummyModel(models.Model): + """Model for demo purpose.""" + + id = fields.IntField(pk=True) + name = fields.CharField(max_length=200) # noqa: WPS432 + + def __str__(self) -> str: + return self.name 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 index 11d9a820bb736a19559abc2b4910a730d18f75d8..89e199ca245de0c23de097fe615863802439af58 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/settings.py +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/settings.py @@ -49,12 +49,20 @@ class Settings(BaseSettings): """ {%- if cookiecutter.db_info.name == "sqlite" %} return URL.build( + {%- if cookiecutter.orm == "sqlalchemy" %} + scheme="{{cookiecutter.db_info.async_driver}}", + {%- elif cookiecutter.orm == "tortoise" %} scheme="{{cookiecutter.db_info.driver}}", + {%- endif %} path=f"///{self.db_file}" ) - {% else %} + {%- else %} return URL.build( + {%- if cookiecutter.orm == "sqlalchemy" %} + scheme="{{cookiecutter.db_info.async_driver}}", + {%- elif cookiecutter.orm == "tortoise" %} scheme="{{cookiecutter.db_info.driver}}", + {%- endif %} host=self.db_host, port=self.db_port, user=self.db_user, diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/tests/test_dummy.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/tests/test_dummy.py index 0d7e71eef546e990b4dfe0eb00c776ea0230036c..bd518fe490cb15372a5ecf463988361d4ff772f5 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/tests/test_dummy.py +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/tests/test_dummy.py @@ -2,9 +2,7 @@ import uuid import pytest from fastapi.testclient import TestClient from fastapi import FastAPI -from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy.sql.functions import count from starlette import status from {{cookiecutter.project_name}}.db.models.dummy_model import DummyModel from {{cookiecutter.project_name}}.db.dao.dummy_dao import DummyDAO @@ -13,26 +11,22 @@ from {{cookiecutter.project_name}}.db.dao.dummy_dao import DummyDAO async def test_creation( fastapi_app: FastAPI, client: TestClient, - dbsession: AsyncSession + {%- if cookiecutter.orm == "sqlalchemy" %} + dbsession: AsyncSession, + {%- endif %} ) -> None: - """ - Tests dummy instance creation. - - :param fastapi_app: current app fixture. - :param client: client for app. - :param dbsession: current db session. - """ + """Tests dummy instance creation.""" url = fastapi_app.url_path_for('create_dummy_model') test_name = uuid.uuid4().hex response = client.put(url, json={ "name": test_name }) assert response.status_code == status.HTTP_200_OK - instance_count = await dbsession.scalar( - select(count()).select_from(DummyModel) - ) - assert instance_count == 1 + {%- if cookiecutter.orm == "sqlalchemy" %} dao = DummyDAO(dbsession) + {%- elif cookiecutter.orm == "tortoise" %} + dao = DummyDAO() + {%- endif %} instances = await dao.filter(name=test_name) assert instances[0].name == test_name @@ -41,19 +35,18 @@ async def test_creation( async def test_getting( fastapi_app: FastAPI, client: TestClient, - dbsession: AsyncSession + {%- if cookiecutter.orm == "sqlalchemy" %} + dbsession: AsyncSession, + {%- endif %} ) -> None: - """ - Tests dummy instance retrieval. - - :param fastapi_app: current app fixture. - :param client: client for app. - :param dbsession: current db session. - """ + """Tests dummy instance retrieval.""" + {%- if cookiecutter.orm == "sqlalchemy" %} dao = DummyDAO(dbsession) + {%- elif cookiecutter.orm == "tortoise" %} + dao = DummyDAO() + {%- endif %} test_name = uuid.uuid4().hex - dao.create_dummy_model(name=test_name) - await dbsession.commit() + await dao.create_dummy_model(name=test_name) url = fastapi_app.url_path_for('get_dummy_models') response = client.get(url) @@ -62,4 +55,3 @@ async def test_getting( assert len(response_json) == 1 assert response_json[0]['name'] == test_name - 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 index 44662fd5671736b1313015f52870b1fc416ae235..80fef78d99f27c13a115e726e55c649fb403bc02 100644 --- 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 @@ -1,9 +1,10 @@ -from typing import Any, List +from typing import 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}}.db.models.dummy_model import DummyModel from {{cookiecutter.project_name}}.web.api.dummy.schema import DummyModelDTO, DummyModelInputDTO router = APIRouter() @@ -14,7 +15,7 @@ async def get_dummy_models( limit: int = 10, offset: int = 0, dummy_dao: DummyDAO = Depends(), -) -> Any: +) -> List[DummyModel]: """ Retrieve all dummy objects from database. @@ -23,12 +24,11 @@ async def get_dummy_models( :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() + return await dummy_dao.get_all_dummies(limit=limit, offset=offset) @router.put("/") -def create_dummy_model( +async def create_dummy_model( new_dummy_object: DummyModelInputDTO, dummy_dao: DummyDAO = Depends(), ) -> None: @@ -38,4 +38,4 @@ def create_dummy_model( :param new_dummy_object: new dummy model item. :param dummy_dao: DAO for dummy models. """ - dummy_dao.create_dummy_model(**new_dummy_object.dict()) + await dummy_dao.create_dummy_model(**new_dummy_object.dict()) 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 index 1f3d48618008e22c954583351d2496cd81c8dce0..0731def605363499d2e7815e5bf8353299c60082 100644 --- 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 @@ -4,10 +4,17 @@ from {{cookiecutter.project_name}}.web.api.router import api_router from {{cookiecutter.project_name}}.web.lifetime import shutdown, startup from importlib import metadata +{%- if cookiecutter.orm == 'tortoise' %} +from tortoise.contrib.fastapi import register_tortoise +from {{cookiecutter.project_name}}.db.config import TORTOISE_CONFIG +{%- endif %} + + {%- if cookiecutter.self_hosted_swagger == 'True' %} from fastapi.staticfiles import StaticFiles from pathlib import Path + APP_ROOT = Path(__file__).parent.parent {%- endif %} @@ -47,4 +54,8 @@ def get_app() -> FastAPI: ) {% endif %} + {%- if cookiecutter.orm == 'tortoise' %} + register_tortoise(app, config=TORTOISE_CONFIG, add_exception_handlers=True) + {%- endif %} + 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 index 22ac7be7815f233364969417591518c2a8d266db..b69107afceeecb8e4a274eb69b89822ab3845b0c 100644 --- 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 @@ -8,7 +8,7 @@ from {{cookiecutter.project_name}}.settings import settings import aioredis {%- endif %} -{%- if cookiecutter.db_info.name != "none" %} +{%- if cookiecutter.db_info.name != "none" and cookiecutter.orm == "sqlalchemy" %} from asyncio import current_task from sqlalchemy.ext.asyncio import ( AsyncSession, @@ -16,10 +16,8 @@ from sqlalchemy.ext.asyncio import ( 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. @@ -63,7 +61,7 @@ def startup(app: FastAPI) -> Callable[[], Awaitable[None]]: """ async def _startup() -> None: # noqa: WPS430 - {%- if cookiecutter.db_info.name != "none" %} + {%- if cookiecutter.db_info.name != "none" and cookiecutter.orm == "sqlalchemy" %} _setup_db(app) {%- endif %} {%- if cookiecutter.enable_redis == "True" %} @@ -83,7 +81,7 @@ def shutdown(app: FastAPI) -> Callable[[], Awaitable[None]]: """ async def _shutdown() -> None: # noqa: WPS430 - {%- if cookiecutter.db_info.name != "none" %} + {%- if cookiecutter.db_info.name != "none" and cookiecutter.orm == "sqlalchemy" %} await app.state.db_engine.dispose() {%- endif %} {%- if cookiecutter.enable_redis == "True" %} diff --git a/fastapi_template/tests/conftest.py b/fastapi_template/tests/conftest.py index 320dbaa3626eff1e847e77e14ea212664e3c53a4..b54a50de81ccd2dde473405fc98c34f5dab8c8ff 100644 --- a/fastapi_template/tests/conftest.py +++ b/fastapi_template/tests/conftest.py @@ -5,7 +5,7 @@ import tempfile import shutil from faker import Faker from pathlib import Path -from fastapi_template.input_model import BuilderContext, CIType +from fastapi_template.input_model import DB_INFO, BuilderContext, CIType, DatabaseType from fastapi_template.tests.utils import run_docker_compose_command @@ -18,7 +18,7 @@ def project_name() -> str: """ fake = Faker() raw_name: str = fake.name_female() - return raw_name.lower().replace(" ", "_").replace("-", "_") + return raw_name.lower().replace(" ", "_").replace("-", "_").replace(".", "_") @pytest.fixture(scope="session", autouse=True) @@ -39,7 +39,7 @@ def generator_start_dir() -> str: @pytest.fixture() -def defautl_context(project_name: str) -> None: +def default_context(project_name: str) -> None: """ Default builder context without features. @@ -51,11 +51,13 @@ def defautl_context(project_name: str) -> None: kube_name=project_name.replace("_", "-"), project_description="Generated by pytest.", ci_type=CIType.none, + db=DatabaseType.none, + db_info=DB_INFO[DatabaseType.none], enable_redis=False, - enable_alembic=True, + enable_migrations=False, enable_kube=False, enable_routers=True, - add_dummy=True, + add_dummy=False, self_hosted_swagger=False, force=True, ) @@ -87,7 +89,7 @@ def default_dir(generator_start_dir: str) -> None: os.chdir(generator_start_dir) -@pytest.fixture(scope="module", autouse=True) +@pytest.fixture(autouse=True) def docker_module_shutdown(generator_start_dir: str, project_name: str) -> None: """ Cleans up docker context. @@ -98,6 +100,8 @@ def docker_module_shutdown(generator_start_dir: str, project_name: str) -> None: yield cwd = os.getcwd() project_dir = Path(generator_start_dir) / project_name + if not project_dir.exists(): + return os.chdir(project_dir) run_docker_compose_command("down -v") os.chdir(cwd) @@ -114,6 +118,8 @@ def docker_shutdown(generator_start_dir: str, project_name: str) -> None: yield cwd = os.getcwd() project_dir = Path(generator_start_dir) / project_name + if not project_dir.exists(): + return os.chdir(project_dir) run_docker_compose_command("down -v --rmi=all") os.chdir(cwd) diff --git a/fastapi_template/tests/test_generator.py b/fastapi_template/tests/test_generator.py new file mode 100644 index 0000000000000000000000000000000000000000..8fc80c4009d55862b8ce9858d1b74cf986172f03 --- /dev/null +++ b/fastapi_template/tests/test_generator.py @@ -0,0 +1,66 @@ +from typing import Optional +from fastapi_template.tests.utils import run_default_check +import pytest + +from fastapi_template.input_model import ORM, BuilderContext, DatabaseType, DB_INFO + + +def init_context( + context: BuilderContext, db: DatabaseType, orm: Optional[ORM] +) -> BuilderContext: + context.db = db + context.db_info = DB_INFO[db] + context.orm = orm + + context.enable_migrations = db != DatabaseType.none + context.add_dummy = db != DatabaseType.none + + return context + + +def test_default_without_db(default_context: BuilderContext): + run_default_check(init_context(default_context, DatabaseType.none, None)) + + +@pytest.mark.parametrize( + "db", + [ + DatabaseType.postgresql, + DatabaseType.sqlite, + DatabaseType.mysql, + ], +) +@pytest.mark.parametrize("orm", [ORM.sqlalchemy, ORM.tortoise]) +def test_default_with_db(default_context: BuilderContext, db: DatabaseType, orm: ORM): + run_default_check(init_context(default_context, db, orm)) + + +@pytest.mark.parametrize("orm", [ORM.sqlalchemy, ORM.tortoise]) +def test_without_routers(default_context: BuilderContext, orm: ORM): + context = init_context(default_context, DatabaseType.postgresql, orm) + context.enable_routers = False + run_default_check(context) + + +@pytest.mark.parametrize("orm", [ORM.sqlalchemy, ORM.tortoise]) +def test_without_migrations(default_context: BuilderContext, orm: ORM): + context = init_context(default_context, DatabaseType.postgresql, orm) + context.enable_migrations = False + run_default_check(context) + + +def test_with_selfhosted_swagger(default_context: BuilderContext): + default_context.self_hosted_swagger = True + run_default_check(default_context) + + +@pytest.mark.parametrize("orm", [ORM.sqlalchemy, ORM.tortoise]) +def test_without_dummy(default_context: BuilderContext, orm: ORM): + context = init_context(default_context, DatabaseType.postgresql, orm) + context.add_dummy = False + run_default_check(context) + + +def test_redis(default_context: BuilderContext): + default_context.enable_redis = True + run_default_check(default_context) diff --git a/fastapi_template/tests/test_mysql.py b/fastapi_template/tests/test_mysql.py deleted file mode 100644 index ae546719185bccaa5e5760c0438eae6965b4a0cd..0000000000000000000000000000000000000000 --- a/fastapi_template/tests/test_mysql.py +++ /dev/null @@ -1,47 +0,0 @@ -from fastapi_template.tests.utils import run_default_check -import pytest - -from fastapi_template.input_model import BuilderContext, DatabaseType, DB_INFO - - -@pytest.fixture() -def mysql_context(defautl_context: BuilderContext) -> BuilderContext: - defautl_context.db = DatabaseType.mysql - defautl_context.db_info = DB_INFO[DatabaseType.mysql] - - return defautl_context - - -@pytest.mark.mysql -def test_default_mysql(mysql_context: BuilderContext): - run_default_check(mysql_context) - - -@pytest.mark.mysql -def test_mysql_without_routers(mysql_context: BuilderContext): - mysql_context.enable_routers = False - run_default_check(mysql_context) - - -@pytest.mark.mysql -def test_mysql_without_alembic(mysql_context: BuilderContext): - mysql_context.enable_alembic = False - run_default_check(mysql_context) - - -@pytest.mark.mysql -def test_mysql_with_selfhosted_swagger(mysql_context: BuilderContext): - mysql_context.self_hosted_swagger = True - run_default_check(mysql_context) - - -@pytest.mark.mysql -def test_mysql_without_dummy(mysql_context: BuilderContext): - mysql_context.add_dummy = False - run_default_check(mysql_context) - - -@pytest.mark.mysql -def test_mysql_and_redis(mysql_context: BuilderContext): - mysql_context.enable_redis = True - run_default_check(mysql_context) diff --git a/fastapi_template/tests/test_postgres.py b/fastapi_template/tests/test_postgres.py deleted file mode 100644 index 8fbc28f0b2c68e058a1a8d1556f9fe20b3bb0f6c..0000000000000000000000000000000000000000 --- a/fastapi_template/tests/test_postgres.py +++ /dev/null @@ -1,47 +0,0 @@ -from fastapi_template.tests.utils import run_default_check -import pytest - -from fastapi_template.input_model import BuilderContext, DatabaseType, DB_INFO - - -@pytest.fixture() -def pg_context(defautl_context: BuilderContext) -> BuilderContext: - defautl_context.db = DatabaseType.postgresql - defautl_context.db_info = DB_INFO[DatabaseType.postgresql] - - return defautl_context - - -@pytest.mark.pg -def test_default_pg(pg_context: BuilderContext): - run_default_check(pg_context) - - -@pytest.mark.pg -def test_pg_without_routers(pg_context: BuilderContext): - pg_context.enable_routers = False - run_default_check(pg_context) - - -@pytest.mark.pg -def test_pg_without_alembic(pg_context: BuilderContext): - pg_context.enable_alembic = False - run_default_check(pg_context) - - -@pytest.mark.pg -def test_pg_with_selfhosted_swagger(pg_context: BuilderContext): - pg_context.self_hosted_swagger = True - run_default_check(pg_context) - - -@pytest.mark.pg -def test_pg_without_dummy(pg_context: BuilderContext): - pg_context.add_dummy = False - run_default_check(pg_context) - - -@pytest.mark.pg -def test_pg_and_redis(pg_context: BuilderContext): - pg_context.enable_redis = True - run_default_check(pg_context) diff --git a/fastapi_template/tests/test_sqlite.py b/fastapi_template/tests/test_sqlite.py deleted file mode 100644 index e7c51453605e16cbbda88463e83c61f477af805e..0000000000000000000000000000000000000000 --- a/fastapi_template/tests/test_sqlite.py +++ /dev/null @@ -1,47 +0,0 @@ -from fastapi_template.tests.utils import run_default_check -import pytest - -from fastapi_template.input_model import BuilderContext, DatabaseType, DB_INFO - - -@pytest.fixture() -def sqlite_context(defautl_context: BuilderContext) -> BuilderContext: - defautl_context.db = DatabaseType.sqlite - defautl_context.db_info = DB_INFO[DatabaseType.sqlite] - - return defautl_context - - -@pytest.mark.sqlite -def test_default_sqlite(sqlite_context: BuilderContext): - run_default_check(sqlite_context) - - -@pytest.mark.sqlite -def test_sqlite_without_routers(sqlite_context: BuilderContext): - sqlite_context.enable_routers = False - run_default_check(sqlite_context) - - -@pytest.mark.sqlite -def test_sqlite_without_alembic(sqlite_context: BuilderContext): - sqlite_context.enable_alembic = False - run_default_check(sqlite_context) - - -@pytest.mark.sqlite -def test_sqlite_with_selfhosted_swagger(sqlite_context: BuilderContext): - sqlite_context.self_hosted_swagger = True - run_default_check(sqlite_context) - - -@pytest.mark.sqlite -def test_sqlite_without_dummy(sqlite_context: BuilderContext): - sqlite_context.add_dummy = False - run_default_check(sqlite_context) - - -@pytest.mark.sqlite -def test_sqlite_and_redis(sqlite_context: BuilderContext): - sqlite_context.enable_redis = True - run_default_check(sqlite_context) diff --git a/pyproject.toml b/pyproject.toml index 1257b5784f77c27780bb71b83b5c78d9efe7dc98..b944f93dad59a32e431060d75f72332becfe0004 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "fastapi_template" -version = "2.5.0" +version = "3.0.0" description = "Feature-rich robust FastAPI template" authors = ["Pavel Kirilin <win10@list.ru>"] packages = [{ include = "fastapi_template" }]