aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar ChrisJL <[email protected]>2024-08-01 21:29:55 +0100
committerGravatar GitHub <[email protected]>2024-08-01 21:29:55 +0100
commitf3cc0fac7c584b1de8b0e444ddca9e681961687f (patch)
treef68b496fe3390506d50457eaf2aca1cf5f14769f
Initial commit
-rw-r--r--.dockerignore7
-rw-r--r--.gitattributes1
-rw-r--r--.gitignore132
-rw-r--r--.pre-commit-config.yaml40
-rw-r--r--Dockerfile16
-rw-r--r--LICENSE21
-rw-r--r--Makefile18
-rw-r--r--README.md29
-rw-r--r--_.github/dependabot.yml28
-rw-r--r--_.github/workflows/build.yaml42
-rw-r--r--_.github/workflows/lint.yaml32
-rw-r--r--_.github/workflows/main.yaml46
-rw-r--r--app/__init__.py22
-rw-r--r--app/__main__.py6
-rw-r--r--app/settings.py9
-rw-r--r--docker-compose.yml8
-rw-r--r--pyproject.toml49
17 files changed, 506 insertions, 0 deletions
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..bbf578b
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,7 @@
+# Ignore everything
+**
+
+# Except what we need
+!app
+!requirements.txt
+!LICENSE
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..176a458
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+* text=auto
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..51641d6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,132 @@
+# Project specific
+.vscode
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+pip-wheel-metadata/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+.python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..e08fd76
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,40 @@
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.5.0
+ hooks:
+ - id: check-merge-conflict
+ - id: check-toml
+ - id: check-yaml
+ - id: end-of-file-fixer
+ - id: mixed-line-ending
+ args: [--fix=lf]
+ - id: trailing-whitespace
+ args: [--markdown-linebreak-ext=md]
+
+ - repo: local
+ hooks:
+ - id: poetry
+ name: poetry-check
+ description: Checks the validity of the pyproject.toml file.
+ entry: poetry check
+ language: system
+ files: pyproject.toml
+ pass_filenames: false
+ require_serial: true
+
+ - id: ruff-lint
+ name: ruff linting
+ description: Run ruff linting
+ entry: poetry run ruff check --force-exclude
+ language: system
+ 'types_or': [python, pyi]
+ require_serial: true
+ args: [--fix, --exit-non-zero-on-fix]
+
+ - id: ruff-format
+ name: ruff formatting
+ description: Run ruff formatting
+ entry: poetry run ruff format --force-exclude
+ language: system
+ 'types_or': [python, pyi]
+ require_serial: true
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..179d9b3
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,16 @@
+FROM --platform=linux/amd64 python:3.12-slim
+
+# Define Git SHA build argument for sentry
+ARG git_sha="development"
+ENV GIT_SHA=$git_sha
+
+# Install project dependencies
+WORKDIR /app
+COPY requirements.txt ./
+RUN pip install -r requirements.txt
+
+# Copy the source code in last to optimize rebuilding the image
+COPY . .
+
+ENTRYPOINT ["python"]
+CMD ["-m", "app"]
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..74d0eae
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 ChrisJL
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..1375330
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,18 @@
+.PHONEY: setup sync lock lint precommit
+
+setup: sync precommit lint
+
+sync:
+ poetry install --sync
+
+lock:
+ poetry lock
+ @poetry export --only main --output requirements.txt
+ poetry install --sync --no-root
+ pre-commit run --files pyproject.toml poetry.lock requirements.txt
+
+lint:
+ poetry run pre-commit run --all-files
+
+precommit:
+ poetry run pre-commit install
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..4491923
--- /dev/null
+++ b/README.md
@@ -0,0 +1,29 @@
+# Python template
+
+[![License](https://img.shields.io/github/license/owl-corp/python-template)](https://github.com/owl-corp/python-template)
+
+## Summary
+A template Python repo with docker, linting & CI.
+
+To copy this simply press the "Use this template" green button near the top of the repo.
+
+## Changes required copying
+- Pin the dependencies in [`pyproject.toml`](pyproject.toml) to a specific version
+- Update the `tool.poetry` section of in [`pyproject.toml`](pyproject.toml) to be relevant to your project
+- Run `make lock` to lock poetry dependencies and export to a `requirements.txt` file
+- Rename [`_.github/`](_.github/) to `.github` so that CI runs
+
+## Changes to consider after copying
+- Update the [LICENSE](LICENSE) file
+- Update the schedule of the [dependabot config](.github/dependabot.yml)
+- Add a static type checker, such as mypy or Pyright
+- Delete [`.dockerignore](.dockerignore), [`docker-compose.yml`](docker-compose.yml) and the [build step](_.github/workflows/build.yaml) of CI if you do not plan to use docker.
+
+
+# Contributing
+Run `make` from the project root to both install this project's dependencies & install the pre-commit hooks.
+- This requires both [make](https://www.gnu.org/software/make/) & [poetry](https://python-poetry.org/) to be installed.
+
+## Other make targets
+- `make lint` will run the pre-commit linting against all files in the repository
+- `make lock` wil relock project dependencies and update the [`requirements.txt`](./requirements.txt) file with production dependencies
diff --git a/_.github/dependabot.yml b/_.github/dependabot.yml
new file mode 100644
index 0000000..a0ad2a1
--- /dev/null
+++ b/_.github/dependabot.yml
@@ -0,0 +1,28 @@
+version: 2
+updates:
+ - package-ecosystem: "pip"
+ directory: "/"
+ schedule:
+ interval: "daily"
+ groups:
+ python-dependencies:
+ patterns:
+ - "*"
+
+ - package-ecosystem: "docker"
+ directory: "/"
+ schedule:
+ interval: "daily"
+ groups:
+ docker-dependencies:
+ patterns:
+ - "*"
+
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "daily"
+ groups:
+ ci-dependencies:
+ patterns:
+ - "*"
diff --git a/_.github/workflows/build.yaml b/_.github/workflows/build.yaml
new file mode 100644
index 0000000..23db7e0
--- /dev/null
+++ b/_.github/workflows/build.yaml
@@ -0,0 +1,42 @@
+on:
+ workflow_call:
+ inputs:
+ sha-tag:
+ description: "A short-form SHA tag for the commit that triggered this flow"
+ required: true
+ type: string
+ lower-repo:
+ description: "The repository name in lowercase"
+ required: true
+ type: string
+
+jobs:
+ build:
+ name: Build & Push
+ runs-on: ubuntu-latest
+
+ steps:
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Login to Github Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.repository_owner }}
+ password: ${{ github.token }}
+
+ # Build and push the container to the GitHub Container
+ # Repository. The container will be tagged as "latest"
+ # and with the short SHA of the commit.
+ - name: Build and push
+ uses: docker/build-push-action@v5
+ with:
+ push: ${{ github.ref == github.event.repository.default_branch }}
+ cache-from: type=registry,ref=ghcr.io/${{ inputs.lower-repo }}:latest
+ cache-to: type=inline
+ tags: |
+ ghcr.io/${{ inputs.lower-repo }}:latest
+ ghcr.io/${{ inputs.lower-repo }}:${{ inputs.sha-tag }}
+ build-args: git_sha=${{ github.sha }}
diff --git a/_.github/workflows/lint.yaml b/_.github/workflows/lint.yaml
new file mode 100644
index 0000000..44b58ca
--- /dev/null
+++ b/_.github/workflows/lint.yaml
@@ -0,0 +1,32 @@
+on:
+ workflow_call:
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Install Python Dependencies
+ uses: HassanAbouelela/actions/setup-python@setup-python_v1.4.2
+ with:
+ python_version: "3.12"
+ install_args: "--only linting"
+
+ - name: Ensure requirements.txt is up to date
+ shell: bash
+ run: |
+ poetry export --output temp-requirements.txt -vvv
+
+ if ! cmp -s "requirements.txt" "temp-requirements.txt"; then
+ echo "::error file=requirements.txt,title=Requirements out of date!::Run 'poetry export --output requirements.txt'"
+ exit 1
+ fi
+
+ - name: Run pre-commit hooks
+ run: SKIP=ruff-lint pre-commit run --all-files
+
+ # Run `ruff` using github formatting to enable automatic inline annotations.
+ - name: Run ruff
+ run: "ruff check --output-format=github ."
diff --git a/_.github/workflows/main.yaml b/_.github/workflows/main.yaml
new file mode 100644
index 0000000..c109acd
--- /dev/null
+++ b/_.github/workflows/main.yaml
@@ -0,0 +1,46 @@
+name: main
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ generate-inputs:
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ shell: bash
+
+ outputs:
+ sha-tag: ${{ steps.sha-tag.outputs.sha-tag }}
+ lower-repo: ${{ steps.lower-repo.outputs.lower-repo }}
+
+ steps:
+ - name: Create SHA Container Tag
+ id: sha-tag
+ run: |
+ tag=$(cut -c 1-7 <<< $GITHUB_SHA)
+ echo "sha-tag=$tag" >> $GITHUB_OUTPUT
+ # docker/build-push-action doesn't allow capital letters in the URL
+ - name: Get repo in lowercase
+ id: lower-repo
+ run: |
+ echo "lower-repo=${GITHUB_REPOSITORY,,}" >> $GITHUB_OUTPUT
+
+ lint:
+ uses: ./.github/workflows/lint.yaml
+
+ build:
+ uses: ./.github/workflows/build.yaml
+ needs:
+ - lint
+ - generate-inputs
+ with:
+ sha-tag: ${{ needs.generate-inputs.outputs.sha-tag }}
+ lower-repo: ${{ needs.generate-inputs.outputs.lower-repo }}
diff --git a/app/__init__.py b/app/__init__.py
new file mode 100644
index 0000000..8bf4913
--- /dev/null
+++ b/app/__init__.py
@@ -0,0 +1,22 @@
+import logging
+
+from app.settings import SETTINGS
+
+# Console handler prints to terminal
+console_handler = logging.StreamHandler()
+level = logging.DEBUG if SETTINGS.debug else logging.INFO
+console_handler.setLevel(level)
+
+# Remove old loggers, if any
+root = logging.getLogger()
+if root.handlers:
+ for handler in root.handlers:
+ root.removeHandler(handler)
+
+# Setup new logging configuration
+logging.basicConfig(
+ format="%(asctime)s - %(name)s %(levelname)s: %(message)s",
+ datefmt="%D %H:%M:%S",
+ level=logging.DEBUG if SETTINGS.debug else logging.INFO,
+ handlers=[console_handler],
+)
diff --git a/app/__main__.py b/app/__main__.py
new file mode 100644
index 0000000..d2f7b64
--- /dev/null
+++ b/app/__main__.py
@@ -0,0 +1,6 @@
+import logging
+
+from app.settings import SETTINGS
+
+log = logging.getLogger(__name__)
+log.info("Hello world! %s", SETTINGS.git_sha)
diff --git a/app/settings.py b/app/settings.py
new file mode 100644
index 0000000..cca11aa
--- /dev/null
+++ b/app/settings.py
@@ -0,0 +1,9 @@
+from pydantic_settings import BaseSettings
+
+
+class _Settings(BaseSettings, env_file=".env", env_file_encoding="utf-8"):
+ debug: bool = False
+ git_sha: str = "development"
+
+
+SETTINGS: _Settings = _Settings()
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..b32c568
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,8 @@
+services:
+ app:
+ build: .
+ restart: unless-stopped
+ volumes:
+ - ./app:/app/app:ro
+ env_file:
+ - .env
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..c61566d
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,49 @@
+[tool.poetry]
+name = "python-template"
+version = "1.2.0"
+description = "A template project for Python applications."
+authors = ["Chris Lovering <[email protected]>"]
+license = "MIT"
+
+[tool.poetry.dependencies]
+python = "3.12.*"
+
+pydantic = "*"
+pydantic-settings = "*"
+
+[tool.poetry.group.linting.dependencies]
+pre-commit = "*"
+ruff = "*"
+
+[tool.poetry.group.dev.dependencies]
+poetry-plugin-export = "*"
+
+[build-system]
+requires = ["poetry-core>=1.5.0"]
+build-backend = "poetry.core.masonry.api"
+
+[tool.ruff]
+target-version = "py312"
+extend-exclude = [".cache"]
+line-length = 120
+unsafe-fixes = true
+preview = true
+output-format = "concise"
+
+[tool.ruff.lint]
+select = ["ALL"]
+ignore = [
+ "ANN002", "ANN003", "ANN101", "ANN102",
+ "C901",
+ "CPY001",
+ "D100", "D104", "D105", "D107", "D203", "D212", "D214", "D215", "D416",
+
+ # Rules suggested to be ignored when using ruff format
+ "COM812", "COM819", "D206", "E111", "E114", "E117", "E501", "ISC001", "Q000", "Q001", "Q002", "Q003", "W191"
+]
+
+[tool.ruff.lint.isort]
+known-first-party = ["app"]
+order-by-type = false
+case-sensitive = true
+combine-as-imports = true