diff --git a/AGENTS.md b/AGENTS.md index 2d25e03..a579515 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,7 +22,7 @@ cd my-app && uv sync && uv run pytest # Add a local extension CI=true uv run create-awesome-python-app my-app \ --template "file://$PWD?subdir=templates/fastapi-starter" \ - --addons "file://$PWD?subdir=extensions/github-setup" \ + --addons "file://$PWD?subdir=extensions/all-github-setup" \ --no-interactive ``` diff --git a/README.md b/README.md index 92ad1c7..fc14644 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ uvx create-awesome-python-app # With template and extensions uvx create-awesome-python-app my-api \ --template fastapi-starter \ - --addons github-setup python-docker + --addons github-setup fastapi-docker ``` For local development against this checkout: diff --git a/ci/profiles/celery-default.json b/ci/profiles/celery-default.json index cf9063f..4439fa1 100644 --- a/ci/profiles/celery-default.json +++ b/ci/profiles/celery-default.json @@ -2,5 +2,8 @@ "id": "celery-default", "description": "Celery worker with GitHub CI + Docker", "templateDir": "celery-worker", - "addons": ["github-setup", "python-docker"] + "addons": [ + "github-setup", + "celery-docker" + ] } diff --git a/ci/profiles/cli-default.json b/ci/profiles/cli-default.json index 3619ab4..5b6789d 100644 --- a/ci/profiles/cli-default.json +++ b/ci/profiles/cli-default.json @@ -2,5 +2,8 @@ "id": "cli-default", "description": "CLI tool with GitHub CI + Dev Container", "templateDir": "cli-starter", - "addons": ["github-setup", "python-devcontainer"] + "addons": [ + "github-setup", + "development-container" + ] } diff --git a/ci/profiles/django-default.json b/ci/profiles/django-default.json index c8d9729..d7ae5c1 100644 --- a/ci/profiles/django-default.json +++ b/ci/profiles/django-default.json @@ -1,6 +1,10 @@ { "id": "django-default", - "description": "Django API with GitHub CI + Dev Container", + "description": "Django API with GitHub CI + Docker + Dev Container", "templateDir": "django-api", - "addons": ["github-setup", "python-devcontainer"] + "addons": [ + "github-setup", + "development-container", + "django-docker" + ] } diff --git a/ci/profiles/fastapi-default.json b/ci/profiles/fastapi-default.json index 1bb87fe..43bb2d6 100644 --- a/ci/profiles/fastapi-default.json +++ b/ci/profiles/fastapi-default.json @@ -2,5 +2,9 @@ "id": "fastapi-default", "description": "Typical FastAPI API: GitHub CI + Docker + Postgres", "templateDir": "fastapi-starter", - "addons": ["github-setup", "python-docker", "python-postgres"] + "addons": [ + "github-setup", + "fastapi-docker", + "postgres" + ] } diff --git a/ci/profiles/fastapi-devcontainer.json b/ci/profiles/fastapi-devcontainer.json index f509eaf..036d4c3 100644 --- a/ci/profiles/fastapi-devcontainer.json +++ b/ci/profiles/fastapi-devcontainer.json @@ -2,5 +2,8 @@ "id": "fastapi-devcontainer", "description": "FastAPI with GitHub CI and VS Code Dev Container", "templateDir": "fastapi-starter", - "addons": ["github-setup", "python-devcontainer"] + "addons": [ + "github-setup", + "development-container" + ] } diff --git a/ci/profiles/uv-workspace-default.json b/ci/profiles/uv-workspace-default.json index d804b56..de7bd27 100644 --- a/ci/profiles/uv-workspace-default.json +++ b/ci/profiles/uv-workspace-default.json @@ -2,5 +2,7 @@ "id": "uv-workspace-default", "description": "uv workspace monorepo with GitHub CI", "templateDir": "uv-workspace-starter", - "addons": ["github-setup"] + "addons": [ + "github-setup" + ] } diff --git a/ci/profiles/uv-workspace-devcontainer.json b/ci/profiles/uv-workspace-devcontainer.json index 436f9e5..45e1e7a 100644 --- a/ci/profiles/uv-workspace-devcontainer.json +++ b/ci/profiles/uv-workspace-devcontainer.json @@ -2,5 +2,8 @@ "id": "uv-workspace-devcontainer", "description": "uv workspace monorepo with GitHub CI and VS Code Dev Container", "templateDir": "uv-workspace-starter", - "addons": ["github-setup", "python-devcontainer"] + "addons": [ + "github-setup", + "development-container" + ] } diff --git a/docs/AUTHORING.md b/docs/AUTHORING.md index f4e6719..fadcbda 100644 --- a/docs/AUTHORING.md +++ b/docs/AUTHORING.md @@ -132,6 +132,21 @@ Examples: **Never** use a generic `python-*` folder/slug for overlays that write FastAPI `app/` paths or a FastAPI-only `CMD`. +CI enforces this in `scripts/ci/validate-registry.py`: + +- Extension folders must be `all-*`, or `{stack}-*` matching a single `type` + (see `STACK_PREFIX_BY_TYPE`); `python-*` is rejected. +- Every catalog template must ship the quality-bar docs/files listed below + (enforced on the maturity tip / after the template uplift PRs land). + +### `incompatibleWith` (path collisions) + +Use symmetric `incompatibleWith` when two extensions would overwrite the same +generated paths (for example two Docker overlays that both ship `Dockerfile` / +`compose.yml` for the **same** template `type`). Today stack Docker extensions +are isolated by `type`; when a type gains a second packaging strategy, declare +mutual incompatibility like cna-templates does for Redux saga/thunk. + ### Template quality bar (every catalog template) Every template registered in `templates.json` must ship at least: diff --git a/docs/FUTURE_TEMPLATES.md b/docs/FUTURE_TEMPLATES.md index 96a7905..d3d019f 100644 --- a/docs/FUTURE_TEMPLATES.md +++ b/docs/FUTURE_TEMPLATES.md @@ -6,7 +6,7 @@ Planned templates and extensions not yet in `templates.json`. These are phased a | Slug | Type | Description | |------|------|-------------| -| `uv-workspace-starter` | `uv-workspace` | Python monorepo using uv workspaces: shared `packages/` libraries and `apps/` deployables with one lockfile, shared Ruff/Pyright/pytest, and a Typer CLI that consumes a local library. Compatible extensions: `github-setup`, `python-devcontainer`. | +| `uv-workspace-starter` | `uv-workspace` | Python monorepo using uv workspaces: shared `packages/` libraries and `apps/` deployables with one lockfile, shared Ruff/Pyright/pytest, and a Typer CLI that consumes a local library. Compatible extensions: `github-setup`, `development-container`. | ## Planned templates @@ -19,13 +19,13 @@ Planned templates and extensions not yet in `templates.json`. These are phased a - **When:** Command-line tools, internal utilities, or libraries shipped as console scripts. - **Stack:** `pyproject.toml` with `[project.scripts]`, Typer (or Click), pytest for CLI invocation tests. -- **Extensions:** `github-setup`, `python-devcontainer`; Docker extension optional for containerized CLI distribution. +- **Extensions:** `github-setup`, `development-container`; Docker extension optional for containerized CLI distribution. ### `celery-worker` - **When:** Background jobs, scheduled tasks, or async processing separate from an HTTP API. - **Stack:** Celery app module, worker/beat commands, health probe, example task. -- **Extensions:** `python-docker`, `python-postgres` (for result backend or ORM), message-broker compose under `docker/redis/` or similar. +- **Extensions:** `fastapi-docker`, `postgres` (for result backend or ORM), message-broker compose under `docker/redis/` or similar. ## Contributing diff --git a/docs/MAINTENANCE_CI.md b/docs/MAINTENANCE_CI.md index 303ff67..f26e424 100644 --- a/docs/MAINTENANCE_CI.md +++ b/docs/MAINTENANCE_CI.md @@ -104,7 +104,7 @@ Curated JSON under `ci/profiles/`. Validator enforces **one extension per catego REPO="$PWD" python scripts/ci/run-scaffold-check.py \ --template-url "file://$REPO?subdir=templates/fastapi-starter" \ - --addon-url "file://$REPO?subdir=extensions/python-docker" \ + --addon-url "file://$REPO?subdir=extensions/fastapi-docker" \ --workdir /tmp/cpa-check ``` diff --git a/docs/MAINTENANCE_TEMPLATES.md b/docs/MAINTENANCE_TEMPLATES.md index b4f0e5d..cd43b20 100644 --- a/docs/MAINTENANCE_TEMPLATES.md +++ b/docs/MAINTENANCE_TEMPLATES.md @@ -92,11 +92,11 @@ Key questions: ls extensions/ # Read the registry entry -grep -A 15 '"slug": "python-docker"' templates.json +grep -A 15 '"slug": "fastapi-docker"' templates.json # Read partial manifest and files -cat extensions/python-docker/pyproject.toml -ls -la extensions/python-docker +cat extensions/fastapi-docker/pyproject.toml +ls -la extensions/fastapi-docker ``` Key questions: diff --git a/docs/TESTING.md b/docs/TESTING.md index c75b06f..1fe8728 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -7,7 +7,7 @@ After entries are registered in `templates.json` and the CLI catalog points to t ```sh CI=true uvx create-awesome-python-app my-app \ --template fastapi-starter \ - --addons github-setup python-docker \ + --addons github-setup fastapi-docker \ --no-interactive cd my-app && uv sync && uv run ruff check . && uv run pytest ``` @@ -29,8 +29,8 @@ CI=true uvx create-awesome-python-app my-app \ CI=true uvx create-awesome-python-app my-app \ --template "file://$REPO?subdir=templates/fastapi-starter" \ --addons \ - "file://$REPO?subdir=extensions/github-setup" \ - "file://$REPO?subdir=extensions/python-docker" \ + "file://$REPO?subdir=extensions/all-github-setup" \ + "file://$REPO?subdir=extensions/fastapi-docker" \ --no-interactive cd my-app && uv sync && uv run pytest ``` diff --git a/extensions/python-devcontainer/README.md b/extensions/all-devcontainer/README.md similarity index 87% rename from extensions/python-devcontainer/README.md rename to extensions/all-devcontainer/README.md index 1cec78c..7acf2c3 100644 --- a/extensions/python-devcontainer/README.md +++ b/extensions/all-devcontainer/README.md @@ -1,6 +1,6 @@ # Dev Container (extension bank) -Maintainer-facing notes for the **python-devcontainer** extension in `cpa-templates`. +Maintainer-facing notes for the **development-container** extension in `cpa-templates`. Copied into generated projects (via `template/`): @@ -18,7 +18,7 @@ The bank `README.md` (this file) stays **outside** `template/` so it does not ov ```sh uvx create-awesome-python-app my-api \ --template fastapi-starter \ - --addons python-devcontainer \ + --addons development-container \ --yes ``` diff --git a/extensions/python-devcontainer/template/.devcontainer/Dockerfile b/extensions/all-devcontainer/template/.devcontainer/Dockerfile similarity index 100% rename from extensions/python-devcontainer/template/.devcontainer/Dockerfile rename to extensions/all-devcontainer/template/.devcontainer/Dockerfile diff --git a/extensions/python-devcontainer/template/.devcontainer/devcontainer.json b/extensions/all-devcontainer/template/.devcontainer/devcontainer.json similarity index 100% rename from extensions/python-devcontainer/template/.devcontainer/devcontainer.json rename to extensions/all-devcontainer/template/.devcontainer/devcontainer.json diff --git a/extensions/python-devcontainer/template/docs/DEVCONTAINER_GUIDE.md b/extensions/all-devcontainer/template/docs/DEVCONTAINER_GUIDE.md similarity index 88% rename from extensions/python-devcontainer/template/docs/DEVCONTAINER_GUIDE.md rename to extensions/all-devcontainer/template/docs/DEVCONTAINER_GUIDE.md index d67d444..9054dde 100644 --- a/extensions/python-devcontainer/template/docs/DEVCONTAINER_GUIDE.md +++ b/extensions/all-devcontainer/template/docs/DEVCONTAINER_GUIDE.md @@ -2,7 +2,7 @@ ## Overview -The **python-devcontainer** extension opens the project in a [Dev Container](https://code.visualstudio.com/docs/devcontainers/containers) with Python 3.12 and [uv](https://docs.astral.sh/uv/). It is editor-focused and does not replace **python-docker** for deployment or CI image builds. You can use both: Dev Container for daily coding, Docker Compose for integration testing. +The **development-container** extension opens the project in a [Dev Container](https://code.visualstudio.com/docs/devcontainers/containers) with Python 3.12 and [uv](https://docs.astral.sh/uv/). It is editor-focused and does not replace **fastapi-docker** for deployment or CI image builds. You can use both: Dev Container for daily coding, Docker Compose for integration testing. ## What it adds @@ -37,7 +37,7 @@ uv run uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 Port `8000` is forwarded — open http://localhost:8000/docs. -### With python-postgres +### With postgres The Dev Container does not start Postgres automatically. Options: diff --git a/extensions/python-devcontainer/template/docs/README.md.append b/extensions/all-devcontainer/template/docs/README.md.append similarity index 100% rename from extensions/python-devcontainer/template/docs/README.md.append rename to extensions/all-devcontainer/template/docs/README.md.append diff --git a/extensions/github-setup/README.md b/extensions/all-github-setup/README.md similarity index 100% rename from extensions/github-setup/README.md rename to extensions/all-github-setup/README.md diff --git a/extensions/github-setup/template/.github/CODE_OF_CONDUCT.md b/extensions/all-github-setup/template/.github/CODE_OF_CONDUCT.md similarity index 100% rename from extensions/github-setup/template/.github/CODE_OF_CONDUCT.md rename to extensions/all-github-setup/template/.github/CODE_OF_CONDUCT.md diff --git a/extensions/github-setup/template/.github/ISSUE_TEMPLATE/bug-report.yml b/extensions/all-github-setup/template/.github/ISSUE_TEMPLATE/bug-report.yml similarity index 100% rename from extensions/github-setup/template/.github/ISSUE_TEMPLATE/bug-report.yml rename to extensions/all-github-setup/template/.github/ISSUE_TEMPLATE/bug-report.yml diff --git a/extensions/github-setup/template/.github/ISSUE_TEMPLATE/config.yml b/extensions/all-github-setup/template/.github/ISSUE_TEMPLATE/config.yml similarity index 100% rename from extensions/github-setup/template/.github/ISSUE_TEMPLATE/config.yml rename to extensions/all-github-setup/template/.github/ISSUE_TEMPLATE/config.yml diff --git a/extensions/github-setup/template/.github/ISSUE_TEMPLATE/documentation.yml b/extensions/all-github-setup/template/.github/ISSUE_TEMPLATE/documentation.yml similarity index 100% rename from extensions/github-setup/template/.github/ISSUE_TEMPLATE/documentation.yml rename to extensions/all-github-setup/template/.github/ISSUE_TEMPLATE/documentation.yml diff --git a/extensions/github-setup/template/.github/ISSUE_TEMPLATE/feature-request.yml b/extensions/all-github-setup/template/.github/ISSUE_TEMPLATE/feature-request.yml similarity index 100% rename from extensions/github-setup/template/.github/ISSUE_TEMPLATE/feature-request.yml rename to extensions/all-github-setup/template/.github/ISSUE_TEMPLATE/feature-request.yml diff --git a/extensions/github-setup/template/.github/PULL_REQUEST_TEMPLATE.md b/extensions/all-github-setup/template/.github/PULL_REQUEST_TEMPLATE.md similarity index 100% rename from extensions/github-setup/template/.github/PULL_REQUEST_TEMPLATE.md rename to extensions/all-github-setup/template/.github/PULL_REQUEST_TEMPLATE.md diff --git a/extensions/github-setup/template/.github/dependabot.yml b/extensions/all-github-setup/template/.github/dependabot.yml similarity index 100% rename from extensions/github-setup/template/.github/dependabot.yml rename to extensions/all-github-setup/template/.github/dependabot.yml diff --git a/extensions/github-setup/template/.github/workflows/ci.yml b/extensions/all-github-setup/template/.github/workflows/ci.yml similarity index 100% rename from extensions/github-setup/template/.github/workflows/ci.yml rename to extensions/all-github-setup/template/.github/workflows/ci.yml diff --git a/extensions/github-setup/template/.github/workflows/mega-linter.yml b/extensions/all-github-setup/template/.github/workflows/mega-linter.yml similarity index 100% rename from extensions/github-setup/template/.github/workflows/mega-linter.yml rename to extensions/all-github-setup/template/.github/workflows/mega-linter.yml diff --git a/extensions/github-setup/template/.github/workflows/pr-review.yml b/extensions/all-github-setup/template/.github/workflows/pr-review.yml similarity index 100% rename from extensions/github-setup/template/.github/workflows/pr-review.yml rename to extensions/all-github-setup/template/.github/workflows/pr-review.yml diff --git a/extensions/github-setup/template/.github/workflows/todo.yml b/extensions/all-github-setup/template/.github/workflows/todo.yml similarity index 100% rename from extensions/github-setup/template/.github/workflows/todo.yml rename to extensions/all-github-setup/template/.github/workflows/todo.yml diff --git a/extensions/github-setup/template/.mega-linter.yml b/extensions/all-github-setup/template/.mega-linter.yml similarity index 100% rename from extensions/github-setup/template/.mega-linter.yml rename to extensions/all-github-setup/template/.mega-linter.yml diff --git a/extensions/github-setup/template/docs/GITHUB_SETUP_GUIDE.md b/extensions/all-github-setup/template/docs/GITHUB_SETUP_GUIDE.md similarity index 100% rename from extensions/github-setup/template/docs/GITHUB_SETUP_GUIDE.md rename to extensions/all-github-setup/template/docs/GITHUB_SETUP_GUIDE.md diff --git a/extensions/github-setup/template/docs/README.md.append b/extensions/all-github-setup/template/docs/README.md.append similarity index 100% rename from extensions/github-setup/template/docs/README.md.append rename to extensions/all-github-setup/template/docs/README.md.append diff --git a/extensions/github-setup/template/tools/danger/.gitignore b/extensions/all-github-setup/template/tools/danger/.gitignore similarity index 100% rename from extensions/github-setup/template/tools/danger/.gitignore rename to extensions/all-github-setup/template/tools/danger/.gitignore diff --git a/extensions/github-setup/template/tools/danger/dangerfile.ts b/extensions/all-github-setup/template/tools/danger/dangerfile.ts similarity index 100% rename from extensions/github-setup/template/tools/danger/dangerfile.ts rename to extensions/all-github-setup/template/tools/danger/dangerfile.ts diff --git a/extensions/github-setup/template/tools/danger/package.json b/extensions/all-github-setup/template/tools/danger/package.json similarity index 100% rename from extensions/github-setup/template/tools/danger/package.json rename to extensions/all-github-setup/template/tools/danger/package.json diff --git a/extensions/github-setup/template/tools/danger/tsconfig.json b/extensions/all-github-setup/template/tools/danger/tsconfig.json similarity index 100% rename from extensions/github-setup/template/tools/danger/tsconfig.json rename to extensions/all-github-setup/template/tools/danger/tsconfig.json diff --git a/extensions/python-postgres/README.md b/extensions/all-postgres/README.md similarity index 85% rename from extensions/python-postgres/README.md rename to extensions/all-postgres/README.md index 0b3232a..e607af9 100644 --- a/extensions/python-postgres/README.md +++ b/extensions/all-postgres/README.md @@ -1,6 +1,6 @@ # PostgreSQL (extension bank) -Maintainer-facing notes for the **python-postgres** extension in `cpa-templates`. +Maintainer-facing notes for the **postgres** extension in `cpa-templates`. Copied into generated projects (via `template/`): @@ -20,16 +20,16 @@ The bank `README.md` (this file) stays **outside** `template/` so it does not ov ```sh uvx create-awesome-python-app my-api \ --template fastapi-starter \ - --addons python-postgres \ + --addons postgres \ --yes ``` -Often combined with `python-docker`: +Often combined with `fastapi-docker`: ```sh uvx create-awesome-python-app my-api \ --template fastapi-starter \ - --addons python-docker python-postgres \ + --addons fastapi-docker postgres \ --yes ``` diff --git a/extensions/python-postgres/template/.env.example.append b/extensions/all-postgres/template/.env.example.append similarity index 78% rename from extensions/python-postgres/template/.env.example.append rename to extensions/all-postgres/template/.env.example.append index 63ff292..d8c2f90 100644 --- a/extensions/python-postgres/template/.env.example.append +++ b/extensions/all-postgres/template/.env.example.append @@ -1,5 +1,5 @@ -# PostgreSQL (python-postgres extension) +# PostgreSQL (postgres extension) # PostgreSQL (optional overlay) # DATABASE_URL=postgresql+psycopg://app:app@localhost:5432/app POSTGRES_USER=app diff --git a/extensions/python-postgres/template/docker/postgres/.env.example b/extensions/all-postgres/template/docker/postgres/.env.example similarity index 100% rename from extensions/python-postgres/template/docker/postgres/.env.example rename to extensions/all-postgres/template/docker/postgres/.env.example diff --git a/extensions/python-postgres/template/docker/postgres/compose.yml b/extensions/all-postgres/template/docker/postgres/compose.yml similarity index 100% rename from extensions/python-postgres/template/docker/postgres/compose.yml rename to extensions/all-postgres/template/docker/postgres/compose.yml diff --git a/extensions/python-postgres/template/docs/POSTGRES_GUIDE.md b/extensions/all-postgres/template/docs/POSTGRES_GUIDE.md similarity index 90% rename from extensions/python-postgres/template/docs/POSTGRES_GUIDE.md rename to extensions/all-postgres/template/docs/POSTGRES_GUIDE.md index ba5f1db..fe3f4af 100644 --- a/extensions/python-postgres/template/docs/POSTGRES_GUIDE.md +++ b/extensions/all-postgres/template/docs/POSTGRES_GUIDE.md @@ -2,7 +2,7 @@ ## Overview -The **python-postgres** extension adds a local Postgres 16 Compose service under `docker/postgres/`, merges the `psycopg[binary]` driver into `pyproject.toml`, and appends Postgres-related keys to `.env.example`. +The **postgres** extension adds a local Postgres 16 Compose service under `docker/postgres/`, merges the `psycopg[binary]` driver into `pyproject.toml`, and appends Postgres-related keys to `.env.example`. It ships the **database service and client library** — wire your ORM and migrations in application code after scaffold. @@ -29,7 +29,7 @@ Connect from the host: psql "postgresql://app:app@localhost:5432/app" ``` -### With python-docker +### With fastapi-docker ```sh docker compose -f compose.yml -f docker/postgres/compose.yml up --build @@ -59,7 +59,7 @@ Root `.env.example` gains these keys from `.env.example.append`: | `POSTGRES_DB` | `app` | Database name | | `DATABASE_URL` | (commented example) | e.g. `postgresql+psycopg://app:app@localhost:5432/app` | -When combined with **python-docker**, the postgres compose overlay sets: +When combined with **fastapi-docker**, the postgres compose overlay sets: ```env DATABASE_URL=postgresql+psycopg://app:app@db:5432/app diff --git a/extensions/python-postgres/template/docs/README.md.append b/extensions/all-postgres/template/docs/README.md.append similarity index 100% rename from extensions/python-postgres/template/docs/README.md.append rename to extensions/all-postgres/template/docs/README.md.append diff --git a/extensions/python-postgres/template/pyproject.toml b/extensions/all-postgres/template/pyproject.toml similarity index 100% rename from extensions/python-postgres/template/pyproject.toml rename to extensions/all-postgres/template/pyproject.toml diff --git a/extensions/celery-docker/README.md b/extensions/celery-docker/README.md new file mode 100644 index 0000000..8925d42 --- /dev/null +++ b/extensions/celery-docker/README.md @@ -0,0 +1,33 @@ +# Docker for Celery (extension bank) + +Maintainer-facing notes for the **celery-docker** extension. + +Copied into generated projects (via `template/`): + +| Path | Purpose | +|------|---------| +| `Dockerfile` | uv-based image; Celery worker CMD | +| `.dockerignore` | Excludes `.venv`, caches, git metadata | +| `compose.yml` | Dev worker compose (bind mount) | +| `compose.prod.yml` | Prod worker overlay | +| `docs/DOCKER_GUIDE.md` | Long-form guide | +| `docs/README.md.append` | Index bullet | + +Compose includes a Redis broker. Env vars are `BROKER_URL` / `RESULT_BACKEND` +(matching `worker/config.py`). Pair with `postgres` only when the worker also +needs a database. + +## Apply + +```sh +uvx create-awesome-python-app my-worker \ + --template celery-worker \ + --addons celery-docker \ + --yes +``` + +## Verify + +```sh +docker compose up --build +``` diff --git a/extensions/python-docker/template/.dockerignore b/extensions/celery-docker/template/.dockerignore similarity index 100% rename from extensions/python-docker/template/.dockerignore rename to extensions/celery-docker/template/.dockerignore diff --git a/extensions/celery-docker/template/Dockerfile b/extensions/celery-docker/template/Dockerfile new file mode 100644 index 0000000..197a70c --- /dev/null +++ b/extensions/celery-docker/template/Dockerfile @@ -0,0 +1,15 @@ +FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim + +WORKDIR /app + +ENV UV_COMPILE_BYTECODE=1 +ENV UV_LINK_MODE=copy +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +COPY pyproject.toml README.md ./ +COPY worker ./worker + +RUN uv sync --no-dev + +CMD ["uv", "run", "celery", "-A", "worker.celery_app", "worker", "--loglevel=INFO"] diff --git a/extensions/celery-docker/template/compose.prod.yml b/extensions/celery-docker/template/compose.prod.yml new file mode 100644 index 0000000..6f93b03 --- /dev/null +++ b/extensions/celery-docker/template/compose.prod.yml @@ -0,0 +1,15 @@ +services: + redis: + image: redis:7-alpine + restart: always + worker: + build: . + env_file: + - .env + environment: + BROKER_URL: redis://redis:6379/0 + RESULT_BACKEND: redis://redis:6379/1 + restart: always + depends_on: + - redis + command: uv run celery -A worker.celery_app worker --loglevel=INFO --concurrency=2 diff --git a/extensions/celery-docker/template/compose.yml b/extensions/celery-docker/template/compose.yml new file mode 100644 index 0000000..bcbeadd --- /dev/null +++ b/extensions/celery-docker/template/compose.yml @@ -0,0 +1,17 @@ +services: + redis: + image: redis:7-alpine + ports: + - "6379:6379" + worker: + build: . + env_file: + - .env + environment: + BROKER_URL: redis://redis:6379/0 + RESULT_BACKEND: redis://redis:6379/1 + volumes: + - .:/app + depends_on: + - redis + command: uv run celery -A worker.celery_app worker --loglevel=INFO diff --git a/extensions/celery-docker/template/docs/DOCKER_GUIDE.md b/extensions/celery-docker/template/docs/DOCKER_GUIDE.md new file mode 100644 index 0000000..6b00b9d --- /dev/null +++ b/extensions/celery-docker/template/docs/DOCKER_GUIDE.md @@ -0,0 +1,46 @@ +# Docker guide (Celery) + +## Overview + +The **celery-docker** extension packages the Celery worker for local and +production-style containers, including a Redis broker service. + +## What it adds + +- `Dockerfile` — Python 3.12 + uv; copies `worker/`; runs Celery worker +- `compose.yml` / `compose.prod.yml` — `redis` + `worker` services +- Env overrides in Compose: `BROKER_URL` / `RESULT_BACKEND` point at the + `redis` service (not `localhost`) + +These names match `worker/config.py` (pydantic-settings fields `broker_url` / +`result_backend`). Do **not** use `CELERY_BROKER_URL` unless you also rename the +settings fields. + +## Usage + +```sh +docker compose up --build +``` + +## Verification + +1. `docker compose up --build` +2. Confirm Redis is healthy and the worker log shows it is ready +3. Enqueue a task (another shell / one-off container): + +```sh +docker compose exec worker uv run python -c \ + "from worker.tasks import ping; print(ping.delay().get(timeout=10))" +``` + +## Troubleshooting + +| Symptom | Fix | +|---------|-----| +| Cannot connect to Redis | Use `redis://redis:6379/0` inside Compose (service name), not `localhost` | +| Import errors for `worker` | Confirm `COPY worker` matches the template layout | +| Wrong env var name | Template reads `BROKER_URL` / `RESULT_BACKEND` | + +## Resources + +- [Celery first steps](https://docs.celeryq.dev/en/stable/getting-started/first-steps-with-celery.html) diff --git a/extensions/celery-docker/template/docs/README.md.append b/extensions/celery-docker/template/docs/README.md.append new file mode 100644 index 0000000..0734164 --- /dev/null +++ b/extensions/celery-docker/template/docs/README.md.append @@ -0,0 +1 @@ +- [Docker](./DOCKER_GUIDE.md) — container workflow for this worker diff --git a/extensions/django-docker/README.md b/extensions/django-docker/README.md new file mode 100644 index 0000000..0c4a61e --- /dev/null +++ b/extensions/django-docker/README.md @@ -0,0 +1,30 @@ +# Docker for Django (extension bank) + +Maintainer-facing notes for the **django-docker** extension. + +Copied into generated projects (via `template/`): + +| Path | Purpose | +|------|---------| +| `Dockerfile` | uv-based image; `gunicorn config.wsgi` | +| `.dockerignore` | Excludes `.venv`, caches, git metadata | +| `compose.yml` | Dev compose (`runserver` + bind mount) | +| `compose.prod.yml` | Prod overlay (`gunicorn`, restart always) | +| `docs/DOCKER_GUIDE.md` | Long-form guide | +| `docs/README.md.append` | Index bullet | + +## Apply + +```sh +uvx create-awesome-python-app my-api \ + --template django-api \ + --addons django-docker \ + --yes +``` + +## Verify + +```sh +docker compose up --build +curl -s http://localhost:8000/api/healthz/ +``` diff --git a/extensions/django-docker/template/.dockerignore b/extensions/django-docker/template/.dockerignore new file mode 100644 index 0000000..ac81cf1 --- /dev/null +++ b/extensions/django-docker/template/.dockerignore @@ -0,0 +1,11 @@ +.venv +__pycache__ +*.py[cod] +.pytest_cache +.ruff_cache +.git +.env +data +*.egg-info +dist +.mypy_cache diff --git a/extensions/django-docker/template/Dockerfile b/extensions/django-docker/template/Dockerfile new file mode 100644 index 0000000..cd76793 --- /dev/null +++ b/extensions/django-docker/template/Dockerfile @@ -0,0 +1,19 @@ +FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim + +WORKDIR /app + +ENV UV_COMPILE_BYTECODE=1 +ENV UV_LINK_MODE=copy +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +COPY pyproject.toml README.md ./ +COPY manage.py ./ +COPY config ./config +COPY apps ./apps + +RUN uv sync --no-dev + +EXPOSE 8000 + +CMD ["uv", "run", "gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000"] diff --git a/extensions/django-docker/template/compose.prod.yml b/extensions/django-docker/template/compose.prod.yml new file mode 100644 index 0000000..03d33d9 --- /dev/null +++ b/extensions/django-docker/template/compose.prod.yml @@ -0,0 +1,9 @@ +services: + api: + build: . + ports: + - "8000:8000" + env_file: + - .env + restart: always + command: uv run gunicorn config.wsgi:application --bind 0.0.0.0:8000 diff --git a/extensions/django-docker/template/compose.yml b/extensions/django-docker/template/compose.yml new file mode 100644 index 0000000..1e4e31a --- /dev/null +++ b/extensions/django-docker/template/compose.yml @@ -0,0 +1,10 @@ +services: + api: + build: . + ports: + - "8000:8000" + env_file: + - .env + volumes: + - .:/app + command: uv run python manage.py runserver 0.0.0.0:8000 diff --git a/extensions/django-docker/template/docs/DOCKER_GUIDE.md b/extensions/django-docker/template/docs/DOCKER_GUIDE.md new file mode 100644 index 0000000..e0f698c --- /dev/null +++ b/extensions/django-docker/template/docs/DOCKER_GUIDE.md @@ -0,0 +1,49 @@ +# Docker guide (Django) + +## Overview + +The **django-docker** extension adds a reproducible container workflow for Django + DRF APIs. Compose files follow Create-Node-App naming: **`compose.yml`** / **`compose.prod.yml`**. + +## What it adds + +- `Dockerfile` — Python 3.12 + uv; copies `manage.py`, `config/`, `apps/`; runs `gunicorn config.wsgi` +- `compose.yml` — local `runserver` with bind mount +- `compose.prod.yml` — `gunicorn` without reload + +## Usage + +```sh +docker compose up --build +curl -s http://localhost:8000/api/healthz/ +``` + +Production-style: + +```sh +docker compose -f compose.prod.yml up --build -d +``` + +## Configuration + +| Variable | Purpose | +|----------|---------| +| `.env` | Loaded by Compose (`env_file`) | +| `DATABASE_URL` / Django `DATABASES` | Wire via settings; use hostname `db` when stacked with `postgres` | + +## Verification + +1. `docker compose up --build` +2. Hit `/api/healthz/` +3. `docker compose down` + +## Troubleshooting + +| Symptom | Fix | +|---------|-----| +| Module not found `apps` | Ensure Dockerfile `COPY apps` matches project layout | +| DB connection refused | Use Compose service hostname, not `localhost`, inside containers | + +## Resources + +- [Docker Compose](https://docs.docker.com/compose/) +- [Gunicorn](https://docs.gunicorn.org/) diff --git a/extensions/django-docker/template/docs/README.md.append b/extensions/django-docker/template/docs/README.md.append new file mode 100644 index 0000000..0daf9bd --- /dev/null +++ b/extensions/django-docker/template/docs/README.md.append @@ -0,0 +1 @@ +- [Docker](./DOCKER_GUIDE.md) — container workflow for this project diff --git a/extensions/python-auth-jwt/README.md b/extensions/fastapi-auth-jwt/README.md similarity index 89% rename from extensions/python-auth-jwt/README.md rename to extensions/fastapi-auth-jwt/README.md index 25c7065..4c989bc 100644 --- a/extensions/python-auth-jwt/README.md +++ b/extensions/fastapi-auth-jwt/README.md @@ -1,6 +1,6 @@ # Auth JWT (extension bank) -Maintainer-facing notes for the **python-auth-jwt** extension in `cpa-templates`. +Maintainer-facing notes for the **fastapi-auth-jwt** extension in `cpa-templates`. Copied into generated projects (via `template/`): @@ -19,7 +19,7 @@ The bank `README.md` (this file) stays **outside** `template/` so it does not ov ```sh uvx create-awesome-python-app my-api \ --template fastapi-starter \ - --addons python-auth-jwt \ + --addons fastapi-auth-jwt \ --yes ``` diff --git a/extensions/python-auth-jwt/template/.env.example.append b/extensions/fastapi-auth-jwt/template/.env.example.append similarity index 61% rename from extensions/python-auth-jwt/template/.env.example.append rename to extensions/fastapi-auth-jwt/template/.env.example.append index cb09f00..a9ca938 100644 --- a/extensions/python-auth-jwt/template/.env.example.append +++ b/extensions/fastapi-auth-jwt/template/.env.example.append @@ -1,5 +1,5 @@ -# Auth JWT (python-auth-jwt extension) +# Auth JWT (fastapi-auth-jwt extension) JWT_SECRET=change-me JWT_ALGORITHM=HS256 JWT_EXPIRE_MINUTES=60 diff --git a/extensions/fastapi-auth-jwt/template/app/features/auth/__init__.py b/extensions/fastapi-auth-jwt/template/app/features/auth/__init__.py new file mode 100644 index 0000000..d3c26b9 --- /dev/null +++ b/extensions/fastapi-auth-jwt/template/app/features/auth/__init__.py @@ -0,0 +1 @@ +"""JWT auth feature skeleton (fastapi-auth-jwt extension).""" diff --git a/extensions/python-auth-jwt/template/app/features/auth/router.py b/extensions/fastapi-auth-jwt/template/app/features/auth/router.py similarity index 100% rename from extensions/python-auth-jwt/template/app/features/auth/router.py rename to extensions/fastapi-auth-jwt/template/app/features/auth/router.py diff --git a/extensions/python-auth-jwt/template/app/features/auth/schemas.py b/extensions/fastapi-auth-jwt/template/app/features/auth/schemas.py similarity index 100% rename from extensions/python-auth-jwt/template/app/features/auth/schemas.py rename to extensions/fastapi-auth-jwt/template/app/features/auth/schemas.py diff --git a/extensions/python-auth-jwt/template/app/features/auth/service.py b/extensions/fastapi-auth-jwt/template/app/features/auth/service.py similarity index 100% rename from extensions/python-auth-jwt/template/app/features/auth/service.py rename to extensions/fastapi-auth-jwt/template/app/features/auth/service.py diff --git a/extensions/python-auth-jwt/template/docs/AUTH_JWT_GUIDE.md b/extensions/fastapi-auth-jwt/template/docs/AUTH_JWT_GUIDE.md similarity index 92% rename from extensions/python-auth-jwt/template/docs/AUTH_JWT_GUIDE.md rename to extensions/fastapi-auth-jwt/template/docs/AUTH_JWT_GUIDE.md index 9e04dd0..935f095 100644 --- a/extensions/python-auth-jwt/template/docs/AUTH_JWT_GUIDE.md +++ b/extensions/fastapi-auth-jwt/template/docs/AUTH_JWT_GUIDE.md @@ -2,7 +2,7 @@ ## Overview -The **python-auth-jwt** extension adds a JWT + password-hashing skeleton for FastAPI starters: Pydantic schemas, `pwdlib` (Argon2) helpers, PyJWT encode/decode, and a demo `/auth` router with an in-memory user. +The **fastapi-auth-jwt** extension adds a JWT + password-hashing skeleton for FastAPI starters: Pydantic schemas, `pwdlib` (Argon2) helpers, PyJWT encode/decode, and a demo `/auth` router with an in-memory user. Replace the demo user and secret before any production use. diff --git a/extensions/python-auth-jwt/template/docs/README.md.append b/extensions/fastapi-auth-jwt/template/docs/README.md.append similarity index 100% rename from extensions/python-auth-jwt/template/docs/README.md.append rename to extensions/fastapi-auth-jwt/template/docs/README.md.append diff --git a/extensions/python-auth-jwt/template/pyproject.toml b/extensions/fastapi-auth-jwt/template/pyproject.toml similarity index 100% rename from extensions/python-auth-jwt/template/pyproject.toml rename to extensions/fastapi-auth-jwt/template/pyproject.toml diff --git a/extensions/python-docker/README.md b/extensions/fastapi-docker/README.md similarity index 89% rename from extensions/python-docker/README.md rename to extensions/fastapi-docker/README.md index 1aa8163..a62a471 100644 --- a/extensions/python-docker/README.md +++ b/extensions/fastapi-docker/README.md @@ -1,6 +1,6 @@ # Docker (extension bank) -Maintainer-facing notes for the **python-docker** extension in `cpa-templates`. +Maintainer-facing notes for the **fastapi-docker** extension in `cpa-templates`. Copied into generated projects (via `template/`): @@ -20,7 +20,7 @@ The bank `README.md` (this file) stays **outside** `template/` so it does not ov ```sh uvx create-awesome-python-app my-api \ --template fastapi-starter \ - --addons python-docker \ + --addons fastapi-docker \ --yes ``` diff --git a/extensions/fastapi-docker/template/.dockerignore b/extensions/fastapi-docker/template/.dockerignore new file mode 100644 index 0000000..ac81cf1 --- /dev/null +++ b/extensions/fastapi-docker/template/.dockerignore @@ -0,0 +1,11 @@ +.venv +__pycache__ +*.py[cod] +.pytest_cache +.ruff_cache +.git +.env +data +*.egg-info +dist +.mypy_cache diff --git a/extensions/python-docker/template/Dockerfile b/extensions/fastapi-docker/template/Dockerfile similarity index 100% rename from extensions/python-docker/template/Dockerfile rename to extensions/fastapi-docker/template/Dockerfile diff --git a/extensions/python-docker/template/compose.prod.yml b/extensions/fastapi-docker/template/compose.prod.yml similarity index 100% rename from extensions/python-docker/template/compose.prod.yml rename to extensions/fastapi-docker/template/compose.prod.yml diff --git a/extensions/python-docker/template/compose.yml b/extensions/fastapi-docker/template/compose.yml similarity index 100% rename from extensions/python-docker/template/compose.yml rename to extensions/fastapi-docker/template/compose.yml diff --git a/extensions/python-docker/template/docs/DOCKER_GUIDE.md b/extensions/fastapi-docker/template/docs/DOCKER_GUIDE.md similarity index 88% rename from extensions/python-docker/template/docs/DOCKER_GUIDE.md rename to extensions/fastapi-docker/template/docs/DOCKER_GUIDE.md index bfea746..61d016e 100644 --- a/extensions/python-docker/template/docs/DOCKER_GUIDE.md +++ b/extensions/fastapi-docker/template/docs/DOCKER_GUIDE.md @@ -2,9 +2,9 @@ ## Overview -The **python-docker** extension adds a reproducible container workflow for FastAPI (or other uv-based Python API) projects. Compose files follow Create-Node-App naming: **`compose.yml`** / **`compose.prod.yml`** (not `docker-compose.yml`). +The **fastapi-docker** extension adds a reproducible container workflow for FastAPI (or other uv-based Python API) projects. Compose files follow Create-Node-App naming: **`compose.yml`** / **`compose.prod.yml`** (not `docker-compose.yml`). -Use it when you want local containers without installing Python on the host, or when you ship the API as an image. Pair with **python-postgres** when you need a database service. +Use it when you want local containers without installing Python on the host, or when you ship the API as an image. Pair with **postgres** when you need a database service. ## What it adds @@ -36,7 +36,7 @@ docker compose -f compose.yml -f compose.prod.yml up --build -d The prod overlay removes the source bind mount and `--reload`, and sets `restart: always`. -### With python-postgres +### With postgres ```sh docker compose -f compose.yml -f docker/postgres/compose.yml up --build @@ -72,7 +72,7 @@ For production-style runs, set `DEBUG=false` in `.env`. Pin base image tags in ` | Module not found inside container | Image built without latest `pyproject.toml` | Rebuild: `docker compose build --no-cache` | | Health 404 | Wrong prefix | Check `API_PREFIX` / scaffold `apiPrefix` (default `/api/v1/healthz`) | | Env vars missing | No `.env` | Copy `.env.example` → `.env` | -| Postgres hostname errors | Using `localhost` inside compose | Use service name `db` when stacked with `python-postgres` | +| Postgres hostname errors | Using `localhost` inside compose | Use service name `db` when stacked with `postgres` | ## Resources diff --git a/extensions/python-docker/template/docs/README.md.append b/extensions/fastapi-docker/template/docs/README.md.append similarity index 100% rename from extensions/python-docker/template/docs/README.md.append rename to extensions/fastapi-docker/template/docs/README.md.append diff --git a/extensions/python-redis/README.md b/extensions/fastapi-redis/README.md similarity index 84% rename from extensions/python-redis/README.md rename to extensions/fastapi-redis/README.md index 75f20a8..57fb9a9 100644 --- a/extensions/python-redis/README.md +++ b/extensions/fastapi-redis/README.md @@ -1,6 +1,6 @@ # Redis (extension bank) -Maintainer-facing notes for the **python-redis** extension in `cpa-templates`. +Maintainer-facing notes for the **fastapi-redis** extension in `cpa-templates`. Copied into generated projects (via `template/`): @@ -20,16 +20,16 @@ The bank `README.md` (this file) stays **outside** `template/` so it does not ov ```sh uvx create-awesome-python-app my-api \ --template fastapi-starter \ - --addons python-redis \ + --addons fastapi-redis \ --yes ``` -Often combined with `python-docker`: +Often combined with `fastapi-docker`: ```sh uvx create-awesome-python-app my-api \ --template fastapi-starter \ - --addons python-docker python-redis \ + --addons fastapi-docker fastapi-redis \ --yes ``` diff --git a/extensions/python-redis/template/.env.example.append b/extensions/fastapi-redis/template/.env.example.append similarity index 51% rename from extensions/python-redis/template/.env.example.append rename to extensions/fastapi-redis/template/.env.example.append index d29aa39..c9aa352 100644 --- a/extensions/python-redis/template/.env.example.append +++ b/extensions/fastapi-redis/template/.env.example.append @@ -1,3 +1,3 @@ -# Redis (python-redis extension) +# Redis (fastapi-redis extension) REDIS_URL=redis://localhost:6379/0 diff --git a/extensions/python-redis/template/app/core/redis_client.py b/extensions/fastapi-redis/template/app/core/redis_client.py similarity index 83% rename from extensions/python-redis/template/app/core/redis_client.py rename to extensions/fastapi-redis/template/app/core/redis_client.py index 02446f2..73dcfe2 100644 --- a/extensions/python-redis/template/app/core/redis_client.py +++ b/extensions/fastapi-redis/template/app/core/redis_client.py @@ -1,4 +1,4 @@ -"""Redis client helper (python-redis extension).""" +"""Redis client helper (fastapi-redis extension).""" from __future__ import annotations diff --git a/extensions/python-redis/template/docker/redis/compose.yml b/extensions/fastapi-redis/template/docker/redis/compose.yml similarity index 100% rename from extensions/python-redis/template/docker/redis/compose.yml rename to extensions/fastapi-redis/template/docker/redis/compose.yml diff --git a/extensions/python-redis/template/docs/README.md.append b/extensions/fastapi-redis/template/docs/README.md.append similarity index 100% rename from extensions/python-redis/template/docs/README.md.append rename to extensions/fastapi-redis/template/docs/README.md.append diff --git a/extensions/python-redis/template/docs/REDIS_GUIDE.md b/extensions/fastapi-redis/template/docs/REDIS_GUIDE.md similarity index 92% rename from extensions/python-redis/template/docs/REDIS_GUIDE.md rename to extensions/fastapi-redis/template/docs/REDIS_GUIDE.md index d8c3328..05602c4 100644 --- a/extensions/python-redis/template/docs/REDIS_GUIDE.md +++ b/extensions/fastapi-redis/template/docs/REDIS_GUIDE.md @@ -2,7 +2,7 @@ ## Overview -The **python-redis** extension adds a Redis client dependency, a small `get_redis()` helper, env docs, and a local Redis 7 Compose service under `docker/redis/`. +The **fastapi-redis** extension adds a Redis client dependency, a small `get_redis()` helper, env docs, and a local Redis 7 Compose service under `docker/redis/`. Use it for cache, sessions, rate limits, or as a broker when pairing with Celery later. @@ -25,7 +25,7 @@ uv sync uv run python -c "from app.core.redis_client import get_redis; print(get_redis().ping())" ``` -### With python-docker +### With fastapi-docker ```sh docker compose -f compose.yml -f docker/redis/compose.yml up --build diff --git a/extensions/python-redis/template/pyproject.toml b/extensions/fastapi-redis/template/pyproject.toml similarity index 100% rename from extensions/python-redis/template/pyproject.toml rename to extensions/fastapi-redis/template/pyproject.toml diff --git a/extensions/python-sentry/README.md b/extensions/fastapi-sentry/README.md similarity index 89% rename from extensions/python-sentry/README.md rename to extensions/fastapi-sentry/README.md index f0c4b85..8aacab6 100644 --- a/extensions/python-sentry/README.md +++ b/extensions/fastapi-sentry/README.md @@ -1,6 +1,6 @@ # Sentry (extension bank) -Maintainer-facing notes for the **python-sentry** extension in `cpa-templates`. +Maintainer-facing notes for the **fastapi-sentry** extension in `cpa-templates`. Copied into generated projects (via `template/`): @@ -19,7 +19,7 @@ The bank `README.md` (this file) stays **outside** `template/` so it does not ov ```sh uvx create-awesome-python-app my-api \ --template fastapi-starter \ - --addons python-sentry \ + --addons fastapi-sentry \ --yes ``` diff --git a/extensions/python-sentry/template/.env.example.append b/extensions/fastapi-sentry/template/.env.example.append similarity index 67% rename from extensions/python-sentry/template/.env.example.append rename to extensions/fastapi-sentry/template/.env.example.append index 40d2c60..6fbc5bc 100644 --- a/extensions/python-sentry/template/.env.example.append +++ b/extensions/fastapi-sentry/template/.env.example.append @@ -1,5 +1,5 @@ -# Sentry (python-sentry extension) +# Sentry (fastapi-sentry extension) SENTRY_DSN= SENTRY_TRACES_SAMPLE_RATE=0.0 SENTRY_ENVIRONMENT=development diff --git a/extensions/python-sentry/template/app/core/sentry.py b/extensions/fastapi-sentry/template/app/core/sentry.py similarity index 91% rename from extensions/python-sentry/template/app/core/sentry.py rename to extensions/fastapi-sentry/template/app/core/sentry.py index 9570700..72718d6 100644 --- a/extensions/python-sentry/template/app/core/sentry.py +++ b/extensions/fastapi-sentry/template/app/core/sentry.py @@ -1,4 +1,4 @@ -"""Sentry initialization helper (python-sentry extension).""" +"""Sentry initialization helper (fastapi-sentry extension).""" from __future__ import annotations diff --git a/extensions/python-sentry/template/docs/README.md.append b/extensions/fastapi-sentry/template/docs/README.md.append similarity index 100% rename from extensions/python-sentry/template/docs/README.md.append rename to extensions/fastapi-sentry/template/docs/README.md.append diff --git a/extensions/python-sentry/template/docs/SENTRY_GUIDE.md b/extensions/fastapi-sentry/template/docs/SENTRY_GUIDE.md similarity index 90% rename from extensions/python-sentry/template/docs/SENTRY_GUIDE.md rename to extensions/fastapi-sentry/template/docs/SENTRY_GUIDE.md index 789fda9..5aa243a 100644 --- a/extensions/python-sentry/template/docs/SENTRY_GUIDE.md +++ b/extensions/fastapi-sentry/template/docs/SENTRY_GUIDE.md @@ -2,7 +2,7 @@ ## Overview -The **python-sentry** extension adds `sentry-sdk[fastapi]` and a small `init_sentry()` helper. Initialization is a **no-op** when `SENTRY_DSN` is empty, so local development stays quiet until you opt in. +The **fastapi-sentry** extension adds `sentry-sdk[fastapi]` and a small `init_sentry()` helper. Initialization is a **no-op** when `SENTRY_DSN` is empty, so local development stays quiet until you opt in. ## What it adds diff --git a/extensions/python-sentry/template/pyproject.toml b/extensions/fastapi-sentry/template/pyproject.toml similarity index 100% rename from extensions/python-sentry/template/pyproject.toml rename to extensions/fastapi-sentry/template/pyproject.toml diff --git a/extensions/python-sqlalchemy/README.md b/extensions/fastapi-sqlalchemy/README.md similarity index 84% rename from extensions/python-sqlalchemy/README.md rename to extensions/fastapi-sqlalchemy/README.md index db31eae..deb6cd2 100644 --- a/extensions/python-sqlalchemy/README.md +++ b/extensions/fastapi-sqlalchemy/README.md @@ -1,6 +1,6 @@ # SQLAlchemy + Alembic (extension bank) -Maintainer-facing notes for the **python-sqlalchemy** extension in `cpa-templates`. +Maintainer-facing notes for the **fastapi-sqlalchemy** extension in `cpa-templates`. Copied into generated projects (via `template/`): @@ -20,16 +20,16 @@ The bank `README.md` (this file) stays **outside** `template/` so it does not ov ```sh uvx create-awesome-python-app my-api \ --template fastapi-starter \ - --addons python-sqlalchemy \ + --addons fastapi-sqlalchemy \ --yes ``` -Often combined with `python-postgres`: +Often combined with `postgres`: ```sh uvx create-awesome-python-app my-api \ --template fastapi-starter \ - --addons python-postgres python-sqlalchemy \ + --addons postgres fastapi-sqlalchemy \ --yes ``` diff --git a/extensions/python-sqlalchemy/template/.env.example.append b/extensions/fastapi-sqlalchemy/template/.env.example.append similarity index 72% rename from extensions/python-sqlalchemy/template/.env.example.append rename to extensions/fastapi-sqlalchemy/template/.env.example.append index 34a6155..b1f59b9 100644 --- a/extensions/python-sqlalchemy/template/.env.example.append +++ b/extensions/fastapi-sqlalchemy/template/.env.example.append @@ -1,5 +1,5 @@ -# SQLAlchemy / Alembic (python-sqlalchemy extension) +# SQLAlchemy / Alembic (fastapi-sqlalchemy extension) # DATABASE_URL=postgresql+psycopg://app:app@localhost:5432/app # For SQLite local smoke without Postgres: # DATABASE_URL=sqlite:///./app.db diff --git a/extensions/python-sqlalchemy/template/alembic.ini b/extensions/fastapi-sqlalchemy/template/alembic.ini similarity index 100% rename from extensions/python-sqlalchemy/template/alembic.ini rename to extensions/fastapi-sqlalchemy/template/alembic.ini diff --git a/extensions/python-sqlalchemy/template/alembic/env.py b/extensions/fastapi-sqlalchemy/template/alembic/env.py similarity index 100% rename from extensions/python-sqlalchemy/template/alembic/env.py rename to extensions/fastapi-sqlalchemy/template/alembic/env.py diff --git a/extensions/python-sqlalchemy/template/alembic/script.py.mako b/extensions/fastapi-sqlalchemy/template/alembic/script.py.mako similarity index 100% rename from extensions/python-sqlalchemy/template/alembic/script.py.mako rename to extensions/fastapi-sqlalchemy/template/alembic/script.py.mako diff --git a/extensions/python-sqlalchemy/template/alembic/versions/.gitkeep b/extensions/fastapi-sqlalchemy/template/alembic/versions/.gitkeep similarity index 100% rename from extensions/python-sqlalchemy/template/alembic/versions/.gitkeep rename to extensions/fastapi-sqlalchemy/template/alembic/versions/.gitkeep diff --git a/extensions/python-sqlalchemy/template/app/db/__init__.py b/extensions/fastapi-sqlalchemy/template/app/db/__init__.py similarity index 59% rename from extensions/python-sqlalchemy/template/app/db/__init__.py rename to extensions/fastapi-sqlalchemy/template/app/db/__init__.py index f4b307a..ff53ea7 100644 --- a/extensions/python-sqlalchemy/template/app/db/__init__.py +++ b/extensions/fastapi-sqlalchemy/template/app/db/__init__.py @@ -1,4 +1,4 @@ -"""Database session helpers (python-sqlalchemy extension).""" +"""Database session helpers (fastapi-sqlalchemy extension).""" from app.db.session import get_db, session_factory diff --git a/extensions/python-sqlalchemy/template/app/db/base.py b/extensions/fastapi-sqlalchemy/template/app/db/base.py similarity index 100% rename from extensions/python-sqlalchemy/template/app/db/base.py rename to extensions/fastapi-sqlalchemy/template/app/db/base.py diff --git a/extensions/python-sqlalchemy/template/app/db/session.py b/extensions/fastapi-sqlalchemy/template/app/db/session.py similarity index 100% rename from extensions/python-sqlalchemy/template/app/db/session.py rename to extensions/fastapi-sqlalchemy/template/app/db/session.py diff --git a/extensions/python-sqlalchemy/template/docs/README.md.append b/extensions/fastapi-sqlalchemy/template/docs/README.md.append similarity index 100% rename from extensions/python-sqlalchemy/template/docs/README.md.append rename to extensions/fastapi-sqlalchemy/template/docs/README.md.append diff --git a/extensions/python-sqlalchemy/template/docs/SQLALCHEMY_GUIDE.md b/extensions/fastapi-sqlalchemy/template/docs/SQLALCHEMY_GUIDE.md similarity index 87% rename from extensions/python-sqlalchemy/template/docs/SQLALCHEMY_GUIDE.md rename to extensions/fastapi-sqlalchemy/template/docs/SQLALCHEMY_GUIDE.md index 983fa9b..d64e930 100644 --- a/extensions/python-sqlalchemy/template/docs/SQLALCHEMY_GUIDE.md +++ b/extensions/fastapi-sqlalchemy/template/docs/SQLALCHEMY_GUIDE.md @@ -2,9 +2,9 @@ ## Overview -The **python-sqlalchemy** extension adds SQLAlchemy 2.x session helpers and an Alembic migration layout for FastAPI starters. It merges `sqlalchemy` and `alembic` into `pyproject.toml` and documents `DATABASE_URL` in `.env.example`. +The **fastapi-sqlalchemy** extension adds SQLAlchemy 2.x session helpers and an Alembic migration layout for FastAPI starters. It merges `sqlalchemy` and `alembic` into `pyproject.toml` and documents `DATABASE_URL` in `.env.example`. -Pair with **python-postgres** when you want a local Postgres Compose service and the `psycopg` driver; the default URL is SQLite for smoke tests without Docker. +Pair with **postgres** when you want a local Postgres Compose service and the `psycopg` driver; the default URL is SQLite for smoke tests without Docker. ## What it adds @@ -42,7 +42,7 @@ def list_items(db: Session = Depends(get_db)) -> list[dict]: ... ``` -### With python-postgres +### With postgres ```sh docker compose -f docker/postgres/compose.yml up -d diff --git a/extensions/python-sqlalchemy/template/pyproject.toml b/extensions/fastapi-sqlalchemy/template/pyproject.toml similarity index 100% rename from extensions/python-sqlalchemy/template/pyproject.toml rename to extensions/fastapi-sqlalchemy/template/pyproject.toml diff --git a/extensions/python-auth-jwt/template/app/features/auth/__init__.py b/extensions/python-auth-jwt/template/app/features/auth/__init__.py deleted file mode 100644 index 18b0211..0000000 --- a/extensions/python-auth-jwt/template/app/features/auth/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""JWT auth feature skeleton (python-auth-jwt extension).""" diff --git a/scripts/ci/validate-registry.py b/scripts/ci/validate-registry.py index e7afd0f..30854d9 100755 --- a/scripts/ci/validate-registry.py +++ b/scripts/ci/validate-registry.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""L0 integrity: registry paths on disk + schema shape.""" +"""L0 integrity: registry paths, naming law, and template quality bar.""" from __future__ import annotations @@ -19,6 +19,108 @@ template_dir, ) +# Folder prefix required for stack-bound extensions (single type). +STACK_PREFIX_BY_TYPE: dict[str, str] = { + "fastapi-backend": "fastapi", + "django-backend": "django", + "celery-worker": "celery", + "cli-app": "cli", + "uv-workspace": "uv-workspace", +} + +REQUIRED_TEMPLATE_DOCS = ( + "docs/README.md", + "docs/PROJECT_STRUCTURE.md", + "docs/CONFIGURATION.md", + "docs/TESTING_GUIDE.md", + "docs/DEPLOYMENT.md", + "docs/TYPING.md", +) + +HTTP_API_TYPES = frozenset({"fastapi-backend", "django-backend"}) + + +def _has_root_readme(path: Path) -> bool: + return (path / "README.md").is_file() or (path / "README.md.template").is_file() + + +def _has_env_example(path: Path) -> bool: + return (path / ".env.example").is_file() or (path / ".env.example.template").is_file() + + +def _has_agents_contributing(path: Path) -> bool: + agents = (path / "AGENTS.md").is_file() or (path / "AGENTS.md.template").is_file() + contrib = (path / "CONTRIBUTING.md").is_file() or (path / "CONTRIBUTING.md.template").is_file() + return agents and contrib + + +def _has_tests(path: Path) -> bool: + tests = path / "tests" + if tests.is_dir(): + return any(tests.rglob("test_*.py")) or any(tests.rglob("test_*.py.template")) + return any( + p.is_dir() + and p.name == "tests" + and (any(p.rglob("test_*.py")) or any(p.rglob("test_*.py.template"))) + for p in path.rglob("tests") + ) + + +def validate_extension_folder_name(directory: str, types: list[str], slug: str) -> list[str]: + errors: list[str] = [] + if directory.startswith("python-"): + errors.append( + f"extension {slug}: folder `{directory}` must not use deprecated python-* prefix" + ) + return errors + + if directory.startswith("all-"): + return errors + + if len(types) != 1: + errors.append( + f"extension {slug}: multi-type overlays must use all-* folder " + f"(got `{directory}` for types={types})" + ) + return errors + + prefix = STACK_PREFIX_BY_TYPE.get(types[0]) + if prefix is None: + errors.append( + f"extension {slug}: unknown type {types[0]!r} for stack folder check " + f"(update STACK_PREFIX_BY_TYPE)" + ) + return errors + + expected = f"{prefix}-" + if not directory.startswith(expected): + errors.append( + f"extension {slug}: folder `{directory}` must be `all-*` or start with " + f"`{expected}` for type {types[0]}" + ) + return errors + + +def validate_template_quality_bar(slug: str, type_name: str, path: Path) -> list[str]: + errors: list[str] = [] + for relative in REQUIRED_TEMPLATE_DOCS: + if not (path / relative).is_file(): + errors.append(f"template {slug}: missing quality-bar file {relative}") + + if not _has_root_readme(path): + errors.append(f"template {slug}: missing README.md or README.md.template") + if not _has_agents_contributing(path): + errors.append(f"template {slug}: missing AGENTS.md and/or CONTRIBUTING.md") + if not _has_env_example(path): + errors.append(f"template {slug}: missing .env.example or .env.example.template") + if not _has_tests(path): + errors.append(f"template {slug}: missing tests (test_*.py under tests/)") + + if type_name in HTTP_API_TYPES and not (path / "docs" / "API.md").is_file(): + errors.append(f"template {slug}: HTTP API templates require docs/API.md") + + return errors + def main() -> None: registry = load_registry() @@ -29,49 +131,59 @@ def main() -> None: schema_path = REPO_ROOT / "templates.schema.json" if schema_path.is_file(): - # Structural presence only; full JSON Schema validation is optional. json.loads(schema_path.read_text(encoding="utf-8")) category_slugs = {c["slug"] for c in registry.get("categories", [])} for template in registry.get("templates", []): + slug = template.get("slug", "") directory = template_dir(template) if not directory: - errors.append(f"template {template.get('slug')}: cannot parse url") + errors.append(f"template {slug}: cannot parse url") continue path = on_disk_path_for_entry("template", template) if path is None or not path.is_dir(): - errors.append(f"template {template.get('slug')}: missing on-disk path {path}") + errors.append(f"template {slug}: missing on-disk path {path}") + continue category = template.get("category") if category not in category_slugs: - errors.append(f"template {template.get('slug')}: unknown category {category}") + errors.append(f"template {slug}: unknown category {category}") + type_name = template.get("type") + if not isinstance(type_name, str) or not type_name: + errors.append(f"template {slug}: missing string type") + else: + errors.extend(validate_template_quality_bar(slug, type_name, path)) for extension in registry.get("extensions", []): + slug = extension.get("slug", "") directory = extension_dir(extension) if not directory: - errors.append(f"extension {extension.get('slug')}: cannot parse url") + errors.append(f"extension {slug}: cannot parse url") continue path = on_disk_path_for_entry("extension", extension) if path is None or not path.is_dir(): - errors.append(f"extension {extension.get('slug')}: missing on-disk path {path}") + errors.append(f"extension {slug}: missing on-disk path {path}") + continue category = extension.get("category") if category not in category_slugs: - errors.append(f"extension {extension.get('slug')}: unknown category {category}") - if not as_types(extension.get("type")): - errors.append(f"extension {extension.get('slug')}: empty type") + errors.append(f"extension {slug}: unknown category {category}") + types = as_types(extension.get("type")) + if not types: + errors.append(f"extension {slug}: empty type") + else: + errors.extend(validate_extension_folder_name(directory, types, slug)) - # Bidirectional incompatibleWith when declared for other_slug in extension.get("incompatibleWith") or []: other = next( (e for e in registry["extensions"] if e["slug"] == other_slug), None ) if other is None: errors.append( - f"extension {extension['slug']}: incompatibleWith unknown slug {other_slug}" + f"extension {slug}: incompatibleWith unknown slug {other_slug}" ) - elif extension["slug"] not in (other.get("incompatibleWith") or []): + elif slug not in (other.get("incompatibleWith") or []): errors.append( - f"extension {extension['slug']}: incompatibleWith {other_slug} " + f"extension {slug}: incompatibleWith {other_slug} " "is not symmetric" ) diff --git a/templates.json b/templates.json index c3b390c..7d0c87e 100644 --- a/templates.json +++ b/templates.json @@ -105,7 +105,7 @@ { "name": "FastAPI Starter", "slug": "fastapi-starter", - "description": "Production-ready FastAPI API with uv, Ruff, pytest, mypy, pyright, and pydantic-settings", + "description": "Production-ready FastAPI API with feature-based layout, uv, Ruff, pytest, mypy, pyright, and pydantic-settings. The reference CPA backend template.", "url": "https://github.com/Create-Python-App/cpa-templates?subdir=templates/fastapi-starter", "type": "fastapi-backend", "category": "backend-applications", @@ -123,7 +123,7 @@ { "name": "CLI Starter", "slug": "cli-starter", - "description": "Typer CLI with uv, Ruff, pytest, and a console script entry point", + "description": "Typer CLI with multi-command layout, uv, Ruff, pytest, and a console script entry point. Best for internal tools and shippable CLIs.", "url": "https://github.com/Create-Python-App/cpa-templates?subdir=templates/cli-starter", "type": "cli-app", "category": "cli-applications", @@ -137,7 +137,7 @@ { "name": "Celery Worker", "slug": "celery-worker", - "description": "Celery worker with Redis defaults, pydantic-settings, and eager-mode tests", + "description": "Celery worker with modular tasks, Redis defaults, pydantic-settings, and eager-mode tests. Choose it for background jobs outside the HTTP process.", "url": "https://github.com/Create-Python-App/cpa-templates?subdir=templates/celery-worker", "type": "celery-worker", "category": "backend-applications", @@ -152,7 +152,7 @@ { "name": "Django API", "slug": "django-api", - "description": "Django + DRF API starter with uv, Ruff, pytest-django, and health probes", + "description": "Django + DRF API with feature apps, OpenAPI-friendly health probes, uv, Ruff, and pytest-django. Use when you want Django's ORM and admin with a clean API layout.", "url": "https://github.com/Create-Python-App/cpa-templates?subdir=templates/django-api", "type": "django-backend", "category": "backend-applications", @@ -167,7 +167,7 @@ { "name": "uv Workspace Starter", "slug": "uv-workspace-starter", - "description": "Python monorepo using uv workspaces: shared packages/ libraries and apps/ deployables with one lockfile, Ruff, Pyright, and pytest", + "description": "Python monorepo using uv workspaces: shared packages/ libraries and apps/ deployables with one lockfile, Ruff, Pyright, and pytest.", "url": "https://github.com/Create-Python-App/cpa-templates?subdir=templates/uv-workspace-starter", "type": "uv-workspace", "category": "monorepo", @@ -184,8 +184,8 @@ { "name": "GitHub Setup", "slug": "github-setup", - "description": "GitHub Actions CI, MegaLinter, Danger PR review, todo-to-issue, Dependabot, and issue/PR templates", - "url": "https://github.com/Create-Python-App/cpa-templates?subdir=extensions/github-setup", + "description": "GitHub Actions CI, MegaLinter, Danger PR review, todo-to-issue, Dependabot, and issue/PR templates. Use for any CPA template that lives on GitHub.", + "url": "https://github.com/Create-Python-App/cpa-templates?subdir=extensions/all-github-setup", "type": [ "celery-worker", "cli-app", @@ -203,27 +203,29 @@ ] }, { - "name": "Python Docker", - "slug": "python-docker", - "description": "Dockerfile, compose.yml, and compose.prod.yml for local and containerized runs", - "url": "https://github.com/Create-Python-App/cpa-templates?subdir=extensions/python-docker", + "name": "Development Container", + "slug": "development-container", + "description": "VS Code Dev Container with Python 3.12 and uv for consistent editor environments across stacks.", + "url": "https://github.com/Create-Python-App/cpa-templates?subdir=extensions/all-devcontainer", "type": [ "celery-worker", "cli-app", - "fastapi-backend" + "django-backend", + "fastapi-backend", + "uv-workspace" ], - "category": "containers", + "category": "editor", "labels": [ - "Docker", - "DevOps", - "Container" + "DevContainer", + "VS Code", + "uv" ] }, { - "name": "Python Postgres", - "slug": "python-postgres", - "description": "PostgreSQL service under docker/postgres/compose.yml plus env examples", - "url": "https://github.com/Create-Python-App/cpa-templates?subdir=extensions/python-postgres", + "name": "Postgres", + "slug": "postgres", + "description": "PostgreSQL 16 Compose service under docker/postgres/ plus env examples. Infra-only \u2014 does not write application ORM code.", + "url": "https://github.com/Create-Python-App/cpa-templates?subdir=extensions/all-postgres", "type": [ "celery-worker", "django-backend", @@ -237,29 +239,55 @@ ] }, { - "name": "Python Dev Container", - "slug": "python-devcontainer", - "description": "VS Code Dev Container with Python 3.12 and uv", - "url": "https://github.com/Create-Python-App/cpa-templates?subdir=extensions/python-devcontainer", + "name": "FastAPI Docker", + "slug": "fastapi-docker", + "description": "Dockerfile and Compose for FastAPI (uvicorn app.main:app). Reach for it when containerizing the FastAPI starter.", + "url": "https://github.com/Create-Python-App/cpa-templates?subdir=extensions/fastapi-docker", "type": [ - "celery-worker", - "cli-app", - "django-backend", - "fastapi-backend", - "uv-workspace" + "fastapi-backend" ], - "category": "editor", + "category": "containers", "labels": [ - "DevContainer", - "VS Code", - "uv" + "Docker", + "FastAPI", + "Container" ] }, { - "name": "Python SQLAlchemy", - "slug": "python-sqlalchemy", - "description": "SQLAlchemy 2.x session helpers and Alembic migrations for FastAPI", - "url": "https://github.com/Create-Python-App/cpa-templates?subdir=extensions/python-sqlalchemy", + "name": "Django Docker", + "slug": "django-docker", + "description": "Dockerfile and Compose for Django (runserver / gunicorn config.wsgi). Use with django-api.", + "url": "https://github.com/Create-Python-App/cpa-templates?subdir=extensions/django-docker", + "type": [ + "django-backend" + ], + "category": "containers", + "labels": [ + "Docker", + "Django", + "Container" + ] + }, + { + "name": "Celery Docker", + "slug": "celery-docker", + "description": "Dockerfile and Compose for a Celery worker plus Redis broker. Pair with a reachable BROKER_URL (Compose sets redis://redis:6379/0).", + "url": "https://github.com/Create-Python-App/cpa-templates?subdir=extensions/celery-docker", + "type": [ + "celery-worker" + ], + "category": "containers", + "labels": [ + "Docker", + "Celery", + "Container" + ] + }, + { + "name": "FastAPI SQLAlchemy", + "slug": "fastapi-sqlalchemy", + "description": "SQLAlchemy 2.x session helpers and Alembic migrations for FastAPI (writes app/db/).", + "url": "https://github.com/Create-Python-App/cpa-templates?subdir=extensions/fastapi-sqlalchemy", "type": [ "fastapi-backend" ], @@ -268,14 +296,14 @@ "SQLAlchemy", "Alembic", "ORM", - "Database" + "FastAPI" ] }, { - "name": "Python Redis", - "slug": "python-redis", - "description": "Redis client helper and local Compose service", - "url": "https://github.com/Create-Python-App/cpa-templates?subdir=extensions/python-redis", + "name": "FastAPI Redis", + "slug": "fastapi-redis", + "description": "Redis client helper under app/core plus a local Redis Compose service for FastAPI apps.", + "url": "https://github.com/Create-Python-App/cpa-templates?subdir=extensions/fastapi-redis", "type": [ "fastapi-backend" ], @@ -283,14 +311,14 @@ "labels": [ "Redis", "Cache", - "Queue" + "FastAPI" ] }, { - "name": "Python Sentry", - "slug": "python-sentry", - "description": "Sentry SDK for FastAPI with env-driven init helper", - "url": "https://github.com/Create-Python-App/cpa-templates?subdir=extensions/python-sentry", + "name": "FastAPI Sentry", + "slug": "fastapi-sentry", + "description": "Sentry SDK helper for FastAPI with env-driven init (no-op without SENTRY_DSN).", + "url": "https://github.com/Create-Python-App/cpa-templates?subdir=extensions/fastapi-sentry", "type": [ "fastapi-backend" ], @@ -298,14 +326,14 @@ "labels": [ "Sentry", "Observability", - "Errors" + "FastAPI" ] }, { - "name": "Python Auth JWT", - "slug": "python-auth-jwt", - "description": "JWT auth feature skeleton with password hashing helpers", - "url": "https://github.com/Create-Python-App/cpa-templates?subdir=extensions/python-auth-jwt", + "name": "FastAPI Auth JWT", + "slug": "fastapi-auth-jwt", + "description": "JWT auth feature skeleton under app/features/auth for FastAPI starters.", + "url": "https://github.com/Create-Python-App/cpa-templates?subdir=extensions/fastapi-auth-jwt", "type": [ "fastapi-backend" ], @@ -313,6 +341,7 @@ "labels": [ "Auth", "JWT", + "FastAPI", "Security" ] } diff --git a/templates/celery-worker/AGENTS.md b/templates/celery-worker/AGENTS.md new file mode 100644 index 0000000..cba2622 --- /dev/null +++ b/templates/celery-worker/AGENTS.md @@ -0,0 +1,34 @@ +# AGENTS.md – AI Interaction & Execution Guide (Humans: see CONTRIBUTING.md & docs/) + +## Authoritative references + +| Topic | Source | +|-------|--------| +| Architecture | docs/PROJECT_STRUCTURE.md | +| Configuration | docs/CONFIGURATION.md | +| Testing | docs/TESTING_GUIDE.md | +| Deployment | docs/DEPLOYMENT.md | +| Typing | docs/TYPING.md | + +## Key commands + +| Command | Purpose | +|---------|---------| +| `uv run celery -A worker.celery_app worker --loglevel=INFO` | Run worker | +| `uv run pytest` | Eager-mode tests | +| `uv run ruff check .` | Lint | +| `uv run mypy worker` | Types | + +## Task work protocol + +1. Add modules under `worker/tasks/.py`. +2. Export from `worker/tasks/__init__.py` when callers import by name. +3. Keep task names stable (`worker.tasks.`). +4. Cover with eager pytest; do not require Redis in unit tests. +5. Update docs when introducing retries, routing, or beat schedules. + +## Guardrails + +- Do not put business logic only inside `@app.task` wrappers without a testable function body. +- Do not hardcode broker URLs in code — use settings / env. +- Flag large dependency additions for human confirmation. diff --git a/templates/celery-worker/CONTRIBUTING.md b/templates/celery-worker/CONTRIBUTING.md new file mode 100644 index 0000000..89d3fc9 --- /dev/null +++ b/templates/celery-worker/CONTRIBUTING.md @@ -0,0 +1,30 @@ +# Contributing + +## Setup + +```bash +uv sync +uv run pytest +``` + +Optional local broker: + +```bash +docker run --rm -p 6379:6379 redis:7 +uv run celery -A worker.celery_app worker --loglevel=INFO +``` + +## Style + +- Task modules under `worker/tasks/` +- Ruff for lint/format +- Typed public task signatures + +## Docs + +Update the matching file under `docs/` when behaviour changes. Keep `AGENTS.md` +as a pointer table, not a second docs tree. + +## Extensions + +Optional: `celery-docker`, `postgres`, `github-setup`, `development-container`. diff --git a/templates/celery-worker/README.md b/templates/celery-worker/README.md index c8d5a67..941cd00 100644 --- a/templates/celery-worker/README.md +++ b/templates/celery-worker/README.md @@ -1,6 +1,8 @@ # Celery Worker -Celery worker starter with Redis broker defaults, [uv](https://docs.astral.sh/uv/), [Ruff](https://docs.astral.sh/ruff/), and [pytest](https://docs.pytest.org/). +Celery worker starter with Redis broker defaults, feature-style task modules, +[uv](https://docs.astral.sh/uv/), [Ruff](https://docs.astral.sh/ruff/), and +[pytest](https://docs.pytest.org/). ## Quick start @@ -26,7 +28,7 @@ uv run python -c "from worker.tasks import ping; print(ping.delay().get(timeout= | `uv run celery -A worker.celery_app worker --loglevel=INFO` | Run worker | | `uv run celery -A worker.celery_app beat --loglevel=INFO` | Run beat (when schedules are added) | | `uv run ruff check .` | Lint | -| `uv run pytest` | Run eager-mode unit tests (no broker) | +| `uv run pytest` | Eager-mode unit tests (no broker) | ## Project layout @@ -34,17 +36,27 @@ uv run python -c "from worker.tasks import ping; print(ping.delay().get(timeout= worker/ celery_app.py # Celery app config.py # pydantic-settings - tasks.py # example tasks + tasks/ # task modules (health, math, …) tests/ # eager-mode pytest suite +docs/ # structure, config, testing, deployment ``` ## Configuration -Copy `.env.example` to `.env`. Scaffold-time options: +Copy `.env.example` to `.env`. See [docs/CONFIGURATION.md](docs/CONFIGURATION.md). + +Scaffold-time options: | Option | Default | Description | |--------|---------|-------------| | `brokerUrl` | `redis://localhost:6379/0` | Celery broker | | `resultBackend` | `redis://localhost:6379/1` | Result backend | -Compatible extensions: `python-docker`, `python-postgres`, `github-setup`, `python-devcontainer`. +Compatible extensions: `celery-docker`, `postgres`, `github-setup`, `development-container`. + +## Docs + +- [docs/PROJECT_STRUCTURE.md](docs/PROJECT_STRUCTURE.md) +- [docs/TESTING_GUIDE.md](docs/TESTING_GUIDE.md) +- [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) +- [AGENTS.md](AGENTS.md) · [CONTRIBUTING.md](CONTRIBUTING.md) diff --git a/templates/celery-worker/docs/CONFIGURATION.md b/templates/celery-worker/docs/CONFIGURATION.md new file mode 100644 index 0000000..2fc5967 --- /dev/null +++ b/templates/celery-worker/docs/CONFIGURATION.md @@ -0,0 +1,38 @@ +# Configuration + +Settings live in `worker/config.py` (pydantic-settings) and load from the +environment / `.env`. + +| Variable | Default (scaffold) | Purpose | +|----------|--------------------|---------| +| `BROKER_URL` | `redis://localhost:6379/0` | Celery broker | +| `RESULT_BACKEND` | `redis://localhost:6379/1` | Result store | +| `TASK_ALWAYS_EAGER` | `false` | Run tasks in-process (tests / local debug) | + +Scaffold-time options in `cpa.config.json`: + +| Option | Default | Description | +|--------|---------|-------------| +| `brokerUrl` | `redis://localhost:6379/0` | Written into config template | +| `resultBackend` | `redis://localhost:6379/1` | Written into config template | + +## Celery conf highlights + +Set in `worker/celery_app.py`: + +- JSON serializers only +- UTC timezone +- `task_track_started=True` +- `task_always_eager` driven by settings + +## Retries + +Example: `worker.tasks.add` uses `autoretry_for=(ValueError,)`, `retry_backoff=True`, +and `max_retries=3`. Prefer the same pattern for I/O-bound work; use `bind=True` +when you need `self.retry(...)` / request metadata. + +## Compatible extensions + +- `celery-docker` — container image with worker CMD +- `postgres` — Compose Postgres (if you later store results / app state) +- `github-setup`, `development-container` diff --git a/templates/celery-worker/docs/DEPLOYMENT.md b/templates/celery-worker/docs/DEPLOYMENT.md new file mode 100644 index 0000000..dbac2d8 --- /dev/null +++ b/templates/celery-worker/docs/DEPLOYMENT.md @@ -0,0 +1,34 @@ +# Deployment + +## Process model + +Run one or more Celery worker processes against a shared broker: + +```bash +uv run celery -A worker.celery_app worker --loglevel=INFO --concurrency=2 +``` + +Add beat only when you introduce periodic schedules: + +```bash +uv run celery -A worker.celery_app beat --loglevel=INFO +``` + +## Containers + +Scaffold with the `celery-docker` extension for a Dockerfile that launches the +worker CMD. Pair with Redis (and optionally Postgres) via Compose. + +## Production checklist + +- [ ] Broker / backend URLs from secrets, not committed `.env` +- [ ] Concurrency and prefetch tuned for task mix +- [ ] Health: expose a probe task (`ping`) or process-level liveness +- [ ] Logs structured; avoid dumping PII in task args +- [ ] Graceful shutdown (`SIGTERM`) so in-flight tasks finish or requeue + +## Observability + +Wire metrics / Sentry at the Celery signal layer when you outgrow stdout logs. +Do not reuse FastAPI-only `fastapi-sentry` overlays on this stack without adapting +imports. diff --git a/templates/celery-worker/docs/PROJECT_STRUCTURE.md b/templates/celery-worker/docs/PROJECT_STRUCTURE.md new file mode 100644 index 0000000..95c1526 --- /dev/null +++ b/templates/celery-worker/docs/PROJECT_STRUCTURE.md @@ -0,0 +1,32 @@ +# Project structure + +``` +worker/ + __init__.py + celery_app.py # Celery application + conf + config.py # pydantic-settings (from env / .env) + tasks/ + __init__.py # re-exports registered tasks + health.py # probe tasks (ping) + math.py # example compute tasks (add) +tests/ + conftest.py # eager-mode autouse fixture + test_tasks.py +docs/ # this documentation suite +.env.example # broker / backend defaults +pyproject.toml +``` + +## Adding a task module + +1. Create `worker/tasks/.py` with `@app.task(...)` functions. +2. Import the new symbols from `worker/tasks/__init__.py` (or rely on + `include=["worker.tasks"]` plus side-effect imports). +3. Add tests under `tests/` that call `.delay(...).get()` (eager in CI). +4. Document retries / routing in CONFIGURATION.md when introducing new patterns. + +## Conventions + +- Task names use a stable dotted path (`worker.tasks.`). +- Keep business logic in plain functions; wrap with `@app.task` at the edge. +- Do not put Redis/broker I/O in unit tests — use eager mode. diff --git a/templates/celery-worker/docs/README.md b/templates/celery-worker/docs/README.md index 0aade62..e9f6f45 100644 --- a/templates/celery-worker/docs/README.md +++ b/templates/celery-worker/docs/README.md @@ -1,5 +1,17 @@ -# Celery Worker docs +# Documentation -- Unit tests run with `task_always_eager=True` so CI does not need Redis. -- For local/runtime checks, start Redis and run the worker process. -- Add new tasks in `worker/tasks.py` and include modules via `celery_app.py`. +Guides for this Celery worker scaffold. + +## Resources + +- [PROJECT_STRUCTURE.md](PROJECT_STRUCTURE.md) — package layout and where to add tasks +- [CONFIGURATION.md](CONFIGURATION.md) — broker, backend, eager mode, env vars +- [TESTING_GUIDE.md](TESTING_GUIDE.md) — pytest with `task_always_eager` +- [TYPING.md](TYPING.md) — mypy / pyright +- [DEPLOYMENT.md](DEPLOYMENT.md) — running workers in production / containers + +## See also + +- [../README.md](../README.md) — overview and quickstart +- [../CONTRIBUTING.md](../CONTRIBUTING.md) — human contribution workflow +- [../AGENTS.md](../AGENTS.md) — guide for AI assistants diff --git a/templates/celery-worker/docs/TESTING_GUIDE.md b/templates/celery-worker/docs/TESTING_GUIDE.md new file mode 100644 index 0000000..843fb61 --- /dev/null +++ b/templates/celery-worker/docs/TESTING_GUIDE.md @@ -0,0 +1,39 @@ +# Testing guide + +Unit tests run **without Redis** via Celery eager mode. + +## How it works + +`tests/conftest.py` sets: + +```python +app.conf.task_always_eager = True +app.conf.task_eager_propagates = True +``` + +Calls like `add.delay(2, 3).get()` execute in-process and raise errors immediately. + +## Commands + +```bash +uv sync +uv run pytest +uv run ruff check . +uv run mypy worker +``` + +## Integration smoke (optional) + +With Redis running: + +```bash +uv run celery -A worker.celery_app worker --loglevel=INFO +# another shell +uv run python -c "from worker.tasks import ping; print(ping.delay().get(timeout=5))" +``` + +## What to cover + +- Happy path for each public task +- Failure / validation cases when you add retries or soft time limits +- Avoid asserting on broker transport details in unit tests diff --git a/templates/celery-worker/docs/TYPING.md b/templates/celery-worker/docs/TYPING.md new file mode 100644 index 0000000..58a8f81 --- /dev/null +++ b/templates/celery-worker/docs/TYPING.md @@ -0,0 +1,9 @@ +# Typing + +```bash +uv run mypy worker +uv run pyright +``` + +Annotate public task signatures. Celery/kombu imports are ignored in mypy via +`pyproject.toml` overrides when stubs are missing. diff --git a/templates/celery-worker/worker/tasks/__init__.py b/templates/celery-worker/worker/tasks/__init__.py new file mode 100644 index 0000000..7e691f2 --- /dev/null +++ b/templates/celery-worker/worker/tasks/__init__.py @@ -0,0 +1,12 @@ +"""Example Celery tasks package. + +Add new modules under ``worker/tasks/`` and import them from +``worker.tasks`` so autodiscovery registers them. +""" + +from __future__ import annotations + +from worker.tasks.health import ping +from worker.tasks.math import add + +__all__ = ["add", "ping"] diff --git a/templates/celery-worker/worker/tasks.py b/templates/celery-worker/worker/tasks/health.py similarity index 51% rename from templates/celery-worker/worker/tasks.py rename to templates/celery-worker/worker/tasks/health.py index f97a346..76e8c8a 100644 --- a/templates/celery-worker/worker/tasks.py +++ b/templates/celery-worker/worker/tasks/health.py @@ -1,16 +1,10 @@ -"""Example Celery tasks.""" +"""Health / probe tasks.""" from __future__ import annotations from worker.celery_app import app -@app.task(name="worker.tasks.add") -def add(left: int, right: int) -> int: - """Return the sum of two integers.""" - return left + right - - @app.task(name="worker.tasks.ping") def ping() -> str: """Health-style probe task.""" diff --git a/templates/celery-worker/worker/tasks/math.py b/templates/celery-worker/worker/tasks/math.py new file mode 100644 index 0000000..dce0c4f --- /dev/null +++ b/templates/celery-worker/worker/tasks/math.py @@ -0,0 +1,18 @@ +"""Math-related tasks.""" + +from __future__ import annotations + +from worker.celery_app import app + + +@app.task( + name="worker.tasks.add", + autoretry_for=(ValueError,), + retry_backoff=True, + retry_kwargs={"max_retries": 3}, +) +def add(left: int, right: int) -> int: + """Return the sum of two integers (retries on ValueError for demo).""" + if not isinstance(left, int) or not isinstance(right, int): + raise ValueError("add expects integers") + return left + right diff --git a/templates/cli-starter/.env.example b/templates/cli-starter/.env.example new file mode 100644 index 0000000..54df4d5 --- /dev/null +++ b/templates/cli-starter/.env.example @@ -0,0 +1,2 @@ +# CLI log level (example — see docs/CONFIGURATION.md) +CLI_LOG_LEVEL=INFO diff --git a/templates/cli-starter/AGENTS.md b/templates/cli-starter/AGENTS.md new file mode 100644 index 0000000..d5630db --- /dev/null +++ b/templates/cli-starter/AGENTS.md @@ -0,0 +1,33 @@ +# AGENTS.md – AI Interaction & Execution Guide (Humans: see CONTRIBUTING.md & docs/) + +## Authoritative references + +| Topic | Source | +|-------|--------| +| Architecture | docs/PROJECT_STRUCTURE.md | +| Configuration | docs/CONFIGURATION.md | +| Testing | docs/TESTING_GUIDE.md | +| Deployment | docs/DEPLOYMENT.md | +| Typing | docs/TYPING.md | + +## Key commands + +| Command | Purpose | +|---------|---------| +| `uv run my-cli --help` | CLI help (name from scaffold) | +| `uv run pytest` | Tests | +| `uv run ruff check .` | Lint | +| `uv run mypy cli_app` | Types | + +## Command work protocol + +1. Add modules under `cli_app/commands/`. +2. Register thin Typer wrappers in `cli_app/cli.py` that call `commands/`. +3. Cover with CliRunner tests. +4. Update docs when introducing config or packaging changes. + +## Guardrails + +- Prefer Rich for user output; avoid print noise in libraries. +- Do not dump secrets to stdout. +- Flag large dependency additions for human confirmation. diff --git a/templates/cli-starter/CONTRIBUTING.md b/templates/cli-starter/CONTRIBUTING.md new file mode 100644 index 0000000..d658c20 --- /dev/null +++ b/templates/cli-starter/CONTRIBUTING.md @@ -0,0 +1,24 @@ +# Contributing + +## Setup + +```bash +uv sync +uv run pytest +uv run my-cli --help +``` + +## Style + +- Command groups under `cli_app/commands/` +- Ruff for lint/format +- Typed public functions + +## Docs + +Update the matching file under `docs/` when behaviour changes. Keep `AGENTS.md` +as a pointer table, not a second docs tree. + +## Extensions + +Optional: `github-setup`, `development-container`. diff --git a/templates/cli-starter/README.md b/templates/cli-starter/README.md index cd8359a..2702cd6 100644 --- a/templates/cli-starter/README.md +++ b/templates/cli-starter/README.md @@ -1,6 +1,7 @@ # CLI Starter -Typer CLI starter with [uv](https://docs.astral.sh/uv/), [Ruff](https://docs.astral.sh/ruff/), and [pytest](https://docs.pytest.org/). +Typer CLI starter with multi-command packages, [uv](https://docs.astral.sh/uv/), +[Ruff](https://docs.astral.sh/ruff/), and [pytest](https://docs.pytest.org/). ## Quick start @@ -8,6 +9,7 @@ Typer CLI starter with [uv](https://docs.astral.sh/uv/), [Ruff](https://docs.ast uv sync uv run my-cli --help uv run my-cli hello +uv run my-cli version ``` ## Commands @@ -15,15 +17,20 @@ uv run my-cli hello | Command | Description | |---------|-------------| | `uv run my-cli --help` | Show CLI help | -| `uv run my-cli hello [name]` | Sample command | +| `uv run my-cli hello [name]` | Sample greeting | +| `uv run my-cli version` | Print package version | | `uv run ruff check .` | Lint | | `uv run pytest` | Run tests | ## Project layout ``` -cli_app/ # Typer application package -tests/ # CLI invocation tests +cli_app/ + cli.py # Typer root + config.py # env helpers + commands/ # command groups (greet, info, …) +tests/ +docs/ ``` ## Configuration @@ -34,4 +41,13 @@ Scaffold-time option (`cpa.config.json`): |--------|---------|-------------| | `commandName` | `my-cli` | Console script name written to `[project.scripts]` | -Compatible extensions: `github-setup`, `python-devcontainer`, `python-docker`. +See [docs/CONFIGURATION.md](docs/CONFIGURATION.md). + +Compatible extensions: `github-setup`, `development-container`. + +## Docs + +- [docs/PROJECT_STRUCTURE.md](docs/PROJECT_STRUCTURE.md) +- [docs/TESTING_GUIDE.md](docs/TESTING_GUIDE.md) +- [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) +- [AGENTS.md](AGENTS.md) · [CONTRIBUTING.md](CONTRIBUTING.md) diff --git a/templates/cli-starter/cli_app/cli.py b/templates/cli-starter/cli_app/cli.py index 0ef31ef..d26e4b1 100644 --- a/templates/cli-starter/cli_app/cli.py +++ b/templates/cli-starter/cli_app/cli.py @@ -2,10 +2,14 @@ from __future__ import annotations +import logging + import typer from rich.console import Console from cli_app import __version__ +from cli_app.commands import greet, info +from cli_app.config import load_settings app = typer.Typer( name="cli", @@ -20,6 +24,11 @@ def main( ctx: typer.Context, version: bool = typer.Option(False, "--version", help="Show version and exit."), ) -> None: + settings = load_settings() + logging.basicConfig(level=getattr(logging, settings.log_level.upper(), logging.INFO)) + ctx.ensure_object(dict) + ctx.obj["settings"] = settings + if version: console.print(__version__) raise typer.Exit(0) @@ -27,7 +36,13 @@ def main( console.print(ctx.get_help()) -@app.command() -def hello(name: str = typer.Argument("world", help="Who to greet.")) -> None: +@app.command("hello") +def hello_cmd(name: str = typer.Argument("world", help="Who to greet.")) -> None: """Print a friendly greeting.""" - console.print(f"Hello, {name}!") + greet.hello(name) + + +@app.command("version") +def version_cmd() -> None: + """Print the package version (same as --version).""" + info.show_version() diff --git a/templates/cli-starter/cli_app/commands/__init__.py b/templates/cli-starter/cli_app/commands/__init__.py new file mode 100644 index 0000000..9b84056 --- /dev/null +++ b/templates/cli-starter/cli_app/commands/__init__.py @@ -0,0 +1,3 @@ +"""Command modules for the CLI.""" + +from __future__ import annotations diff --git a/templates/cli-starter/cli_app/commands/greet.py b/templates/cli-starter/cli_app/commands/greet.py new file mode 100644 index 0000000..1ece23f --- /dev/null +++ b/templates/cli-starter/cli_app/commands/greet.py @@ -0,0 +1,12 @@ +"""Greeting command implementation.""" + +from __future__ import annotations + +from rich.console import Console + +console = Console() + + +def hello(name: str = "world") -> None: + """Print a friendly greeting.""" + console.print(f"Hello, {name}!") diff --git a/templates/cli-starter/cli_app/commands/info.py b/templates/cli-starter/cli_app/commands/info.py new file mode 100644 index 0000000..22661ee --- /dev/null +++ b/templates/cli-starter/cli_app/commands/info.py @@ -0,0 +1,14 @@ +"""Informational command implementations.""" + +from __future__ import annotations + +from rich.console import Console + +from cli_app import __version__ + +console = Console() + + +def show_version() -> None: + """Print the package version.""" + console.print(__version__) diff --git a/templates/cli-starter/cli_app/config.py b/templates/cli-starter/cli_app/config.py new file mode 100644 index 0000000..0d1aec7 --- /dev/null +++ b/templates/cli-starter/cli_app/config.py @@ -0,0 +1,20 @@ +"""Shared CLI configuration helpers. + +Extend this module when commands need env-based settings (API URLs, verbosity). +""" + +from __future__ import annotations + +import os +from dataclasses import dataclass + + +@dataclass(frozen=True) +class Settings: + """Runtime settings loaded from the environment.""" + + log_level: str = "INFO" + + +def load_settings() -> Settings: + return Settings(log_level=os.environ.get("CLI_LOG_LEVEL", "INFO")) diff --git a/templates/cli-starter/docs/CONFIGURATION.md b/templates/cli-starter/docs/CONFIGURATION.md new file mode 100644 index 0000000..089dd9d --- /dev/null +++ b/templates/cli-starter/docs/CONFIGURATION.md @@ -0,0 +1,17 @@ +# Configuration + +| Variable | Default | Purpose | +|----------|---------|---------| +| `CLI_LOG_LEVEL` | `INFO` | Example setting via `cli_app.config.load_settings()` | + +Scaffold-time option (`cpa.config.json`): + +| Option | Default | Description | +|--------|---------|-------------| +| `commandName` | `my-cli` | Console script name written to `[project.scripts]` | + +## Compatible extensions + +- `github-setup`, `development-container` +- Stack Docker overlays (`fastapi-docker`, etc.) are not a fit for a pure CLI; + add a dedicated packaging story under DEPLOYMENT.md if you need containers. diff --git a/templates/cli-starter/docs/DEPLOYMENT.md b/templates/cli-starter/docs/DEPLOYMENT.md new file mode 100644 index 0000000..21be69b --- /dev/null +++ b/templates/cli-starter/docs/DEPLOYMENT.md @@ -0,0 +1,31 @@ +# Deployment + +## Console script + +After `uv sync` / install, the scaffold exposes: + +```bash +uv run my-cli --help +``` + +(`my-cli` is the default `commandName`; rename via scaffold options.) + +## Packaging + +```bash +uv build +# publish wheel/sdist to your index when ready +``` + +## Containers (optional) + +Pure CLIs often ship as wheels or standalone binaries (PyInstaller / shiv). +If you need a container, write a thin Dockerfile that installs the wheel and +sets `ENTRYPOINT` to the console script — do not reuse FastAPI Docker overlays. + +## Checklist + +- [ ] Console script name stable for users +- [ ] `--version` matches package version +- [ ] CI runs `pytest` + `ruff` +- [ ] README documents install for end users diff --git a/templates/cli-starter/docs/PROJECT_STRUCTURE.md b/templates/cli-starter/docs/PROJECT_STRUCTURE.md new file mode 100644 index 0000000..5440c2c --- /dev/null +++ b/templates/cli-starter/docs/PROJECT_STRUCTURE.md @@ -0,0 +1,29 @@ +# Project structure + +``` +cli_app/ + __init__.py # package version + __main__.py # python -m cli_app + cli.py # Typer root app + callbacks + config.py # env-based settings helpers + commands/ + greet.py # `hello` command group + info.py # `info` command group +tests/ + test_cli.py +docs/ +pyproject.toml # [project.scripts] console entry +``` + +## Adding a command + +1. Create `cli_app/commands/.py` with a `typer.Typer()` group. +2. Register commands in `cli_app/cli.py` (thin Typer wrappers calling `commands/`). +3. Add CliRunner tests in `tests/`. +4. Document user-facing flags in the root README when they are part of the UX. + +## Conventions + +- Prefer Rich for user-visible output. +- Keep parsing/validation at the Typer boundary; put reusable logic in plain functions. +- Avoid a single mega-`cli.py` — split by domain under `commands/`. diff --git a/templates/cli-starter/docs/README.md b/templates/cli-starter/docs/README.md index 51d711d..0a51787 100644 --- a/templates/cli-starter/docs/README.md +++ b/templates/cli-starter/docs/README.md @@ -1,5 +1,17 @@ -# CLI Starter docs +# Documentation -- Keep commands documented in the root README. -- Prefer Typer + Rich for user-facing output. -- Add new commands under `cli_app/cli.py` (or split modules as the tool grows). +Guides for this Typer CLI scaffold. + +## Resources + +- [PROJECT_STRUCTURE.md](PROJECT_STRUCTURE.md) — package layout and command modules +- [CONFIGURATION.md](CONFIGURATION.md) — env vars and scaffold options +- [TESTING_GUIDE.md](TESTING_GUIDE.md) — CliRunner patterns +- [TYPING.md](TYPING.md) — mypy / pyright +- [DEPLOYMENT.md](DEPLOYMENT.md) — packaging and distributing the console script + +## See also + +- [../README.md](../README.md) — overview and quickstart +- [../CONTRIBUTING.md](../CONTRIBUTING.md) — human contribution workflow +- [../AGENTS.md](../AGENTS.md) — guide for AI assistants diff --git a/templates/cli-starter/docs/TESTING_GUIDE.md b/templates/cli-starter/docs/TESTING_GUIDE.md new file mode 100644 index 0000000..1cb9aa6 --- /dev/null +++ b/templates/cli-starter/docs/TESTING_GUIDE.md @@ -0,0 +1,29 @@ +# Testing guide + +Use Typer's `CliRunner` for in-process invocation (no subprocess). + +```python +from typer.testing import CliRunner +from cli_app.cli import app + +runner = CliRunner() +result = runner.invoke(app, ["hello", "CPA"]) +assert result.exit_code == 0 +``` + +Strip ANSI codes when asserting Rich output (see `tests/test_cli.py`). + +## Commands + +```bash +uv sync +uv run pytest +uv run ruff check . +uv run mypy cli_app +``` + +## What to cover + +- `--version` / help paths +- Happy path for each command group +- Error exits (`raise typer.Exit(code=1)`) when you add validation diff --git a/templates/cli-starter/docs/TYPING.md b/templates/cli-starter/docs/TYPING.md new file mode 100644 index 0000000..52b5009 --- /dev/null +++ b/templates/cli-starter/docs/TYPING.md @@ -0,0 +1,8 @@ +# Typing + +```bash +uv run mypy cli_app +uv run pyright +``` + +Annotate command functions and shared helpers under `cli_app/`. diff --git a/templates/cli-starter/tests/test_cli.py b/templates/cli-starter/tests/test_cli.py index 1c48bc5..4492d19 100644 --- a/templates/cli-starter/tests/test_cli.py +++ b/templates/cli-starter/tests/test_cli.py @@ -12,7 +12,7 @@ def _plain(text: str) -> str: return _ANSI.sub("", text) -def test_version() -> None: +def test_version_flag() -> None: result = runner.invoke(app, ["--version"]) assert result.exit_code == 0 assert "0.1.0" in _plain(result.stdout) @@ -28,3 +28,9 @@ def test_hello_name() -> None: result = runner.invoke(app, ["hello", "CPA"]) assert result.exit_code == 0 assert "Hello, CPA!" in _plain(result.stdout) + + +def test_version_command() -> None: + result = runner.invoke(app, ["version"]) + assert result.exit_code == 0 + assert "0.1.0" in _plain(result.stdout) diff --git a/templates/django-api/AGENTS.md b/templates/django-api/AGENTS.md new file mode 100644 index 0000000..6a3e027 --- /dev/null +++ b/templates/django-api/AGENTS.md @@ -0,0 +1,30 @@ +# AGENTS.md – AI Interaction & Execution Guide (Humans: see CONTRIBUTING.md & docs/) + +## Authoritative references + +| Topic | Source | +|-------|--------| +| Architecture | docs/PROJECT_STRUCTURE.md | +| API | docs/API.md | +| Testing | docs/TESTING_GUIDE.md | +| Deployment | docs/DEPLOYMENT.md | +| Configuration | docs/CONFIGURATION.md | +| Typing | docs/TYPING.md | + +## Key commands + +| Command | Purpose | +|---------|---------| +| `uv run python manage.py runserver` | Dev server | +| `uv run gunicorn config.wsgi:application` | Prod-style WSGI | +| `uv run ruff check .` | Lint | +| `uv run pytest` | Tests | +| `uv run mypy apps config` | Types | + +## Feature work protocol + +1. Copy `docs/examples/feature-app/` → `apps//`. +2. Register in `INSTALLED_APPS`. +3. Wire urls under `API_PREFIX`. +4. Add tests; keep response envelope `{data,error,meta}`. +5. Update OpenAPI-facing serializers when shapes change (`/api/.../docs/`). diff --git a/templates/django-api/CONTRIBUTING.md b/templates/django-api/CONTRIBUTING.md new file mode 100644 index 0000000..6f213c6 --- /dev/null +++ b/templates/django-api/CONTRIBUTING.md @@ -0,0 +1,24 @@ +# Contributing + +## Setup + +```bash +uv sync +uv run python manage.py migrate +uv run pytest +``` + +## Style + +- Feature apps under `apps/` +- Copy new features from `docs/examples/feature-app/` +- Ruff for lint/format +- Typed public APIs (serializers + view annotations) + +## Docs + +Update the matching file under `docs/` when behaviour changes. Keep `AGENTS.md` as a pointer table, not a second docs tree. + +## Extensions + +Optional: `django-docker`, `postgres`, `github-setup`, `development-container`. diff --git a/templates/django-api/QUALITY.md b/templates/django-api/QUALITY.md new file mode 100644 index 0000000..57c87a0 --- /dev/null +++ b/templates/django-api/QUALITY.md @@ -0,0 +1,12 @@ +# Quality checklist (django-api) — M1 + +## Required + +- [x] Feature apps under `apps/` with serializers / services / views / urls +- [x] Docs suite: PROJECT_STRUCTURE, API, CONFIGURATION, TESTING_GUIDE, DEPLOYMENT, TYPING +- [x] README, AGENTS, CONTRIBUTING present +- [x] OpenAPI via drf-spectacular (`/schema/`, `/docs/`) +- [x] pytest-django health test +- [x] Ruff + mypy/pyright configured +- [x] Feature scaffold lives under `docs/examples/` (not an installed app) +- [x] Compatible with `django-docker` and `postgres` extensions diff --git a/templates/django-api/README.md b/templates/django-api/README.md index e80109a..84abfb4 100644 --- a/templates/django-api/README.md +++ b/templates/django-api/README.md @@ -1,45 +1,51 @@ -# Django API +# Django + DRF API -Django + Django REST Framework API starter with [uv](https://docs.astral.sh/uv/), [Ruff](https://docs.astral.sh/ruff/), and [pytest-django](https://pytest-django.readthedocs.io/). +Production-oriented Django REST Framework starter with a **feature-app** layout, +uv, Ruff, pytest-django, mypy/pyright stubs, and docs that match the CPA quality bar. -## Quick start +## Quickstart -```sh +```bash uv sync uv run python manage.py migrate uv run python manage.py runserver +curl -s http://127.0.0.1:8000/api/v1/healthz/ +# OpenAPI UI: http://127.0.0.1:8000/api/v1/docs/ ``` -## Commands +Tests and lint: -| Command | Description | -|---------|-------------| -| `uv run python manage.py runserver` | Start dev server | -| `uv run python manage.py migrate` | Apply migrations | -| `uv run ruff check .` | Lint | -| `uv run pytest` | Run tests | - -## Health probes +```bash +uv run pytest +uv run ruff check . +uv run mypy apps config +``` -| Endpoint | Purpose | -|----------|---------| -| `GET /ping/` | Minimal probe | -| `GET {apiPrefix}/healthz` | API readiness probe | +## Architecture -## Compatible extensions +Domain code lives under `apps//` (serializers, services, views, urls). +Project wiring lives under `config/`. Copy `docs/examples/feature-app/` when adding a feature. -| Slug | Notes | -|------|-------| -| `github-setup` | CI / Dependabot | -| `python-devcontainer` | VS Code Dev Container | -| `python-postgres` | Postgres Compose + driver (wire `DATABASES` manually) | +See [docs/PROJECT_STRUCTURE.md](./docs/PROJECT_STRUCTURE.md). -`python-docker` currently targets FastAPI (`uvicorn app.main:app`) and is not compatible yet. +## Documentation -## Configuration +| Doc | Topic | +|-----|-------| +| [docs/README.md](./docs/README.md) | Index | +| [docs/API.md](./docs/API.md) | Endpoints, envelope, OpenAPI | +| [docs/CONFIGURATION.md](./docs/CONFIGURATION.md) | Env and tooling | +| [docs/TESTING_GUIDE.md](./docs/TESTING_GUIDE.md) | pytest-django | +| [docs/DEPLOYMENT.md](./docs/DEPLOYMENT.md) | Containers and prod | +| [docs/TYPING.md](./docs/TYPING.md) | mypy / pyright | +| [AGENTS.md](./AGENTS.md) | AI assistant guide | +| [CONTRIBUTING.md](./CONTRIBUTING.md) | Human contributor guide | -Copy `.env.example` to `.env`. Scaffold option: +## Compatible extensions -| Option | Default | Description | -|--------|---------|-------------| -| `apiPrefix` | `/api/v1` | Prefix for API routes including `/healthz` | +| Slug | Purpose | +|------|---------| +| `github-setup` | CI / Dependabot / PR automation | +| `development-container` | VS Code Dev Container | +| `django-docker` | Dockerfile + Compose for Django | +| `postgres` | Postgres Compose service (wire `DATABASES` / URL) | diff --git a/templates/django-api/api/urls.py b/templates/django-api/api/urls.py deleted file mode 100644 index d8be3e0..0000000 --- a/templates/django-api/api/urls.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.urls import path - -from api.views import HealthzView - -urlpatterns = [ - path("healthz", HealthzView.as_view(), name="healthz"), -] diff --git a/templates/django-api/api/urls_ping.py b/templates/django-api/api/urls_ping.py deleted file mode 100644 index 1d9c996..0000000 --- a/templates/django-api/api/urls_ping.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.urls import path - -from api.views import PingView - -urlpatterns = [ - path("", PingView.as_view(), name="ping"), -] diff --git a/templates/django-api/api/views.py b/templates/django-api/api/views.py deleted file mode 100644 index 45f6b3c..0000000 --- a/templates/django-api/api/views.py +++ /dev/null @@ -1,25 +0,0 @@ -"""API views for the django-api starter.""" - -from rest_framework.response import Response -from rest_framework.views import APIView - - -class HealthzView(APIView): - authentication_classes: list = [] - permission_classes: list = [] - - def get(self, request): # noqa: ARG002 - return Response( - { - "data": {"status": "healthy"}, - "message": "Service is healthy", - } - ) - - -class PingView(APIView): - authentication_classes: list = [] - permission_classes: list = [] - - def get(self, request): # noqa: ARG002 - return Response({"status": "ok"}) diff --git a/templates/django-api/api/__init__.py b/templates/django-api/apps/__init__.py similarity index 100% rename from templates/django-api/api/__init__.py rename to templates/django-api/apps/__init__.py diff --git a/templates/django-api/apps/health/__init__.py b/templates/django-api/apps/health/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/templates/django-api/api/apps.py b/templates/django-api/apps/health/apps.py similarity index 54% rename from templates/django-api/api/apps.py rename to templates/django-api/apps/health/apps.py index 878e7d5..64553da 100644 --- a/templates/django-api/api/apps.py +++ b/templates/django-api/apps/health/apps.py @@ -1,6 +1,7 @@ from django.apps import AppConfig -class ApiConfig(AppConfig): +class HealthConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" - name = "api" + name = "apps.health" + label = "health" diff --git a/templates/django-api/apps/health/serializers.py b/templates/django-api/apps/health/serializers.py new file mode 100644 index 0000000..ba6df27 --- /dev/null +++ b/templates/django-api/apps/health/serializers.py @@ -0,0 +1,15 @@ +"""Health check serializers.""" + +from __future__ import annotations + +from rest_framework import serializers + + +class HealthStatusSerializer(serializers.Serializer): + status = serializers.CharField() + + +class HealthEnvelopeSerializer(serializers.Serializer): + data = HealthStatusSerializer() + error = serializers.JSONField(allow_null=True) + meta = serializers.DictField() diff --git a/templates/django-api/apps/health/services.py b/templates/django-api/apps/health/services.py new file mode 100644 index 0000000..bff46ac --- /dev/null +++ b/templates/django-api/apps/health/services.py @@ -0,0 +1,7 @@ +"""Health check business logic.""" + +from __future__ import annotations + + +def get_health_status() -> dict[str, str]: + return {"status": "healthy"} diff --git a/templates/django-api/apps/health/urls.py b/templates/django-api/apps/health/urls.py new file mode 100644 index 0000000..de1c1d5 --- /dev/null +++ b/templates/django-api/apps/health/urls.py @@ -0,0 +1,9 @@ +"""Health feature URLs.""" + +from django.urls import path + +from apps.health.views import HealthzView + +urlpatterns = [ + path("healthz/", HealthzView.as_view(), name="healthz"), +] diff --git a/templates/django-api/apps/health/views.py b/templates/django-api/apps/health/views.py new file mode 100644 index 0000000..e7add23 --- /dev/null +++ b/templates/django-api/apps/health/views.py @@ -0,0 +1,21 @@ +"""Health check HTTP endpoints.""" + +from __future__ import annotations + +from drf_spectacular.utils import extend_schema +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.views import APIView + +from apps.health.serializers import HealthEnvelopeSerializer, HealthStatusSerializer +from apps.health.services import get_health_status + + +class HealthzView(APIView): + authentication_classes: list[object] = [] + permission_classes: list[object] = [] + + @extend_schema(responses={200: HealthEnvelopeSerializer}) + def get(self, request: Request) -> Response: + serializer = HealthStatusSerializer(get_health_status()) + return Response({"data": serializer.data, "error": None, "meta": {}}) diff --git a/templates/django-api/config/settings.py.template b/templates/django-api/config/settings.py.template index 23c8e80..d74a7f4 100644 --- a/templates/django-api/config/settings.py.template +++ b/templates/django-api/config/settings.py.template @@ -27,7 +27,8 @@ INSTALLED_APPS = [ "django.contrib.messages", "django.contrib.staticfiles", "rest_framework", - "api", + "drf_spectacular", + "apps.health", ] MIDDLEWARE = [ @@ -66,6 +67,9 @@ DATABASES = { } } +# When using the `postgres` extension, prefer DATABASE_URL / psycopg and replace +# the sqlite engine above. See docs/CONFIGURATION.md and docs/DEPLOYMENT.md. + AUTH_PASSWORD_VALIDATORS = [ {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"}, {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, @@ -90,4 +94,11 @@ REST_FRAMEWORK = { "DEFAULT_PARSER_CLASSES": [ "rest_framework.parsers.JSONParser", ], + "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", +} + +SPECTACULAR_SETTINGS = { + "TITLE": "Django API", + "DESCRIPTION": "API scaffolded with create-awesome-python-app", + "VERSION": "0.1.0", } diff --git a/templates/django-api/config/urls.py b/templates/django-api/config/urls.py index 3798709..5640f51 100644 --- a/templates/django-api/config/urls.py +++ b/templates/django-api/config/urls.py @@ -3,11 +3,17 @@ from django.conf import settings from django.contrib import admin from django.urls import include, path +from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView api_prefix = getattr(settings, "API_PREFIX", "/api/v1").lstrip("/") urlpatterns = [ path("admin/", admin.site.urls), - path("ping/", include("api.urls_ping")), - path(f"{api_prefix}/", include("api.urls")), + path(f"{api_prefix}/schema/", SpectacularAPIView.as_view(), name="schema"), + path( + f"{api_prefix}/docs/", + SpectacularSwaggerView.as_view(url_name="schema"), + name="swagger-ui", + ), + path(f"{api_prefix}/", include("apps.health.urls")), ] diff --git a/templates/django-api/docs/API.md b/templates/django-api/docs/API.md new file mode 100644 index 0000000..fb9f9fb --- /dev/null +++ b/templates/django-api/docs/API.md @@ -0,0 +1,23 @@ +# API + +## Health + +`GET {{ apiPrefix }}/healthz/` + +```json +{ + "data": { "status": "healthy" }, + "error": null, + "meta": {} +} +``` + +## OpenAPI + +| Path | Purpose | +|------|---------| +| `{{ apiPrefix }}/schema/` | OpenAPI 3 schema (drf-spectacular) | +| `{{ apiPrefix }}/docs/` | Swagger UI | + +Keep DRF serializers as the source of truth for request/response shapes; Spectacular +reads them via `DEFAULT_SCHEMA_CLASS`. diff --git a/templates/django-api/docs/CONFIGURATION.md b/templates/django-api/docs/CONFIGURATION.md new file mode 100644 index 0000000..90f083b --- /dev/null +++ b/templates/django-api/docs/CONFIGURATION.md @@ -0,0 +1,24 @@ +# Configuration + +## Environment + +| Variable | Default | Purpose | +|----------|---------|---------| +| `DJANGO_SECRET_KEY` | `dev-only-change-me` | Django secret | +| `DJANGO_DEBUG` | `true` | Debug mode | +| `DJANGO_ALLOWED_HOSTS` | `localhost,127.0.0.1` | Comma-separated hosts | + +Scaffold option `apiPrefix` (default `/api/v1`) becomes `API_PREFIX` in settings. + +## Tooling + +| Tool | Command | +|------|---------| +| Ruff | `uv run ruff check .` | +| mypy | `uv run mypy apps config` | +| pyright | `uv run pyright` | +| pytest | `uv run pytest` | + +## Database + +Default is SQLite. With the `postgres` extension, add `psycopg` (merged via extension `pyproject.toml`) and point Django `DATABASES` (or a URL helper) at the Compose service hostname `db`. diff --git a/templates/django-api/docs/DEPLOYMENT.md b/templates/django-api/docs/DEPLOYMENT.md new file mode 100644 index 0000000..1b20b53 --- /dev/null +++ b/templates/django-api/docs/DEPLOYMENT.md @@ -0,0 +1,22 @@ +# Deployment + +## Local process + +```bash +uv run python manage.py migrate +uv run python manage.py runserver +``` + +## Containers + +Add the `django-docker` extension for `Dockerfile` + Compose (`runserver` locally, `gunicorn config.wsgi` in prod overlay). + +Pair with `postgres` when you need a database service. Inside Compose, use hostname `db` instead of `localhost`. + +## Checklist + +- [ ] Set a strong `DJANGO_SECRET_KEY` +- [ ] `DJANGO_DEBUG=false` in production +- [ ] Restrict `DJANGO_ALLOWED_HOSTS` +- [ ] Run migrations before traffic +- [ ] Terminate TLS at the proxy / platform diff --git a/templates/django-api/docs/PROJECT_STRUCTURE.md b/templates/django-api/docs/PROJECT_STRUCTURE.md new file mode 100644 index 0000000..6f135ac --- /dev/null +++ b/templates/django-api/docs/PROJECT_STRUCTURE.md @@ -0,0 +1,29 @@ +# Project structure + +``` +apps/ + health/ # example feature app + serializers.py + services.py + views.py + urls.py +config/ # Django project package (settings, urls, asgi/wsgi) +docs/ + examples/feature-app/ # copy-me scaffold (not installed) +manage.py +tests/ +``` + +## Adding a feature + +1. Copy `docs/examples/feature-app/` → `apps//`. +2. Set `name = "apps."` in `apps.py` and fix imports. +3. Register in `INSTALLED_APPS`. +4. Wire urls under `API_PREFIX` in `config/urls.py`. +5. Add tests; keep the `{data, error, meta}` envelope for JSON APIs. + +## Conventions + +- Business logic in `services.py`; HTTP in `views.py`; shapes in `serializers.py`. +- Do not put domain logic in `config/`. +- Prefer one Django app per bounded context. diff --git a/templates/django-api/docs/README.md b/templates/django-api/docs/README.md index 8dbfc93..6f4c47f 100644 --- a/templates/django-api/docs/README.md +++ b/templates/django-api/docs/README.md @@ -1,5 +1,9 @@ -# Django API docs +# Documentation index -- Prefer DRF `APIView` / viewsets under `api/` as the project grows. -- Keep health probes unauthenticated. -- When adding Postgres, update `DATABASES` in `config/settings.py` and prefer env-driven DSN parsing. +- [Project structure](./PROJECT_STRUCTURE.md) +- [API](./API.md) +- [Configuration](./CONFIGURATION.md) +- [Testing](./TESTING_GUIDE.md) +- [Deployment](./DEPLOYMENT.md) +- [Typing](./TYPING.md) +- [Feature app example](./examples/feature-app/README.md) diff --git a/templates/django-api/docs/TESTING_GUIDE.md b/templates/django-api/docs/TESTING_GUIDE.md new file mode 100644 index 0000000..369b8ca --- /dev/null +++ b/templates/django-api/docs/TESTING_GUIDE.md @@ -0,0 +1,16 @@ +# Testing Guide + +Uses **pytest-django** with `DJANGO_SETTINGS_MODULE=config.settings`. + +## Patterns + +- Prefer `APIClient` for DRF views. +- Keep feature tests close to behaviour (status codes + envelope keys). +- Use eager, isolated DB — default SQLite is fine for unit tests. + +## Commands + +```bash +uv run pytest +uv run pytest -k healthz +``` diff --git a/templates/django-api/docs/TYPING.md b/templates/django-api/docs/TYPING.md new file mode 100644 index 0000000..0cdfe91 --- /dev/null +++ b/templates/django-api/docs/TYPING.md @@ -0,0 +1,17 @@ +# Typing + +This starter ships **mypy** (with `django-stubs` / `djangorestframework-stubs`) and +**pyright**. + +```bash +uv run mypy apps config +uv run pyright +``` + +## Conventions + +- Annotate public view methods and service functions +- Prefer concrete serializer fields over untyped `dict` at API boundaries +- Avoid unjustified `# type: ignore` + +See also FastAPI starter `docs/TYPING.md` for the shared CPA typed-Python bar. diff --git a/templates/django-api/docs/examples/feature-app/README.md b/templates/django-api/docs/examples/feature-app/README.md new file mode 100644 index 0000000..e774a2b --- /dev/null +++ b/templates/django-api/docs/examples/feature-app/README.md @@ -0,0 +1,10 @@ +# Feature app scaffold (copy into `apps/`) + +This directory is documentation — it is **not** an installed Django app. + +1. Copy `docs/examples/feature-app/` → `apps//`. +2. Rename the app config `name` in `apps.py` to `apps.`. +3. Fix imports from `apps....`. +4. Add the app to `INSTALLED_APPS` in `config/settings.py`. +5. Include its urls from `config/urls.py` under `API_PREFIX`. +6. Add tests under `tests/`. diff --git a/templates/django-api/docs/examples/feature-app/__init__.py b/templates/django-api/docs/examples/feature-app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/templates/django-api/docs/examples/feature-app/apps.py b/templates/django-api/docs/examples/feature-app/apps.py new file mode 100644 index 0000000..da8b1ae --- /dev/null +++ b/templates/django-api/docs/examples/feature-app/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig + + +class FeatureAppConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + # After copying to apps//, set: + # name = "apps." + name = "apps.REPLACE_ME" diff --git a/templates/django-api/docs/examples/feature-app/serializers.py b/templates/django-api/docs/examples/feature-app/serializers.py new file mode 100644 index 0000000..8401084 --- /dev/null +++ b/templates/django-api/docs/examples/feature-app/serializers.py @@ -0,0 +1,9 @@ +"""Example serializers — rename package imports after copying into apps/.""" + +from __future__ import annotations + +from rest_framework import serializers + + +class ExampleSerializer(serializers.Serializer): + message = serializers.CharField() diff --git a/templates/django-api/docs/examples/feature-app/services.py b/templates/django-api/docs/examples/feature-app/services.py new file mode 100644 index 0000000..2ea1746 --- /dev/null +++ b/templates/django-api/docs/examples/feature-app/services.py @@ -0,0 +1,7 @@ +"""Example service layer.""" + +from __future__ import annotations + + +def example_message() -> dict[str, str]: + return {"message": "replace-me"} diff --git a/templates/django-api/docs/examples/feature-app/urls.py b/templates/django-api/docs/examples/feature-app/urls.py new file mode 100644 index 0000000..65eb3f4 --- /dev/null +++ b/templates/django-api/docs/examples/feature-app/urls.py @@ -0,0 +1,7 @@ +from django.urls import path + +from .views import ExampleView + +urlpatterns = [ + path("example/", ExampleView.as_view(), name="example"), +] diff --git a/templates/django-api/docs/examples/feature-app/views.py b/templates/django-api/docs/examples/feature-app/views.py new file mode 100644 index 0000000..fd54681 --- /dev/null +++ b/templates/django-api/docs/examples/feature-app/views.py @@ -0,0 +1,18 @@ +"""Example views — after copy, import from apps..*.""" + +from __future__ import annotations + +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.views import APIView + +# from apps..serializers import ExampleSerializer +# from apps..services import example_message +from .serializers import ExampleSerializer +from .services import example_message + + +class ExampleView(APIView): + def get(self, request: Request) -> Response: + serializer = ExampleSerializer(example_message()) + return Response({"data": serializer.data, "error": None, "meta": {}}) diff --git a/templates/django-api/pyproject.toml b/templates/django-api/pyproject.toml index 5356a91..b0a8f4e 100644 --- a/templates/django-api/pyproject.toml +++ b/templates/django-api/pyproject.toml @@ -7,6 +7,8 @@ requires-python = ">=3.12" dependencies = [ "django>=5.1.0", "djangorestframework>=3.15.0", + "drf-spectacular>=0.27.0", + "gunicorn>=23.0.0", "python-dotenv>=1.0.0", ] @@ -26,7 +28,7 @@ requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] -packages = ["config", "api"] +packages = ["config", "apps"] [tool.pytest.ini_options] DJANGO_SETTINGS_MODULE = "config.settings" @@ -41,7 +43,7 @@ target-version = "py312" select = ["E", "F", "I", "UP", "B"] [tool.pyright] -include = ["api", "config", "tests"] +include = ["apps", "config", "tests"] pythonVersion = "3.12" typeCheckingMode = "basic" reportMissingImports = false @@ -49,7 +51,7 @@ reportMissingTypeStubs = false [tool.mypy] python_version = "3.12" -files = ["api", "config", "tests"] +files = ["apps", "config", "tests"] plugins = ["mypy_django_plugin.main"] warn_return_any = true warn_unused_configs = true diff --git a/templates/django-api/tests/test_health.py.template b/templates/django-api/tests/test_health.py.template index 1bf45c9..0f9c48b 100644 --- a/templates/django-api/tests/test_health.py.template +++ b/templates/django-api/tests/test_health.py.template @@ -10,13 +10,16 @@ def client() -> APIClient: def test_healthz_returns_healthy(client: APIClient) -> None: - response = client.get(f"{API_PREFIX}/healthz") + response = client.get(f"{API_PREFIX}/healthz/") assert response.status_code == 200 body = response.json() assert body["data"]["status"] == "healthy" + assert body["error"] is None -def test_ping_returns_ok(client: APIClient) -> None: - response = client.get("/ping/") +def test_openapi_schema_available(client: APIClient) -> None: + response = client.get(f"{API_PREFIX}/schema/") assert response.status_code == 200 - assert response.json()["status"] == "ok" + body = response.content.decode() + assert "openapi" in body + assert "healthy" in body or "healthz" in body.lower() or "Healthz" in body diff --git a/templates/fastapi-starter/CONTRIBUTING.md b/templates/fastapi-starter/CONTRIBUTING.md index 061fd56..46fb333 100644 --- a/templates/fastapi-starter/CONTRIBUTING.md +++ b/templates/fastapi-starter/CONTRIBUTING.md @@ -20,7 +20,7 @@ Thanks for your interest in contributing to this project! This document is the h - Python matching `requires-python` in `pyproject.toml` (3.12+) - [uv](https://docs.astral.sh/uv/) installed -- Optional: Docker if using `python-docker` / `python-postgres` extensions +- Optional: Docker if using `fastapi-docker` / `postgres` extensions ## Project Bootstrap diff --git a/templates/fastapi-starter/QUALITY.md b/templates/fastapi-starter/QUALITY.md index d1c434c..7fdbecc 100644 --- a/templates/fastapi-starter/QUALITY.md +++ b/templates/fastapi-starter/QUALITY.md @@ -20,20 +20,20 @@ Use this checklist before calling a FastAPI starter "mature" for CPA. ## Migrations -This starter does not ship an ORM by default. When using the `python-sqlalchemy` -and/or `python-postgres` extensions: +This starter does not ship an ORM by default. When using the `fastapi-sqlalchemy` +and/or `postgres` extensions: ```sh -uv run alembic upgrade head # after adding python-sqlalchemy +uv run alembic upgrade head # after adding fastapi-sqlalchemy ``` ## Extension slots (catalog slugs) | Slug | Role | |------|------| -| `python-docker` | Dockerfile + Compose | -| `python-postgres` | Postgres Compose + `psycopg` | -| `python-devcontainer` | VS Code Dev Container | +| `fastapi-docker` | Dockerfile + Compose | +| `postgres` | Postgres Compose + `psycopg` | +| `development-container` | VS Code Dev Container | | `github-setup` | GitHub Actions / Dependabot / templates | Example: @@ -41,9 +41,9 @@ Example: ```sh uvx create-awesome-python-app@latest my-api \ --template fastapi-starter \ - --addons python-docker \ - --addons python-postgres \ - --addons python-devcontainer \ + --addons fastapi-docker \ + --addons postgres \ + --addons development-container \ --no-interactive ``` diff --git a/templates/fastapi-starter/README.md b/templates/fastapi-starter/README.md index 7fe4f36..af83a91 100644 --- a/templates/fastapi-starter/README.md +++ b/templates/fastapi-starter/README.md @@ -86,7 +86,7 @@ Scripts and notes for shipping: | `uv run uvicorn app.main:app --host 0.0.0.0 --port 8000` | Production-style ASGI process | | Health probe | `GET {API_PREFIX}/healthz` → `dev_code: HEALTH_OK` | -Full guidance: [Deployment](./docs/DEPLOYMENT.md). Pair with the `python-docker` / `python-postgres` extensions when you need containers or a database. +Full guidance: [Deployment](./docs/DEPLOYMENT.md). Pair with the `fastapi-docker` / `postgres` extensions when you need containers or a database. ## Configuration @@ -117,13 +117,13 @@ Use catalog slugs (not informal names like `docker` / `vscode`): | Slug | Adds | |------|------| -| `python-docker` | Dockerfile + Compose | -| `python-postgres` | Postgres Compose + driver | -| `python-devcontainer` | VS Code Dev Container | +| `fastapi-docker` | Dockerfile + Compose | +| `postgres` | Postgres Compose + driver | +| `development-container` | VS Code Dev Container | | `github-setup` | CI / Dependabot / issue templates | -Migrations are not bundled in the base starter. Add `python-sqlalchemy` (and usually -`python-postgres`) when you need Alembic/`DATABASE_URL` workflows. +Migrations are not bundled in the base starter. Add `fastapi-sqlalchemy` (and usually +`postgres`) when you need Alembic/`DATABASE_URL` workflows. ## Quality bar diff --git a/templates/fastapi-starter/docs/DEPLOYMENT.md b/templates/fastapi-starter/docs/DEPLOYMENT.md index 598360f..8dd5694 100644 --- a/templates/fastapi-starter/docs/DEPLOYMENT.md +++ b/templates/fastapi-starter/docs/DEPLOYMENT.md @@ -62,7 +62,7 @@ Expect HTTP `200` and `"dev_code": "HEALTH_OK"`. ### Docker Compose -Pair this API with the `python-docker` / `python-postgres` extensions when you +Pair this API with the `fastapi-docker` / `postgres` extensions when you need local containers or a database. Keep the API service dependent on DB health before accepting traffic. diff --git a/templates/uv-workspace-starter/.env.example b/templates/uv-workspace-starter/.env.example new file mode 100644 index 0000000..64fe9d1 --- /dev/null +++ b/templates/uv-workspace-starter/.env.example @@ -0,0 +1,7 @@ +# Environment + +Member apps may ship their own `.env.example`. Keep secrets out of the repo; +this root file is a pointer for workspace-wide conventions. + +# Example (override per app as needed) +# CLI_LOG_LEVEL=INFO diff --git a/templates/uv-workspace-starter/AGENTS.md b/templates/uv-workspace-starter/AGENTS.md index a801224..4a5eed3 100644 --- a/templates/uv-workspace-starter/AGENTS.md +++ b/templates/uv-workspace-starter/AGENTS.md @@ -10,6 +10,10 @@ This file is for AI assistants. Humans: see `README.md` and `docs/`. | Human contribution guide | `CONTRIBUTING.md` (`CONTRIBUTING.md.template`) | | Monorepo layout | `docs/PROJECT_STRUCTURE.md` | | Day-to-day workflow | `docs/DEVELOPMENT_WORKFLOW.md` | +| Configuration | `docs/CONFIGURATION.md` | +| Testing | `docs/TESTING_GUIDE.md` | +| Typing | `docs/TYPING.md` | +| Deployment | `docs/DEPLOYMENT.md` | | Publishing members | `docs/RELEASING.md` | ## Key commands (run from the workspace root) diff --git a/templates/uv-workspace-starter/README.md.template b/templates/uv-workspace-starter/README.md.template index 6c2c43f..2967a0b 100644 --- a/templates/uv-workspace-starter/README.md.template +++ b/templates/uv-workspace-starter/README.md.template @@ -32,8 +32,8 @@ For detailed information about the architecture, see [Project Structure](./docs/ ## Extra documentation -You can find useful information such as the project structure, development -workflow, releasing guide, and much more in the [docs](./docs) folder! +Project structure, development workflow, configuration, testing, deployment, +and releasing live in the [docs](./docs) folder. ## Pre-packed diff --git a/templates/uv-workspace-starter/docs/CONFIGURATION.md b/templates/uv-workspace-starter/docs/CONFIGURATION.md new file mode 100644 index 0000000..3e5f46b --- /dev/null +++ b/templates/uv-workspace-starter/docs/CONFIGURATION.md @@ -0,0 +1,25 @@ +# Configuration + +## Tooling (workspace root) + +Shared Ruff / Pyright / mypy / pytest live in the root `pyproject.toml` and apply +across members unless a member overrides. + +| Concern | Where | +|---------|--------| +| Python version | root `requires-python` / `.python-version` | +| Lint | `[tool.ruff]` at root | +| Types | `[tool.pyright]` + `[tool.mypy]` at root | +| Tests | `[tool.pytest.ini_options]` + member `tests/` | + +## Member env + +Apps that need secrets should ship their own `.env.example` and load via +pydantic-settings or `python-dotenv` inside that member — keep env local to the +deployable, not the shared library. + +## Compatible extensions + +- `github-setup`, `development-container` +- Prefer stack-specific Docker overlays only when an `apps/*` member matches + (`fastapi-docker`, `django-docker`, `celery-docker`). diff --git a/templates/uv-workspace-starter/docs/DEPLOYMENT.md b/templates/uv-workspace-starter/docs/DEPLOYMENT.md new file mode 100644 index 0000000..b1c4469 --- /dev/null +++ b/templates/uv-workspace-starter/docs/DEPLOYMENT.md @@ -0,0 +1,25 @@ +# Deployment + +## Build all members + +```bash +uv build --all-packages +``` + +See [RELEASING.md](RELEASING.md) for versioning and publishing individual +distributions. + +## Deployable apps + +Treat `apps/*` as the units you containerize or ship: + +- CLI apps → console scripts / wheels (see member README) +- HTTP / worker apps → use the matching stack Docker extension when scaffolding + that member's layout + +## Checklist + +- [ ] Root lockfile (`uv.lock`) committed +- [ ] Each publishable member has a clear version policy +- [ ] CI runs lint + types + tests across the workspace +- [ ] Secrets never committed; per-app `.env.example` only diff --git a/templates/uv-workspace-starter/docs/README.md b/templates/uv-workspace-starter/docs/README.md index ddd3262..2110b70 100644 --- a/templates/uv-workspace-starter/docs/README.md +++ b/templates/uv-workspace-starter/docs/README.md @@ -1,7 +1,7 @@ # Documentation Extra documentation for this uv workspace monorepo — project structure, -day-to-day workflow, and releasing. +day-to-day workflow, configuration, testing, releasing, and deployment. ## Resources @@ -9,6 +9,10 @@ day-to-day workflow, and releasing. how members reference each other. - [DEVELOPMENT_WORKFLOW.md](DEVELOPMENT_WORKFLOW.md) — day-to-day commands, adding members, and troubleshooting. +- [CONFIGURATION.md](CONFIGURATION.md) — shared tooling and per-app env. +- [TESTING_GUIDE.md](TESTING_GUIDE.md) — workspace-wide pytest / typecheck. +- [TYPING.md](TYPING.md) — mypy / pyright across members. +- [DEPLOYMENT.md](DEPLOYMENT.md) — building and shipping members. - [RELEASING.md](RELEASING.md) — versioning and publishing individual members. ## See also diff --git a/templates/uv-workspace-starter/docs/TESTING_GUIDE.md b/templates/uv-workspace-starter/docs/TESTING_GUIDE.md new file mode 100644 index 0000000..e8340f6 --- /dev/null +++ b/templates/uv-workspace-starter/docs/TESTING_GUIDE.md @@ -0,0 +1,27 @@ +# Testing guide + +Run the full workspace suite from the root: + +```bash +uv sync +uv run pytest +# or +make check # lint + typecheck + tests when Makefile is present +``` + +## Layout + +- Shared library tests: `packages//tests/` +- App tests: `apps//tests/` +- Prefer testing libraries in isolation; apps may depend on path/workspace deps. + +## Conventions + +- One logical assertion focus per test +- Type-check both packages and apps (`uv run pyright` / `uv run mypy`) +- Do not skip members in CI without documenting why + +## Adding coverage + +When you add a member, ensure pytest discovers its `tests/` (or configure +`testpaths` / `pythonpath` accordingly) and that `make check` stays green. diff --git a/templates/uv-workspace-starter/docs/TYPING.md b/templates/uv-workspace-starter/docs/TYPING.md new file mode 100644 index 0000000..bd88ecc --- /dev/null +++ b/templates/uv-workspace-starter/docs/TYPING.md @@ -0,0 +1,10 @@ +# Typing + +Workspace members are typed by default (`py.typed` on libraries). From the root: + +```bash +uv run pyright +uv run mypy +``` + +Avoid unjustified `Any` and `# type: ignore` in shared packages.