From c988e1ee17354189cee1ade3cb1a8386edc22332 Mon Sep 17 00:00:00 2001 From: Irving Rodriguez Date: Fri, 25 Apr 2025 14:35:18 -0700 Subject: [PATCH 1/6] pull latest changes from https://github.com/fmind/cookiecutter-mlops-package/pull/2 --- .github/workflows/tests.yml/tests.yml | 27 + .gitignore | 14 + .pre-commit-config.yaml | 15 + README.md | 85 ++- justfile | 33 + poetry.toml | 5 - pyproject.toml | 48 +- tests/test_cookiecutter.py | 29 +- uv.lock | 655 ++++++++++++++++++ .../{{cookiecutter.repository}}/.env.example | 0 .../.github/actions/setup/action.yml | 13 + .../.github/rulesets/main.json | 58 ++ .../.github/workflows/check.yml | 20 + .../.github/workflows/publish.yml | 47 ++ .../{{cookiecutter.repository}}/.gitignore | 31 + .../.pre-commit-config.yaml | 33 + .../.python-version | 1 + .../{{cookiecutter.repository}}/Dockerfile | 6 + .../{{cookiecutter.repository}}/LICENSE.txt | 1 + .../{{cookiecutter.repository}}/README.md | 23 + .../confs/inference.yaml | 0 .../confs/training.yaml | 0 .../confs/tuning.yaml | 0 .../docker-compose.yml | 10 + .../{{cookiecutter.repository}}/justfile | 38 + .../pyproject.toml | 101 +++ .../src/{{cookiecutter.package}}/__init__.py | 1 + .../src/{{cookiecutter.package}}/__main__.py | 10 + .../src/{{cookiecutter.package}}/scripts.py | 14 + .../tasks/check.just | 33 + .../tasks/clean.just | 71 ++ .../tasks/commit.just | 14 + .../tasks/doc.just | 13 + .../tasks/docker.just | 18 + .../tasks/format.just | 13 + .../tasks/install.just | 24 + .../tasks/package.just | 13 + .../tasks/project.just | 14 + .../tests/conftest.py | 1 + .../tests/test_scripts.py | 14 + ...{{cookiecutter.repository}}.code-workspace | 30 + 41 files changed, 1500 insertions(+), 76 deletions(-) create mode 100644 .github/workflows/tests.yml/tests.yml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 justfile delete mode 100644 poetry.toml create mode 100644 uv.lock create mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/.env.example create mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/.github/actions/setup/action.yml create mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/.github/rulesets/main.json create mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/.github/workflows/check.yml create mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/.github/workflows/publish.yml create mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/.gitignore create mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/.pre-commit-config.yaml create mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/.python-version create mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/Dockerfile create mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/LICENSE.txt create mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/README.md create mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/confs/inference.yaml create mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/confs/training.yaml create mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/confs/tuning.yaml create mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/docker-compose.yml create mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/justfile create mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/pyproject.toml create mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/src/{{cookiecutter.package}}/__init__.py create mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/src/{{cookiecutter.package}}/__main__.py create mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/src/{{cookiecutter.package}}/scripts.py create mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/check.just create mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/clean.just create mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/commit.just create mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/doc.just create mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/docker.just create mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/format.just create mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/install.just create mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/package.just create mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/project.just create mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/tests/conftest.py create mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/tests/test_scripts.py create mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/{{cookiecutter.repository}}.code-workspace diff --git a/.github/workflows/tests.yml/tests.yml b/.github/workflows/tests.yml/tests.yml new file mode 100644 index 0000000..ad1c6f2 --- /dev/null +++ b/.github/workflows/tests.yml/tests.yml @@ -0,0 +1,27 @@ +name: Tests +on: + pull_request: + branches: + - '*' +permissions: + contents: read + id-token: write +concurrency: + cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.ref }} +jobs: + cookiecutter: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version-file: .python-version + - name: Test + run: uv run just test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed29e00 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# https://git-scm.com/docs/gitignore + +# IDE +/.idea/ +/.vscode/ + +# Python +.venv/ +*.py[cod] +__pycache__/ +.mypy_cache/ +.ruff_cache/ +.pytest_cache/ +.ipynb_checkpoints/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..efc69e0 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,15 @@ +# https://pre-commit.com +# https://pre-commit.com/hooks.html + +default_language_version: + python: python3.13 +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: 'v5.0.0' + hooks: + - id: check-added-large-files + - id: check-case-conflict + - id: check-merge-conflict + - id: end-of-file-fixer + - id: mixed-line-ending + - id: trailing-whitespace diff --git a/README.md b/README.md index 838df9f..59ccecd 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,44 @@ -# Irving's Python Project Cookiecutter Template +# Cookiecutter - MLOps Package -A python project template to simplify project setup. Adapted from https://github.com/fmind/cookiecutter-mlops-package +[![Release](https://img.shields.io/github/v/release/fmind/cookiecutter-mlops-package)](https://github.com/fmind/cookiecutter-mlops-package/releases) +[![License](https://img.shields.io/github/license/fmind/cookiecutter-mlops-package)](https://github.com/fmind/cookiecutter-mlops-package/blob/main/LICENSE.txt) -This template copy omits the MLFlow functionality. Use the linked mlops-package template if this is desired +**Jumpstart your MLOps projects with this comprehensive [Cookiecutter template](https://cookiecutter.readthedocs.io/)**. -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. +The template provides a robust foundation for building, testing, packaging, and deploying Python packages and Docker Images tailored for MLOps tasks. -**Source resources**: +**Related 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. +- **[LLMOps Coding Package (Example)](https://github.com/callmesora/llmops-python-package/)**: Example with best practices and tools to support your LLMOps projects. ## Philosophy -This [Cookiecutter](https://cookiecutter.readthedocs.io/) is designed to be a common ground for diverse python environments. Whether you're working with [Kubernetes](https://www.kubeflow.org/), [Vertex AI](https://cloud.google.com/vertex-ai), [Databricks](https://www.databricks.com/), [Azure ML](https://azure.microsoft.com/en-us/products/machine-learning), or [AWS SageMaker](https://aws.amazon.com/sagemaker/), the core principles of using Python packages and Docker images remain consistent. +This [Cookiecutter](https://cookiecutter.readthedocs.io/) is designed to be a common ground for diverse MLOps environments. Whether you're working with [Kubernetes](https://www.kubeflow.org/), [Vertex AI](https://cloud.google.com/vertex-ai), [Databricks](https://www.databricks.com/), [Azure ML](https://azure.microsoft.com/en-us/products/machine-learning), or [AWS SageMaker](https://aws.amazon.com/sagemaker/), the core principles of using Python packages and Docker images remain consistent. -This template equips you with the essentials for creating, testing, and packaging your code, providing a solid base for integration into your chosen platform. +This template equips you with the essentials for creating, testing, and packaging your AI/ML code, providing a solid base for [integration into your chosen MLOps platform](https://fmind.medium.com/stop-building-rigid-ai-ml-pipelines-embrace-reusable-components-for-flexible-mlops-6e165d837110). To fully leverage its capabilities within a specific environment, you might need to combine it with external tools like [Airflow](https://airflow.apache.org/) for orchestration or platform-specific SDKs for deployment. 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. ## Key Features -(This section was copied into the created project's README so tool info is available to users.) - * **Streamlined Project Structure:** A well-defined directory layout for source code, tests, documentation, tasks, and Docker configurations. -* **Poetry Integration:** Effortless dependency management and packaging with [Poetry](https://python-poetry.org/). +* **Uv Integration:** Effortless dependency management and packaging with [uv](https://docs.astral.sh/uv/). * **Automated Testing and Checks:** Pre-configured workflows using [Pytest](https://docs.pytest.org/), [Ruff](https://docs.astral.sh/ruff/), [Mypy](https://mypy.readthedocs.io/), [Bandit](https://bandit.readthedocs.io/), and [Coverage](https://coverage.readthedocs.io/) to ensure code quality, style, security, and type safety. * **Pre-commit Hooks:** Automatic code formatting and linting with [Ruff](https://docs.astral.sh/ruff/) and other pre-commit hooks to maintain consistency. * **Dockerized Deployment:** Dockerfile and docker-compose.yml for building and running the package within a containerized environment ([Docker](https://www.docker.com/)). -* **Invoke Task Automation:** [PyInvoke](https://www.pyinvoke.org/) tasks to simplify development workflows such as cleaning, installing, formatting, checking, building, documenting and running the project. +* **Invoke Task Automation:** [PyInvoke](https://www.pyinvoke.org/) tasks to simplify development workflows such as cleaning, installing, formatting, checking, building, documenting, and running MLflow projects. * **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. + ## Quick Start 1. **Generate your project:** ```bash pip install cookiecutter -cookiecutter gh:irod973/python-project-template +cookiecutter gh:fmind/cookiecutter-mlops-package ``` You'll be prompted for the following variables: @@ -46,52 +47,74 @@ You'll be prompted for the following variables: - `name`: The name of your project. - `repository`: The name of your GitHub repository. - `package`: The name of your Python package. -- `license`: The license for your project (Note: use "NA" until we define a standard licence or omit entirely) +- `license`: The license for your project. - `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). +- `python_version`: The Python version to use (e.g., 3.13). +- `mlflow_version`: The MLflow version to use (e.g., 2.20.3). 2. **Initialize a git repository:** ```bash cd {{ cookiecutter.repository }} git init -# Should also create remote repo, e.g. via Github web console -# Then make first push e.g. -# git commit -m "Initial commit" -# git remote add origin https://github.com/irod973/{{myproject}}.git -# git push -u origin main ``` -3. **Explore the generated project:** +3. **Enable GitHub Pages Workflow:** + +- Navigate to your repository settings on GitHub: "Settings" -> "Actions" -> "General." +- Under "Workflow permissions," ensure "Read and write permissions" is selected. + - This allows the workflow to automatically publish your documentation. + +4. **Explore the generated project:** - `src/{{cookiecutter.package}}`: Your Python package source code. - `tests/`: Unit tests for your package. - `tasks/`: PyInvoke tasks for automation. - `Dockerfile`: Configuration for building your Docker image. -- `docker-compose.yml`: Orchestration file for running your project. +- `docker-compose.yml`: Orchestration file for running MLflow and your project. -4. **Start developing!** +5. **Start developing!** Use the provided Invoke tasks to manage your development workflow: -- `invoke installs`: Install dependencies and pre-commit hooks. -- `invoke formats`: Format your code. -- `invoke checks`: Run code quality, type, security, and test checks. -- `invoke docs`: Generate API documentation. -- `invoke packages`: Build your Python package. -- `invoke containers`: Build and run your Docker image. +- `uv run just check`: Run code quality, type, security, and test checks. +- `uv run just clean`: Clean up generated files. +- `uv run just commit`: Commit changes to your repository. +- `uv run just doc`: Generate API documentation. +- `uv run just docker`: Build and run your Docker image. +- `uv run just format`: Format your code with Ruff. +- `uv run just install`: Install dependencies, pre-commit hooks, and GitHub rulesets. +- `uv run just mlflow`: Start an Mlflow server. +- `uv run just package`: Build your Python package. +- `uv run just project`: Run the project in the CLI. ## Example Usage +### Running the Project Script + +After installing dependencies and setting up MLflow: + +```bash +uv run just project +``` + +This will execute the job with the configuration file in your `confs` folder. + ### Building and Running Your Docker Image ```bash -invoke containers +invoke docker ``` -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. +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. The `CMD` in the Dockerfile executes your package with the `--help` flag. + +## Contributions + +We welcome [contributions](https://github.com/fmind/cookiecutter-mlops-package/blob/main/CODE_OF_CONDUCT.md) to enhance this [Cookiecutter template](https://cookiecutter.readthedocs.io/) for generating MLOps projects. + +Feel free to open [issues](https://github.com/fmind/cookiecutter-mlops-package/issues) or [pull requests](https://github.com/fmind/cookiecutter-mlops-package/pulls) for any improvements, bug fixes, or feature requests. ## 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. +This project 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/justfile b/justfile new file mode 100644 index 0000000..8b93c66 --- /dev/null +++ b/justfile @@ -0,0 +1,33 @@ +# https://just.systems/man/en/ + +# REQUIRES + +find := require("find") +rm := require("rm") +uv := require("uv") + +# DEFAULTS + +# display help information +default: + @just --list + +# TASKS + +# clean the project +clean: + rm -rf .venv/ + rm -rf .mypy_cache/ + rm -rf .pytest_cache/ + +# install the project +install: + uv sync --all-groups + +# setup the project hooks +hooks: + uv run pre-commit install + +# run the project unit tests +test: + uv run pytest tests/ diff --git a/poetry.toml b/poetry.toml deleted file mode 100644 index 7783e1b..0000000 --- a/poetry.toml +++ /dev/null @@ -1,5 +0,0 @@ -# https://python-poetry.org/docs/configuration/ - -[virtualenvs] -in-project = true -prefer-active-python = true diff --git a/pyproject.toml b/pyproject.toml index 4df96b1..1083e03 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,45 +1,41 @@ -# https://python-poetry.org/docs/pyproject/ +# https://docs.astral.sh/uv/reference/settings/ +# https://packaging.python.org/en/latest/guides/writing-pyproject-toml/ -# PACKAGE +# PROJECT -[tool.poetry] +[project] name = "cookiecutter-mlops-package" -version = "1.0.0" -description = "Build and deploy Python packages and Docker images for MLOps tasks." -repository = "https://github.com/fmind/cookiecutter-mlops-package" -authors = ["Médéric HURIER "] +version = "4.1.0" +description = "Build and deploy Python packages and Docker images for MLOps projects." +authors = [{ name = "Médéric HURIER", email = "github@fmind.dev" }] readme = "README.md" -license = "MIT" -package-mode = false +requires-python = ">=3.13" +license = { file = "LICENSE.txt" } # DEPENDENCIES -[tool.poetry.dependencies] -python = "^3.12" +[dependency-groups] +dev = [ + "commitizen>=4.1.0", + "pre-commit>=4.0.1", + "pytest-cookies>=0.7.0", + "pytest-shell-utilities>=1.9.7", + "pytest>=8.3.4", + "rust-just>=1.39.0", +] -[tool.poetry.group.dev.dependencies] -commitizen = "^3.28.0" -invoke = "^2.2.0" -pre-commit = "^3.7.1" -pytest = "^8.3.2" -pytest-cookies = "^0.7.0" -pytest-shell-utilities = "^1.9.0" +# TOOLS -# CONFIGURATIONS +[tool.uv] +package = false [tool.commitizen] name = "cz_conventional_commits" tag_format = "v$version" version_scheme = "pep440" -version_provider = "poetry" +version_provider = "pep621" update_changelog_on_bump = true [tool.pytest.ini_options] log_cli = true log_cli_level = "INFO" - -# SYSTEMS - -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" diff --git a/tests/test_cookiecutter.py b/tests/test_cookiecutter.py index 50df86d..4646f81 100644 --- a/tests/test_cookiecutter.py +++ b/tests/test_cookiecutter.py @@ -9,14 +9,15 @@ COMMANDS = [ "git init", - "invoke cleans.reset", - "invoke installs", - "invoke formats", - "invoke checks", - "invoke docs", - "invoke projects", - "invoke packages", - "invoke containers", + "uv run just clean", + "uv run just install", + "uv run just format", + "uv run just check", + "uv run just doc", + "uv run just project", + "uv run just package", + "uv run just docker", + "uv run just mlflow-doctor", ] # %% TESTS @@ -25,14 +26,14 @@ def test_project_generation(cookies: Cookies) -> None: """Test the generation of the project.""" # given - context = { - "user": "test", + context = { + "user": "tester", "name": "MLOps 123", "license": "apache-2", "version": "1.0.0", - "description": "DONE", - "python_version": "3.12", - "mlflow_version": "2.14.3", + "description": "A test project.", + "python_version": "3.13", + "mlflow_version": "2.20.3", } repository = context['name'].lower().replace(' ', '-') package = repository.replace('-', '_') @@ -47,8 +48,8 @@ def test_project_generation(cookies: Cookies) -> None: assert result.context == { "user": context['user'], "name": context['name'], - "repository": repository, "package": package, + "repository": repository, "license": context['license'], "version": context['version'], "description": context['description'], diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..55e726b --- /dev/null +++ b/uv.lock @@ -0,0 +1,655 @@ +version = 1 +requires-python = ">=3.13" + +[[package]] +name = "argcomplete" +version = "3.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/be/6c23d80cb966fb8f83fb1ebfb988351ae6b0554d0c3a613ee4531c026597/argcomplete-3.5.3.tar.gz", hash = "sha256:c12bf50eded8aebb298c7b7da7a5ff3ee24dffd9f5281867dfe1424b58c55392", size = 72999 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/08/2a4db06ec3d203124c967fc89295e85a202e5cbbcdc08fd6a64b65217d1e/argcomplete-3.5.3-py3-none-any.whl", hash = "sha256:2ab2c4a215c59fd6caaff41a869480a23e8f6a5f910b266c1808037f4e375b61", size = 43569 }, +] + +[[package]] +name = "arrow" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "types-python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/00/0f6e8fcdb23ea632c866620cc872729ff43ed91d284c866b515c6342b173/arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85", size = 131960 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/ed/e97229a566617f2ae958a6b13e7cc0f585470eac730a73e9e82c32a3cdd2/arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80", size = 66419 }, +] + +[[package]] +name = "attrs" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/7c/fdf464bcc51d23881d110abd74b512a42b3d5d376a55a831b44c603ae17f/attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e", size = 810562 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/30/d4986a882011f9df997a55e6becd864812ccfcd821d64aac8570ee39f719/attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a", size = 63152 }, +] + +[[package]] +name = "binaryornot" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "chardet" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/fe/7ebfec74d49f97fc55cd38240c7a7d08134002b1e14be8c3897c0dd5e49b/binaryornot-0.4.4.tar.gz", hash = "sha256:359501dfc9d40632edc9fac890e19542db1a287bbcfa58175b66658392018061", size = 371054 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/7e/f7b6f453e6481d1e233540262ccbfcf89adcd43606f44a028d7f5fae5eb2/binaryornot-0.4.4-py2.py3-none-any.whl", hash = "sha256:b8b71173c917bddcd2c16070412e369c3ed7f0528926f70cac18a6c97fd563e4", size = 9006 }, +] + +[[package]] +name = "certifi" +version = "2025.1.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, +] + +[[package]] +name = "chardet" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, + { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, + { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, + { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, + { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, + { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, + { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, + { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, + { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, + { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, + { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, + { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, + { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, + { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "commitizen" +version = "4.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argcomplete" }, + { name = "charset-normalizer" }, + { name = "colorama" }, + { name = "decli" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "questionary" }, + { name = "termcolor" }, + { name = "tomlkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/fd/cd449bed87a26ecb61c950410e2d94e97ac31bf1f3ec69cc718b215384ce/commitizen-4.4.1.tar.gz", hash = "sha256:626d9f545fb9b2db42305e16ef35d6348a35081a80527bad863a05a7ba0bec21", size = 52345 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/0a/03fd9b5b7d9de11ff5e7c5a73b66c17775d00c9eea07b585d4fd7bf45a31/commitizen-4.4.1-py3-none-any.whl", hash = "sha256:98dbee784cc74fd1b24915e265e99ce81caccd64e54cb42b347a37d1dd2a4cd8", size = 74882 }, +] + +[[package]] +name = "cookiecutter" +version = "2.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "arrow" }, + { name = "binaryornot" }, + { name = "click" }, + { name = "jinja2" }, + { name = "python-slugify" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/17/9f2cd228eb949a91915acd38d3eecdc9d8893dde353b603f0db7e9f6be55/cookiecutter-2.6.0.tar.gz", hash = "sha256:db21f8169ea4f4fdc2408d48ca44859349de2647fbe494a9d6c3edfc0542c21c", size = 158767 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/d9/0137658a353168ffa9d0fc14b812d3834772040858ddd1cb6eeaf09f7a44/cookiecutter-2.6.0-py3-none-any.whl", hash = "sha256:a54a8e37995e4ed963b3e82831072d1ad4b005af736bb17b99c2cbd9d41b6e2d", size = 39177 }, +] + +[[package]] +name = "cookiecutter-mlops-package" +version = "4.1.0" +source = { virtual = "." } + +[package.dev-dependencies] +dev = [ + { name = "commitizen" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-cookies" }, + { name = "pytest-shell-utilities" }, + { name = "rust-just" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "commitizen", specifier = ">=4.1.0" }, + { name = "pre-commit", specifier = ">=4.0.1" }, + { name = "pytest", specifier = ">=8.3.4" }, + { name = "pytest-cookies", specifier = ">=0.7.0" }, + { name = "pytest-shell-utilities", specifier = ">=1.9.7" }, + { name = "rust-just", specifier = ">=1.39.0" }, +] + +[[package]] +name = "decli" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/a0/a4658f93ecb589f479037b164dc13c68d108b50bf6594e54c820749f97ac/decli-0.6.2.tar.gz", hash = "sha256:36f71eb55fd0093895efb4f416ec32b7f6e00147dda448e3365cf73ceab42d6f", size = 7424 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/70/3ea48dc9e958d7d66c44c9944809181f1ca79aaef25703c023b5092d34ff/decli-0.6.2-py3-none-any.whl", hash = "sha256:2fc84106ce9a8f523ed501ca543bdb7e416c064917c12a59ebdc7f311a97b7ed", size = 7854 }, +] + +[[package]] +name = "distlib" +version = "0.3.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 }, +] + +[[package]] +name = "filelock" +version = "3.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/9c/0b15fb47b464e1b663b1acd1253a062aa5feecb07d4e597daea542ebd2b5/filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e", size = 18027 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/ec/00d68c4ddfedfe64159999e5f8a98fb8442729a63e2077eb9dcd89623d27/filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338", size = 16164 }, +] + +[[package]] +name = "identify" +version = "2.6.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/fa/5eb460539e6f5252a7c5a931b53426e49258cde17e3d50685031c300a8fd/identify-2.6.8.tar.gz", hash = "sha256:61491417ea2c0c5c670484fd8abbb34de34cdae1e5f39a73ee65e48e4bb663fc", size = 99249 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/8c/4bfcab2d8286473b8d83ea742716f4b79290172e75f91142bc1534b05b9a/identify-2.6.8-py2.py3-none-any.whl", hash = "sha256:83657f0f766a3c8d0eaea16d4ef42494b39b34629a4b3192a9d020d349b3e255", size = 99109 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "pre-commit" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/13/b62d075317d8686071eb843f0bb1f195eb332f48869d3c31a4c6f1e063ac/pre_commit-4.1.0.tar.gz", hash = "sha256:ae3f018575a588e30dfddfab9a05448bfbd6b73d78709617b5a2b853549716d4", size = 193330 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/b3/df14c580d82b9627d173ceea305ba898dca135feb360b6d84019d0803d3b/pre_commit-4.1.0-py2.py3-none-any.whl", hash = "sha256:d29e7cb346295bcc1cc75fc3e92e343495e3ea0196c9ec6ba53f49f10ab6ae7b", size = 220560 }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.50" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/e1/bd15cb8ffdcfeeb2bdc215de3c3cffca11408d829e4b8416dcfe71ba8854/prompt_toolkit-3.0.50.tar.gz", hash = "sha256:544748f3860a2623ca5cd6d2795e7a14f3d0e1c3c9728359013f79877fc89bab", size = 429087 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/ea/d836f008d33151c7a1f62caf3d8dd782e4d15f6a43897f64480c2b8de2ad/prompt_toolkit-3.0.50-py3-none-any.whl", hash = "sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198", size = 387816 }, +] + +[[package]] +name = "psutil" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051 }, + { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535 }, + { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004 }, + { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986 }, + { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544 }, + { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053 }, + { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885 }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, +] + +[[package]] +name = "pytest-cookies" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cookiecutter" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/2e/11a3e1abb4bbf10e0af3f194ba4c55600de3fe52417ef3594c18d28ecdbe/pytest-cookies-0.7.0.tar.gz", hash = "sha256:1aaa6b4def8238d0d1709d3d773b423351bfb671c1e3438664d824e0859d6308", size = 8840 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/f7/438af2f3a6c58f81d22c126707ee5d079f653a76961f4fb7d995e526a9c4/pytest_cookies-0.7.0-py3-none-any.whl", hash = "sha256:52770f090d77b16428f6a24a208e6be76addb2e33458035714087b4de49389ea", size = 6386 }, +] + +[[package]] +name = "pytest-helpers-namespace" +version = "2021.12.29" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e8/61/7f1a476b375c1238d152c164f7ffb694c662da8247816947be84b5fe2e8a/pytest-helpers-namespace-2021.12.29.tar.gz", hash = "sha256:792038247e0021beb966a7ea6e3a70ff5fcfba77eb72c6ec8fd6287af871c35b", size = 53355 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/a7/34844a563c425962658f093b359636deefd19eab6783896e0d378dae88a1/pytest_helpers_namespace-2021.12.29-py3-none-any.whl", hash = "sha256:d5c0262642998437a73d85cb6ae0db57d574facc551c4a4695e92ec50469eb98", size = 10501 }, +] + +[[package]] +name = "pytest-shell-utilities" +version = "1.9.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "psutil" }, + { name = "pytest" }, + { name = "pytest-helpers-namespace" }, + { name = "pytest-skip-markers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/de/6b4060bef2b650f87e01c3b5f2d0ee41f0b49f4bd903253941959fac08d2/pytest_shell_utilities-1.9.7.tar.gz", hash = "sha256:84a2283333925b43780509ccfd3d5be0d7db243b7414b89f1b63954bbcc9d3ec", size = 81231 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/3d/09b2335b10f6a292a8c99907634655acbe0054850934574c904cab172a86/pytest_shell_utilities-1.9.7-py3-none-any.whl", hash = "sha256:e9975718160d9d35c715e0b349ddb78afb43c810ac5c98a6d0714462c0456eb6", size = 28812 }, +] + +[[package]] +name = "pytest-skip-markers" +version = "1.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "distro" }, + { name = "pytest" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/8b/15c961e983eb3f71d277679cfe93c1c0cd698507c461f90208d80252534f/pytest_skip_markers-1.5.2.tar.gz", hash = "sha256:76d61be67f1fd639cca37830a4756b09cea5521361616e66344f7ef00b4d06c0", size = 75342 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/c3/de4b07aa387e5e529d4c095e39cc665eb67588c321e489f632ace5015c5c/pytest_skip_markers-1.5.2-py3-none-any.whl", hash = "sha256:416be63bebacb3c17020d6a65b4306cb685161a2071ca5e7a7df529627f7e0f7", size = 20167 }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + +[[package]] +name = "python-slugify" +version = "8.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "text-unidecode" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/c7/5e1547c44e31da50a460df93af11a535ace568ef89d7a811069ead340c4a/python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856", size = 10921 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/62/02da182e544a51a5c3ccf4b03ab79df279f9c60c5e82d5e8bec7ca26ac11/python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", size = 10051 }, +] + +[[package]] +name = "pywin32" +version = "308" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/a4/aa562d8935e3df5e49c161b427a3a2efad2ed4e9cf81c3de636f1fdddfd0/pywin32-308-cp313-cp313-win32.whl", hash = "sha256:1c44539a37a5b7b21d02ab34e6a4d314e0788f1690d65b48e9b0b89f31abbbed", size = 5938579 }, + { url = "https://files.pythonhosted.org/packages/c7/50/b0efb8bb66210da67a53ab95fd7a98826a97ee21f1d22949863e6d588b22/pywin32-308-cp313-cp313-win_amd64.whl", hash = "sha256:fd380990e792eaf6827fcb7e187b2b4b1cede0585e3d0c9e84201ec27b9905e4", size = 6542056 }, + { url = "https://files.pythonhosted.org/packages/26/df/2b63e3e4f2df0224f8aaf6d131f54fe4e8c96400eb9df563e2aae2e1a1f9/pywin32-308-cp313-cp313-win_arm64.whl", hash = "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd", size = 7974986 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + +[[package]] +name = "questionary" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "prompt-toolkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/b8/d16eb579277f3de9e56e5ad25280fab52fc5774117fb70362e8c2e016559/questionary-2.1.0.tar.gz", hash = "sha256:6302cdd645b19667d8f6e6634774e9538bfcd1aad9be287e743d96cacaf95587", size = 26775 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/3f/11dd4cd4f39e05128bfd20138faea57bec56f9ffba6185d276e3107ba5b2/questionary-2.1.0-py3-none-any.whl", hash = "sha256:44174d237b68bc828e4878c763a9ad6790ee61990e0ae72927694ead57bab8ec", size = 36747 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "rich" +version = "13.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, +] + +[[package]] +name = "rust-just" +version = "1.39.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/e5/37254d353a23f567ac218ddd2c461337fafe3a43d44dcd6f0c8e72d096f4/rust_just-1.39.0.tar.gz", hash = "sha256:247d0b293924cc8089a73428c9c03a3c2c0627bb8f205addb976ded0681f0dac", size = 1395439 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/9e/c7151bfa84c1cd3ac4c11a60d1a2f074b7a244ae96959d968e8b9e8e3f29/rust_just-1.39.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3845ab10254c994ddebcf489b30c53a24c1d11585c9e0eeaf1cb0da422bee87f", size = 1781993 }, + { url = "https://files.pythonhosted.org/packages/e4/0f/c93ef08567835356033bee162c2e5e06b018811f5188b94ef3e74dafe7eb/rust_just-1.39.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fd5c12118a8d65266ccdacfbc24bab26f77d509caaf263095cb96611ea6ce7e8", size = 1652880 }, + { url = "https://files.pythonhosted.org/packages/f3/e7/22647f9c18537046940b3b4159377665912acccbacedd775917610c0390d/rust_just-1.39.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:428d07b1e798777c4e9a8c245539d72743be095558010f0a86823e1c442930f9", size = 1771788 }, + { url = "https://files.pythonhosted.org/packages/d5/1f/2a36afad5eeca6756fc8c87e08d102cdadf8e4b31c5f8bcb6f12c109b5b4/rust_just-1.39.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:135a7a65a8641b00a2fe7f3156a97ab7052e4830a922a71e67ca4e38ccd54cd2", size = 1778377 }, + { url = "https://files.pythonhosted.org/packages/97/5b/45effb44bbfab892774a239446920cfb9b3d999921fe7cff3d99b36394a7/rust_just-1.39.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94eb45e585fda019f7f9cbac198e10e31f81c704371887cbdec9b7a1ae2e0d29", size = 1896235 }, + { url = "https://files.pythonhosted.org/packages/b5/12/7e88a7e917c2e933846a32a2f537786829b48c827a6029e85943d5eeadc0/rust_just-1.39.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de4a8566ca1eb87b5ff2669a6dd9474b16977c5d712534a5a9c7a950271da2d0", size = 1954347 }, + { url = "https://files.pythonhosted.org/packages/9a/36/5dc917a2c0a0b66f0cc4bb0be4985090deefa33433dd869649a4ce2bc8f3/rust_just-1.39.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7ecd8fd862729c243498951caa54d778ff480c2524039280ff3ebb9a64299f", size = 2450909 }, + { url = "https://files.pythonhosted.org/packages/da/fc/b9224b354da8a89b38cd4145ec0e9e089635a45c1459231e349647cdad64/rust_just-1.39.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:576229024d2ef8fc696d5a049ecd0d8f3d9b920a32e76f65e95840d24d804101", size = 1895058 }, + { url = "https://files.pythonhosted.org/packages/0b/ef/985cb93c9dd36f9bc41f26b3ce6d420c69fb18166ac61783bced2c328f75/rust_just-1.39.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c1cd9240e2c1b352d7ccc6b89ce84fcc0352f15bb9660cdc6bc34802b36251b6", size = 1767816 }, + { url = "https://files.pythonhosted.org/packages/50/43/fc644746a7479fadbe6878ae9fa1da83860104d1e64a49de47306fa1a591/rust_just-1.39.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d6eff0461df7e36eba6e7f0addf16ef98563cf8cb483e4c8393be5456d6af5c6", size = 1791992 }, + { url = "https://files.pythonhosted.org/packages/32/d9/6924547c02afba3a49b14f53bed09b54d4292016b830f366d7ed1f80288a/rust_just-1.39.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0dfcc49a5fa126ba923b58e48921fd117e429660495577a854494c6ced3134c9", size = 1884357 }, + { url = "https://files.pythonhosted.org/packages/15/96/7e8d3795588c2e8e629f9f0d9cdd0cf1ba1bef5bf9c8b64aae33c78f1993/rust_just-1.39.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:826203ad02c869ad8621993a608adb01394ef9c9c9ca6aa7dd7875b1f272aa46", size = 1936706 }, + { url = "https://files.pythonhosted.org/packages/23/28/2492c4b7c8f0e526f52a263f20b951880387f992151bbc46c6c8914786a8/rust_just-1.39.0-py3-none-win32.whl", hash = "sha256:dcef0926b287449e853b878f6f34759a797d017cefb83afbcd74820d37259b78", size = 1577519 }, + { url = "https://files.pythonhosted.org/packages/6f/c2/1d576be1ca714df4a90255cfbcb4ec06d9aafbf7b14924c2881a8596a68e/rust_just-1.39.0-py3-none-win_amd64.whl", hash = "sha256:3139f3f76434a8ebbf35b213d149e647c4d9546312b438e262df7ec41e7ef7bc", size = 1705761 }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "termcolor" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/72/88311445fd44c455c7d553e61f95412cf89054308a1aa2434ab835075fc5/termcolor-2.5.0.tar.gz", hash = "sha256:998d8d27da6d48442e8e1f016119076b690d962507531df4890fcd2db2ef8a6f", size = 13057 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/be/df630c387a0a054815d60be6a97eb4e8f17385d5d6fe660e1c02750062b4/termcolor-2.5.0-py3-none-any.whl", hash = "sha256:37b17b5fc1e604945c2642c872a3764b5d547a48009871aea3edd3afa180afb8", size = 7755 }, +] + +[[package]] +name = "text-unidecode" +version = "1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/e2/e9a00f0ccb71718418230718b3d900e71a5d16e701a3dae079a21e9cd8f8/text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93", size = 76885 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/a5/c0b6468d3824fe3fde30dbb5e1f687b291608f9473681bbf7dabbf5a87d7/text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", size = 78154 }, +] + +[[package]] +name = "tomlkit" +version = "0.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/09/a439bec5888f00a54b8b9f05fa94d7f901d6735ef4e55dcec9bc37b5d8fa/tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79", size = 192885 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/b6/a447b5e4ec71e13871be01ba81f5dfc9d0af7e473da256ff46bc0e24026f/tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", size = 37955 }, +] + +[[package]] +name = "types-python-dateutil" +version = "2.9.0.20241206" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/60/47d92293d9bc521cd2301e423a358abfac0ad409b3a1606d8fbae1321961/types_python_dateutil-2.9.0.20241206.tar.gz", hash = "sha256:18f493414c26ffba692a72369fea7a154c502646301ebfe3d56a04b3767284cb", size = 13802 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/b3/ca41df24db5eb99b00d97f89d7674a90cb6b3134c52fb8121b6d8d30f15c/types_python_dateutil-2.9.0.20241206-py3-none-any.whl", hash = "sha256:e248a4bc70a486d3e3ec84d0dc30eec3a5f979d6e7ee4123ae043eedbb987f53", size = 14384 }, +] + +[[package]] +name = "urllib3" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, +] + +[[package]] +name = "virtualenv" +version = "20.29.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/88/dacc875dd54a8acadb4bcbfd4e3e86df8be75527116c91d8f9784f5e9cab/virtualenv-20.29.2.tar.gz", hash = "sha256:fdaabebf6d03b5ba83ae0a02cfe96f48a716f4fae556461d180825866f75b728", size = 4320272 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/fa/849483d56773ae29740ae70043ad88e068f98a6401aa819b5d6bee604683/virtualenv-20.29.2-py3-none-any.whl", hash = "sha256:febddfc3d1ea571bdb1dc0f98d7b45d24def7428214d4fb73cc486c9568cce6a", size = 4301478 }, +] + +[[package]] +name = "wcwidth" +version = "0.2.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, +] diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.env.example b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.env.example new file mode 100644 index 0000000..e69de29 diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.github/actions/setup/action.yml b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.github/actions/setup/action.yml new file mode 100644 index 0000000..cd4f2ef --- /dev/null +++ b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.github/actions/setup/action.yml @@ -0,0 +1,13 @@ +name: Setup +description: Setup for project workflows +runs: + using: composite + steps: + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version-file: .python-version diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.github/rulesets/main.json b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.github/rulesets/main.json new file mode 100644 index 0000000..98d3035 --- /dev/null +++ b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.github/rulesets/main.json @@ -0,0 +1,58 @@ +{ + "name": "main", + "target": "branch", + "enforcement": "active", + "conditions": { + "ref_name": { + "exclude": [], + "include": [ + "~DEFAULT_BRANCH" + ] + } + }, + "rules": [ + { + "type": "deletion" + }, + { + "type": "required_linear_history" + }, + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 0, + "dismiss_stale_reviews_on_push": true, + "require_code_owner_review": false, + "require_last_push_approval": false, + "required_review_thread_resolution": false, + "allowed_merge_methods": [ + "squash", + "rebase" + ] + } + }, + { + "type": "required_status_checks", + "parameters": { + "strict_required_status_checks_policy": true, + "do_not_enforce_on_create": false, + "required_status_checks": [ + { + "context": "checks", + "integration_id": 15368 + } + ] + } + }, + { + "type": "non_fast_forward" + } + ], + "bypass_actors": [ + { + "actor_id": 5, + "actor_type": "RepositoryRole", + "bypass_mode": "always" + } + ] +} diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.github/workflows/check.yml b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.github/workflows/check.yml new file mode 100644 index 0000000..53508f1 --- /dev/null +++ b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.github/workflows/check.yml @@ -0,0 +1,20 @@ +name: Check +on: + pull_request: + branches: + - '*' +concurrency: + cancel-in-progress: true + group: {% raw %}${{ github.workflow }}-${{ github.ref }}{% endraw %} +jobs: + checks: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup + - run: uv sync --group=check + - run: uv run just check-code + - run: uv run just check-type + - run: uv run just check-format + - run: uv run just check-security + - run: uv run just check-coverage diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.github/workflows/publish.yml b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.github/workflows/publish.yml new file mode 100644 index 0000000..c3a4814 --- /dev/null +++ b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.github/workflows/publish.yml @@ -0,0 +1,47 @@ +name: Publish +on: + release: + types: + - edited + - published +env: + DOCKER_IMAGE: ghcr.io/fmind/mlops-python-package +concurrency: + cancel-in-progress: true + group: publish-workflow +jobs: + pages: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup + - run: uv sync --group=doc + - run: uv run just doc + - uses: JamesIves/github-pages-deploy-action@v4 + with: + folder: docs/ + branch: gh-pages + packages: + permissions: + packages: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup + - run: uv sync --only-dev + - run: uv run just package + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: {% raw %}${{ github.actor }}{% endraw %} + password: {% raw %}${{ secrets.GITHUB_TOKEN }}{% endraw %} + - uses: docker/setup-buildx-action@v3 + - uses: docker/build-push-action@v6 + with: + push: true + context: . + cache-to: type=gha + cache-from: type=gha + tags: | + {% raw %}${{ env.DOCKER_IMAGE }}:latest{% endraw %} + {% raw %}${{ env.DOCKER_IMAGE }}:${{ github.ref_name }}{% endraw %} diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.gitignore b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.gitignore new file mode 100644 index 0000000..0abc593 --- /dev/null +++ b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.gitignore @@ -0,0 +1,31 @@ +# https://git-scm.com/docs/gitignore + +# Build +/dist/ +/build/ + +# Cache +.cache/ +.coverage* +.mypy_cache/ +.ruff_cache/ +.pytest_cache/ + +# Editor +/.idea/ +/.vscode/ +.ipynb_checkpoints/ + +# Environs +.env +/.venv/ + +# Project +/docs/* +/mlruns/* +/outputs/* +!**/.gitkeep + +# Python +*.py[cod] +__pycache__/ diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.pre-commit-config.yaml b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.pre-commit-config.yaml new file mode 100644 index 0000000..9eebedd --- /dev/null +++ b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.pre-commit-config.yaml @@ -0,0 +1,33 @@ +# https://pre-commit.com +# https://pre-commit.com/hooks.html + +default_language_version: + python: python3.13 +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: 'v5.0.0' + hooks: + - id: check-added-large-files + - id: check-case-conflict + - id: check-merge-conflict + - id: check-toml + - id: check-yaml + - id: debug-statements + - id: end-of-file-fixer + - id: mixed-line-ending + - id: trailing-whitespace + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: 'v0.9.9' + hooks: + - id: ruff + - id: ruff-format + - repo: https://github.com/PyCQA/bandit + rev: '1.8.3' + hooks: + - id: bandit + # - repo: https://github.com/commitizen-tools/commitizen + # rev: 'v4.4.1' + # hooks: + # - id: commitizen + # - id: commitizen-branch + # stages: [pre-push] diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.python-version b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.python-version new file mode 100644 index 0000000..a2b2ca5 --- /dev/null +++ b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.python-version @@ -0,0 +1 @@ +{{cookiecutter.python_version}} diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/Dockerfile b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/Dockerfile new file mode 100644 index 0000000..cd59030 --- /dev/null +++ b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/Dockerfile @@ -0,0 +1,6 @@ +# https://docs.docker.com/engine/reference/builder/ + +FROM ghcr.io/astral-sh/uv:python{{cookiecutter.python_version}}-bookworm +COPY dist/*.whl . +RUN uv pip install --system *.whl +CMD ["{{cookiecutter.repository}}", "--help"] diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/LICENSE.txt b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/LICENSE.txt new file mode 100644 index 0000000..4ea2668 --- /dev/null +++ b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/LICENSE.txt @@ -0,0 +1 @@ +https://opensource.org/license/{{cookiecutter.license}} diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/README.md b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/README.md new file mode 100644 index 0000000..e07f42d --- /dev/null +++ b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/README.md @@ -0,0 +1,23 @@ +# {{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) +[![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) +[![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) + +{{cookiecutter.description}}. + +# Installation + +Use the package manager [uv](https://docs.astral.sh/uv/): + +```bash +uv sync +``` + +# Usage + +```bash +uv run {{cookiecutter.repository}} +``` diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/confs/inference.yaml b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/confs/inference.yaml new file mode 100644 index 0000000..e69de29 diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/confs/training.yaml b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/confs/training.yaml new file mode 100644 index 0000000..e69de29 diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/confs/tuning.yaml b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/confs/tuning.yaml new file mode 100644 index 0000000..e69de29 diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/docker-compose.yml b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/docker-compose.yml new file mode 100644 index 0000000..730552c --- /dev/null +++ b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/docker-compose.yml @@ -0,0 +1,10 @@ +# https://docs.docker.com/compose/compose-file/ + +services: + mlflow: + image: ghcr.io/mlflow/mlflow:v{{cookiecutter.mlflow_version}} + ports: + - 5000:5000 + environment: + - MLFLOW_HOST=0.0.0.0 + command: mlflow server diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/justfile b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/justfile new file mode 100644 index 0000000..0fdd396 --- /dev/null +++ b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/justfile @@ -0,0 +1,38 @@ +# https://just.systems/man/en/ + +# REQUIRES + +docker := require("docker") +find := require("find") +rm := require("rm") +uv := require("uv") + +# SETTINGS + +set dotenv-load := true + +# VARIABLES + +PACKAGE := "{{cookiecutter.package}}" +REPOSITORY := "{{cookiecutter.repository}}" +SOURCES := "src" +TESTS := "tests" + +# DEFAULTS + +# display help information +default: + @just --list + +# IMPORTS + +import 'tasks/check.just' +import 'tasks/clean.just' +import 'tasks/commit.just' +import 'tasks/doc.just' +import 'tasks/docker.just' +import 'tasks/format.just' +import 'tasks/install.just' +import 'tasks/mlflow.just' +import 'tasks/package.just' +import 'tasks/project.just' diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/pyproject.toml b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/pyproject.toml new file mode 100644 index 0000000..0e7b3d7 --- /dev/null +++ b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/pyproject.toml @@ -0,0 +1,101 @@ +# https://docs.astral.sh/uv/reference/settings/ +# https://packaging.python.org/en/latest/guides/writing-pyproject-toml/ + +# PROJECT + +[project] +name = "{{cookiecutter.repository}}" +version = "{{cookiecutter.version}}" +description = "{{cookiecutter.description}}" +authors = [{ name = "{{cookiecutter.user}}" }] +readme = "README.md" +license = { file = "LICENSE.txt" } +keywords = ["mlops", "python", "package"] +requires-python = ">={{cookiecutter.python_version}}" +dependencies = [ + "hatchling>=1.27.0", + "mlflow>={{cookiecutter.mlflow_version}}", +] + +# LINKS + +[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" +"Bug Tracker" = "https://github.com/{{cookiecutter.user}}/{{cookiecutter.repository}}/issues" +Changelog = "https://github.com/{{cookiecutter.user}}/{{cookiecutter.repository}}/blob/main/CHANGELOG.md" + +# SCRIPTS + +[project.scripts] +{{cookiecutter.repository}} = "{{cookiecutter.package}}.scripts:main" + +# DEPENDENCIES + +[dependency-groups] +check = [ + "bandit>=1.8.3", + "coverage>=7.6.12", + "mypy>=1.15.0", + "pytest>=8.3.5", + "pytest-cov>=6.0.0", + "pytest-mock>=3.14.0", + "pytest-xdist>=3.6.1", + "ruff>=0.9.9", +] +commit = ["commitizen>=4.4.1", "pre-commit>=4.1.0"] +dev = ["rust-just>=1.39.0"] +doc = ["pdoc>=15.0.1"] +notebook = ["ipykernel>=6.29.5", "nbformat>=5.10.4"] + +# TOOLS + +[tool.uv] +default-groups = ["check", "commit", "dev", "doc", "notebook"] + +[tool.bandit] +targets = ["src"] + +[tool.commitizen] +name = "cz_conventional_commits" +tag_format = "v$version" +version_scheme = "pep440" +version_provider = "pep621" +update_changelog_on_bump = true + +[tool.coverage.run] +branch = true +source = ["src"] +omit = ["__main__.py"] + +[tool.mypy] +pretty = true +python_version = "3.13" +check_untyped_defs = true +ignore_missing_imports = true + +[tool.pytest.ini_options] +addopts = "--verbosity=2" +pythonpath = ["src"] + +[tool.ruff] +fix = true +indent-width = 4 +line-length = 100 +target-version = "py313" + +[tool.ruff.format] +docstring-code-format = true + +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.ruff.lint.per-file-ignores] +"tests/*.py" = ["D100", "D103"] + +# SYSTEMS + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/src/{{cookiecutter.package}}/__init__.py b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/src/{{cookiecutter.package}}/__init__.py new file mode 100644 index 0000000..537a370 --- /dev/null +++ b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/src/{{cookiecutter.package}}/__init__.py @@ -0,0 +1 @@ +"""{{cookiecutter.description}}.""" diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/src/{{cookiecutter.package}}/__main__.py b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/src/{{cookiecutter.package}}/__main__.py new file mode 100644 index 0000000..4691a30 --- /dev/null +++ b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/src/{{cookiecutter.package}}/__main__.py @@ -0,0 +1,10 @@ +"""Entry point of the package.""" + +# %% IMPORTS + +from {{cookiecutter.package}} import scripts + +# %% MAIN + +if __name__ == "__main__": + scripts.main() diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/src/{{cookiecutter.package}}/scripts.py b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/src/{{cookiecutter.package}}/scripts.py new file mode 100644 index 0000000..eb336db --- /dev/null +++ b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/src/{{cookiecutter.package}}/scripts.py @@ -0,0 +1,14 @@ +"""Scripts of the project.""" + +# %% IMPORTS + +import sys + +# %% FUNCTIONS + + +def main(argv: list[str] | None = None) -> int: + """Run the main script function.""" + args = argv or sys.argv[1:] + print("Args:", args) + return 0 diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/check.just b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/check.just new file mode 100644 index 0000000..f7b84ba --- /dev/null +++ b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/check.just @@ -0,0 +1,33 @@ +# run check tasks +[group('check')] +check: check-code check-type check-format check-security check-coverage + +# check code quality +[group('check')] +check-code: + {% raw %}uv run ruff check {{SOURCES}} {{TESTS}}{% endraw %} + +# check code coverage +[group('check')] +check-coverage numprocesses="auto" cov_fail_under="80": + {% raw %}uv run pytest --numprocesses={{numprocesses}} --cov={{SOURCES}} --cov-fail-under={{cov_fail_under}} {{TESTS}}{% endraw %} + +# check code format +[group('check')] +check-format: + {% raw %}uv run ruff format --check {{SOURCES}} {{TESTS}}{% endraw %} + +# check code security +[group('check')] +check-security: + {% raw %}uv run bandit --recursive --configfile=pyproject.toml {{SOURCES}}{% endraw %} + +# check unit tests +[group('check')] +check-test numprocesses="auto": + {% raw %}uv run pytest --numprocesses={{numprocesses}} {{TESTS}}{% endraw %} + +# check code typing +[group('check')] +check-type: + {% raw %}uv run mypy {{SOURCES}} {{TESTS}}{% endraw %} diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/clean.just b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/clean.just new file mode 100644 index 0000000..21061a5 --- /dev/null +++ b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/clean.just @@ -0,0 +1,71 @@ +# run clean tasks +[group('clean')] +clean: clean-build clean-cache clean-constraints clean-coverage clean-docs clean-mlruns clean-mypy clean-outputs clean-pytest clean-python clean-requirements clean-ruff + +# clean build folders +[group('clean')] +clean-build: + rm -rf dist/ + rm -rf build/ + +# clean cache folder +[group('clean')] +clean-cache: + rm -rf .cache/ + +# clean constraints file +[group('clean')] +clean-constraints: + rm -rf constraints.txt + +# clean coverage files +[group('clean')] +clean-coverage: + rm -rf .coverage* + +# clean docs folder +[group('clean')] +clean-docs: + rm -rf docs/ + +# clean mlruns folder +[group('clean')] +clean-mlruns: + rm -rf mlruns/* + +# clean mypy folders +[group('clean')] +clean-mypy: + rm -rf .mypy_cache/ + +# clean outputs folder +[group('clean')] +clean-outputs: + rm -rf outputs/* + +# clean pytest cache +[group('clean')] +clean-pytest: + rm -rf .pytest_cache/ + +# clean python caches +[group('clean')] +clean-python: + find . -type f -name '*.py[co]' -delete + find . -type d -name __pycache__ -exec rm -r {} \+ + +# clean requirements file +[group('clean')] +clean-requirements: + rm -f requirements.txt + +# clean ruff cache +[group('clean')] +clean-ruff: + rm -rf .ruff_cache/ + +# clean venv folder +[confirm] +[group('clean')] +clean-venv: + rm -rf .venv/ diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/commit.just b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/commit.just new file mode 100644 index 0000000..5171dbe --- /dev/null +++ b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/commit.just @@ -0,0 +1,14 @@ +# bump package +[group('commit')] +commit-bump: + uv run cz bump + +# commit package +[group('commit')] +commit-files: + uv run cz commit + +# get commit info +[group('commit')] +commit-info: + uv run cz info diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/doc.just b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/doc.just new file mode 100644 index 0000000..9763649 --- /dev/null +++ b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/doc.just @@ -0,0 +1,13 @@ +# run doc tasks +[group('doc')] +doc: doc-build + +# build documentation +[group('doc')] +doc-build format="google" output="docs": clean-docs + {% raw %}uv run pdoc --docformat={{format}} --output-directory={{output}} {{SOURCES}}/{{PACKAGE}}{% endraw %} + +# serve documentation +[group('doc')] +doc-serve format="google" port="8088": + {% raw %}uv run pdoc --docformat={{format}} --port={{port}} {{SOURCES}}/{{PACKAGE}}{% endraw %} diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/docker.just b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/docker.just new file mode 100644 index 0000000..432ea70 --- /dev/null +++ b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/docker.just @@ -0,0 +1,18 @@ +# run docker tasks +[group('docker')] +docker: docker-build docker-run + +# build docker image +[group('docker')] +docker-build tag="latest": package-build + {% raw %}docker build --tag={{REPOSITORY}}:{{tag}} .{% endraw %} + +# start docker compose +[group('docker')] +docker-compose: + docker compose up + +# run latest docker image +[group('docker')] +docker-run tag="latest": + {% raw %}docker run --rm {{REPOSITORY}}:{{tag}}{% endraw %} diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/format.just b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/format.just new file mode 100644 index 0000000..78bea80 --- /dev/null +++ b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/format.just @@ -0,0 +1,13 @@ +# run format tasks +[group('format')] +format: format-import format-source + +# format code import +[group('format')] +format-import: + {% raw %}uv run ruff check --select=I --fix {{SOURCES}} {{TESTS}}{% endraw %} + +# format code source +[group('format')] +format-source: + {% raw %}uv run ruff format {{SOURCES}} {{TESTS}}{% endraw %} diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/install.just b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/install.just new file mode 100644 index 0000000..e8a74df --- /dev/null +++ b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/install.just @@ -0,0 +1,24 @@ +# run install tasks +[group('install')] +install: install-project install-hooks + +# install git hooks +[group('install')] +install-hooks: + uv run pre-commit install --hook-type=pre-push + uv run pre-commit install --hook-type=commit-msg + +# install the project +[group('install')] +install-project: + uv sync --all-groups + +# install github rulesets +[group('install')] +install-rulesets: + #!/usr/bin/env bash + set -euo pipefail + repo=$(gh repo view --json=name --jq=.name) + owner=$(gh repo view --json=owner --jq=.owner.login) + gh api --method POST -H "Accept: application/vnd.github+json" \ + "/repos/$owner/$repo/rulesets" --input=".github/rulesets/main.json" diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/package.just b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/package.just new file mode 100644 index 0000000..765ca2b --- /dev/null +++ b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/package.just @@ -0,0 +1,13 @@ +# run package tasks +[group('package')] +package: package-build + +# build package constraints +[group('package')] +package-constraints constraints="constraints.txt": + {% raw %}uv pip compile pyproject.toml --generate-hashes --output-file={{constraints}}{% endraw %} + +# build python package +[group('package')] +package-build constraints="constraints.txt": clean-build package-constraints + {% raw %}uv build --build-constraint={{constraints}} --require-hashes --wheel{% endraw %} diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/project.just b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/project.just new file mode 100644 index 0000000..318dc74 --- /dev/null +++ b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/project.just @@ -0,0 +1,14 @@ +# run project tasks +[group('project')] +project: project-requirements (project-run "tuning") (project-run "training") (project-run "inference") + +# export requirements file +[group('project')] +project-requirements: + uv export --format=requirements-txt --no-dev --no-hashes \ + --no-editable --no-emit-project --output-file=requirements.txt + +# run project job with a config +[group('project')] +project-run job: + {% raw %}uv run {{REPOSITORY}} confs/{job}.yaml{% endraw %} diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tests/conftest.py b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tests/conftest.py new file mode 100644 index 0000000..ad0e36f --- /dev/null +++ b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tests/conftest.py @@ -0,0 +1 @@ +"""Configuration for the tests.""" diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tests/test_scripts.py b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tests/test_scripts.py new file mode 100644 index 0000000..a77d674 --- /dev/null +++ b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tests/test_scripts.py @@ -0,0 +1,14 @@ +# %% IMPORTS + +from {{cookiecutter.package}} import scripts + +# %% FUNCTIONS + + +def test_main() -> None: + # given + argv = ["x", "y", "z"] + # when + result = scripts.main(argv) + # then + assert result == 0, "Result should be 0!" diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/{{cookiecutter.repository}}.code-workspace b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/{{cookiecutter.repository}}.code-workspace new file mode 100644 index 0000000..bf84e6f --- /dev/null +++ b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/{{cookiecutter.repository}}.code-workspace @@ -0,0 +1,30 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": { + "editor.formatOnSave": true, + "python.defaultInterpreterPath": ".venv/bin/python", + "python.testing.pytestEnabled": true, + "python.testing.pytestArgs": [ + "tests" + ], + "[python]": { + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + }, + "editor.defaultFormatter": "charliermarsh.ruff", + }, + }, + "extensions": { + "recommendations": [ + "charliermarsh.ruff", + "ms-python.mypy-type-checker", + "ms-python.python", + "ms-python.vscode-pylance", + "redhat.vscode-yaml", + ] + } +} From bc9c8f091002184d1e3c873ad58720f832de29a3 Mon Sep 17 00:00:00 2001 From: Irving Rodriguez Date: Fri, 25 Apr 2025 14:54:56 -0700 Subject: [PATCH 2/6] Fix source directory --- .github/workflows/{tests.yml => }/tests.yml | 0 README.md | 74 +++----- tasks.py | 25 --- .../.env.example | 0 .../.github/actions/setup/action.yml | 12 +- .../.github/rulesets/main.json | 0 .../.github/workflows/check.yml | 10 +- .../.github/workflows/publish.yml | 10 +- {{cookiecutter.repository}}/.gitignore | 165 ------------------ .../.pre-commit-config.yaml | 18 +- .../.python-version | 0 {{cookiecutter.repository}}/Dockerfile | 17 +- {{cookiecutter.repository}}/README.md | 65 +------ .../docker-compose.yml | 12 +- {{cookiecutter.repository}}/invoke.yaml | 8 - .../justfile | 0 {{cookiecutter.repository}}/poetry.toml | 5 - {{cookiecutter.repository}}/pyproject.toml | 89 +++++----- .../src/example_app/__init__.py | 0 .../src/example_app/main.py | 15 -- {{cookiecutter.repository}}/tasks/__init__.py | 33 ---- .../tasks/check.just | 0 {{cookiecutter.repository}}/tasks/checks.py | 55 ------ .../tasks/clean.just | 0 {{cookiecutter.repository}}/tasks/cleans.py | 105 ----------- .../tasks/commit.just | 0 {{cookiecutter.repository}}/tasks/commits.py | 31 ---- .../tasks/containers.py | 37 ---- .../tasks/doc.just | 0 .../tasks/docker.just | 0 {{cookiecutter.repository}}/tasks/docs.py | 34 ---- .../tasks/format.just | 0 {{cookiecutter.repository}}/tasks/formats.py | 25 --- .../tasks/install.just | 0 {{cookiecutter.repository}}/tasks/installs.py | 26 --- {{cookiecutter.repository}}/tasks/mlflow.just | 13 ++ .../tasks/package.just | 0 {{cookiecutter.repository}}/tasks/packages.py | 25 --- .../tasks/project.just | 0 .../tests/test_scripts.py | 1 + .../.github/actions/setup/action.yml | 13 -- .../.github/workflows/check.yml | 20 --- .../.github/workflows/publish.yml | 47 ----- .../{{cookiecutter.repository}}/.gitignore | 31 ---- .../.pre-commit-config.yaml | 33 ---- .../{{cookiecutter.repository}}/Dockerfile | 6 - .../{{cookiecutter.repository}}/LICENSE.txt | 1 - .../{{cookiecutter.repository}}/README.md | 23 --- .../confs/inference.yaml | 0 .../confs/training.yaml | 0 .../confs/tuning.yaml | 0 .../docker-compose.yml | 10 -- .../pyproject.toml | 101 ----------- .../src/{{cookiecutter.package}}/__init__.py | 1 - .../src/{{cookiecutter.package}}/__main__.py | 10 -- .../src/{{cookiecutter.package}}/scripts.py | 14 -- .../tests/conftest.py | 1 - .../tests/test_scripts.py | 14 -- ...{{cookiecutter.repository}}.code-workspace | 30 ---- 59 files changed, 130 insertions(+), 1135 deletions(-) rename .github/workflows/{tests.yml => }/tests.yml (100%) delete mode 100644 tasks.py rename {{cookiecutter.repository}}/{{{cookiecutter.repository}} => }/.env.example (100%) rename {{cookiecutter.repository}}/{{{cookiecutter.repository}} => }/.github/rulesets/main.json (100%) rename {{cookiecutter.repository}}/{{{cookiecutter.repository}} => }/.python-version (100%) delete mode 100644 {{cookiecutter.repository}}/invoke.yaml rename {{cookiecutter.repository}}/{{{cookiecutter.repository}} => }/justfile (100%) delete mode 100644 {{cookiecutter.repository}}/poetry.toml delete mode 100644 {{cookiecutter.repository}}/src/example_app/__init__.py delete mode 100644 {{cookiecutter.repository}}/src/example_app/main.py delete mode 100644 {{cookiecutter.repository}}/tasks/__init__.py rename {{cookiecutter.repository}}/{{{cookiecutter.repository}} => }/tasks/check.just (100%) delete mode 100644 {{cookiecutter.repository}}/tasks/checks.py rename {{cookiecutter.repository}}/{{{cookiecutter.repository}} => }/tasks/clean.just (100%) delete mode 100644 {{cookiecutter.repository}}/tasks/cleans.py rename {{cookiecutter.repository}}/{{{cookiecutter.repository}} => }/tasks/commit.just (100%) delete mode 100644 {{cookiecutter.repository}}/tasks/commits.py delete mode 100644 {{cookiecutter.repository}}/tasks/containers.py rename {{cookiecutter.repository}}/{{{cookiecutter.repository}} => }/tasks/doc.just (100%) rename {{cookiecutter.repository}}/{{{cookiecutter.repository}} => }/tasks/docker.just (100%) delete mode 100644 {{cookiecutter.repository}}/tasks/docs.py rename {{cookiecutter.repository}}/{{{cookiecutter.repository}} => }/tasks/format.just (100%) delete mode 100644 {{cookiecutter.repository}}/tasks/formats.py rename {{cookiecutter.repository}}/{{{cookiecutter.repository}} => }/tasks/install.just (100%) delete mode 100644 {{cookiecutter.repository}}/tasks/installs.py create mode 100644 {{cookiecutter.repository}}/tasks/mlflow.just rename {{cookiecutter.repository}}/{{{cookiecutter.repository}} => }/tasks/package.just (100%) delete mode 100644 {{cookiecutter.repository}}/tasks/packages.py rename {{cookiecutter.repository}}/{{{cookiecutter.repository}} => }/tasks/project.just (100%) delete mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/.github/actions/setup/action.yml delete mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/.github/workflows/check.yml delete mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/.github/workflows/publish.yml delete mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/.gitignore delete mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/.pre-commit-config.yaml delete mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/Dockerfile delete mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/LICENSE.txt delete mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/README.md delete mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/confs/inference.yaml delete mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/confs/training.yaml delete mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/confs/tuning.yaml delete mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/docker-compose.yml delete mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/pyproject.toml delete mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/src/{{cookiecutter.package}}/__init__.py delete mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/src/{{cookiecutter.package}}/__main__.py delete mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/src/{{cookiecutter.package}}/scripts.py delete mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/tests/conftest.py delete mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/tests/test_scripts.py delete mode 100644 {{cookiecutter.repository}}/{{cookiecutter.repository}}/{{cookiecutter.repository}}.code-workspace diff --git a/.github/workflows/tests.yml/tests.yml b/.github/workflows/tests.yml similarity index 100% rename from .github/workflows/tests.yml/tests.yml rename to .github/workflows/tests.yml diff --git a/README.md b/README.md index 59ccecd..c5244f6 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,43 @@ -# Cookiecutter - MLOps Package +# Irving's Python Project Cookiecutter Template -[![Release](https://img.shields.io/github/v/release/fmind/cookiecutter-mlops-package)](https://github.com/fmind/cookiecutter-mlops-package/releases) -[![License](https://img.shields.io/github/license/fmind/cookiecutter-mlops-package)](https://github.com/fmind/cookiecutter-mlops-package/blob/main/LICENSE.txt) +A python project template to simplify project setup. Adapted from https://github.com/fmind/cookiecutter-mlops-package -**Jumpstart your MLOps projects with this comprehensive [Cookiecutter template](https://cookiecutter.readthedocs.io/)**. +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 tailored for MLOps tasks. +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. -**Related resources**: +**Source 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. -- **[LLMOps Coding Package (Example)](https://github.com/callmesora/llmops-python-package/)**: Example with best practices and tools to support your LLMOps projects. ## Philosophy -This [Cookiecutter](https://cookiecutter.readthedocs.io/) is designed to be a common ground for diverse MLOps environments. Whether you're working with [Kubernetes](https://www.kubeflow.org/), [Vertex AI](https://cloud.google.com/vertex-ai), [Databricks](https://www.databricks.com/), [Azure ML](https://azure.microsoft.com/en-us/products/machine-learning), or [AWS SageMaker](https://aws.amazon.com/sagemaker/), the core principles of using Python packages and Docker images remain consistent. +This [Cookiecutter](https://cookiecutter.readthedocs.io/) is designed to be a common ground for diverse python environments. Whether you're working with [Kubernetes](https://www.kubeflow.org/), [Vertex AI](https://cloud.google.com/vertex-ai), [Databricks](https://www.databricks.com/), [Azure ML](https://azure.microsoft.com/en-us/products/machine-learning), or [AWS SageMaker](https://aws.amazon.com/sagemaker/), the core principles of using Python packages and Docker images remain consistent. -This template equips you with the essentials for creating, testing, and packaging your AI/ML code, providing a solid base for [integration into your chosen MLOps platform](https://fmind.medium.com/stop-building-rigid-ai-ml-pipelines-embrace-reusable-components-for-flexible-mlops-6e165d837110). To fully leverage its capabilities within a specific environment, you might need to combine it with external tools like [Airflow](https://airflow.apache.org/) for orchestration or platform-specific SDKs for deployment. +This template equips you with the essentials for creating, testing, and packaging your code, providing a solid base for integration into your chosen platform. 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. ## Key Features +(This section was copied into the created project's README so tool info is available to users.) + * **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/). +Uv Integration: Effortless dependency management and packaging with [uv](https://docs.astral.sh/uv/). * **Automated Testing and Checks:** Pre-configured workflows using [Pytest](https://docs.pytest.org/), [Ruff](https://docs.astral.sh/ruff/), [Mypy](https://mypy.readthedocs.io/), [Bandit](https://bandit.readthedocs.io/), and [Coverage](https://coverage.readthedocs.io/) to ensure code quality, style, security, and type safety. * **Pre-commit Hooks:** Automatic code formatting and linting with [Ruff](https://docs.astral.sh/ruff/) and other pre-commit hooks to maintain consistency. * **Dockerized Deployment:** Dockerfile and docker-compose.yml for building and running the package within a containerized environment ([Docker](https://www.docker.com/)). -* **Invoke Task Automation:** [PyInvoke](https://www.pyinvoke.org/) tasks to simplify development workflows such as cleaning, installing, formatting, checking, building, documenting, and running MLflow projects. +* **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. - ## Quick Start 1. **Generate your project:** ```bash pip install cookiecutter -cookiecutter gh:fmind/cookiecutter-mlops-package +cookiecutter gh:irod973/python-project-template ``` You'll be prompted for the following variables: @@ -47,34 +46,34 @@ You'll be prompted for the following variables: - `name`: The name of your project. - `repository`: The name of your GitHub repository. - `package`: The name of your Python package. -- `license`: The license for your project. +- `license`: The license for your project (Note: use "NA" until we define a standard licence or omit entirely) - `version`: The initial version of your project. - `description`: A brief description of your project. -- `python_version`: The Python version to use (e.g., 3.13). -- `mlflow_version`: The MLflow version to use (e.g., 2.20.3). +- `python_version`: The Python version to use (e.g., 3.12). 2. **Initialize a git repository:** ```bash cd {{ cookiecutter.repository }} git init +# Should also create remote repo, e.g. via Github web console +# Then make first push e.g. +# git commit -m "Initial commit" +# git remote add origin https://github.com/irod973/{{myproject}}.git +# git push -u origin main ``` -3. **Enable GitHub Pages Workflow:** - -- Navigate to your repository settings on GitHub: "Settings" -> "Actions" -> "General." -- Under "Workflow permissions," ensure "Read and write permissions" is selected. - - This allows the workflow to automatically publish your documentation. - -4. **Explore the generated project:** +3. **Explore the generated project:** - `src/{{cookiecutter.package}}`: Your Python package source code. - `tests/`: Unit tests for your package. -- `tasks/`: PyInvoke tasks for automation. +- `tasks/`: `just` commands for automation. - `Dockerfile`: Configuration for building your Docker image. -- `docker-compose.yml`: Orchestration file for running MLflow and your project. +- `docker-compose.yml`: Orchestration file for running your project. -5. **Start developing!** +4. **Start developing!** + +Use the provided `just` commands to manage your development workflow: Use the provided Invoke tasks to manage your development workflow: @@ -85,36 +84,19 @@ Use the provided Invoke tasks to manage your development workflow: - `uv run just docker`: Build and run your Docker image. - `uv run just format`: Format your code with Ruff. - `uv run just install`: Install dependencies, pre-commit hooks, and GitHub rulesets. -- `uv run just mlflow`: Start an Mlflow server. - `uv run just package`: Build your Python package. - `uv run just project`: Run the project in the CLI. ## Example Usage -### Running the Project Script - -After installing dependencies and setting up MLflow: - -```bash -uv run just project -``` - -This will execute the job with the configuration file in your `confs` folder. - ### Building and Running Your Docker Image ```bash -invoke docker +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. The `CMD` in the Dockerfile executes your package with the `--help` flag. - -## Contributions - -We welcome [contributions](https://github.com/fmind/cookiecutter-mlops-package/blob/main/CODE_OF_CONDUCT.md) to enhance this [Cookiecutter template](https://cookiecutter.readthedocs.io/) for generating MLOps projects. - -Feel free to open [issues](https://github.com/fmind/cookiecutter-mlops-package/issues) or [pull requests](https://github.com/fmind/cookiecutter-mlops-package/pulls) for any improvements, bug fixes, or feature requests. +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 -This project 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. +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/tasks.py b/tasks.py deleted file mode 100644 index 5460133..0000000 --- a/tasks.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Tasks of the project.""" - -# %% IMPORTS - -from invoke import task -from invoke.context import Context - -# %% TASKS - -@task -def install(ctx: Context) -> None: - """Install the project.""" - ctx.run("poetry install") - - -@task -def hooks(ctx: Context) -> None: - """Setup the project hooks.""" - ctx.run("poetry run pre-commit install") - - -@task -def test(ctx: Context) -> None: - """Run the project unit tests.""" - ctx.run("poetry run pytest tests/") diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.env.example b/{{cookiecutter.repository}}/.env.example similarity index 100% rename from {{cookiecutter.repository}}/{{cookiecutter.repository}}/.env.example rename to {{cookiecutter.repository}}/.env.example diff --git a/{{cookiecutter.repository}}/.github/actions/setup/action.yml b/{{cookiecutter.repository}}/.github/actions/setup/action.yml index 09c8777..cd4f2ef 100644 --- a/{{cookiecutter.repository}}/.github/actions/setup/action.yml +++ b/{{cookiecutter.repository}}/.github/actions/setup/action.yml @@ -3,9 +3,11 @@ description: Setup for project workflows runs: using: composite steps: - - run: pipx install invoke poetry - shell: bash - - uses: actions/setup-python@v5 + - name: Install uv + uses: astral-sh/setup-uv@v5 with: - python-version: {{cookiecutter.python_version}} - cache: poetry + enable-cache: true + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version-file: .python-version diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.github/rulesets/main.json b/{{cookiecutter.repository}}/.github/rulesets/main.json similarity index 100% rename from {{cookiecutter.repository}}/{{cookiecutter.repository}}/.github/rulesets/main.json rename to {{cookiecutter.repository}}/.github/rulesets/main.json diff --git a/{{cookiecutter.repository}}/.github/workflows/check.yml b/{{cookiecutter.repository}}/.github/workflows/check.yml index c9a1bb3..53508f1 100644 --- a/{{cookiecutter.repository}}/.github/workflows/check.yml +++ b/{{cookiecutter.repository}}/.github/workflows/check.yml @@ -2,7 +2,7 @@ name: Check on: pull_request: branches: - - main + - '*' concurrency: cancel-in-progress: true group: {% raw %}${{ github.workflow }}-${{ github.ref }}{% endraw %} @@ -12,5 +12,9 @@ jobs: steps: - uses: actions/checkout@v4 - uses: ./.github/actions/setup - - run: poetry install --with checks - - run: poetry run invoke checks + - run: uv sync --group=check + - run: uv run just check-code + - run: uv run just check-type + - run: uv run just check-format + - run: uv run just check-security + - run: uv run just check-coverage diff --git a/{{cookiecutter.repository}}/.github/workflows/publish.yml b/{{cookiecutter.repository}}/.github/workflows/publish.yml index 210dca7..c3a4814 100644 --- a/{{cookiecutter.repository}}/.github/workflows/publish.yml +++ b/{{cookiecutter.repository}}/.github/workflows/publish.yml @@ -5,7 +5,7 @@ on: - edited - published env: - DOCKER_IMAGE: ghcr.io/{{cookiecutter.user}}/{{cookiecutter.repository}} + DOCKER_IMAGE: ghcr.io/fmind/mlops-python-package concurrency: cancel-in-progress: true group: publish-workflow @@ -15,8 +15,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: ./.github/actions/setup - - run: poetry install --with docs - - run: poetry run invoke docs + - run: uv sync --group=doc + - run: uv run just doc - uses: JamesIves/github-pages-deploy-action@v4 with: folder: docs/ @@ -28,8 +28,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: ./.github/actions/setup - - run: poetry install --with dev - - run: poetry run invoke packages + - run: uv sync --only-dev + - run: uv run just package - uses: docker/login-action@v3 with: registry: ghcr.io diff --git a/{{cookiecutter.repository}}/.gitignore b/{{cookiecutter.repository}}/.gitignore index 23bdea3..0abc593 100644 --- a/{{cookiecutter.repository}}/.gitignore +++ b/{{cookiecutter.repository}}/.gitignore @@ -29,168 +29,3 @@ # Python *.py[cod] __pycache__/ - -# https://github.com/github/gitignore/blob/main/Python.gitignore - -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control -.pdm.toml -.pdm-python -.pdm-build/ - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ diff --git a/{{cookiecutter.repository}}/.pre-commit-config.yaml b/{{cookiecutter.repository}}/.pre-commit-config.yaml index 0b893e6..9eebedd 100644 --- a/{{cookiecutter.repository}}/.pre-commit-config.yaml +++ b/{{cookiecutter.repository}}/.pre-commit-config.yaml @@ -2,10 +2,10 @@ # https://pre-commit.com/hooks.html default_language_version: - python: python{{cookiecutter.python_version}} + python: python3.13 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: 'v5.0.0' hooks: - id: check-added-large-files - id: check-case-conflict @@ -16,18 +16,18 @@ repos: - id: end-of-file-fixer - id: mixed-line-ending - id: trailing-whitespace - - repo: https://github.com/python-poetry/poetry - rev: 1.8.3 - hooks: - - id: poetry-check - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.0 + rev: 'v0.9.9' hooks: - id: ruff - id: ruff-format + - repo: https://github.com/PyCQA/bandit + rev: '1.8.3' + hooks: + - id: bandit # - repo: https://github.com/commitizen-tools/commitizen - # rev: v3.27.0 + # rev: 'v4.4.1' # hooks: # - id: commitizen # - id: commitizen-branch - # stages: [push] + # stages: [pre-push] diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.python-version b/{{cookiecutter.repository}}/.python-version similarity index 100% rename from {{cookiecutter.repository}}/{{cookiecutter.repository}}/.python-version rename to {{cookiecutter.repository}}/.python-version diff --git a/{{cookiecutter.repository}}/Dockerfile b/{{cookiecutter.repository}}/Dockerfile index 19d9a46..cd59030 100644 --- a/{{cookiecutter.repository}}/Dockerfile +++ b/{{cookiecutter.repository}}/Dockerfile @@ -1,15 +1,6 @@ # https://docs.docker.com/engine/reference/builder/ -FROM python:{{cookiecutter.python_version}}-slim -# Install dependencies with poetry -RUN pip install --progress-bar off poetry==1.8.2 -COPY poetry.lock pyproject.toml ./ -RUN poetry export -f requirements.txt --output requirements.txt --without-hashes -RUN pip install --progress-bar off -r requirements.txt -RUN rm poetry.lock pyproject.toml - -COPY ./src /app -ENV PYTHONPATH="${PYTHONPATH}:/app" -WORKDIR /app - -CMD ["uvicorn", "example_app.main:app", "--host", "0.0.0.0"] +FROM ghcr.io/astral-sh/uv:python{{cookiecutter.python_version}}-bookworm +COPY dist/*.whl . +RUN uv pip install --system *.whl +CMD ["{{cookiecutter.repository}}", "--help"] diff --git a/{{cookiecutter.repository}}/README.md b/{{cookiecutter.repository}}/README.md index e827219..e07f42d 100644 --- a/{{cookiecutter.repository}}/README.md +++ b/{{cookiecutter.repository}}/README.md @@ -6,75 +6,18 @@ [![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) -# Description - {{cookiecutter.description}}. -(This README is generated from a cookiecutter template. Delete this comment and modify your README!) - # Installation -Initialize your project with the provided invoke command. -```bash -# Install dependencies and pre-commit hooks -invoke installs -``` - -# Usage +Use the package manager [uv](https://docs.astral.sh/uv/): -(The source comes with an example python package and an example FastAPI app. Delete this comment and add details for your application.) - -Test the example package ```bash -poetry run {{cookiecutter.repository}} +uv sync ``` -Test the example API with Docker: -```bash -poetry add fastapi uvicorn - -# Invoke docker compose -invoke containers - -# Or run with docker compose -docker compose up --build - -# Or run with docker -# Note: specify platform if running on Apple M chip -docker build --platform linux/amd64 -t angle-to-geo-image -f Dockerfile . -docker run -it --platform linux/amd64 --name angle-to-geo-test-ctr -p 8000:8000 angle-to-geo-image -``` +# Usage ```bash -poetry add fastapi uvicorn -# Test the API using the local environment -cd src -poetry run uvicorn example_app.main:app --reload +uv run {{cookiecutter.repository}} ``` - -## Development Features - -* **Streamlined Project Structure:** A well-defined directory layout for source code, tests, documentation, tasks, and Docker configurations. -* **Poetry Integration:** Effortless dependency management and packaging with [Poetry](https://python-poetry.org/). -* **Automated Testing and Checks:** Pre-configured workflows using [Pytest](https://docs.pytest.org/), [Ruff](https://docs.astral.sh/ruff/), [Mypy](https://mypy.readthedocs.io/), [Bandit](https://bandit.readthedocs.io/), and [Coverage](https://coverage.readthedocs.io/) to ensure code quality, style, security, and type safety. -* **Pre-commit Hooks:** Automatic code formatting and linting with [Ruff](https://docs.astral.sh/ruff/) and other pre-commit hooks to maintain consistency. -* **Dockerized Deployment:** Dockerfile and docker-compose.yml for building and running the package within a containerized environment ([Docker](https://www.docker.com/)). -* **Invoke Task Automation:** [PyInvoke](https://www.pyinvoke.org/) tasks to simplify development workflows such as cleaning, installing, formatting, checking, building, documenting, and running MLflow projects. -* **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. - -Use the provided Invoke tasks to manage your development workflow: - -- `invoke installs`: Install dependencies and pre-commit hooks. -- `invoke formats`: Format your code. -- `invoke checks`: Run code quality, type, security, and test checks. -- `invoke docs`: Generate API documentation. -- `invoke packages`: Build your Python package. -- `invoke containers`: Build and run your Docker image. - -### Using Ruff with PyCharm - -Note: First version of [Ruff plugin](https://github.com/koxudaxi/ruff-pycharm-plugin) would have issues (v0.0.27) apparently due to my PyCharm version (2023.2). - -I upgraded to (2024.2) and Ruff 0.0.39 and now autosave works. - diff --git a/{{cookiecutter.repository}}/docker-compose.yml b/{{cookiecutter.repository}}/docker-compose.yml index bb1c479..730552c 100644 --- a/{{cookiecutter.repository}}/docker-compose.yml +++ b/{{cookiecutter.repository}}/docker-compose.yml @@ -1,10 +1,10 @@ # https://docs.docker.com/compose/compose-file/ services: - fastapi_app: # Name your service - build: . # Build the image from the current directory (.) - command: ["uvicorn", "angle_to_geo.main:app", "--host", "0.0.0.0", "--reload"] + mlflow: + image: ghcr.io/mlflow/mlflow:v{{cookiecutter.mlflow_version}} ports: - - "8000:8000" # Map container port 8000 to host port 8000 - volumes: - - ./src:/app/src # Mount your source code directory + - 5000:5000 + environment: + - MLFLOW_HOST=0.0.0.0 + command: mlflow server diff --git a/{{cookiecutter.repository}}/invoke.yaml b/{{cookiecutter.repository}}/invoke.yaml deleted file mode 100644 index 272d689..0000000 --- a/{{cookiecutter.repository}}/invoke.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# https://docs.pyinvoke.org/en/latest/index.html - -run: - echo: true -project: - name: {{cookiecutter.name}} - package: {{cookiecutter.package}} - repository: {{cookiecutter.repository}} diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/justfile b/{{cookiecutter.repository}}/justfile similarity index 100% rename from {{cookiecutter.repository}}/{{cookiecutter.repository}}/justfile rename to {{cookiecutter.repository}}/justfile diff --git a/{{cookiecutter.repository}}/poetry.toml b/{{cookiecutter.repository}}/poetry.toml deleted file mode 100644 index 7783e1b..0000000 --- a/{{cookiecutter.repository}}/poetry.toml +++ /dev/null @@ -1,5 +0,0 @@ -# https://python-poetry.org/docs/configuration/ - -[virtualenvs] -in-project = true -prefer-active-python = true diff --git a/{{cookiecutter.repository}}/pyproject.toml b/{{cookiecutter.repository}}/pyproject.toml index abe7d16..0e7b3d7 100644 --- a/{{cookiecutter.repository}}/pyproject.toml +++ b/{{cookiecutter.repository}}/pyproject.toml @@ -1,49 +1,58 @@ -# https://python-poetry.org/docs/pyproject/ +# https://docs.astral.sh/uv/reference/settings/ +# https://packaging.python.org/en/latest/guides/writing-pyproject-toml/ # PROJECT -[tool.poetry] +[project] name = "{{cookiecutter.repository}}" version = "{{cookiecutter.version}}" description = "{{cookiecutter.description}}" -repository = "https://github.com/{{cookiecutter.user}}/{{cookiecutter.repository}}" -documentation = "https://{{cookiecutter.user}}.github.io/{{cookiecutter.repository}}/" -authors = ["{{cookiecutter.user}}"] +authors = [{ name = "{{cookiecutter.user}}" }] readme = "README.md" -license = "{{cookiecutter.license}}" -packages = [{ include = "{{cookiecutter.package}}", from = "src" }] +license = { file = "LICENSE.txt" } +keywords = ["mlops", "python", "package"] +requires-python = ">={{cookiecutter.python_version}}" +dependencies = [ + "hatchling>=1.27.0", + "mlflow>={{cookiecutter.mlflow_version}}", +] + +# LINKS + +[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" +"Bug Tracker" = "https://github.com/{{cookiecutter.user}}/{{cookiecutter.repository}}/issues" +Changelog = "https://github.com/{{cookiecutter.user}}/{{cookiecutter.repository}}/blob/main/CHANGELOG.md" # SCRIPTS -[tool.poetry.scripts] +[project.scripts] {{cookiecutter.repository}} = "{{cookiecutter.package}}.scripts:main" # DEPENDENCIES -[tool.poetry.dependencies] -python = "^{{cookiecutter.python_version}}" -loguru = "^0.7.2" - -[tool.poetry.group.checks.dependencies] -bandit = "^1.7.9" -coverage = "^7.5.4" -mypy = "^1.10.1" -pytest = "^8.2.2" -pytest-cov = "^5.0.0" -pytest-xdist = "^3.6.1" -ruff = "^0.5.0" - -[tool.poetry.group.commits.dependencies] -commitizen = "^3.27.0" -pre-commit = "^3.7.1" - -[tool.poetry.group.dev.dependencies] -invoke = "^2.2.0" - -[tool.poetry.group.docs.dependencies] -pdoc = "^14.5.1" - -# CONFIGURATIONS +[dependency-groups] +check = [ + "bandit>=1.8.3", + "coverage>=7.6.12", + "mypy>=1.15.0", + "pytest>=8.3.5", + "pytest-cov>=6.0.0", + "pytest-mock>=3.14.0", + "pytest-xdist>=3.6.1", + "ruff>=0.9.9", +] +commit = ["commitizen>=4.4.1", "pre-commit>=4.1.0"] +dev = ["rust-just>=1.39.0"] +doc = ["pdoc>=15.0.1"] +notebook = ["ipykernel>=6.29.5", "nbformat>=5.10.4"] + +# TOOLS + +[tool.uv] +default-groups = ["check", "commit", "dev", "doc", "notebook"] [tool.bandit] targets = ["src"] @@ -52,7 +61,7 @@ targets = ["src"] name = "cz_conventional_commits" tag_format = "v$version" version_scheme = "pep440" -version_provider = "poetry" +version_provider = "pep621" update_changelog_on_bump = true [tool.coverage.run] @@ -62,11 +71,9 @@ omit = ["__main__.py"] [tool.mypy] pretty = true -strict = true -python_version = "3.12" +python_version = "3.13" check_untyped_defs = true ignore_missing_imports = true -disable_error_code = "import-untyped" [tool.pytest.ini_options] addopts = "--verbosity=2" @@ -76,11 +83,7 @@ pythonpath = ["src"] fix = true indent-width = 4 line-length = 100 -target-version = "py312" -src = ["src"] - -[tool.ruff.lint] -extend-select = ["I"] +target-version = "py313" [tool.ruff.format] docstring-code-format = true @@ -94,5 +97,5 @@ convention = "google" # SYSTEMS [build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/{{cookiecutter.repository}}/src/example_app/__init__.py b/{{cookiecutter.repository}}/src/example_app/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/{{cookiecutter.repository}}/src/example_app/main.py b/{{cookiecutter.repository}}/src/example_app/main.py deleted file mode 100644 index b5b63c5..0000000 --- a/{{cookiecutter.repository}}/src/example_app/main.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -Setup: - pip install fastapi uvicorn - -Run: - uvicorn main:app --reload -""" - -from fastapi import FastAPI - -app = FastAPI() - -@app.get("/") -def hello_world(): - return {"Hello": "World"} \ No newline at end of file diff --git a/{{cookiecutter.repository}}/tasks/__init__.py b/{{cookiecutter.repository}}/tasks/__init__.py deleted file mode 100644 index 9d2aa99..0000000 --- a/{{cookiecutter.repository}}/tasks/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Task collections for the project.""" - -# mypy: ignore-errors - -# %% IMPORTS - -from invoke import Collection - -from . import ( - checks, - cleans, - commits, - containers, - docs, - formats, - installs, - packages, -) - -# %% NAMESPACES - -ns = Collection() - -# %% COLLECTIONS - -ns.add_collection(checks) -ns.add_collection(cleans) -ns.add_collection(commits) -ns.add_collection(containers) -ns.add_collection(docs) -ns.add_collection(formats) -ns.add_collection(installs) -ns.add_collection(packages) diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/check.just b/{{cookiecutter.repository}}/tasks/check.just similarity index 100% rename from {{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/check.just rename to {{cookiecutter.repository}}/tasks/check.just diff --git a/{{cookiecutter.repository}}/tasks/checks.py b/{{cookiecutter.repository}}/tasks/checks.py deleted file mode 100644 index 853d109..0000000 --- a/{{cookiecutter.repository}}/tasks/checks.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Check tasks for pyinvoke.""" - -# %% IMPORTS - -from invoke.context import Context -from invoke.tasks import task - -# %% TASKS - - -@task -def poetry(ctx: Context) -> None: - """Check poetry config files.""" - ctx.run("poetry check --lock") - - -@task -def format(ctx: Context) -> None: - """Check the formats with ruff.""" - ctx.run("poetry run ruff format --check src/ tasks/ tests/") - - -@task -def type(ctx: Context) -> None: - """Check the types with mypy.""" - ctx.run("poetry run mypy src/ tasks/ tests/") - - -@task -def code(ctx: Context) -> None: - """Check the codes with ruff.""" - ctx.run("poetry run ruff check src/ tasks/ tests/") - - -@task -def test(ctx: Context) -> None: - """Check the tests with pytest.""" - ctx.run("poetry run pytest --numprocesses='auto' tests/") - - -@task -def security(ctx: Context) -> None: - """Check the security with bandit.""" - ctx.run("poetry run bandit --recursive --configfile=pyproject.toml src/") - - -@task -def coverage(ctx: Context) -> None: - """Check the coverage with coverage.""" - ctx.run("poetry run pytest --numprocesses='auto' --cov=src/ --cov-fail-under=80 tests/") - - -@task(pre=[poetry, format, type, code, security, coverage], default=True) -def all(_: Context) -> None: - """Run all check tasks.""" diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/clean.just b/{{cookiecutter.repository}}/tasks/clean.just similarity index 100% rename from {{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/clean.just rename to {{cookiecutter.repository}}/tasks/clean.just diff --git a/{{cookiecutter.repository}}/tasks/cleans.py b/{{cookiecutter.repository}}/tasks/cleans.py deleted file mode 100644 index be82427..0000000 --- a/{{cookiecutter.repository}}/tasks/cleans.py +++ /dev/null @@ -1,105 +0,0 @@ -"""Clean tasks for pyinvoke.""" - -# %% IMPORTS - -from invoke.context import Context -from invoke.tasks import task - -# %% TASKS - -# %% - Tools - - -@task -def mypy(ctx: Context) -> None: - """Clean the mypy tool.""" - ctx.run("rm -rf .mypy_cache/") - - -@task -def ruff(ctx: Context) -> None: - """Clean the ruff tool.""" - ctx.run("rm -rf .ruff_cache/") - - -@task -def pytest(ctx: Context) -> None: - """Clean the pytest tool.""" - ctx.run("rm -rf .pytest_cache/") - - -@task -def coverage(ctx: Context) -> None: - """Clean the coverage tool.""" - ctx.run("rm -f .coverage*") - - -# %% - Folders - - -@task -def dist(ctx: Context) -> None: - """Clean the dist folder.""" - ctx.run("rm -f dist/*") - - -@task -def docs(ctx: Context) -> None: - """Clean the docs folder.""" - ctx.run("rm -rf docs/*") - - -@task -def cache(ctx: Context) -> None: - """Clean the cache folder.""" - ctx.run("rm -rf .cache/") - - -# %% - Sources - - -@task -def venv(ctx: Context) -> None: - """Clean the venv folder.""" - ctx.run("rm -rf .venv/") - - -@task -def poetry(ctx: Context) -> None: - """Clean poetry lock file.""" - ctx.run("rm -f poetry.lock") - - -@task -def python(ctx: Context) -> None: - """Clean python caches and bytecodes.""" - ctx.run("find . -type f -name '*.py[co]' -delete") - ctx.run(r"find . -type d -name __pycache__ -exec rm -r {} \+") - - -# %% - Combines - - -@task(pre=[mypy, ruff, pytest, coverage]) -def tools(_: Context) -> None: - """Run all tools tasks.""" - - -@task(pre=[dist, docs, cache]) -def folders(_: Context) -> None: - """Run all folders tasks.""" - - -@task(pre=[venv, poetry, python]) -def sources(_: Context) -> None: - """Run all sources tasks.""" - - -@task(pre=[tools, folders], default=True) -def all(_: Context) -> None: - """Run all tools and folders tasks.""" - - -@task(pre=[all, sources]) -def reset(_: Context) -> None: - """Run all tools, folders, sources, and projects tasks.""" diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/commit.just b/{{cookiecutter.repository}}/tasks/commit.just similarity index 100% rename from {{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/commit.just rename to {{cookiecutter.repository}}/tasks/commit.just diff --git a/{{cookiecutter.repository}}/tasks/commits.py b/{{cookiecutter.repository}}/tasks/commits.py deleted file mode 100644 index 88b087b..0000000 --- a/{{cookiecutter.repository}}/tasks/commits.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Commits tasks for pyinvoke.""" - -# %% IMPORTS - -from invoke.context import Context -from invoke.tasks import task - -# %% TASKS - - -@task -def info(ctx: Context) -> None: - """Print a guide for messages.""" - ctx.run("poetry run cz info") - - -@task -def bump(ctx: Context) -> None: - """Bump the version of the package.""" - ctx.run("poetry run cz bump", pty=True) - - -@task -def commit(ctx: Context) -> None: - """Commit all changes with a message.""" - ctx.run("poetry run cz commit", pty=True) - - -@task(pre=[commit], default=True) -def all(_: Context) -> None: - """Run all commit tasks.""" diff --git a/{{cookiecutter.repository}}/tasks/containers.py b/{{cookiecutter.repository}}/tasks/containers.py deleted file mode 100644 index d14152e..0000000 --- a/{{cookiecutter.repository}}/tasks/containers.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Container tasks for pyinvoke.""" - -# %% IMPORTS - -from invoke.context import Context -from invoke.tasks import task - -from . import packages - -# %% CONFIGS - -IMAGE_TAG = "latest" - -# %% TASKS - - -@task -def compose(ctx: Context) -> None: - """Start up docker compose.""" - ctx.run("docker compose up --build") - - -@task(pre=[packages.build]) -def build(ctx: Context, tag: str = IMAGE_TAG) -> None: - """Build the container image.""" - ctx.run(f"docker build --tag={ctx.project.repository}:{tag} .") - - -@task -def run(ctx: Context, tag: str = IMAGE_TAG) -> None: - """Run the container image.""" - ctx.run(f"docker run --rm {ctx.project.repository}:{tag}") - - -@task(pre=[build, run], default=True) -def all(_: Context) -> None: - """Run all container tasks.""" diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/doc.just b/{{cookiecutter.repository}}/tasks/doc.just similarity index 100% rename from {{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/doc.just rename to {{cookiecutter.repository}}/tasks/doc.just diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/docker.just b/{{cookiecutter.repository}}/tasks/docker.just similarity index 100% rename from {{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/docker.just rename to {{cookiecutter.repository}}/tasks/docker.just diff --git a/{{cookiecutter.repository}}/tasks/docs.py b/{{cookiecutter.repository}}/tasks/docs.py deleted file mode 100644 index 21eea9e..0000000 --- a/{{cookiecutter.repository}}/tasks/docs.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Docs tasks for pyinvoke.""" - -# %% IMPORTS - -from invoke.context import Context -from invoke.tasks import task - -from . import cleans - -# %% CONFIGS - -DOC_FORMAT = "google" -OUTPUT_DIR = "docs/" - -# %% TASKS - - -@task -def serve(ctx: Context, format: str = DOC_FORMAT, port: int = 8088) -> None: - """Serve the API docs with pdoc.""" - ctx.run(f"poetry run pdoc --docformat={format} --port={port} src/{ctx.project.package}") - - -@task -def api(ctx: Context, format: str = DOC_FORMAT, output_dir: str = OUTPUT_DIR) -> None: - """Generate the API docs with pdoc.""" - ctx.run( - f"poetry run pdoc --docformat={format} --output-directory={output_dir} src/{ctx.project.package}" - ) - - -@task(pre=[cleans.docs, api], default=True) -def all(_: Context) -> None: - """Run all docs tasks.""" diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/format.just b/{{cookiecutter.repository}}/tasks/format.just similarity index 100% rename from {{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/format.just rename to {{cookiecutter.repository}}/tasks/format.just diff --git a/{{cookiecutter.repository}}/tasks/formats.py b/{{cookiecutter.repository}}/tasks/formats.py deleted file mode 100644 index 4631bae..0000000 --- a/{{cookiecutter.repository}}/tasks/formats.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Format tasks for pyinvoke.""" - -# %% IMPORTS - -from invoke.context import Context -from invoke.tasks import task - -# %% TASKS - - -@task -def imports(ctx: Context) -> None: - """Format python imports with ruff.""" - ctx.run("poetry run ruff check --select I --fix") - - -@task -def sources(ctx: Context) -> None: - """Format python sources with ruff.""" - ctx.run("poetry run ruff format src/ tasks/ tests/") - - -@task(pre=[imports, sources], default=True) -def all(_: Context) -> None: - """Run all format tasks.""" diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/install.just b/{{cookiecutter.repository}}/tasks/install.just similarity index 100% rename from {{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/install.just rename to {{cookiecutter.repository}}/tasks/install.just diff --git a/{{cookiecutter.repository}}/tasks/installs.py b/{{cookiecutter.repository}}/tasks/installs.py deleted file mode 100644 index 05669be..0000000 --- a/{{cookiecutter.repository}}/tasks/installs.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Install tasks for pyinvoke.""" - -# %% IMPORTS - -from invoke.context import Context -from invoke.tasks import task - -# %% TASKS - - -@task -def poetry(ctx: Context) -> None: - """Install poetry packages.""" - ctx.run("poetry install") - - -@task -def pre_commit(ctx: Context) -> None: - """Install pre-commit hooks on git.""" - ctx.run("poetry run pre-commit install --hook-type pre-push") - ctx.run("poetry run pre-commit install --hook-type commit-msg") - - -@task(pre=[poetry, pre_commit], default=True) -def all(_: Context) -> None: - """Run all install tasks.""" diff --git a/{{cookiecutter.repository}}/tasks/mlflow.just b/{{cookiecutter.repository}}/tasks/mlflow.just new file mode 100644 index 0000000..b7f584d --- /dev/null +++ b/{{cookiecutter.repository}}/tasks/mlflow.just @@ -0,0 +1,13 @@ +# run mlflow tasks +[group('mlflow')] +mlflow: mlflow-doctor mlflow-serve + +# run mlflow doctor +[group('mlflow')] +mlflow-doctor: + uv run mlflow doctor + +# start mlflow server +[group('mlflow')] +mlflow-serve host="127.0.0.1" port="5000" uri="./mlruns": + {% raw %}uv run mlflow server --host={{host}} --port={{port}} --backend-store-uri={{uri}}{% endraw %} diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/package.just b/{{cookiecutter.repository}}/tasks/package.just similarity index 100% rename from {{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/package.just rename to {{cookiecutter.repository}}/tasks/package.just diff --git a/{{cookiecutter.repository}}/tasks/packages.py b/{{cookiecutter.repository}}/tasks/packages.py deleted file mode 100644 index 38e9c3d..0000000 --- a/{{cookiecutter.repository}}/tasks/packages.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Package tasks for pyinvoke.""" - -# %% IMPORTS - -from invoke.context import Context -from invoke.tasks import task - -from . import cleans - -# %% CONFIGS - -BUILD_FORMAT = "wheel" - -# %% TASKS - - -@task(pre=[cleans.dist]) -def build(ctx: Context, format: str = BUILD_FORMAT) -> None: - """Build the python package.""" - ctx.run(f"poetry build --format={format}") - - -@task(pre=[build], default=True) -def all(_: Context) -> None: - """Run all package tasks.""" diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/project.just b/{{cookiecutter.repository}}/tasks/project.just similarity index 100% rename from {{cookiecutter.repository}}/{{cookiecutter.repository}}/tasks/project.just rename to {{cookiecutter.repository}}/tasks/project.just diff --git a/{{cookiecutter.repository}}/tests/test_scripts.py b/{{cookiecutter.repository}}/tests/test_scripts.py index 568b8c1..a77d674 100644 --- a/{{cookiecutter.repository}}/tests/test_scripts.py +++ b/{{cookiecutter.repository}}/tests/test_scripts.py @@ -4,6 +4,7 @@ # %% FUNCTIONS + def test_main() -> None: # given argv = ["x", "y", "z"] diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.github/actions/setup/action.yml b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.github/actions/setup/action.yml deleted file mode 100644 index cd4f2ef..0000000 --- a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.github/actions/setup/action.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: Setup -description: Setup for project workflows -runs: - using: composite - steps: - - name: Install uv - uses: astral-sh/setup-uv@v5 - with: - enable-cache: true - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version-file: .python-version diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.github/workflows/check.yml b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.github/workflows/check.yml deleted file mode 100644 index 53508f1..0000000 --- a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.github/workflows/check.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: Check -on: - pull_request: - branches: - - '*' -concurrency: - cancel-in-progress: true - group: {% raw %}${{ github.workflow }}-${{ github.ref }}{% endraw %} -jobs: - checks: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/setup - - run: uv sync --group=check - - run: uv run just check-code - - run: uv run just check-type - - run: uv run just check-format - - run: uv run just check-security - - run: uv run just check-coverage diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.github/workflows/publish.yml b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.github/workflows/publish.yml deleted file mode 100644 index c3a4814..0000000 --- a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.github/workflows/publish.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Publish -on: - release: - types: - - edited - - published -env: - DOCKER_IMAGE: ghcr.io/fmind/mlops-python-package -concurrency: - cancel-in-progress: true - group: publish-workflow -jobs: - pages: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/setup - - run: uv sync --group=doc - - run: uv run just doc - - uses: JamesIves/github-pages-deploy-action@v4 - with: - folder: docs/ - branch: gh-pages - packages: - permissions: - packages: write - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/setup - - run: uv sync --only-dev - - run: uv run just package - - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: {% raw %}${{ github.actor }}{% endraw %} - password: {% raw %}${{ secrets.GITHUB_TOKEN }}{% endraw %} - - uses: docker/setup-buildx-action@v3 - - uses: docker/build-push-action@v6 - with: - push: true - context: . - cache-to: type=gha - cache-from: type=gha - tags: | - {% raw %}${{ env.DOCKER_IMAGE }}:latest{% endraw %} - {% raw %}${{ env.DOCKER_IMAGE }}:${{ github.ref_name }}{% endraw %} diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.gitignore b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.gitignore deleted file mode 100644 index 0abc593..0000000 --- a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.gitignore +++ /dev/null @@ -1,31 +0,0 @@ -# https://git-scm.com/docs/gitignore - -# Build -/dist/ -/build/ - -# Cache -.cache/ -.coverage* -.mypy_cache/ -.ruff_cache/ -.pytest_cache/ - -# Editor -/.idea/ -/.vscode/ -.ipynb_checkpoints/ - -# Environs -.env -/.venv/ - -# Project -/docs/* -/mlruns/* -/outputs/* -!**/.gitkeep - -# Python -*.py[cod] -__pycache__/ diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.pre-commit-config.yaml b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.pre-commit-config.yaml deleted file mode 100644 index 9eebedd..0000000 --- a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/.pre-commit-config.yaml +++ /dev/null @@ -1,33 +0,0 @@ -# https://pre-commit.com -# https://pre-commit.com/hooks.html - -default_language_version: - python: python3.13 -repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: 'v5.0.0' - hooks: - - id: check-added-large-files - - id: check-case-conflict - - id: check-merge-conflict - - id: check-toml - - id: check-yaml - - id: debug-statements - - id: end-of-file-fixer - - id: mixed-line-ending - - id: trailing-whitespace - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.9.9' - hooks: - - id: ruff - - id: ruff-format - - repo: https://github.com/PyCQA/bandit - rev: '1.8.3' - hooks: - - id: bandit - # - repo: https://github.com/commitizen-tools/commitizen - # rev: 'v4.4.1' - # hooks: - # - id: commitizen - # - id: commitizen-branch - # stages: [pre-push] diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/Dockerfile b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/Dockerfile deleted file mode 100644 index cd59030..0000000 --- a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/Dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -# https://docs.docker.com/engine/reference/builder/ - -FROM ghcr.io/astral-sh/uv:python{{cookiecutter.python_version}}-bookworm -COPY dist/*.whl . -RUN uv pip install --system *.whl -CMD ["{{cookiecutter.repository}}", "--help"] diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/LICENSE.txt b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/LICENSE.txt deleted file mode 100644 index 4ea2668..0000000 --- a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/LICENSE.txt +++ /dev/null @@ -1 +0,0 @@ -https://opensource.org/license/{{cookiecutter.license}} diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/README.md b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/README.md deleted file mode 100644 index e07f42d..0000000 --- a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# {{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) -[![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) -[![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) - -{{cookiecutter.description}}. - -# Installation - -Use the package manager [uv](https://docs.astral.sh/uv/): - -```bash -uv sync -``` - -# Usage - -```bash -uv run {{cookiecutter.repository}} -``` diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/confs/inference.yaml b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/confs/inference.yaml deleted file mode 100644 index e69de29..0000000 diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/confs/training.yaml b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/confs/training.yaml deleted file mode 100644 index e69de29..0000000 diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/confs/tuning.yaml b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/confs/tuning.yaml deleted file mode 100644 index e69de29..0000000 diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/docker-compose.yml b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/docker-compose.yml deleted file mode 100644 index 730552c..0000000 --- a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/docker-compose.yml +++ /dev/null @@ -1,10 +0,0 @@ -# https://docs.docker.com/compose/compose-file/ - -services: - mlflow: - image: ghcr.io/mlflow/mlflow:v{{cookiecutter.mlflow_version}} - ports: - - 5000:5000 - environment: - - MLFLOW_HOST=0.0.0.0 - command: mlflow server diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/pyproject.toml b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/pyproject.toml deleted file mode 100644 index 0e7b3d7..0000000 --- a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/pyproject.toml +++ /dev/null @@ -1,101 +0,0 @@ -# https://docs.astral.sh/uv/reference/settings/ -# https://packaging.python.org/en/latest/guides/writing-pyproject-toml/ - -# PROJECT - -[project] -name = "{{cookiecutter.repository}}" -version = "{{cookiecutter.version}}" -description = "{{cookiecutter.description}}" -authors = [{ name = "{{cookiecutter.user}}" }] -readme = "README.md" -license = { file = "LICENSE.txt" } -keywords = ["mlops", "python", "package"] -requires-python = ">={{cookiecutter.python_version}}" -dependencies = [ - "hatchling>=1.27.0", - "mlflow>={{cookiecutter.mlflow_version}}", -] - -# LINKS - -[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" -"Bug Tracker" = "https://github.com/{{cookiecutter.user}}/{{cookiecutter.repository}}/issues" -Changelog = "https://github.com/{{cookiecutter.user}}/{{cookiecutter.repository}}/blob/main/CHANGELOG.md" - -# SCRIPTS - -[project.scripts] -{{cookiecutter.repository}} = "{{cookiecutter.package}}.scripts:main" - -# DEPENDENCIES - -[dependency-groups] -check = [ - "bandit>=1.8.3", - "coverage>=7.6.12", - "mypy>=1.15.0", - "pytest>=8.3.5", - "pytest-cov>=6.0.0", - "pytest-mock>=3.14.0", - "pytest-xdist>=3.6.1", - "ruff>=0.9.9", -] -commit = ["commitizen>=4.4.1", "pre-commit>=4.1.0"] -dev = ["rust-just>=1.39.0"] -doc = ["pdoc>=15.0.1"] -notebook = ["ipykernel>=6.29.5", "nbformat>=5.10.4"] - -# TOOLS - -[tool.uv] -default-groups = ["check", "commit", "dev", "doc", "notebook"] - -[tool.bandit] -targets = ["src"] - -[tool.commitizen] -name = "cz_conventional_commits" -tag_format = "v$version" -version_scheme = "pep440" -version_provider = "pep621" -update_changelog_on_bump = true - -[tool.coverage.run] -branch = true -source = ["src"] -omit = ["__main__.py"] - -[tool.mypy] -pretty = true -python_version = "3.13" -check_untyped_defs = true -ignore_missing_imports = true - -[tool.pytest.ini_options] -addopts = "--verbosity=2" -pythonpath = ["src"] - -[tool.ruff] -fix = true -indent-width = 4 -line-length = 100 -target-version = "py313" - -[tool.ruff.format] -docstring-code-format = true - -[tool.ruff.lint.pydocstyle] -convention = "google" - -[tool.ruff.lint.per-file-ignores] -"tests/*.py" = ["D100", "D103"] - -# SYSTEMS - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/src/{{cookiecutter.package}}/__init__.py b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/src/{{cookiecutter.package}}/__init__.py deleted file mode 100644 index 537a370..0000000 --- a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/src/{{cookiecutter.package}}/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""{{cookiecutter.description}}.""" diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/src/{{cookiecutter.package}}/__main__.py b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/src/{{cookiecutter.package}}/__main__.py deleted file mode 100644 index 4691a30..0000000 --- a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/src/{{cookiecutter.package}}/__main__.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Entry point of the package.""" - -# %% IMPORTS - -from {{cookiecutter.package}} import scripts - -# %% MAIN - -if __name__ == "__main__": - scripts.main() diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/src/{{cookiecutter.package}}/scripts.py b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/src/{{cookiecutter.package}}/scripts.py deleted file mode 100644 index eb336db..0000000 --- a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/src/{{cookiecutter.package}}/scripts.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Scripts of the project.""" - -# %% IMPORTS - -import sys - -# %% FUNCTIONS - - -def main(argv: list[str] | None = None) -> int: - """Run the main script function.""" - args = argv or sys.argv[1:] - print("Args:", args) - return 0 diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tests/conftest.py b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tests/conftest.py deleted file mode 100644 index ad0e36f..0000000 --- a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tests/conftest.py +++ /dev/null @@ -1 +0,0 @@ -"""Configuration for the tests.""" diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tests/test_scripts.py b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tests/test_scripts.py deleted file mode 100644 index a77d674..0000000 --- a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/tests/test_scripts.py +++ /dev/null @@ -1,14 +0,0 @@ -# %% IMPORTS - -from {{cookiecutter.package}} import scripts - -# %% FUNCTIONS - - -def test_main() -> None: - # given - argv = ["x", "y", "z"] - # when - result = scripts.main(argv) - # then - assert result == 0, "Result should be 0!" diff --git a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/{{cookiecutter.repository}}.code-workspace b/{{cookiecutter.repository}}/{{cookiecutter.repository}}/{{cookiecutter.repository}}.code-workspace deleted file mode 100644 index bf84e6f..0000000 --- a/{{cookiecutter.repository}}/{{cookiecutter.repository}}/{{cookiecutter.repository}}.code-workspace +++ /dev/null @@ -1,30 +0,0 @@ -{ - "folders": [ - { - "path": "." - } - ], - "settings": { - "editor.formatOnSave": true, - "python.defaultInterpreterPath": ".venv/bin/python", - "python.testing.pytestEnabled": true, - "python.testing.pytestArgs": [ - "tests" - ], - "[python]": { - "editor.codeActionsOnSave": { - "source.organizeImports": "explicit" - }, - "editor.defaultFormatter": "charliermarsh.ruff", - }, - }, - "extensions": { - "recommendations": [ - "charliermarsh.ruff", - "ms-python.mypy-type-checker", - "ms-python.python", - "ms-python.vscode-pylance", - "redhat.vscode-yaml", - ] - } -} From 2655ccc01369dc6beb7d4e1f64b727dc0b7aac24 Mon Sep 17 00:00:00 2001 From: Irving Rodriguez Date: Fri, 25 Apr 2025 15:13:10 -0700 Subject: [PATCH 3/6] addressing self-review results --- README.md | 2 - tests/test_cookiecutter.py | 3 -- .../.pre-commit-config.yaml | 2 +- {{cookiecutter.repository}}/Dockerfile | 6 +++ {{cookiecutter.repository}}/README.md | 50 +++++++++++++++++-- .../docker-compose.yml | 12 ++--- {{cookiecutter.repository}}/pyproject.toml | 15 +++--- .../src/example_app/main.py | 8 +++ {{cookiecutter.repository}}/tasks/docker.just | 2 +- {{cookiecutter.repository}}/tasks/mlflow.just | 13 ----- 10 files changed, 76 insertions(+), 37 deletions(-) create mode 100644 {{cookiecutter.repository}}/src/example_app/main.py delete mode 100644 {{cookiecutter.repository}}/tasks/mlflow.just diff --git a/README.md b/README.md index c5244f6..b22bd2a 100644 --- a/README.md +++ b/README.md @@ -75,8 +75,6 @@ git init Use the provided `just` commands to manage your development workflow: -Use the provided Invoke tasks to manage your development workflow: - - `uv run just check`: Run code quality, type, security, and test checks. - `uv run just clean`: Clean up generated files. - `uv run just commit`: Commit changes to your repository. diff --git a/tests/test_cookiecutter.py b/tests/test_cookiecutter.py index 4646f81..6a44364 100644 --- a/tests/test_cookiecutter.py +++ b/tests/test_cookiecutter.py @@ -17,7 +17,6 @@ "uv run just project", "uv run just package", "uv run just docker", - "uv run just mlflow-doctor", ] # %% TESTS @@ -33,7 +32,6 @@ def test_project_generation(cookies: Cookies) -> None: "version": "1.0.0", "description": "A test project.", "python_version": "3.13", - "mlflow_version": "2.20.3", } repository = context['name'].lower().replace(' ', '-') package = repository.replace('-', '_') @@ -54,7 +52,6 @@ def test_project_generation(cookies: Cookies) -> None: "version": context['version'], "description": context['description'], "python_version": context['python_version'], - "mlflow_version": context['mlflow_version'], } # - commands shell = Subprocess(cwd=result.project_path) diff --git a/{{cookiecutter.repository}}/.pre-commit-config.yaml b/{{cookiecutter.repository}}/.pre-commit-config.yaml index 9eebedd..98b4fa3 100644 --- a/{{cookiecutter.repository}}/.pre-commit-config.yaml +++ b/{{cookiecutter.repository}}/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # https://pre-commit.com/hooks.html default_language_version: - python: python3.13 + python: python{{cookiecutter.python_version}} repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: 'v5.0.0' diff --git a/{{cookiecutter.repository}}/Dockerfile b/{{cookiecutter.repository}}/Dockerfile index cd59030..929c2e4 100644 --- a/{{cookiecutter.repository}}/Dockerfile +++ b/{{cookiecutter.repository}}/Dockerfile @@ -4,3 +4,9 @@ FROM ghcr.io/astral-sh/uv:python{{cookiecutter.python_version}}-bookworm COPY dist/*.whl . RUN uv pip install --system *.whl CMD ["{{cookiecutter.repository}}", "--help"] + +COPY ./src /app +ENV PYTHONPATH="${PYTHONPATH}:/app" +WORKDIR /app + +CMD ["uvicorn", "example_app.main:app", "--host", "0.0.0.0"] diff --git a/{{cookiecutter.repository}}/README.md b/{{cookiecutter.repository}}/README.md index e07f42d..2d38585 100644 --- a/{{cookiecutter.repository}}/README.md +++ b/{{cookiecutter.repository}}/README.md @@ -6,18 +6,60 @@ [![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) +# Description + {{cookiecutter.description}}. +(This README is generated from a cookiecutter template. Delete this comment and modify your README!) + # Installation -Use the package manager [uv](https://docs.astral.sh/uv/): +Initialize your project with the provided `just` command. +```bash +# Install dependencies and pre-commit hooks +uv run just install +``` +# Usage + +(The source comes with an example python package and an example FastAPI app. Delete this comment and add details for your application.) +Test the example package ```bash -uv sync +uv run {{cookiecutter.repository}} ``` -# Usage +Test the example API with Docker: +```bash +uv add fastapi uvicorn + +# Invoke docker compose +uv run just docker-compose +# Or run with docker compose +docker compose up --build + +# 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 +``` + +Test the API using the local environment: ```bash -uv run {{cookiecutter.repository}} +cd src +uv run uvicorn example_app.main:app --reload ``` + +## Development Features + +(This section was copied into the created project's README so tool info is available to users.) + +* **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/). +* **Automated Testing and Checks:** Pre-configured workflows using [Pytest](https://docs.pytest.org/), [Ruff](https://docs.astral.sh/ruff/), [Mypy](https://mypy.readthedocs.io/), [Bandit](https://bandit.readthedocs.io/), and [Coverage](https://coverage.readthedocs.io/) to ensure code quality, style, security, and type safety. +* **Pre-commit Hooks:** Automatic code formatting and linting with [Ruff](https://docs.astral.sh/ruff/) and other pre-commit hooks to maintain consistency. +* **Dockerized Deployment:** Dockerfile and docker-compose.yml for building and running the package within a containerized environment ([Docker](https://www.docker.com/)). +* **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. + diff --git a/{{cookiecutter.repository}}/docker-compose.yml b/{{cookiecutter.repository}}/docker-compose.yml index 730552c..134016e 100644 --- a/{{cookiecutter.repository}}/docker-compose.yml +++ b/{{cookiecutter.repository}}/docker-compose.yml @@ -1,10 +1,10 @@ # https://docs.docker.com/compose/compose-file/ services: - mlflow: - image: ghcr.io/mlflow/mlflow:v{{cookiecutter.mlflow_version}} + fastapi_app: # Name your service + build: . # Build the image from the current directory (.) + command: ["uvicorn", "{{cookiecutter.repository}}.main:app", "--host", "0.0.0.0", "--reload"] ports: - - 5000:5000 - environment: - - MLFLOW_HOST=0.0.0.0 - command: mlflow server + - "8000:8000" # Map container port 8000 to host port 8000 + volumes: + - ./src:/app/src # Mount your source code directory diff --git a/{{cookiecutter.repository}}/pyproject.toml b/{{cookiecutter.repository}}/pyproject.toml index 0e7b3d7..623fa56 100644 --- a/{{cookiecutter.repository}}/pyproject.toml +++ b/{{cookiecutter.repository}}/pyproject.toml @@ -9,12 +9,10 @@ version = "{{cookiecutter.version}}" description = "{{cookiecutter.description}}" authors = [{ name = "{{cookiecutter.user}}" }] readme = "README.md" -license = { file = "LICENSE.txt" } -keywords = ["mlops", "python", "package"] +license = "{{cookiecutter.license}}" requires-python = ">={{cookiecutter.python_version}}" dependencies = [ "hatchling>=1.27.0", - "mlflow>={{cookiecutter.mlflow_version}}", ] # LINKS @@ -24,12 +22,10 @@ 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" "Bug Tracker" = "https://github.com/{{cookiecutter.user}}/{{cookiecutter.repository}}/issues" -Changelog = "https://github.com/{{cookiecutter.user}}/{{cookiecutter.repository}}/blob/main/CHANGELOG.md" # SCRIPTS [project.scripts] -{{cookiecutter.repository}} = "{{cookiecutter.package}}.scripts:main" # DEPENDENCIES @@ -47,12 +43,11 @@ check = [ commit = ["commitizen>=4.4.1", "pre-commit>=4.1.0"] dev = ["rust-just>=1.39.0"] doc = ["pdoc>=15.0.1"] -notebook = ["ipykernel>=6.29.5", "nbformat>=5.10.4"] # TOOLS [tool.uv] -default-groups = ["check", "commit", "dev", "doc", "notebook"] +default-groups = ["check", "commit", "dev", "doc"] [tool.bandit] targets = ["src"] @@ -74,6 +69,9 @@ pretty = true python_version = "3.13" check_untyped_defs = true ignore_missing_imports = true +strict = true +disable_error_code = "import-untyped" +src = ["src"] [tool.pytest.ini_options] addopts = "--verbosity=2" @@ -85,6 +83,9 @@ indent-width = 4 line-length = 100 target-version = "py313" +[tool.ruff.lint] +extend-select = ["I"] + [tool.ruff.format] docstring-code-format = true diff --git a/{{cookiecutter.repository}}/src/example_app/main.py b/{{cookiecutter.repository}}/src/example_app/main.py new file mode 100644 index 0000000..5651f9e --- /dev/null +++ b/{{cookiecutter.repository}}/src/example_app/main.py @@ -0,0 +1,8 @@ +# Ensure to install before running +from fastapi import FastAPI + +app = FastAPI() + +@app.get("/") +def hello_world(): + return {"Hello": "World"} diff --git a/{{cookiecutter.repository}}/tasks/docker.just b/{{cookiecutter.repository}}/tasks/docker.just index 432ea70..d9d7440 100644 --- a/{{cookiecutter.repository}}/tasks/docker.just +++ b/{{cookiecutter.repository}}/tasks/docker.just @@ -10,7 +10,7 @@ docker-build tag="latest": package-build # start docker compose [group('docker')] docker-compose: - docker compose up + docker compose up --build # run latest docker image [group('docker')] diff --git a/{{cookiecutter.repository}}/tasks/mlflow.just b/{{cookiecutter.repository}}/tasks/mlflow.just deleted file mode 100644 index b7f584d..0000000 --- a/{{cookiecutter.repository}}/tasks/mlflow.just +++ /dev/null @@ -1,13 +0,0 @@ -# run mlflow tasks -[group('mlflow')] -mlflow: mlflow-doctor mlflow-serve - -# run mlflow doctor -[group('mlflow')] -mlflow-doctor: - uv run mlflow doctor - -# start mlflow server -[group('mlflow')] -mlflow-serve host="127.0.0.1" port="5000" uri="./mlruns": - {% raw %}uv run mlflow server --host={{host}} --port={{port}} --backend-store-uri={{uri}}{% endraw %} From 39b87de5022c279fb0b30314fc251e7b59dfa8f8 Mon Sep 17 00:00:00 2001 From: Irving Rodriguez Date: Fri, 25 Apr 2025 15:15:13 -0700 Subject: [PATCH 4/6] adjust files --- .python-version | 1 + CHANGELOG.md | 0 2 files changed, 1 insertion(+) create mode 100644 .python-version delete mode 100644 CHANGELOG.md diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index e69de29..0000000 From fa668de5c2ea408c0f9646f602bc3febe3259b99 Mon Sep 17 00:00:00 2001 From: Irving Rodriguez Date: Fri, 25 Apr 2025 15:30:40 -0700 Subject: [PATCH 5/6] test fixes --- tests/test_cookiecutter.py | 2 +- {{cookiecutter.repository}}/justfile | 1 - {{cookiecutter.repository}}/pyproject.toml | 4 ++-- {{cookiecutter.repository}}/src/example_app/main.py | 4 ++-- {{cookiecutter.repository}}/tasks/project.just | 7 +------ 5 files changed, 6 insertions(+), 12 deletions(-) diff --git a/tests/test_cookiecutter.py b/tests/test_cookiecutter.py index 6a44364..4d7111c 100644 --- a/tests/test_cookiecutter.py +++ b/tests/test_cookiecutter.py @@ -28,7 +28,7 @@ def test_project_generation(cookies: Cookies) -> None: context = { "user": "tester", "name": "MLOps 123", - "license": "apache-2", + "license": "Apache-2.0", # Note: needs to be a "valid SPDX identifier" "version": "1.0.0", "description": "A test project.", "python_version": "3.13", diff --git a/{{cookiecutter.repository}}/justfile b/{{cookiecutter.repository}}/justfile index 0fdd396..e274338 100644 --- a/{{cookiecutter.repository}}/justfile +++ b/{{cookiecutter.repository}}/justfile @@ -33,6 +33,5 @@ import 'tasks/doc.just' import 'tasks/docker.just' import 'tasks/format.just' import 'tasks/install.just' -import 'tasks/mlflow.just' import 'tasks/package.just' import 'tasks/project.just' diff --git a/{{cookiecutter.repository}}/pyproject.toml b/{{cookiecutter.repository}}/pyproject.toml index 623fa56..5255155 100644 --- a/{{cookiecutter.repository}}/pyproject.toml +++ b/{{cookiecutter.repository}}/pyproject.toml @@ -26,6 +26,7 @@ Repository = "https://github.com/{{cookiecutter.user}}/{{cookiecutter.repository # SCRIPTS [project.scripts] +{{cookiecutter.repository}} = "{{cookiecutter.package}}.scripts:main" # DEPENDENCIES @@ -69,9 +70,8 @@ pretty = true python_version = "3.13" check_untyped_defs = true ignore_missing_imports = true -strict = true +strict = false disable_error_code = "import-untyped" -src = ["src"] [tool.pytest.ini_options] addopts = "--verbosity=2" diff --git a/{{cookiecutter.repository}}/src/example_app/main.py b/{{cookiecutter.repository}}/src/example_app/main.py index 5651f9e..baf17c8 100644 --- a/{{cookiecutter.repository}}/src/example_app/main.py +++ b/{{cookiecutter.repository}}/src/example_app/main.py @@ -3,6 +3,6 @@ app = FastAPI() -@app.get("/") -def hello_world(): +@app.get("/") # type: ignore +def hello_world() -> dict[str, str]: return {"Hello": "World"} diff --git a/{{cookiecutter.repository}}/tasks/project.just b/{{cookiecutter.repository}}/tasks/project.just index 318dc74..76f278c 100644 --- a/{{cookiecutter.repository}}/tasks/project.just +++ b/{{cookiecutter.repository}}/tasks/project.just @@ -1,14 +1,9 @@ # run project tasks [group('project')] -project: project-requirements (project-run "tuning") (project-run "training") (project-run "inference") +project: project-requirements # export requirements file [group('project')] project-requirements: uv export --format=requirements-txt --no-dev --no-hashes \ --no-editable --no-emit-project --output-file=requirements.txt - -# run project job with a config -[group('project')] -project-run job: - {% raw %}uv run {{REPOSITORY}} confs/{job}.yaml{% endraw %} From 5907fb1afffa813573eb7c13e83d7e8222f153c4 Mon Sep 17 00:00:00 2001 From: Irving Rodriguez Date: Fri, 25 Apr 2025 15:41:10 -0700 Subject: [PATCH 6/6] tests pass --- {{cookiecutter.repository}}/Dockerfile | 4 +++- {{cookiecutter.repository}}/pyproject.toml | 3 ++- {{cookiecutter.repository}}/src/example_app/main.py | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/{{cookiecutter.repository}}/Dockerfile b/{{cookiecutter.repository}}/Dockerfile index 929c2e4..8059e6f 100644 --- a/{{cookiecutter.repository}}/Dockerfile +++ b/{{cookiecutter.repository}}/Dockerfile @@ -9,4 +9,6 @@ COPY ./src /app ENV PYTHONPATH="${PYTHONPATH}:/app" WORKDIR /app -CMD ["uvicorn", "example_app.main:app", "--host", "0.0.0.0"] +CMD ["{{cookiecutter.repository}}", "--help"] +# Example command +# CMD ["uvicorn", "example_app.main:app", "--host", "0.0.0.0"] diff --git a/{{cookiecutter.repository}}/pyproject.toml b/{{cookiecutter.repository}}/pyproject.toml index 5255155..97cbc34 100644 --- a/{{cookiecutter.repository}}/pyproject.toml +++ b/{{cookiecutter.repository}}/pyproject.toml @@ -71,7 +71,8 @@ python_version = "3.13" check_untyped_defs = true ignore_missing_imports = true strict = false -disable_error_code = "import-untyped" +disable_error_code = ["import-untyped", "disallow-untyped-decorators"] +# See https://mypy.readthedocs.io/en/stable/config_file.html#untyped-definitions-and-calls [tool.pytest.ini_options] addopts = "--verbosity=2" diff --git a/{{cookiecutter.repository}}/src/example_app/main.py b/{{cookiecutter.repository}}/src/example_app/main.py index baf17c8..41bbc5b 100644 --- a/{{cookiecutter.repository}}/src/example_app/main.py +++ b/{{cookiecutter.repository}}/src/example_app/main.py @@ -3,6 +3,6 @@ app = FastAPI() -@app.get("/") # type: ignore +@app.get("/") def hello_world() -> dict[str, str]: return {"Hello": "World"}