diff --git a/.dockerignore b/.dockerignore index e713043..c2e4622 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1,7 @@ -tests/test_init_project.py \ No newline at end of file +tests/test_init_project.py + +**/__pycache__/ +**/*.pyc +**/*.pyo +**/*.pyd +.pytest_cache/ \ No newline at end of file diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index b755d0e..700b139 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -2,7 +2,7 @@ name: main on: push: - branches: [main, develop] + branches: [main] pull_request: jobs: @@ -10,10 +10,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v4.1.0 + uses: actions/setup-python@v6 with: python-version: 3.12 @@ -24,28 +24,31 @@ jobs: run: flake8 --count tests: - name: Python ${{ matrix.python-version }} on ${{ matrix.os }} + name: tests - Python ${{ matrix.python-version }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: true matrix: os: ["ubuntu-latest"] - python-version: ["3.10", "3.11", "3.12", "3.13"] + python-version: ["3.13"] env: VENV: .venv steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} cache: 'pip' # cache option make the step fail if you don“t have requirements.txt or pyproject.toml on root. # https://github.com/actions/setup-python/issues/807. + - name: Install UV + run: make uv + - name: Create virtual environment - run: python -m venv $VENV + run: uv venv $VENV - name: Install package with test dependencies run: | @@ -60,19 +63,20 @@ jobs: - name: Upload coverage reports to Codecov if: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') && matrix.python-version == '3.12' - uses: codecov/codecov-action@v4.0.1 + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} tests-in-docker: + name: tests - Python 3.12 on Docker runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4.1.0 + uses: actions/setup-python@v6 - name: Build docker image run: make docker-build - name: Run tests - run: make docker-ci-test + run: make docker-test diff --git a/.gitignore b/.gitignore index eb99da3..0838c5a 100644 --- a/.gitignore +++ b/.gitignore @@ -170,3 +170,6 @@ terraform.tfstate.backup # project stuff scripts/config.sh test/ + +# UV +.python-version \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d55211d..d83c281 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -65,13 +65,18 @@ make docker-build make docker-gcp ``` +Install UV for faster installs (otherwise modify Makefile to use regular pip): +```shell +make uv +``` + 4. Create virtual environment and activate it: ```shell make venv ./.venv/bin/activate ``` -5. Install dependencies and the python package: +5. Install all dependencies for development and the python package in editable mode: ```shell make install ``` diff --git a/Dockerfile b/Dockerfile index 983b9f3..726671e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,69 +1,60 @@ # --------------------------------------------------------------------------------------- -# BASE IMAGE +# BUILDER # --------------------------------------------------------------------------------------- -FROM python:3.12.10-slim-bookworm AS base +FROM python:3.12-slim-bookworm AS builder -# Setup a volume for configuration and authtentication. VOLUME ["/root/.config"] -# Update system and install build tools. Remove unneeded stuff afterwards. -# Upgrade PIP. -# Create working directory. -RUN apt-get update && \ - apt-get install -y --no-install-recommends gcc g++ build-essential && \ - rm -rf /var/lib/apt/lists/* && \ - pip install --upgrade pip && \ - mkdir -p /opt/project +# Use uv for high-speed installs +COPY --from=ghcr.io/astral-sh/uv:0.10.9 /uv /usr/local/bin/uv -# Set working directory. -WORKDIR /opt/project +ENV UV_COMPILE_BYTECODE=1 -# --------------------------------------------------------------------------------------- -# DEPENDENCIES IMAGE (installed project dependencies) -# --------------------------------------------------------------------------------------- -# We do this first so when we modify code while development, this layer is reused -# from cache and only the layer installing the package executes again. -FROM base AS deps -COPY requirements.txt . -RUN pip install -r requirements.txt +COPY pyproject.toml requirements.txt README.md MANIFEST.in ./ +COPY src ./src -# --------------------------------------------------------------------------------------- -# Apache Beam integration IMAGE -# --------------------------------------------------------------------------------------- -FROM deps AS beam -# Copy files from official SDK image, including script/dependencies. -# IMPORTANT: This version must match the one in requirements.txt -COPY --from=apache/beam_python3.12_sdk:2.64.0 /opt/apache/beam /opt/apache/beam - -# Set the entrypoint to Apache Beam SDK launcher. -ENTRYPOINT ["/opt/apache/beam/boot"] +RUN uv pip install --system --upgrade pip && \ + uv pip install --system build && \ + uv pip install --system --prefix=/install -r requirements.txt && \ + uv pip install --system --prefix=/install --no-deps . # --------------------------------------------------------------------------------------- # PRODUCTION IMAGE # --------------------------------------------------------------------------------------- -# If you need Apache Beam integration, replace "deps" base image with "beam". -FROM deps AS prod +FROM python:3.12-slim-bookworm AS prod + +ENV PYTHONUNBUFFERED=1 -COPY . /opt/project -RUN pip install . && \ - rm -rf /root/.cache/pip && \ - rm -rf /opt/project/* +# Copy the pre-compiled packages from builder +COPY --from=builder /install /usr/local + +# APACHE BEAM INTEGRATION (Uncomment if needed) +# COPY --from=apache/beam_python3.12_sdk:2.71.0 /opt/apache/beam /opt/apache/beam +# ENTRYPOINT ["/opt/apache/beam/boot"] + +WORKDIR /opt/project # --------------------------------------------------------------------------------------- -# DEVELOPMENT IMAGE (editable install and development tools) +# DEVELOPMENT IMAGE # --------------------------------------------------------------------------------------- -# If you need Apache Beam integration, replace "deps" base image with "beam". -FROM deps AS dev +FROM builder AS dev -COPY . /opt/project -RUN make install +WORKDIR /opt/project + +COPY . . +RUN uv pip install --system -e .[lint,dev,build] && \ + uv pip install --system -r requirements-test.txt # --------------------------------------------------------------------------------------- -# TEST IMAGE (This one allows to check that package is properly installed in prod image) +# TEST IMAGE # --------------------------------------------------------------------------------------- FROM prod AS test -COPY ./tests /opt/project/tests -COPY ./requirements-test.txt /opt/project/ +COPY ./requirements-test.txt . +RUN pip install -r requirements-test.txt + +COPY ./tests ./tests -RUN pip install -r requirements-test.txt \ No newline at end of file +# Suppress all warnings during tests +# To see/address warnings, run tests in your development environment. +ENV PYTHONWARNINGS=ignore \ No newline at end of file diff --git a/Makefile b/Makefile index be8b58e..bbc8a5a 100644 --- a/Makefile +++ b/Makefile @@ -2,17 +2,25 @@ VENV_NAME:=.venv REQS_PROD:=requirements.txt +SETUP_FILE:=pyproject.toml +SOURCES = src + DOCKER_DEV_SERVICE:=dev -DOCKER_CI_TEST_SERVICE:=test -DOCKER_ISOLATED_SERVICE:=isolated +DOCKER_DEV_NO_GCP_SERVICE:=dev_no_gcp +DOCKER_PROD_SERVICE:=prod +DOCKER_TEST_SERVICE:=test GCP_PROJECT:=world-fishing-827 GCP_DOCKER_VOLUME:=gcp -sources = python_app_template +PYTHON_VERSION:=3.12 +UV_VERSION := 0.10.9 + +VENV:=uv venv +PIP:=uv pip +PIP_COMPILE:=uv pip compile + -PYTHON:=python -PIP:=${PYTHON} -m pip # --------------------- # DOCKER @@ -32,31 +40,36 @@ docker-gcp: docker-volume docker compose run gcloud config set project ${GCP_PROJECT} docker compose run gcloud auth application-default set-quota-project ${GCP_PROJECT} -.PHONY: docker-ci-test ## Runs tests using prod image, exporting coverage.xml report. -docker-ci-test: - docker compose run --rm ${DOCKER_CI_TEST_SERVICE} +.PHONY: docker-test ## Runs tests using prod image, exporting coverage.xml report. +docker-test: + docker compose run --rm ${DOCKER_TEST_SERVICE} .PHONY: docker-shell ## Enters to docker container shell. docker-shell: docker-volume docker compose run --rm -it ${DOCKER_DEV_SERVICE} -.PHONY: reqs ## Compiles requirements.txt with pip-tools. +.PHONY: docker-reqs ## Compiles requirements.txt with pip-tools. reqs: - docker compose run --rm ${DOCKER_ISOLATED_SERVICE} -c \ - 'pip-compile -o ${REQS_PROD} -v' + docker compose run --rm ${DOCKER_DEV_NO_GCP_SERVICE} -c \ + '${PIP_COMPILE} -o ${REQS_PROD} ${SETUP_FILE} -v' -.PHONY: reqs-upgrade ## Upgrades requirements.txt with pip-tools. +.PHONY: docker-reqs-upgrade ## Upgrades requirements.txt with pip-tools. reqs-upgrade: - docker compose run --rm ${DOCKER_ISOLATED_SERVICE} -c \ - 'pip-compile -o ${REQS_PROD} -U -v' + docker compose run --rm ${DOCKER_DEV_NO_GCP_SERVICE} -c \ + '${PIP_COMPILE} -o ${REQS_PROD} ${SETUP_FILE} -U -v' # --------------------- # VIRTUAL ENVIRONMENT # --------------------- +.PHONY: uv ## Installs UV +uv: + curl -LsSf https://astral.sh/uv/install.sh | UV_VERSION=$(UV_VERSION) sh + uv python pin ${PYTHON_VERSION} + .PHONY: venv ## Creates virtual environment. venv: - ${PYTHON} -m venv ${VENV_NAME} + ${VENV} ${VENV_NAME} .PHONY: upgrade-pip ## Upgrades pip. upgrade-pip: @@ -68,11 +81,12 @@ install-test: upgrade-pip .PHONY: install ## Install the package in editable mode & all dependencies for local development. install: upgrade-pip - ${PIP} install -e .[lint,dev,build,test] + ${PIP} install -e .[lint,dev,build] + make install-test .PHONY: test ## Run all unit tests exporting coverage.xml report. test: - ${PYTHON} -m pytest -m "not integration" --cov-report term --cov-report=xml --cov=$(sources) + python -m pytest -m "not integration" --cov-report term --cov-report=xml --cov=$(SOURCES) # --------------------- # QUALITY CHECKS @@ -80,36 +94,36 @@ test: .PHONY: hooks ## Install and pre-commit hooks. hooks: - ${PYTHON} -m pre_commit install --install-hooks - ${PYTHON} -m pre_commit install --hook-type commit-msg + python -m pre_commit install --install-hooks + python -m pre_commit install --hook-type commit-msg .PHONY: format ## Auto-format python source files according with PEP8. format: - ${PYTHON} -m black $(sources) - ${PYTHON} -m ruff check --fix $(sources) - ${PYTHON} -m ruff format $(sources) + python -m black $(SOURCES) + python -m ruff check --fix $(SOURCES) + python -m ruff format $(SOURCES) .PHONY: lint ## Lint python source files. lint: - ${PYTHON} -m ruff check $(sources) - ${PYTHON} -m ruff format --check $(sources) - ${PYTHON} -m black $(sources) --check --diff + python -m ruff check $(SOURCES) + python -m ruff format --check $(SOURCES) + python -m black $(SOURCES) --check --diff .PHONY: codespell ## Use Codespell to do spell checking. codespell: - ${PYTHON} -m codespell + python -m codespell .PHONY: typecheck ## Perform type-checking. typecheck: - ${PYTHON} -m mypy + python -m mypy .PHONY: audit ## Use pip-audit to scan for known vulnerabilities. audit: - ${PYTHON} -m pip_audit . + python -m pip_audit . .PHONY: pre-commit ## Run all pre-commit hooks. pre-commit: - ${PYTHON} -m pre_commit run --all-files + python -m pre_commit run --all-files .PHONY: all ## Run the standard set of checks performed in CI. all: lint codespell typecheck audit test @@ -121,7 +135,7 @@ all: lint codespell typecheck audit test .PHONY: build ## Build a source distribution and a wheel distribution. build: all clean - ${PYTHON} -m build + python -m build .PHONY: publish ## Publish the distribution to PyPI. publish: build diff --git a/README.md b/README.md index d6db6e2..d81548f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ - Python versions + Python versions Last release diff --git a/docker-compose.yml b/docker-compose.yml index b734138..b155461 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,36 +6,39 @@ services: - 'gcp:/root/.config/' entrypoint: gcloud - dev: + dev: &dev + image: gfw/python-app-template-dev + platform: linux/amd64 build: - context: . target: dev - platform: linux/amd64 volumes: - '.:/opt/project' - 'gcp:/root/.config/' entrypoint: /bin/bash + dev_no_gcp: + image: gfw/python-app-template-dev + # Dev container without GCP credentials. + <<: *dev + volumes: + - ".:/opt/project" + + prod: + image: gfw/python-app-template-prod + build: + target: prod + platform: linux/amd64 + entrypoint: /bin/bash + test: + image: gfw/python-app-template-test platform: linux/amd64 # Runs tests using the production Docker image. # Intended to be executed in the GitHub CI environment. build: - context: . target: test entrypoint: 'pytest -v' - isolated: - # Minimal dev container without GCP credentials. - # Used for tasks that don't need external dependencies (e.g., compiling requirements). - build: - context: . - target: dev - platform: linux/amd64 - volumes: - - '.:/opt/project' - entrypoint: /bin/bash - volumes: gcp: external: true diff --git a/init_project.py b/init_project.py index 4562d9f..42607ea 100644 --- a/init_project.py +++ b/init_project.py @@ -7,6 +7,7 @@ FILES_TO_UPDATE = ( "cloudbuild.yaml", "CONTRIBUTING.md", + "docker-compose.yml", "Makefile", "pyproject.toml", "README.md", diff --git a/requirements.txt b/requirements.txt index 240e028..b403fac 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,5 @@ -# -# This file is autogenerated by pip-compile with Python 3.12 -# by the following command: -# -# pip-compile --output-file=requirements.txt -# +# This file was autogenerated by uv via the following command: +# uv pip compile -o requirements.txt pyproject.toml markdown-it-py==4.0.0 # via rich mdurl==0.1.2 @@ -11,10 +7,10 @@ mdurl==0.1.2 pygments==2.19.2 # via rich pyyaml==6.0.3 - # via python-app-template (setup.py) + # via python-app-template (pyproject.toml) rich==14.2.0 # via - # python-app-template (setup.py) + # python-app-template (pyproject.toml) # rich-argparse rich-argparse==1.7.2 - # via python-app-template (setup.py) + # via python-app-template (pyproject.toml)