diff --git a/.devcontainer/Dockerfile.dev b/.devcontainer/Dockerfile.dev index cebe14c..d51fffb 100644 --- a/.devcontainer/Dockerfile.dev +++ b/.devcontainer/Dockerfile.dev @@ -7,5 +7,6 @@ RUN apt-get update -y && apt-get install --no-install-recommends -y \ RUN apt install nano -y -# Installing the requirements.txt +# Install common python packages for development RUN pip install --upgrade pip +RUN pip install black pytest coverage poetry pylint pre-commit diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index aa2c14b..f2ac535 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,8 +2,8 @@ // README at: https://github.com/devcontainers/templates/tree/main/src/anaconda { "name": "Python 3.12", - - "build": { + + "build": { "dockerfile": "Dockerfile.dev" }, @@ -14,7 +14,7 @@ // "forwardPorts": [], // Use 'postCreateCommand' to run commands after the container is created. - "postCreateCommand": "python --version", + "postCreateCommand": "python --version && pre-commit install", "customizations": { "vscode": { "extensions": [ diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..58124a6 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "pip" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" + - package-ecosystem: "docker" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" diff --git a/.github/workflows/publish-docker-image.yml b/.github/workflows/publish-docker-image.yml new file mode 100644 index 0000000..a13bfa5 --- /dev/null +++ b/.github/workflows/publish-docker-image.yml @@ -0,0 +1,71 @@ +name: Template for Docker Image Push to Docker Hub +on: + push: + branches: [ "main", "dev"] + paths: + - 'Dockerfile' + - 'app/**' + - 'tests/**' + - 'pyproject.toml' + - '.github/workflows/publish-docker-image.yml' + + workflow_dispatch: # allow user to specify which + inputs: + environment: + description: 'Target environment' + required: true + default: 'dev' # default value if not provided + +permissions: + contents: read + packages: write + +jobs: + push_to_registry: + name: Push Docker Image to Docker Hub + runs-on: ubuntu-latest + steps: + - name: Checkout the repo + uses: actions/checkout@v4 + - name: Build version + id: date + run: echo "{name}=date::$(date +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5.5.1 + with: + images: | + ninjaasa/template-python-project + ghcr.io/${{ github.repository }} + # tag image to be latest if pushing to main branch, dev if pushing to dev branch. + # also tag combined with run_id to ensure unique tag + tags: | + type=raw,value=${{ github.ref == 'refs/heads/main' && 'latest' || 'dev' }}_${{ github.run_id }} + type=raw,value=${{ github.ref == 'refs/heads/main' && 'latest' || 'dev' }} + labels: | + org.label-schema.build-date=${{ steps.date.outputs.date}} + org.opencontainers.image.created=${{ steps.date.outputs.date}} + + - name: Build and push Docker image + uses: docker/build-push-action@v5.3.0 + with: + context: . + file: Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 0000000..370812c --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,78 @@ +name: Run Tests + +on: + pull_request: + # Run when PR is submitted to main branch + branches: [ main ] + push: + branches: + # Run when code is pushed to main branch + - main + schedule: + # Run every weekday at 2am + - cron: "0 2 * * 1-5" + workflow_dispatch: + # Run when manually triggered + +permissions: + actions: read + contents: read + pull-requests: write + +jobs: + test: + name: Unit Tests and Coverage + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12"] + steps: + - uses: actions/checkout@v4.1.4 + - name: Set up Python + uses: actions/setup-python@v5.1.0 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install poetry + poetry install + - name: Run tests + run: poetry run pytest + - name: Check Coverage + if: matrix.python-version == '3.10' + run: | + poetry run coverage run -m pytest + poetry run coverage xml + echo "COVERAGE_PERCENT=$(poetry run coverage report --format=total | awk '{print $NF}' | tr -d '%')" >> $GITHUB_ENV + + - name: Python Coverage + uses: orgoro/coverage@v3.1 + # only if pull request or push + if: github.event_name == 'pull_request' && matrix.python-version == '3.10' + with: + # local path to a coverage xml file like the output of pytest --cov + coverageFile: "coverage.xml" + # github token + token: ${{ secrets.GITHUB_TOKEN }} + # the coverage threshold for average over all files [0,1] + thresholdAll: 0.7 + + - name: Dynamic Badges + if: matrix.python-version == '3.10' + uses: Schneegans/dynamic-badges-action@v1.7.0 + with: + # Your secret with the gist scope + auth: ${{ secrets.GIST_SECRET }} + # The ID of the gist to use + gistID: 8e54c78cf86c9b23df72f9f987282266 + # The *.json or *.svg filename of the badge data + filename: template-python-project-coverage.json + # The left text of the badge + label: Coverage + # The right text of the badge + message: ${{ env.COVERAGE_PERCENT }} % + valColorRange: ${{ env.COVERAGE_PERCENT }} + maxColorRange: 100 + minColorRange: 0 + forceUpdate: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..66354c1 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,45 @@ +repos: + - repo: https://github.com/PyCQA/isort + rev: 5.13.2 + hooks: + - id: isort + name: isort + args: ["--profile=black"] + - id: isort + name: isort (cython) + types: [cython] + args: ["--profile=black"] + + - repo: https://github.com/psf/black-pre-commit-mirror + rev: 24.4.2 + hooks: + - id: black + # It is recommended to specify the latest version of Python + # supported by your project here, or alternatively use + # pre-commit's default_language_version, see + # https://pre-commit.com/#top_level-default_language_version + language_version: python3.12 + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: check-added-large-files + - id: check-ast + - id: check-builtin-literals + - id: check-case-conflict + - id: check-docstring-first + - id: check-shebang-scripts-are-executable + - id: check-merge-conflict + - id: debug-statements + - id: destroyed-symlinks + - id: detect-private-key + - id: end-of-file-fixer + exclude: ^LICENSE|\.(html|csv|txt|svg|py)$ + - id: name-tests-test + args: [--pytest-test-first] + - id: no-commit-to-branch + args: [--branch, main] + - id: requirements-txt-fixer + - id: trailing-whitespace + args: [--markdown-linebreak-ext=md] + exclude: \.(html|svg)$ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..906bcc3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,37 @@ +# Specifying the base image +FROM python:3.12-bullseye + +# Install system dependencies and pip +RUN apt-get update -y && apt-get install --no-install-recommends -y \ + python3-pip + +RUN apt install nano -y + +# Installing the requirements.txt +RUN pip install --upgrade pip + +# Install local package and its requirements +COPY . /workspace +WORKDIR /workspace +RUN pip install . + +# Set labels +LABEL org.opencontainers.image.source=https://github.com/ninja-asa/template-python-project/ +LABEL org.label-schema.vcs-url=https://github.com/ninja-asa/template-python-project/ + +LABEL org.opencontainers.image.description="Template Python Project." +LABEL org.label-schema.description="Template Python Project" + +LABEL org.opencontainers.image.licenses="MIT" + +LABEL org.label-schema.schema-version="1.0" + +LABEL org.label-schema.docker.cmd="docker run" + +LABEL org.opencontainers.image.authors="ninja-asa" + +LABEL org.opencontainers.image.title="Template Python Project" +LABEL org.label-schema.title="Template Python Project" + +# Start the application +CMD ["python", "app"] diff --git a/Github.md b/Github.md new file mode 100644 index 0000000..7b31132 --- /dev/null +++ b/Github.md @@ -0,0 +1,41 @@ +# Github Actions + +Templates for Github Actions workflows I have used for Continuous Testing and Integration can be found in the `.github/workflows` directory. + +## Run Unit Tests and Code Coverage + +This workflow has the following jobs: +- run unit tests using `pytest` +- run code coverage using `coverage` and generating a report published in the PR +- update the code coverage badge in the `README.md` file + +If using this template, you will need to perform the following steps: +- In [github.com](https://github.com): + - add a secret `GIST_SECRET` with read and write access to gists in your repository settings + - add a secret to dependabot `GIST_SECRET` with read and write access to gists in your repository settings + - create a public gist with the code coverage badge + - add `Read and Write` permissions under the repository settings, `Actions` section, to the `GITHUB_TOKEN` secret (allows publishing the coverage report in the PR) +- In the [workflow file](.github/workflows/run_tests.yml): + - update the `GIST_ID` and `filename` to match the gist you created +- In the [README.md](README.md): + - update the code coverage badge URL to contain the url to the gist you created + +Useful links: +- [Github Action - Dynamic Badges](https://github.com/marketplace/actions/dynamic-badges) + +## Publish Docker Image + +This workflow has the following jobs: +- build the docker image +- publish the docker image to the Github Container Registry and Docker Hub + +If using this template, you will need to perform the following steps: +- In [dockerhub.com](https://hub.docker.com): + - create a token with read and write access to your repositories +- In [github.com](https://github.com): + - add a secret `DOCKERHUB_USERNAME` with your Docker Hub username + - add a secret `DOCKERHUB_TOKEN` with your Docker Hub token +- In the [workflow file](.github/workflows/publish_docker_image.yml): + - update the `image_name` to match the name of your image + - update the `dockerfile` to match the name of your Dockerfile + - validate the tags being used diff --git a/README.md b/README.md index 888a79f..4aa5eb7 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,46 @@ -# template-python-project -This is intended to be a repository to serve as a template to my own, and others projects using Python. +# template-python-project :page_facing_up: + +[![Run Tests](https://github.com/ninja-asa/template-python-project/actions/workflows/unit-tests.yml/badge.svg)](https://github.com/ninja-asa/template-python-project/actions/workflows/unit-tests.yml) +![Coverage Badge](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/ninja-asa/8e54c78cf86c9b23df72f9f987282266/raw/7f5d2722c29497fa777f925552778219a137756d/template-python-project-coverage.json) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +This is intended to be a repository to serve as a template to my own, and others projects using Python. + +## Contents + +- `.github/workflows/`: contains the GitHub Actions workflows. +- `.vscode/`: contains the settings for Visual Studio Code. +- `.devcontainer/`: contains the settings for the development container and the development Dockerfile. +- `app/`: contains the source code of the project. +- `tests/`: contains the tests of the project. ## Getting Started + +This template relies on using Docker for development and using Visual Studio Code as the IDE. + +### Pre-requisites +- [Docker](https://www.docker.com/) +- [Visual Studio Code](https://code.visualstudio.com/) +- [Remote - Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) + +### Step by Step +To start developing in any repository using this template: - Clone the repository - Open the repository in VSCode - Setup intended Python version in the `Dockerfile.dev` - Press `F1` and type `Remote-Containers: Reopen in Container` -- Start developing +- Start developing :rocket: + +> During this setup, suggested extensions will be installed in the container, and the Python environment will be created, with `pytest`, `coverage`, `poetry` and `black` installed. -## Features -### Docker-based development -This is tuned for VSCode and to support container based development. +### Configuration -You will find the `.vscode` directory with the files needed to make it work. However, before starting to develop, check the `python` version in the `Dockerfile.dev` - ensure you are using a version that suits your needs. +- [Github Action](Github.md): details needed configuration for the GitHub Actions workflows. ## Useful links: - support status of `python` in the [Python Developer's Guide](https://devguide.python.org/versions/#versions). - vulnerabilities in the [Mailing List by Python Software Foundation CVE Numbering Authority and Python Security Response Team](https://mail.python.org/archives/list/security-announce@python.org/latest). -- vulnerabilities in the +- Microsoft Package Template for Python [here](https://github.com/microsoft/python-package-template/blob/main/pyproject.toml). ## Common Issues ### Dev Container Cannot Start - Issue with communicating with Docker Enginer @@ -47,4 +70,4 @@ Issue: - Dev container cannot start building Solution: -- Have no internet connectivity to get docker image from remote registry +- Have no internet connectivity to get docker image from remote registry \ No newline at end of file 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..9eb63ed --- /dev/null +++ b/app/__main__.py @@ -0,0 +1,7 @@ +from placeholder import placeholder + +def main(): + print(placeholder()) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/app/placeholder.py b/app/placeholder.py new file mode 100644 index 0000000..7f4f510 --- /dev/null +++ b/app/placeholder.py @@ -0,0 +1,2 @@ +def placeholder(): + return True \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..29c5158 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,81 @@ +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry] +name = "app" +version = "1.0.0" +description = "" +authors = ["ninja-asa"] + +[tool.poetry.dependencies] +python = "^3.10" +pandas = "^2.2.2" + +[tool.poetry.dev-dependencies] +pytest = "^8.2.0" +black = "^24.4.2" +coverage = "^7.5.0" + +[tool.pytest.ini_options] +minversion = "6.0" +addopts = "-ra -q" +testpaths = [ + "tests" +] + +[tool.black] +line-length = 100 +include = '\.pyi?$' +exclude = ''' +/( + \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist +)/ +''' + +[tool.coverage.run] +source = ["app"] +branch = true +omit = [ + "*/__init__.py", + "*/__main__.py", + "*/tests/*", + "*/test_*", + "*/setup.py", + "*/conftest.py", + "*/_version.py", + "*main.py", + "*config.py", +] + +[tool.coverage.report] +# Regexes for lines to exclude from consideration +exclude_also = [ + # Don't complain about missing debug-only code: + "def __repr__", + "if self\\.debug", + + # Don't complain if tests don't hit defensive assertion code: + "raise AssertionError", + "raise NotImplementedError", + + # Don't complain if non-runnable code isn't run: + "if 0:", + "if __name__ == .__main__.:", + + # Don't complain about abstract methods, they aren't run: + "@(abc\\.)?abstractmethod", + ] + +ignore_errors = true + +[tool.coverage.html] +directory = "coverage_html_report" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/placeholder_test.py b/tests/placeholder_test.py new file mode 100644 index 0000000..5429de0 --- /dev/null +++ b/tests/placeholder_test.py @@ -0,0 +1,4 @@ +from app.placeholder import placeholder + +def test_placeholder(): + assert placeholder() \ No newline at end of file