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
22 changes: 21 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,32 @@ jobs:
- uses: ./.github/actions/setup-python-dev
- name: Test with pytest
run: |
uv run -m pytest --cov-report html
uv run -m pytest --cov-report html --cov-report term
- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: htmlcov
bandit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-python-dev
- name: Security check
run: |
uv run bandit -r example/ -f json -o bandit-report.json || true
- uses: actions/upload-artifact@v4
with:
name: bandit-report
path: bandit-report.json
pip-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-python-dev
- name: Audit dependencies
run: |
uv run pip-audit --desc
version-check:
runs-on: ubuntu-latest
steps:
Expand Down
28 changes: 28 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- id: check-toml
- id: check-merge-conflict
- id: mixed-line-ending

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.4
hooks:
- id: ruff
args: [--fix]
- id: ruff-format

- repo: local
hooks:
- id: mypy
name: mypy
entry: uv run mypy
language: system
types: [python]
require_serial: true
args: [--config-file=pyproject.toml]
2 changes: 1 addition & 1 deletion .python-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.12
3.13
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Multi-stage Dockerfile for Python Template Server
# Stage 1: Build stage - build wheel using uv
FROM python:3.12-slim AS builder
FROM python:3.13-slim AS builder

WORKDIR /build

Expand All @@ -16,7 +16,7 @@ COPY pyproject.toml .here LICENSE README.md ./
RUN uv build --wheel

# Stage 2: Runtime stage
FROM python:3.12-slim
FROM python:3.13-slim

