From b37821a06704576627722102653bc75f2605334c Mon Sep 17 00:00:00 2001 From: Irving Rodriguez Date: Fri, 5 Dec 2025 22:13:19 -0800 Subject: [PATCH 01/11] add torchvision app template --- cookiecutter.json | 2 + hooks/post_gen_project.py | 4 ++ .../docker-compose.yml | 19 ++++++--- {{cookiecutter.repository}}/pyproject.toml | 40 +++++++++++++++++- .../src/torchvision_app/__init__.py | 0 .../src/torchvision_app/assets/astronaut.jpg | Bin 0 -> 40344 bytes .../torchvision_app/torchvision_example.py | 22 ++++++++++ 7 files changed, 80 insertions(+), 7 deletions(-) create mode 100644 {{cookiecutter.repository}}/src/torchvision_app/__init__.py create mode 100644 {{cookiecutter.repository}}/src/torchvision_app/assets/astronaut.jpg create mode 100644 {{cookiecutter.repository}}/src/torchvision_app/torchvision_example.py diff --git a/cookiecutter.json b/cookiecutter.json index f9d5241..45abfb1 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -10,6 +10,7 @@ "include_fastapi": true, "include_metaflow": true, "include_package": true, + "include_torchvision": true, "coverage_threshold": 80, "__prompts__": { "user": "GitHub User", @@ -22,6 +23,7 @@ "include_fastapi": "Include FastAPI component? (y/n)", "include_metaflow": "Include Metaflow component? (y/n)", "include_package": "Include Python package component? (y/n)", + "include_torchvision": "Include TorchVision example component? (y/n)", "coverage_threshold": "Minimum code coverage percentage (cov-fail-under)" } } diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py index 9c7afd3..06a7d60 100644 --- a/hooks/post_gen_project.py +++ b/hooks/post_gen_project.py @@ -28,3 +28,7 @@ def is_false(val): for line in lines: if 'publish.yml' not in line: f.write(line) + +# Remove torchvision app if not included in template options +if is_false('{{ cookiecutter.include_torchvision }}'): + shutil.rmtree('src/torchvision_app', ignore_errors=True) diff --git a/{{cookiecutter.repository}}/docker-compose.yml b/{{cookiecutter.repository}}/docker-compose.yml index aeab110..76d6772 100644 --- a/{{cookiecutter.repository}}/docker-compose.yml +++ b/{{cookiecutter.repository}}/docker-compose.yml @@ -1,7 +1,7 @@ # https://docs.docker.com/compose/compose-file/ services: - {% if cookiecutter.include_fastapi %} +{% if cookiecutter.include_fastapi %} fastapi_app: build: . command: ["uvicorn", "example_app.main:app", "--host", "0.0.0.0", "--reload"] @@ -9,18 +9,25 @@ services: - "8000:8000" volumes: - ./src:/app/src - {% endif %} - {% if cookiecutter.include_metaflow %} +{% endif %} +{% if cookiecutter.include_metaflow %} metaflow_app: build: . command: ["python", "metaflow_app/spin_prototype.py"] volumes: - ./src:/app/src - {% endif %} - {% if cookiecutter.include_package %} +{% endif %} +{% if cookiecutter.include_package %} python_package: build: . command: ["python", "-m", "{{cookiecutter.package}}"] volumes: - ./src:/app/src - {% endif %} +{% endif %} +{% if cookiecutter.include_torchvision %} + torchvision_app: + build: . + command: ["python", "torchvision_app/torchvision_example.py"] + volumes: + - ./src:/app/src +{% endif %} diff --git a/{{cookiecutter.repository}}/pyproject.toml b/{{cookiecutter.repository}}/pyproject.toml index 22763cf..46e9fb5 100644 --- a/{{cookiecutter.repository}}/pyproject.toml +++ b/{{cookiecutter.repository}}/pyproject.toml @@ -20,7 +20,7 @@ dependencies = [ [project.urls] Homepage = "https://github.com/{{cookiecutter.user}}/{{cookiecutter.repository}}" Documentation = "https://{{cookiecutter.user}}.github.io/{{cookiecutter.repository}}/" -Repository = "https://github.com/{{cookiecutter.user}}/{{cookiecutter.repository}}e" +Repository = "https://github.com/{{cookiecutter.user}}/{{cookiecutter.repository}}" "Bug Tracker" = "https://github.com/{{cookiecutter.user}}/{{cookiecutter.repository}}/issues" # SCRIPTS @@ -54,6 +54,44 @@ doc = ["pdoc>=15.0.1"] [tool.uv] default-groups = ["check", "commit", "dev", "doc"] +conflicts = [ + [ + { extra = "cpu" }, + { extra = "cu128" }, + ], +] + +[project.optional-dependencies] +cpu = [ + "torch>=2.7.0", + "torchvision>=0.22.0", +] +cu128 = [ + "torch>=2.7.0", + "torchvision>=0.22.0", +] + +# https://docs.astral.sh/uv/guides/integration/pytorch/#configuring-accelerators-with-optional-dependencies +# enable selecting with uv sync --extra {cpu, cu128} +[tool.uv.sources] +torch = [ + { index = "pytorch-cpu", extra = "cpu" }, + { index = "pytorch-cu128", extra = "cu128" }, +] +torchvision = [ + { index = "pytorch-cpu", extra = "cpu" }, + { index = "pytorch-cu128", extra = "cu128" }, +] + +[[tool.uv.index]] +name = "pytorch-cpu" +url = "https://download.pytorch.org/whl/cpu" +explicit = true + +[[tool.uv.index]] +name = "pytorch-cu128" +url = "https://download.pytorch.org/whl/cu128" +explicit = true [tool.bandit] targets = ["src"] diff --git a/{{cookiecutter.repository}}/src/torchvision_app/__init__.py b/{{cookiecutter.repository}}/src/torchvision_app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/{{cookiecutter.repository}}/src/torchvision_app/assets/astronaut.jpg b/{{cookiecutter.repository}}/src/torchvision_app/assets/astronaut.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9716f6562697103a3a5086ae98493eb8e5fbb46b GIT binary patch literal 40344 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 IsW3nP*~(Sxwg3PC literal 0 HcmV?d00001 diff --git a/{{cookiecutter.repository}}/src/torchvision_app/torchvision_example.py b/{{cookiecutter.repository}}/src/torchvision_app/torchvision_example.py new file mode 100644 index 0000000..8260ab6 --- /dev/null +++ b/{{cookiecutter.repository}}/src/torchvision_app/torchvision_example.py @@ -0,0 +1,22 @@ +from pathlib import Path +import torch +from torchvision.transforms import v2 +from torchvision.io import decode_image +from loguru import logger + +torch.manual_seed(1) + +def main(): + img = decode_image(str(Path(__file__).parent / "assets/astronaut.jpg")) + logger.info(f"{type(img)=}, {img.dtype=}, {img.shape=}") + transforms = v2.Compose([ + v2.RandomResizedCrop(size=(224, 224), antialias=True), + v2.RandomHorizontalFlip(p=0.5), + v2.ToDtype(torch.float32, scale=True), + v2.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), + ]) + out = transforms(img) + logger.info(f"{type(out)=}, {out.dtype=}, {out.shape=}") + +if __name__ == "__main__": + main() From d6bf1377d5a447229cc035c624ce81fd60ad55d0 Mon Sep 17 00:00:00 2001 From: Irving Rodriguez Date: Fri, 5 Dec 2025 23:01:31 -0800 Subject: [PATCH 02/11] parametrize dependencies --- hooks/post_gen_project.py | 29 +++++---------- {{cookiecutter.repository}}/README.md | 43 ++++++++++++++++------ {{cookiecutter.repository}}/pyproject.toml | 43 ++++++++++++++++------ 3 files changed, 73 insertions(+), 42 deletions(-) diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py index 06a7d60..ad035ee 100644 --- a/hooks/post_gen_project.py +++ b/hooks/post_gen_project.py @@ -1,34 +1,25 @@ import shutil import os -def is_false(val): - return str(val).strip().lower() in ("false", "n", "no", "0") +def is_false(val: str) -> bool: + return val.strip().lower() in ("false", "n", "no", "0") # Remove FastAPI component if not included -if is_false('{{cookiecutter.include_fastapi}}'): - shutil.rmtree('src/fastapi_app', ignore_errors=True) +if is_false("{{cookiecutter.include_fastapi}}"): + shutil.rmtree("src/fastapi_app", ignore_errors=True) # Remove Metaflow component if not included -if is_false('{{cookiecutter.include_metaflow}}'): - shutil.rmtree('src/metaflow_app', ignore_errors=True) +if is_false("{{cookiecutter.include_metaflow}}"): + shutil.rmtree("src/metaflow_app", ignore_errors=True) # Remove Python package component if not included -if is_false('{{cookiecutter.include_package}}'): +if is_false("{{cookiecutter.include_package}}"): shutil.rmtree("src/{{cookiecutter.package}}", ignore_errors=True) # Remove publish workflow if present - workflow_path = os.path.join('.github', 'workflows', 'publish.yml') + workflow_path = os.path.join(".github", "workflows", "publish.yml") if os.path.exists(workflow_path): os.remove(workflow_path) - # Remove publish badge from README if present - readme_path = os.path.join('.', 'README.md') - if os.path.exists(readme_path): - with open(readme_path, 'r') as f: - lines = f.readlines() - with open(readme_path, 'w') as f: - for line in lines: - if 'publish.yml' not in line: - f.write(line) # Remove torchvision app if not included in template options -if is_false('{{ cookiecutter.include_torchvision }}'): - shutil.rmtree('src/torchvision_app', ignore_errors=True) +if is_false("{{ cookiecutter.include_torchvision }}"): + shutil.rmtree("src/torchvision_app", ignore_errors=True) diff --git a/{{cookiecutter.repository}}/README.md b/{{cookiecutter.repository}}/README.md index 99b6b16..367ced1 100644 --- a/{{cookiecutter.repository}}/README.md +++ b/{{cookiecutter.repository}}/README.md @@ -1,7 +1,9 @@ # {{cookiecutter.name}} [![check.yml](https://github.com/{{cookiecutter.user}}/{{cookiecutter.repository}}/actions/workflows/check.yml/badge.svg)](https://github.com/{{cookiecutter.user}}/{{cookiecutter.repository}}/actions/workflows/check.yml) +{% if cookiecutter.include_package %} [![publish.yml](https://github.com/{{cookiecutter.user}}/{{cookiecutter.repository}}/actions/workflows/publish.yml/badge.svg)](https://github.com/{{cookiecutter.user}}/{{cookiecutter.repository}}/actions/workflows/publish.yml) +{% endif %} [![Documentation](https://img.shields.io/badge/documentation-available-brightgreen.svg)](https://{{cookiecutter.user}}.github.io/{{cookiecutter.repository}}/) [![License](https://img.shields.io/github/license/{{cookiecutter.user}}/{{cookiecutter.repository}})](https://github.com/{{cookiecutter.user}}/{{cookiecutter.repository}}/blob/main/LICENCE.txt) [![Release](https://img.shields.io/github/v/release/{{cookiecutter.user}}/{{cookiecutter.repository}})](https://github.com/{{cookiecutter.user}}/{{cookiecutter.repository}}/releases) @@ -10,7 +12,7 @@ {{cookiecutter.description}}. -This README is generated from a cookiecutter template. Delete this comment and modify your README! +TODO: This README is generated from a cookiecutter template. Delete this comment and modify your README! # Installation @@ -21,21 +23,40 @@ uv run just install ``` # Usage -The provided template apps can be executed with the existing Docker templates. **Note:** Dependencies are not yet parametrized and need to be added using `uv add` +TODO: Fill in with your project's details. -```shell -# Invoke docker compose -uv run just docker-compose +{% if cookiecutter.include_torchvision %} +## Torchvision app -# Or run with docker compose -docker compose up --build +This template includes an example torchvision app and associated dependencies. +```shell +# Install dependencies +uv sync --group torch --extra cpu # or --extra cu128 if running in CUDA-enabled system +# Then invoke docker compose +uv run just docker-compose torchvision_app +``` +{% endif %} +{% if cookiecutter.include_metaflow %} +## Metaflow app -# Or run with docker -# Note: specify platform if running on Apple M chip -docker build --platform linux/amd64 -t {{cookiecutter.repository}}-image -f Dockerfile . -docker run -it --platform linux/amd64 --name {{cookiecutter.repository}}-ctr -p 8000:8000 {{cookiecutter.repository}}-image +This template includes an example Metaflow app and associated dependencies. +```shell +# Install dependencies +uv sync --group metaflow +# Invoke docker compose +uv run just docker-compose metaflow_app ``` +{% endif %} +## FastAPI +This template includes an example FastAPI app and associated dependencies. +```shell +# Install dependencies +uv sync --group fastapi +# Invoke docker compose +uv run just docker-compose metaflow_app +``` +{% endif %} ## Development Features * **Streamlined Project Structure:** A well-defined directory layout for source code, tests, documentation, tasks, and Docker configurations. diff --git a/{{cookiecutter.repository}}/pyproject.toml b/{{cookiecutter.repository}}/pyproject.toml index 46e9fb5..3dc73c5 100644 --- a/{{cookiecutter.repository}}/pyproject.toml +++ b/{{cookiecutter.repository}}/pyproject.toml @@ -49,11 +49,32 @@ dev =[ "pyinstrument>=4.7.3,<5.0.0", ] doc = ["pdoc>=15.0.1"] +{% if cookiecutter.include_torchvision %} +torch = [ + "torch>=2.7.0", + "torchvision>=0.22.0", +] +{% endif %} +{% if cookiecutter.include_metaflow %} +metaflow = [ + "metaflow>=2.19.7", +] +{% endif %} +{% if cookiecutter.include_fastapi %} +fastapi = [ + "fastapi>=0.123.10", + "uvicorn>=0.38.0", +] +{% endif %} # TOOLS [tool.uv] default-groups = ["check", "commit", "dev", "doc"] +{% if cookiecutter.include_torchvision %} +# torch +# enable selecting with uv sync --group torch --extra {cpu, cu128} +# https://docs.astral.sh/uv/guides/integration/pytorch/#configuring-accelerators-with-optional-dependencies conflicts = [ [ { extra = "cpu" }, @@ -61,18 +82,16 @@ conflicts = [ ], ] -[project.optional-dependencies] -cpu = [ - "torch>=2.7.0", - "torchvision>=0.22.0", -] -cu128 = [ - "torch>=2.7.0", - "torchvision>=0.22.0", -] +#[project.optional-dependencies] +#cpu = [ +# "torch>=2.7.0", +# "torchvision>=0.22.0", +#] +#cu128 = [ +# "torch>=2.7.0", +# "torchvision>=0.22.0", +#] -# https://docs.astral.sh/uv/guides/integration/pytorch/#configuring-accelerators-with-optional-dependencies -# enable selecting with uv sync --extra {cpu, cu128} [tool.uv.sources] torch = [ { index = "pytorch-cpu", extra = "cpu" }, @@ -92,7 +111,7 @@ explicit = true name = "pytorch-cu128" url = "https://download.pytorch.org/whl/cu128" explicit = true - +{% endif %} [tool.bandit] targets = ["src"] From 2ea6ca36779bb33101e7c5288ac999c800412d6b Mon Sep 17 00:00:00 2001 From: Irving Rodriguez Date: Fri, 5 Dec 2025 23:21:53 -0800 Subject: [PATCH 03/11] docker fixes (in progress) --- cookiecutter.json | 2 +- {{cookiecutter.repository}}/Dockerfile | 4 ++- {{cookiecutter.repository}}/README.md | 3 ++- {{cookiecutter.repository}}/pyproject.toml | 27 ++++++++----------- {{cookiecutter.repository}}/tasks/docker.just | 4 +-- 5 files changed, 19 insertions(+), 21 deletions(-) diff --git a/cookiecutter.json b/cookiecutter.json index 45abfb1..48f5692 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -5,7 +5,7 @@ "package": "{{cookiecutter.repository.replace('-', '_')}}", "license": "MIT", "version": "0.1.0", - "description": "TODO", + "description": "TODO: Fill in description.", "python_version": "3.12", "include_fastapi": true, "include_metaflow": true, diff --git a/{{cookiecutter.repository}}/Dockerfile b/{{cookiecutter.repository}}/Dockerfile index 9c6b6b7..23c768e 100644 --- a/{{cookiecutter.repository}}/Dockerfile +++ b/{{cookiecutter.repository}}/Dockerfile @@ -1,4 +1,6 @@ # https://docs.docker.com/engine/reference/builder/ +# TODO: not convenient to have a single Dockerfile for all templates. add a docker/ subdirectory and use separate files. update docker-compose accordingly +# TODO: debug torch application # https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers # Install uv @@ -15,7 +17,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --locked --no-install-project # Copy the project into the image -ADD . /app +ADD src/ /app # Sync the project RUN --mount=type=cache,target=/root/.cache/uv \ diff --git a/{{cookiecutter.repository}}/README.md b/{{cookiecutter.repository}}/README.md index 367ced1..db27ef3 100644 --- a/{{cookiecutter.repository}}/README.md +++ b/{{cookiecutter.repository}}/README.md @@ -31,7 +31,7 @@ TODO: Fill in with your project's details. This template includes an example torchvision app and associated dependencies. ```shell # Install dependencies -uv sync --group torch --extra cpu # or --extra cu128 if running in CUDA-enabled system +uv sync --extra cpu # or --extra cu128 if running in CUDA-enabled system # Then invoke docker compose uv run just docker-compose torchvision_app ``` @@ -47,6 +47,7 @@ uv sync --group metaflow uv run just docker-compose metaflow_app ``` {% endif %} +{% if cookiecutter.include_fastapi %} ## FastAPI This template includes an example FastAPI app and associated dependencies. diff --git a/{{cookiecutter.repository}}/pyproject.toml b/{{cookiecutter.repository}}/pyproject.toml index 3dc73c5..ae5ef2e 100644 --- a/{{cookiecutter.repository}}/pyproject.toml +++ b/{{cookiecutter.repository}}/pyproject.toml @@ -49,12 +49,6 @@ dev =[ "pyinstrument>=4.7.3,<5.0.0", ] doc = ["pdoc>=15.0.1"] -{% if cookiecutter.include_torchvision %} -torch = [ - "torch>=2.7.0", - "torchvision>=0.22.0", -] -{% endif %} {% if cookiecutter.include_metaflow %} metaflow = [ "metaflow>=2.19.7", @@ -73,7 +67,7 @@ fastapi = [ default-groups = ["check", "commit", "dev", "doc"] {% if cookiecutter.include_torchvision %} # torch -# enable selecting with uv sync --group torch --extra {cpu, cu128} +# enable selecting torch wheel based on environment with uv sync --extra {cpu, cu128} # https://docs.astral.sh/uv/guides/integration/pytorch/#configuring-accelerators-with-optional-dependencies conflicts = [ [ @@ -82,15 +76,15 @@ conflicts = [ ], ] -#[project.optional-dependencies] -#cpu = [ -# "torch>=2.7.0", -# "torchvision>=0.22.0", -#] -#cu128 = [ -# "torch>=2.7.0", -# "torchvision>=0.22.0", -#] +[project.optional-dependencies] +cpu = [ + "torch>=2.7.0", + "torchvision>=0.22.0", +] +cu128 = [ + "torch>=2.7.0", + "torchvision>=0.22.0", +] [tool.uv.sources] torch = [ @@ -162,6 +156,7 @@ convention = "google" # SYSTEMS +# TODO: is this only for the package management? [build-system] requires = ["hatchling"] build-backend = "hatchling.build" diff --git a/{{cookiecutter.repository}}/tasks/docker.just b/{{cookiecutter.repository}}/tasks/docker.just index d9d7440..c985dfd 100644 --- a/{{cookiecutter.repository}}/tasks/docker.just +++ b/{{cookiecutter.repository}}/tasks/docker.just @@ -9,8 +9,8 @@ docker-build tag="latest": package-build # start docker compose [group('docker')] -docker-compose: - docker compose up --build +docker-compose-service service: + docker compose up --build {{service}} # run latest docker image [group('docker')] From dce03f86fcb67f1555d665e998aefd89d21268b2 Mon Sep 17 00:00:00 2001 From: Irving Rodriguez Date: Sat, 6 Dec 2025 23:13:21 -0800 Subject: [PATCH 04/11] Polished application skeletons and docker commands --- README.md | 28 ++++------ {{cookiecutter.repository}}/Dockerfile | 27 --------- {{cookiecutter.repository}}/README.md | 14 ++--- .../docker-compose.yml | 47 +++++++++++++--- .../docker/Dockerfile.python | 55 +++++++++++++++++++ {{cookiecutter.repository}}/pyproject.toml | 15 +++-- {{cookiecutter.repository}}/tasks/docker.just | 4 +- 7 files changed, 120 insertions(+), 70 deletions(-) delete mode 100644 {{cookiecutter.repository}}/Dockerfile create mode 100644 {{cookiecutter.repository}}/docker/Dockerfile.python diff --git a/README.md b/README.md index ccccfcb..686c0ea 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,9 @@ A python project template to simplify project setup. Adapted from https://github.com/fmind/cookiecutter-mlops-package -This template copy omits the MLFlow functionality. Use the linked mlops-package template if this is desired - The template provides a robust foundation for building, testing, packaging, and deploying Python packages and Docker Images. Adapt it to your project's needs; the source material is MLOps-focused but is suitable for a wide array of Python projects. -**Source resources**: +**Original resources**: - **[MLOps Coding Course (Learning)](https://mlops-coding-course.fmind.dev/)**: Learn how to create, develop, and maintain a state-of-the-art MLOps code base. - **[MLOps Python Package (Example)](https://github.com/fmind/mlops-python-package)**: Kickstart your MLOps initiative with a flexible, robust, and productive Python package. @@ -18,9 +16,13 @@ This template equips you with the essentials for creating, testing, and packagin You have the freedom to structure your `src/` and `tests/` directories according to your preferences. Alternatively, you can draw inspiration from the structure used in the [MLOps Python Package](https://github.com/fmind/mlops-python-package) project for a ready-made implementation. +## Applications + +This template includes a few optional application skeletons. See the nested README for details. + ## Key Features -(This section was copied into the created project's README so tool info is available to users.) +This section was copied into the created project's README so tool info is available. * **Streamlined Project Structure:** A well-defined directory layout for source code, tests, documentation, tasks, and Docker configurations. Uv Integration: Effortless dependency management and packaging with [uv](https://docs.astral.sh/uv/). @@ -43,7 +45,7 @@ uv tool install cookiecutter cookiecutter gh:irod973/python-project-template ``` -You'll be prompted for the following variables: +You'll be prompted for the following variables. - `user`: Your GitHub username. - `name`: The name of your project. @@ -53,6 +55,10 @@ You'll be prompted for the following variables: - `version`: The initial version of your project. - `description`: A brief description of your project. - `python_version`: The Python version to use (e.g., 3.12). +- `include_fastapi`: Whether to include a sample FastAPI application. +- `include_metaflow`: Whether to include a sample Metaflow application. +- `include_torchvision`: Whether to include a sample Torchvision application. +- `include_package`: Whether to include a sample application for publishing a Python package. 2. **Initialize a git repository:** @@ -72,7 +78,7 @@ git init - `src/{{cookiecutter.package}}`: Your Python package source code. - `tests/`: Unit tests for your package. - `tasks/`: `just` commands for automation. -- `Dockerfile`: Configuration for building your Docker image. +- `docker/Dockerfile.python`: Configuration for building your Docker image. - `docker-compose.yml`: Orchestration file for running your project. 4. **Start developing!** @@ -89,16 +95,6 @@ Use the provided `just` commands to manage your development workflow: - `uv run just package`: Build your Python package. - `uv run just project`: Run the project in the CLI. -## Example Usage - -### Building and Running Your Docker Image - -```bash -invoke containers -``` - -This builds a Docker image based on your [`Dockerfile`](https://github.com/fmind/cookiecutter-mlops-package/blob/main/%7B%7Bcookiecutter.repository%7D%7D/Dockerfile) and runs it. - ## License The source material this is adapted from is licensed under the [MIT License](https://opensource.org/license/mit). See the [`LICENSE.txt`](https://github.com/fmind/cookiecutter-mlops-package/blob/main/LICENSE.txt) file for details. diff --git a/{{cookiecutter.repository}}/Dockerfile b/{{cookiecutter.repository}}/Dockerfile deleted file mode 100644 index 23c768e..0000000 --- a/{{cookiecutter.repository}}/Dockerfile +++ /dev/null @@ -1,27 +0,0 @@ -# https://docs.docker.com/engine/reference/builder/ -# TODO: not convenient to have a single Dockerfile for all templates. add a docker/ subdirectory and use separate files. update docker-compose accordingly -# TODO: debug torch application -# https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers - -# Install uv -FROM python:{{cookiecutter.python_version}}-slim -COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ - -# Change the working directory to the `app` directory -WORKDIR /app - -# Install dependencies -RUN --mount=type=cache,target=/root/.cache/uv \ - --mount=type=bind,source=uv.lock,target=uv.lock \ - --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ - uv sync --locked --no-install-project - -# Copy the project into the image -ADD src/ /app - -# Sync the project -RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked - -# Default command (overridden by docker-compose) -CMD ["python", "--version"] diff --git a/{{cookiecutter.repository}}/README.md b/{{cookiecutter.repository}}/README.md index db27ef3..d781d21 100644 --- a/{{cookiecutter.repository}}/README.md +++ b/{{cookiecutter.repository}}/README.md @@ -29,10 +29,10 @@ TODO: Fill in with your project's details. ## Torchvision app This template includes an example torchvision app and associated dependencies. + +This has support for installing either CPU-only or CUDA 12.8 torch wheels via the `--extra {cpu, gpu}` uv sync argument. ```shell -# Install dependencies -uv sync --extra cpu # or --extra cu128 if running in CUDA-enabled system -# Then invoke docker compose +# Invoke docker compose uv run just docker-compose torchvision_app ``` {% endif %} @@ -41,8 +41,6 @@ uv run just docker-compose torchvision_app This template includes an example Metaflow app and associated dependencies. ```shell -# Install dependencies -uv sync --group metaflow # Invoke docker compose uv run just docker-compose metaflow_app ``` @@ -51,11 +49,11 @@ uv run just docker-compose metaflow_app ## FastAPI This template includes an example FastAPI app and associated dependencies. + +Note that this runs `fastapi dev` which includes auto-reload by default. This should be switched to `fastapi run` in production. ```shell -# Install dependencies -uv sync --group fastapi # Invoke docker compose -uv run just docker-compose metaflow_app +uv run just docker-compose fastapi_app ``` {% endif %} ## Development Features diff --git a/{{cookiecutter.repository}}/docker-compose.yml b/{{cookiecutter.repository}}/docker-compose.yml index 76d6772..0f07bd2 100644 --- a/{{cookiecutter.repository}}/docker-compose.yml +++ b/{{cookiecutter.repository}}/docker-compose.yml @@ -1,33 +1,62 @@ # https://docs.docker.com/compose/compose-file/ services: + python: + build: + context: . + dockerfile: docker/Dockerfile.python + volumes: + - ./pyproject.toml:/app/pyproject.toml + - ./uv.lock:/app/uv.lock {% if cookiecutter.include_fastapi %} fastapi_app: - build: . - command: ["uvicorn", "example_app.main:app", "--host", "0.0.0.0", "--reload"] + build: + context: . + dockerfile: docker/Dockerfile.python + args: + UV_SYNC_OPTIONS: "--group fastapi" + command: ["fastapi", "dev", "fastapi_app/main.py", "--host", "0.0.0.0"] ports: - "8000:8000" volumes: - - ./src:/app/src + - ./src/fastapi_app:/app/fastapi_app + - ./pyproject.toml:/app/pyproject.toml + - ./uv.lock:/app/uv.lock {% endif %} {% if cookiecutter.include_metaflow %} metaflow_app: - build: . + build: + context: . + dockerfile: docker/Dockerfile.python + args: + UV_SYNC_OPTIONS: "--group metaflow" command: ["python", "metaflow_app/spin_prototype.py"] volumes: - - ./src:/app/src + - ./src/metaflow_app:/app/metaflow_app + - ./pyproject.toml:/app/pyproject.toml + - ./uv.lock:/app/uv.lock {% endif %} {% if cookiecutter.include_package %} python_package: - build: . + build: + context: . + dockerfile: docker/Dockerfile.python command: ["python", "-m", "{{cookiecutter.package}}"] volumes: - - ./src:/app/src + - ./src/{{cookiecutter.package}}:/app/{{cookiecutter.package}} + - ./pyproject.toml:/app/pyproject.toml + - ./uv.lock:/app/uv.lock {% endif %} {% if cookiecutter.include_torchvision %} torchvision_app: - build: . + build: + context: . + dockerfile: docker/Dockerfile.python + args: + UV_SYNC_OPTIONS: "--extra cpu" command: ["python", "torchvision_app/torchvision_example.py"] volumes: - - ./src:/app/src + - ./src/torchvision_app:/app/torchvision_app + - ./pyproject.toml:/app/pyproject.toml + - ./uv.lock:/app/uv.lock {% endif %} diff --git a/{{cookiecutter.repository}}/docker/Dockerfile.python b/{{cookiecutter.repository}}/docker/Dockerfile.python new file mode 100644 index 0000000..a1d9403 --- /dev/null +++ b/{{cookiecutter.repository}}/docker/Dockerfile.python @@ -0,0 +1,55 @@ +# An example using multi-stage image builds to create a final image without uv +# https://github.com/astral-sh/uv-docker-example/blob/main/multistage.Dockerfile + +# First, build the application in the `/app` directory. +# See `Dockerfile` for details. +FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS builder +# Enable bytecode compilation; copy from the cache instead of linking since it's a mounted volume +ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy + +# Add any additional uv sync arguments +ARG UV_SYNC_OPTIONS + +# Disable Python downloads, because we want to use the system interpreter +# across both images. If using a managed Python version, it needs to be +# copied from the build image into the final image; see `standalone.Dockerfile` +# for an example. +ENV UV_PYTHON_DOWNLOADS=0 + +# Install the project's dependencies using the lockfile and settings +WORKDIR /app +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=uv.lock,target=uv.lock \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + uv sync --locked --no-install-project --no-dev $UV_SYNC_OPTIONS + +# Then, add the rest of the project source code and install it +# Installing separately from its dependencies allows optimal layer caching +COPY . /app +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --locked --no-dev $UV_SYNC_OPTIONS + +# Then, use a final image without uv +FROM python:3.12-slim-bookworm +# It is important to use the image that matches the builder, as the path to the +# Python executable must be the same, e.g., using `python:3.11-slim-bookworm` +# will fail. + +# Setup a non-root user +RUN groupadd --system --gid 999 nonroot \ + && useradd --system --gid 999 --uid 999 --create-home nonroot + +# Copy the application from the builder +COPY --from=builder --chown=nonroot:nonroot /app /app + +# Place executables in the environment at the front of the path +ENV PATH="/app/.venv/bin:$PATH" + +# Use the non-root user to run our application +USER nonroot + +# Use `/app` as the working directory +WORKDIR /app + +# Baseline command, modify as needed +CMD ["python", "--version"] diff --git a/{{cookiecutter.repository}}/pyproject.toml b/{{cookiecutter.repository}}/pyproject.toml index ae5ef2e..961b598 100644 --- a/{{cookiecutter.repository}}/pyproject.toml +++ b/{{cookiecutter.repository}}/pyproject.toml @@ -13,6 +13,7 @@ license = "{{cookiecutter.license}}" requires-python = ">={{cookiecutter.python_version}}" dependencies = [ "hatchling>=1.27.0", + "loguru>=0.7.3", ] # LINKS @@ -43,7 +44,6 @@ check = [ ] commit = ["commitizen>=4.4.1", "pre-commit>=4.1.0"] dev =[ - "loguru>=0.7.3", "rust-just>=1.39.0", "locust>=2.31.8,<3.0.0", "pyinstrument>=4.7.3,<5.0.0", @@ -56,8 +56,7 @@ metaflow = [ {% endif %} {% if cookiecutter.include_fastapi %} fastapi = [ - "fastapi>=0.123.10", - "uvicorn>=0.38.0", + "fastapi[standard]>=0.123.10", ] {% endif %} @@ -67,12 +66,12 @@ fastapi = [ default-groups = ["check", "commit", "dev", "doc"] {% if cookiecutter.include_torchvision %} # torch -# enable selecting torch wheel based on environment with uv sync --extra {cpu, cu128} +# enable selecting torch wheel based on environment with uv sync --extra {cpu, gpu} # https://docs.astral.sh/uv/guides/integration/pytorch/#configuring-accelerators-with-optional-dependencies conflicts = [ [ { extra = "cpu" }, - { extra = "cu128" }, + { extra = "gpu" }, ], ] @@ -81,7 +80,7 @@ cpu = [ "torch>=2.7.0", "torchvision>=0.22.0", ] -cu128 = [ +gpu = [ "torch>=2.7.0", "torchvision>=0.22.0", ] @@ -89,11 +88,11 @@ cu128 = [ [tool.uv.sources] torch = [ { index = "pytorch-cpu", extra = "cpu" }, - { index = "pytorch-cu128", extra = "cu128" }, + { index = "pytorch-cu128", extra = "gpu" }, ] torchvision = [ { index = "pytorch-cpu", extra = "cpu" }, - { index = "pytorch-cu128", extra = "cu128" }, + { index = "pytorch-cu128", extra = "gpu" }, ] [[tool.uv.index]] diff --git a/{{cookiecutter.repository}}/tasks/docker.just b/{{cookiecutter.repository}}/tasks/docker.just index c985dfd..a3633fa 100644 --- a/{{cookiecutter.repository}}/tasks/docker.just +++ b/{{cookiecutter.repository}}/tasks/docker.just @@ -5,12 +5,12 @@ docker: docker-build docker-run # build docker image [group('docker')] docker-build tag="latest": package-build - {% raw %}docker build --tag={{REPOSITORY}}:{{tag}} .{% endraw %} + {% raw %}docker build --tag={{REPOSITORY}}:{{tag}} .{% endraw %} -f docker/Dockerfile.python # start docker compose [group('docker')] docker-compose-service service: - docker compose up --build {{service}} + docker compose up --build {% raw %}{{service}}{% endraw %} # run latest docker image [group('docker')] From 8f2adf8cca41dbb532b5aded17d882b40a8903f6 Mon Sep 17 00:00:00 2001 From: Irving Rodriguez Date: Sat, 6 Dec 2025 23:29:03 -0800 Subject: [PATCH 05/11] add torchvision prompt to test --- tests/test_cookiecutter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_cookiecutter.py b/tests/test_cookiecutter.py index e2feb79..4f74086 100644 --- a/tests/test_cookiecutter.py +++ b/tests/test_cookiecutter.py @@ -58,8 +58,9 @@ def test_project_generation(cookies: Cookies) -> None: "python_version": context["python_version"], "include_fastapi": context["include_fastapi"], "include_metaflow": context["include_metaflow"], + "include_torchvision": context["include_torchvision"], "include_package": context["include_package"], - "coverage_threshold": context["coverage_threshold"] + "coverage_threshold": context["coverage_threshold"], } # - commands shell = Subprocess(cwd=result.project_path) From 64da92b86b7e07d66127c1d54779c95c195cbb67 Mon Sep 17 00:00:00 2001 From: Irving Rodriguez Date: Sat, 6 Dec 2025 23:30:02 -0800 Subject: [PATCH 06/11] add torchvision prompt to test --- tests/test_cookiecutter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_cookiecutter.py b/tests/test_cookiecutter.py index 4f74086..780c691 100644 --- a/tests/test_cookiecutter.py +++ b/tests/test_cookiecutter.py @@ -35,7 +35,8 @@ def test_project_generation(cookies: Cookies) -> None: "include_fastapi": "y", "include_metaflow": "y", "include_package": "y", - "coverage_threshold": "1" + "include_torchvision": "y", + "coverage_threshold": "1", } repository = context['name'].lower().replace(' ', '-') package = repository.replace('-', '_') From b5d04685e6e80d6821ab707dcb63d31c076640bc Mon Sep 17 00:00:00 2001 From: Irving Rodriguez Date: Sat, 6 Dec 2025 23:57:18 -0800 Subject: [PATCH 07/11] check fixes --- README.md | 17 +++++++++++++++++ hooks/post_gen_project.py | 3 ++- pyproject.toml | 10 ++++++++++ tests/test_cookiecutter.py | 2 +- .../src/metaflow_app/spin_prototype.py | 2 +- .../src/torchvision_app/torchvision_example.py | 5 +++-- 6 files changed, 34 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 686c0ea..54b7139 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,23 @@ Uv Integration: Effortless dependency management and packaging with [uv](https:/ * **Comprehensive Documentation:** [pdoc](https://pdoc.dev/) generates API documentation, and Markdown files provide clear usage instructions. * **GitHub Workflow Integration:** Continuous integration and deployment workflows are set up using [GitHub Actions](https://github.com/features/actions), automating testing, checks, and publishing. +## Development + +### Testing +```shell +uv run just test +``` + +### Checks + +This will run formatting, typing and coverage checks. + +```shell +uv run just check +``` + +### Type checking + ## Quick Start 1. **Generate your project:** diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py index ad035ee..410e4a2 100644 --- a/hooks/post_gen_project.py +++ b/hooks/post_gen_project.py @@ -1,5 +1,6 @@ -import shutil import os +import shutil + def is_false(val: str) -> bool: return val.strip().lower() in ("false", "n", "no", "0") diff --git a/pyproject.toml b/pyproject.toml index 1083e03..52b1d1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,3 +39,13 @@ update_changelog_on_bump = true [tool.pytest.ini_options] log_cli = true log_cli_level = "INFO" + +[tool.mypy] +pretty = true +python_version = "3.13" +check_untyped_defs = true +ignore_missing_imports = true +strict = false +disable_error_code = ["import-untyped"] +# See https://mypy.readthedocs.io/en/stable/config_file.html#untyped-definitions-and-calls +disallow_untyped_decorators = false diff --git a/tests/test_cookiecutter.py b/tests/test_cookiecutter.py index 780c691..9f57d2e 100644 --- a/tests/test_cookiecutter.py +++ b/tests/test_cookiecutter.py @@ -31,7 +31,7 @@ def test_project_generation(cookies: Cookies) -> None: "license": "Apache-2.0", # Note: needs to be a "valid SPDX identifier" "version": "1.0.0", "description": "A test project.", - "python_version": "3.13", + "python_version": "3.12", "include_fastapi": "y", "include_metaflow": "y", "include_package": "y", diff --git a/{{cookiecutter.repository}}/src/metaflow_app/spin_prototype.py b/{{cookiecutter.repository}}/src/metaflow_app/spin_prototype.py index ec1c171..60784bb 100644 --- a/{{cookiecutter.repository}}/src/metaflow_app/spin_prototype.py +++ b/{{cookiecutter.repository}}/src/metaflow_app/spin_prototype.py @@ -28,7 +28,7 @@ def start(self): from io import StringIO self.rows = [] - for row in csv.reader(StringIO(self.movie_data), delimiter=","): + for row in csv.reader(StringIO(self.movie_data), delimiter=","): # type: ignore logger.info(f"{row=}") self.rows.append(row) diff --git a/{{cookiecutter.repository}}/src/torchvision_app/torchvision_example.py b/{{cookiecutter.repository}}/src/torchvision_app/torchvision_example.py index 8260ab6..f27ac6b 100644 --- a/{{cookiecutter.repository}}/src/torchvision_app/torchvision_example.py +++ b/{{cookiecutter.repository}}/src/torchvision_app/torchvision_example.py @@ -1,8 +1,9 @@ from pathlib import Path + import torch -from torchvision.transforms import v2 -from torchvision.io import decode_image from loguru import logger +from torchvision.io import decode_image +from torchvision.transforms import v2 torch.manual_seed(1) From 71eb93fa0d02714d7fde9375bebf3b3b26322353 Mon Sep 17 00:00:00 2001 From: Irving Rodriguez Date: Sun, 7 Dec 2025 00:00:27 -0800 Subject: [PATCH 08/11] Update hooks/post_gen_project.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- hooks/post_gen_project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py index 410e4a2..6c2683d 100644 --- a/hooks/post_gen_project.py +++ b/hooks/post_gen_project.py @@ -22,5 +22,5 @@ def is_false(val: str) -> bool: os.remove(workflow_path) # Remove torchvision app if not included in template options -if is_false("{{ cookiecutter.include_torchvision }}"): +if is_false("{{cookiecutter.include_torchvision}}"): shutil.rmtree("src/torchvision_app", ignore_errors=True) From 9560a3ed669a9eae890a812f618d9c3a7041f95b Mon Sep 17 00:00:00 2001 From: Irving Rodriguez Date: Sun, 7 Dec 2025 09:45:20 -0800 Subject: [PATCH 09/11] update build backend; add profilers --- README.md | 9 +++------ {{cookiecutter.repository}}/docker/Dockerfile.python | 4 ++-- {{cookiecutter.repository}}/pyproject.toml | 8 ++++---- {{cookiecutter.repository}}/tasks/package.just | 2 +- 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 54b7139..d23adf9 100644 --- a/README.md +++ b/README.md @@ -32,17 +32,14 @@ Uv Integration: Effortless dependency management and packaging with [uv](https:/ * **uv+just Task Automation:** [just](https://github.com/casey/just) commands to simplify development workflows such as cleaning, installing, formatting, checking, building, documenting and running the project. * **Comprehensive Documentation:** [pdoc](https://pdoc.dev/) generates API documentation, and Markdown files provide clear usage instructions. * **GitHub Workflow Integration:** Continuous integration and deployment workflows are set up using [GitHub Actions](https://github.com/features/actions), automating testing, checks, and publishing. +* Profiling: Several standard profilers are included for developers to choose from. Two popular call-stack profilers are [pyinstrument](https://github.com/joerick/pyinstrument) and [pyspy](https://github.com/benfred/py-spy). [memray](https://github.com/bloomberg/memray) is included for memory profiling. +* Load testing with [Locust](https://locust.io/). ## Development -### Testing -```shell -uv run just test -``` - ### Checks -This will run formatting, typing and coverage checks. +This will run all checks on this cookiecutter repo (not just the project template) as specified in the `tasks/check.just` command: code quality, test coverage, unit tests, formatting, typing, and security. ```shell uv run just check diff --git a/{{cookiecutter.repository}}/docker/Dockerfile.python b/{{cookiecutter.repository}}/docker/Dockerfile.python index a1d9403..744dd27 100644 --- a/{{cookiecutter.repository}}/docker/Dockerfile.python +++ b/{{cookiecutter.repository}}/docker/Dockerfile.python @@ -3,7 +3,7 @@ # First, build the application in the `/app` directory. # See `Dockerfile` for details. -FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS builder +FROM ghcr.io/astral-sh/uv:python{{cookiecutter.python_version}}-bookworm-slim AS builder # Enable bytecode compilation; copy from the cache instead of linking since it's a mounted volume ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy @@ -30,7 +30,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --locked --no-dev $UV_SYNC_OPTIONS # Then, use a final image without uv -FROM python:3.12-slim-bookworm +FROM python:{{cookiecutter.python_version}}-slim-bookworm # It is important to use the image that matches the builder, as the path to the # Python executable must be the same, e.g., using `python:3.11-slim-bookworm` # will fail. diff --git a/{{cookiecutter.repository}}/pyproject.toml b/{{cookiecutter.repository}}/pyproject.toml index 961b598..5a8ee34 100644 --- a/{{cookiecutter.repository}}/pyproject.toml +++ b/{{cookiecutter.repository}}/pyproject.toml @@ -12,7 +12,6 @@ readme = "README.md" license = "{{cookiecutter.license}}" requires-python = ">={{cookiecutter.python_version}}" dependencies = [ - "hatchling>=1.27.0", "loguru>=0.7.3", ] @@ -47,6 +46,8 @@ dev =[ "rust-just>=1.39.0", "locust>=2.31.8,<3.0.0", "pyinstrument>=4.7.3,<5.0.0", + "pyspy>=0.1.1", + ] doc = ["pdoc>=15.0.1"] {% if cookiecutter.include_metaflow %} @@ -155,7 +156,6 @@ convention = "google" # SYSTEMS -# TODO: is this only for the package management? [build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" +requires = ["uv_build>=0.9.16,<0.10.0"] +build-backend = "uv_build" diff --git a/{{cookiecutter.repository}}/tasks/package.just b/{{cookiecutter.repository}}/tasks/package.just index 765ca2b..4c83e86 100644 --- a/{{cookiecutter.repository}}/tasks/package.just +++ b/{{cookiecutter.repository}}/tasks/package.just @@ -10,4 +10,4 @@ package-constraints constraints="constraints.txt": # build python package [group('package')] package-build constraints="constraints.txt": clean-build package-constraints - {% raw %}uv build --build-constraint={{constraints}} --require-hashes --wheel{% endraw %} + {% raw %}uv build --build-constraint={{constraints}} --wheel{% endraw %} From e3ec941ecaeacc7b3ed21bf9d058bb332f113da1 Mon Sep 17 00:00:00 2001 From: Irving Rodriguez Date: Sun, 7 Dec 2025 09:49:27 -0800 Subject: [PATCH 10/11] add postgen script docstring --- hooks/post_gen_project.py | 1 + 1 file changed, 1 insertion(+) diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py index 6c2683d..0f98fbe 100644 --- a/hooks/post_gen_project.py +++ b/hooks/post_gen_project.py @@ -1,3 +1,4 @@ +"""Remove any unwanted optional components after project generation.""" import os import shutil From 7d8f638c773927d3fbf4c987683842519090c4d8 Mon Sep 17 00:00:00 2001 From: Irving Rodriguez Date: Sun, 7 Dec 2025 09:51:19 -0800 Subject: [PATCH 11/11] Update {{cookiecutter.repository}}/tasks/docker.just Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- {{cookiecutter.repository}}/tasks/docker.just | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/{{cookiecutter.repository}}/tasks/docker.just b/{{cookiecutter.repository}}/tasks/docker.just index a3633fa..2829dcc 100644 --- a/{{cookiecutter.repository}}/tasks/docker.just +++ b/{{cookiecutter.repository}}/tasks/docker.just @@ -5,7 +5,7 @@ docker: docker-build docker-run # build docker image [group('docker')] docker-build tag="latest": package-build - {% raw %}docker build --tag={{REPOSITORY}}:{{tag}} .{% endraw %} -f docker/Dockerfile.python + {% raw %}docker build --tag={{REPOSITORY}}:{{tag}} -f docker/Dockerfile.python .{% endraw %} # start docker compose [group('docker')]