Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ __pycache__/
.venv/
.venv
.env/
.env.development
.env.production
.Python
.mypy_cache/
.ruff_cache/
Expand All @@ -16,6 +18,7 @@ __pycache__/
.hypothesis/
pip-log.txt
pip-delete-this-directory.txt
requirements.txt

# Editor directories and OS files
.idea/
Expand All @@ -41,4 +44,14 @@ mkdocs.yml

# Project files you don't want in image
LICENSE
Makefile
Makefile

# Github
.devcontainer/
.github/

# Docker files
dev.Dockerfile
prod.Dockerfile
docker-compose.yml
.dockerignore
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ celerybeat.pid

# Environments
.env
.env.development
.env.test
.env.production
.venv
env/
venv/
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
## 0.9.0 (2025-06-07)

### Feat

- add loading env variables based on app stage

### Fix

- move getting correct path to env file to another variable
- fix name of workflow

## 0.8.0 (2025-06-05)

### Feat
Expand Down
47 changes: 34 additions & 13 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
PROJECT_NAME = python-boilerplate
DEV_NAME = dev
PROD_NAME = prod
d = docker
dc = docker compose

# ============ Commands for local development

run:
uv run __main__.py
Expand All @@ -15,26 +21,41 @@ format:
typecheck:
uv run mypy --config-file=pyproject.toml --explicit-package-bases ./src/

docker-build:
docker build -t $(PROJECT_NAME) .
dev-logs:
$(d) logs -f $(DEV_NAME)

dev-exec:
$(d) exec -it $(DEV_NAME) /bin/bash

docker-run:
docker run --rm -it --env-file example.env $(PROJECT_NAME)
dev-bash:
$(d) run --rm -it --env-file .env.development $(PROJECT_NAME):dev /bin/bash

compose-build:
docker compose up --build -d
dev-build:
$(dc) --env-file=.env.development build

compose-up:
docker compose up -d
dev-up:
$(dc) --env-file=.env.development up -d

compose-stop:
docker compose stop
dev-stop:
$(dc) stop

compose-down:
docker compose down
dev-down:
$(dc) down

clean:
find . -type d -name '__pycache__' -exec rm -rf {} +
rm -rf .mypy_cache .ruff_cache .pytest_cache

.PHONY: run test lint format typecheck check docker-build docker-run compose-build compose-up compose-stop compose-down clean

# ============ Commands to check prod image

prod-build:
$(d) build -t $(PROJECT_NAME):prod -f prod.Dockerfile .

prod-run:
$(d) run -d --env-file .env.production --name $(PROD_NAME) $(PROJECT_NAME):prod

prod-logs:
$(d) logs -f $(PROD_NAME)

.PHONY: run test lint format typecheck dev-logs dev-exec dev-bash dev-build dev-up dev-stop dev-down clean prod-build prod-run
3 changes: 3 additions & 0 deletions __main__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from src.config.env import APP_STAGE, ENV_PATH
from src.utils.logging import setup_logging


def main() -> None:
logger = setup_logging()
logger.info("Starting the application...")
logger.info(f"App stage: {APP_STAGE}")
logger.info(f"Environment variables loaded from: {ENV_PATH}")


if __name__ == "__main__":
Expand Down
36 changes: 36 additions & 0 deletions dev.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Example taken from https://github.com/astral-sh/uv-docker-example/blob/main/multistage.Dockerfile
# An example using multi-stage image builds to create a final image without uv.

# First, build the application in the `/app` directory.
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS builder
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy

# Disable Python downloads, because we want to use the system interpreter
# across both images. If using a managed Python version, it needs to be
# copied from the build image into the final image; see `standalone.Dockerfile`
# for an example.
ENV UV_PYTHON_DOWNLOADS=0

WORKDIR /app
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 --locked
COPY . /app
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked

FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim

# Copy the application from the builder
COPY --from=builder --chown=app:app /app /app

WORKDIR /app

# Place executables in the environment at the front of the path
ENV PATH="/app/.venv/bin:$PATH"

# Set the application stage to production
ENV APP_STAGE="production"

