Skip to content
Draft
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
3 changes: 2 additions & 1 deletion .markdownlint.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"MD013": false,
"MD033": false
"MD033": false,
"MD024": false
}
4 changes: 2 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"python-envs.pythonProjects": [
{
"path": ".",
"packageManager": "ms-python.python:pip",
"envManager": "ms-python.python:venv"
"packageManager": "ms-python.python:poetry",
"envManager": "ms-python.python:poetry"
}
],
"files.exclude": {
Expand Down
48 changes: 48 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,54 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Install Dependencies",
"type": "shell",
"command": "poetry install",
"options": {
"cwd": "${workspaceFolder}"
},
"group": {
"kind": "build",
"isDefault": false
},
"presentation": {
"reveal": "always",
"panel": "shared"
}
},
{
"label": "Lint",
"type": "shell",
"command": "poe lint",
"options": {
"cwd": "${workspaceFolder}"
},
"group": {
"kind": "test",
"isDefault": false
},
"presentation": {
"reveal": "always",
"panel": "shared"
}
},
{
"label": "Format",
"type": "shell",
"command": "poe format",
"options": {
"cwd": "${workspaceFolder}"
},
"group": {
"kind": "test",
"isDefault": false
},
"presentation": {
"reveal": "always",
"panel": "shared"
}
},
{
"label": "Build",
"type": "shell",
Expand Down
165 changes: 138 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Common Python tasks
# Common Python Tasks

This package is a collection of (very) opinionated [Poe the Poet Python tasks](https://poethepoet.natn.io/guides/packaged_tasks.html) for common Python development workflows.
This package is a collection of (very) opinionated [Poe the Poet](https://poethepoet.natn.io/guides/packaged_tasks.html) Python tasks for common Python development workflows.

## Quick start

Expand All @@ -27,9 +27,9 @@ This will complete the following steps.
```toml
[project]
name = "my-awesome-project"
version = "0.0.1"
version = "0.0.2"
dependencies = [
"common-python-tasks==0.0.1", # Always pin to a specific version
"common-python-tasks==0.0.2", # Always pin to a specific version
]

[tool.poe]
Expand All @@ -54,19 +54,92 @@ This will complete the following steps.

Internal tasks are used by other tasks and are not meant to be run directly.

<!-- tasks-table -->
| Task | Description | Tags |
| - | - | - |
| `build` | Build the project; also builds container images when the `containers` tag is included | packaging, containers |
| `build-image` | Build a container image using the bundled Containerfile template | containers, build |
| `build` | Build the project and its containers (when `containers` tag is included) | packaging, containers |
| `build-image` | Build the container image for this project using the Containerfile template | containers, build |
| `build-package` | Build the package (wheel and sdist) | packaging, build |
| `bump-version` | Bump project version and create a git tag | packaging |
| `clean` | Remove build, cache, and coverage artifacts | clean |
| `bump-version` | Bump the project version | packaging |
| `clean` | Clean up temporary files and directories | clean |
| `container-shell` | Run the debug image with an interactive shell | containers, debug |
| `db-shell` | Open a psql shell to the database container | web, containers, database |
| `format` | Format code with autoflake, black, and isort | format |
| `lint` | Run autoflake, black, isort checks, and flake8 linting | lint |
| `publish-package` | Publish the package to PyPI via Poetry | packaging |
| `push-image` | Push container images to the configured registry | containers, packaging, release |
| `run-container` | Run the built container image with the selected tag | containers |
| `test` | Run tests with pytest and generate coverage reports | test |
| `lint` | Lint Python code with autoflake, black, isort, and flake8 | lint |
| `publish-package` | Publish the package to the PyPI server | packaging |
| `push-image` | Push the Docker image to the container registry | containers, packaging, release |
| `reset-db` | Reset the database by deleting the database volume | web, containers, database |
| `run-container` | Run the Docker image as a container | containers |
| `run-db-migrations` | Run database migrations | web, containers, database |
| `stack-down` | Bring down the development stack for the application | web, containers |
| `stack-up` | Bring up the development stack for the application | web, containers |
| `test` | Run the test suite with coverage | test |
<!-- end-tasks-table -->

## Docker Compose Development Stacks

Some tasks of certain tags provide Docker Compose-based development stacks for running your application with supporting services (databases, caches, etc.). Currently supports FastAPI applications with PostgreSQL.

### Configuration

#### `COMPOSE_TYPE`

Specifies the type of application stack. Currently supported:

- `fastapi` - FastAPI application with optional database, Alembic migrations

Set via environment variable:

```toml
[tool.poe.env]
COMPOSE_TYPE = "fastapi"
```

#### `COMPOSE_ADDONS`

Colon-separated list of additional services to include. Available addons:

- `db` - PostgreSQL database with Alembic migration support and Adminer web UI

Example:

```toml
[tool.poe.env]
COMPOSE_ADDONS = "db"
```

For multiple addons (future): `COMPOSE_ADDONS = "db:redis:cache"`

### Compose File Customization

The compose setup follows this precedence.

1. **Environment override** - `COMPOSE_FILE` environment variable with colon-separated paths
2. **Auto-loaded files** - Based on `COMPOSE_TYPE` and `COMPOSE_ADDONS`:
- `compose-base.yml` - Core application service
- `compose-{addon}.yml` - For each addon (e.g., `compose-db.yml`)
- `compose-debug.yml` - When `--debug` flag is used
- `compose-{addon}-debug.yml` - Debug overlays for addons
3. **Additional overlays** - `COMPOSE_OVERLAY_FILES` with colon-separated paths

You can provide local compose files or let the tasks use bundled templates.

### `fastapi`

The `fastapi` stack includes a service for your FastAPI application. It uses the standard Containerfile included with this package.

#### Environment variables

- `API_PORT` - Port for the API server (default: `8080`)
- `SECRET_KEY` - Application secret key (auto-generated and stored in `.env` if not set)
- `ENVIRONMENT` - Environment name like `development` or `production` (default: `production`)
- `DEBUG_PORT` - Port for the Python debugger when using `--debug` (default: `5678`)
- `DB_PORT` - PostgreSQL port (default: `5432`)
- `DB_USER` - Database user (default: package name)
- `DB_BASE` - Database name (default: package name)
- `DB_PASS` - Database password (auto-generated and stored in `.env` if not set)
- `POSTGRES_VERSION` - PostgreSQL Docker image version (default: `17`)
- `ADMINER_PORT` - Adminer web UI port (default: `8081`)

## How it works

Expand All @@ -76,7 +149,7 @@ Your project must meet the following requirements.

- Use Poetry for dependency management
- Have a `pyproject.toml` file at the root
- Have a package name (automatically inferred from `project.name` in `pyproject.toml`, or set via `PACKAGE_NAME` environment variable)
- Have a package name (automatically inferred from `project.name` in `pyproject.toml` or set via `PACKAGE_NAME` environment variable)

### Configuration precedence

Expand Down Expand Up @@ -110,6 +183,25 @@ The following environment variables configure package and container behavior.
- `CONTAINER_REGISTRY_URL` specifies the registry URL (default is `docker.io/{username}`)
- `CUSTOM_IMAGE_ENTRYPOINT` specifies a custom entrypoint script name for containers

#### Docker Compose settings

The following environment variables configure Docker Compose stacks (when using the `web` tag).

- `COMPOSE_TYPE` specifies the type of application stack (e.g., `fastapi`)
- `COMPOSE_ADDONS` colon-separated list of services to include (e.g., `db` for database)
- `COMPOSE_FILE` overrides all compose files with colon-separated paths
- `COMPOSE_OVERLAY_FILES` additional compose files to merge (colon-separated paths)
- `API_PORT` port for the API server (default: `8080`)
- `SECRET_KEY` application secret key (auto-generated if not set)
- `ENVIRONMENT` environment name (default: `production`)
- `DEBUG_PORT` port for Python debugger in debug mode (default: `5678`)
- `DB_PORT` PostgreSQL port (default: `5432`)
- `DB_USER` database user (default: package name)
- `DB_BASE` database name (default: package name)
- `DB_PASS` database password (auto-generated if not set)
- `POSTGRES_VERSION` PostgreSQL version (default: `17`)
- `ADMINER_PORT` Adminer web UI port (default: `8081`)

#### Debugging

The following environment variable enables debugging output.
Expand Down Expand Up @@ -198,13 +290,6 @@ git push --tags

## Troubleshooting

### "No tests were collected"

The `test` task exits with code 5 if no tests are found. You can address this in one of the following ways.

- Add tests to your `tests/` directory
- Exclude the `test` tag and simply do not run `poe test` with this configuration `include_script = "common_python_tasks:tasks(exclude_tags=['test', 'internal'])"`

### Tasks not showing up with `poe --help`

Check your `[tool.poe]` configuration in `pyproject.toml`. Make sure you're using `include_script`, not `includes`.
Expand All @@ -224,7 +309,7 @@ includes = "common_python_tasks:tasks"
This is expected behavior. The `bump-version` task requires commits between the last tag and HEAD. You can resolve this in one of the following ways.

- Make changes and commit them first
- If you need to re-tag the same commit, delete the old tag (for example, `git tag -d v0.0.1`). This is not recommended. Versions should be immutable, and if you need to fix something, you should create a new patch version instead
- Delete the old tag (for example, `git tag -d v0.0.1`). This is not recommended. Versions should be immutable, and if you need to fix something, you should create a new patch version instead. Rarely do you want to pass off new code as an old version

### Config files not being used

Expand All @@ -239,24 +324,50 @@ COMMON_PYTHON_TASKS_LOG_LEVEL=DEBUG poe test
Make sure your `pyproject.toml` contains the following.

- A correct package name in `[project]`
- A package location defined with this configuration `[tool.poetry] packages = [{ include = "your_package", from = "src" }]`
- A package location defined with this configuration: `[tool.poetry] packages = [{ include = "your_package", from = "src" }]`

### Stack fails to start or services won't connect

If `stack-up` builds successfully but services can't connect:

- Check that required environment variables are set (`COMPOSE_TYPE` at minimum)
- Verify ports aren't already in use (defaults: 8080 for API, 5432 for database, 8081 for Adminer)
- Check Docker daemon is running: `docker info`
- View service logs: `docker-compose logs` in your project directory

### Database migrations fail

If `run-db-migrations` fails:

- Ensure the `db` addon is included: `COMPOSE_ADDONS=db`
- Check that your project has Alembic configured with migrations in the expected location
- Verify database credentials in `.env` match your Alembic configuration
- Manually inspect the database: `poe db-shell`

### Secrets not being generated

If `SECRET_KEY` or `DB_PASS` aren't auto-generated:

- Ensure `.env` file is writable in your project root
- Check file permissions: `ls -la .env`
- Generate manually: `python -c "import secrets; print(secrets.token_hex(32))"`

## Design choices

### Containerfile (see [src/common_python_tasks/data/Containerfile](src/common_python_tasks/data/Containerfile))

The standard Python Containerfile incorporates several intentional design choices.

- Multi-stage build: the build stage installs Poetry and builds a wheel while the runtime stage installs only the wheel to keep the final image slim and reproducible
- Cache-aware installs mean pip and Poetry cache mounts speed up iterative builds without bloating the final image
- Multi-stage build: The build stage installs Poetry and builds a wheel while the runtime stage installs only the wheel to keep the final image slim and reproducible
- Pip and Poetry cache mounts speed up iterative builds without bloating the final image
- Explicit inputs through build args (`PYTHON_VERSION`, `POETRY_VERSION`, `PACKAGE_NAME`, `AUTHORS`, `GIT_COMMIT`, `CUSTOM_ENTRYPOINT`) make image metadata and behavior predictable and auditable
- Optional debug stage exports and installs the `debug` dependency group only when present without failing otherwise and is not part of the default final image
- Stable package path creates symlinks to the installed package so entrypoints and consumers have a consistent `/pkg` and `/_$PACKAGE_NAME` path regardless of wheel layout, which ensures that the package can be reliably imported and executed from a known location, and allows for the less common use case of reading files directly from the package path
- Safe entrypoint selection means the default entrypoint resolves the console script matching the package name while `CUSTOM_ENTRYPOINT` allows overriding at build time while keeping runtime behavior predictable
- Minimal final image uses the slim Python base, cleans wheel artifacts and caches, and sets `runtime` as the explicit final target so the debug stage is opt-in
- Minimal final image uses the slim Python base by default, cleans wheel artifacts and caches, and sets `runtime` as the explicit final target so the debug stage is opt-in

## Notes

- This project dogfoods itself - it uses `common-python-tasks` for its own development
- Contributions welcome! Open an issue/discussion to discuss changes before submitting a PR. I do not claim to have all the answers, and you can help determine the future of low-code solutions for Python. I am very interested in your feedback as I don't want to work in a vacuum
- Alpha status: expect breaking changes between minor versions until 1.0.0
- Alpha status: Expect breaking changes between minor versions until 1.0.0
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ dependencies = [
"pytest-cov (>=7.0.0,<8.0.0)",
"pytest (>=9.0.1,<10.0.0)",
"tomlkit (>=0.13.3,<0.14.0)",
"jinja2 (>=3.1.6,<4.0.0)",
]
dynamic = ["version"]

Expand All @@ -36,7 +37,7 @@ Source = "http://github.com/ci-sourcerer/common-python-tasks.git"
Issues = "http://github.com/ci-sourcerer/common-python-tasks/issues"

[tool.poe]
include_script = "common_python_tasks:tasks(exclude_tags=['containers'])"
include_script = "common_python_tasks:tasks(exclude_tags=['fastapi'])"

[tool.poetry.requires-plugins]
poetry-dynamic-versioning = { version = ">=1.0.0,<2.0.0", extras = ["plugin"] }
Expand Down
2 changes: 1 addition & 1 deletion src/common_python_tasks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

def tasks(
include_tags: "Sequence[str]" = tuple(), exclude_tags: "Sequence[str]" = tuple()
):
) -> dict:
from .tasks import tasks

return tasks(include_tags=include_tags, exclude_tags=exclude_tags)
8 changes: 8 additions & 0 deletions src/common_python_tasks/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import sys

if __name__ == "__main__":
print(
"common_python_tasks is not intended to be run as a standalone script. Invoke a task via poethepoet.",
file=sys.stderr,
)
sys.exit(1)
Empty file.
52 changes: 52 additions & 0 deletions src/common_python_tasks/data/fastapi/alembic.ini.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
[alembic]
script_location = {{ package_name }}:db/migrations
file_template = %%(year)d-%%(month).2d-%%(day).2d-%%(hour).2d-%%(minute).2d_%%(rev)s
prepend_sys_path = .
output_encoding = utf-8

[post_write_hooks]
hooks = black,isort

black.type = console_scripts
black.entrypoint = black

autoflake.type = console_scripts
autoflake.entrypoint = autoflake

isort.type = console_scripts
isort.entrypoint = isort

# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARN
handlers = console
qualname =

[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine

[logger_alembic]
level = INFO
handlers =
qualname = alembic

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
Loading