diff --git a/README.md b/README.md index 551c124..ab63640 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,7 @@ python3 -m fastapi_template # Answer all the questions # 🍪 Enjoy your new project 🍪 cd new_project -docker-compose -f deploy/docker-compose.yml --project-directory . build -docker-compose -f deploy/docker-compose.yml --project-directory . up --build +docker-compose up --build ``` If you want to install it from sources, try this: @@ -90,6 +89,7 @@ Options: Choose Object–Relational Mapper lib --ci [none|gitlab_ci|github] Select a CI for your app --redis Add redis support + --add_users Add fastapi-users support --rabbit Add RabbitMQ support --taskiq Add Taskiq support --migrations Add Migrations @@ -104,5 +104,7 @@ Options: --traefik Adds traefik labels to docker container --kafka Add Kafka support --gunicorn Add gunicorn server + --cookie-auth Add authentication via cookie support + --jwt-auth Add JWT auth support --help Show this message and exit. ``` diff --git a/fastapi_template/cli.py b/fastapi_template/cli.py index 5cbeb00..70a865a 100644 --- a/fastapi_template/cli.py +++ b/fastapi_template/cli.py @@ -597,7 +597,7 @@ def checker(ctx: BuilderContext) -> bool: entries=[ MenuEntry( code="cookie_auth", - cli_name="cookie auth", + cli_name="cookie-auth", user_view="Add authentication via cookie support", description=( "Adds {cookie} authentication support.".format( @@ -610,7 +610,7 @@ def checker(ctx: BuilderContext) -> bool: ), MenuEntry( code="jwt_auth", - cli_name="jwt auth", + cli_name="jwt-auth", user_view="Add JWT auth support", description=( "Adds {name} authentication support.".format( diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/.flake8 b/fastapi_template/template/{{cookiecutter.project_name}}/.flake8 deleted file mode 100644 index 4e1064a..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/.flake8 +++ /dev/null @@ -1,115 +0,0 @@ -[flake8] -max-complexity = 6 -inline-quotes = double -max-line-length = 88 -extend-ignore = E203 -docstring_style=sphinx - -ignore = - ; Found `f` string - WPS305, - ; Missing docstring in public module - D100, - ; Missing docstring in magic method - D105, - ; Missing docstring in __init__ - D107, - ; Found `__init__.py` module with logic - WPS412, - ; Found class without a base class - WPS306, - ; Missing docstring in public nested class - D106, - ; First line should be in imperative mood - D401, - ; Found wrong variable name - WPS110, - ; Found `__init__.py` module with logic - WPS326, - ; Found string constant over-use - WPS226, - ; Found upper-case constant in a class - WPS115, - ; Found nested function - WPS602, - ; Found method without arguments - WPS605, - ; Found overused expression - WPS204, - ; Found too many module members - WPS202, - ; Found too high module cognitive complexity - WPS232, - ; line break before binary operator - W503, - ; Found module with too many imports - WPS201, - ; Inline strong start-string without end-string. - RST210, - ; Found nested class - WPS431, - ; Found wrong module name - WPS100, - ; Found too many methods - WPS214, - ; Found too long ``try`` body - WPS229, - ; Found unpythonic getter or setter - WPS615, - ; Found a line that starts with a dot - WPS348, - ; Found complex default value (for dependency injection) - WPS404, - ; not perform function calls in argument defaults (for dependency injection) - B008, - ; Model should define verbose_name in its Meta inner class - DJ10, - ; Model should define verbose_name_plural in its Meta inner class - DJ11, - ; Found mutable module constant. - WPS407, - ; Found too many empty lines in `def` - WPS473, - ; too many no-cover comments. - WPS403, - -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, - ; Missing parameter(s) in Docstring - DAR101, - ; Found too many arguments - WPS211, - - ; all init files - __init__.py: - ; ignore not used imports - F401, - ; ignore import with wildcard - F403, - ; Found wrong metadata variable - WPS410, - -exclude = - ./.cache, - ./.git, - ./.idea, - ./.mypy_cache, - ./.pytest_cache, - ./.venv, - ./venv, - ./env, - ./cached_venv, - ./docs, - ./deploy, - ./var, - ./.vscode, - *migrations*, 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 9e35a5b..a9c8603 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/.github/workflows/tests.yml +++ b/fastapi_template/template/{{cookiecutter.project_name}}/.github/workflows/tests.yml @@ -13,128 +13,31 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Install poetry + run: pipx install poetry - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.11' + cache: 'poetry' - name: Install deps run: poetry install - name: Run lint check run: poetry run pre-commit run -a {{ '${{' }} matrix.cmd {{ '}}' }} pytest: runs-on: ubuntu-latest - {%- if ((cookiecutter.db_info.name != "none" and cookiecutter.db_info.name != "sqlite") or - (cookiecutter.enable_rmq == "True") or - (cookiecutter.enable_kafka == "True")) %} - services: - {%- if cookiecutter.db_info.name != "none" and cookiecutter.db_info.name != "sqlite" %} - - {{cookiecutter.project_name}}-db: - image: {{ cookiecutter.db_info.image }} - env: - {%- if cookiecutter.db_info.name == "postgresql" %} - POSTGRES_PASSWORD: {{ cookiecutter.project_name }} - POSTGRES_USER: {{ cookiecutter.project_name }} - POSTGRES_DB: {{ cookiecutter.project_name }} - {%- endif %} - {%- if cookiecutter.db_info.name == "mysql" %} - MYSQL_ROOT_PASSWORD: "{{ cookiecutter.project_name }}" - MYSQL_USER: "{{ cookiecutter.project_name }}" - MYSQL_DATABASE: "{{ cookiecutter.project_name }}" - {%- endif %} - {%- if cookiecutter.db_info.name == "mongodb" %} - MONGO_INITDB_ROOT_USERNAME: "{{ cookiecutter.project_name }}" - MONGO_INITDB_ROOT_PASSWORD: "{{ cookiecutter.project_name }}" - {%- endif %} - {%- if cookiecutter.db_info.name == "mysql" %} - options: >- - --health-cmd="mysqladmin ping --user={{ cookiecutter.project_name }} --password={{ cookiecutter.project_name }}" - --health-interval=15s - --health-timeout=5s - --health-retries=6 - {%- endif %} - {%- if cookiecutter.db_info.name == "postgresql" %} - options: >- - --health-cmd="pg_isready" - --health-interval=10s - --health-timeout=5s - --health-retries=5 - {%- endif %} - ports: - - {{ cookiecutter.db_info.port }}:{{ cookiecutter.db_info.port }} - {%- endif %} - {%- if cookiecutter.enable_rmq == "True" %} - - {{cookiecutter.project_name}}-rmq: - image: rabbitmq:3.9.16-alpine - env: - RABBITMQ_DEFAULT_USER: "guest" - RABBITMQ_DEFAULT_PASS: "guest" - RABBITMQ_DEFAULT_VHOST: "/" - options: >- - --health-cmd="rabbitmq-diagnostics check_running -q" - --health-interval=10s - --health-timeout=5s - --health-retries=8 - ports: - - 5672:5672 - {%- endif %} - {%- if cookiecutter.enable_kafka == "True" %} - - {{cookiecutter.project_name}}-zookeeper: - image: "bitnami/zookeeper:3.7.1" - env: - ALLOW_ANONYMOUS_LOGIN: "yes" - ZOO_LOG_LEVEL: "ERROR" - options: >- - --health-cmd="zkServer.sh status" - --health-interval=10s - --health-timeout=5s - --health-retries=8 - - {{cookiecutter.project_name}}-kafka: - image: bitnami/kafka:3.2.0 - env: - KAFKA_BROKER_ID: "1" - ALLOW_PLAINTEXT_LISTENER: "yes" - KAFKA_CFG_LISTENERS: "PLAINTEXT://0.0.0.0:9092" - KAFKA_CFG_ADVERTISED_LISTENERS: "PLAINTEXT://localhost:9092" - KAFKA_CFG_ZOOKEEPER_CONNECT: "{{cookiecutter.project_name}}-zookeeper:2181" - options: >- - --health-cmd="kafka-topics.sh --list --bootstrap-server localhost:9092" - --health-interval=10s - --health-timeout=5s - --health-retries=8 - ports: - - 9092:9092 - {%- endif %} - {%- endif %} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + - name: Create .env + run: touch .env - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: - python-version: '3.9' - - name: Install deps - uses: knowsuchagency/poetry-install@v1 - env: - POETRY_VIRTUALENVS_CREATE: false - - name: Run pytest check - run: poetry run pytest -vv --cov="{{cookiecutter.project_name}}" . - env: - {{ 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_HOST: localhost - {%- endif %} - {%- if cookiecutter.db_info.name == "mongodb" %} - {{ cookiecutter.project_name | upper }}_DB_BASE: admin - {%- endif %} - {%- endif %} - {%- if cookiecutter.enable_rmq == "True" %} - {{ cookiecutter.project_name | upper }}_RABBIT_HOST: localhost - {%- endif %} - {%- if cookiecutter.enable_kafka == "True" %} - {{ cookiecutter.project_name | upper }}_KAFKA_BOOTSTRAP_SERVERS: '["localhost:9092"]' - {%- endif %} + python-version: '3.11' + - name: Update docker-compose + uses: KengoTODA/actions-setup-docker-compose@v1 + with: + version: "2.28.0" + - name: run tests + run: docker-compose run --rm api pytest -vv diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/.gitlab-ci.yml b/fastapi_template/template/{{cookiecutter.project_name}}/.gitlab-ci.yml index f05bfa1..e0e64f3 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/.gitlab-ci.yml +++ b/fastapi_template/template/{{cookiecutter.project_name}}/.gitlab-ci.yml @@ -3,14 +3,15 @@ stages: .test-template: stage: test - image: python:3.9.6-slim-buster + image: python:3.11.4-slim-bullseye tags: - kubernetes-runner - docker-runner except: - tags before_script: - - pip install poetry==1.4.2 + - apt update && apt install -y git + - pip install poetry==1.8.2 - poetry config virtualenvs.create false - poetry install @@ -18,104 +19,17 @@ black: extends: - .test-template script: - - black --check . + - pre-commit run black -a -flake8: +ruff: extends: - .test-template script: - - flake8 --count . + - pre-commit run ruff -a mypy: extends: - .test-template script: - - mypy . - -pytest: - extends: - - .test-template - {%- if ((cookiecutter.db_info.name != "none" and cookiecutter.db_info.name != "sqlite") or - (cookiecutter.enable_rmq == "True") or - (cookiecutter.enable_kafka == "True")) %} - services: - {%- if cookiecutter.db_info.name != "none" and cookiecutter.db_info.name != "sqlite" %} - - name: {{ cookiecutter.db_info.image }} - alias: database - {%- endif %} - {%- if cookiecutter.enable_rmq == "True" %} - - name: rabbitmq:3.9.16-alpine - alias: rmq - {%- endif %} - {%- if cookiecutter.enable_kafka == "True" %} - - name: bitnami/kafka:3.2.0 - alias: kafka - {%- endif %} - variables: - {%- if cookiecutter.db_info.name == "postgresql" %} - - # Postgresql variables - {{ cookiecutter.project_name | upper }}_DB_HOST: database - POSTGRES_PASSWORD: {{ cookiecutter.project_name }} - POSTGRES_USER: {{ cookiecutter.project_name }} - POSTGRES_DB: {{ cookiecutter.project_name }} - {%- endif %} - {%- if cookiecutter.db_info.name == "mysql" %} - - # MySQL variables - {{ cookiecutter.project_name | upper }}_DB_HOST: database - MYSQL_PASSWORD: {{ cookiecutter.project_name }} - MYSQL_USER: {{ cookiecutter.project_name }} - MYSQL_DATABASE: {{ cookiecutter.project_name }} - ALLOW_EMPTY_PASSWORD: yes - {%- endif %} - - {%- if cookiecutter.db_info.name == "mongodb" %} - - # MongoDB variables - {{ cookiecutter.project_name | upper }}_DB_HOST: database - {{ cookiecutter.project_name | upper }}_DB_BASE: admin - MONGO_INITDB_ROOT_USERNAME: {{ cookiecutter.project_name }} - MONGO_INITDB_ROOT_PASSWORD: {{ cookiecutter.project_name }} - {%- endif %} - - {%- if cookiecutter.enable_rmq == "True" %} - - # Rabbitmq variables - RABBITMQ_DEFAULT_USER: "guest" - RABBITMQ_DEFAULT_PASS: "guest" - RABBITMQ_DEFAULT_VHOST: "/" - {{ cookiecutter.project_name | upper }}_RABBIT_HOST: rmq - {%- endif %} - {%- if cookiecutter.enable_kafka == "True" %} - - # Kafka variables - KAFKA_BROKER_ID: "1" - KAFKA_ENABLE_KRAFT: "yes" - ALLOW_PLAINTEXT_LISTENER: "yes" - KAFKA_CFG_PROCESS_ROLES: "broker,controller" - KAFKA_CFG_CONTROLLER_LISTENER_NAMES: "CONTROLLER" - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: "CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT" - KAFKA_CFG_LISTENERS: "PLAINTEXT://:9092,CONTROLLER://:9093" - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: "1@127.0.0.1:9093" - {{ cookiecutter.project_name | upper }}_KAFKA_BOOTSTRAP_SERVERS: '["kafka:9092"]' - {%- endif %} - {%- endif %} - script: - {%- if cookiecutter.db_info.name != "none" %} - {%- if cookiecutter.db_info.name != "sqlite" %} - - apt update - - apt install -y wait-for-it - - wait-for-it -t 180 ${{ cookiecutter.project_name | upper }}_DB_HOST:{{cookiecutter.db_info.port}} - {%- endif %} - {%- endif %} - - pytest -vv --junitxml=report.xml --cov="{{cookiecutter.project_name}}" . - - coverage xml - artifacts: - when: always - reports: - junit: report.xml - coverage_report: - coverage_format: cobertura - path: coverage.xml + - pre-commit run ruff -a diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/README.md b/fastapi_template/template/{{cookiecutter.project_name}}/README.md index 423fd1e..25522bd 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/README.md +++ b/fastapi_template/template/{{cookiecutter.project_name}}/README.md @@ -25,14 +25,14 @@ You can read more about poetry here: https://python-poetry.org/ You can start the project with docker using this command: ```bash -docker-compose -f deploy/docker-compose.yml --project-directory . up --build +docker-compose up --build ``` -If you want to develop in docker with autoreload add `-f deploy/docker-compose.dev.yml` to your docker command. +If you want to develop in docker with autoreload and exposed ports add `-f deploy/docker-compose.dev.yml` to your docker command. Like this: ```bash -docker-compose -f deploy/docker-compose.yml -f deploy/docker-compose.dev.yml --project-directory . up --build +docker-compose -f docker-compose.yml -f deploy/docker-compose.dev.yml --project-directory . up --build ``` This command exposes the web application on port 8000, mounts current directory and enables autoreload. @@ -40,7 +40,7 @@ This command exposes the web application on port 8000, mounts current directory But you have to rebuild image every time you modify `poetry.lock` or `pyproject.toml` with this command: ```bash -docker-compose -f deploy/docker-compose.yml --project-directory . build +docker-compose build ``` ## Project structure @@ -98,7 +98,7 @@ you can add `-f ./deploy/docker-compose.otlp.yml` to your docker command. Like this: ```bash -docker-compose -f deploy/docker-compose.yml -f deploy/docker-compose.otlp.yml --project-directory . up +docker-compose -f docker-compose.yml -f deploy/docker-compose.otlp.yml --project-directory . up ``` This command will start OpenTelemetry collector and jaeger. @@ -124,8 +124,7 @@ It's configured using .pre-commit-config.yaml file. By default it runs: * black (formats your code); * mypy (validates types); -* isort (sorts imports in all files); -* flake8 (spots possible bugs); +* ruff (spots possible bugs); You can read more about pre-commit here: https://pre-commit.com/ @@ -145,7 +144,7 @@ It will create needed components. If you haven't pushed to docker registry yet, you can build image locally. ```bash -docker-compose -f deploy/docker-compose.yml --project-directory . build +docker-compose build docker save --output {{cookiecutter.project_name}}.tar {{cookiecutter.project_name}}:latest ``` @@ -211,8 +210,8 @@ aerich migrate If you want to run it in docker, simply run: ```bash -docker-compose -f deploy/docker-compose.yml -f deploy/docker-compose.dev.yml --project-directory . run --build --rm api pytest -vv . -docker-compose -f deploy/docker-compose.yml -f deploy/docker-compose.dev.yml --project-directory . down +docker-compose run --build --rm api pytest -vv . +docker-compose down ``` For running tests on your local machine. diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/deploy/docker-compose.dev.yml b/fastapi_template/template/{{cookiecutter.project_name}}/deploy/docker-compose.dev.yml index fc88a97..733f1c2 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/deploy/docker-compose.dev.yml +++ b/fastapi_template/template/{{cookiecutter.project_name}}/deploy/docker-compose.dev.yml @@ -5,7 +5,6 @@ services: - "8000:8000" build: context: . - target: dev volumes: # Adds current directory as volume. - .:/app/src/ diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/deploy/docker-compose.yml b/fastapi_template/template/{{cookiecutter.project_name}}/docker-compose.yml similarity index 99% rename from fastapi_template/template/{{cookiecutter.project_name}}/deploy/docker-compose.yml rename to fastapi_template/template/{{cookiecutter.project_name}}/docker-compose.yml index d54a0ad..43440d6 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/deploy/docker-compose.yml +++ b/fastapi_template/template/{{cookiecutter.project_name}}/docker-compose.yml @@ -3,7 +3,6 @@ services: build: context: . dockerfile: ./Dockerfile - target: prod image: {{cookiecutter.project_name}}:{{"${" }}{{cookiecutter.project_name | upper }}_VERSION:-latest{{"}"}} restart: always env_file: diff --git a/fastapi_template/tests/utils.py b/fastapi_template/tests/utils.py index dfab3bb..31df380 100644 --- a/fastapi_template/tests/utils.py +++ b/fastapi_template/tests/utils.py @@ -22,31 +22,20 @@ def run_pre_commit() -> int: def run_docker_compose_command( command: Optional[str] = None, ) -> subprocess.CompletedProcess: - docker_command = [ - "docker-compose", - "-f", - "deploy/docker-compose.yml", - "--project-directory", - ".", - ] + docker_command = ["docker-compose"] if command: docker_command.extend(shlex.split(command)) else: - docker_command.extend( - [ - "build", - ] - ) + docker_command.extend(["build"]) return subprocess.run(docker_command) def run_default_check(context: BuilderContext, worker_id: str, without_pytest=False): generate_project_and_chdir(context) - compose = Path("./deploy/docker-compose.yml") + compose = Path("./docker-compose.yml") with compose.open("r") as compose_file: data = yaml.safe_load(compose_file) - data['services']['api']['build'].pop('target', None) - data['services']['api']['image'] = f"test_image:v{worker_id}" + data["services"]["api"]["image"] = f"test_image:v{worker_id}" with compose.open("w") as compose_file: yaml.safe_dump(data, compose_file)