Skip to content
Open
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
2 changes: 1 addition & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@

# ... except for dependencies and source
!pyproject.toml
!poetry.lock
!uv.lock
!src/
32 changes: 12 additions & 20 deletions .github/actions/project-environment-setup/action.yml
Original file line number Diff line number Diff line change
@@ -1,26 +1,18 @@
name: Project Environment Setup
description: Setup project environment
description: Setup project environment with uv
runs:
using: "composite"
steps:
- name: Install Poetry
shell: bash
run: pipx install poetry==2.1.4
# The actions/cache step below uses this id to get the exact python version
- id: setup-python
uses: actions/setup-python@v5
with:
python-version: "3.13.7"
architecture: x64
# https://github.com/actions/setup-python/blob/main/docs/advanced-usage.md#caching-packages
cache: "poetry"
# https://github.com/actions/cache/blob/main/examples.md#python---pipenv
- id: cache-pipenv
uses: actions/cache@v3
# https://docs.astral.sh/uv/guides/integration/github/
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
path: ~/.local/share/virtualenvs
key: ${{ runner.os }}-python-${{ steps.setup-python.outputs.python-version }}-poetry-${{ hashFiles('poetry.lock') }}
- name: Install Dependencies with Poetry
if: steps.cache-pipenv.outputs.cache-hit != 'true'
# Install a specific version of uv.
version: "0.9.13"
enable-cache: true
- name: Set up Python
shell: bash
run: uv python install
- name: Install Dependencies with uv
shell: bash
run: poetry install --with test
run: uv sync --all-extras
4 changes: 2 additions & 2 deletions .github/workflows/publish-docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ jobs:
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4

- name: Publish to GitHub Packages
uses: whoan/docker-build-with-cache-action@v2
uses: whoan/docker-build-with-cache-action@v6
with:
registry: docker.pkg.github.com
username: ${{ github.actor }}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Python CI with Poetry
name: Python CI with uv

on:
pull_request:
Expand Down Expand Up @@ -33,32 +33,31 @@ jobs:
static-code-analysis:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: ./.github/actions/project-environment-setup
- run: poetry run ruff check .
- run: uv run ruff check .
- name: Install mypy types
run: |
yes | poetry run mypy --install-types \
yes | uv run mypy --install-types \
|| test $? -eq 2 && echo "Installed mypy types successfully" \
|| echo "Failed to install mypy types"
- run: poetry run mypy
- run: uv run mypy
- name: Python Tests with pytest
run: env LOG_LEVEL=INFO poetry run pytest --quiet --cov --cov-report term -n auto --benchmark-disable
run: env LOG_LEVEL=INFO uv run pytest --quiet --cov --cov-report term -n auto --benchmark-disable
- name: Python Benchmark with pytest
run: env LOG_LEVEL=ERROR poetry run pytest --capture=no --log-cli-level=ERROR -n 0 --benchmark-only
run: env LOG_LEVEL=ERROR uv run pytest --capture=no --log-cli-level=ERROR -n 0 --benchmark-only
- name: Run Python Application
run: |
poetry env activate
PYTHONPATH=`pwd` poetry run python3 src/python_boilerplate/__main__.py
PYTHONPATH=`pwd` uv run python3 src/python_boilerplate/__main__.py
pyinstaller-smoke-test:
needs: static-code-analysis
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: ./.github/actions/project-environment-setup
- name: Build Application Executable Package with PyInstaller
run: |
poetry run pyinstaller --windowed --noconsole \
uv run pyinstaller --windowed --noconsole \
--add-data "pyproject.toml:." \
--add-data "src/python_boilerplate/resources/*:python_boilerplate/resources" \
--name multithread_and_thread_pool_usage \
Expand All @@ -72,7 +71,7 @@ jobs:
needs: static-code-analysis
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Build Docker Image
run: |
docker build . -t python_boilerplate:smoke-test-tag
Expand All @@ -85,11 +84,11 @@ jobs:
check-versions:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: ./.github/actions/project-environment-setup
- name: Check Versions of Python Packages
run: |
output=$(poetry run pip list --outdated)
output=$(uv pip list --outdated)
if [ -z "$output" ]
then
echo "🎉 Congrats! Everything is up-to-date"
Expand Down
36 changes: 22 additions & 14 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# See https://pre-commit.com/ for usage and config
fail_fast: true
exclude: |
(?x)^(
.venv/|
.sqlite3
)$^
Comment on lines +4 to +6
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The top-level exclude regex ends with $^, which makes it impossible to match anything (end-of-string immediately followed by start-of-string). This likely means .venv/ and sqlite files will not be excluded from hooks. Fix the regex terminator (e.g., )$) and ensure the sqlite pattern matches the intended filenames (e.g., .*\.sqlite3$).

Suggested change
.venv/|
.sqlite3
)$^
\.venv/|
.*\.sqlite3
)$

