From 2f36891bc502ce9999ddc0065f5cc0a3c120709b Mon Sep 17 00:00:00 2001 From: Pavel Kirilin <win10@list.ru> Date: Mon, 27 Sep 2021 18:47:58 +0400 Subject: [PATCH] Added tests for generated project. (#16) * Added tests for generated project. * Added tests for template generator. * Bumped version. Closes #15, #7. --- .github/workflows/test.yml | 19 + .gitignore | 5 + fastapi_template/__main__.py | 21 +- fastapi_template/cli.py | 32 +- fastapi_template/input_model.py | 23 + .../{{cookiecutter.project_name}}/.env | 5 + .../{{cookiecutter.project_name}}/.flake8 | 28 +- .../.github/workflows/tests.yml | 2 +- .../.gitlab-ci.yml | 4 +- .../.pre-commit-config.yaml | 119 ++- .../{{cookiecutter.project_name}}/README.md | 44 +- .../conditional_files.json | 9 +- .../deploy/Dockerfile | 2 +- .../deploy/docker-compose.yml | 91 ++- .../pyproject.toml | 23 +- .../{{cookiecutter.project_name}}/conftest.py | 142 +++- .../db/dao/dummy_dao.py | 19 +- .../db/migrations/env.py | 5 +- .../{{cookiecutter.project_name}}/db/utils.py | 87 +++ .../{{cookiecutter.project_name}}/settings.py | 2 +- .../tests/test_dummy.py | 65 ++ .../tests/test_echo.py | 21 + .../tests/test_redis.py | 57 ++ .../test_{{cookiecutter.project_name}}.py | 17 +- .../web/api/docs/views.py | 23 +- .../web/api/dummy/views.py | 4 +- .../web/api/monitoring/__init__.py | 4 + .../web/api/monitoring/views.py | 11 + .../web/api/redis/views.py | 4 +- .../web/api/router.py | 3 +- .../web/lifetime.py | 4 +- fastapi_template/tests/conftest.py | 119 +++ fastapi_template/tests/test_mysql.py | 47 ++ fastapi_template/tests/test_postgres.py | 47 ++ fastapi_template/tests/test_sqlite.py | 47 ++ fastapi_template/tests/utils.py | 43 ++ poetry.lock | 705 ------------------ pyproject.toml | 28 +- 38 files changed, 1043 insertions(+), 888 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/utils.py create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/tests/test_dummy.py create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/tests/test_echo.py create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/tests/test_redis.py create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/monitoring/__init__.py create mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/monitoring/views.py create mode 100644 fastapi_template/tests/conftest.py create mode 100644 fastapi_template/tests/test_mysql.py create mode 100644 fastapi_template/tests/test_postgres.py create mode 100644 fastapi_template/tests/test_sqlite.py create mode 100644 fastapi_template/tests/utils.py delete mode 100644 poetry.lock diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..b218488 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,19 @@ +name: Testing fastapi-template + +on: push + +jobs: + pytest: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.9' + - name: Install deps + uses: knowsuchagency/poetry-install@v1 + env: + POETRY_VIRTUALENVS_CREATE: false + - name: Run tests + run: poetry run pytest -vv diff --git a/.gitignore b/.gitignore index 6f47b3e..10676df 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,9 @@ .idea +.vscode + +{%- if cookiecutter.db_info.name == "sqlite" %} +*.sqlite3 +{%- endif %} # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/fastapi_template/__main__.py b/fastapi_template/__main__.py index 8e52abe..818ed6e 100644 --- a/fastapi_template/__main__.py +++ b/fastapi_template/__main__.py @@ -10,12 +10,12 @@ from fastapi_template.input_model import BuilderContext script_dir = Path(__file__).parent -def main(): - try: - context = get_context() - except KeyboardInterrupt: - print("Goodbye!") - return +def generate_project(context: BuilderContext) -> None: + """ + Generate actual project with given context. + + :param context: builder_context + """ try: cookiecutter( template=f"{script_dir}/template", @@ -33,5 +33,14 @@ def main(): ) +def main() -> None: + """Starting point.""" + try: + context = get_context() + except KeyboardInterrupt: + print("Goodbye!") + return + generate_project(context) + if __name__ == "__main__": main() diff --git a/fastapi_template/cli.py b/fastapi_template/cli.py index 81bd204..ff9715f 100644 --- a/fastapi_template/cli.py +++ b/fastapi_template/cli.py @@ -7,7 +7,7 @@ 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, CIType +from fastapi_template.input_model import BuilderContext, DB_INFO, DatabaseType, CIType class SnakeCaseValidator(Validator): @@ -17,30 +17,6 @@ class SnakeCaseValidator(Validator): 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", @@ -157,7 +133,6 @@ def ask_features(current_context: BuilderContext) -> BuilderContext: "name": "add_dummy", "value": current_context.add_dummy } - checkbox_values = [] for feature_name, feature in features.items(): if feature["value"] is None: @@ -192,8 +167,9 @@ def read_user_input(current_context: BuilderContext) -> BuilderContext: ).run() if current_context.db is None: raise KeyboardInterrupt() - if current_context.db == DatabaseType.none: - current_context.enable_alembic = False + if current_context.db == DatabaseType.none: + current_context.enable_alembic = False + current_context.add_dummy = False if current_context.ci_type is None: current_context.ci_type = radiolist_dialog( "CI", diff --git a/fastapi_template/input_model.py b/fastapi_template/input_model.py index 5b2c7c2..edd41cd 100644 --- a/fastapi_template/input_model.py +++ b/fastapi_template/input_model.py @@ -25,6 +25,29 @@ class Database(BaseModel): driver: Optional[str] port: Optional[int] +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 + ), +} class BuilderContext(BaseModel): """Options for project generation.""" diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/.env b/fastapi_template/template/{{cookiecutter.project_name}}/.env index 8d89e98..0a784e0 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/.env +++ b/fastapi_template/template/{{cookiecutter.project_name}}/.env @@ -1 +1,6 @@ {{cookiecutter.project_name | upper}}_RELOAD=True +{%- if cookiecutter.db_info.name == "sqlite" %} +{{cookiecutter.project_name | upper}}_DB_FILE=db.sqlite3 +{%- else %} +{{cookiecutter.project_name | upper}}_DB_HOST=localhost +{%- endif %} diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/.flake8 b/fastapi_template/template/{{cookiecutter.project_name}}/.flake8 index 6572ef1..a4b9f06 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/.flake8 +++ b/fastapi_template/template/{{cookiecutter.project_name}}/.flake8 @@ -14,6 +14,8 @@ ignore = D105, ; Missing docstring in __init__ D107, + ; Found `__init__.py` module with logic + WPS412, ; Found class without a base class WPS306, ; Missing docstring in public nested class @@ -21,16 +23,12 @@ ignore = ; 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, @@ -44,8 +42,6 @@ ignore = 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 @@ -65,6 +61,18 @@ ignore = ; not perform function calls in argument defaults (for dependency injection) B008, +per-file-ignores = + ; all tests + test_*.py,tests.py,tests_*.py,*/tests/*,conftest.py: + ; Use of assert detected + S101, + ; Found outer scope names shadowing + WPS442, + ; Found too many local variables + WPS210, + ; Found magic number + WPS432, + ; all init files __init__.py: ; ignore not used imports @@ -74,14 +82,6 @@ ignore = ; 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, diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/.github/workflows/tests.yml b/fastapi_template/template/{{cookiecutter.project_name}}/.github/workflows/tests.yml index f4a9a18..a869b8d 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/.github/workflows/tests.yml +++ b/fastapi_template/template/{{cookiecutter.project_name}}/.github/workflows/tests.yml @@ -85,7 +85,7 @@ jobs: env: POETRY_VIRTUALENVS_CREATE: false - name: Run pytest check - run: poetry run pytest -vv --cov="{{cookiecutter.project_name}}" . {% if cookiecutter.enable_alembic %}--test-alembic{%- endif %} + run: poetry run pytest -vv --cov="{{cookiecutter.project_name}}" . {%- if cookiecutter.db_info.name != "none" %} {%- if cookiecutter.db_info.name != "sqlite" %} env: diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/.gitlab-ci.yml b/fastapi_template/template/{{cookiecutter.project_name}}/.gitlab-ci.yml index b568e79..094d90e 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/.gitlab-ci.yml +++ b/fastapi_template/template/{{cookiecutter.project_name}}/.gitlab-ci.yml @@ -39,7 +39,6 @@ pytest: {%- if cookiecutter.db_info.name != "sqlite" %} services: - name: {{ cookiecutter.db_info.image }} - {%- endif %} variables: {%- if cookiecutter.db_info.name == "postgresql" %} {{ cookiecutter.project_name | upper }}_DB_HOST: localhost @@ -55,6 +54,7 @@ pytest: ALLOW_EMPTY_PASSWORD: yes {%- endif %} {%- endif %} + {%- endif %} script: {%- if cookiecutter.db_info.name != "none" %} {%- if cookiecutter.db_info.name != "sqlite" %} @@ -63,4 +63,4 @@ pytest: - wait-for-it -t 180 ${{ cookiecutter.project_name | upper }}_DB_HOST:{{cookiecutter.db_info.port}} {%- endif %} {%- endif %} - - pytest -vv --cov="{{cookiecutter.project_name}}" . {% if cookiecutter.enable_alembic %}--test-alembic{%- endif %} + - pytest -vv --cov="{{cookiecutter.project_name}}" . 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 294b534..2dac1b2 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/.pre-commit-config.yaml +++ b/fastapi_template/template/{{cookiecutter.project_name}}/.pre-commit-config.yaml @@ -1,63 +1,62 @@ +--- # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.4.0 - hooks: - - id: check-ast - - id: trailing-whitespace - - id: check-toml - - id: end-of-file-fixer - - - repo: https://github.com/asottile/add-trailing-comma - rev: v2.1.0 - hooks: - - id: add-trailing-comma - - - 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 - name: Check with Flake8 - entry: poetry run flake8 - language: system - pass_filenames: false - types: [ python ] - args: [--count, .] - - - id: mypy - 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 ] + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.4.0 + hooks: + - id: check-ast + - id: trailing-whitespace + - id: check-toml + - id: end-of-file-fixer + + - repo: https://github.com/asottile/add-trailing-comma + rev: v2.1.0 + hooks: + - id: add-trailing-comma + + - repo: https://github.com/jumanjihouse/pre-commit-hook-yamlfmt + rev: 0.0.11 # or specific tag + hooks: + - id: yamlfmt + + - 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 + name: Check with Flake8 + entry: poetry run flake8 + language: system + pass_filenames: false + types: [python] + args: [--count, .] + + - id: mypy + 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] diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/README.md b/fastapi_template/template/{{cookiecutter.project_name}}/README.md index a0ea83d..8357b6c 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/README.md +++ b/fastapi_template/template/{{cookiecutter.project_name}}/README.md @@ -2,14 +2,14 @@ Start a project with: -``` +```bash docker-compose -f deploy/docker-compose.yml --project-directory . up ``` ## Pre-commit To install pre-commit simply run inside of the shell: -``` +```bash pre-commit install ``` {%- if cookiecutter.enable_kube == 'True' %} @@ -17,7 +17,7 @@ pre-commit install ## Kubernetes To run your app in kubernetes just run: -``` +```bash kubectl apply -f deploy/kube ``` @@ -25,7 +25,7 @@ It will create needed components. If you hasn't pushed to docker registry yet, you can build image locally. -``` +```bash docker-compose -f deploy/docker-compose.yml --project-directory . build docker save --output {{cookiecutter.project_name}}.tar {{cookiecutter.project_name}}:latest ``` @@ -33,7 +33,7 @@ docker save --output {{cookiecutter.project_name}}.tar {{cookiecutter.project_na {%- endif %} {%- if cookiecutter.enable_alembic == 'True' %} -# Migrations +## Migrations If you want to migrate your database, you should run following commands: ```bash @@ -67,3 +67,37 @@ alembic revision ``` {%- endif %} + + +## Running tests + +If you want to run it in docker, simply run: + +```bash +docker-compose -f deploy/docker-compose.yml --project-directory . run --rm api pytest -vv . +docker-compose -f deploy/docker-compose.yml --project-directory . down +``` + +For running tests on your local machine. + +{%- if cookiecutter.db_info.name != "none" %} +{%- if cookiecutter.db_info.name != "sqlite" %} +1. you need to start a database. + +I prefer doing it with docker: +``` +{%- if cookiecutter.db_info.name == "postgresql" %} +docker run -p "{{cookiecutter.db_info.port}}:{{cookiecutter.db_info.port}}" -e "POSTGRES_PASSWORD={{cookiecutter.project_name}}" -e "POSTGRES_USER={{cookiecutter.project_name}}" -e "POSTGRES_DB={{cookiecutter.project_name}}" {{cookiecutter.db_info.image}} +{%- endif %} +{%- if cookiecutter.db_info.name == "mysql" %} +docker run -p "{{cookiecutter.db_info.port}}:{{cookiecutter.db_info.port}}" -e "MYSQL_PASSWORD={{cookiecutter.project_name}}" -e "MYSQL_USER={{cookiecutter.project_name}}" -e "MYSQL_DATABASE={{cookiecutter.project_name}}" -e ALLOW_EMPTY_PASSWORD=yes {{cookiecutter.db_info.image}} +{%- endif %} +``` +{%- endif %} +{%- endif %} + + +2. Run the pytest. +```bash +pytest -vv . +``` diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/conditional_files.json b/fastapi_template/template/{{cookiecutter.project_name}}/conditional_files.json index bc3495c..0b3b443 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/conditional_files.json +++ b/fastapi_template/template/{{cookiecutter.project_name}}/conditional_files.json @@ -4,6 +4,7 @@ "resources": [ "{{cookiecutter.project_name}}/web/api/redis", "{{cookiecutter.project_name}}/services/redis", + "{{cookiecutter.project_name}}/tests/test_redis.py", "deploy/kube/redis.yml" ] }, @@ -18,7 +19,7 @@ "resources": [ "{{cookiecutter.project_name}}/web/api/dummy", "{{cookiecutter.project_name}}/db", - "{{cookiecutter.project_name}}/conftest.py", + "{{cookiecutter.project_name}}/tests/test_dummy.py", "deploy/kube/db.yml", "alembic.ini" ] @@ -53,7 +54,10 @@ "resources": [ "{{cookiecutter.project_name}}/web/api/echo", "{{cookiecutter.project_name}}/web/api/dummy", - "{{cookiecutter.project_name}}/web/api/redis" + "{{cookiecutter.project_name}}/web/api/redis", + "{{cookiecutter.project_name}}/tests/test_echo.py", + "{{cookiecutter.project_name}}/tests/test_dummy.py", + "{{cookiecutter.project_name}}/tests/test_redis.py" ] }, "Dummy model": { @@ -62,6 +66,7 @@ "{{cookiecutter.project_name}}/web/api/dummy", "{{cookiecutter.project_name}}/db/dao", "{{cookiecutter.project_name}}/db/models/dummy_model.py", + "{{cookiecutter.project_name}}/tests/test_dummy.py", "{{cookiecutter.project_name}}/db/migrations/versions/2021-08-16-16-55_2b7380507a71.py" ] }, diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/deploy/Dockerfile b/fastapi_template/template/{{cookiecutter.project_name}}/deploy/Dockerfile index 7c1e824..9a5c226 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/deploy/Dockerfile +++ b/fastapi_template/template/{{cookiecutter.project_name}}/deploy/Dockerfile @@ -12,7 +12,7 @@ RUN poetry config virtualenvs.create false COPY pyproject.toml poetry.lock /app/src/ WORKDIR /app/src -RUN poetry install --no-dev +RUN poetry install # Copying actuall application COPY . /app/src/ 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 318833b..80d1bf2 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/deploy/docker-compose.yml +++ b/fastapi_template/template/{{cookiecutter.project_name}}/deploy/docker-compose.yml @@ -8,11 +8,34 @@ services: image: {{cookiecutter.project_name}}:{{"${" }}{{cookiecutter.project_name | upper }}_VERSION:-latest{{"}"}} env_file: - .env + {%- if ((cookiecutter.db_info.name != "none" and cookiecutter.db_info.name != "sqlite") or + (cookiecutter.enable_redis == "True")) %} + depends_on: + {%- if cookiecutter.db_info.name != "none" %} + {%- if cookiecutter.db_info.name != "sqlite" %} + db: + condition: service_healthy + {%- endif %} + {%- endif %} + {%- if cookiecutter.enable_redis == "True" %} + redis: + condition: service_healthy + {%- endif %} + {%- endif %} environment: - - {{cookiecutter.project_name | upper }}_HOST=0.0.0.0 + {{cookiecutter.project_name | upper }}_HOST: 0.0.0.0 + {%- if cookiecutter.db_info.name != "none" %} {%- if cookiecutter.db_info.name == "sqlite" %} - - {{cookiecutter.project_name | upper }}_DB_FILE=/db_data/db.sqlite3 + {{cookiecutter.project_name | upper }}_DB_FILE: /db_data/db.sqlite3 + {%- else %} + {{cookiecutter.project_name | upper}}_DB_HOST: {{cookiecutter.project_name}}-db + {{cookiecutter.project_name | upper}}_DB_PORT: {{cookiecutter.db_info.port}} + {{cookiecutter.project_name | upper}}_DB_USER: {{cookiecutter.project_name}} + {{cookiecutter.project_name | upper}}_DB_PASS: {{cookiecutter.project_name}} + {{cookiecutter.project_name | upper}}_DB_BASE: {{cookiecutter.project_name}} + {%- endif %} {%- endif %} + {%- if cookiecutter.db_info.name == "sqlite" %} volumes: - {{cookiecutter.project_name}}-db-data:/db_data/ @@ -20,49 +43,81 @@ services: {%- if cookiecutter.db_info.name == "postgresql" %} db: - image: postgres:13.4-buster + image: {{cookiecutter.db_info.image}} hostname: {{cookiecutter.project_name}}-db environment: - - POSTGRES_PASSWORD={{cookiecutter.project_name}} - - POSTGRES_USER={{cookiecutter.project_name}} - - POSTGRES_DB={{cookiecutter.project_name}} + - "POSTGRES_PASSWORD={{cookiecutter.project_name}}" + - "POSTGRES_USER={{cookiecutter.project_name}}" + - "POSTGRES_DB={{cookiecutter.project_name}}" volumes: - {{cookiecutter.project_name}}-db-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready"] + interval: 2s + timeout: 3s + retries: 40 {%- endif %} - {%- if cookiecutter.db_info.name == "mysql" %} + {% if cookiecutter.db_info.name == "mysql" -%} db: - image: bitnami/mysql:8.0.26 + image: {{cookiecutter.db_info.image}} hostname: {{cookiecutter.project_name}}-db environment: - - MYSQL_PASSWORD={{cookiecutter.project_name}} - - MYSQL_USER={{cookiecutter.project_name}} - - MYSQL_DATABASE={{cookiecutter.project_name}} - - ALLOW_EMPTY_PASSWORD=yes + - "ALLOW_EMPTY_PASSWORD=yes" + - "MYSQL_ROOT_PASSWORD={{cookiecutter.project_name}}" + - "MYSQL_ROOT_USER={{cookiecutter.project_name}}" + - "MYSQL_DATABASE={{cookiecutter.project_name}}" + healthcheck: + test: + - "CMD" + - "mysql" + - "-u" + - "{{cookiecutter.project_name}}" + - "-p{{cookiecutter.project_name}}" + - "-e" + - "SELECT 1" + interval: 2s + timeout: 3s + retries: 40 volumes: - {{cookiecutter.project_name}}-db-data:/bitnami/mysql/data {%- endif %} - {% if cookiecutter.enable_alembic == 'True' %} + {% if cookiecutter.enable_alembic == 'True' -%} migrator: image: {{cookiecutter.project_name}}:{{"${" }}{{cookiecutter.project_name | upper }}_VERSION:-latest{{"}"}} - {%- if cookiecutter.db_info.name == "sqlite" %} command: alembic upgrade head + {%- if cookiecutter.db_info.name == "sqlite" %} environment: - - {{cookiecutter.project_name | upper }}_DB_FILE=/db_data/db.sqlite3 + {{cookiecutter.project_name | upper }}_DB_FILE: /db_data/db.sqlite3 volumes: - {{cookiecutter.project_name}}-db-data:/db_data/ {%- else %} - command: wait-for-it -t 180 {{cookiecutter.project_name}}-db:{{cookiecutter.db_info.port}} -- alembic upgrade head + environment: + {{cookiecutter.project_name | upper}}_DB_HOST: {{cookiecutter.project_name}}-db + {{cookiecutter.project_name | upper}}_DB_PORT: {{cookiecutter.db_info.port}} + {{cookiecutter.project_name | upper}}_DB_USER: {{cookiecutter.project_name}} + {{cookiecutter.project_name | upper}}_DB_PASS: {{cookiecutter.project_name}} + {{cookiecutter.project_name | upper}}_DB_BASE: {{cookiecutter.project_name}} + {%- endif %} + {%- if cookiecutter.db_info.name != "sqlite" %} + depends_on: + db: + condition: service_healthy {%- endif %} {%- endif %} - {%- if cookiecutter.enable_redis == "True" %} + {% if cookiecutter.enable_redis == "True" -%} redis: image: bitnami/redis:6.2.5 hostname: {{cookiecutter.project_name}}-redis environment: - - ALLOW_EMPTY_PASSWORD=yes + - "ALLOW_EMPTY_PASSWORD=yes" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 1s + timeout: 3s + retries: 30 {%- endif %} {% if cookiecutter.db_info.name != 'none' %} diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/pyproject.toml b/fastapi_template/template/{{cookiecutter.project_name}}/pyproject.toml index fa9def7..12490b7 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/pyproject.toml +++ b/fastapi_template/template/{{cookiecutter.project_name}}/pyproject.toml @@ -50,18 +50,18 @@ 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.db_info.name != "none" %} SQLAlchemy = {version = "^1.4", extras = ["mypy"]} {%- endif %} -{%- if cookiecutter.enable_alembic %} -pytest-alembic = "^0.3.3" -{%- endif %} pytest-cov = "^2.12.1" pytest-asyncio = "^0.15.1" +nest-asyncio = "^1.5.1" +pytest-env = "^0.6.2" +{%- if cookiecutter.enable_redis == "True" %} +fakeredis = "^1.6.1" +{%- endif %} +requests = "^2.26.0" [tool.isort] profile = "black" @@ -80,6 +80,17 @@ allow_untyped_decorators = true plugins = ["sqlalchemy.ext.mypy.plugin"] {%- endif %} +[tool.pytest.ini_options] +{%- if cookiecutter.db_info.name != "none" %} +env = [ + {%- if cookiecutter.db_info.name == "sqlite" %} + "{{cookiecutter.project_name | upper}}_DB_FILE=test_db.sqlite3", + {%- else %} + "{{cookiecutter.project_name | upper}}_DB_BASE={{cookiecutter.project_name}}_test", + {%- endif %} +] +{%- endif %} + [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" 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 86b6683..61da6c4 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,11 +1,27 @@ import asyncio import sys -from typing import Any, Generator +from typing import Any, Generator, AsyncGenerator import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +{%- if cookiecutter.enable_redis == "True" %} +from fakeredis.aioredis import FakeRedis +from {{cookiecutter.project_name}}.services.redis.dependency import get_redis_connection +{%- endif %} from {{cookiecutter.project_name}}.settings import settings +{%- if cookiecutter.db_info.name != "none" %} +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 +{%- endif %} +from {{cookiecutter.project_name}}.web.application import get_app +import nest_asyncio + +nest_asyncio.apply() @pytest.fixture(scope="session") def event_loop() -> Generator[asyncio.AbstractEventLoop, None, None]: @@ -29,26 +45,126 @@ def event_loop() -> Generator[asyncio.AbstractEventLoop, None, None]: loop.close() +{%- if cookiecutter.db_info.name != "none" %} @pytest.fixture(scope="session") -def database_url() -> str: +@pytest.mark.asyncio +async def _engine() -> AsyncGenerator[AsyncEngine, None]: """ - Возвращает ÑÑылку на теÑтовю базу данных. + Create engine and databases. - :return: URL. + :yield: new engine. """ - return str(settings.db_url) + 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() + await create_database() -@pytest.fixture(scope="session") -def init_database() -> Any: + engine = create_async_engine(str(settings.db_url)) + async with engine.begin() as conn: + await conn.run_sync(meta.create_all) + + try: + yield engine + finally: + await engine.dispose() + await drop_database() + +@pytest.fixture() +@pytest.mark.asyncio +async def dbsession(_engine: AsyncEngine) -> AsyncGenerator[AsyncSession, None]: """ - Create a database for tests. + Get session to database. - :return: ничего + Fixture that returns a SQLAlchemy session with a SAVEPOINT, and the rollback to it + after the test completes. + + :param _engine: current engine. + :yields: async session. """ - from {{cookiecutter.project_name}}.db.meta import meta # noqa: WPS433 - from {{cookiecutter.project_name}}.db.models import load_all_models # noqa: WPS433 + connection = await _engine.connect() + trans = await connection.begin() - load_all_models() + session_maker = sessionmaker( + connection, expire_on_commit=False, class_=AsyncSession, + ) + session = session_maker() + + try: + yield session + finally: + await session.close() + await trans.rollback() + await connection.close() + + +@pytest.fixture() +@pytest.mark.asyncio +async def transaction(_engine: AsyncEngine) -> AsyncGenerator[AsyncConnection, None]: + """ + Create and obtain a transaction. - return meta.create_all + :param _engine: current database engine. + :yield: connection. + """ + conn = await _engine.begin() + try: + yield conn + finally: + await conn.rollback() +{%- endif %} + + +{% if cookiecutter.enable_redis == "True" -%} +@pytest.fixture() +def fake_redis() -> FakeRedis: + """ + Get instance of a fake redis. + + :return: FakeRedis instance. + """ + return FakeRedis(decode_responses=True) +{%- endif %} + +@pytest.fixture() +def fastapi_app( + {%- if cookiecutter.db_info.name != "none" %} + dbsession: AsyncSession, + {%- endif %} + {% if cookiecutter.enable_redis == "True" -%} + fake_redis: FakeRedis, + {%- endif %} +) -> FastAPI: + """ + 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" %} + application.dependency_overrides[get_db_session] = lambda: dbsession + {%- endif %} + {%- if cookiecutter.enable_redis == "True" %} + application.dependency_overrides[get_redis_connection] = lambda: fake_redis + {%- endif %} + + return application # noqa: WPS331 + +@pytest.fixture(scope="function") +def client( + fastapi_app: FastAPI, +) -> TestClient: + """ + Fixture that creates client for requesting server. + + :param fastapi_app: the application. + :return: client for the app. + """ + return TestClient(app=fastapi_app) 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 index 2180ffd..03a51db 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/dao/dummy_dao.py @@ -1,3 +1,4 @@ +from typing import List, Optional from fastapi import Depends from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncScalarResult, AsyncSession @@ -12,7 +13,7 @@ class DummyDAO: def __init__(self, session: AsyncSession = Depends(get_db_session)): self.session = session - async def create_dummy_model(self, name: str) -> None: + def create_dummy_model(self, name: str) -> None: """ Add single dummy to session. @@ -33,3 +34,19 @@ class DummyDAO: ) return raw_stream.scalars() + + async def filter( + self, + name: Optional[str] = None + ) -> List[DummyModel]: + """ + Get specific dummy model. + + :param name: name of dummy instance. + :return: dummy models. + """ + query = select(DummyModel) + if name: + query = query.where(DummyModel.name == name) + rows = await self.session.execute(query) + return rows.scalars().fetchall() 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 index 4e58cd7..792e21d 100644 --- 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 @@ -11,13 +11,14 @@ 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 # type: ignore +config = context.config load_all_models() # Interpret the config file for Python logging. # This line sets up loggers basically. -fileConfig(config.config_file_name) +if config.config_file_name is not None: + fileConfig(config.config_file_name) # add your model's MetaData object here # for 'autogenerate' support 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/utils.py new file mode 100644 index 0000000..2f353b2 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db/utils.py @@ -0,0 +1,87 @@ +import os +from sqlalchemy import text +from sqlalchemy.engine import URL, make_url +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, AsyncEngine +from sqlalchemy.orm import sessionmaker +from {{cookiecutter.project_name}}.settings import settings + +{% if cookiecutter.db_info.name == "postgresql" -%} +async def create_database() -> None: + """Create a databse.""" + db_url = make_url(str(settings.db_url.with_path('/postgres'))) + engine = create_async_engine(db_url, isolation_level="AUTOCOMMIT") + + async with engine.connect() as conn: + database_existance = await conn.execute( + text( + f"SELECT 1 FROM pg_database WHERE datname='{settings.db_base}'", # noqa: E501, S608 + ) + ) + database_exists = database_existance.scalar() == 1 + + if database_exists: + await drop_database() + + async with engine.connect() as conn: # noqa: WPS440 + await conn.execute( + text( + f'CREATE DATABASE "{settings.db_base}" ENCODING "utf8" TEMPLATE template1', # noqa: E501 + ) + ) + +async def drop_database() -> None: + """Drop current database.""" + db_url = make_url(str(settings.db_url.with_path('/postgres'))) + engine = create_async_engine(db_url, isolation_level="AUTOCOMMIT") + async with engine.connect() as conn: + disc_users = ( + "SELECT pg_terminate_backend(pg_stat_activity.pid) " # noqa: S608 + "FROM pg_stat_activity " + f"WHERE pg_stat_activity.datname = '{settings.db_base}' " + "AND pid <> pg_backend_pid();" + ) + await conn.execute(text(disc_users)) + await conn.execute(text(f'DROP DATABASE "{settings.db_base}"')) + + +{%- endif %} +{%- if cookiecutter.db_info.name == "mysql" %} +async def create_database() -> None: + """Create a databse.""" + engine = create_async_engine(str(settings.db_url.with_path("/mysql"))) + + async with engine.connect() as conn: + database_existance = await conn.execute( + text( + "SELECT 1 FROM INFORMATION_SCHEMA.SCHEMATA" # noqa: S608 + f" WHERE SCHEMA_NAME='{settings.db_base}';", + ) + ) + database_exists = database_existance.scalar() == 1 + + if database_exists: + await drop_database() + + async with engine.connect() as conn: # noqa: WPS440 + await conn.execute( + text( + f'CREATE DATABASE {settings.db_base};' + ) + ) + +async def drop_database() -> None: + """Drop current database.""" + engine = create_async_engine(str(settings.db_url.with_path("/mysql"))) + async with engine.connect() as conn: + await conn.execute(text(f'DROP DATABASE {settings.db_base};')) +{%- endif %} +{%- if cookiecutter.db_info.name == "sqlite" %} +async def create_database() -> None: + """Create a databse.""" + +async def drop_database() -> None: + """Drop current database.""" + if settings.db_file.exists(): + os.remove(settings.db_file) + +{%- endif %} 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 11f8691..11d9a82 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 @@ -22,7 +22,7 @@ class Settings(BaseSettings): {%- if cookiecutter.db_info.name == "sqlite" %} db_file: Path = TEMP_DIR / "db.sqlite3" {%- else %} - db_host: str = "{{cookiecutter.project_name}}-db" + db_host: str = "localhost" db_port: int = {{cookiecutter.db_info.port}} db_user: str = "{{cookiecutter.project_name}}" db_pass: str = "{{cookiecutter.project_name}}" 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 new file mode 100644 index 0000000..0d7e71e --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/tests/test_dummy.py @@ -0,0 +1,65 @@ +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 + +@pytest.mark.asyncio +async def test_creation( + fastapi_app: FastAPI, + client: TestClient, + dbsession: AsyncSession +) -> None: + """ + Tests dummy instance creation. + + :param fastapi_app: current app fixture. + :param client: client for app. + :param dbsession: current db session. + """ + 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 + dao = DummyDAO(dbsession) + instances = await dao.filter(name=test_name) + assert instances[0].name == test_name + + +@pytest.mark.asyncio +async def test_getting( + fastapi_app: FastAPI, + client: TestClient, + dbsession: AsyncSession +) -> None: + """ + Tests dummy instance retrieval. + + :param fastapi_app: current app fixture. + :param client: client for app. + :param dbsession: current db session. + """ + dao = DummyDAO(dbsession) + test_name = uuid.uuid4().hex + dao.create_dummy_model(name=test_name) + await dbsession.commit() + + url = fastapi_app.url_path_for('get_dummy_models') + response = client.get(url) + assert response.status_code == status.HTTP_200_OK + response_json = response.json() + assert len(response_json) == 1 + assert response_json[0]['name'] == test_name + + diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/tests/test_echo.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/tests/test_echo.py new file mode 100644 index 0000000..fee5395 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/tests/test_echo.py @@ -0,0 +1,21 @@ +import uuid + +from fastapi.testclient import TestClient +from fastapi import FastAPI +from starlette import status + +def test_echo(fastapi_app: FastAPI, client: TestClient) -> None: + """ + Tests that echo route works. + + :param fastapi_app: current application. + :param client: clien for the app. + """ + url = fastapi_app.url_path_for('send_echo_message') + message = uuid.uuid4().hex + response = client.post(url, json={ + "message": message + }) + assert response.status_code == status.HTTP_200_OK + assert response.json()['message'] == message + diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/tests/test_redis.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/tests/test_redis.py new file mode 100644 index 0000000..3eb6f43 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/tests/test_redis.py @@ -0,0 +1,57 @@ +import uuid +import fakeredis +import pytest + +from fastapi import FastAPI +from fastapi.testclient import TestClient +from starlette import status +from fakeredis.aioredis import FakeRedis + + +@pytest.mark.asyncio +async def test_setting_value( + fastapi_app: FastAPI, + fake_redis: FakeRedis, + client: TestClient, +) -> None: + """ + Tests that you can set value in redis. + + :param fastapi_app: current application fixture. + :param fake_redis: fake redis instance. + :param client: client fixture. + """ + url = fastapi_app.url_path_for('set_redis_value') + test_key = uuid.uuid4().hex + test_val = uuid.uuid4().hex + response = client.put(url, json={ + "key": test_key, + "value": test_val + }) + assert response.status_code == status.HTTP_200_OK + actual_value = await fake_redis.get(test_key) + assert actual_value == test_val + + +@pytest.mark.asyncio +async def test_getting_value( + fastapi_app: FastAPI, + fake_redis: FakeRedis, + client: TestClient, +) -> None: + """ + Tests that you can get value from redis by key. + + :param fastapi_app: current application fixture. + :param fake_redis: fake redis instance. + :param client: client fixture. + """ + test_key = uuid.uuid4().hex + test_val = uuid.uuid4().hex + await fake_redis.set(test_key, test_val) + + url = fastapi_app.url_path_for('get_redis_value') + response = client.get(url, params={"key": test_key}) + assert response.status_code == status.HTTP_200_OK + assert response.json()['key'] == test_key + assert response.json()['value'] == test_val diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/tests/test_{{cookiecutter.project_name}}.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/tests/test_{{cookiecutter.project_name}}.py index 23a09e9..e09e5e2 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/tests/test_{{cookiecutter.project_name}}.py +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/tests/test_{{cookiecutter.project_name}}.py @@ -1,8 +1,15 @@ -def test_stub() -> None: +from fastapi import FastAPI +from fastapi.testclient import TestClient +from starlette import status + + +def test_health(client: TestClient, fastapi_app: FastAPI) -> None: """ - Test stub. + Checks the health endpoint. - It must be removed in real applications. + :param client: client for the app. + :param fastapi_app: current FastAPI application. """ - test_val = 3 - assert test_val == 3 + url = fastapi_app.url_path_for('health_check') + response = client.get(url) + assert response.status_code == status.HTTP_200_OK diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/docs/views.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/docs/views.py index 709cdb4..f5a95a9 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/docs/views.py +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/docs/views.py @@ -10,9 +10,16 @@ router = APIRouter() @router.get("/docs", include_in_schema=False) async def custom_swagger_ui_html(request: Request) -> HTMLResponse: + """ + Swagger UI. + + :param request: current request. + :return: rendered swagger UI. + """ + title = request.app.title return get_swagger_ui_html( openapi_url=request.app.openapi_url, - title=request.app.title + " - Swagger UI", + title=f"{title} - Swagger UI", oauth2_redirect_url=request.url_for("swagger_ui_redirect"), swagger_js_url="/static/docs/swagger-ui-bundle.js", swagger_css_url="/static/docs/swagger-ui.css", @@ -21,13 +28,25 @@ async def custom_swagger_ui_html(request: Request) -> HTMLResponse: @router.get("/swagger-redirect", include_in_schema=False) async def swagger_ui_redirect() -> HTMLResponse: + """ + Redirect to swagger. + + :return: redirect. + """ return get_swagger_ui_oauth2_redirect_html() @router.get("/redoc", include_in_schema=False) async def redoc_html(request: Request) -> HTMLResponse: + """ + Redoc UI. + + :param request: current request. + :return: rendered redoc UI. + """ + title = request.app.title return get_redoc_html( openapi_url=request.app.openapi_url, - title=request.app.title + " - ReDoc", + title=f"{title} - ReDoc", redoc_js_url="/static/docs/redoc.standalone.js", ) 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 955c9c6..44662fd 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 @@ -28,7 +28,7 @@ async def get_dummy_models( @router.put("/") -async def create_dummy_model( +def create_dummy_model( new_dummy_object: DummyModelInputDTO, dummy_dao: DummyDAO = Depends(), ) -> None: @@ -38,4 +38,4 @@ async def create_dummy_model( :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()) + dummy_dao.create_dummy_model(**new_dummy_object.dict()) diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/monitoring/__init__.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/monitoring/__init__.py new file mode 100644 index 0000000..c9bc60d --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/monitoring/__init__.py @@ -0,0 +1,4 @@ +"""API for checking project status.""" +from {{cookiecutter.project_name}}.web.api.monitoring.views import router + +__all__ = ['router'] diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/monitoring/views.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/monitoring/views.py new file mode 100644 index 0000000..c951b09 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/monitoring/views.py @@ -0,0 +1,11 @@ +from fastapi import APIRouter + +router = APIRouter() + +@router.get('/health') +def health_check() -> None: + """ + Checks the health of a project. + + It returns 200 if the project is healthy. + """ 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 index 5c2ab6b..e2aa629 100644 --- 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 @@ -9,7 +9,7 @@ router = APIRouter() @router.get("/", response_model=RedisValueDTO) -async def get_value( +async def get_redis_value( key: str, redis: Redis = Depends(get_redis_connection), ) -> RedisValueDTO: @@ -28,7 +28,7 @@ async def get_value( @router.put("/") -async def set_value( +async def set_redis_value( redis_value: RedisValueDTO, redis: Redis = Depends(get_redis_connection), ) -> None: 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 index 03dc240..63f736f 100644 --- 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 @@ -14,9 +14,10 @@ from {{cookiecutter.project_name}}.web.api import redis {%- if cookiecutter.self_hosted_swagger == "True" %} from {{cookiecutter.project_name}}.web.api import docs {%- endif %} - +from {{cookiecutter.project_name}}.web.api import monitoring api_router = APIRouter() +api_router.include_router(monitoring.router) {%- if cookiecutter.self_hosted_swagger == "True" %} api_router.include_router(docs.router) {%- endif %} 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 56a383a..22ac7be 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 @@ -62,7 +62,7 @@ def startup(app: FastAPI) -> Callable[[], Awaitable[None]]: :return: function that actually performs actions. """ - async def _startup() -> None: + async def _startup() -> None: # noqa: WPS430 {%- if cookiecutter.db_info.name != "none" %} _setup_db(app) {%- endif %} @@ -82,7 +82,7 @@ def shutdown(app: FastAPI) -> Callable[[], Awaitable[None]]: :return: function that actually performs actions. """ - async def _shutdown() -> None: + async def _shutdown() -> None: # noqa: WPS430 {%- if cookiecutter.db_info.name != "none" %} await app.state.db_engine.dispose() {%- endif %} diff --git a/fastapi_template/tests/conftest.py b/fastapi_template/tests/conftest.py new file mode 100644 index 0000000..320dbaa --- /dev/null +++ b/fastapi_template/tests/conftest.py @@ -0,0 +1,119 @@ +import pytest +import os +import uuid +import tempfile +import shutil +from faker import Faker +from pathlib import Path +from fastapi_template.input_model import BuilderContext, CIType +from fastapi_template.tests.utils import run_docker_compose_command + + +@pytest.fixture(scope="session") +def project_name() -> str: + """ + Generate name for test project. + + :return: project name. + """ + fake = Faker() + raw_name: str = fake.name_female() + return raw_name.lower().replace(" ", "_").replace("-", "_") + + +@pytest.fixture(scope="session", autouse=True) +def generator_start_dir() -> str: + """ + Generate directory to work into + + :yield: this fixture generates dir for all test projects. + """ + old_cwd = os.getcwd() + newpath = tempfile.mkdtemp() + os.chdir(newpath) + try: + yield newpath + finally: + os.chdir(old_cwd) + shutil.rmtree(newpath, ignore_errors=True) + + +@pytest.fixture() +def defautl_context(project_name: str) -> None: + """ + Default builder context without features. + + :param project_name: current project name. + :return: context. + """ + return BuilderContext( + project_name=project_name, + kube_name=project_name.replace("_", "-"), + project_description="Generated by pytest.", + ci_type=CIType.none, + enable_redis=False, + enable_alembic=True, + enable_kube=False, + enable_routers=True, + add_dummy=True, + self_hosted_swagger=False, + force=True, + ) + + +@pytest.fixture(autouse=True) +def lock_remover(project_name: str) -> None: + """ + Automatically removes lock file + after each test. + + :param project_name: generated project. + """ + yield + + Path("poetry.lock").unlink(missing_ok=True) + + +@pytest.fixture(autouse=True) +def default_dir(generator_start_dir: str) -> None: + """ + Change directory to generator_start_dir. + + :param generator_start_dir: start_dir. + """ + yield + cwd = os.getcwd() + if cwd != generator_start_dir: + os.chdir(generator_start_dir) + + +@pytest.fixture(scope="module", autouse=True) +def docker_module_shutdown(generator_start_dir: str, project_name: str) -> None: + """ + Cleans up docker context. + + :param generator_start_dir: generator dir. + :param project_name: name of the project. + """ + yield + cwd = os.getcwd() + project_dir = Path(generator_start_dir) / project_name + os.chdir(project_dir) + run_docker_compose_command("down -v") + os.chdir(cwd) + + +@pytest.fixture(scope="session", autouse=True) +def docker_shutdown(generator_start_dir: str, project_name: str) -> None: + """ + Cleans up docker context. + + :param generator_start_dir: generator dir. + :param project_name: name of the project. + """ + yield + cwd = os.getcwd() + project_dir = Path(generator_start_dir) / project_name + os.chdir(project_dir) + run_docker_compose_command("down -v --rmi=all") + os.chdir(cwd) diff --git a/fastapi_template/tests/test_mysql.py b/fastapi_template/tests/test_mysql.py new file mode 100644 index 0000000..ae54671 --- /dev/null +++ b/fastapi_template/tests/test_mysql.py @@ -0,0 +1,47 @@ +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 new file mode 100644 index 0000000..8fbc28f --- /dev/null +++ b/fastapi_template/tests/test_postgres.py @@ -0,0 +1,47 @@ +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 new file mode 100644 index 0000000..e7c5145 --- /dev/null +++ b/fastapi_template/tests/test_sqlite.py @@ -0,0 +1,47 @@ +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/fastapi_template/tests/utils.py b/fastapi_template/tests/utils.py new file mode 100644 index 0000000..6fcdfaf --- /dev/null +++ b/fastapi_template/tests/utils.py @@ -0,0 +1,43 @@ +import os +import subprocess +from typing import Optional +from fastapi_template.input_model import BuilderContext +from fastapi_template.__main__ import generate_project + + +def generate_project_and_chdir(context: BuilderContext): + generate_project(context) + os.chdir(context.project_name) + + +def run_pre_commit() -> int: + results = subprocess.run(["pre-commit", "run", "-a"]) + return results.returncode + + +def run_docker_compose_command(command: Optional[str] = None) -> subprocess.CompletedProcess: + docker_command = [ + "docker-compose", + "-f", + "deploy/docker-compose.yml", + "--project-directory", + ".", + ] + if command: + docker_command.extend(command.split()) + else: + docker_command.extend( + [ + "build", + ] + ) + return subprocess.run(docker_command) + + +def run_default_check(context: BuilderContext): + generate_project_and_chdir(context) + assert run_pre_commit() == 0 + build = run_docker_compose_command("build") + assert build.returncode == 0 + tests = run_docker_compose_command("run --rm api pytest -vv .") + assert tests.returncode == 0 diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index c8baf6c..0000000 --- a/poetry.lock +++ /dev/null @@ -1,705 +0,0 @@ -[[package]] -name = "arrow" -version = "1.1.1" -description = "Better dates & times for Python" -category = "main" -optional = false -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" -description = "Ultra-lightweight pure Python package to check if a file is binary or text." -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -chardet = ">=3.0.2" - -[[package]] -name = "certifi" -version = "2021.5.30" -description = "Python package for providing Mozilla's CA Bundle." -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "cffi" -version = "1.14.6" -description = "Foreign Function Interface for Python calling C code." -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -pycparser = "*" - -[[package]] -name = "cfgv" -version = "3.3.1" -description = "Validate configuration and produce human readable error messages." -category = "main" -optional = false -python-versions = ">=3.6.1" - -[[package]] -name = "chardet" -version = "4.0.0" -description = "Universal encoding detector for Python 2 and 3" -category = "main" -optional = false -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 = "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.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 -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[package.dependencies] -binaryornot = ">=0.4.4" -click = ">=7.0" -Jinja2 = ">=2.7,<4.0.0" -jinja2-time = ">=0.2.0" -poyo = ">=0.5.0" -python-slugify = ">=4.0.0" -requests = ">=2.23.0" -six = ">=1.10" - -[[package]] -name = "distlib" -version = "0.3.2" -description = "Distribution utilities" -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "filelock" -version = "3.0.12" -description = "A platform independent file lock." -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "identify" -version = "2.2.13" -description = "File identification library for Python" -category = "main" -optional = false -python-versions = ">=3.6.1" - -[package.extras] -license = ["editdistance-s"] - -[[package]] -name = "idna" -version = "3.2" -description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" -optional = false -python-versions = ">=3.5" - -[[package]] -name = "jinja2" -version = "3.0.1" -description = "A very fast and expressive template engine." -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "jinja2-time" -version = "0.2.0" -description = "Jinja2 Extension for Dates and Times" -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -arrow = "*" -jinja2 = "*" - -[[package]] -name = "markupsafe" -version = "2.0.1" -description = "Safely add untrusted strings to HTML/XML markup." -category = "main" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "nodeenv" -version = "1.6.0" -description = "Node.js virtual environment builder" -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "platformdirs" -version = "2.3.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" -description = "A lightweight YAML Parser for Python. ðŸ“" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "pre-commit" -version = "2.15.0" -description = "A framework for managing and maintaining multi-language pre-commit hooks." -category = "main" -optional = false -python-versions = ">=3.6.1" - -[package.dependencies] -cfgv = ">=2.0.0" -identify = ">=1.0.0" -nodeenv = ">=0.11.1" -pyyaml = ">=5.1" -toml = "*" -virtualenv = ">=20.0.8" - -[[package]] -name = "prompt-toolkit" -version = "3.0.20" -description = "Library for building powerful interactive command lines in Python" -category = "main" -optional = false -python-versions = ">=3.6.2" - -[package.dependencies] -wcwidth = "*" - -[[package]] -name = "pycparser" -version = "2.20" -description = "C parser in Python" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "pydantic" -version = "1.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.6.1" -description = "Python bindings for libgit2." -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -cffi = ">=1.4.0" - -[[package]] -name = "python-dateutil" -version = "2.8.2" -description = "Extensions to the standard Python datetime module" -category = "main" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "python-slugify" -version = "5.0.2" -description = "A Python Slugify application that handles Unicode" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -text-unidecode = ">=1.3" - -[package.extras] -unidecode = ["Unidecode (>=1.1.1)"] - -[[package]] -name = "pyyaml" -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.*, !=3.5.*" - -[[package]] -name = "requests" -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.*, !=3.5.*" - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} -idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} -urllib3 = ">=1.21.1,<1.27" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] -use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] - -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" - -[[package]] -name = "termcolor" -version = "1.1.0" -description = "ANSII Color formatting for output in terminal." -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "text-unidecode" -version = "1.3" -description = "The most basic Text::Unidecode port" -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -category = "main" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" - -[[package]] -name = "typing-extensions" -version = "3.10.0.2" -description = "Backported and Experimental Type Hints for Python 3.5+" -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "urllib3" -version = "1.26.6" -description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" - -[package.extras] -brotli = ["brotlipy (>=0.6.0)"] -secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] - -[[package]] -name = "virtualenv" -version = "20.7.2" -description = "Virtual Python Environment builder" -category = "main" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" - -[package.dependencies] -"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)", "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 = "f75e0608a162229e1e7cf160a4adcbbe2e1cb11621073400604d5d5086504c71" - -[metadata.files] -arrow = [ - {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"}, -] -certifi = [ - {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.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.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, - {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, -] -chardet = [ - {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, - {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, -] -charset-normalizer = [ - {file = "charset-normalizer-2.0.4.tar.gz", hash = "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"}, - {file = "charset_normalizer-2.0.4-py3-none-any.whl", hash = "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b"}, -] -click = [ - {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.3-py2.py3-none-any.whl", hash = "sha256:f8671531fa96ab14339d0c59b4f662a4f12a2ecacd94a0f70a3500843da588e2"}, - {file = "cookiecutter-1.7.3.tar.gz", hash = "sha256:6b9a4d72882e243be077a7397d0f1f76fe66cf3df91f3115dbb5330e214fa457"}, -] -distlib = [ - {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-2.2.13-py2.py3-none-any.whl", hash = "sha256:7199679b5be13a6b40e6e19ea473e789b11b4e3b60986499b1f589ffb03c217c"}, - {file = "identify-2.2.13.tar.gz", hash = "sha256:7bc6e829392bd017236531963d2d937d66fc27cadc643ac0aba2ce9f26157c79"}, -] -idna = [ - {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, - {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"}, -] -jinja2 = [ - {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-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.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"}, - {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"}, -] -platformdirs = [ - {file = "platformdirs-2.3.0-py3-none-any.whl", hash = "sha256:8003ac87717ae2c7ee1ea5a84a1a61e87f3fbd16eb5aadba194ea30a9019f648"}, - {file = "platformdirs-2.3.0.tar.gz", hash = "sha256:15b056538719b1c94bdaccb29e5f81879c7f7f0f4a153f46086d155dffcd4f0f"}, -] -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.15.0-py2.py3-none-any.whl", hash = "sha256:a4ed01000afcb484d9eb8d504272e642c4c4099bbad3a6b27e519bd6a3e928a6"}, - {file = "pre_commit-2.15.0.tar.gz", hash = "sha256:3c25add78dbdfb6a28a651780d5c311ac40dd17f160eb3954a0c59da40a505a7"}, -] -prompt-toolkit = [ - {file = "prompt_toolkit-3.0.20-py3-none-any.whl", hash = "sha256:6076e46efae19b1e0ca1ec003ed37a933dc94b4d20f486235d436e64771dcd5c"}, - {file = "prompt_toolkit-3.0.20.tar.gz", hash = "sha256:eb71d5a6b72ce6db177af4a7d4d7085b99756bf656d98ffcc4fecd36850eea6c"}, -] -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.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.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-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.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.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, - {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, -] -six = [ - {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"}, -] -text-unidecode = [ - {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, - {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, -] -toml = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] -typing-extensions = [ - {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, - {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, - {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, -] -urllib3 = [ - {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.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 d265a47..1257b57 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,19 +1,13 @@ [tool.poetry] name = "fastapi_template" -version = "2.4.1" +version = "2.5.0" description = "Feature-rich robust FastAPI template" authors = ["Pavel Kirilin <win10@list.ru>"] -packages = [ - { include = "fastapi_template" }, -] +packages = [{ include = "fastapi_template" }] repository = "https://github.com/s3rius/FastAPI-template" homepage = "https://github.com/s3rius/FastAPI-template" readme = "README.md" -keywords = [ - "FastAPI", - "Cookiecutter", - "Template" -] +keywords = ["FastAPI", "Cookiecutter", "Template"] [tool.poetry.dependencies] @@ -26,6 +20,22 @@ pydantic = "^1.8.2" prompt-toolkit = "^3.0.19" [tool.poetry.dev-dependencies] +pytest = "^6.2.5" +pytest-env = "^0.6.2" +Faker = "^8.14.0" + + +[tool.pytest.ini_options] +minversion = "6.0" +markers = [ + "pg: tests for postgresql.", + "mysql: tests for mysql.", + "sqlite: tests for sqlite3.", +] +env = [ + "POETRY_VIRTUALENVS_IN_PROJECT=True" +] +testpaths = ["fastapi_template/tests"] [tool.poetry.scripts] fastapi_template = "fastapi_template.__main__:main" -- GitLab