diff --git a/README.md b/README.md index 71b2de9..639824b 100644 --- a/README.md +++ b/README.md @@ -14,15 +14,14 @@
Templator in action
-You can install it directly from pypi with pip. -```bash -python3 -m pip install fastapi_template -python3 -m fastapi_template -# or fastapi_template -# Answer all the questions -# 🍪 Enjoy your new project 🍪 +You can install and run it directly from pypi with uvx. +```shell +uvx fastapi_template +``` +### 🍪 Enjoy your new project 🍪 +```shell cd new_project -docker-compose up --build +docker compose up --build ``` If you want to install it from sources, try this: @@ -70,7 +69,7 @@ Generator features: This project can handle arguments passed through command line. ```shell -$ python -m fastapi_template --help +$ uvx fastapi_template --help Usage: fastapi_template [OPTIONS] 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 a9c8603..58073ed 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/.github/workflows/tests.yml +++ b/fastapi_template/template/{{cookiecutter.project_name}}/.github/workflows/tests.yml @@ -6,38 +6,53 @@ jobs: lint: strategy: matrix: - cmd: - - black - - ruff - - mypy + include: + - name: Ruff format + cmd: ruff format --diff + - name: Ruff check + cmd: ruff check {{cookiecutter.project_name}} tests + - name: Mypy + cmd: mypy {{cookiecutter.project_name}} tests 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 + - uses: actions/checkout@v6 + + - name: Install the latest version of uv + uses: astral-sh/setup-uv@v7 with: - python-version: '3.11' - cache: 'poetry' + version: '0.9.17' + python-version: '3.13' + enable-cache: "auto" + - name: Install deps - run: poetry install + run: uv sync --locked --all-groups + - name: Run lint check - run: poetry run pre-commit run -a {{ '${{' }} matrix.cmd {{ '}}' }} + run: uv run {{ '${{' }} matrix.cmd {{ '}}' }} + pytest: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + - name: Create .env run: touch .env - - name: Set up Python - uses: actions/setup-python@v5 - with: - 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 + uses: docker/setup-compose-action@v1 + + - name: Build api image (dev target) + run: | + docker compose \ + -f docker-compose.yml \ + -f deploy/docker-compose.dev.yml \ + --project-directory . \ + build api + - name: Run pytest + run: | + docker compose \ + -f docker-compose.yml \ + -f deploy/docker-compose.dev.yml \ + --project-directory . \ + 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 4f0100c..335511d 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/.gitlab-ci.yml +++ b/fastapi_template/template/{{cookiecutter.project_name}}/.gitlab-ci.yml @@ -1,33 +1,56 @@ stages: - "test" +variables: + UV_VERSION: "0.9.17" + PYTHON_VERSION: "3.13" + BASE_LAYER: bookworm-slim + UV_LINK_MODE: copy + .test-template: stage: test - image: ghcr.io/astral-sh/uv:0.9.12-python3.13-bookworm-slim + image: ghcr.io/astral-sh/uv:$UV_VERSION-python$PYTHON_VERSION-$BASE_LAYER tags: - kubernetes-runner - docker-runner except: - tags + variables: + UV_CACHE_DIR: .uv-cache + cache: + - key: + files: + - uv.lock + paths: + - $UV_CACHE_DIR before_script: - - apt update && apt install -y git - - uv sync + - cd $CI_PROJECT_DIR + - uv sync --locked --all-extras --dev + after_script: + - uv cache prune --ci -black: +formatter: extends: - .test-template script: - - pre-commit run ruff-format -a + - uv run ruff format --diff ruff: extends: - .test-template script: - - pre-commit run ruff -a + - uv run ruff check {{cookiecutter.project_name}} tests --output-format=gitlab --output-file=code-quality-report.json + artifacts: + reports: + codequality: $CI_PROJECT_DIR/code-quality-report.json mypy: extends: - .test-template script: - - pre-commit run mypy -a - + - uv run mypy {{cookiecutter.project_name}} tests --output=json > mypy-out.json || true + - uv run mypy-gitlab-code-quality < mypy-out.json > codequality.json + artifacts: + when: always + reports: + codequality: $CI_PROJECT_DIR/codequality.json 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 a8a296b..9d44f22 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/.pre-commit-config.yaml +++ b/fastapi_template/template/{{cookiecutter.project_name}}/.pre-commit-config.yaml @@ -2,6 +2,11 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks repos: + - repo: https://github.com/astral-sh/uv-pre-commit + rev: 0.9.17 + hooks: + - id: uv-lock + - repo: local hooks: diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/Dockerfile b/fastapi_template/template/{{cookiecutter.project_name}}/Dockerfile index 9894639..6bc4630 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/Dockerfile +++ b/fastapi_template/template/{{cookiecutter.project_name}}/Dockerfile @@ -1,10 +1,8 @@ -FROM ghcr.io/astral-sh/uv:0.9.12-bookworm AS uv - # ----------------------------------- -# STAGE 1: prod stage +# STAGE BUILDER: Prepare builder image # Only install main dependencies # ----------------------------------- -FROM python:3.13-slim-bookworm AS prod +FROM ghcr.io/astral-sh/uv:0.9.17-python3.13-bookworm-slim AS builder {%- if cookiecutter.db_info.name == "mysql" %} RUN apt-get update && apt-get install -y \ @@ -21,37 +19,58 @@ RUN apt-get update && apt-get install -y \ && rm -rf /var/lib/apt/lists/* {%- endif %} -ENV UV_COMPILE_BYTECODE=1 -ENV UV_LINK_MODE=copy -ENV UV_PROJECT_ENVIRONMENT=/usr/local -ENV UV_PYTHON_DOWNLOADS=never -ENV UV_NO_MANAGED_PYTHON=1 -{%- if cookiecutter.orm == 'piccolo' %} -ENV PICCOLO_CONF="{{cookiecutter.project_name}}.piccolo_conf" -{%- endif %} +ENV UV_COMPILE_BYTECODE=1 \ + UV_LINK_MODE=copy \ + UV_PYTHON_DOWNLOADS=0 \ + UV_PROJECT_ENVIRONMENT=/opt/.venv \ + VIRTUAL_ENV="/opt/.venv" \ + PATH="/opt/.venv/bin:$PATH" \ + {%- if cookiecutter.orm == 'piccolo' %} + PICCOLO_CONF="{{cookiecutter.project_name}}.piccolo_conf" \ + {%- endif %} + UV_NO_DEV=1 WORKDIR /app/src -RUN --mount=from=uv,source=/usr/local/bin/uv,target=/bin/uv \ - --mount=type=cache,target=/root/.cache/uv \ +RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ - uv sync --locked --no-install-project --no-dev + uv sync --locked --no-install-project COPY . . -RUN --mount=from=uv,source=/usr/local/bin/uv,target=/bin/uv \ - --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-dev +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --locked + +# ----------------------------------- +# STAGE PROD: Production image +# Copy dependencies and environment from builder image +# ----------------------------------- +FROM python:3.13-slim-bookworm AS prod -CMD ["/usr/local/bin/python", "-m", "{{cookiecutter.project_name}}"] +RUN groupadd --system --gid 999 nonroot \ + && useradd --system --gid 999 --uid 999 --create-home nonroot + +ENV VIRTUAL_ENV="/opt/.venv" \ + {%- if cookiecutter.orm == 'piccolo' %} + PICCOLO_CONF="{{cookiecutter.project_name}}.piccolo_conf" \ + {%- endif %} + PATH="/opt/.venv/bin:$PATH" + +COPY --from=builder --chown=nonroot:nonroot /app/src /app/src +COPY --from=builder --chown=nonroot:nonroot /opt/.venv /opt/.venv + +USER nonroot + +WORKDIR /app/src # ----------------------------------- -# STAGE 3: development build -# Includes dev dependencies +# STAGE DEVELOPMENT: Development image +# Copy dependencies and environment from builder image and add dev dependencies # ----------------------------------- -FROM prod AS dev +FROM builder AS dev + +ENV UV_NO_DEV=0 -RUN --mount=from=uv,source=/usr/local/bin/uv,target=/bin/uv \ - --mount=type=cache,target=/root/.cache/uv \ +RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --locked --all-groups diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/README.md b/fastapi_template/template/{{cookiecutter.project_name}}/README.md index 205276c..7b3d560 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/README.md +++ b/fastapi_template/template/{{cookiecutter.project_name}}/README.md @@ -25,24 +25,18 @@ You can read more about uv here: https://docs.astral.sh/ruff/ You can start the project with docker using this command: ```bash -docker-compose up --build +docker compose up --build ``` 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 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 --watch ``` This command exposes the web application on port 8000, mounts current directory and enables autoreload. -But you have to rebuild image every time you modify `uv.lock` or `pyproject.toml` with this command: - -```bash -docker-compose build -``` - ## Project structure ```bash @@ -98,7 +92,7 @@ you can add `-f ./deploy/docker-compose.otlp.yml` to your docker command. Like this: ```bash -docker-compose -f 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. @@ -189,8 +183,8 @@ aerich migrate If you want to run it in docker, simply run: ```bash -docker-compose run --build --rm api pytest -vv . -docker-compose down +docker compose -f docker-compose.yml -f deploy/docker-compose.dev.yml 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 733f1c2..50c1321 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 @@ -3,18 +3,26 @@ services: ports: # Exposes application port. - "8000:8000" - build: + build: &build context: . + dockerfile: ./Dockerfile + target: dev volumes: # Adds current directory as volume. - .:/app/src/ environment: # Enables autoreload. {{cookiecutter.project_name | upper}}_RELOAD: "True" + develop: + watch: + - action: rebuild + path: uv.lock {%- if cookiecutter.enable_taskiq == "True" %} taskiq-worker: + build: + <<: *build volumes: # Adds current directory as volume. - .:/app/src/ diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/docker-compose.yml b/fastapi_template/template/{{cookiecutter.project_name}}/docker-compose.yml index ad3b8b3..580fe15 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/docker-compose.yml +++ b/fastapi_template/template/{{cookiecutter.project_name}}/docker-compose.yml @@ -3,6 +3,7 @@ services: build: context: . dockerfile: ./Dockerfile + target: prod image: {{cookiecutter.project_name}}:{{"${" }}{{cookiecutter.project_name | upper }}_VERSION:-latest{{"}"}} restart: always env_file: @@ -78,6 +79,10 @@ services: volumes: - {{cookiecutter.project_name}}-db-data:/db_data/ {%- endif %} + command: + - python + - -m + - {{cookiecutter.project_name}} {%- if cookiecutter.enable_taskiq == "True" %} diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/pyproject.toml b/fastapi_template/template/{{cookiecutter.project_name}}/pyproject.toml index ce05943..ff52569 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/pyproject.toml +++ b/fastapi_template/template/{{cookiecutter.project_name}}/pyproject.toml @@ -133,7 +133,7 @@ dependencies = [ "aiokafka >=0.12.0,<1", {%- endif %} {%- if cookiecutter.enable_taskiq == "True" %} - "taskiq >=0.12.0,<1", + "taskiq[reload] >=0.12.0,<1", "taskiq-fastapi >=0.3.6,<1", {%- if cookiecutter.enable_redis == "True" %} "taskiq-redis >=1.1.2,<2", @@ -166,9 +166,7 @@ dev = [ "nest-asyncio >=1.6.0,<2", {%- endif %} "httpx >=0.28.1,<1", -{%- if cookiecutter.enable_taskiq == "True" %} - "taskiq[reload] >=0.12.0,<1", -{%- endif %} + "mypy-gitlab-code-quality>=1.3.0,<2", ] diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/__main__.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/__main__.py index 5c0449a..b484a94 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/__main__.py +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/__main__.py @@ -52,6 +52,7 @@ def main() -> None: host=settings.host, port=settings.port, reload=settings.reload, + reload_excludes=[".venv/*"], log_level=settings.log_level.value.lower(), factory=True, ) diff --git a/fastapi_template/tests/utils.py b/fastapi_template/tests/utils.py index 2f1060e..df51fc0 100644 --- a/fastapi_template/tests/utils.py +++ b/fastapi_template/tests/utils.py @@ -40,9 +40,15 @@ def run_default_check(context: BuilderContext, worker_id: str, without_pytest=Fa if without_pytest: return - build = run_docker_compose_command("--progress=plain build") + build = run_docker_compose_command( + "-f docker-compose.yml -f deploy/docker-compose.dev.yml " + "--project-directory . --progress=plain build" + ) assert build == 0 - tests = run_docker_compose_command("--progress=plain run --rm api pytest -vv .") + tests = run_docker_compose_command( + "-f docker-compose.yml -f deploy/docker-compose.dev.yml " + "--project-directory . --progress=plain run --build --rm api pytest -vv ." + ) assert tests == 0