Copilot uses AI. Check for mistakes.
repos:
# https://github.com/pre-commit/pre-commit-hooks/releases/tag/v6.0.0
- repo: https://github.com/pre-commit/pre-commit-hooks
Expand All @@ -19,27 +23,31 @@ repos:
- id: check-case-conflict
- repo: local
hooks:
- id: poetry-check
name: Poetry Check
- id: ruff-fix-imports
name: Ruff Fix Imports
stages: [ pre-commit ]
language: python
entry: poetry check
pass_filenames: false
files: ^(.*/)?(poetry\.lock|pyproject\.toml)$
- id: ruff-format
language: system
entry: uv run ruff check --select I --fix
types: [ python ]
- id: ruff-check-fix
name: Ruff Check and Fix
stages: [ pre-commit ]
language: system
# Ruff checks and fixes Python code for both /src and /tests directories
entry: poetry run ruff check .
entry: uv run ruff check --fix
types: [ python ]
- id: ruff-format
name: Ruff Format
stages: [ pre-commit ]
language: system
# Ruff checks and fixes Python code for both /src and /tests directories
entry: uv run ruff format
types: [ python ]
args:
- "--fix"
- id: mypy
name: mypy
stages: [ pre-commit ]
language: system
entry: poetry run mypy
entry: uv run mypy
types: [ python ]
require_serial: true
- id: pytest
Expand All @@ -48,7 +56,7 @@ repos:
language: system
# https://github.com/pytest-dev/pytest/issues/5502#issuecomment-1020761655
# Prevent Pytest logging error like: ValueError: I/O operation on closed file.
entry: env LOG_LEVEL=DEBUG poetry run pytest --cov --cov-report html --html=./build/.pytest_report/report.html --self-contained-html --log-cli-level=DEBUG -n auto --benchmark-disable
entry: env LOG_LEVEL=DEBUG uv run pytest --cov --cov-report html --html=./build/.pytest_report/report.html --self-contained-html --log-cli-level=DEBUG -n auto --benchmark-disable
types: [ python ]
pass_filenames: false
- id: pytest-cov
Expand All @@ -57,6 +65,6 @@ repos:
language: system
# https://github.com/pytest-dev/pytest/issues/5502#issuecomment-1020761655
# Prevent Pytest logging error like: ValueError: I/O operation on closed file.
entry: env LOG_LEVEL=ERROR poetry run pytest --cov --cov-fail-under=90 --capture=no --log-cli-level=ERROR -n auto
entry: env LOG_LEVEL=ERROR uv run pytest --cov --cov-fail-under=90 --capture=no --log-cli-level=ERROR -n auto
types: [ python ]
pass_filenames: false
191 changes: 191 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
# AGENTS.md

Guidelines for AI agents working in this repository.

## Build & Run Commands

| Task | Command |
|------|---------|
| Install deps | `uv sync --extra test --extra dev --extra linter` |
| Run application | `uv run python -m python_boilerplate` |
| Lint (ruff) | `uv run ruff check .` |
| Lint auto-fix | `uv run ruff check --fix .` |
| Format | `uv run ruff format .` |
| Type check | `uv run mypy` |
| Install mypy stubs | `uv run mypy --install-types` |
| All tests | `uv run pytest --cov -n auto --benchmark-disable` |
| Single test file | `uv run pytest tests/test_python_boilerplate/common/test_profiling.py` |
| Single test func | `uv run pytest tests/test_python_boilerplate/common/test_profiling.py -k test_elapsed_time` |
| Tests with logging | `uv run pytest --log-cli-level=DEBUG --capture=no <path>` |
| Benchmarks only | `uv run pytest --capture=no --log-cli-level=ERROR -n 0 --benchmark-only` |
| Pre-commit (all) | `uv run pre-commit run --all-files` |

## Project Structure

```
src/
python_boilerplate/
__init__.py # freeze_support() call for PyInstaller
__main__.py # Application entry point
common/ # Shared utilities (decorators, helpers)
configuration/ # App config, thread pool, logging, scheduler, DB
data_migration/ # Database migration scripts
demo/ # Example/demo scripts
message/ # Messaging (email)
repository/ # Data access layer (peewee ORM models + repos)
resources/ # Static resources (HOCON config files)
template/ # Jinja2 templates
tests/
conftest.py # Global fixtures (auto_profile with pyinstrument)
test_python_boilerplate/ # Mirrors src/ structure exactly
common/
configuration/
demo/
message/
repository/
template/
```

Source lives in `src/` (setuptools `package-dir = {"" = "src"}`). Tests mirror the source tree under `tests/test_python_boilerplate/`.

## Tool Configuration

- **Python**: >=3.13.1, <3.14 (`target-version = "py313"`)
- **Package manager**: uv (with `uv.lock`)
- **Linter/Formatter**: Ruff 0.15.x (`lint.select = ["ALL"]` with specific ignores)
- **Type checker**: mypy (strict mode, pydantic plugin)
- **Test runner**: pytest with pytest-xdist (`-n auto`), pytest-cov, pytest-mock, pytest-asyncio, pytest-benchmark
- **Pre-commit hooks**: ruff import sort, ruff check --fix, ruff format, mypy, pytest (pre-commit stage), pytest-cov (pre-push stage)
- **Line length**: 120 characters
- **Indent**: 4 spaces, UTF-8, LF line endings

