diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c263eb1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,60 @@ +# Git and version control +.git +.gitignore + +# Virtual environments +.venv + +# Python cache and bytecode +__pycache__ +*.py[cod] +*$py.class +*.so +*.egg-info +*.egg + +# Testing +tests +.pytest_cache +.coverage +.coverage.* +htmlcov +.tox +.nox +.hypothesis + +# Development tools and docs +Makefile +.pre-commit-config.yaml +CHANGELOG.md +README.md + +# IDE and editor files +.vscode +.idea +*.swp +*.swo +*~ + +# OS files +.DS_Store +Thumbs.db + +# Build artifacts +build +dist +*.whl +*.tar.gz + +# Linting and type checking caches +.ruff_cache +.mypy_cache +.dmypy.json + +# Misc +*.bak +.cache + +# Docker files +Dockerfile +.dockerignore diff --git a/.github/workflows/actions.yaml b/.github/workflows/actions.yaml index e4403bc..48dcb78 100644 --- a/.github/workflows/actions.yaml +++ b/.github/workflows/actions.yaml @@ -1,6 +1,5 @@ name: All checks on: - workflow_call: push: tags: - 'v[0-9]+' @@ -11,22 +10,11 @@ on: - 'v[0-9]+.[0-9]+.[0-9]+-*' branches: - '**' + pull_request: ~ jobs: - pre-commit-preparation: - name: Pre-commit - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v3 - - - name: Copy config - run: | - if [ ! -f .pre-commit-config.yaml ]; then - curl -o .pre-commit-config.yaml https://raw.githubusercontent.com/EO-DataHub/github-actions/main/.pre-commit-config-python.yaml - fi - - - uses: pre-commit/action@v3.0.1 + qa: + uses: EO-DataHub/github-actions/.github/workflows/qa-python.yaml@main security-scan: name: Call Security Scan @@ -34,32 +22,14 @@ jobs: unit-tests: name: Run unit tests - uses: EO-DataHub/github-actions/.github/workflows/unit-tests-python.yaml@main + uses: EO-DataHub/github-actions/.github/workflows/unit-tests-python-uv.yaml@main - get-tag-name: - runs-on: ubuntu-latest - outputs: - image_tag: ${{ steps.get-image-tag.outputs.IMAGE_TAG }} - steps: - - name: Get image tag - id: get-image-tag - run: | - IMAGE_TAG=$(if [[ "${GITHUB_REF##refs/tags/}" =~ ^v ]]; then echo ${GITHUB_REF##refs/tags/v}; elif [[ "${GITHUB_REF}" == "refs/heads/main" ]]; then echo "latest"; else echo "${GITHUB_REF##refs/heads/}-latest" | sed "s/[^a-zA-Z0-9]/-/g" ; fi) >> "$GITHUB_ENV" - echo $IMAGE_TAG - echo "IMAGE_TAG=$IMAGE_TAG" >> "$GITHUB_OUTPUT" - - aws-ecr-build: - name: Build ECR image - needs: get-tag-name - uses: EO-DataHub/github-actions/.github/workflows/docker-image-to-aws-ecr.yaml@main + publish: + name: Build and push Docker image + uses: EO-DataHub/github-actions/.github/workflows/ecr-publish.yaml@main with: - image_name: CHANGE-ME - image_tag: ${{ needs.get-tag-name.outputs.image_tag }} - permissions: + image_name: ${{ vars.IMAGE_NAME }} + aws_role_arn: ${{ vars.AWS_ROLE_ARN }} + aws_ecr_alias: ${{ vars.AWS_ECR_ALIAS }}s id-token: write - contents: read - secrets: - AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }} - AWS_ECR: ${{ secrets.AWS_ECR }} - AWS_REGION: ${{ secrets.AWS_REGION }} - + contents: read \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index b1b9437..00505ff 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,22 @@ # syntax=docker/dockerfile:1 -FROM python:3.11-slim-bullseye +FROM ghcr.io/astral-sh/uv:python3.13-trixie-slim -RUN rm -f /etc/apt/apt.conf.d/docker-clean; \ - echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache +ENV UV_NO_DEV=1 -RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ - --mount=type=cache,target=/var/lib/apt,sharing=locked \ - apt-get update -y && apt-get upgrade -y +WORKDIR /app -WORKDIR /CHANGME-component-name -ADD LICENSE.txt requirements.txt ./ -ADD CHANGEME-module-name ./CHANGEME-module-name/ -ADD pyproject.toml ./ -RUN --mount=type=cache,target=/root/.cache/pip pip3 install -r requirements.txt . +# Install dependencies +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 --frozen --no-install-project -# Change as required, eg -# CMD ["gunicorn", "-w", "2", "-b", "0.0.0.0", "-k", "uvicorn.workers.UvicornWorker", "--log-level", "debug", "mymodule.main:app"] -CMD python -m my.module +# Copy project files +COPY . /app +# Sync the project +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --frozen + +# Change as required +CMD ["uv", "run", "--no-sync", "python", "-m", "my.module"] diff --git a/Makefile b/Makefile index 7e25452..59f38f2 100644 --- a/Makefile +++ b/Makefile @@ -1,58 +1,56 @@ -.PHONY: dockerbuild dockerpush test testonce ruff black lint isort pre-commit-check requirements-update requirements setup VERSION ?= latest IMAGENAME = CHANGEME -DOCKERREPO ?= public.ecr.aws/n1b3o1k2/ukeodhp +DOCKERREPO ?= public.ecr.aws/eodh +uv-run ?= uv run --no-sync +.PHONY: dockerbuild dockerbuild: DOCKER_BUILDKIT=1 docker build -t ${IMAGENAME}:${VERSION} . -dockerpush: dockerbuild testdocker +.PHONY: dockerpush +dockerpush: dockerbuild docker tag ${IMAGENAME}:${VERSION} ${DOCKERREPO}/${IMAGENAME}:${VERSION} docker push ${DOCKERREPO}/${IMAGENAME}:${VERSION} +.PHONY: test test: - ./venv/bin/ptw CHANGEME-test-package-names + ${uv-run} ptw CHANGEME-test-package-names +.PHONY: testonce testonce: - ./venv/bin/pytest + ${uv-run} pytest -ruff: - ./venv/bin/ruff check . - -black: - ./venv/bin/black . - -isort: - ./venv/bin/isort . --profile black - -validate-pyproject: - validate-pyproject pyproject.toml - -lint: ruff black isort validate-pyproject - -requirements.txt: venv pyproject.toml - ./venv/bin/pip-compile +.git/hooks/pre-commit: + ${uv-run} pre-commit install + curl -o .pre-commit-config.yaml https://raw.githubusercontent.com/EO-DataHub/github-actions/main/.pre-commit-config-python.yaml -requirements-dev.txt: venv pyproject.toml - ./venv/bin/pip-compile --extra dev -o requirements-dev.txt +.PHONY: setup +setup: update .git/hooks/pre-commit -requirements: requirements.txt requirements-dev.txt +.PHONY: pre-commit +pre-commit: + ${uv-run} pre-commit -requirements-update: venv - ./venv/bin/pip-compile -U - ./venv/bin/pip-compile --extra dev -o requirements-dev.txt -U +.PHONY: pre-commit-all +pre-commit-all: + ${uv-run} pre-commit run --all-files -venv: - virtualenv -p python3.11 venv - ./venv/bin/python -m ensurepip -U - ./venv/bin/pip3 install pip-tools +.PHONY: check +check: + ${uv-run} ruff check + ${uv-run} ruff format --check --diff + ${uv-run} pyright + ${uv-run} validate-pyproject pyproject.toml -.make-venv-installed: venv requirements.txt requirements-dev.txt - ./venv/bin/pip3 install -r requirements.txt -r requirements-dev.txt - touch .make-venv-installed +.PHONY: format +format: + ${uv-run} ruff check --fix + ${uv-run} ruff format -.git/hooks/pre-commit: - ./venv/bin/pre-commit install - curl -o .pre-commit-config.yaml https://raw.githubusercontent.com/EO-DataHub/github-actions/main/.pre-commit-config-python.yaml +.PHONY: install +install: + uv sync --frozen -setup: venv requirements .make-venv-installed .git/hooks/pre-commit +.PHONY: update +update: + uv sync \ No newline at end of file diff --git a/README.md b/README.md index ec3e448..e72f8bf 100644 --- a/README.md +++ b/README.md @@ -15,85 +15,45 @@ When using this template remember to: ## Getting started +### Prerequisites + +Install the `uv` package manager by following the [official documentation](https://docs.astral.sh/uv/getting-started/installation/). + ### Install via makefile ```commandline make setup ``` -This will create a virtual environment called `venv`, build `requirements.txt` and -`requirements-dev.txt` from `pyproject.toml` if they're out of date, install the Python -and Node dependencies and install `pre-commit`. - -It's safe and fast to run `make setup` repeatedly as it will only update these things if -they have changed. - -After `make setup` you can run `pre-commit` to run pre-commit checks on staged changes and -`pre-commit run --all-files` to run them on all files. This replicates the linter checks that -run from GitHub actions. - - -### Alternative installation - -You will need Python 3.11. On Debian you may need: -* `sudo add-apt-repository -y 'deb http://ppa.launchpad.net/deadsnakes/ppa/ubuntu focal main'` (or `jammy` in place of `focal` for later Debian) -* `sudo apt update` -* `sudo apt install python3.11 python3.11-venv` - -and on Ubuntu you may need -* `sudo add-apt-repository -y 'ppa:deadsnakes/ppa'` -* `sudo apt update` -* `sudo apt install python3.11 python3.11-venv` - -To prepare running it: +This will create a Python virtual environment, install basic testing and linting +dependencies and install `pre-commit`. -* `virtualenv venv -p python3.11` -* `. venv/bin/activate` -* `rehash` -* `python -m ensurepip -U` -* `pip3 install -r requirements.txt` -* `pip3 install -r requirements-dev.txt` - -You should also configure your IDE to use black so that code is automatically reformatted on save. +After `make setup` you can run `make check` to run linting and type checking. ## Building and testing -This component uses `pytest` tests and the `ruff` and `black` linters. `black` will reformat your code in an -opinionated way. +This component uses `pytest` tests and the `ruff` linter and `pyright` type checker. + +See Makefile for all available commands. A number of `make` targets are defined: * `make test`: run tests continuously * `make testonce`: run tests once -* `make lint`: lint and reformat +* `make check`: run linting and type checking +* `make format`: reformat code +* `make install`: install dependencies +* `make update`: update dependencies * `make dockerbuild`: build a `latest` Docker image (use `make dockerbuild `VERSION=1.2.3` for a release image) * `make dockerpush`: push a `latest` Docker image (again, you can add `VERSION=1.2.3`) - normally this should be done only via the build system and its GitHub actions. -## Managing requirements - -Requirements are specified in `pyproject.toml`, with development requirements listed separately. Specify version -constraints as necessary but not specific versions. After changing them: - -* Run `pip-compile` (or `pip-compile -U` to upgrade requirements within constraints) to regenerate `requirements.txt` -* Run `pip-compile --extra dev -o requirements-dev.txt` (again, add `-U` to upgrade) to regenerate - `requirements-dev.txt`. -* Run the `pip3 install -r requirements.txt` and `pip3 install -r requirements-dev.txt` commands again and test. -* Commit these files. - -If you see the error - -```commandline -Backend subprocess exited when trying to invoke get_requires_for_build_wheel -Failed to parse /.../template-python/pyproject.toml -``` - -then install and run `validate-pyproject pyproject.toml` and/or `pip3 install .` to check its syntax. +## Managing dependencies -To check for vulnerable dependencies, run `pip-audit`. +To add a new dependency, run `uv add ` or see `uv help add`. ## Releasing -Ensure that `make lint` and `make test` work correctly and produce no further changes to code formatting before +Ensure that `make check` and `make test` work correctly and produce no further changes to code formatting before continuing. Releases tagged `latest` and targeted at development environments can be created from the `main` branch. Releases for diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..f330c5e --- /dev/null +++ b/app/main.py @@ -0,0 +1,6 @@ +def main() -> None: + print("Hello, World!") + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml index f44785f..957c9f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,8 +18,8 @@ version = "0.1.0" description = "CHANGEME a short description" readme = "README.md" -# Our target version is Python 3.11, but it may work with earlier versions. -requires-python = ">=3.11" +# Our target version is Python 3.13. +requires-python = ">=3.13,<3.14" license = {file = "LICENSE"} @@ -53,29 +53,25 @@ classifiers = [ # "License :: Other/Proprietary License", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3 :: Only", ] -# This field lists other packages that your project depends on to run. -# Any package you put here will be installed by pip when your project is -# installed, so they must be valid existing projects. -# -# For an analysis of this field vs pip's requirements files see: -# https://packaging.python.org/discussions/install-requires-vs-requirements/ + dependencies = [ ] -# List additional groups of dependencies here (e.g. development -# dependencies). Users will be able to install these using the "extras" -# syntax, for example: -# -# $ pip install sampleproject[dev] -# -# Similar to `dependencies` above, these must be valid existing -# projects. -[project.optional-dependencies] # Optional -dev = ["pip-tools", "pytest", "pytest-xdist", "pytest-mock", "pytest-watcher", "black", "ruff", "isort", "pre-commit"] +[dependency-groups] +dev = [ + "pre-commit", + "pyright", + "pytest", + "pytest-mock", + "pytest-watcher", + "pytest-xdist", + "ruff", + "validate-pyproject", +] # List URLs that are relevant to your project # @@ -88,63 +84,46 @@ dev = ["pip-tools", "pytest", "pytest-xdist", "pytest-mock", "pytest-watcher", " # maintainers, and where to support the project financially. The key is # what's used to render the link text on PyPI. [project.urls] # Optional -homepage = "https://github.com/UKEODHP/" -repository = "CHANGEME-https://github.com/UKEODHP/..." -changelog = "CHANGEME-https://github.com/UKEODHP/.../master/CHANGELOG.md" +homepage = "https://github.com/EO-DataHub/" +repository = "CHANGEME-https://github.com/EO-DataHub/" +changelog = "CHANGEME-https://github.com/EO-DataHub//main/CHANGELOG.md" + + +[tool.uv] +# see https://docs.astral.sh/uv/concepts/projects/config/#project-packaging to see if you need to set this to true +package = false [build-system] -# These are the assumed default build requirements from pip: -# https://pip.pypa.io/en/stable/reference/pip/#pep-517-and-518-support -requires = ["setuptools>=69.0.0", "wheel"] -build-backend = "setuptools.build_meta" +requires = ["hatchling"] +build-backend = "hatchling.build" [tool.pytest.ini_options] -pythonpath = ["."] -markers = [ - "integrationtest: Integration test" -] - -[tool.setuptools] -packages = ["CHANGEME-pythonpkgname"] +addopts = ["--import-mode=importlib"] -[tool.black] -line-length = 100 -target-version = ['py311'] [tool.ruff] -line-length = 110 -target-version = "py311" -select = ["E", "F", "B", "W"] - -exclude = [ - ".bzr", - ".direnv", - ".eggs", - ".git", - ".git-rewrite", - ".hg", - ".mypy_cache", - ".nox", - ".pants.d", - ".pytype", - ".ruff_cache", - ".svn", - ".tox", - ".venv", - "__pypackages__", - "_build", - "buck-out", - "build", - "dist", - "node_modules", - "venv", +line-length = 119 + +[tool.ruff.lint] +select = [ + "E", # Pycodestyle errors + "W", # Pycodestyle warnings + "F", # Pyflakes + "I", # isort + "FAST", # FastAPI + "RUF", # RUF-specific rules + "SIM", # Simplifications + "B", # Bugbear + "ANN", # Annotations + "PLC", # Pylint convention + "PLE", # Pylint errors + "UP", # pyupgrade + "FURB", # refurb + "PT", # pytest +] +ignore = [ + "E501", # line too long + "SIM108", # ternary operator + "SIM103", # return bool directly + "B008" # function call in default argument (incompatible with FastAPI) ] - -[tool.pylint.'MESSAGES CONTROL'] -disable = ''' - line-too-long, - missing-class-docstring, - too-many-locals, - too-many-instance-attributes, - logging-fstring-interpolation, -''' diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 3d9b49f..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,78 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.11 -# by the following command: -# -# pip-compile --extra=dev --output-file=requirements-dev.txt -# -black==24.8.0 - # via component-name (pyproject.toml) -build==1.2.2 - # via pip-tools -cfgv==3.4.0 - # via pre-commit -click==8.1.7 - # via - # black - # pip-tools -distlib==0.3.8 - # via virtualenv -execnet==2.1.1 - # via pytest-xdist -filelock==3.16.1 - # via virtualenv -identify==2.6.1 - # via pre-commit -iniconfig==2.0.0 - # via pytest -isort==5.13.2 - # via component-name (pyproject.toml) -mypy-extensions==1.0.0 - # via black -nodeenv==1.9.1 - # via pre-commit -packaging==24.1 - # via - # black - # build - # pytest -pathspec==0.12.1 - # via black -pip-tools==7.4.1 - # via component-name (pyproject.toml) -platformdirs==4.3.6 - # via - # black - # virtualenv -pluggy==1.5.0 - # via pytest -pre-commit==3.8.0 - # via component-name (pyproject.toml) -pyproject-hooks==1.2.0 - # via - # build - # pip-tools -pytest==8.3.3 - # via - # component-name (pyproject.toml) - # pytest-mock - # pytest-xdist -pytest-mock==3.14.0 - # via component-name (pyproject.toml) -pytest-watcher==0.4.3 - # via component-name (pyproject.toml) -pytest-xdist==3.6.1 - # via component-name (pyproject.toml) -pyyaml==6.0.2 - # via pre-commit -ruff==0.6.8 - # via component-name (pyproject.toml) -virtualenv==20.26.6 - # via pre-commit -watchdog==5.0.3 - # via pytest-watcher -wheel==0.44.0 - # via pip-tools - -# The following packages are considered to be unsafe in a requirements file: -# pip -# setuptools diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 8d63c79..0000000 --- a/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.11 -# by the following command: -# -# pip-compile -# diff --git a/tests/test_package.py b/tests/test_package.py index 416fae2..232aee9 100644 --- a/tests/test_package.py +++ b/tests/test_package.py @@ -1,2 +1,2 @@ -def test__success(): +def test__success() -> None: assert True