# Build arguments for environment-specific config
ARG ENV=dev
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
[![python](https://img.shields.io/badge/Python-3.12-3776AB.svg?style=flat&logo=python&logoColor=ffd343)](https://docs.python.org/3.12/)
[![python](https://img.shields.io/badge/Python-3.13-3776AB.svg?style=flat&logo=python&logoColor=ffd343)](https://docs.python.org/3.13/)
[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
[![FastAPI](https://img.shields.io/badge/FastAPI-Latest-009688?style=flat&logo=fastapi&logoColor=white)](https://fastapi.tiangolo.com/)
[![CI](https://img.shields.io/github/actions/workflow/status/javidahmed64592/python-template-server/ci.yml?branch=main&style=flat-square&label=CI&logo=github)](https://github.com/javidahmed64592/python-template-server/actions/workflows/ci.yml)
[![Docker](https://img.shields.io/github/actions/workflow/status/javidahmed64592/python-template-server/docker.yml?branch=main&style=flat-square&label=Docker&logo=github)](https://github.com/javidahmed64592/python-template-server/actions/workflows/docker.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

<!-- omit from toc -->
Expand All @@ -21,6 +23,7 @@ A production-ready FastAPI server template with built-in authentication, rate li
- [Using as a Template](#using-as-a-template)
- [Docker Deployment](#docker-deployment)
- [Documentation](#documentation)
- [License](#license)

## Features

Expand All @@ -46,7 +49,7 @@ This project uses a **`TemplateServer` base class** that encapsulates cross-cutt

### Prerequisites

- Python 3.12+
- Python 3.13+
- [uv](https://docs.astral.sh/uv/) package manager

Install `uv`:
Expand Down
13 changes: 11 additions & 2 deletions docs/SMG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ This document outlines how to configure and setup a development environment to w

## Backend (Python)

[![Python](https://img.shields.io/badge/Python-3.12-3776AB?style=flat-square&logo=python&logoColor=ffd343)](https://docs.python.org/3.12/)
[![Python](https://img.shields.io/badge/Python-3.13-3776AB?style=flat-square&logo=python&logoColor=ffd343)](https://docs.python.org/3.13/)
[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json&style=flat-square)](https://github.com/astral-sh/uv)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json&style=flat-square)](https://github.com/astral-sh/ruff)
[![FastAPI](https://img.shields.io/badge/FastAPI-Latest-009688?style=flat-square&logo=fastapi&logoColor=white)](https://fastapi.tiangolo.com/)
Expand Down Expand Up @@ -79,6 +79,12 @@ To include development dependencies:
uv sync --extra dev
```

After installing dev dependencies, set up pre-commit hooks:

```sh
uv run pre-commit install
```

### Setting Up Certificates and Authentication

Before running the server, you need to generate SSL certificates and an API authentication token.
Expand Down Expand Up @@ -138,7 +144,10 @@ curl -k -H "X-API-Key: your-token-here" https://localhost:443/api/your-endpoint

### Testing, Linting, and Type Checking

- **Run tests:** `uv run pytest`
- **Run all pre-commit checks:** `uv run pre-commit run --all-files`
- **Lint code:** `uv run ruff check .`
- **Format code:** `uv run ruff format .`
- **Type check:** `uv run mypy .`
- **Run tests:** `uv run pytest`
- **Security scan:** `uv run bandit -r example/`
- **Audit dependencies:** `uv run pip-audit`
39 changes: 26 additions & 13 deletions docs/WORKFLOWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,42 @@ It consists of the following jobs:

### validate-pyproject
- Checkout code
- Install uv with caching
- Set up Python from `.python-version`
- Install dependencies with `uv sync --extra dev`
- Validate `pyproject.toml` using `uv run validate-pyproject pyproject.toml`
- Setup Python environment with dev dependencies (via custom action)
- Validate `pyproject.toml` structure using `validate-pyproject`

### ruff
- Checkout code
- Run Ruff linter using `chartboost/ruff-action@v1`
- Setup Python environment with dev dependencies (via custom action)
- Run Ruff linter with `uv run -m ruff check`

### mypy
- Checkout code
- Install uv with caching
- Set up Python from `.python-version`
- Install dependencies with `uv sync --extra dev`
- Setup Python environment with dev dependencies (via custom action)
- Run mypy type checking with `uv run -m mypy .`

### test
- Checkout code
- Install uv with caching
- Set up Python from `.python-version`
- Install dependencies with `uv sync --extra dev`
- Run pytest with coverage report using `uv run -m pytest --cov-report html`
- Upload coverage report as artifact
- Setup Python environment with dev dependencies (via custom action)
- Run pytest with coverage (HTML and terminal reports) using `uv run -m pytest --cov-report html --cov-report term`
- Fails if coverage drops below 80% (configured in `pyproject.toml`)
- Upload HTML coverage report as artifact

### bandit
- Checkout code
- Setup Python environment with dev dependencies (via custom action)
- Run security scanning with bandit on `example/` directory
- Generate JSON report for artifacts
- Fail if security vulnerabilities are found

### pip-audit
- Checkout code
- Setup Python environment with dev dependencies (via custom action)
- Audit dependencies for known CVEs using `pip-audit --desc`

### version-check
- Checkout code
- Setup Python environment with dev dependencies (via custom action)
- Check version consistency across `pyproject.toml` and `uv.lock`

## Docker Workflow

Expand Down
25 changes: 20 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ name = "python-template-server"
version = "0.1.0"
description = "A template FastAPI server with authentication, rate limiting and Prometheus metrics."
readme = "README.md"
requires-python = ">=3.12"
requires-python = ">=3.13"
license = { file = "LICENSE" }
authors = [
{ name = "Javid Ahmed", email = "[email protected]" }
]
classifiers = [
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"License :: OSI Approved :: MIT License",
]
dependencies = [
Expand All @@ -31,11 +31,14 @@ dependencies = [

[project.optional-dependencies]
dev = [
"ruff",
"bandit>=1.9.2",
"mypy",
"pip-audit>=2.10.0",
"pre-commit>=4.5.0",
"pytest",
"pytest-asyncio",
"pytest-cov",
"ruff",
"validate-pyproject",
]

Expand All @@ -52,8 +55,8 @@ allow-direct-references = true

[tool.hatch.build]
include = [
"configuration/**",
"python_template_server/**",
"configuration/**",
".here",
"LICENSE",
"README.md",
Expand All @@ -67,8 +70,17 @@ addopts = [
"term-missing",
]

[tool.coverage.run]
branch = true
source = ["python_template_server"]

[tool.coverage.report]
fail_under = 80
skip_covered = false
show_missing = true

[tool.ruff]
target-version = "py312"
target-version = "py313"
line-length = 120
indent-width = 4

Expand Down Expand Up @@ -154,6 +166,9 @@ indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "auto"

[tool.ruff.lint.pydocstyle]
convention = "google"

[tool.mypy]
warn_unused_configs = true
disallow_incomplete_defs = true
Expand Down
2 changes: 1 addition & 1 deletion python_template_server/template_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def __init__(

@staticmethod
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
"""Handle application lifespan events."""
yield

Expand Down
18 changes: 9 additions & 9 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,65 +18,65 @@

# General fixtures
@pytest.fixture(autouse=True)
def mock_here(tmp_path: str) -> Generator[MagicMock, None, None]:
def mock_here(tmp_path: str) -> Generator[MagicMock]:
"""Mock the here() function to return a temporary directory."""
with patch("pyhere.here") as mock_here:
mock_here.return_value = tmp_path
yield mock_here


@pytest.fixture
def mock_exists() -> Generator[MagicMock, None, None]:
def mock_exists() -> Generator[MagicMock]:
"""Mock the Path.exists() method."""
with patch("pathlib.Path.exists") as mock_exists:
yield mock_exists


@pytest.fixture
def mock_mkdir() -> Generator[MagicMock, None, None]:
def mock_mkdir() -> Generator[MagicMock]:
"""Mock Path.mkdir method."""
with patch("pathlib.Path.mkdir") as mock_mkdir:
yield mock_mkdir


@pytest.fixture
def mock_open_file() -> Generator[MagicMock, None, None]:
def mock_open_file() -> Generator[MagicMock]:
"""Mock the Path.open() method."""
with patch("pathlib.Path.open", mock_open()) as mock_file:
yield mock_file


@pytest.fixture
def mock_touch() -> Generator[MagicMock, None, None]:
def mock_touch() -> Generator[MagicMock]:
"""Mock the Path.touch() method."""
with patch("pathlib.Path.touch") as mock_touch:
yield mock_touch


@pytest.fixture
def mock_sys_exit() -> Generator[MagicMock, None, None]:
def mock_sys_exit() -> Generator[MagicMock]:
"""Mock sys.exit to raise SystemExit."""
with patch("sys.exit") as mock_exit:
mock_exit.side_effect = SystemExit
yield mock_exit


@pytest.fixture
def mock_set_key() -> Generator[MagicMock, None, None]:
def mock_set_key() -> Generator[MagicMock]:
"""Mock the set_key function."""
with patch("dotenv.set_key") as mock_set_key:
yield mock_set_key


@pytest.fixture
def mock_os_getenv() -> Generator[MagicMock, None, None]:
def mock_os_getenv() -> Generator[MagicMock]:
"""Mock the os.getenv function."""
with patch("os.getenv") as mock_getenv:
yield mock_getenv


@pytest.fixture(autouse=True)
def clear_prometheus_registry() -> Generator[None, None, None]:
def clear_prometheus_registry() -> Generator[None]:
"""Clear Prometheus registry before each test to avoid duplicate metric errors."""
# Clear all collectors from the registry
collectors = list(REGISTRY._collector_to_names.keys())
Expand Down
8 changes: 4 additions & 4 deletions tests/test_authentication_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,21 @@


@pytest.fixture
def mock_generate_token() -> Generator[MagicMock, None, None]:
def mock_generate_token() -> Generator[MagicMock]:
"""Mock the generate_token function."""
with patch("python_template_server.authentication_handler.generate_token") as mock_generate:
yield mock_generate


@pytest.fixture
def mock_hash_token() -> Generator[MagicMock, None, None]:
def mock_hash_token() -> Generator[MagicMock]:
"""Mock the hash_token function."""
with patch("python_template_server.authentication_handler.hash_token") as mock_hash:
yield mock_hash


@pytest.fixture
def mock_saved_hashed_token() -> Generator[MagicMock, None, None]:
def mock_saved_hashed_token() -> Generator[MagicMock]:
"""Mock the save_hashed_token function."""
with patch("python_template_server.authentication_handler.save_hashed_token") as mock_save:
yield mock_save
Expand Down Expand Up @@ -99,7 +99,7 @@ def test_verify_token(

def test_verify_token_no_stored_hash(self) -> None:
"""Test the verify_token function when no stored hash is provided."""
with pytest.raises(ValueError, match="No stored token hash found for verification."):
with pytest.raises(ValueError, match=r"No stored token hash found for verification."):
verify_token("sometoken", "")

def test_generate_new_token(
Expand Down
Loading