CMD ["/bin/sh", "./docker/docker-entrypoint.sh"]
23 changes: 11 additions & 12 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,39 +1,38 @@
# rename to your project name
name: python-boilerplate


# rename to your network
networks:
project-network:
name: project-network
name: ${DOCKER_NETWORK_NAME}
driver: bridge

services:
python-boilerplate:
image: python-boilerplate
container_name: py-blplt
image: ${DOCKER_PROJECT_NAME}:${DOCKER_IMAGE_TAG}
container_name: ${DOCKER_CONTAINER_NAME}
build:
context: ./
dockerfile: Dockerfile
context: .
dockerfile: dev.Dockerfile

develop:
watch:
# Sync the working directory with the `/app` directory in the container
- action: sync
path: .
target: /app
# Exclude the project virtual environment
ignore:
- app/.venv/

# Rebuild the image on changes to the `pyproject.toml`
- action: rebuild
path: ./app/pyproject.toml

ports:
- "8080:8000"
restart: on-failure

# Можливо, краще монтувати все в /app (якщо твій код у src, скоригуй)
volumes:
- ./src/:/src/
- ./docs/:/app/docs
- ./tests/:/app/tests
- ./src/:/app/src/
networks:
- project-network
- project-network
6 changes: 6 additions & 0 deletions example.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
DOCKER_PROJECT_NAME=python-boilerplate
DOCKER_NETWORK_NAME=project-network
DOCKER_IMAGE_TAG=dev
DOCKER_CONTAINER_NAME=dev

APP_STAGE=development
6 changes: 5 additions & 1 deletion Dockerfile → prod.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
# An example using multi-stage image builds to create a final image without uv.

# First, build the application in the `/app` directory.
# See `Dockerfile` for details.
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS builder
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy

Expand All @@ -21,6 +20,8 @@ COPY . /app
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-dev

# Remove the uv lock file and pyproject.toml, so that the final image does not
RUN rm -f pyproject.toml uv.lock .python-version

# Then, use a final image without uv
FROM python:3.12-slim-bookworm
Expand All @@ -36,4 +37,7 @@ WORKDIR /app
# Place executables in the environment at the front of the path
ENV PATH="/app/.venv/bin:$PATH"

# Set the application stage to production
ENV APP_STAGE="development"

ENTRYPOINT ["/bin/sh", "./docker/docker-entrypoint.sh"]
11 changes: 6 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
[project]
name = "python-boilerplate"
version = "0.8.0"
version = "0.9.0"
description = "A template for quickly starting new projects in Python "
readme = "README.md"
requires-python = ">=3.12"
dependencies = []
dependencies = ["environs>=14.2.0"]

[dependency-groups]
dev = [
Expand All @@ -28,7 +28,7 @@ lint.select = [
"N", # pep8-naming
# "D", # pydocstyle
"UP", # pyupgrade
"I", # isort
"I", # isort
]

# Ignore some rules that may be too strict for general use
Expand Down Expand Up @@ -73,12 +73,13 @@ section-order = [
[tool.ruff.format]
quote-style = "double" # Use double quotes
indent-style = "space" # Use spaces for indentation
line-ending = "auto" # Auto-detect line endings
line-ending = "auto" # Auto-detect line endings

# ================================= MYPY
[tool.mypy]
strict = true # Enable strict type checking
strict = true # Enable strict type checking
explicit_package_bases = true
ignore_missing_imports = true # Ignore missing imports for third-party libraries

# ================================= COMMITIZEN
[tool.commitizen]
Expand Down
15 changes: 13 additions & 2 deletions src/config/env.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import os
from pathlib import Path
from typing import Final

from environs import Env

ABS_PATH: Final[Path] = Path(__file__).resolve().parent.parent.parent

ENV_PATH: Final[Path] = ABS_PATH / ".env"
APP_STAGE: Final[str] = os.getenv("APP_STAGE", "development")

ENV_FILE_MAP: Final[dict[str, Path]] = {
"development": ABS_PATH / ".env.development",
"production": ABS_PATH / ".env.production",
}

ENV_PATH: Final[Path] = ENV_FILE_MAP[APP_STAGE]

print(ENV_PATH)
env = Env()
env.read_env(ENV_PATH)