## Code Style

### Imports

Imports are organized in three groups separated by blank lines, sorted by ruff (`I001`):

```python
# 1. Standard library
import functools
import time
from collections.abc import Callable
from typing import Any

# 2. Third-party
from loguru import logger

# 3. Local (absolute imports only, no relative imports)
from python_boilerplate.common.common_function import json_serial
```

- Use `from collections.abc import Callable, Coroutine` (NOT `from typing import Callable`)
- Relative imports are banned (`ban-relative-imports = "all"` in ruff config)
- `__init__.py` files are mostly empty (no re-exports)

### Type Annotations

- All functions MUST have return type annotations (mypy `disallow_untyped_defs = true`)
- Use PEP 695 type parameter syntax for generic functions: `def foo[T](x: T) -> T:` (NOT `TypeVar`)
- Use `typing.Any` when dynamic types are unavoidable
- Use `typing.Final` for module-level constants
- Use `TYPE_CHECKING` guard for import-only types: `if TYPE_CHECKING: from _pytest.nodes import Node`
- Use `X | Y` union syntax (NOT `Union[X, Y]` or `Optional[X]`)

### Naming Conventions

- Files: `snake_case.py`
- Functions/variables: `snake_case`
- Classes: `PascalCase`
- Constants: `UPPER_SNAKE_CASE` with `Final` annotation
- Test files: `test_<module_name>.py` mirroring source structure
- Test functions: `test_<what_is_tested>` or `test_<what_is_tested>_<scenario>`

### Docstrings

Sphinx/reST style with `:param:` and `:return:` directives:

```python
def get_login_user() -> str:
"""
Get current login user who is using the OS.

:param username: the username to look up
:return: the username
"""
```

- Opening `"""` on same line as summary for single-line docstrings
- Opening `"""` on first line with summary for multi-line docstrings
- One blank line between summary and `:param` block (when description exists)

### Error Handling

- Catch specific exceptions, log with `logger.exception()` or `logger.error()`
- Re-raise after logging: `raise e` (do not swallow)
- Use `pytest.raises` for testing expected exceptions
- Pattern: try/execute/except-log-raise/finally-cleanup

```python
try:
return func(*arg, **kwarg)
except Exception as e:
logger.warning(f"Captured exception. {e}")
raise e
finally:
cleanup()
```

### Logging

- Use `loguru.logger` exclusively (NOT stdlib `logging`)
- f-string formatting in log messages: `logger.info(f"Result: {value}")`
- Available levels: TRACE, DEBUG, INFO, WARNING, ERROR

### Decorators

This codebase makes heavy use of function decorators in `src/python_boilerplate/common/`:

- `@async_function` - Run sync function in thread pool, returns `Future`
- `@async_function_wrapper` - Add done_callback to async function
- `@elapsed_time(level="INFO")` - Profile function timing
- `@async_elapsed_time(level="INFO")` - Profile async function timing
- `@mem_profile()` / `@cpu_profile()` - Resource profiling
- `@peewee_table` - Register ORM table class
- `@trace` / `@async_trace` - Function call tracing to DB
- `@debounce(wait)` / `@throttle(limit)` - Rate limiting

### Testing

- Test files mirror source: `src/.../foo.py` -> `tests/test_python_boilerplate/.../test_foo.py`
- Use `pytest.fixture` for setup; `conftest.py` has `auto_profile` fixture (runs pyinstrument on every test)
- Use `pytest-mock` (`MockerFixture`) for mocking/spying
- Async tests use `@pytest.mark.asyncio`
- Name pattern: `--pytest-test-first` enforced (functions must start with `test_`)
- Coverage target: 90% (enforced in pre-push hook)

### Ruff Ignored Rules

Key intentionally-ignored lint rules (do NOT "fix" these patterns):

- `ANN401` - `typing.Any` is allowed in `*args`/`**kwargs`
- `D100-D104,D106` - Missing docstrings in public modules/classes/functions are allowed
- `EM101,EM102` - String literals in exceptions are OK
- `PLR2004` - Magic values in comparisons are OK
- `COM812` - Trailing commas are not enforced
- `S311` - Pseudo-random generators are OK
- `TRY003,TRY201` - Exception message style is relaxed

Test files (`tests/*`) additionally ignore: `ANN` (annotations), `ARG` (unused args), `INP001`, `S101` (assert).

## CI Pipeline

GitHub Actions (`python-ci-with-uv.yml`) runs on push to main/develop/feature/release/hotfix branches:

1. `uv run ruff check .` (no `--fix` in CI - must pass clean)
2. `uv run mypy` (after installing types)
3. `uv run pytest --quiet --cov --cov-report term -n auto --benchmark-disable`
4. `uv run pytest --benchmark-only` (benchmarks separate)
5. Application smoke test

Note: CI runs `ruff check` WITHOUT `--fix`. Code must pass lint cleanly before push.
Loading