diff options
author | 2024-08-01 21:29:55 +0100 | |
---|---|---|
committer | 2024-08-01 21:29:55 +0100 | |
commit | f3cc0fac7c584b1de8b0e444ddca9e681961687f (patch) | |
tree | f68b496fe3390506d50457eaf2aca1cf5f14769f |
Initial commit
-rw-r--r-- | .dockerignore | 7 | ||||
-rw-r--r-- | .gitattributes | 1 | ||||
-rw-r--r-- | .gitignore | 132 | ||||
-rw-r--r-- | .pre-commit-config.yaml | 40 | ||||
-rw-r--r-- | Dockerfile | 16 | ||||
-rw-r--r-- | LICENSE | 21 | ||||
-rw-r--r-- | Makefile | 18 | ||||
-rw-r--r-- | README.md | 29 | ||||
-rw-r--r-- | _.github/dependabot.yml | 28 | ||||
-rw-r--r-- | _.github/workflows/build.yaml | 42 | ||||
-rw-r--r-- | _.github/workflows/lint.yaml | 32 | ||||
-rw-r--r-- | _.github/workflows/main.yaml | 46 | ||||
-rw-r--r-- | app/__init__.py | 22 | ||||
-rw-r--r-- | app/__main__.py | 6 | ||||
-rw-r--r-- | app/settings.py | 9 | ||||
-rw-r--r-- | docker-compose.yml | 8 | ||||
-rw-r--r-- | pyproject.toml | 49 |
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"] @@ -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 + +[](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 |