From c9f5d67313b075daf3074a34c64537911c982c7e Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Tue, 6 Jun 2023 10:24:44 -0400 Subject: [PATCH 01/68] modified cookiecutter to fit into baseline app development needs --- README.md | 2 +- cookiecutter.json | 20 ++++++++-------- .../.github/pull_request_tempalte.md | 10 ++++++++ .../.github/workflows/teamwork.yml | 24 +++++++++++++++++++ .../pyproject.toml | 6 ++--- 5 files changed, 48 insertions(+), 14 deletions(-) create mode 100644 {{ cookiecutter.__package_name_kebab_case }}/.github/pull_request_tempalte.md create mode 100644 {{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml diff --git a/README.md b/README.md index 10a01cdd..9336af50 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ To create a new Python project with this template: 2. Create a new repository and clone it locally. 3. In the directory that contains the cloned repository, run: ```sh - cruft create -f https://github.com/radix-ai/poetry-cookiecutter + cruft create -f https://github.com/Baseline-quebec/baseline-app-cookiecutter ``` 4. _Optional:_ if your repository name differs from your project's slugified name, you will need to copy the scaffolded project into the repository with: ```sh diff --git a/cookiecutter.json b/cookiecutter.json index 7e68f6bc..a1923cbe 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -4,17 +4,17 @@ "package_url": "https://github.com/user/my-package", "author_name": "John Smith", "author_email": "john@example.com", - "python_version": "3.8", - "development_environment": ["simple", "strict"], - "with_conventional_commits": "{% if cookiecutter.development_environment == 'simple' %}0{% else %}1{% endif %}", - "with_fastapi_api": "0", + "python_version": "3.11", + "development_environment": ["strict"], + "with_conventional_commits": "1", + "with_fastapi_api": "1", "with_jupyter_lab": "0", - "with_pydantic_typing": "0", - "with_sentry_logging": "0", - "with_streamlit_app": "0", - "with_typer_cli": "0", - "continuous_integration": ["GitHub", "GitLab"], - "docstring_style": ["NumPy", "Google"], + "with_pydantic_typing": "1", + "with_sentry_logging": "1", + "with_streamlit_app": "1", + "with_typer_cli": "1", + "continuous_integration": ["GitHub"], + "docstring_style": ["NumPy"], "private_package_repository_name": "", "private_package_repository_url": "", "__package_name_kebab_case": "{{ cookiecutter.package_name|slugify }}", diff --git a/{{ cookiecutter.__package_name_kebab_case }}/.github/pull_request_tempalte.md b/{{ cookiecutter.__package_name_kebab_case }}/.github/pull_request_tempalte.md new file mode 100644 index 00000000..8eb00817 --- /dev/null +++ b/{{ cookiecutter.__package_name_kebab_case }}/.github/pull_request_tempalte.md @@ -0,0 +1,10 @@ +[:]() + +## Describe your changes + + +## Checklist before requesting a review +- [ ] I have performed a self-review of my code. +- [ ] If it is a core feature, I have added thorough tests. +- [ ] I have made corresponding changes to the documentation. +- [ ] I have bumped the version if needed. \ No newline at end of file diff --git a/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml b/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml new file mode 100644 index 00000000..4ab3fd09 --- /dev/null +++ b/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml @@ -0,0 +1,24 @@ +name: teamwork + +on: + pull_request: + types: [opened, closed] + pull_request_review: + types: [submitted] + +jobs: + teamwork-sync: + runs-on: ubuntu-latest + name: Teamwork Sync + steps: + - uses: teamwork/github-sync@master + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TEAMWORK_URI: ${{ secrets.TEAMWORK_URI }} + TEAMWORK_API_TOKEN: ${{ secrets.TEAMWORK_API_TOKEN }} + AUTOMATIC_TAGGING: false + BOARD_COLUMN_OPENED: 'PR Open' + BOARD_COLUMN_MERGED: 'Ready to Test' + BOARD_COLUMN_CLOSED: 'Rejected' + env: + IGNORE_PROJECT_IDS: '1 2 3' \ No newline at end of file diff --git a/{{ cookiecutter.__package_name_kebab_case }}/pyproject.toml b/{{ cookiecutter.__package_name_kebab_case }}/pyproject.toml index e5679f27..d9193576 100644 --- a/{{ cookiecutter.__package_name_kebab_case }}/pyproject.toml +++ b/{{ cookiecutter.__package_name_kebab_case }}/pyproject.toml @@ -90,7 +90,7 @@ url = "{{ cookiecutter.private_package_repository_url }}" {%- endif %} [tool.black] # https://black.readthedocs.io/en/stable/usage_and_configuration/the_basics.html#configuration-via-a-file -line-length = 100 +line-length = 99 target-version = ["py{{ cookiecutter.python_version.split('.')[:2]|join }}"] [tool.coverage.report] # https://coverage.readthedocs.io/en/latest/config.html#report @@ -146,7 +146,7 @@ xfail_strict = true [tool.ruff] # https://github.com/charliermarsh/ruff fix = true ignore-init-module-imports = true -line-length = 100 +line-length = 99 {%- if cookiecutter.development_environment == "strict" %} select = ["A", "ASYNC", "B", "BLE", "C4", "C90", "D", "DTZ", "E", "EM", "ERA", "F", "FLY", "G", "I", "ICN", "INP", "ISC", "N", "NPY", "PGH", "PIE", "PLC", "PLE", "PLR", "PLW", "PT", "PTH", "PYI", "RET", "RSE", "RUF", "S", "SIM", "SLF", "T10", "T20", "TCH", "TID", "TRY", "UP", "W", "YTT"] ignore = ["E501", "PGH001", "RET504", "S101"] @@ -164,7 +164,7 @@ ban-relative-imports = "all" {%- if cookiecutter.development_environment == "strict" %} [tool.ruff.pycodestyle] -max-doc-length = 100 +max-doc-length = 99 {%- endif %} [tool.ruff.pydocstyle] From e17be1c28bcae9d2ec191cea251a3173aef3ef38 Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Tue, 6 Jun 2023 10:27:35 -0400 Subject: [PATCH 02/68] updated README --- README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 9336af50..d04ce38e 100644 --- a/README.md +++ b/README.md @@ -55,22 +55,22 @@ To update your Python project with the latest template: ## πŸ€“ Template parameters | Parameter | Description | -| ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +|-------------------------------------------------------------------------------------| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `package_name`
"Spline Reticulator" | The name of the package. Will be slugified to `snake_case` for importing and `kebab-case` for installing. | | `package_description`
"A Python package that reticulates splines." | A single-line description of the package. | | `package_url`
"https://github.com/user/spline-reticulator" | The URL to the package's repository. | | `author_name`
"John Smith" | The full name of the primary author of the package. | | `author_email`
"john@example.com" | The email address of the primary author of the package. | -| `python_version`
"3.8" | The minimum Python version that the package requires. | -| `development_environment`
["simple", "strict"] | Whether to configure the development environment with a focus on simplicity or with a focus on strictness. In strict mode, additional [Ruff rules](https://beta.ruff.rs/docs/rules/) are added, and tools such as [Mypy](https://github.com/python/mypy) and [Pytest](https://github.com/pytest-dev/pytest) are set to strict mode. | -| `with_conventional_commits`
["0", "1"] | If "1", [Commitizen](https://github.com/commitizen-tools/commitizen) will verify that your commits follow the [Conventional Commits](https://www.conventionalcommits.org/) standard. In return, `cz bump` may be used to automate [Semantic Versioning](https://semver.org/) and [Keep A Changelog](https://keepachangelog.com/). | -| `with_fastapi_api`
["0", "1"] | If "1", [FastAPI](https://github.com/tiangolo/fastapi) is added as a run time dependency, FastAPI API stubs and tests are added, a `poe api` command for serving the API is added, and an `app` stage that packages the API is added to the Dockerfile. Additionally, the CI workflow will push the application as a Docker image instead of publishing the Python package. | -| `with_jupyter_lab`
["0", "1"] | If "1", [JupyterLab](https://github.com/jupyterlab/jupyterlab) is added to Poetry's dev dependencies, and a `poe lab` command is added to start Jupyter Lab in the `notebooks/` directory. | -| `with_pydantic_typing`
["0", "1"] | If "1", [Pydantic](https://github.com/samuelcolvin/pydantic) is added as a run time dependency, and the [Pydantic mypy plugin](https://pydantic-docs.helpmanual.io/mypy_plugin/) is enabled and configured. | -| `with_sentry_logging`
["0", "1"] | If "1", [Sentry](https://github.com/getsentry/sentry-python) is added as a run time dependency, and a Sentry configuration stub and tests are added. | -| `with_streamlit_app`
["0", "1"] | If "1", [Streamlit](https://github.com/streamlit/streamlit) is added as a run time dependency, a Streamlit application stub is added, a `poe app` command to serve the Streamlit app is added, and an `app` stage that packages the Streamlit app is added to the Dockerfile. Additionally, the CI workflow will push the application as a Docker image instead of publishing the Python package. | -| `with_typer_cli`
["0", "1"] | If "1", [Typer](https://github.com/tiangolo/typer) is added as a run time dependency, Typer CLI stubs and tests are added, the package itself is registered as a CLI, and an `app` stage is added to the Dockerfile that packages the CLI. | -| `continuous_integration`
["GitHub", "GitLab"] | Whether to include a [GitHub Actions](https://docs.github.com/en/actions) or a [GitLab CI/CD](https://docs.gitlab.com/ee/ci/) continuous integration workflow for testing and publishing the package or app. | -| `docstring_style`
["NumPy", "Google"] | Whether to use and validate [NumPy-style](https://numpydoc.readthedocs.io/en/latest/format.html) or [Google-style docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings). | +| `python_version`
"3.11" | The minimum Python version that the package requires. | +| `development_environment`
"strict" | In strict mode, additional [Ruff rules](https://beta.ruff.rs/docs/rules/) are added, and tools such as [Mypy](https://github.com/python/mypy) and [Pytest](https://github.com/pytest-dev/pytest) are set to strict mode. | +| `with_conventional_commits`
"1" | If "1", [Commitizen](https://github.com/commitizen-tools/commitizen) will verify that your commits follow the [Conventional Commits](https://www.conventionalcommits.org/) standard. In return, `cz bump` may be used to automate [Semantic Versioning](https://semver.org/) and [Keep A Changelog](https://keepachangelog.com/). | +| `with_fastapi_api`
"1" | If "1", [FastAPI](https://github.com/tiangolo/fastapi) is added as a run time dependency, FastAPI API stubs and tests are added, a `poe api` command for serving the API is added, and an `app` stage that packages the API is added to the Dockerfile. Additionally, the CI workflow will push the application as a Docker image instead of publishing the Python package. | +| `with_jupyter_lab`
"0" | If "1", [JupyterLab](https://github.com/jupyterlab/jupyterlab) is added to Poetry's dev dependencies, and a `poe lab` command is added to start Jupyter Lab in the `notebooks/` directory. | +| `with_pydantic_typing`
"1" | If "1", [Pydantic](https://github.com/samuelcolvin/pydantic) is added as a run time dependency, and the [Pydantic mypy plugin](https://pydantic-docs.helpmanual.io/mypy_plugin/) is enabled and configured. | +| `with_sentry_logging`
"1" | If "1", [Sentry](https://github.com/getsentry/sentry-python) is added as a run time dependency, and a Sentry configuration stub and tests are added. | +| `with_streamlit_app`
"1" | If "1", [Streamlit](https://github.com/streamlit/streamlit) is added as a run time dependency, a Streamlit application stub is added, a `poe app` command to serve the Streamlit app is added, and an `app` stage that packages the Streamlit app is added to the Dockerfile. Additionally, the CI workflow will push the application as a Docker image instead of publishing the Python package. | +| `with_typer_cli`
"1" | If "1", [Typer](https://github.com/tiangolo/typer) is added as a run time dependency, Typer CLI stubs and tests are added, the package itself is registered as a CLI, and an `app` stage is added to the Dockerfile that packages the CLI. | +| `continuous_integration`
"GitHub" | Whether to include a [GitHub Actions](https://docs.github.com/en/actions) or a [GitLab CI/CD](https://docs.gitlab.com/ee/ci/) continuous integration workflow for testing and publishing the package or app. | +| `docstring_style`
"NumPy" | Whether to use and validate [NumPy-style](https://numpydoc.readthedocs.io/en/latest/format.html) or [Google-style docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings). | | `private_package_repository_name`
"Private Package Repository" | Optional name of a private package repository to install packages from and publish this package to. | | `private_package_repository_url`
"https://pypi.example.com/simple" | Optional URL of a private package repository to install packages from and publish this package to. Make sure to include the `/simple` suffix. For instance, when using a GitLab Package Registry this value should be of the form `https://gitlab.com/api/v4/projects/` `{project_id}` `/packages/pypi/simple`. | From 6ec31c85154cd7cb00358a5cbb0043ac462ea37c Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Tue, 6 Jun 2023 10:43:13 -0400 Subject: [PATCH 03/68] updated readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d04ce38e..826ce04e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/radix-ai/poetry-cookiecutter) [![Open in GitHub Codespaces](https://img.shields.io/static/v1?label=GitHub%20Codespaces&message=Open&color=blue&logo=github)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=444870763) -# Poetry Cookiecutter +# Baseline app Cookiecutter A modern [Cookiecutter](https://github.com/cookiecutter/cookiecutter) template for scaffolding Python packages and apps. From a3f233bf701832ff229ae7f8264881610574a210 Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur <46036275+GameSetAndMatch@users.noreply.github.com> Date: Tue, 6 Jun 2023 11:03:36 -0400 Subject: [PATCH 04/68] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 826ce04e..ec06c37a 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Starting development in My Package can be done with a single click by [opening M - πŸ§ͺ Test coverage with [Coverage.py](https://github.com/nedbat/coveragepy) - πŸ— Scaffolding updates with [Cookiecutter](https://github.com/cookiecutter/cookiecutter) and [Cruft](https://github.com/cruft/cruft) - 🧰 Dependency updates with [Dependabot](https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/about-dependabot-version-updates) +- 🀹 Task management made easy with references to PR and Issues with [Teamwork](https://baseline6.teamwork.com/app/home/activity?from=homepage) ## ✨ Using From 64c8766af640994f798a817119b3999e4edc490e Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Tue, 6 Jun 2023 11:04:44 -0400 Subject: [PATCH 05/68] added teamwork secrets as strings --- .../.github/workflows/teamwork.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml b/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml index 4ab3fd09..9644a140 100644 --- a/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml +++ b/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml @@ -13,9 +13,9 @@ jobs: steps: - uses: teamwork/github-sync@master with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TEAMWORK_URI: ${{ secrets.TEAMWORK_URI }} - TEAMWORK_API_TOKEN: ${{ secrets.TEAMWORK_API_TOKEN }} + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + TEAMWORK_URI: "${{ secrets.TEAMWORK_URI }}" + TEAMWORK_API_TOKEN: "${{ secrets.TEAMWORK_API_TOKEN }}" AUTOMATIC_TAGGING: false BOARD_COLUMN_OPENED: 'PR Open' BOARD_COLUMN_MERGED: 'Ready to Test' From 71c40a793901974628d3d880bce606520df26c3e Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Tue, 6 Jun 2023 11:10:12 -0400 Subject: [PATCH 06/68] commented teamwork integration --- .../.github/workflows/teamwork.yml | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml b/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml index 9644a140..5a00a1cf 100644 --- a/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml +++ b/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml @@ -1,24 +1,24 @@ -name: teamwork - -on: - pull_request: - types: [opened, closed] - pull_request_review: - types: [submitted] - -jobs: - teamwork-sync: - runs-on: ubuntu-latest - name: Teamwork Sync - steps: - - uses: teamwork/github-sync@master - with: - GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" - TEAMWORK_URI: "${{ secrets.TEAMWORK_URI }}" - TEAMWORK_API_TOKEN: "${{ secrets.TEAMWORK_API_TOKEN }}" - AUTOMATIC_TAGGING: false - BOARD_COLUMN_OPENED: 'PR Open' - BOARD_COLUMN_MERGED: 'Ready to Test' - BOARD_COLUMN_CLOSED: 'Rejected' - env: - IGNORE_PROJECT_IDS: '1 2 3' \ No newline at end of file +#name: teamwork +# +#on: +# pull_request: +# types: [opened, closed] +# pull_request_review: +# types: [submitted] +# +#jobs: +# teamwork-sync: +# runs-on: ubuntu-latest +# name: Teamwork Sync +# steps: +# - uses: teamwork/github-sync@master +# with: +# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +# TEAMWORK_URI: ${{ secrets.TEAMWORK_URI }} +# TEAMWORK_API_TOKEN: ${{ secrets.TEAMWORK_API_TOKEN }} +# AUTOMATIC_TAGGING: false +# BOARD_COLUMN_OPENED: 'PR Open' +# BOARD_COLUMN_MERGED: 'Ready to Test' +# BOARD_COLUMN_CLOSED: 'Rejected' +# env: +# IGNORE_PROJECT_IDS: '1 2 3' \ No newline at end of file From 2906c063e0b31080bdf156ace72c1a0290412f43 Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Tue, 6 Jun 2023 11:14:22 -0400 Subject: [PATCH 07/68] added teamwork back to cookie cutter --- .../.github/workflows/teamwork.yml | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml b/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml index 5a00a1cf..2501b038 100644 --- a/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml +++ b/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml @@ -2,23 +2,23 @@ # #on: # pull_request: -# types: [opened, closed] -# pull_request_review: -# types: [submitted] -# -#jobs: -# teamwork-sync: -# runs-on: ubuntu-latest -# name: Teamwork Sync -# steps: -# - uses: teamwork/github-sync@master -# with: -# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -# TEAMWORK_URI: ${{ secrets.TEAMWORK_URI }} -# TEAMWORK_API_TOKEN: ${{ secrets.TEAMWORK_API_TOKEN }} -# AUTOMATIC_TAGGING: false -# BOARD_COLUMN_OPENED: 'PR Open' -# BOARD_COLUMN_MERGED: 'Ready to Test' -# BOARD_COLUMN_CLOSED: 'Rejected' -# env: -# IGNORE_PROJECT_IDS: '1 2 3' \ No newline at end of file + types: [opened, closed] + pull_request_review: + types: [submitted] + +jobs: + teamwork-sync: + runs-on: ubuntu-latest + name: Teamwork Sync + steps: + - uses: teamwork/github-sync@master + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TEAMWORK_URI: ${{ secrets.TEAMWORK_URI }} + TEAMWORK_API_TOKEN: ${{ secrets.TEAMWORK_API_TOKEN }} + AUTOMATIC_TAGGING: false + BOARD_COLUMN_OPENED: 'PR Open' + BOARD_COLUMN_MERGED: 'Ready to Test' + BOARD_COLUMN_CLOSED: 'Rejected' + env: + IGNORE_PROJECT_IDS: '1 2 3' \ No newline at end of file From ea0b2c01d42acc4f24f731bf94c9acbeb74d74fa Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Wed, 7 Jun 2023 09:39:12 -0400 Subject: [PATCH 08/68] commented secrets --- .../.github/workflows/teamwork.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml b/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml index 2501b038..2d5d1aba 100644 --- a/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml +++ b/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml @@ -1,7 +1,7 @@ -#name: teamwork -# -#on: -# pull_request: +name: teamwork + +on: + pull_request: types: [opened, closed] pull_request_review: types: [submitted] @@ -13,9 +13,9 @@ jobs: steps: - uses: teamwork/github-sync@master with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TEAMWORK_URI: ${{ secrets.TEAMWORK_URI }} - TEAMWORK_API_TOKEN: ${{ secrets.TEAMWORK_API_TOKEN }} + #GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + #TEAMWORK_URI: ${{ secrets.TEAMWORK_URI }} + #TEAMWORK_API_TOKEN: ${{ secrets.TEAMWORK_API_TOKEN }} AUTOMATIC_TAGGING: false BOARD_COLUMN_OPENED: 'PR Open' BOARD_COLUMN_MERGED: 'Ready to Test' From 582a6f5076288228b812f24023944d12ecde9e3f Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Wed, 7 Jun 2023 09:42:23 -0400 Subject: [PATCH 09/68] added token as string --- .../.github/workflows/teamwork.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml b/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml index 2d5d1aba..57796d06 100644 --- a/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml +++ b/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml @@ -1,3 +1,7 @@ +#${{ secrets.GITHUB_TOKEN }} +#${{ secrets.TEAMWORK_URI }} +#${{ secrets.TEAMWORK_API_TOKEN }} + name: teamwork on: @@ -13,9 +17,9 @@ jobs: steps: - uses: teamwork/github-sync@master with: - #GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - #TEAMWORK_URI: ${{ secrets.TEAMWORK_URI }} - #TEAMWORK_API_TOKEN: ${{ secrets.TEAMWORK_API_TOKEN }} + GITHUB_TOKEN: "GITHUB_TOKEN" + TEAMWORK_URI: "TEAMWORK_URI" + TEAMWORK_API_TOKEN: "TEAMWORK_API_TOKEN" AUTOMATIC_TAGGING: false BOARD_COLUMN_OPENED: 'PR Open' BOARD_COLUMN_MERGED: 'Ready to Test' From e44ed07385db632b35be918bbec3c848d865e539 Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Wed, 7 Jun 2023 09:44:58 -0400 Subject: [PATCH 10/68] removed secrets --- .../.github/teamwork_secrets.txt | 0 .../.github/workflows/teamwork.yml | 4 ---- 2 files changed, 4 deletions(-) create mode 100644 {{ cookiecutter.__package_name_kebab_case }}/.github/teamwork_secrets.txt diff --git a/{{ cookiecutter.__package_name_kebab_case }}/.github/teamwork_secrets.txt b/{{ cookiecutter.__package_name_kebab_case }}/.github/teamwork_secrets.txt new file mode 100644 index 00000000..e69de29b diff --git a/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml b/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml index 57796d06..93461ead 100644 --- a/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml +++ b/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml @@ -1,7 +1,3 @@ -#${{ secrets.GITHUB_TOKEN }} -#${{ secrets.TEAMWORK_URI }} -#${{ secrets.TEAMWORK_API_TOKEN }} - name: teamwork on: From 03024902b7a7f39b4ea46614d119f81cbb4cc6f8 Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Wed, 7 Jun 2023 09:53:17 -0400 Subject: [PATCH 11/68] test secrets github --- .../{pull_request_tempalte.md => pull_request_template.md} | 0 .../.github/teamwork_secrets.txt | 1 + .../.github/workflows/teamwork.yml | 2 +- 3 files changed, 2 insertions(+), 1 deletion(-) rename {{ cookiecutter.__package_name_kebab_case }}/.github/{pull_request_tempalte.md => pull_request_template.md} (100%) diff --git a/{{ cookiecutter.__package_name_kebab_case }}/.github/pull_request_tempalte.md b/{{ cookiecutter.__package_name_kebab_case }}/.github/pull_request_template.md similarity index 100% rename from {{ cookiecutter.__package_name_kebab_case }}/.github/pull_request_tempalte.md rename to {{ cookiecutter.__package_name_kebab_case }}/.github/pull_request_template.md diff --git a/{{ cookiecutter.__package_name_kebab_case }}/.github/teamwork_secrets.txt b/{{ cookiecutter.__package_name_kebab_case }}/.github/teamwork_secrets.txt index e69de29b..10c28801 100644 --- a/{{ cookiecutter.__package_name_kebab_case }}/.github/teamwork_secrets.txt +++ b/{{ cookiecutter.__package_name_kebab_case }}/.github/teamwork_secrets.txt @@ -0,0 +1 @@ +Wow ! trΓ¨s bonne nouvelle, j'ai hΓ’te de travailler avec toi @Alex Dube \ No newline at end of file diff --git a/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml b/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml index 93461ead..fcca99c1 100644 --- a/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml +++ b/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml @@ -13,7 +13,7 @@ jobs: steps: - uses: teamwork/github-sync@master with: - GITHUB_TOKEN: "GITHUB_TOKEN" + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TEAMWORK_URI: "TEAMWORK_URI" TEAMWORK_API_TOKEN: "TEAMWORK_API_TOKEN" AUTOMATIC_TAGGING: false From 36c58c0735928431e60bd5d6f38a851e0c8a6c7e Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Wed, 7 Jun 2023 10:00:11 -0400 Subject: [PATCH 12/68] added secret variables in a txt file for teamwork --- .../.github/workflows/teamwork.yml | 2 +- .../teamwork_secret_variables.txt | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 {{ cookiecutter.__package_name_kebab_case }}/teamwork_secret_variables.txt diff --git a/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml b/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml index fcca99c1..93461ead 100644 --- a/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml +++ b/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml @@ -13,7 +13,7 @@ jobs: steps: - uses: teamwork/github-sync@master with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: "GITHUB_TOKEN" TEAMWORK_URI: "TEAMWORK_URI" TEAMWORK_API_TOKEN: "TEAMWORK_API_TOKEN" AUTOMATIC_TAGGING: false diff --git a/{{ cookiecutter.__package_name_kebab_case }}/teamwork_secret_variables.txt b/{{ cookiecutter.__package_name_kebab_case }}/teamwork_secret_variables.txt new file mode 100644 index 00000000..aa4d8f42 --- /dev/null +++ b/{{ cookiecutter.__package_name_kebab_case }}/teamwork_secret_variables.txt @@ -0,0 +1,3 @@ +#GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" +#TEAMWORK_URI: "${{ secrets.TEAMWORK_URI }}" +#TEAMWORK_API_TOKEN: "${{ secrets.TEAMWORK_API_TOKEN }}" \ No newline at end of file From 693b5349a6bad2c8405c4b444ebc177193ee00b7 Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Wed, 7 Jun 2023 10:05:10 -0400 Subject: [PATCH 13/68] removed variables and added documentation on how to set the variables --- .../.github/teamwork_secrets.txt | 1 - .../.github/workflows/teamwork.yml | 8 +++++--- 2 files changed, 5 insertions(+), 4 deletions(-) delete mode 100644 {{ cookiecutter.__package_name_kebab_case }}/.github/teamwork_secrets.txt diff --git a/{{ cookiecutter.__package_name_kebab_case }}/.github/teamwork_secrets.txt b/{{ cookiecutter.__package_name_kebab_case }}/.github/teamwork_secrets.txt deleted file mode 100644 index 10c28801..00000000 --- a/{{ cookiecutter.__package_name_kebab_case }}/.github/teamwork_secrets.txt +++ /dev/null @@ -1 +0,0 @@ -Wow ! trΓ¨s bonne nouvelle, j'ai hΓ’te de travailler avec toi @Alex Dube \ No newline at end of file diff --git a/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml b/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml index 93461ead..3f7ee09f 100644 --- a/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml +++ b/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml @@ -1,3 +1,5 @@ +# Follow this link for documentation on how to set the secrets needed: https://github.com/Teamwork/github-sync + name: teamwork on: @@ -13,9 +15,9 @@ jobs: steps: - uses: teamwork/github-sync@master with: - GITHUB_TOKEN: "GITHUB_TOKEN" - TEAMWORK_URI: "TEAMWORK_URI" - TEAMWORK_API_TOKEN: "TEAMWORK_API_TOKEN" + GITHUB_TOKEN: "YOUR SECRET GITHUB_TOKEN" + TEAMWORK_URI: "YOUR SECRET TEAMWORK_URI" + TEAMWORK_API_TOKEN: "YOUR SECRET TEAMWORK_API_TOKEN" AUTOMATIC_TAGGING: false BOARD_COLUMN_OPENED: 'PR Open' BOARD_COLUMN_MERGED: 'Ready to Test' From 9630607ff16a14154f6a2e8ec39c691297019a92 Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Wed, 7 Jun 2023 10:05:56 -0400 Subject: [PATCH 14/68] removed variables file --- .../teamwork_secret_variables.txt | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 {{ cookiecutter.__package_name_kebab_case }}/teamwork_secret_variables.txt diff --git a/{{ cookiecutter.__package_name_kebab_case }}/teamwork_secret_variables.txt b/{{ cookiecutter.__package_name_kebab_case }}/teamwork_secret_variables.txt deleted file mode 100644 index aa4d8f42..00000000 --- a/{{ cookiecutter.__package_name_kebab_case }}/teamwork_secret_variables.txt +++ /dev/null @@ -1,3 +0,0 @@ -#GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" -#TEAMWORK_URI: "${{ secrets.TEAMWORK_URI }}" -#TEAMWORK_API_TOKEN: "${{ secrets.TEAMWORK_API_TOKEN }}" \ No newline at end of file From 3a31b0a75945a52cb8ab55b572cc5e5b258496de Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Wed, 7 Jun 2023 10:12:27 -0400 Subject: [PATCH 15/68] added teamwork step in the README.md --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ec06c37a..0f2ee7f7 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,15 @@ To create a new Python project with this template: ```sh cruft create -f https://github.com/Baseline-quebec/baseline-app-cookiecutter ``` -4. _Optional:_ if your repository name differs from your project's slugified name, you will need to copy the scaffolded project into the repository with: +4. Add the required Secrets to the Teamwork Integration inside Github Workflow using this documentation: https://github.com/Teamwork/github-sync + +There are the lines to update: +```yaml + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TEAMWORK_URI: ${{ secrets.TEAMWORK_URI }} + TEAMWORK_API_TOKEN: ${{ secrets.TEAMWORK_API_TOKEN }} +``` +5. _Optional:_ if your repository name differs from your project's slugified name, you will need to copy the scaffolded project into the repository with: ```sh cp -r {package-name}/ {repository-name}/ ``` From 70d5778be45d5de43aaf6470f6daa33c2d63da27 Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Fri, 28 Jul 2023 15:38:07 -0400 Subject: [PATCH 16/68] moved dependabot to increase instead --- README.md | 7 +++---- .../.github/dependabot.yml | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0f2ee7f7..a3e3e324 100644 --- a/README.md +++ b/README.md @@ -37,12 +37,11 @@ To create a new Python project with this template: ```sh pip install --upgrade cruft>=2.12.0 cookiecutter>=2.1.1 ``` -2. Create a new repository and clone it locally. -3. In the directory that contains the cloned repository, run: +2. In the directory that contains the cloned repository, run: ```sh cruft create -f https://github.com/Baseline-quebec/baseline-app-cookiecutter ``` -4. Add the required Secrets to the Teamwork Integration inside Github Workflow using this documentation: https://github.com/Teamwork/github-sync +3. _Optional:_ Add the required Secrets to the Teamwork Integration inside Github Workflow using this documentation: https://github.com/Teamwork/github-sync There are the lines to update: ```yaml @@ -50,7 +49,7 @@ There are the lines to update: TEAMWORK_URI: ${{ secrets.TEAMWORK_URI }} TEAMWORK_API_TOKEN: ${{ secrets.TEAMWORK_API_TOKEN }} ``` -5. _Optional:_ if your repository name differs from your project's slugified name, you will need to copy the scaffolded project into the repository with: +4. _Optional:_ if your repository name differs from your project's slugified name, you will need to copy the scaffolded project into the repository with: ```sh cp -r {package-name}/ {repository-name}/ ``` diff --git a/{{ cookiecutter.__package_name_kebab_case }}/.github/dependabot.yml b/{{ cookiecutter.__package_name_kebab_case }}/.github/dependabot.yml index e98ee6ed..7ce8c74c 100644 --- a/{{ cookiecutter.__package_name_kebab_case }}/.github/dependabot.yml +++ b/{{ cookiecutter.__package_name_kebab_case }}/.github/dependabot.yml @@ -17,6 +17,6 @@ updates: prefix: "build" prefix-development: "build" include: "scope" - versioning-strategy: lockfile-only + versioning-strategy: increase allow: - dependency-type: "all" From 5544a2e66c6b6d727a8c6186e962f735a3f43d6b Mon Sep 17 00:00:00 2001 From: Jean-Samuel Leboeuf Date: Thu, 7 Sep 2023 18:46:56 -0400 Subject: [PATCH 17/68] Update precommit and customize linter rules (#1) --- .../.pre-commit-config.yaml | 19 ++++++------------- .../pyproject.toml | 6 ++++-- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/{{ cookiecutter.__package_name_kebab_case }}/.pre-commit-config.yaml b/{{ cookiecutter.__package_name_kebab_case }}/.pre-commit-config.yaml index e9c786c0..98ddc680 100644 --- a/{{ cookiecutter.__package_name_kebab_case }}/.pre-commit-config.yaml +++ b/{{ cookiecutter.__package_name_kebab_case }}/.pre-commit-config.yaml @@ -45,6 +45,11 @@ repos: {%- endif %} - id: trailing-whitespace types: [python] + - repo: https://github.com/python-poetry/poetry + rev: '1.6.0' + hooks: + - id: poetry-check + - id: poetry-lock - repo: local hooks: {%- if cookiecutter.with_conventional_commits|int %} @@ -76,21 +81,9 @@ repos: args: [--check-sourced] language: system types: [shell] - {%- endif %} - - id: poetry-check - name: poetry check - entry: poetry check - language: system - files: pyproject.toml - pass_filenames: false - - id: poetry-lock-check - name: poetry lock check - entry: poetry lock - args: [--check] - language: system - pass_filenames: false - id: mypy name: mypy entry: mypy language: system types: [python] + {%- endif %} diff --git a/{{ cookiecutter.__package_name_kebab_case }}/pyproject.toml b/{{ cookiecutter.__package_name_kebab_case }}/pyproject.toml index d9193576..53a9a94e 100644 --- a/{{ cookiecutter.__package_name_kebab_case }}/pyproject.toml +++ b/{{ cookiecutter.__package_name_kebab_case }}/pyproject.toml @@ -78,6 +78,8 @@ typeguard = ">=3.0.2" [tool.poetry.group.dev.dependencies] # https://python-poetry.org/docs/master/managing-dependencies/ cruft = ">=2.14.0" +ipython = ">=8.14.0" +ipykernel = ">=6.25.1" {%- if cookiecutter.with_jupyter_lab|int %} jupyterlab = ">=3.6.3" {%- endif %} @@ -149,11 +151,11 @@ ignore-init-module-imports = true line-length = 99 {%- if cookiecutter.development_environment == "strict" %} select = ["A", "ASYNC", "B", "BLE", "C4", "C90", "D", "DTZ", "E", "EM", "ERA", "F", "FLY", "G", "I", "ICN", "INP", "ISC", "N", "NPY", "PGH", "PIE", "PLC", "PLE", "PLR", "PLW", "PT", "PTH", "PYI", "RET", "RSE", "RUF", "S", "SIM", "SLF", "T10", "T20", "TCH", "TID", "TRY", "UP", "W", "YTT"] -ignore = ["E501", "PGH001", "RET504", "S101"] +ignore = ["E501", "PGH001", "RET504", "S101", "W505", "PTH123"] unfixable = ["ERA001", "F401", "F841", "T201", "T203"] {%- else %} select = ["A", "ASYNC", "B", "C4", "C90", "D", "DTZ", "E", "F", "FLY", "I", "ISC", "N", "NPY", "PGH", "PIE", "PLC", "PLE", "PLR", "PLW", "PT", "RET", "RUF", "RSE", "SIM", "TID", "UP", "W", "YTT"] -ignore = ["E501", "PGH001", "PGH002", "PGH003", "RET504", "S101"] +ignore = ["E501", "PGH001", "PGH002", "PGH003", "RET504", "S101", "W505", "PTH123"] unfixable = ["F401", "F841"] {%- endif %} src = ["src", "tests"] From 6e56c2fd8f6214932a9cb3f60e9b1723de79c1ec Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Fri, 5 Jan 2024 10:20:54 -0500 Subject: [PATCH 18/68] updated values --- README.md | 4 ++-- cookiecutter.json | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index f174a9c5..e867b126 100644 --- a/README.md +++ b/README.md @@ -76,8 +76,8 @@ To update your Python project with the latest template: | `author_email`
"john@example.com" | The email address of the primary author of the package. | | `python_version`
"3.8" | The minimum Python version that the package requires. | | `docker_image`
"python:$PYTHON_VERSION-slim" | The base Docker image to use for the Dev Container and application. The $PYTHON_VERSION build argument is equal to the `python_version` value by default, but may be overridden when building the image to test different Python versions. If CUDA support is required, you may use [radixai/python-gpu:$PYTHON_VERSION-cuda11.8](https://github.com/radix-ai/python-gpu). | -| `development_environment`
["simple", "strict"] | Whether to configure the development environment with a focus on simplicity or with a focus on strictness. In strict mode, additional [Ruff rules](https://beta.ruff.rs/docs/rules/) are added, and tools such as [Mypy](https://github.com/python/mypy) and [Pytest](https://github.com/pytest-dev/pytest) are set to strict mode. | -| `with_conventional_commits`
["0", "1"] | If "1", [Commitizen](https://github.com/commitizen-tools/commitizen) will verify that your commits follow the [Conventional Commits](https://www.conventionalcommits.org/) standard. In return, `cz bump` may be used to automate [Semantic Versioning](https://semver.org/) and [Keep A Changelog](https://keepachangelog.com/). | +| `development_environment`
"strict" | Focus on strictness. In strict mode, [Ruff rules](https://beta.ruff.rs/docs/rules/) are added, and tools such as [Mypy](https://github.com/python/mypy) and [Pytest](https://github.com/pytest-dev/pytest) are set to strict mode. | +| `with_conventional_commits`
"1" | [Commitizen](https://github.com/commitizen-tools/commitizen) will verify that your commits follow the [Conventional Commits](https://www.conventionalcommits.org/) standard. In return, `cz bump` may be used to automate [Semantic Versioning](https://semver.org/) and [Keep A Changelog](https://keepachangelog.com/). | | `with_fastapi_api`
["0", "1"] | If "1", [FastAPI](https://github.com/tiangolo/fastapi) is added as a run time dependency, FastAPI API stubs and tests are added, a `poe api` command for serving the API is added, and an `app` stage that packages the API is added to the Dockerfile. Additionally, the CI workflow will push the application as a Docker image instead of publishing the Python package. | | `with_jupyter_lab`
["0", "1"] | If "1", [JupyterLab](https://github.com/jupyterlab/jupyterlab) is added to Poetry's dev dependencies, and a `poe lab` command is added to start Jupyter Lab in the `notebooks/` directory. | | `with_pydantic_typing`
["0", "1"] | If "1", [Pydantic](https://github.com/samuelcolvin/pydantic) is added as a run time dependency, and the [Pydantic mypy plugin](https://pydantic-docs.helpmanual.io/mypy_plugin/) is enabled and configured. | diff --git a/cookiecutter.json b/cookiecutter.json index da0cb763..ef6ed07a 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -5,17 +5,17 @@ "author_name": "John Smith", "author_email": "john@example.com", "python_version": "3.11", - "with_fastapi_api": "1", "docker_image": "python:$PYTHON_VERSION-slim", - "development_environment": ["simple", "strict"], - "with_conventional_commits": "{% if cookiecutter.development_environment == 'simple' %}0{% else %}1{% endif %}", + "development_environment": "strict", + "with_conventional_commits": "1", + "with_fastapi_api": "1", "with_jupyter_lab": "0", "with_pydantic_typing": "1", "with_sentry_logging": "1", - "with_streamlit_app": "1", - "with_typer_cli": "1", - "continuous_integration": ["GitHub"], - "docstring_style": ["NumPy"], + "with_streamlit_app": "0", + "with_typer_cli": "0", + "continuous_integration": "GitHub", + "docstring_style": "NumPy", "private_package_repository_name": "", "private_package_repository_url": "", "__package_name_kebab_case": "{{ cookiecutter.package_name|slugify }}", From 273a33470a07f9fe18c100f6628025ba6c967174 Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Fri, 5 Jan 2024 10:26:30 -0500 Subject: [PATCH 19/68] removed endif --- .../.pre-commit-config.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/{{ cookiecutter.__package_name_kebab_case }}/.pre-commit-config.yaml b/{{ cookiecutter.__package_name_kebab_case }}/.pre-commit-config.yaml index e07390f3..e4870347 100644 --- a/{{ cookiecutter.__package_name_kebab_case }}/.pre-commit-config.yaml +++ b/{{ cookiecutter.__package_name_kebab_case }}/.pre-commit-config.yaml @@ -89,5 +89,4 @@ repos: name: mypy entry: mypy language: system - types: [python] - {%- endif %} + types: [python] \ No newline at end of file From 5d45043ac340f318d700f6a070cd48e350f805a6 Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Fri, 5 Jan 2024 10:34:57 -0500 Subject: [PATCH 20/68] add raw condition to .github/workflows/teamwork.yml --- .../.github/workflows/teamwork.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml b/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml index 27c9f8df..d0c32315 100644 --- a/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml +++ b/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml @@ -15,9 +15,9 @@ jobs: steps: - uses: teamwork/github-sync@master with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TEAMWORK_URI: ${{ secrets.TEAMWORK_URI }} - TEAMWORK_API_TOKEN: ${{ secrets.TEAMWORK_API_TOKEN }} + GITHUB_TOKEN: "{% raw %}${{ secrets.GITHUB_TOKEN }}{% endraw %}" + TEAMWORK_URI: "{% raw %}${{ secrets.TEAMWORK_URI }}{% endraw %}" + TEAMWORK_API_TOKEN: "{% raw %}${{ secrets.TEAMWORK_API_TOKEN }}{% endraw %}" AUTOMATIC_TAGGING: false BOARD_COLUMN_OPENED: "PR Open" BOARD_COLUMN_MERGED: "Ready to Test" From cedb8fc895548a425d1f272b4008ed88ca23485a Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Fri, 5 Jan 2024 10:36:58 -0500 Subject: [PATCH 21/68] update postgenproject --- hooks/post_gen_project.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py index 396618b0..31efae1c 100644 --- a/hooks/post_gen_project.py +++ b/hooks/post_gen_project.py @@ -36,15 +36,8 @@ os.remove(f"src/{package_name}/cli.py") os.remove("tests/test_cli.py") -# Remove the continuous integration provider that is not selected. -if continuous_integration != "GitHub": - shutil.rmtree(".github/") -elif continuous_integration != "GitLab": - os.remove(".gitlab-ci.yml") - -# Remove unused GitHub Actions workflows. -if continuous_integration == "GitHub": - if not is_deployable_app: - os.remove(".github/workflows/deploy.yml") - if not is_publishable_package: - os.remove(".github/workflows/publish.yml") + +if not is_deployable_app: + os.remove(".github/workflows/deploy.yml") +if not is_publishable_package: + os.remove(".github/workflows/publish.yml") From 127365ba840ddeb1aa908354a7c7d88241a9fd7a Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Fri, 5 Jan 2024 10:37:27 -0500 Subject: [PATCH 22/68] update linting --- hooks/post_gen_project.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py index 31efae1c..abc666ee 100644 --- a/hooks/post_gen_project.py +++ b/hooks/post_gen_project.py @@ -9,8 +9,14 @@ with_streamlit_app = int("{{ cookiecutter.with_streamlit_app }}") with_typer_cli = int("{{ cookiecutter.with_typer_cli }}") continuous_integration = "{{ cookiecutter.continuous_integration }}" -is_deployable_app = "{{ not not cookiecutter.with_fastapi_api|int or not not cookiecutter.with_streamlit_app|int }}" == "True" -is_publishable_package = "{{ not cookiecutter.with_fastapi_api|int and not cookiecutter.with_streamlit_app|int }}" == "True" +is_deployable_app = ( + "{{ not not cookiecutter.with_fastapi_api|int or not not cookiecutter.with_streamlit_app|int }}" + == "True" +) +is_publishable_package = ( + "{{ not cookiecutter.with_fastapi_api|int and not cookiecutter.with_streamlit_app|int }}" + == "True" +) # Remove py.typed and Dependabot if not in strict mode. if development_environment != "strict": From 4f08bf5c62d0aadcfe7d1728b9ae3bc548bb1427 Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Fri, 5 Jan 2024 11:10:45 -0500 Subject: [PATCH 23/68] update readme --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e867b126..ee7cfaa3 100644 --- a/README.md +++ b/README.md @@ -38,11 +38,11 @@ To create a new Python project with this template: ```sh pip install --upgrade "cruft>=2.12.0" "cookiecutter>=2.1.1" ``` -2. Create a new repository for your Python project, then clone it locally. -3. Run the following command in the parent directory of the cloned repository to apply the Poetry Cookiecutter template: +2. Run the following command in the parent directory where you want your package to apply the Poetry Cookiecutter template: ```sh cruft create -f https://github.com/Baseline-quebec/baseline-app-cookiecutter ``` +3. Create a new repository for your Python project and add the origin to your local package. 4. _Optional:_ Add the required Secrets to the Teamwork Integration inside Github Workflow using this documentation: https://github.com/Teamwork/github-sync There are the lines to update: @@ -76,8 +76,8 @@ To update your Python project with the latest template: | `author_email`
"john@example.com" | The email address of the primary author of the package. | | `python_version`
"3.8" | The minimum Python version that the package requires. | | `docker_image`
"python:$PYTHON_VERSION-slim" | The base Docker image to use for the Dev Container and application. The $PYTHON_VERSION build argument is equal to the `python_version` value by default, but may be overridden when building the image to test different Python versions. If CUDA support is required, you may use [radixai/python-gpu:$PYTHON_VERSION-cuda11.8](https://github.com/radix-ai/python-gpu). | -| `development_environment`
"strict" | Focus on strictness. In strict mode, [Ruff rules](https://beta.ruff.rs/docs/rules/) are added, and tools such as [Mypy](https://github.com/python/mypy) and [Pytest](https://github.com/pytest-dev/pytest) are set to strict mode. | -| `with_conventional_commits`
"1" | [Commitizen](https://github.com/commitizen-tools/commitizen) will verify that your commits follow the [Conventional Commits](https://www.conventionalcommits.org/) standard. In return, `cz bump` may be used to automate [Semantic Versioning](https://semver.org/) and [Keep A Changelog](https://keepachangelog.com/). | +| `development_environment`
"strict" | Focus on strictness. In strict mode, [Ruff rules](https://beta.ruff.rs/docs/rules/) are added, and tools such as [Mypy](https://github.com/python/mypy) and [Pytest](https://github.com/pytest-dev/pytest) are set to strict mode. | +| `with_conventional_commits`
"1" | [Commitizen](https://github.com/commitizen-tools/commitizen) will verify that your commits follow the [Conventional Commits](https://www.conventionalcommits.org/) standard. In return, `cz bump` may be used to automate [Semantic Versioning](https://semver.org/) and [Keep A Changelog](https://keepachangelog.com/). | | `with_fastapi_api`
["0", "1"] | If "1", [FastAPI](https://github.com/tiangolo/fastapi) is added as a run time dependency, FastAPI API stubs and tests are added, a `poe api` command for serving the API is added, and an `app` stage that packages the API is added to the Dockerfile. Additionally, the CI workflow will push the application as a Docker image instead of publishing the Python package. | | `with_jupyter_lab`
["0", "1"] | If "1", [JupyterLab](https://github.com/jupyterlab/jupyterlab) is added to Poetry's dev dependencies, and a `poe lab` command is added to start Jupyter Lab in the `notebooks/` directory. | | `with_pydantic_typing`
["0", "1"] | If "1", [Pydantic](https://github.com/samuelcolvin/pydantic) is added as a run time dependency, and the [Pydantic mypy plugin](https://pydantic-docs.helpmanual.io/mypy_plugin/) is enabled and configured. | From c70f91f4aa13f2573340d4ff82ec0a754c53a407 Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Fri, 5 Jan 2024 11:14:18 -0500 Subject: [PATCH 24/68] update README --- README.md | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index ee7cfaa3..4f325b6a 100644 --- a/README.md +++ b/README.md @@ -42,21 +42,8 @@ To create a new Python project with this template: ```sh cruft create -f https://github.com/Baseline-quebec/baseline-app-cookiecutter ``` -3. Create a new repository for your Python project and add the origin to your local package. -4. _Optional:_ Add the required Secrets to the Teamwork Integration inside Github Workflow using this documentation: https://github.com/Teamwork/github-sync - -There are the lines to update: - -```yaml -GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -TEAMWORK_URI: ${{ secrets.TEAMWORK_URI }} -TEAMWORK_API_TOKEN: ${{ secrets.TEAMWORK_API_TOKEN }} -``` - -4. _Optional:_ if your repository name differs from your project's slugified package name (see `package_name` in the [Template parameters](https://github.com/radix-ai/poetry-cookiecutter#-template-parameters) below), you will need to copy the scaffolded project into the repository with: - ```sh - cp -r {package-name}/ {repository-name}/ - ``` +3. Create a new repository for your Python project and add the remote origin to your local package. +4. _Optional:_ Link your repository to a Teamwork Project by adding the required Secrets to the Github Repository for the Teamwork Integration Github Workflow using this documentation: https://github.com/Teamwork/github-sync ### Updating your Python project From d836eb2d054081a5c398e258e1bb3e9633b3052c Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Fri, 5 Jan 2024 11:25:14 -0500 Subject: [PATCH 25/68] removed strings --- .../.github/workflows/teamwork.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml b/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml index d0c32315..50992b17 100644 --- a/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml +++ b/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml @@ -15,9 +15,9 @@ jobs: steps: - uses: teamwork/github-sync@master with: - GITHUB_TOKEN: "{% raw %}${{ secrets.GITHUB_TOKEN }}{% endraw %}" - TEAMWORK_URI: "{% raw %}${{ secrets.TEAMWORK_URI }}{% endraw %}" - TEAMWORK_API_TOKEN: "{% raw %}${{ secrets.TEAMWORK_API_TOKEN }}{% endraw %}" + GITHUB_TOKEN: {% raw %}${{ secrets.GITHUB_TOKEN }}{% endraw %} + TEAMWORK_URI: {% raw %}${{ secrets.TEAMWORK_URI }}{% endraw %} + TEAMWORK_API_TOKEN: {% raw %}${{ secrets.TEAMWORK_API_TOKEN }}{% endraw %} AUTOMATIC_TAGGING: false BOARD_COLUMN_OPENED: "PR Open" BOARD_COLUMN_MERGED: "Ready to Test" From 4a354e6f7d1a6d33796dd2e85e3579f6f1ee48b0 Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Fri, 5 Jan 2024 11:58:44 -0500 Subject: [PATCH 26/68] test-cookie-cutter --- README.md | 7 +++--- .../.devcontainer/devcontainer.json | 4 +++- .../.github/dependabot.yml | 24 +++++++++++++++++++ .../.github/pull_request_template.md | 6 ++--- .../.github/workflows/deploy.yml | 2 +- .../.github/workflows/teamwork.yml | 2 +- .../.github/workflows/test.yml | 8 ++++--- .../pyproject.toml | 12 ++++++++-- 8 files changed, 51 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 4f325b6a..40d832eb 100644 --- a/README.md +++ b/README.md @@ -38,12 +38,13 @@ To create a new Python project with this template: ```sh pip install --upgrade "cruft>=2.12.0" "cookiecutter>=2.1.1" ``` -2. Run the following command in the parent directory where you want your package to apply the Poetry Cookiecutter template: +2. Create a new repository for your Python project +3. Run the following command in the parent directory where you want your package to apply the Poetry Cookiecutter template: ```sh cruft create -f https://github.com/Baseline-quebec/baseline-app-cookiecutter ``` -3. Create a new repository for your Python project and add the remote origin to your local package. -4. _Optional:_ Link your repository to a Teamwork Project by adding the required Secrets to the Github Repository for the Teamwork Integration Github Workflow using this documentation: https://github.com/Teamwork/github-sync +4. Add the remote origin to your local package. +5. _Optional:_ Link your repository to a Teamwork Project by adding the required Secrets to the Github Repository for the Teamwork Integration Github Workflow using this documentation: https://github.com/Teamwork/github-sync ### Updating your Python project diff --git a/{{ cookiecutter.__package_name_kebab_case }}/.devcontainer/devcontainer.json b/{{ cookiecutter.__package_name_kebab_case }}/.devcontainer/devcontainer.json index b36cc5aa..a8fa0404 100644 --- a/{{ cookiecutter.__package_name_kebab_case }}/.devcontainer/devcontainer.json +++ b/{{ cookiecutter.__package_name_kebab_case }}/.devcontainer/devcontainer.json @@ -18,6 +18,8 @@ "ryanluker.vscode-coverage-gutters", "tamasfe.even-better-toml", "visualstudioexptteam.vscodeintellicode" + "GitHub.copilot", + "GitHub.copilot-chat" ], "settings": { "coverage-gutters.coverageFileNames": [ @@ -35,7 +37,7 @@ "editor.formatOnSave": false }, "editor.rulers": [ - 100 + 99 ], "files.autoSave": "onFocusChange", "mypy-type-checker.importStrategy": "fromEnvironment", diff --git a/{{ cookiecutter.__package_name_kebab_case }}/.github/dependabot.yml b/{{ cookiecutter.__package_name_kebab_case }}/.github/dependabot.yml index 7ce8c74c..6476f978 100644 --- a/{{ cookiecutter.__package_name_kebab_case }}/.github/dependabot.yml +++ b/{{ cookiecutter.__package_name_kebab_case }}/.github/dependabot.yml @@ -9,6 +9,18 @@ updates: prefix: "ci" prefix-development: "ci" include: "scope" + groups: + ci-dependencies: + patterns: + - "*" + update-types: + - "minor" + - "patch" + ci-major-updates: + patterns: + - "*" + update-types: + - "major" - package-ecosystem: pip directory: / schedule: @@ -20,3 +32,15 @@ updates: versioning-strategy: increase allow: - dependency-type: "all" + groups: + dependencies: + patterns: + - "*" + update-types: + - "minor" + - "patch" + major-updates: + patterns: + - "*" + update-types: + - "major" diff --git a/{{ cookiecutter.__package_name_kebab_case }}/.github/pull_request_template.md b/{{ cookiecutter.__package_name_kebab_case }}/.github/pull_request_template.md index 8eb00817..d8b550f9 100644 --- a/{{ cookiecutter.__package_name_kebab_case }}/.github/pull_request_template.md +++ b/{{ cookiecutter.__package_name_kebab_case }}/.github/pull_request_template.md @@ -1,10 +1,10 @@ -[:]() +[:]({{ cookiecutter.teamwork_uri }}/-) ## Describe your changes - ## Checklist before requesting a review + - [ ] I have performed a self-review of my code. - [ ] If it is a core feature, I have added thorough tests. - [ ] I have made corresponding changes to the documentation. -- [ ] I have bumped the version if needed. \ No newline at end of file +- [ ] I have bumped the version if needed. diff --git a/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/deploy.yml b/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/deploy.yml index 8771a41c..5873d22b 100644 --- a/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/deploy.yml +++ b/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/deploy.yml @@ -3,7 +3,7 @@ name: Deploy on: push: tags: - - "v*.*.*" + - "v[1-9]+.*.*" workflow_dispatch: inputs: environment: diff --git a/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml b/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml index 50992b17..c5c992f7 100644 --- a/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml +++ b/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml @@ -16,7 +16,7 @@ jobs: - uses: teamwork/github-sync@master with: GITHUB_TOKEN: {% raw %}${{ secrets.GITHUB_TOKEN }}{% endraw %} - TEAMWORK_URI: {% raw %}${{ secrets.TEAMWORK_URI }}{% endraw %} + TEAMWORK_URI: {{ cookiecutter.teamwork_uri }} TEAMWORK_API_TOKEN: {% raw %}${{ secrets.TEAMWORK_API_TOKEN }}{% endraw %} AUTOMATIC_TAGGING: false BOARD_COLUMN_OPENED: "PR Open" diff --git a/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/test.yml b/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/test.yml index bc676f1f..b119894c 100644 --- a/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/test.yml +++ b/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/test.yml @@ -41,7 +41,9 @@ jobs: - name: Test package run: devcontainer exec --workspace-folder . poe test - - name: Upload coverage - uses: codecov/codecov-action@v3 + - name: Upload Coverage Report + uses: actions/upload-artifact@v3 with: - files: reports/coverage.xml + path: htmlcov/ + name: coverage-report + retention-days: 7 diff --git a/{{ cookiecutter.__package_name_kebab_case }}/pyproject.toml b/{{ cookiecutter.__package_name_kebab_case }}/pyproject.toml index 9b292129..db005cec 100644 --- a/{{ cookiecutter.__package_name_kebab_case }}/pyproject.toml +++ b/{{ cookiecutter.__package_name_kebab_case }}/pyproject.toml @@ -142,11 +142,12 @@ ignore-init-module-imports = true line-length = 99 {%- if cookiecutter.development_environment == "strict" %} select = ["A", "ASYNC", "B", "BLE", "C4", "C90", "D", "DTZ", "E", "EM", "ERA", "F", "FBT", "FLY", "FURB", "G", "I", "ICN", "INP", "INT", "ISC", "LOG", "N", "NPY", "PERF", "PGH", "PIE", "PLC", "PLE", "PLR", "PLW", "PT", "PTH", "PYI", "RET", "RSE", "RUF", "S", "SIM", "SLF", "SLOT", "T10", "T20", "TCH", "TID", "TRY", "UP", "W", "YTT"] -ignore = ["E501", "PGH001", "PTH123", "RET504", "S101", "W505"] +ignore = ["E501", "E731", "PGH001", "PTH123", "RET504", "S101", "W505"] + unfixable = ["ERA001", "F401", "F841", "T201", "T203"] {%- else %} select = ["A", "ASYNC", "B", "C4", "C90", "D", "DTZ", "E", "F", "FLY", "FURB", "I", "ISC", "LOG", "N", "NPY", "PERF", "PGH", "PIE", "PLC", "PLE", "PLR", "PLW", "PT", "RET", "RUF", "RSE", "SIM", "TID", "UP", "W", "YTT"] -ignore = ["E501", "PGH001", "PGH002", "PGH003", "RET504", "S101"] +ignore = ["E501", "E731", "PGH001", "PGH002", "PGH003", "PTH123", "RET504", "S101", "W505"] unfixable = ["F401", "F841"] {%- endif %} src = ["src", "tests"] @@ -154,6 +155,10 @@ target-version = "py{{ cookiecutter.python_version.split('.')[:2]|join }}" [tool.ruff.flake8-tidy-imports] ban-relative-imports = "all" + +[tool.ruff.isort] +lines-after-imports = 2 + {%- if cookiecutter.development_environment == "strict" %} [tool.ruff.pycodestyle] @@ -163,6 +168,9 @@ max-doc-length = 99 [tool.ruff.pydocstyle] convention = "{{ cookiecutter.docstring_style|lower }}" +[tool.ruff.pylint] +max-args = 6 + [tool.poe.tasks] # https://github.com/nat-n/poethepoet {%- if cookiecutter.with_fastapi_api|int %} From 535c341328ef0339d65e32481843fd077e9d1fb2 Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Fri, 5 Jan 2024 12:02:26 -0500 Subject: [PATCH 27/68] removed error --- .../.github/pull_request_template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/{{ cookiecutter.__package_name_kebab_case }}/.github/pull_request_template.md b/{{ cookiecutter.__package_name_kebab_case }}/.github/pull_request_template.md index d8b550f9..049a892a 100644 --- a/{{ cookiecutter.__package_name_kebab_case }}/.github/pull_request_template.md +++ b/{{ cookiecutter.__package_name_kebab_case }}/.github/pull_request_template.md @@ -1,4 +1,4 @@ -[:]({{ cookiecutter.teamwork_uri }}/-) +[:]({{ cookiecutter.teamwork_uri }}/) ## Describe your changes From 9a24851d858a6c2a739abb69322a0b336ca08e49 Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Fri, 5 Jan 2024 12:06:57 -0500 Subject: [PATCH 28/68] updated teamwork uri --- README.md | 1 + cookiecutter.json | 2 ++ 2 files changed, 3 insertions(+) diff --git a/README.md b/README.md index 40d832eb..a4301698 100644 --- a/README.md +++ b/README.md @@ -76,3 +76,4 @@ To update your Python project with the latest template: | `docstring_style`
"NumPy" | Validates [NumPy-style](https://numpydoc.readthedocs.io/en/latest/format.html) . | | `private_package_repository_name`
"Private Package Repository" | Optional name of a private package repository to install packages from and publish this package to. | | `private_package_repository_url`
"https://pypi.example.com/simple" | Optional URL of a private package repository to install packages from and publish this package to. Make sure to include the `/simple` suffix. For instance, when using a GitLab Package Registry this value should be of the form `https://gitlab.com/api/v4/projects/` `{project_id}` `/packages/pypi/simple`. | +| `teamwork_uri`
"Teamwork URI" | Optional URI of your Teamwork's page. | diff --git a/cookiecutter.json b/cookiecutter.json index ef6ed07a..4399a59b 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -18,6 +18,8 @@ "docstring_style": "NumPy", "private_package_repository_name": "", "private_package_repository_url": "", + "teamwork_uri": "", "__package_name_kebab_case": "{{ cookiecutter.package_name|slugify }}", "__package_name_snake_case": "{{ cookiecutter.package_name|slugify(separator='_') }}" + } \ No newline at end of file From 153a10083308c3104e9d9b49195c4d5dba14ebae Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Fri, 5 Jan 2024 12:25:45 -0500 Subject: [PATCH 29/68] updated uri --- .../.github/pull_request_template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/{{ cookiecutter.__package_name_kebab_case }}/.github/pull_request_template.md b/{{ cookiecutter.__package_name_kebab_case }}/.github/pull_request_template.md index 049a892a..c9dd2a5e 100644 --- a/{{ cookiecutter.__package_name_kebab_case }}/.github/pull_request_template.md +++ b/{{ cookiecutter.__package_name_kebab_case }}/.github/pull_request_template.md @@ -1,4 +1,4 @@ -[:]({{ cookiecutter.teamwork_uri }}/) +[:]({{ cookiecutter.teamwork_uri }}/app/tasks/) ## Describe your changes From 14e1d677d3071dd3a47d87795956039aa560d601 Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Fri, 5 Jan 2024 12:31:15 -0500 Subject: [PATCH 30/68] added , --- .../.devcontainer/devcontainer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/{{ cookiecutter.__package_name_kebab_case }}/.devcontainer/devcontainer.json b/{{ cookiecutter.__package_name_kebab_case }}/.devcontainer/devcontainer.json index a8fa0404..78e59402 100644 --- a/{{ cookiecutter.__package_name_kebab_case }}/.devcontainer/devcontainer.json +++ b/{{ cookiecutter.__package_name_kebab_case }}/.devcontainer/devcontainer.json @@ -17,7 +17,7 @@ {%- endif %} "ryanluker.vscode-coverage-gutters", "tamasfe.even-better-toml", - "visualstudioexptteam.vscodeintellicode" + "visualstudioexptteam.vscodeintellicode", "GitHub.copilot", "GitHub.copilot-chat" ], From ce3d7b0369f8f2fc6f577613e47dadf2ea50dfe7 Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Tue, 6 Feb 2024 14:59:44 -0500 Subject: [PATCH 31/68] updated repo with geothentic-vrp preferences --- README.md | 4 ++-- cookiecutter.json | 2 +- .../.github/workflows/teamwork.yml | 2 +- .../.github/workflows/test.yml | 4 ++-- .../.pre-commit-config.yaml | 5 ----- {{ cookiecutter.__package_name_kebab_case }}/pyproject.toml | 6 +++--- 6 files changed, 9 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index a4301698..485912d7 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ To update your Python project with the latest template: | `with_streamlit_app`
["0", "1"] | If "1", [Streamlit](https://github.com/streamlit/streamlit) is added as a run time dependency, a Streamlit application stub is added, a `poe app` command to serve the Streamlit app is added, and an `app` stage that packages the Streamlit app is added to the Dockerfile. Additionally, the CI workflow will push the application as a Docker image instead of publishing the Python package. | | `with_typer_cli`
["0", "1"] | If "1", [Typer](https://github.com/tiangolo/typer) is added as a run time dependency, Typer CLI stubs and tests are added, the package itself is registered as a CLI, and an `app` stage is added to the Dockerfile that packages the CLI. | | `continuous_integration`
"GitHub" | Includes a [GitHub Actions](https://docs.github.com/en/actions) continuous integration workflow for testing and publishing the package or app. | -| `docstring_style`
"NumPy" | Validates [NumPy-style](https://numpydoc.readthedocs.io/en/latest/format.html) . | +| `docstring_style`
"Google" | Validates [Google-style](https://google.github.io/styleguide/pyguide.html) . | | `private_package_repository_name`
"Private Package Repository" | Optional name of a private package repository to install packages from and publish this package to. | | `private_package_repository_url`
"https://pypi.example.com/simple" | Optional URL of a private package repository to install packages from and publish this package to. Make sure to include the `/simple` suffix. For instance, when using a GitLab Package Registry this value should be of the form `https://gitlab.com/api/v4/projects/` `{project_id}` `/packages/pypi/simple`. | -| `teamwork_uri`
"Teamwork URI" | Optional URI of your Teamwork's page. | + diff --git a/cookiecutter.json b/cookiecutter.json index 4399a59b..0fcd91a4 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -15,7 +15,7 @@ "with_streamlit_app": "0", "with_typer_cli": "0", "continuous_integration": "GitHub", - "docstring_style": "NumPy", + "docstring_style": "Google", "private_package_repository_name": "", "private_package_repository_url": "", "teamwork_uri": "", diff --git a/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml b/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml index c5c992f7..50992b17 100644 --- a/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml +++ b/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/teamwork.yml @@ -16,7 +16,7 @@ jobs: - uses: teamwork/github-sync@master with: GITHUB_TOKEN: {% raw %}${{ secrets.GITHUB_TOKEN }}{% endraw %} - TEAMWORK_URI: {{ cookiecutter.teamwork_uri }} + TEAMWORK_URI: {% raw %}${{ secrets.TEAMWORK_URI }}{% endraw %} TEAMWORK_API_TOKEN: {% raw %}${{ secrets.TEAMWORK_API_TOKEN }}{% endraw %} AUTOMATIC_TAGGING: false BOARD_COLUMN_OPENED: "PR Open" diff --git a/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/test.yml b/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/test.yml index b119894c..5b023c5f 100644 --- a/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/test.yml +++ b/{{ cookiecutter.__package_name_kebab_case }}/.github/workflows/test.yml @@ -42,8 +42,8 @@ jobs: run: devcontainer exec --workspace-folder . poe test - name: Upload Coverage Report - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - path: htmlcov/ + path: reports/htmlcov/ name: coverage-report retention-days: 7 diff --git a/{{ cookiecutter.__package_name_kebab_case }}/.pre-commit-config.yaml b/{{ cookiecutter.__package_name_kebab_case }}/.pre-commit-config.yaml index e4870347..e5372574 100644 --- a/{{ cookiecutter.__package_name_kebab_case }}/.pre-commit-config.yaml +++ b/{{ cookiecutter.__package_name_kebab_case }}/.pre-commit-config.yaml @@ -42,11 +42,6 @@ repos: args: [--pytest-test-first] - id: trailing-whitespace types: [python] - - repo: https://github.com/python-poetry/poetry - rev: '1.6.0' - hooks: - - id: poetry-check - - id: poetry-lock - repo: local hooks: {%- if cookiecutter.with_conventional_commits|int %} diff --git a/{{ cookiecutter.__package_name_kebab_case }}/pyproject.toml b/{{ cookiecutter.__package_name_kebab_case }}/pyproject.toml index db005cec..1c773874 100644 --- a/{{ cookiecutter.__package_name_kebab_case }}/pyproject.toml +++ b/{{ cookiecutter.__package_name_kebab_case }}/pyproject.toml @@ -100,8 +100,8 @@ command_line = "--module pytest" data_file = "reports/.coverage" source = ["src"] -[tool.coverage.xml] # https://coverage.readthedocs.io/en/latest/config.html#xml -output = "reports/coverage.xml" +[tool.coverage.html] # https://coverage.readthedocs.io/en/latest/cmd.html#html-reporting-coverage-html +directory = "reports/htmlcov" [tool.mypy] # https://mypy.readthedocs.io/en/latest/config_file.html junit_xml = "reports/mypy.xml" @@ -305,4 +305,4 @@ max-args = 6 cmd = "coverage report" [[tool.poe.tasks.test.sequence]] - cmd = "coverage xml" + cmd = "coverage html" From 28c95a524479ef4bee58c00f54ba4edbe0fc8fd0 Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Tue, 6 Feb 2024 17:45:07 -0500 Subject: [PATCH 32/68] updated with ruff rules --- .../pyproject.toml | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/{{ cookiecutter.__package_name_kebab_case }}/pyproject.toml b/{{ cookiecutter.__package_name_kebab_case }}/pyproject.toml index 1c773874..f735333d 100644 --- a/{{ cookiecutter.__package_name_kebab_case }}/pyproject.toml +++ b/{{ cookiecutter.__package_name_kebab_case }}/pyproject.toml @@ -138,25 +138,26 @@ xfail_strict = true [tool.ruff] # https://github.com/charliermarsh/ruff fix = true -ignore-init-module-imports = true +preview = true +lint.ignore-init-module-imports = true line-length = 99 {%- if cookiecutter.development_environment == "strict" %} -select = ["A", "ASYNC", "B", "BLE", "C4", "C90", "D", "DTZ", "E", "EM", "ERA", "F", "FBT", "FLY", "FURB", "G", "I", "ICN", "INP", "INT", "ISC", "LOG", "N", "NPY", "PERF", "PGH", "PIE", "PLC", "PLE", "PLR", "PLW", "PT", "PTH", "PYI", "RET", "RSE", "RUF", "S", "SIM", "SLF", "SLOT", "T10", "T20", "TCH", "TID", "TRY", "UP", "W", "YTT"] -ignore = ["E501", "E731", "PGH001", "PTH123", "RET504", "S101", "W505"] +lint.select = ["A", "ASYNC", "B", "BLE", "C4", "C90", "D", "DTZ", "E", "EM", "ERA", "F", "FBT", "FLY", "FURB", "G", "I", "ICN", "INP", "INT", "ISC", "LOG", "N", "NPY", "PERF", "PGH", "PIE", "PLC", "PLE", "PLR", "PLW", "PT", "PTH", "PYI", "RET", "RSE", "RUF", "S", "SIM", "SLF", "SLOT", "T10", "T20", "TCH", "TID", "TRY", "UP", "W", "YTT"] +lint.ignore = ["E501", "E731", "PTH123", "RET504", "S101", "S307", "W505"] -unfixable = ["ERA001", "F401", "F841", "T201", "T203"] +lint.unfixable = ["ERA001", "F401", "F841", "T201", "T203"] {%- else %} -select = ["A", "ASYNC", "B", "C4", "C90", "D", "DTZ", "E", "F", "FLY", "FURB", "I", "ISC", "LOG", "N", "NPY", "PERF", "PGH", "PIE", "PLC", "PLE", "PLR", "PLW", "PT", "RET", "RUF", "RSE", "SIM", "TID", "UP", "W", "YTT"] -ignore = ["E501", "E731", "PGH001", "PGH002", "PGH003", "PTH123", "RET504", "S101", "W505"] -unfixable = ["F401", "F841"] +lint.select = ["A", "ASYNC", "B", "C4", "C90", "D", "DTZ", "E", "F", "FLY", "FURB", "I", "ISC", "LOG", "N", "NPY", "PERF", "PGH", "PIE", "PLC", "PLE", "PLR", "PLW", "PT", "RET", "RUF", "RSE", "SIM", "TID", "UP", "W", "YTT"] +lint.ignore = ["E501", "E731", "PGH001", "PGH002", "PGH003", "PTH123", "RET504", "S101", "W505"] +lint.unfixable = ["F401", "F841"] {%- endif %} src = ["src", "tests"] target-version = "py{{ cookiecutter.python_version.split('.')[:2]|join }}" -[tool.ruff.flake8-tidy-imports] +[tool.ruff.lint.flake8-tidy-imports] ban-relative-imports = "all" -[tool.ruff.isort] +[tool.ruff.lint.isort] lines-after-imports = 2 {%- if cookiecutter.development_environment == "strict" %} From 284c3e99ad1d635a6d42cbcd4d13c81510ebff4e Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Fri, 31 May 2024 20:11:39 +0000 Subject: [PATCH 33/68] feat(project.py): update with new poetry cookiecutter update --- .../.devcontainer/devcontainer.json | 16 ++++----- .../.github/workflows/publish.yml | 33 ------------------- 2 files changed, 8 insertions(+), 41 deletions(-) delete mode 100644 {{ cookiecutter.__project_name_kebab_case }}/.github/workflows/publish.yml diff --git a/{{ cookiecutter.__project_name_kebab_case }}/.devcontainer/devcontainer.json b/{{ cookiecutter.__project_name_kebab_case }}/.devcontainer/devcontainer.json index 9cfa244d..d86060ef 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/.devcontainer/devcontainer.json +++ b/{{ cookiecutter.__project_name_kebab_case }}/.devcontainer/devcontainer.json @@ -10,12 +10,8 @@ "vscode": { "extensions": [ "charliermarsh.ruff", - {%- if cookiecutter.continuous_integration == "GitHub" %} "GitHub.vscode-github-actions", "GitHub.vscode-pull-request-github", - {%- elif cookiecutter.continuous_integration == "GitLab" %} - "GitLab.gitlab-workflow", - {%- endif %} "ms-python.mypy-type-checker", "ms-python.python", "ms-toolsai.jupyter", @@ -23,7 +19,7 @@ "tamasfe.even-better-toml", "visualstudioexptteam.vscodeintellicode", "GitHub.copilot", - "GitHub.copilot-chat" + "GitHub.copilot-chat" ], "settings": { "coverage-gutters.coverageFileNames": [ @@ -44,7 +40,9 @@ 99 ], "files.autoSave": "onFocusChange", - "jupyter.kernels.excludePythonEnvironments": ["/usr/local/bin/python"], + "jupyter.kernels.excludePythonEnvironments": [ + "/usr/local/bin/python" + ], "mypy-type-checker.importStrategy": "fromEnvironment", "notebook.codeActionsOnSave": { "notebook.source.fixAll": "explicit", @@ -55,9 +53,11 @@ "python.terminal.activateEnvironment": false, "python.testing.pytestEnabled": true, "ruff.importStrategy": "fromEnvironment", - {%- if cookiecutter.development_environment == "strict" %} + {%- if cookiecutter.development_environment == "strict" % + } "ruff.logLevel": "warn", - {%- endif %} + {%- endif % + } "terminal.integrated.defaultProfile.linux": "zsh", "terminal.integrated.profiles.linux": { "zsh": { diff --git a/{{ cookiecutter.__project_name_kebab_case }}/.github/workflows/publish.yml b/{{ cookiecutter.__project_name_kebab_case }}/.github/workflows/publish.yml deleted file mode 100644 index 503f9f6c..00000000 --- a/{{ cookiecutter.__project_name_kebab_case }}/.github/workflows/publish.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Publish - -on: - release: - types: - - created - -jobs: - publish: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "{{ cookiecutter.python_version }}" - - - name: Install Poetry - run: pip install --no-input poetry - - - name: Publish package - run: | - {%- if cookiecutter.private_package_repository_name %} - poetry config repositories.private "{{ cookiecutter.private_package_repository_url.replace('simple/', '').replace('simple', '') }}" - poetry config http-basic.private "{% raw %}${{{% endraw %} secrets.POETRY_HTTP_BASIC_{{ cookiecutter.private_package_repository_name|slugify(separator="_")|upper }}_USERNAME }}" "{% raw %}${{{% endraw %} secrets.POETRY_HTTP_BASIC_{{ cookiecutter.private_package_repository_name|slugify(separator="_")|upper }}_PASSWORD }}" - poetry publish --build --repository private - {%- else %} - poetry config pypi-token.pypi "{% raw %}${{ secrets.POETRY_PYPI_TOKEN_PYPI }}{% endraw %}" - poetry publish --build - {%- endif %} From bfbbeee31ace9575710863da9bea07ae8eb2af5d Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Fri, 31 May 2024 20:28:01 +0000 Subject: [PATCH 34/68] feat(project.py): updated with custom parameters --- .devcontainer/devcontainer.json | 2 +- .../Dockerfile | 23 ++------- .../docker-compose.yml | 34 ------------- .../pyproject.toml | 50 +++++++++---------- 4 files changed, 30 insertions(+), 79 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b02b1658..b5925e2f 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,5 +1,5 @@ { - "name": "Poetry Cookiecutter", + "name": "Baseline app Cookiecutter", "image": "mcr.microsoft.com/vscode/devcontainers/python:3.10", "onCreateCommand": "pip install commitizen cruft pre-commit && pre-commit install --install-hooks", "remoteUser": "vscode", diff --git a/{{ cookiecutter.__project_name_kebab_case }}/Dockerfile b/{{ cookiecutter.__project_name_kebab_case }}/Dockerfile index 3ba3ea85..a801dbcf 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/Dockerfile +++ b/{{ cookiecutter.__project_name_kebab_case }}/Dockerfile @@ -57,9 +57,6 @@ COPY --chown=user:user poetry.lock* pyproject.toml /workspaces/{{ cookiecutter._ RUN mkdir -p /home/user/.cache/pypoetry/ && mkdir -p /home/user/.config/pypoetry/ && \ mkdir -p src/{{ cookiecutter.__project_name_snake_case }}/ && touch src/{{ cookiecutter.__project_name_snake_case }}/__init__.py && touch README.md RUN --mount=type=cache,uid=$UID,gid=$GID,target=/home/user/.cache/pypoetry/ \ - {%- if cookiecutter.private_package_repository_name %} - --mount=type=secret,id=poetry-auth,uid=$UID,gid=$GID,target=/home/user/.config/pypoetry/auth.toml \ - {%- endif %} poetry install --only main --all-extras --no-interaction @@ -106,14 +103,6 @@ RUN git clone --branch v$ANTIDOTE_VERSION --depth=1 https://github.com/mattmc3/a echo 'bindkey "^[[B" history-beginning-search-forward' >> ~/.zshrc && \ mkdir ~/.history/ && \ zsh -c 'source ~/.zshrc' -{%- if cookiecutter.private_package_repository_name %} - -# Enable Poetry to read the private package repository credentials. -RUN ln -s /run/secrets/poetry-auth /home/user/.config/pypoetry/auth.toml -{%- endif %} -{%- if cookiecutter.project_type == "app" %} - - FROM base AS app @@ -121,14 +110,10 @@ FROM base AS app COPY --from=poetry $VIRTUAL_ENV $VIRTUAL_ENV # Copy the {{ cookiecutter.project_type }} source code to the working directory. -COPY --chown=user:user . . +COPY --chown=user:user ./src ./src +COPY --chown=user:user ./pyproject.toml . +COPY --chown=user:user ./poetry.lock . # Expose the app. -{%- if cookiecutter.with_typer_cli|int %} -ENTRYPOINT ["/opt/{{ cookiecutter.__project_name_kebab_case }}-env/bin/{{ cookiecutter.__project_name_kebab_case }}"] -CMD [] -{%- else %} ENTRYPOINT ["/opt/{{ cookiecutter.__project_name_kebab_case }}-env/bin/poe"] -CMD [{% if cookiecutter.with_fastapi_api|int %}"api"{% else %}"app"{% endif %}] -{%- endif %} -{%- endif %} +CMD ["api"] diff --git a/{{ cookiecutter.__project_name_kebab_case }}/docker-compose.yml b/{{ cookiecutter.__project_name_kebab_case }}/docker-compose.yml index 07c1e58d..1eee7869 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/docker-compose.yml +++ b/{{ cookiecutter.__project_name_kebab_case }}/docker-compose.yml @@ -25,34 +25,6 @@ services: - ..:/workspaces - command-history-volume:/home/user/.history/ - dev: - extends: devcontainer - stdin_open: true - tty: true - entrypoint: [] - command: - [ - "sh", - "-c", - "sudo chown user $$SSH_AUTH_SOCK && cp --update /opt/build/poetry/poetry.lock /workspaces/{{ cookiecutter.__project_name_kebab_case }}/ && mkdir -p /workspaces/{{ cookiecutter.__project_name_kebab_case }}/.git/hooks/ && cp --update /opt/build/git/* /workspaces/{{ cookiecutter.__project_name_kebab_case }}/.git/hooks/ && zsh" - ] - environment: - {%- if not cookiecutter.private_package_repository_name %} - - POETRY_PYPI_TOKEN_PYPI - {%- endif %} - - SSH_AUTH_SOCK=/run/host-services/ssh-auth.sock - {%- if cookiecutter.with_fastapi_api|int %} - ports: - - "8000" - {%- endif %} - volumes: - - ~/.gitconfig:/etc/gitconfig - - ~/.ssh/known_hosts:/home/user/.ssh/known_hosts - - ${SSH_AGENT_AUTH_SOCK:-/run/host-services/ssh-auth.sock}:/run/host-services/ssh-auth.sock - profiles: - - dev - {%- if cookiecutter.project_type == "app" %} - app: build: context: . @@ -69,12 +41,6 @@ services: profiles: - app {%- endif %} -{%- if cookiecutter.private_package_repository_name %} - -secrets: - poetry-auth: - file: "${POETRY_AUTH_TOML_PATH:-~/Library/Application Support/pypoetry/auth.toml}" -{%- endif %} volumes: command-history-volume: diff --git a/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml b/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml index a894b10f..6279d0e1 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml +++ b/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml @@ -46,9 +46,6 @@ commitizen = ">=3.21.3" {%- endif %} coverage = { extras = ["toml"], version = ">=7.4.4" } mypy = ">=1.9.0" -{%- if cookiecutter.project_type == "package" %} -poethepoet = ">=0.25.0" -{%- endif %} pre-commit = ">=3.7.0" pytest = ">=8.1.1" pytest-mock = ">=3.14.0" @@ -65,17 +62,6 @@ cruft = ">=2.15.0" ipykernel = ">=6.29.4" ipywidgets = ">=8.1.2" pdoc = ">=14.4.0" -{%- if cookiecutter.private_package_repository_name %} - -[[tool.poetry.source]] -name = "pypi" -priority = "default" - -[[tool.poetry.source]] # https://python-poetry.org/docs/repositories/#using-a-private-repository -name = "{{ cookiecutter.private_package_repository_name|slugify }}" -url = "{{ cookiecutter.private_package_repository_url }}" -priority = "explicit" -{%- endif %} [tool.coverage.report] # https://coverage.readthedocs.io/en/latest/config.html#report {%- if cookiecutter.development_environment == "strict" %} @@ -94,6 +80,9 @@ source = ["src"] [tool.coverage.html] # https://coverage.readthedocs.io/en/latest/cmd.html#html-reporting-coverage-html directory = "reports/htmlcov" +[tool.coverage.xml] # https://coverage.readthedocs.io/en/latest/cmd.html#html-reporting-coverage-html +directory = "reports/coverage.xml" + [tool.mypy] # https://mypy.readthedocs.io/en/latest/config_file.html junit_xml = "reports/mypy.xml" {%- if cookiecutter.with_fastapi_api|int %} @@ -136,8 +125,25 @@ target-version = "py{{ cookiecutter.python_version.split('.')[:2]|join }}" [tool.ruff.lint] ignore-init-module-imports = true {%- if cookiecutter.development_environment == "strict" %} -select = ["A", "ASYNC", "B", "BLE", "C4", "C90", "D", "DTZ", "E", "EM", "ERA", "F", "FBT", "FLY", "FURB", "G", "I", "ICN", "INP", "INT", "ISC", "LOG", "N", "NPY", "PERF", "PGH", "PIE", "PL", "PT", "PTH", "PYI", "Q", "RET", "RSE", "RUF", "S", "SIM", "SLF", "SLOT", "T10", "T20", "TCH", "TID", "TRY", "UP", "W", "YTT"] -ignore = ["D203", "D213", "E501", "RET504", "S101", "S307"] +select = ["ALL"] +ignore = [ + "ANN002", # Missing type annotation for args. Complicates the code too much with the current annotation system. + "ANN003", # Missing type annotation for kwargs. Complicates the code too much with the current annotation system. + "ANN101", # Missing type annotation for self in method. Useless info as it's always the same and clutters the code. + "ANN102", # Missing type annotation for cls in classmethod. Useless info as it's always the same and clutters the code. + "ANN401", # Typing with Any is not permitted. + "ARG002", # Unused argument. Sometimes it's useful to keep the argument for future use, for retrocompatibility or to abide by an interface. + "COM812", # Missing trailing comma in a single-line list. Already handled by ruff formater + "CPY001", # Copyright notice missing. Not always needed. + "D417", # Missing argument descriptions in the docstring. Not always needed. + "E501", # Line too long. Already handled by ruff formater + "E731", # Do not assign a lambda expression. Do not agree with this rule, it improves readability as it is self-documenting the lambda function. + "PD901", # Do not use the variable name "df". This is a common name for dataframes and is not a problem. + "RET504", # Unnecessary assign before return. This is not bad, it helps debugging. + "S311", # Standard pseudo-random generators are not suitable for security/cryptographic purposes. Our use case is not security/cryptographic. + "TRY003", # Raise exception with vanilla arguments. Creating exception classes should be done only when really needed. + "W505", # Doc line too long. Editor autowraps doc, and it's too much work to fix it. +] unfixable = ["ERA001", "F401", "F841", "T201", "T203"] {%- else %} select = ["A", "ASYNC", "B", "C4", "C90", "D", "DTZ", "E", "F", "FLY", "FURB", "I", "ISC", "LOG", "N", "NPY", "PERF", "PGH", "PIE", "PL", "PT", "Q", "RET", "RUF", "RSE", "SIM", "TID", "UP", "W", "YTT"] @@ -164,7 +170,6 @@ convention = "{{ cookiecutter.__docstring_style|lower }}" max-args = 6 [tool.poe.tasks] # https://github.com/nat-n/poethepoet -{%- if cookiecutter.with_fastapi_api|int %} [tool.poe.tasks.api] help = "Serve the REST API" @@ -208,14 +213,6 @@ max-args = 6 type = "boolean" name = "dev" options = ["--dev"] -{%- elif cookiecutter.project_type == "app" %} - - [tool.poe.tasks.app] - help = "Serve the app" - - [[tool.poe.tasks.app.sequence]] - cmd = "echo 'Serving app...'" -{%- endif %} [tool.poe.tasks.docs] help = "Generate this {{ cookiecutter.project_type }}'s docs" @@ -264,3 +261,6 @@ max-args = 6 [[tool.poe.tasks.test.sequence]] cmd = "coverage html" + + [[tool.poe.tasks.test.sequence]] + cmd = "coverage xml" \ No newline at end of file From 0443ab7c6216236e0246c5717dcdde0bc3ca217e Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Fri, 31 May 2024 20:36:41 +0000 Subject: [PATCH 35/68] feat(test.yml): removed support for package --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5f19accb..57764bb0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,8 +13,8 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.12"] - project-type: ["app", "package"] + python-version: ["3.12"] + project-type: ["app"] name: Python ${{ matrix.python-version }} ${{ matrix.project-type }} @@ -27,7 +27,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.9" + python-version: "3.12" - name: Scaffold Python project run: | From 88749b4b06e197761b2d311bc420b5e511f07278 Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Fri, 31 May 2024 20:39:20 +0000 Subject: [PATCH 36/68] feat(test.yml): removed pacakge --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 57764bb0..ecf89a27 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,7 @@ jobs: python-version: ["3.12"] project-type: ["app"] - name: Python ${{ matrix.python-version }} ${{ matrix.project-type }} + name: Python ${{ matrix.python-version }} app steps: - name: Checkout @@ -32,7 +32,7 @@ jobs: - name: Scaffold Python project run: | pip install --no-input cruft - cruft create --no-input --extra-context '{"project_type": "${{ matrix.project-type }}", "project_name": "My Project", "python_version": "3.9", "__docker_image":"radixai/python-gpu:$PYTHON_VERSION-cuda11.8", "with_fastapi_api": "1", "with_typer_cli": "1"}' ./template/ + cruft create --no-input --extra-context '{"project_type": "app", "project_name": "My Project", "python_version": "3.12", "__docker_image":"radixai/python-gpu:$PYTHON_VERSION-cuda11.8", "with_fastapi_api": "1", "with_typer_cli": "1"}' ./template/ - name: Set up Node.js uses: actions/setup-node@v4 From f2349ddaaf621e4869f416de0b91df61618964a5 Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Fri, 31 May 2024 20:40:03 +0000 Subject: [PATCH 37/68] fix(test.yml): removed docker push --- .github/workflows/test.yml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ecf89a27..8944d568 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -56,13 +56,3 @@ jobs: - name: Test project run: devcontainer exec --workspace-folder my-project poe test - - - name: Build app Docker image - if: ${{ matrix.project-type == 'app' }} - uses: docker/build-push-action@v5 - with: - build-args: | - SOURCE_BRANCH=${{ env.GITHUB_REF }} - SOURCE_COMMIT=${{ env.GITHUB_SHA }} - context: ./my-project/ - target: app From e52ba5af46e058461dcc6da3e919c74f090bc1fc Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Fri, 31 May 2024 20:44:39 +0000 Subject: [PATCH 38/68] feat(project.py): removed everything about private package --- .../.github/workflows/deploy.yml | 60 ------------------- .../Dockerfile | 3 - .../README.md | 45 +------------- .../docker-compose.yml | 22 +------ 4 files changed, 4 insertions(+), 126 deletions(-) delete mode 100644 {{ cookiecutter.__project_name_kebab_case }}/.github/workflows/deploy.yml diff --git a/{{ cookiecutter.__project_name_kebab_case }}/.github/workflows/deploy.yml b/{{ cookiecutter.__project_name_kebab_case }}/.github/workflows/deploy.yml deleted file mode 100644 index 5873d22b..00000000 --- a/{{ cookiecutter.__project_name_kebab_case }}/.github/workflows/deploy.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: Deploy - -on: - push: - tags: - - "v[1-9]+.*.*" - workflow_dispatch: - inputs: - environment: - required: true - description: Deployment environment - default: development - type: choice - options: - - feature - - development - - test - - acceptance - - production - -env: - DEFAULT_DEPLOYMENT_ENVIRONMENT: feature - DOCKER_REGISTRY: ghcr.io - -jobs: - deploy: - runs-on: ubuntu-latest - - if: startsWith(github.ref, 'refs/tags/v') - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Log in to the Docker registry - uses: docker/login-action@v3 - with: - registry: {% raw %}${{ env.DOCKER_REGISTRY }}{% endraw %} - username: {% raw %}${{ github.actor }}{% endraw %} - password: {% raw %}${{ secrets.GITHUB_TOKEN }}{% endraw %} - - - name: Set Docker image tag - run: echo "GIT_TAG=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - - - name: Build and push Docker image - uses: docker/build-push-action@v5 - with: - context: . - push: true - {%- if cookiecutter.private_package_repository_name %} - secrets: | - "poetry_auth=[http-basic.{{ cookiecutter.private_package_repository_name|slugify }}] - username = ""{% raw %}${{{% endraw %} secrets.POETRY_HTTP_BASIC_{{ cookiecutter.private_package_repository_name|slugify(separator="_")|upper }}_USERNAME }}"" - password = ""{% raw %}${{{% endraw %} secrets.POETRY_HTTP_BASIC_{{ cookiecutter.private_package_repository_name|slugify(separator="_")|upper }}_PASSWORD }}"" - " - {%- endif %} - tags: | - {% raw %}${{ env.DOCKER_REGISTRY }}/${{ github.repository_owner }}/${{ github.repository }}:${{ github.event.inputs.environment || env.DEFAULT_DEPLOYMENT_ENVIRONMENT }}{% endraw %} - {% raw %}${{ env.DOCKER_REGISTRY }}/${{ github.repository_owner }}/${{ github.repository }}:${{ env.GIT_TAG }}{% endraw %} - target: app diff --git a/{{ cookiecutter.__project_name_kebab_case }}/Dockerfile b/{{ cookiecutter.__project_name_kebab_case }}/Dockerfile index a801dbcf..fecace7f 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/Dockerfile +++ b/{{ cookiecutter.__project_name_kebab_case }}/Dockerfile @@ -76,9 +76,6 @@ USER user # Install the development Python dependencies in the virtual environment. RUN --mount=type=cache,uid=$UID,gid=$GID,target=/home/user/.cache/pypoetry/ \ - {%- if cookiecutter.private_package_repository_name %} - --mount=type=secret,id=poetry-auth,uid=$UID,gid=$GID,target=/home/user/.config/pypoetry/auth.toml \ - {%- endif %} poetry install --all-extras --no-interaction # Persist output generated during docker build so that we can restore it in the dev container. diff --git a/{{ cookiecutter.__project_name_kebab_case }}/README.md b/{{ cookiecutter.__project_name_kebab_case }}/README.md index 586039f4..33335eee 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/README.md +++ b/{{ cookiecutter.__project_name_kebab_case }}/README.md @@ -10,10 +10,8 @@ To install this package, run: ```sh -{% if cookiecutter.private_package_repository_name %}poetry add{% else %}pip install{% endif %} {{ cookiecutter.__project_name_kebab_case }} +pip install {{ cookiecutter.__project_name_kebab_case }} ``` -{%- endif %} - ## Using {%- if cookiecutter.with_typer_cli|int %} @@ -88,18 +86,8 @@ import {{ cookiecutter.__project_name_snake_case }} export UID=$(id --user) export GID=$(id --group) - {%- if cookiecutter.private_package_repository_name %} - export POETRY_AUTH_TOML_PATH="~/.config/pypoetry/auth.toml" - {%- endif %} EOF ``` - {%- if cookiecutter.private_package_repository_name %} - - _Windows only_: - - Export the location of your private package repository credentials so that Docker Compose can load these as a [build and run time secret](https://docs.docker.com/compose/compose-file/compose-file-v3/#secrets-configuration-reference): - ```bat - setx POETRY_AUTH_TOML_PATH %APPDATA%\pypoetry\auth.toml - ``` - {%- endif %} @@ -109,37 +97,6 @@ import {{ cookiecutter.__project_name_snake_case }} 1. [Install VS Code](https://code.visualstudio.com/) and [VS Code's Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers). Alternatively, install [PyCharm](https://www.jetbrains.com/pycharm/download/). 2. _Optional:_ install a [Nerd Font](https://www.nerdfonts.com/font-downloads) such as [FiraCode Nerd Font](https://github.com/ryanoasis/nerd-fonts/tree/master/patched-fonts/FiraCode) and [configure VS Code](https://github.com/tonsky/FiraCode/wiki/VS-Code-Instructions) or [configure PyCharm](https://github.com/tonsky/FiraCode/wiki/Intellij-products-instructions) to use it. - -{%- if cookiecutter.private_package_repository_name %} - -
-4. Configure Poetry to use the private package repository - -{% if cookiecutter.continuous_integration == "GitLab" -%} -1. [Create a personal access token](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#create-a-personal-access-token) with the `api` scope and use it to [add your private package repository credentials to your Poetry's `auth.toml` file](https://python-poetry.org/docs/repositories/#configuring-credentials): - ```toml - # Linux: ~/.config/pypoetry/auth.toml - # macOS: ~/Library/Application Support/pypoetry/auth.toml - # Windows: C:\Users\%USERNAME%\AppData\Roaming\pypoetry\auth.toml - [http-basic.{{ cookiecutter.private_package_repository_name|slugify }}] - username = "{personal access token name}" - password = "{personal access token}" - ``` -{%- else -%} -1. [Add your private package repository credentials to your Poetry's `auth.toml` file](https://python-poetry.org/docs/repositories/#configuring-credentials): - ```toml - # Linux: ~/.config/pypoetry/auth.toml - # macOS: ~/Library/Application Support/pypoetry/auth.toml - # Windows: C:\Users\%USERNAME%\AppData\Roaming\pypoetry\auth.toml - [http-basic.{{ cookiecutter.private_package_repository_name|slugify }}] - username = "{username}" - password = "{password}" - ``` -{%- endif %} - -
-{%- endif %} -
diff --git a/{{ cookiecutter.__project_name_kebab_case }}/docker-compose.yml b/{{ cookiecutter.__project_name_kebab_case }}/docker-compose.yml index 1eee7869..2242525e 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/docker-compose.yml +++ b/{{ cookiecutter.__project_name_kebab_case }}/docker-compose.yml @@ -6,41 +6,25 @@ services: build: context: . target: dev - {%- if cookiecutter.private_package_repository_name %} - secrets: - - poetry-auth - {%- endif %} args: PYTHON_VERSION: ${PYTHON_VERSION:-{{ cookiecutter.python_version }}} UID: ${UID:-1000} GID: ${GID:-1000} - {%- if not cookiecutter.private_package_repository_name %} environment: - POETRY_PYPI_TOKEN_PYPI - {%- else %} - secrets: - - poetry-auth - {%- endif %} volumes: - ..:/workspaces - command-history-volume:/home/user/.history/ - app: + api: build: context: . - target: app - {%- if cookiecutter.private_package_repository_name %} - secrets: - - poetry-auth - {%- endif %} + target: api tty: true - {%- if cookiecutter.with_fastapi_api|int %} ports: - "8000:8000" - {%- endif %} profiles: - - app - {%- endif %} + - api volumes: command-history-volume: From 491f1fa0ab418d3dc92075222f08f1d8041f16c4 Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Mon, 3 Jun 2024 13:30:00 +0000 Subject: [PATCH 39/68] fix(README.md): fixed jinja template --- {{ cookiecutter.__project_name_kebab_case }}/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/{{ cookiecutter.__project_name_kebab_case }}/README.md b/{{ cookiecutter.__project_name_kebab_case }}/README.md index 33335eee..71411521 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/README.md +++ b/{{ cookiecutter.__project_name_kebab_case }}/README.md @@ -3,7 +3,6 @@ # {{ cookiecutter.project_name }} {{ cookiecutter.project_description }} -{%- if cookiecutter.project_type == "package" or cookiecutter.with_typer_cli|int %} ## Installing From b42b3eb2628817fd5ac96f9d0bd7127b0b4faba8 Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Mon, 3 Jun 2024 14:07:58 +0000 Subject: [PATCH 40/68] fix(README.md): fix private --- {{ cookiecutter.__project_name_kebab_case }}/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/{{ cookiecutter.__project_name_kebab_case }}/README.md b/{{ cookiecutter.__project_name_kebab_case }}/README.md index 71411521..dd10b1b0 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/README.md +++ b/{{ cookiecutter.__project_name_kebab_case }}/README.md @@ -1,9 +1,10 @@ -[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url={{ cookiecutter.project_url.replace("https://", "git@").replace(".com/", ".com:") if cookiecutter.private_package_repository_url else cookiecutter.project_url }}) +[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url={{cookiecutter.project_url}}) # {{ cookiecutter.project_name }} {{ cookiecutter.project_description }} + ## Installing To install this package, run: @@ -105,7 +106,7 @@ The following development environments are supported: {% if cookiecutter.continuous_integration == "GitHub" %} 1. ⭐️ _GitHub Codespaces_: click on _Code_ and select _Create codespace_ to start a Dev Container with [GitHub Codespaces](https://github.com/features/codespaces). {%- endif %} -1. ⭐️ _Dev Container (with container volume)_: click on [Open in Dev Containers](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url={{ cookiecutter.project_url.replace("https://", "git@").replace(".com/", ".com:") if cookiecutter.private_package_repository_url else cookiecutter.project_url }}) to clone this repository in a container volume and create a Dev Container with VS Code. +1. ⭐️ _Dev Container (with container volume)_: click on [Open in Dev Containers](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url={{cookiecutter.project_url}}) to clone this repository in a container volume and create a Dev Container with VS Code. 1. _Dev Container_: clone this repository, open it with VS Code, and run Ctrl/⌘ + ⇧ + P β†’ _Dev Containers: Reopen in Container_. 1. _PyCharm_: clone this repository, open it with PyCharm, and [configure Docker Compose as a remote interpreter](https://www.jetbrains.com/help/pycharm/using-docker-compose-as-a-remote-interpreter.html#docker-compose-remote) with the `dev` service. 1. _Terminal_: clone this repository, open it with your terminal, and run `docker compose up --detach dev` to start a Dev Container in the background, and then run `docker compose exec dev zsh` to open a shell prompt in the Dev Container. From b33528fa2f46eb9f9499e8ce5fbdfde569c6e1e9 Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Mon, 3 Jun 2024 14:11:46 +0000 Subject: [PATCH 41/68] fix(devcontainer.json): Add warn level everywhere --- .../.devcontainer/devcontainer.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/{{ cookiecutter.__project_name_kebab_case }}/.devcontainer/devcontainer.json b/{{ cookiecutter.__project_name_kebab_case }}/.devcontainer/devcontainer.json index d86060ef..dbc7c5e9 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/.devcontainer/devcontainer.json +++ b/{{ cookiecutter.__project_name_kebab_case }}/.devcontainer/devcontainer.json @@ -53,11 +53,7 @@ "python.terminal.activateEnvironment": false, "python.testing.pytestEnabled": true, "ruff.importStrategy": "fromEnvironment", - {%- if cookiecutter.development_environment == "strict" % - } "ruff.logLevel": "warn", - {%- endif % - } "terminal.integrated.defaultProfile.linux": "zsh", "terminal.integrated.profiles.linux": { "zsh": { From bd018b53b79521a95e6caa5646ce468dc4173d85 Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Mon, 3 Jun 2024 15:27:20 +0000 Subject: [PATCH 42/68] fix(pyproject.toml): updated with lint.pylint --- {{ cookiecutter.__project_name_kebab_case }}/pyproject.toml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml b/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml index 6279d0e1..701f8800 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml +++ b/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml @@ -157,16 +157,13 @@ ban-relative-imports = "all" [tool.ruff.lint.isort] lines-after-imports = 2 -{%- if cookiecutter.development_environment == "strict" %} - [tool.ruff.lint.pycodestyle] max-doc-length = 99 -{%- endif %} [tool.ruff.lint.pydocstyle] convention = "{{ cookiecutter.__docstring_style|lower }}" -[tool.ruff.pylint] +[tool.ruff.lint.pylint] max-args = 6 [tool.poe.tasks] # https://github.com/nat-n/poethepoet From 76ce8e4244e23df626faa0eac077d5359db1f937 Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Mon, 3 Jun 2024 15:32:10 +0000 Subject: [PATCH 43/68] fix(pyproject.toml): updated ruff --- {{ cookiecutter.__project_name_kebab_case }}/pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml b/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml index 701f8800..db28deb1 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml +++ b/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml @@ -121,9 +121,9 @@ fix = true line-length = 99 src = ["src", "tests"] target-version = "py{{ cookiecutter.python_version.split('.')[:2]|join }}" +lint.ignore-init-module-imports = true [tool.ruff.lint] -ignore-init-module-imports = true {%- if cookiecutter.development_environment == "strict" %} select = ["ALL"] ignore = [ @@ -147,7 +147,7 @@ ignore = [ unfixable = ["ERA001", "F401", "F841", "T201", "T203"] {%- else %} select = ["A", "ASYNC", "B", "C4", "C90", "D", "DTZ", "E", "F", "FLY", "FURB", "I", "ISC", "LOG", "N", "NPY", "PERF", "PGH", "PIE", "PL", "PT", "Q", "RET", "RUF", "RSE", "SIM", "TID", "UP", "W", "YTT"] -ignore = ["D203", "D213", "E501", "PGH002", "PGH003", "RET504", "S101", "S307"] +ignore = ["D203", "D213", "E501", "G010", "PGH003", "RET504", "S101", "S307"] unfixable = ["F401", "F841"] {%- endif %} From dcfc7ef70afa0c28e3c0f4279ade6b4183d83f79 Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Mon, 3 Jun 2024 15:55:23 +0000 Subject: [PATCH 44/68] fix(pyproject.toml): removed lint ruff --- .../pyproject.toml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml b/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml index db28deb1..9b384e08 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml +++ b/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml @@ -80,7 +80,7 @@ source = ["src"] [tool.coverage.html] # https://coverage.readthedocs.io/en/latest/cmd.html#html-reporting-coverage-html directory = "reports/htmlcov" -[tool.coverage.xml] # https://coverage.readthedocs.io/en/latest/cmd.html#html-reporting-coverage-html +[tool.coverage.xml] # https://coverage.readthedocs.io/en/latest/cmd.html#cmd-xml directory = "reports/coverage.xml" [tool.mypy] # https://mypy.readthedocs.io/en/latest/config_file.html @@ -121,7 +121,6 @@ fix = true line-length = 99 src = ["src", "tests"] target-version = "py{{ cookiecutter.python_version.split('.')[:2]|join }}" -lint.ignore-init-module-imports = true [tool.ruff.lint] {%- if cookiecutter.development_environment == "strict" %} @@ -131,11 +130,8 @@ ignore = [ "ANN003", # Missing type annotation for kwargs. Complicates the code too much with the current annotation system. "ANN101", # Missing type annotation for self in method. Useless info as it's always the same and clutters the code. "ANN102", # Missing type annotation for cls in classmethod. Useless info as it's always the same and clutters the code. - "ANN401", # Typing with Any is not permitted. - "ARG002", # Unused argument. Sometimes it's useful to keep the argument for future use, for retrocompatibility or to abide by an interface. "COM812", # Missing trailing comma in a single-line list. Already handled by ruff formater "CPY001", # Copyright notice missing. Not always needed. - "D417", # Missing argument descriptions in the docstring. Not always needed. "E501", # Line too long. Already handled by ruff formater "E731", # Do not assign a lambda expression. Do not agree with this rule, it improves readability as it is self-documenting the lambda function. "PD901", # Do not use the variable name "df". This is a common name for dataframes and is not a problem. @@ -260,4 +256,4 @@ max-args = 6 cmd = "coverage html" [[tool.poe.tasks.test.sequence]] - cmd = "coverage xml" \ No newline at end of file + cmd = "coverage xml" \ No newline at end of file From 641c59fc544ca1224083ca1bc3846285f8e3fc36 Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Mon, 3 Jun 2024 19:52:23 +0000 Subject: [PATCH 45/68] fix(pyproject.toml): removed furb --- {{ cookiecutter.__project_name_kebab_case }}/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml b/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml index 9b384e08..7788e371 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml +++ b/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml @@ -142,7 +142,7 @@ ignore = [ ] unfixable = ["ERA001", "F401", "F841", "T201", "T203"] {%- else %} -select = ["A", "ASYNC", "B", "C4", "C90", "D", "DTZ", "E", "F", "FLY", "FURB", "I", "ISC", "LOG", "N", "NPY", "PERF", "PGH", "PIE", "PL", "PT", "Q", "RET", "RUF", "RSE", "SIM", "TID", "UP", "W", "YTT"] +select = ["A", "ASYNC", "B", "C4", "C90", "D", "DTZ", "E", "F", "FLY", "I", "ISC", "LOG", "N", "NPY", "PERF", "PGH", "PIE", "PL", "PT", "Q", "RET", "RUF", "RSE", "SIM", "TID", "UP", "W", "YTT"] ignore = ["D203", "D213", "E501", "G010", "PGH003", "RET504", "S101", "S307"] unfixable = ["F401", "F841"] {%- endif %} From 779aa4f4eb164adf249201ccee2e356b2bf61e71 Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Tue, 4 Jun 2024 11:24:56 +0000 Subject: [PATCH 46/68] fix(pyproject): fixed assert in tests --- {{ cookiecutter.__project_name_kebab_case }}/pyproject.toml | 3 +++ .../src/{{ cookiecutter.__project_name_snake_case }}/api.py | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml b/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml index 7788e371..dabfaa41 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml +++ b/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml @@ -147,6 +147,9 @@ ignore = ["D203", "D213", "E501", "G010", "PGH003", "RET504", "S101", "S307"] unfixable = ["F401", "F841"] {%- endif %} +[tool.ruff.lint.per-file-ignores] +"tests/*" = ["S101", "INP001", "D100", "D101", "SLF"] + [tool.ruff.lint.flake8-tidy-imports] ban-relative-imports = "all" diff --git a/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/api.py b/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/api.py index 02bd6a94..7a78e3ac 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/api.py +++ b/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/api.py @@ -10,7 +10,7 @@ @asynccontextmanager -async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: +async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: # noqa: ARG001 """Handle FastAPI startup and shutdown events.""" # Startup events: # - Remove all handlers associated with the root logger object. @@ -34,3 +34,7 @@ def fibonacci(n: int) -> int: result = await asyncio.to_thread(fibonacci, n) return result + +if __name__ == '__main__': + import uvicorn + uvicorn.run("main:app", port=8000, log_level="info") From ea4b0620e41b7c21c362701bfe08d731a6c5edee Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Tue, 4 Jun 2024 11:29:14 +0000 Subject: [PATCH 47/68] refactor(cookiecutter.json): made strict mode first --- cookiecutter.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cookiecutter.json b/cookiecutter.json index 9338fef4..bed09f4a 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -9,8 +9,8 @@ "continuous_integration": "GitHub", "teamwork_uri": "", "development_environment": [ - "simple", - "strict" + "strict", + "simple" ], "with_conventional_commits": "{% if cookiecutter.development_environment == 'simple' %}0{% else %}1{% endif %}", "with_fastapi_api": "1", From 378d587aedf97bbc0489b5401600c57d4f7ba427 Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Tue, 4 Jun 2024 11:36:45 +0000 Subject: [PATCH 48/68] fix(README.md): removed unused doc --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index 13c2f14e..8384c087 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,6 @@ A modern [Cookiecutter](https://github.com/cookiecutter/cookiecutter) template for scaffolding Python packages and apps. -## 🍿 Demo - -See πŸ‘– [Conformal Tights](https://github.com/radix-ai/conformal-tights) for an example of a Python package that is scaffolded with this template. Contributing to this package can be done with a single click by [starting a GitHub Codespace](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=765698489&skip_quickstart=true) or [starting a Dev Container](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/radix-ai/conformal-tights). ## 🎁 Features @@ -14,13 +11,12 @@ See πŸ‘– [Conformal Tights](https://github.com/radix-ai/conformal-tights) for an - 🌈 Cross-platform support for Linux, macOS (Apple silicon and Intel), and Windows - 🐚 Modern shell prompt with [Starship](https://github.com/starship/starship) - πŸ“¦ Packaging and dependency management with [Poetry](https://github.com/python-poetry/poetry) -- 🚚 Installing from and publishing to private package repositories and [PyPI](https://pypi.org/) - ⚑️ Task running with [Poe the Poet](https://github.com/nat-n/poethepoet) - ✍️ Code formatting with [Ruff](https://github.com/charliermarsh/ruff) - βœ… Code linting with [Pre-commit](https://pre-commit.com/), [Mypy](https://github.com/python/mypy), and [Ruff](https://github.com/charliermarsh/ruff) - 🏷 Optionally follows the [Conventional Commits](https://www.conventionalcommits.org/) standard to automate [Semantic Versioning](https://semver.org/) and [Keep A Changelog](https://keepachangelog.com/) with [Commitizen](https://github.com/commitizen-tools/commitizen) - πŸ’Œ Verified commits with [GPG](https://gnupg.org/) -- ♻️ Continuous integration with [GitHub Actions](https://docs.github.com/en/actions) or [GitLab CI/CD](https://docs.gitlab.com/ee/ci/) +- ♻️ Continuous integration with [GitHub Actions](https://docs.github.com/en/actions) - πŸ§ͺ Test coverage with [Coverage.py](https://github.com/nedbat/coveragepy) - πŸ— Scaffolding updates with [Cookiecutter](https://github.com/cookiecutter/cookiecutter) and [Cruft](https://github.com/cruft/cruft) - 🧰 Dependency updates with [Dependabot](https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/about-dependabot-version-updates) From 5e76dbff7b1ba3dd8ff7924c6ecaa4cac3d8b781 Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Tue, 4 Jun 2024 11:51:42 +0000 Subject: [PATCH 49/68] fix(project): reformated files to pass ruff check --- .../src/{{ cookiecutter.__project_name_snake_case }}/api.py | 2 +- .../src/{{ cookiecutter.__project_name_snake_case }}/cli.py | 4 +++- .../tests/test_api.py | 3 ++- .../tests/test_cli.py | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/api.py b/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/api.py index 7a78e3ac..7f29f9c5 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/api.py +++ b/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/api.py @@ -35,6 +35,6 @@ def fibonacci(n: int) -> int: result = await asyncio.to_thread(fibonacci, n) return result -if __name__ == '__main__': +if __name__ == "__main__": import uvicorn uvicorn.run("main:app", port=8000, log_level="info") diff --git a/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/cli.py b/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/cli.py index 142b1860..e65a1afe 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/cli.py +++ b/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/cli.py @@ -3,10 +3,12 @@ import typer from rich import print + app = typer.Typer() @app.command() def fire(name: str = "Chell") -> None: """Fire portal gun.""" - print(f"[bold red]Alert![/bold red] {name} fired [green]portal gun[/green] :boom:") + print( + f"[bold red]Alert![/bold red] {name} fired [green]portal gun[/green] :boom:") diff --git a/{{ cookiecutter.__project_name_kebab_case }}/tests/test_api.py b/{{ cookiecutter.__project_name_kebab_case }}/tests/test_api.py index 34d0b5a9..cfc5a8ff 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/tests/test_api.py +++ b/{{ cookiecutter.__project_name_kebab_case }}/tests/test_api.py @@ -3,7 +3,8 @@ import httpx from fastapi.testclient import TestClient -from {{ cookiecutter.__project_name_snake_case }}.api import app +from {{cookiecutter.__project_name_snake_case}}.api import app + client = TestClient(app) diff --git a/{{ cookiecutter.__project_name_kebab_case }}/tests/test_cli.py b/{{ cookiecutter.__project_name_kebab_case }}/tests/test_cli.py index 7ff89f4f..c9bd159f 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/tests/test_cli.py +++ b/{{ cookiecutter.__project_name_kebab_case }}/tests/test_cli.py @@ -2,7 +2,8 @@ from typer.testing import CliRunner -from {{ cookiecutter.__project_name_snake_case }}.cli import app +from {{cookiecutter.__project_name_snake_case}}.cli import app + runner = CliRunner() From 4bb4ccfd2df6ebad3123bd61b377bcf9a6f1d41a Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Tue, 4 Jun 2024 11:58:33 +0000 Subject: [PATCH 50/68] fix(pyproject.toml): removed rule that conflicts with the formatter --- {{ cookiecutter.__project_name_kebab_case }}/pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml b/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml index dabfaa41..628b48ce 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml +++ b/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml @@ -139,6 +139,7 @@ ignore = [ "S311", # Standard pseudo-random generators are not suitable for security/cryptographic purposes. Our use case is not security/cryptographic. "TRY003", # Raise exception with vanilla arguments. Creating exception classes should be done only when really needed. "W505", # Doc line too long. Editor autowraps doc, and it's too much work to fix it. + "ISC001", # This rule may cause conflicts when used with the formatter ] unfixable = ["ERA001", "F401", "F841", "T201", "T203"] {%- else %} From 89295b591780eda1c3eb8fc96422badad050a397 Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Tue, 4 Jun 2024 12:06:17 +0000 Subject: [PATCH 51/68] fix(project): fixed files so they pass ruff --- .../src/{{ cookiecutter.__project_name_snake_case }}/api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/api.py b/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/api.py index 7f29f9c5..2ae4ed6b 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/api.py +++ b/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/api.py @@ -35,6 +35,8 @@ def fibonacci(n: int) -> int: result = await asyncio.to_thread(fibonacci, n) return result + if __name__ == "__main__": import uvicorn + uvicorn.run("main:app", port=8000, log_level="info") From 5137d5f9f38fbc51037625d1d214b4a0e42790a0 Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur <46036275+GameSetAndMatch@users.noreply.github.com> Date: Tue, 4 Jun 2024 08:18:35 -0400 Subject: [PATCH 52/68] Update cli.py fix(cli.py): fixed ruff format --- .../src/{{ cookiecutter.__project_name_snake_case }}/cli.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/cli.py b/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/cli.py index e65a1afe..67e029a9 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/cli.py +++ b/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/cli.py @@ -10,5 +10,4 @@ @app.command() def fire(name: str = "Chell") -> None: """Fire portal gun.""" - print( - f"[bold red]Alert![/bold red] {name} fired [green]portal gun[/green] :boom:") + print(f"[bold red]Alert![/bold red] {name} fired [green]portal gun[/green] :boom:") From ec77b0889b669db5d552ec31613404d3f15417ef Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Fri, 7 Jun 2024 12:59:49 -0400 Subject: [PATCH 53/68] fix(docker-compose.yml): add dev service --- .../docker-compose.yml | 22 +++++++++++++++++++ .../pyproject.toml | 3 ++- .../api.py | 2 +- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/{{ cookiecutter.__project_name_kebab_case }}/docker-compose.yml b/{{ cookiecutter.__project_name_kebab_case }}/docker-compose.yml index 2242525e..becd4906 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/docker-compose.yml +++ b/{{ cookiecutter.__project_name_kebab_case }}/docker-compose.yml @@ -16,6 +16,28 @@ services: - ..:/workspaces - command-history-volume:/home/user/.history/ + dev: + extends: devcontainer + stdin_open: true + tty: true + entrypoint: [] + command: + [ + "sh", + "-c", + "sudo chown user $$SSH_AUTH_SOCK && cp --update /opt/build/poetry/poetry.lock /workspaces/{{ cookiecutter.__project_name_kebab_case }}/ && mkdir -p /workspaces/{{ cookiecutter.__project_name_kebab_case }}/.git/hooks/ && cp --update /opt/build/git/* /workspaces/{{ cookiecutter.__project_name_kebab_case }}/.git/hooks/ && zsh" + ] + environment: + - SSH_AUTH_SOCK=/run/host-services/ssh-auth.sock + ports: + - "8000" + volumes: + - ~/.gitconfig:/etc/gitconfig + - ~/.ssh/known_hosts:/home/user/.ssh/known_hosts + - ${SSH_AGENT_AUTH_SOCK:-/run/host-services/ssh-auth.sock}:/run/host-services/ssh-auth.sock + profiles: + - dev + api: build: context: . diff --git a/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml b/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml index 628b48ce..9397d2d7 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml +++ b/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml @@ -139,7 +139,8 @@ ignore = [ "S311", # Standard pseudo-random generators are not suitable for security/cryptographic purposes. Our use case is not security/cryptographic. "TRY003", # Raise exception with vanilla arguments. Creating exception classes should be done only when really needed. "W505", # Doc line too long. Editor autowraps doc, and it's too much work to fix it. - "ISC001", # This rule may cause conflicts when used with the formatter + "ISC001", # single-line-implicit-string-concatenation, `z = "The quick " "brown fox."` becomes `z = "The quick brown fox."` This rule may cause conflicts when used with the formatter +This rule may cause conflicts when used with the formatter ] unfixable = ["ERA001", "F401", "F841", "T201", "T203"] {%- else %} diff --git a/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/api.py b/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/api.py index 2ae4ed6b..34048d50 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/api.py +++ b/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/api.py @@ -39,4 +39,4 @@ def fibonacci(n: int) -> int: if __name__ == "__main__": import uvicorn - uvicorn.run("main:app", port=8000, log_level="info") + uvicorn.run(app, port=8000, log_level="info") From 8aefd9969146c4f1b1e3b0480d5c763694a4a849 Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Fri, 7 Jun 2024 13:11:16 -0400 Subject: [PATCH 54/68] fix(docker-compose.yml): removed duplicated text --- .../docker-compose.yml | 2 -- {{ cookiecutter.__project_name_kebab_case }}/pyproject.toml | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/{{ cookiecutter.__project_name_kebab_case }}/docker-compose.yml b/{{ cookiecutter.__project_name_kebab_case }}/docker-compose.yml index becd4906..b377c2e5 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/docker-compose.yml +++ b/{{ cookiecutter.__project_name_kebab_case }}/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3.9" - services: devcontainer: diff --git a/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml b/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml index 9397d2d7..bef39e1b 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml +++ b/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml @@ -139,8 +139,7 @@ ignore = [ "S311", # Standard pseudo-random generators are not suitable for security/cryptographic purposes. Our use case is not security/cryptographic. "TRY003", # Raise exception with vanilla arguments. Creating exception classes should be done only when really needed. "W505", # Doc line too long. Editor autowraps doc, and it's too much work to fix it. - "ISC001", # single-line-implicit-string-concatenation, `z = "The quick " "brown fox."` becomes `z = "The quick brown fox."` This rule may cause conflicts when used with the formatter -This rule may cause conflicts when used with the formatter + "ISC001", # single-line-implicit-string-concatenation, `z = "The quick " "brown fox."` becomes `z = "The quick brown fox."` This rule may cause conflicts when used with the formatter. ] unfixable = ["ERA001", "F401", "F841", "T201", "T203"] {%- else %} From 2a039e13eeb653946d428b307499317f42e7da12 Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Fri, 7 Jun 2024 13:21:15 -0400 Subject: [PATCH 55/68] fix(docker-compose.yml): add local port to dev service --- {{ cookiecutter.__project_name_kebab_case }}/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/{{ cookiecutter.__project_name_kebab_case }}/docker-compose.yml b/{{ cookiecutter.__project_name_kebab_case }}/docker-compose.yml index b377c2e5..16c56d77 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/docker-compose.yml +++ b/{{ cookiecutter.__project_name_kebab_case }}/docker-compose.yml @@ -28,7 +28,7 @@ services: environment: - SSH_AUTH_SOCK=/run/host-services/ssh-auth.sock ports: - - "8000" + - "8000:8000" volumes: - ~/.gitconfig:/etc/gitconfig - ~/.ssh/known_hosts:/home/user/.ssh/known_hosts From aa71fe21faadd39d9f9acd3340f8844901412800 Mon Sep 17 00:00:00 2001 From: Jean-Samuel Leboeuf Date: Tue, 18 Jun 2024 16:40:58 -0400 Subject: [PATCH 56/68] feat(pyproject.toml): add ruff rule to allow unused *args and **kwargs variables in function signature (#10) --- {{ cookiecutter.__project_name_kebab_case }}/pyproject.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml b/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml index bef39e1b..f800c088 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml +++ b/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml @@ -154,6 +154,9 @@ unfixable = ["F401", "F841"] [tool.ruff.lint.flake8-tidy-imports] ban-relative-imports = "all" +[tool.ruff.lint.flake8-unused-arguments] +ignore-variadic-names = true # Allow unused *args and **kwargs in function signature + [tool.ruff.lint.isort] lines-after-imports = 2 @@ -260,4 +263,4 @@ max-args = 6 cmd = "coverage html" [[tool.poe.tasks.test.sequence]] - cmd = "coverage xml" \ No newline at end of file + cmd = "coverage xml" From 1d1d945ef3245ffe813f6a283d18942667eb16d2 Mon Sep 17 00:00:00 2001 From: Olivier Belhumeur Date: Thu, 26 Sep 2024 18:30:34 +0000 Subject: [PATCH 57/68] feat(project): update with base repo --- README.md | 2 +- .../.devcontainer/devcontainer.json | 3 ++- {{ cookiecutter.__project_name_kebab_case }}/Dockerfile | 1 + {{ cookiecutter.__project_name_kebab_case }}/README.md | 2 +- {{ cookiecutter.__project_name_kebab_case }}/pyproject.toml | 2 +- 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8384c087..b01c3709 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,6 @@ To update your Python project to the latest template version: | `author_name`
"John Smith" | The full name of the primary author of the project. | | `author_email`
"" | The email address of the primary author of the project. | | `python_version`
"3.12" | The minimum Python version that the project requires. | -| `development_environment`
["simple", "strict"] | Whether to configure the development environment with a focus on simplicity or with a focus on strictness. In strict mode, additional [Ruff rules](https://beta.ruff.rs/docs/rules/) are added, and tools such as [Mypy](https://github.com/python/mypy) and [Pytest](https://github.com/pytest-dev/pytest) are set to strict mode. | +| `development_environment`
["simple", "strict"] | Whether to configure the development environment with a focus on simplicity or with a focus on strictness. In strict mode, additional [Ruff rules](https://docs.astral.sh/ruff/rules/) are added, and tools such as [Mypy](https://github.com/python/mypy) and [Pytest](https://github.com/pytest-dev/pytest) are set to strict mode. | | `with_fastapi_api`
["0", "1"] | If "1", [FastAPI](https://github.com/tiangolo/fastapi) is added as a run time dependency, FastAPI API stubs and tests are added, a `poe api` command for serving the API is added. | | `with_typer_cli`
["0", "1"] | If "1", [Typer](https://github.com/tiangolo/typer) is added as a run time dependency, Typer CLI stubs and tests are added, the package itself is registered as a CLI. | diff --git a/{{ cookiecutter.__project_name_kebab_case }}/.devcontainer/devcontainer.json b/{{ cookiecutter.__project_name_kebab_case }}/.devcontainer/devcontainer.json index dbc7c5e9..1cffd0cc 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/.devcontainer/devcontainer.json +++ b/{{ cookiecutter.__project_name_kebab_case }}/.devcontainer/devcontainer.json @@ -44,6 +44,7 @@ "/usr/local/bin/python" ], "mypy-type-checker.importStrategy": "fromEnvironment", + "mypy-type-checker.preferDaemon": true, "notebook.codeActionsOnSave": { "notebook.source.fixAll": "explicit", "notebook.source.organizeImports": "explicit" @@ -53,7 +54,7 @@ "python.terminal.activateEnvironment": false, "python.testing.pytestEnabled": true, "ruff.importStrategy": "fromEnvironment", - "ruff.logLevel": "warn", + "ruff.logLevel": "warning", "terminal.integrated.defaultProfile.linux": "zsh", "terminal.integrated.profiles.linux": { "zsh": { diff --git a/{{ cookiecutter.__project_name_kebab_case }}/Dockerfile b/{{ cookiecutter.__project_name_kebab_case }}/Dockerfile index fecace7f..1df6495d 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/Dockerfile +++ b/{{ cookiecutter.__project_name_kebab_case }}/Dockerfile @@ -72,6 +72,7 @@ RUN --mount=type=cache,target=/var/cache/apt/ \ sh -c "$(curl -fsSL https://starship.rs/install.sh)" -- "--yes" && \ usermod --shell /usr/bin/zsh user && \ echo 'user ALL=(root) NOPASSWD:ALL' > /etc/sudoers.d/user && chmod 0440 /etc/sudoers.d/user +RUN git config --system --add safe.directory '*' USER user # Install the development Python dependencies in the virtual environment. diff --git a/{{ cookiecutter.__project_name_kebab_case }}/README.md b/{{ cookiecutter.__project_name_kebab_case }}/README.md index dd10b1b0..779c2df1 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/README.md +++ b/{{ cookiecutter.__project_name_kebab_case }}/README.md @@ -1,4 +1,4 @@ -[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url={{cookiecutter.project_url}}) +[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url={{cookiecutter.project_url}}){% if cookiecutter.continuous_integration == "GitHub" %} [![Open in GitHub Codespaces](https://img.shields.io/static/v1?label=GitHub%20Codespaces&message=Open&color=blue&logo=github)](https://github.com/codespaces/new/{{ cookiecutter.project_url.replace("https://github.com/", "") }}){% endif %} # {{ cookiecutter.project_name }} diff --git a/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml b/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml index f800c088..58b502e1 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml +++ b/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml @@ -50,7 +50,7 @@ pre-commit = ">=3.7.0" pytest = ">=8.1.1" pytest-mock = ">=3.14.0" pytest-xdist = ">=3.5.0" -ruff = ">=0.3.5" +ruff = ">=0.5.7" {%- if cookiecutter.development_environment == "strict" %} safety = ">=3.1.0" shellcheck-py = ">=0.10.0.1" From e13dc5f274a72aa7c0bbd50df45a93f04a9343c6 Mon Sep 17 00:00:00 2001 From: Jean-Samuel Leboeuf Date: Thu, 16 Jan 2025 15:38:11 -0500 Subject: [PATCH 58/68] Update .pre-commit-config.yaml to avoid problems with docs and PDF (#13) * Update .pre-commit-config.yaml to avoid problems with docs and PDF * Create adr_template.md * Fix ruff in cli.py --- .../.pre-commit-config.yaml | 3 +- .../docs/decisions/adr_template.md | 79 +++++++++++++++++++ .../cli.py | 2 +- 3 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 {{ cookiecutter.__project_name_kebab_case }}/docs/decisions/adr_template.md diff --git a/{{ cookiecutter.__project_name_kebab_case }}/.pre-commit-config.yaml b/{{ cookiecutter.__project_name_kebab_case }}/.pre-commit-config.yaml index e5372574..d3e077ff 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/.pre-commit-config.yaml +++ b/{{ cookiecutter.__project_name_kebab_case }}/.pre-commit-config.yaml @@ -38,6 +38,7 @@ repos: types: [python] - id: fix-byte-order-marker - id: mixed-line-ending + exclude: ^docs/.* - id: name-tests-test args: [--pytest-test-first] - id: trailing-whitespace @@ -84,4 +85,4 @@ repos: name: mypy entry: mypy language: system - types: [python] \ No newline at end of file + types: [python] diff --git a/{{ cookiecutter.__project_name_kebab_case }}/docs/decisions/adr_template.md b/{{ cookiecutter.__project_name_kebab_case }}/docs/decisions/adr_template.md new file mode 100644 index 00000000..1cfb4c68 --- /dev/null +++ b/{{ cookiecutter.__project_name_kebab_case }}/docs/decisions/adr_template.md @@ -0,0 +1,79 @@ +--- +# These are optional elements. Feel free to remove any of them. +status: "{proposed | rejected | accepted | deprecated | … | superseded by [ADR-0005](0005-example.md)}" +date: {YYYY-MM-DD when the decision was last updated} +deciders: {list everyone involved in the decision} +consulted: {list everyone whose opinions are sought (typically subject-matter experts); and with whom there is a two-way communication} +informed: {list everyone who is kept up-to-date on progress; and with whom there is a one-way communication} +--- +# {short title of solved problem and solution} + +## Context and Problem Statement + +{Describe the context and problem statement, e.g., in free form using two to three sentences or in the form of an illustrative story. + You may want to articulate the problem in form of a question and add links to collaboration boards or issue management systems.} + + +## Decision Drivers + +* {decision driver 1, e.g., a force, facing concern, …} +* {decision driver 2, e.g., a force, facing concern, …} +* … + +## Considered Options + +* {title of option 1} +* {title of option 2} +* {title of option 3} +* … + +## Decision Outcome + +Chosen option: "{title of option 1}", because +{justification. e.g., only option, which meets k.o. criterion decision driver | which resolves force {force} | … | comes out best (see below)}. + + +### Consequences + +* Good, because {positive consequence, e.g., improvement of one or more desired qualities, …} +* Bad, because {negative consequence, e.g., compromising one or more desired qualities, …} +* … + + +### Confirmation + +{Describe how the implementation of/compliance with the ADR is confirmed. E.g., by a review or an ArchUnit test. + Although we classify this element as optional, it is included in most ADRs.} + + +## Pros and Cons of the Options + +### {title of option 1} + + +{example | description | pointer to more information | …} + +* Good, because {argument a} +* Good, because {argument b} + +* Neutral, because {argument c} +* Bad, because {argument d} +* … + +### {title of other option} + +{example | description | pointer to more information | …} + +* Good, because {argument a} +* Good, because {argument b} +* Neutral, because {argument c} +* Bad, because {argument d} +* … + + +## More Information + +{You might want to provide additional evidence/confidence for the decision outcome here and/or + document the team agreement on the decision and/or + define when/how this decision the decision should be realized and if/when it should be re-visited. +Links to other decisions and resources might appear here as well.} diff --git a/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/cli.py b/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/cli.py index 67e029a9..10cf3386 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/cli.py +++ b/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/cli.py @@ -1,7 +1,7 @@ """{{ cookiecutter.project_name }} CLI.""" import typer -from rich import print +from rich import print # noqa: A004 app = typer.Typer() From 03f582f0cd873000d9de84a2e229bdaf5a4337e5 Mon Sep 17 00:00:00 2001 From: Jean-Samuel Leboeuf Date: Fri, 2 May 2025 11:34:14 -0400 Subject: [PATCH 59/68] feat(pre-commit): update precommit hooks (#15) --- .../.pre-commit-config.yaml | 9 +++++++-- .../pyproject.toml | 9 +++++++++ .../tests/{test_api.py => api_test.py} | 0 .../tests/{test_cli.py => cli_test.py} | 0 .../tests/{test_import.py => import_test.py} | 0 5 files changed, 16 insertions(+), 2 deletions(-) rename {{ cookiecutter.__project_name_kebab_case }}/tests/{test_api.py => api_test.py} (100%) rename {{ cookiecutter.__project_name_kebab_case }}/tests/{test_cli.py => cli_test.py} (100%) rename {{ cookiecutter.__project_name_kebab_case }}/tests/{test_import.py => import_test.py} (100%) diff --git a/{{ cookiecutter.__project_name_kebab_case }}/.pre-commit-config.yaml b/{{ cookiecutter.__project_name_kebab_case }}/.pre-commit-config.yaml index d3e077ff..e3eca528 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/.pre-commit-config.yaml +++ b/{{ cookiecutter.__project_name_kebab_case }}/.pre-commit-config.yaml @@ -19,6 +19,7 @@ repos: rev: v4.5.0 hooks: - id: check-added-large-files + args: ['--maxkb=50000'] - id: check-ast - id: check-builtin-literals - id: check-case-conflict @@ -38,9 +39,8 @@ repos: types: [python] - id: fix-byte-order-marker - id: mixed-line-ending - exclude: ^docs/.* - id: name-tests-test - args: [--pytest-test-first] + args: [--pytest] - id: trailing-whitespace types: [python] - repo: local @@ -61,6 +61,7 @@ repos: require_serial: true language: system types_or: [python, pyi] + pass_filenames: false - id: ruff-format name: ruff format entry: ruff format @@ -68,6 +69,7 @@ repos: require_serial: true language: system types_or: [python, pyi] + pass_filenames: false {%- if cookiecutter.development_environment == "strict" %} - id: shellcheck name: shellcheck @@ -75,6 +77,7 @@ repos: args: [--check-sourced] language: system types: [shell] + pass_filenames: false {%- endif %} - id: poetry-check name: poetry check @@ -86,3 +89,5 @@ repos: entry: mypy language: system types: [python] + pass_filenames: false + diff --git a/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml b/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml index 58b502e1..71d2ed51 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml +++ b/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml @@ -99,6 +99,15 @@ show_column_numbers = true show_error_codes = true show_error_context = true warn_unreachable = true +files = ["src/**/*.py", "tests/**/*.py"] + +[[tool.mypy.overrides]] +module = "tests.*" +disable_error_code = [ + "method-assign", # Necessary when mocking + "attr-defined", # Mocked attributes are dynamically defined +] + {%- if cookiecutter.development_environment == "strict" and cookiecutter.with_fastapi_api|int %} [tool.pydantic-mypy] # https://pydantic-docs.helpmanual.io/mypy_plugin/#configuring-the-plugin diff --git a/{{ cookiecutter.__project_name_kebab_case }}/tests/test_api.py b/{{ cookiecutter.__project_name_kebab_case }}/tests/api_test.py similarity index 100% rename from {{ cookiecutter.__project_name_kebab_case }}/tests/test_api.py rename to {{ cookiecutter.__project_name_kebab_case }}/tests/api_test.py diff --git a/{{ cookiecutter.__project_name_kebab_case }}/tests/test_cli.py b/{{ cookiecutter.__project_name_kebab_case }}/tests/cli_test.py similarity index 100% rename from {{ cookiecutter.__project_name_kebab_case }}/tests/test_cli.py rename to {{ cookiecutter.__project_name_kebab_case }}/tests/cli_test.py diff --git a/{{ cookiecutter.__project_name_kebab_case }}/tests/test_import.py b/{{ cookiecutter.__project_name_kebab_case }}/tests/import_test.py similarity index 100% rename from {{ cookiecutter.__project_name_kebab_case }}/tests/test_import.py rename to {{ cookiecutter.__project_name_kebab_case }}/tests/import_test.py From 0f2673d71497e209b164a9581d3da9be743653ab Mon Sep 17 00:00:00 2001 From: Vincent Gagnon Date: Mon, 2 Jun 2025 09:42:54 -0400 Subject: [PATCH 60/68] Small updates (#16) * fix(Dockerfile): Small Dockerfile optimizations * fix(README): Update README for starter project * fix: changed logging to loguru * refactor: Update README, upgrade poetry version in Dockerfile * fix(Dockerfile): fixed poetry install command * fix typo in README --------- Co-authored-by: Jean-Samuel Leboeuf --- .../.env_sample | 1 + .../CONTRIBUTING.md | 164 +++++++++++++++ .../Dockerfile | 27 ++- .../README.md | 188 ++++++++---------- .../pyproject.toml | 3 +- .../api.py | 15 +- 6 files changed, 271 insertions(+), 127 deletions(-) create mode 100644 {{ cookiecutter.__project_name_kebab_case }}/.env_sample create mode 100644 {{ cookiecutter.__project_name_kebab_case }}/CONTRIBUTING.md diff --git a/{{ cookiecutter.__project_name_kebab_case }}/.env_sample b/{{ cookiecutter.__project_name_kebab_case }}/.env_sample new file mode 100644 index 00000000..a17418ab --- /dev/null +++ b/{{ cookiecutter.__project_name_kebab_case }}/.env_sample @@ -0,0 +1 @@ +LOG_LEVEL=INFO \ No newline at end of file diff --git a/{{ cookiecutter.__project_name_kebab_case }}/CONTRIBUTING.md b/{{ cookiecutter.__project_name_kebab_case }}/CONTRIBUTING.md new file mode 100644 index 00000000..462804ec --- /dev/null +++ b/{{ cookiecutter.__project_name_kebab_case }}/CONTRIBUTING.md @@ -0,0 +1,164 @@ +# Contributing to {{ cookiecutter.project_name }} project + +## Using + +To serve this app, run: + +```sh +docker compose up app +``` + +and open [localhost:8000](http://localhost:8000) in your browser. + +Within the Dev Container this is equivalent to: + +```sh +poe api +``` + +## Contributing + +### Prerequisites + +
+1. Set up Git to use SSH + +1. [Generate an SSH key](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent#generating-a-new-ssh-key) and [add the SSH key to your GitHub account](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/adding-a-new-ssh-key-to-your-github-account). +1. Configure SSH to automatically load your SSH keys: + + ```sh + cat << EOF >> ~/.ssh/config + + Host * + AddKeysToAgent yes + IgnoreUnknown UseKeychain + UseKeychain yes + ForwardAgent yes + EOF + ``` + +
+ +
+2. Install Docker + +1. [Install Docker Desktop](https://www.docker.com/get-started). + - _Linux only_: + - Export your user's user id and group id so that [files created in the Dev Container are owned by your user](https://github.com/moby/moby/issues/3206): + + ```sh + cat << EOF >> ~/.bashrc + + export UID=$(id --user) + export GID=$(id --group) + EOF + ``` + +
+ +
+3. Install VS Code or PyCharm + +1. [Install VS Code](https://code.visualstudio.com/) and [VS Code's Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers). Alternatively, install [PyCharm](https://www.jetbrains.com/pycharm/download/). +2. _Optional:_ install a [Nerd Font](https://www.nerdfonts.com/font-downloads) such as [FiraCode Nerd Font](https://github.com/ryanoasis/nerd-fonts/tree/master/patched-fonts/FiraCode) and [configure VS Code](https://github.com/tonsky/FiraCode/wiki/VS-Code-Instructions) or [configure PyCharm](https://github.com/tonsky/FiraCode/wiki/Intellij-products-instructions) to use it. + +
+ +### Development environments + +
+Dev container environments + +You can develop "remotely" inside a container using one of the following development environments: + +1. ⭐️ _GitHub Codespaces_: click on _Code_ and select _Create codespace_ to start a Dev Container with [GitHub Codespaces](https://github.com/features/codespaces). +1. ⭐️ _Dev Container (with container volume)_: click on "Open in Dev Containers" to clone this repository in a container volume and create a Dev Container with VS Code. +1. _Dev Container_: clone this repository, open it with VS Code, and run Ctrl/⌘ + ⇧ + P β†’ _Dev Containers: Reopen in Container_. +1. _PyCharm_: clone this repository, open it with PyCharm, and [configure Docker Compose as a remote interpreter](https://www.jetbrains.com/help/pycharm/using-docker-compose-as-a-remote-interpreter.html#docker-compose-remote) with the `dev` service. +1. _Terminal_: clone this repository, open it with your terminal, and run `docker compose up --detach dev` to start a Dev Container in the background, and then run `docker compose exec dev zsh` to open a shell prompt in the Dev Container. + +
+ +
+Local development + +To develop locally, you'll have to install manually some tools. +First, initialize a virtual environment for the project with +```poetry shell``` +and then install the dependencies and the project with +```poetry install``` + +
+ +
+Extra steps + +After setting up the developpement environnement, you'll have to create an .env file at the root of the project, copy the contents of .env_sample into this new file and fill all of the variables with the appropriate values. + +
+ +### Development tools + +The following tools will be automatically installed by poetry to support development: + +- _commitizen_: This project follows the [Conventional Commits](https://www.conventionalcommits.org/) standard to automate [Semantic Versioning](https://semver.org/) and [Keep A Changelog](https://keepachangelog.com/) with [Commitizen](https://github.com/commitizen-tools/commitizen). + +- _pre-commit_: This project uses pre-commit hooks that enforces the submitted code to respect conventions and high quality standards. + +- _pytest_: This project uses the `pytest` framework for unit testing. + +- _ruff_: This project uses `ruff` to lint and automatically format code in order to maintain consistent code across all project and developpers. + +- _mypy_: This project uses the static type checker `mypy` to enforce type annotation and spot bugs before they can happen. + +We use the following integration tools: + +- _github actions_: This project uses GitHub actions to execute verifications in the cloud before allowing to merge with the main branch. + +### Development workflow + +1. Open new branch. +1. Write code. +1. Write tests. +1. Run tests and linter with `poe test` and `poe lint`. +1. Commit code with `cz commit` and follow the instructions. + 1. Retry failed commit (after fixing) with `cz commit --retry`. + 1. _Only in emergencies_, commit with `git commit --no-verify`. +1. Push commits with `git push`. +1. When branch is fully functional, open a pull requests on GitHub and ask for a review. + 1. When approved, bump the versions with `cz bump`. + 1. Build the doc with `poe doc`. + 1. Push to GitHub one last time before merging. +1. Repeat. + +### Good development practices + +- Most if not all functions and methods should be tested. + +- All functions, methods, modules and classes should have proper documentation. + +- All functions and methods should be properly annotated. + +- Commits should be atomic. + +- Dependency injection should be favorized, and objects instantiation should be made at the latest possible moment. + +- Always keep Separation of Concerns in mind. + +### Developing tips and tricks + +- Run `poe` from within the development environment to print a list of [Poe the Poet](https://github.com/nat-n/poethepoet) tasks available to run on this project. + +- Run `poetry add {package}` from within the development environment to install a run time dependency and add it to `pyproject.toml` and `poetry.lock`. Add `--group test` or `--group dev` to install a CI or development dependency, respectively. + +- Run `poetry update` from within the development environment to upgrade all dependencies to the latest versions allowed by `pyproject.toml`. + +- Run `cz bump` to bump the app's version, update the `CHANGELOG.md`, and create a git tag. + +- Many VSCode extensions exists to help you code better and faster. We recommend the following ones: + - The "Python" extension and its suite ("Python", "Pylance", "Python Debugger") + - The "ruff" extension: Automatically shows which linting and formatting rules are failing directly into the code. Tips: bind keyboard shortcuts to format and fix linting errors easily and quickly. + - The "mypy" extension: Automatically shows which type annotation are invalid directly into the code. N.B.: the extension is fairly slow, at least on Windows. + - The "Docker" extension. + - The "DevContainer" extension. + - The "Jupyter" extension: Allows you to edit and run notebooks directly in VS Code diff --git a/{{ cookiecutter.__project_name_kebab_case }}/Dockerfile b/{{ cookiecutter.__project_name_kebab_case }}/Dockerfile index 1df6495d..7d53caa2 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/Dockerfile +++ b/{{ cookiecutter.__project_name_kebab_case }}/Dockerfile @@ -9,8 +9,8 @@ RUN rm /etc/apt/apt.conf.d/docker-clean # Configure Python to print tracebacks on crash [1], and to not buffer stdout and stderr [2]. # [1] https://docs.python.org/3/using/cmdline.html#envvar-PYTHONFAULTHANDLER # [2] https://docs.python.org/3/using/cmdline.html#envvar-PYTHONUNBUFFERED -ENV PYTHONFAULTHANDLER 1 -ENV PYTHONUNBUFFERED 1 +ENV PYTHONFAULTHANDLER=1 +ENV PYTHONUNBUFFERED=1 {%- endif %} # Create a non-root user and switch to it [1]. @@ -23,8 +23,8 @@ RUN groupadd --gid $GID user && \ USER user # Create and activate a virtual environment. -ENV VIRTUAL_ENV /opt/{{ cookiecutter.__project_name_kebab_case }}-env -ENV PATH $VIRTUAL_ENV/bin:$PATH +ENV VIRTUAL_ENV=/opt/{{ cookiecutter.__project_name_kebab_case }}-env +ENV PATH=$VIRTUAL_ENV/bin:$PATH RUN python -m venv $VIRTUAL_ENV # Set the working directory. @@ -32,13 +32,13 @@ WORKDIR /workspaces/{{ cookiecutter.__project_name_kebab_case }}/ -FROM base as poetry +FROM base AS poetry USER root # Install Poetry in separate venv so it doesn't pollute the main venv. -ENV POETRY_VERSION 1.8.0 -ENV POETRY_VIRTUAL_ENV /opt/poetry-env +ENV POETRY_VERSION=2.1.3 +ENV POETRY_VIRTUAL_ENV=/opt/poetry-env RUN --mount=type=cache,target=/root/.cache/pip/ \ python -m venv $POETRY_VIRTUAL_ENV && \ $POETRY_VIRTUAL_ENV/bin/pip install poetry~=$POETRY_VERSION && \ @@ -61,7 +61,7 @@ RUN --mount=type=cache,uid=$UID,gid=$GID,target=/home/user/.cache/pypoetry/ \ -FROM poetry as dev +FROM poetry AS dev # Install development tools: curl, git, gpg, ssh, starship, sudo, vim, and zsh. USER root @@ -72,7 +72,6 @@ RUN --mount=type=cache,target=/var/cache/apt/ \ sh -c "$(curl -fsSL https://starship.rs/install.sh)" -- "--yes" && \ usermod --shell /usr/bin/zsh user && \ echo 'user ALL=(root) NOPASSWD:ALL' > /etc/sudoers.d/user && chmod 0440 /etc/sudoers.d/user -RUN git config --system --add safe.directory '*' USER user # Install the development Python dependencies in the virtual environment. @@ -86,7 +85,7 @@ RUN mkdir -p /opt/build/poetry/ && cp poetry.lock /opt/build/poetry/ && \ mkdir -p /opt/build/git/ && cp .git/hooks/commit-msg .git/hooks/pre-commit /opt/build/git/ # Configure the non-root user's shell. -ENV ANTIDOTE_VERSION 1.8.6 +ENV ANTIDOTE_VERSION=1.8.6 RUN git clone --branch v$ANTIDOTE_VERSION --depth=1 https://github.com/mattmc3/antidote.git ~/.antidote/ && \ echo 'zsh-users/zsh-syntax-highlighting' >> ~/.zsh_plugins.txt && \ echo 'zsh-users/zsh-autosuggestions' >> ~/.zsh_plugins.txt && \ @@ -102,7 +101,13 @@ RUN git clone --branch v$ANTIDOTE_VERSION --depth=1 https://github.com/mattmc3/a mkdir ~/.history/ && \ zsh -c 'source ~/.zshrc' -FROM base AS app +FROM poetry AS base-app + +RUN --mount=type=cache,uid=$UID,gid=$GID,target=/home/user/.cache/pypoetry/ \ + poetry install --only app --all-extras --no-interaction + + +FROM base-app AS app # Copy the virtual environment from the poetry stage. COPY --from=poetry $VIRTUAL_ENV $VIRTUAL_ENV diff --git a/{{ cookiecutter.__project_name_kebab_case }}/README.md b/{{ cookiecutter.__project_name_kebab_case }}/README.md index 779c2df1..5e04b227 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/README.md +++ b/{{ cookiecutter.__project_name_kebab_case }}/README.md @@ -1,128 +1,104 @@ -[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url={{cookiecutter.project_url}}){% if cookiecutter.continuous_integration == "GitHub" %} [![Open in GitHub Codespaces](https://img.shields.io/static/v1?label=GitHub%20Codespaces&message=Open&color=blue&logo=github)](https://github.com/codespaces/new/{{ cookiecutter.project_url.replace("https://github.com/", "") }}){% endif %} - # {{ cookiecutter.project_name }} +## Description + {{ cookiecutter.project_description }} +## Table of contents + +1. [Setup](#setup) +2. [Usage](#usage) + +## Setup + +### Environment variables + +To use this project, you need to set the environment variables. To do so, create a copy of the `.env.sample` file and name it `.env`. Then, fill in the values of the variables. + +### Requirements + +The set of requirements is dependent on how you want to run the application: locally or in a container. -## Installing +#### Local setup -To install this package, run: +To use the application source code, you must have the following tools installed: -```sh -pip install {{ cookiecutter.__project_name_kebab_case }} +- [Python 3.12](https://www.python.org/downloads/) (the exact version is important) +- [Poetry](https://python-poetry.org/docs/#installation) (the exact version is not important, but there are some differences between major versions) + +Check that the tools have been correctly installed by executing the following commands in your terminal: + +```bash +python --version +``` + +```bash +poetry --version ``` -## Using -{%- if cookiecutter.with_typer_cli|int %} -To view the CLI help information, run: +To install the project dependencies, run the following command at the project's root: -```sh -{{ cookiecutter.__project_name_kebab_case }} --help +```bash +poetry install ``` -{%- elif cookiecutter.project_type == "app" %} -To serve this app, run: +This will create a virtual environment and install the dependencies in it at the root of the project in a folder named `.venv`. -```sh -docker compose up app +You can then activate the virtual environment created by Poetry (version less than 2.0) by running the following command: + +```bash +poetry shell +``` + +If you are using Poetry version 2.0 or higher, you need to install the [plugin](https://python-poetry.org/docs/plugins/#using-plugins) `poetry-plugin-shell` before running the previous command. You can do so by running the following command: + +```bash +poetry self add poetry-plugin-shell ``` -{%- if cookiecutter.with_fastapi_api|int %} -and open [localhost:8000](http://localhost:8000) in your browser. -{%- endif %} +#### Container setup + +To use the application in a docker container, you must have Docker Desktop installed: + +- [Docker](https://docs.docker.com/get-docker/) + +## Usage + +### Local usage + +The project uses `poe-the-poet` as a CLI tool to manage the application. The commands are defined in the `pyproject.toml` file. To see available commands, you can run the following command at the project's root: + +```bash +poe +``` -Within the Dev Container this is equivalent to: +To run the application locally, you need to execute the following command at the project's root: -```sh -poe {% if cookiecutter.with_fastapi_api|int %}api{% else %}app{% endif %} +```bash +poe api --dev ``` -{%- else %} -Example usage: +This will start the application in development mode. You can then access the API at the following URL: [`http://localhost:8000`](http://localhost:8000). -```python -import {{ cookiecutter.__project_name_snake_case }} +The API documentation is available at the following URL: [`http://localhost:8000/docs`](http://localhost:8000/docs). You can use it to test the different endpoints of the API. -... +### Container usage + +To run the application in a container, you need to execute the following command at the project's root: + +```bash +docker compose up app ``` -{%- endif %} - -## Contributing - -
-Prerequisites - -
-1. Set up Git to use SSH - -{% if cookiecutter.continuous_integration == "GitLab" -%} -1. [Generate an SSH key](https://docs.gitlab.com/ee/user/ssh.html#generate-an-ssh-key-pair) and [add the SSH key to your GitLab account](https://docs.gitlab.com/ee/user/ssh.html#add-an-ssh-key-to-your-gitlab-account). -{%- else -%} -1. [Generate an SSH key](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent#generating-a-new-ssh-key) and [add the SSH key to your GitHub account](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/adding-a-new-ssh-key-to-your-github-account). -{%- endif %} -1. Configure SSH to automatically load your SSH keys: - ```sh - cat << EOF >> ~/.ssh/config - - Host * - AddKeysToAgent yes - IgnoreUnknown UseKeychain - UseKeychain yes - ForwardAgent yes - EOF - ``` - -
- -
-2. Install Docker - -1. [Install Docker Desktop](https://www.docker.com/get-started). - - _Linux only_: - - Export your user's user id and group id so that [files created in the Dev Container are owned by your user](https://github.com/moby/moby/issues/3206): - ```sh - cat << EOF >> ~/.bashrc - - export UID=$(id --user) - export GID=$(id --group) - EOF - ``` - -
- -
-3. Install VS Code or PyCharm - -1. [Install VS Code](https://code.visualstudio.com/) and [VS Code's Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers). Alternatively, install [PyCharm](https://www.jetbrains.com/pycharm/download/). -2. _Optional:_ install a [Nerd Font](https://www.nerdfonts.com/font-downloads) such as [FiraCode Nerd Font](https://github.com/ryanoasis/nerd-fonts/tree/master/patched-fonts/FiraCode) and [configure VS Code](https://github.com/tonsky/FiraCode/wiki/VS-Code-Instructions) or [configure PyCharm](https://github.com/tonsky/FiraCode/wiki/Intellij-products-instructions) to use it. - -
- -
-Development environments - -The following development environments are supported: -{% if cookiecutter.continuous_integration == "GitHub" %} -1. ⭐️ _GitHub Codespaces_: click on _Code_ and select _Create codespace_ to start a Dev Container with [GitHub Codespaces](https://github.com/features/codespaces). -{%- endif %} -1. ⭐️ _Dev Container (with container volume)_: click on [Open in Dev Containers](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url={{cookiecutter.project_url}}) to clone this repository in a container volume and create a Dev Container with VS Code. -1. _Dev Container_: clone this repository, open it with VS Code, and run Ctrl/⌘ + ⇧ + P β†’ _Dev Containers: Reopen in Container_. -1. _PyCharm_: clone this repository, open it with PyCharm, and [configure Docker Compose as a remote interpreter](https://www.jetbrains.com/help/pycharm/using-docker-compose-as-a-remote-interpreter.html#docker-compose-remote) with the `dev` service. -1. _Terminal_: clone this repository, open it with your terminal, and run `docker compose up --detach dev` to start a Dev Container in the background, and then run `docker compose exec dev zsh` to open a shell prompt in the Dev Container. - -
- -
-Developing -{% if cookiecutter.with_conventional_commits|int %} -- This project follows the [Conventional Commits](https://www.conventionalcommits.org/) standard to automate [Semantic Versioning](https://semver.org/) and [Keep A Changelog](https://keepachangelog.com/) with [Commitizen](https://github.com/commitizen-tools/commitizen). -{%- endif %} -- Run `poe` from within the development environment to print a list of [Poe the Poet](https://github.com/nat-n/poethepoet) tasks available to run on this project. -- Run `poetry add {package}` from within the development environment to install a run time dependency and add it to `pyproject.toml` and `poetry.lock`. Add `--group test` or `--group dev` to install a CI or development dependency, respectively. -- Run `poetry update` from within the development environment to upgrade all dependencies to the latest versions allowed by `pyproject.toml`. -{%- if cookiecutter.with_conventional_commits|int %} -- Run `cz bump` to bump the {{ cookiecutter.project_type }}'s version, update the `CHANGELOG.md`, and create a git tag. -{%- endif %} - -
+ +This will start the application in a container. You can then access the API at the following URL: [`http://localhost:8000`](http://localhost:8000). + +## Project structure + +The project is structured as follows: + +- `src/`: Contains the source code of the application. +- `tests/`: Contains the unit and integration tests of the application. +- `docs/`: Contains the ADRs and configuration guides. + +At the root, you will find the `Dockerfile` and a `docker-compose.yml` file to facilitate the deployment of the application. There is also a `pyproject.toml` file that contains the Poetry project configuration, as well as READMEs for project documentation. +Pour contribuer au projet, lire le fichier [CONTRIBUTING.md](CONTRIBUTING.md). diff --git a/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml b/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml index 71d2ed51..f1e57bbc 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml +++ b/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml @@ -25,8 +25,9 @@ version_provider = "poetry" [tool.poetry.dependencies] # https://python-poetry.org/docs/dependency-specification/ {%- if cookiecutter.with_fastapi_api|int %} -coloredlogs = ">=15.0.1" fastapi = { extras = ["all"], version = ">=0.110.1" } +loguru = "^0.7.3" +python-decouple = "^3.8" gunicorn = ">=21.2.0" {%- endif %} {%- if cookiecutter.project_type == "app" %} diff --git a/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/api.py b/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/api.py index 34048d50..db8d60b8 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/api.py +++ b/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/api.py @@ -1,25 +1,22 @@ """{{ cookiecutter.project_name }} REST API.""" import asyncio -import logging +import sys from collections.abc import AsyncGenerator from contextlib import asynccontextmanager -import coloredlogs +from decouple import config as env_vars from fastapi import FastAPI +from loguru import logger @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: # noqa: ARG001 """Handle FastAPI startup and shutdown events.""" - # Startup events: - # - Remove all handlers associated with the root logger object. - for handler in logging.root.handlers: - logging.root.removeHandler(handler) - # - Add coloredlogs' colored StreamHandler to the root logger. - coloredlogs.install() + logger.remove() + logger.add(sys.stderr, level=env_vars("LOG_LEVEL", default="INFO")) + yield - # Shutdown events. app = FastAPI(lifespan=lifespan) From fcea9b562808dc37385f4d702b6c4fb3e76ab3ae Mon Sep 17 00:00:00 2001 From: Vincent Gagnon Date: Fri, 6 Jun 2025 15:09:27 -0400 Subject: [PATCH 61/68] fix(pyproject.toml): remove src from testpaths (#17) --- {{ cookiecutter.__project_name_kebab_case }}/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml b/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml index f1e57bbc..ae64a349 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml +++ b/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml @@ -123,7 +123,7 @@ addopts = "--color=yes --doctest-modules --exitfirst --failed-first{% if cookiec {%- if cookiecutter.development_environment == "strict" %} filterwarnings = ["error", "ignore::DeprecationWarning"] {%- endif %} -testpaths = ["src", "tests"] +testpaths = ["tests"] xfail_strict = true [tool.ruff] # https://github.com/charliermarsh/ruff From b76c2bbc4bd8ebbbd342fef64e5cf096811ac18c Mon Sep 17 00:00:00 2001 From: David Beauchemin Date: Sat, 21 Feb 2026 12:22:51 -0500 Subject: [PATCH 62/68] =?UTF-8?q?feat:=20sprint=202=20=E2=80=94=20cookiecu?= =?UTF-8?q?tter=20enhancements=20(14=20items)=20(#19)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: resolve 5 P0 bugs that break template generation - Fix post_gen_project.py test file names (test_api.py, test_cli.py) and add cleanup for BDD feature files - Remove dead parameters (continuous_integration, teamwork_uri) from cookiecutter.json and hook - Fix docker-compose target: api β†’ app to match Dockerfile stage name - Wrap poe api task, Dockerfile CMD, and docker-compose api service in FastAPI conditional - Fix coverage.xml config: directory β†’ output in pyproject.toml - Remove broken base-app Dockerfile stage (--only app with no app group) Co-Authored-By: Claude Opus 4.6 * feat: add pytest-bdd structure with feature files and step definitions - Rename *_test.py β†’ test_*.py (standard pytest convention) - Add tests/features/ with Gherkin scenarios (import, api, cli) - Add BDD step definitions using scenarios() and parsers.cfparse - Add tests/conftest.py with shared fixtures and conditional FastAPI TestClient - Add pytest-bdd and httpx to test dependencies - Configure bdd_features_base_dir in pytest ini options Co-Authored-By: Claude Opus 4.6 * chore: update dependencies, CI, and meta-repo configuration - Update pre-commit hooks: pygrep-hooks v1.11, pre-commit-hooks v5.0 - Extend trailing-whitespace and end-of-file-fixer to all file types - Remove obsolete ANN101/ANN102 ruff rules (removed upstream) - Clean simple mode ruff ignores (G010, S101, S307 not in selected set) - Update CI: checkout v5, Node 22, devcontainers@latest, add workflow_dispatch - Add git safe.directory config and Poetry 2.2.1 in Dockerfile - Make PYTHONFAULTHANDLER/PYTHONUNBUFFERED unconditional in Dockerfile - Add TODO for teamwork.yml SHA pinning - Fix README: replace radix-ai refs, remove GPG feature, fix French text, document with_conventional_commits param - Update root devcontainer Python 3.10 β†’ 3.12 Co-Authored-By: Claude Opus 4.6 * feat: sprint 2 β€” cookiecutter enhancements (14 items) Add pre-generation validation, pydantic-settings, enriched stubs, Sentry integration, API middleware, debug configs, and DevOps tooling. Phase 1 β€” Infrastructure: - Add hooks/pre_gen_project.py (validate project_name, python_version >= 3.10, warn sentry without FastAPI) - Add .editorconfig (UTF-8, LF, Python 4sp, YAML/JSON 2sp, Makefile tabs) - Add CHANGELOG.md (Keep a Changelog format, compatible with cz bump) Phase 2 β€” pydantic-settings: - Add settings.py with BaseSettings (env_file=".env", extra="ignore") - Replace python-decouple with pydantic-settings >= 2.2.0 in pyproject.toml - Update .env_sample with all documented variables (General, FastAPI, Sentry sections) Phase 3 β€” Enriched stubs: - Add models.py (ItemCreate, Item, HealthResponse with Field validators) - Add services.py (ItemService with create/get/list_all in-memory pattern) - Rewrite api.py: health endpoint, items CRUD, Annotated + Depends DI, response_model - Rewrite cli.py: @app.callback() with --verbose, info (Rich Table), greet (Annotated arg) - Update post_gen_project.py to remove models.py/services.py when FastAPI disabled - Rewrite test files and feature files (3 scenarios each for API and CLI) Phase 4 β€” API enhancements: - Add StarletteHTTPException handler (structured JSON + log warning) - Add generic Exception handler (500 JSON + log exception) - Add RequestLoggingMiddleware (method, path, status, duration_ms via loguru) Phase 5 β€” Sentry SDK: - Add with_sentry to cookiecutter.json (default "0") - Add sentry-sdk conditional dep in pyproject.toml - Add sentry fields in settings.py (dsn, environment, traces_sample_rate) - Add sentry_sdk.init() in api.py lifespan (conditional on non-empty dsn) - Document with_sentry in root README.md Phase 6 β€” Test infra + cleanup: - Add pytest-asyncio >= 0.23.0 conditional to FastAPI - Add asyncio_mode = "auto" and asyncio_default_fixture_loop_scope = "function" - Split addopts into clean strict/simple if/else blocks - Split .pre-commit-config.yaml ruff args into clean if/else blocks - Add PytestUnraisableExceptionWarning to filterwarnings - Enable ruff preview mode, add max-public-methods = 30 Phase 7 β€” DevOps: - Add .vscode/launch.json (FastAPI uvicorn, pytest, Current File configs) - Update .gitignore: .vscode/* + !.vscode/launch.json - Add multi-Python CI matrix (+ 3.13 if not already selected) - Add Docker HEALTHCHECK using stdlib urllib (no curl in slim images) Co-Authored-By: Claude Opus 4.6 * fix(ci): fix pygrep-hooks tag, pre-commit stages, and CI scaffold - pygrep-hooks v1.11.0 does not exist, downgrade to v1.10.0 - default_stages: commit β†’ pre-commit (deprecated name) - Root CI: use matrix python-version in scaffold, drop radixai image override Co-Authored-By: Claude Opus 4.6 * fix: remove JSON comment from launch.json (fails check-json hook) Co-Authored-By: Claude Opus 4.6 * fix: add missing trailing newlines to template files Fixes end-of-file-fixer pre-commit hook failures on devcontainer.json, dependabot.yml, and pull_request_template.md. Co-Authored-By: Claude Opus 4.6 * fix: remove extra trailing newline in .pre-commit-config.yaml template Co-Authored-By: Claude Opus 4.6 * fix: use --pytest-test-first for name-tests-test hook The --pytest flag expects *_test.py pattern, but our tests use test_*.py. The --pytest-test-first flag matches the correct pattern. Co-Authored-By: Claude Opus 4.6 * fix: remove trailing whitespace in dependabot.yml and PR template Co-Authored-By: Claude Opus 4.6 * fix: resolve ruff preview mode errors (RUF029, DOC201, D107) - Remove async from exception handlers (sync is valid for FastAPI) - Add noqa RUF029 to lifespan (required async by @asynccontextmanager) - Add __init__ docstring to ItemService - Ignore DOC201/DOC501 in strict config (too verbose for templates) Co-Authored-By: Claude Opus 4.6 * fix: resolve FAST001, PLR6301, FBT002 ruff errors - Remove redundant response_model when return type is annotated (FAST001) - Add noqa PLR6301 for dispatch method (required by BaseHTTPMiddleware) - Add noqa FBT002 for verbose bool (required by Typer) Co-Authored-By: Claude Opus 4.6 * fix: resolve remaining ruff auto-fix issues - AsyncGenerator[None, None] β†’ AsyncGenerator[None] (UP043) - Remove stale noqa PLC0415 (rule renamed in latest ruff) - Fix import order in test_cli.py (I001) - Remove deprecated PD901 from ignore list Co-Authored-By: Claude Opus 4.6 * fix: resolve mypy errors in api.py and test_import.py - Replace call_next: ... with RequestResponseEndpoint type - Fix test_import.py return type: type β†’ ModuleType Co-Authored-By: Claude Opus 4.6 * fix: correct bdd scenario paths (double features/ prefix) bdd_features_base_dir is already set to tests/features/ so scenario paths should be relative to that, not include features/ again. Co-Authored-By: Claude Opus 4.6 * fix: avoid typer.Context in CLI to fix typeguard conflict typeguard (strict mode) rejects typer's Context during testing. Use module-level _verbose flag instead of ctx.obj pattern. Co-Authored-By: Claude Opus 4.6 * chore: remove Teamwork integration Remove teamwork.yml workflow, Teamwork link from PR template, and Teamwork references from README. Teamwork is no longer used. Co-Authored-By: Claude Opus 4.6 * feat: address PR review comments - Rename .env_sample to .env.example - Update reference in CONTRIBUTING.md - Add CHANGELOG.md for the cookiecutter repo Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: davebulaval Co-authored-by: Claude Opus 4.6 --- .devcontainer/devcontainer.json | 2 +- .github/workflows/test.yml | 10 +- CHANGELOG.md | 41 ++++++ README.md | 11 +- cookiecutter.json | 3 +- hooks/post_gen_project.py | 12 +- hooks/pre_gen_project.py | 34 +++++ .../.devcontainer/devcontainer.json | 2 +- .../.editorconfig | 22 +++ .../.env.example | 17 +++ .../.env_sample | 1 - .../.github/dependabot.yml | 4 +- .../.github/pull_request_template.md | 6 +- .../.github/workflows/teamwork.yml | 26 ---- .../.github/workflows/test.yml | 9 +- .../.gitignore | 3 +- .../.pre-commit-config.yaml | 23 +++- .../.vscode/launch.json | 36 +++++ .../CHANGELOG.md | 12 ++ .../CONTRIBUTING.md | 2 +- .../Dockerfile | 19 ++- .../README.md | 2 +- .../docker-compose.yml | 4 +- .../pyproject.toml | 43 ++++-- .../api.py | 125 ++++++++++++++++-- .../cli.py | 44 +++++- .../models.py | 23 ++++ .../services.py | 27 ++++ .../settings.py | 25 ++++ .../tests/api_test.py | 15 --- .../tests/cli_test.py | 16 --- .../tests/conftest.py | 14 ++ .../tests/features/api.feature | 19 +++ .../tests/features/cli.feature | 20 +++ .../tests/features/import.feature | 6 + .../tests/import_test.py | 8 -- .../tests/test_api.py | 46 +++++++ .../tests/test_cli.py | 51 +++++++ .../tests/test_import.py | 22 +++ 39 files changed, 660 insertions(+), 145 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 hooks/pre_gen_project.py create mode 100644 {{ cookiecutter.__project_name_kebab_case }}/.editorconfig create mode 100644 {{ cookiecutter.__project_name_kebab_case }}/.env.example delete mode 100644 {{ cookiecutter.__project_name_kebab_case }}/.env_sample delete mode 100644 {{ cookiecutter.__project_name_kebab_case }}/.github/workflows/teamwork.yml create mode 100644 {{ cookiecutter.__project_name_kebab_case }}/.vscode/launch.json create mode 100644 {{ cookiecutter.__project_name_kebab_case }}/CHANGELOG.md create mode 100644 {{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/models.py create mode 100644 {{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/services.py create mode 100644 {{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/settings.py delete mode 100644 {{ cookiecutter.__project_name_kebab_case }}/tests/api_test.py delete mode 100644 {{ cookiecutter.__project_name_kebab_case }}/tests/cli_test.py create mode 100644 {{ cookiecutter.__project_name_kebab_case }}/tests/conftest.py create mode 100644 {{ cookiecutter.__project_name_kebab_case }}/tests/features/api.feature create mode 100644 {{ cookiecutter.__project_name_kebab_case }}/tests/features/cli.feature create mode 100644 {{ cookiecutter.__project_name_kebab_case }}/tests/features/import.feature delete mode 100644 {{ cookiecutter.__project_name_kebab_case }}/tests/import_test.py create mode 100644 {{ cookiecutter.__project_name_kebab_case }}/tests/test_api.py create mode 100644 {{ cookiecutter.__project_name_kebab_case }}/tests/test_cli.py create mode 100644 {{ cookiecutter.__project_name_kebab_case }}/tests/test_import.py diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b5925e2f..50734be4 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { "name": "Baseline app Cookiecutter", - "image": "mcr.microsoft.com/vscode/devcontainers/python:3.10", + "image": "mcr.microsoft.com/vscode/devcontainers/python:3.12", "onCreateCommand": "pip install commitizen cruft pre-commit && pre-commit install --install-hooks", "remoteUser": "vscode", "customizations": { diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8944d568..04f16c8d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,14 +13,14 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.12"] + python-version: ["3.12", "3.13"] project-type: ["app"] name: Python ${{ matrix.python-version }} app steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: path: template @@ -32,15 +32,15 @@ jobs: - name: Scaffold Python project run: | pip install --no-input cruft - cruft create --no-input --extra-context '{"project_type": "app", "project_name": "My Project", "python_version": "3.12", "__docker_image":"radixai/python-gpu:$PYTHON_VERSION-cuda11.8", "with_fastapi_api": "1", "with_typer_cli": "1"}' ./template/ + cruft create --no-input --extra-context '{"project_type": "app", "project_name": "My Project", "python_version": "${{ matrix.python-version }}", "with_fastapi_api": "1", "with_typer_cli": "1", "with_sentry": "0"}' ./template/ - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: 21 + node-version: 22 - name: Install @devcontainers/cli - run: npm install --location=global @devcontainers/cli@0.58.0 + run: npm install --location=global @devcontainers/cli@latest - name: Start Dev Container with Python ${{ matrix.python-version }} run: | diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..c71f24c5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,41 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/), +and this project adheres to [Semantic Versioning](https://semver.org/). + +## [Unreleased] + +### Added + +- pydantic-settings integration (replaces python-decouple) with `.env.example` +- Pydantic models (`models.py`) and service layer (`services.py`) stubs +- Rich API stubs: health endpoint, CRUD items, exception handlers, request logging middleware +- Rich CLI stubs: `info` command with Rich table, `greet` command with `Annotated` +- `.editorconfig` for cross-IDE consistency +- `.vscode/launch.json` with FastAPI, pytest, and current-file debug configs +- `CHANGELOG.md` for generated projects (Keep a Changelog format) +- `pre_gen_project.py` hook for input validation +- Sentry SDK integration (`with_sentry` parameter) +- Multi-Python CI matrix (tests on selected version + 3.13) +- Docker `HEALTHCHECK` instruction (conditional on FastAPI) +- pytest-asyncio support (conditional on FastAPI) + +### Changed + +- Replaced python-decouple with pydantic-settings +- Rewrote `api.py` with dependency injection, structured error handling, and logging +- Rewrote `cli.py` with `@app.callback()` and `Annotated` pattern +- Rewrote BDD test stubs for new API and CLI +- Renamed `.env_sample` to `.env.example` +- Removed Teamwork integration (workflow + PR template link) + +### Fixed + +- 5 P0 bugs that broke template generation +- pytest-bdd feature file paths +- pre-commit hook compatibility (pygrep-hooks tag, `--pytest-test-first`) +- ruff preview mode compliance (DOC201, DOC501, FAST001, PLR6301, etc.) +- mypy strict mode compliance +- typeguard conflict with typer.Context diff --git a/README.md b/README.md index b01c3709..5ec92c6c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/radix-ai/poetry-cookiecutter) [![Open in GitHub Codespaces](https://img.shields.io/static/v1?label=GitHub%20Codespaces&message=Open&color=blue&logo=github)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=444870763) +[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/Baseline-quebec/baseline-app-cookiecutter) [![Open in GitHub Codespaces](https://img.shields.io/static/v1?label=GitHub%20Codespaces&message=Open&color=blue&logo=github)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=Baseline-quebec/baseline-app-cookiecutter) # Baseline app Cookiecutter @@ -15,12 +15,10 @@ A modern [Cookiecutter](https://github.com/cookiecutter/cookiecutter) template f - ✍️ Code formatting with [Ruff](https://github.com/charliermarsh/ruff) - βœ… Code linting with [Pre-commit](https://pre-commit.com/), [Mypy](https://github.com/python/mypy), and [Ruff](https://github.com/charliermarsh/ruff) - 🏷 Optionally follows the [Conventional Commits](https://www.conventionalcommits.org/) standard to automate [Semantic Versioning](https://semver.org/) and [Keep A Changelog](https://keepachangelog.com/) with [Commitizen](https://github.com/commitizen-tools/commitizen) -- πŸ’Œ Verified commits with [GPG](https://gnupg.org/) - ♻️ Continuous integration with [GitHub Actions](https://docs.github.com/en/actions) - πŸ§ͺ Test coverage with [Coverage.py](https://github.com/nedbat/coveragepy) - πŸ— Scaffolding updates with [Cookiecutter](https://github.com/cookiecutter/cookiecutter) and [Cruft](https://github.com/cruft/cruft) - 🧰 Dependency updates with [Dependabot](https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/about-dependabot-version-updates) -- 🀹 Task management made easy with references to PR and Issues with [Teamwork](https://baseline6.teamwork.com/app/home/activity?from=homepage) ## ✨ Using @@ -46,7 +44,7 @@ To create a new Python project with this template: ⚠️ If your repository name β‰  the project's slugified name - If your repository name differs from your project's slugified name (see `project_name` in the [Template parameters](https://github.com/radix-ai/poetry-cookiecutter#-template-parameters) below), you will need to copy the scaffolded project into the repository with: + If your repository name differs from your project's slugified name (see `project_name` in the [Template parameters](https://github.com/Baseline-quebec/baseline-app-cookiecutter#-template-parameters) below), you will need to copy the scaffolded project into the repository with: ```sh cp -r {project-name}/ {repository-name}/ @@ -55,7 +53,6 @@ To create a new Python project with this template:
4. Add the remote origin to your local package. -5. _Optional:_ Link your repository to a Teamwork Project by adding the required Secrets to the Github Repository for the Teamwork Integration Github Workflow using this documentation: https://github.com/Teamwork/github-sync ### Updating your Python project @@ -82,4 +79,6 @@ To update your Python project to the latest template version: | `python_version`
"3.12" | The minimum Python version that the project requires. | | `development_environment`
["simple", "strict"] | Whether to configure the development environment with a focus on simplicity or with a focus on strictness. In strict mode, additional [Ruff rules](https://docs.astral.sh/ruff/rules/) are added, and tools such as [Mypy](https://github.com/python/mypy) and [Pytest](https://github.com/pytest-dev/pytest) are set to strict mode. | | `with_fastapi_api`
["0", "1"] | If "1", [FastAPI](https://github.com/tiangolo/fastapi) is added as a run time dependency, FastAPI API stubs and tests are added, a `poe api` command for serving the API is added. | -| `with_typer_cli`
["0", "1"] | If "1", [Typer](https://github.com/tiangolo/typer) is added as a run time dependency, Typer CLI stubs and tests are added, the package itself is registered as a CLI. | +| `with_conventional_commits`
["0", "1"] | If "1", [Commitizen](https://github.com/commitizen-tools/commitizen) is added for conventional commits and semantic versioning. Automatically set to "1" in strict mode. | +| `with_typer_cli`
["0", "1"] | If "1", [Typer](https://github.com/tiangolo/typer) is added as a run time dependency, Typer CLI stubs and tests are added, the package itself is registered as a CLI. | +| `with_sentry`
["0", "1"] | If "1", [Sentry SDK](https://docs.sentry.io/platforms/python/) is added with FastAPI integration. Requires `with_fastapi_api=1`. Adds `sentry_dsn`, `sentry_environment`, and `sentry_traces_sample_rate` settings. | diff --git a/cookiecutter.json b/cookiecutter.json index bed09f4a..8edc1fa0 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -6,8 +6,6 @@ "author_name": "John Smith", "author_email": "john@example.com", "python_version": "3.12", - "continuous_integration": "GitHub", - "teamwork_uri": "", "development_environment": [ "strict", "simple" @@ -15,6 +13,7 @@ "with_conventional_commits": "{% if cookiecutter.development_environment == 'simple' %}0{% else %}1{% endif %}", "with_fastapi_api": "1", "with_typer_cli": "1", + "with_sentry": "0", "__docker_image": "python:$PYTHON_VERSION-slim", "__docstring_style": "Google", "__project_name_kebab_case": "{{ cookiecutter.project_name|slugify }}", diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py index 48b86e31..40314443 100644 --- a/hooks/post_gen_project.py +++ b/hooks/post_gen_project.py @@ -1,3 +1,5 @@ +"""Post-generation hook: remove files based on cookiecutter options.""" + import os import shutil @@ -6,8 +8,6 @@ development_environment = "{{ cookiecutter.development_environment }}" with_fastapi_api = int("{{ cookiecutter.with_fastapi_api }}") with_typer_cli = int("{{ cookiecutter.with_typer_cli }}") -continuous_integration = "{{ cookiecutter.continuous_integration }}" -is_application = "{{ cookiecutter.project_type == 'app' }}" == "True" # Remove py.typed and Dependabot if not in strict mode. if development_environment != "strict": @@ -17,9 +17,17 @@ # Remove FastAPI if not selected. if not with_fastapi_api: os.remove(f"src/{project_name}/api.py") + os.remove(f"src/{project_name}/models.py") + os.remove(f"src/{project_name}/services.py") os.remove("tests/test_api.py") + os.remove("tests/features/api.feature") # Remove Typer if not selected. if not with_typer_cli: os.remove(f"src/{project_name}/cli.py") os.remove("tests/test_cli.py") + os.remove("tests/features/cli.feature") + +# Remove .vscode/ directory if not using FastAPI (no launch.json needed). +if not with_fastapi_api and not with_typer_cli: + shutil.rmtree(".vscode", ignore_errors=True) diff --git a/hooks/pre_gen_project.py b/hooks/pre_gen_project.py new file mode 100644 index 00000000..575397da --- /dev/null +++ b/hooks/pre_gen_project.py @@ -0,0 +1,34 @@ +"""Pre-generation hook: validate cookiecutter inputs.""" + +import re +import sys + +project_name = "{{ cookiecutter.project_name }}" +python_version = "{{ cookiecutter.python_version }}" +with_sentry = int("{{ cookiecutter.with_sentry }}") +with_fastapi_api = int("{{ cookiecutter.with_fastapi_api }}") + +# Validate project_name: letters, digits, spaces, hyphens. +if not re.match(r"^[A-Za-z0-9 -]+$", project_name): + print( + f"ERROR: Invalid project_name '{project_name}'. " + "Only letters, digits, spaces, and hyphens are allowed." + ) + sys.exit(1) + +# Validate python_version >= 3.10. +try: + major, minor = (int(x) for x in python_version.split(".")[:2]) + if (major, minor) < (3, 10): + print(f"ERROR: python_version must be >= 3.10, got '{python_version}'.") + sys.exit(1) +except ValueError: + print(f"ERROR: Invalid python_version '{python_version}'.") + sys.exit(1) + +# Warn if Sentry is enabled without FastAPI. +if with_sentry and not with_fastapi_api: + print( + "WARNING: with_sentry=1 has no effect without with_fastapi_api=1. " + "Sentry integration requires FastAPI." + ) diff --git a/{{ cookiecutter.__project_name_kebab_case }}/.devcontainer/devcontainer.json b/{{ cookiecutter.__project_name_kebab_case }}/.devcontainer/devcontainer.json index 1cffd0cc..fa70925d 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/.devcontainer/devcontainer.json +++ b/{{ cookiecutter.__project_name_kebab_case }}/.devcontainer/devcontainer.json @@ -64,4 +64,4 @@ } } } -} \ No newline at end of file +} diff --git a/{{ cookiecutter.__project_name_kebab_case }}/.editorconfig b/{{ cookiecutter.__project_name_kebab_case }}/.editorconfig new file mode 100644 index 00000000..15ff9da6 --- /dev/null +++ b/{{ cookiecutter.__project_name_kebab_case }}/.editorconfig @@ -0,0 +1,22 @@ +# https://editorconfig.org +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.py] +indent_style = space +indent_size = 4 + +[*.{yml,yaml,json,toml}] +indent_style = space +indent_size = 2 + +[Makefile] +indent_style = tab + +[*.md] +trim_trailing_whitespace = false diff --git a/{{ cookiecutter.__project_name_kebab_case }}/.env.example b/{{ cookiecutter.__project_name_kebab_case }}/.env.example new file mode 100644 index 00000000..80056188 --- /dev/null +++ b/{{ cookiecutter.__project_name_kebab_case }}/.env.example @@ -0,0 +1,17 @@ +# --- General --- +APP_NAME={{ cookiecutter.project_name }} +LOG_LEVEL=INFO +DEBUG=false +{%- if cookiecutter.with_fastapi_api|int %} + +# --- FastAPI --- +API_HOST=0.0.0.0 +API_PORT=8000 +{%- endif %} +{%- if cookiecutter.with_sentry|int %} + +# --- Sentry --- +SENTRY_DSN= +SENTRY_ENVIRONMENT=development +SENTRY_TRACES_SAMPLE_RATE=0.1 +{%- endif %} diff --git a/{{ cookiecutter.__project_name_kebab_case }}/.env_sample b/{{ cookiecutter.__project_name_kebab_case }}/.env_sample deleted file mode 100644 index a17418ab..00000000 --- a/{{ cookiecutter.__project_name_kebab_case }}/.env_sample +++ /dev/null @@ -1 +0,0 @@ -LOG_LEVEL=INFO \ No newline at end of file diff --git a/{{ cookiecutter.__project_name_kebab_case }}/.github/dependabot.yml b/{{ cookiecutter.__project_name_kebab_case }}/.github/dependabot.yml index 0eaad625..7c976899 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/.github/dependabot.yml +++ b/{{ cookiecutter.__project_name_kebab_case }}/.github/dependabot.yml @@ -20,7 +20,7 @@ updates: patterns: - "*" update-types: - - "major" + - "major" - package-ecosystem: pip directory: / schedule: @@ -48,4 +48,4 @@ updates: patterns: - "*" update-types: - - "major" \ No newline at end of file + - "major" diff --git a/{{ cookiecutter.__project_name_kebab_case }}/.github/pull_request_template.md b/{{ cookiecutter.__project_name_kebab_case }}/.github/pull_request_template.md index 76ebb8a2..322cc558 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/.github/pull_request_template.md +++ b/{{ cookiecutter.__project_name_kebab_case }}/.github/pull_request_template.md @@ -1,9 +1,7 @@ -[:](https://baselinequebec.teamwork.com/app/tasks/) - ## Describe your changes ## Checklist before opening a pull request -- [ ] I have merged or rebased main into my branch. I also ensure the origin of the PR is from main. +- [ ] I have merged or rebased main into my branch. I also ensure the origin of the PR is from main. - [ ] I have run pytest and the tests pass. ## Checklist before requesting a review @@ -13,4 +11,4 @@ ## Checklist before merging into main - [ ] I have bumped the version if needed. -- [ ] I pushed the tag to the repository. \ No newline at end of file +- [ ] I pushed the tag to the repository. diff --git a/{{ cookiecutter.__project_name_kebab_case }}/.github/workflows/teamwork.yml b/{{ cookiecutter.__project_name_kebab_case }}/.github/workflows/teamwork.yml deleted file mode 100644 index 50992b17..00000000 --- a/{{ cookiecutter.__project_name_kebab_case }}/.github/workflows/teamwork.yml +++ /dev/null @@ -1,26 +0,0 @@ -# Follow this link for documentation on how to set the secrets needed: https://github.com/Teamwork/github-sync - -name: teamwork - -on: - pull_request: - types: [opened, closed] - pull_request_review: - types: [submitted] - -jobs: - teamwork-sync: - runs-on: ubuntu-latest - name: Teamwork Sync - steps: - - uses: teamwork/github-sync@master - with: - GITHUB_TOKEN: {% raw %}${{ secrets.GITHUB_TOKEN }}{% endraw %} - TEAMWORK_URI: {% raw %}${{ secrets.TEAMWORK_URI }}{% endraw %} - TEAMWORK_API_TOKEN: {% raw %}${{ secrets.TEAMWORK_API_TOKEN }}{% endraw %} - AUTOMATIC_TAGGING: false - BOARD_COLUMN_OPENED: "PR Open" - BOARD_COLUMN_MERGED: "Ready to Test" - BOARD_COLUMN_CLOSED: "Rejected" - env: - IGNORE_PROJECT_IDS: "1 2 3" diff --git a/{{ cookiecutter.__project_name_kebab_case }}/.github/workflows/test.yml b/{{ cookiecutter.__project_name_kebab_case }}/.github/workflows/test.yml index c78eef4a..abde7519 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/.github/workflows/test.yml +++ b/{{ cookiecutter.__project_name_kebab_case }}/.github/workflows/test.yml @@ -6,6 +6,7 @@ on: - main - master pull_request: + workflow_dispatch: jobs: test: @@ -14,21 +15,21 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["{{ cookiecutter.python_version }}"] + python-version: ["{{ cookiecutter.python_version }}"{% if cookiecutter.python_version != "3.13" %}, "3.13"{% endif %}] name: Python {% raw %}${{{% endraw %} matrix.python-version }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: 21 + node-version: 22 - name: Install @devcontainers/cli - run: npm install --location=global @devcontainers/cli@0.58.0 + run: npm install --location=global @devcontainers/cli@latest - name: Start Dev Container run: | diff --git a/{{ cookiecutter.__project_name_kebab_case }}/.gitignore b/{{ cookiecutter.__project_name_kebab_case }}/.gitignore index 3b903be6..e9050843 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/.gitignore +++ b/{{ cookiecutter.__project_name_kebab_case }}/.gitignore @@ -61,4 +61,5 @@ __pycache__/ .terraform/ # VS Code -.vscode/ +.vscode/* +!.vscode/launch.json diff --git a/{{ cookiecutter.__project_name_kebab_case }}/.pre-commit-config.yaml b/{{ cookiecutter.__project_name_kebab_case }}/.pre-commit-config.yaml index e3eca528..6f51efd1 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/.pre-commit-config.yaml +++ b/{{ cookiecutter.__project_name_kebab_case }}/.pre-commit-config.yaml @@ -1,6 +1,6 @@ # https://pre-commit.com default_install_hook_types: [commit-msg, pre-commit] -default_stages: [commit, manual] +default_stages: [pre-commit, manual] fail_fast: true repos: - repo: meta @@ -16,7 +16,7 @@ repos: - id: rst-inline-touching-normal - id: text-unicode-replacement-char - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v5.0.0 hooks: - id: check-added-large-files args: ['--maxkb=50000'] @@ -36,13 +36,11 @@ repos: - id: destroyed-symlinks - id: detect-private-key - id: end-of-file-fixer - types: [python] - id: fix-byte-order-marker - id: mixed-line-ending - id: name-tests-test - args: [--pytest] + args: [--pytest-test-first] - id: trailing-whitespace - types: [python] - repo: local hooks: {%- if cookiecutter.with_conventional_commits|int %} @@ -54,14 +52,26 @@ repos: language: system stages: [commit-msg] {%- endif %} + # Development mode: {{ cookiecutter.development_environment }} +{%- if cookiecutter.development_environment == "strict" %} + - id: ruff-check + name: ruff check + entry: ruff check + args: ["--force-exclude", "--extend-fixable=ERA001,F401,F841,T201,T203"] + require_serial: true + language: system + types_or: [python, pyi] + pass_filenames: false +{%- else %} - id: ruff-check name: ruff check entry: ruff check - args: ["--force-exclude", "--extend-fixable={% if cookiecutter.development_environment == "strict" %}ERA001,F401,F841,T201,T203{% else %}F401,F841{% endif %}"{% if cookiecutter.development_environment == "simple" %}, "--fix-only"{% endif %}] + args: ["--force-exclude", "--extend-fixable=F401,F841", "--fix-only"] require_serial: true language: system types_or: [python, pyi] pass_filenames: false +{%- endif %} - id: ruff-format name: ruff format entry: ruff format @@ -90,4 +100,3 @@ repos: language: system types: [python] pass_filenames: false - diff --git a/{{ cookiecutter.__project_name_kebab_case }}/.vscode/launch.json b/{{ cookiecutter.__project_name_kebab_case }}/.vscode/launch.json new file mode 100644 index 00000000..8acb5228 --- /dev/null +++ b/{{ cookiecutter.__project_name_kebab_case }}/.vscode/launch.json @@ -0,0 +1,36 @@ +{ + "version": "0.2.0", + "configurations": [ +{%- if cookiecutter.with_fastapi_api|int %} + { + "name": "FastAPI (uvicorn --reload)", + "type": "debugpy", + "request": "launch", + "module": "uvicorn", + "args": [ + "{{ cookiecutter.__project_name_snake_case }}.api:app", + "--reload", + "--host", "0.0.0.0", + "--port", "8000" + ], + "jinja": true, + "envFile": "${workspaceFolder}/.env" + }, +{%- endif %} + { + "name": "pytest", + "type": "debugpy", + "request": "launch", + "module": "pytest", + "args": ["--no-header", "-vv"], + "jinja": true + }, + { + "name": "Current File", + "type": "debugpy", + "request": "launch", + "program": "${file}", + "jinja": true + } + ] +} diff --git a/{{ cookiecutter.__project_name_kebab_case }}/CHANGELOG.md b/{{ cookiecutter.__project_name_kebab_case }}/CHANGELOG.md new file mode 100644 index 00000000..37473ff1 --- /dev/null +++ b/{{ cookiecutter.__project_name_kebab_case }}/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- Initial project scaffolding with baseline-app-cookiecutter. diff --git a/{{ cookiecutter.__project_name_kebab_case }}/CONTRIBUTING.md b/{{ cookiecutter.__project_name_kebab_case }}/CONTRIBUTING.md index 462804ec..d67c6f57 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/CONTRIBUTING.md +++ b/{{ cookiecutter.__project_name_kebab_case }}/CONTRIBUTING.md @@ -93,7 +93,7 @@ and then install the dependencies and the project with
Extra steps -After setting up the developpement environnement, you'll have to create an .env file at the root of the project, copy the contents of .env_sample into this new file and fill all of the variables with the appropriate values. +After setting up the developpement environnement, you'll have to create an .env file at the root of the project, copy the contents of .env.example into this new file and fill all of the variables with the appropriate values.
diff --git a/{{ cookiecutter.__project_name_kebab_case }}/Dockerfile b/{{ cookiecutter.__project_name_kebab_case }}/Dockerfile index 7d53caa2..8053ca90 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/Dockerfile +++ b/{{ cookiecutter.__project_name_kebab_case }}/Dockerfile @@ -4,14 +4,12 @@ FROM {{ cookiecutter.__docker_image }} AS base # Remove docker-clean so we can keep the apt cache in Docker build cache. RUN rm /etc/apt/apt.conf.d/docker-clean -{%- if cookiecutter.development_environment == "strict" %} # Configure Python to print tracebacks on crash [1], and to not buffer stdout and stderr [2]. # [1] https://docs.python.org/3/using/cmdline.html#envvar-PYTHONFAULTHANDLER # [2] https://docs.python.org/3/using/cmdline.html#envvar-PYTHONUNBUFFERED ENV PYTHONFAULTHANDLER=1 ENV PYTHONUNBUFFERED=1 -{%- endif %} # Create a non-root user and switch to it [1]. # [1] https://code.visualstudio.com/remote/advancedcontainers/add-nonroot-user @@ -37,7 +35,7 @@ FROM base AS poetry USER root # Install Poetry in separate venv so it doesn't pollute the main venv. -ENV POETRY_VERSION=2.1.3 +ENV POETRY_VERSION=2.2.1 ENV POETRY_VIRTUAL_ENV=/opt/poetry-env RUN --mount=type=cache,target=/root/.cache/pip/ \ python -m venv $POETRY_VIRTUAL_ENV && \ @@ -69,6 +67,7 @@ RUN --mount=type=cache,target=/var/cache/apt/ \ --mount=type=cache,target=/var/lib/apt/ \ apt-get update && \ apt-get install --no-install-recommends --yes curl git gnupg ssh sudo vim zsh && \ + git config --system safe.directory '*' && \ sh -c "$(curl -fsSL https://starship.rs/install.sh)" -- "--yes" && \ usermod --shell /usr/bin/zsh user && \ echo 'user ALL=(root) NOPASSWD:ALL' > /etc/sudoers.d/user && chmod 0440 /etc/sudoers.d/user @@ -101,13 +100,7 @@ RUN git clone --branch v$ANTIDOTE_VERSION --depth=1 https://github.com/mattmc3/a mkdir ~/.history/ && \ zsh -c 'source ~/.zshrc' -FROM poetry AS base-app - -RUN --mount=type=cache,uid=$UID,gid=$GID,target=/home/user/.cache/pypoetry/ \ - poetry install --only app --all-extras --no-interaction - - -FROM base-app AS app +FROM poetry AS app # Copy the virtual environment from the poetry stage. COPY --from=poetry $VIRTUAL_ENV $VIRTUAL_ENV @@ -116,7 +109,13 @@ COPY --from=poetry $VIRTUAL_ENV $VIRTUAL_ENV COPY --chown=user:user ./src ./src COPY --chown=user:user ./pyproject.toml . COPY --chown=user:user ./poetry.lock . +{%- if cookiecutter.with_fastapi_api|int %} + +# Health check using stdlib only (no curl in slim images). +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" # Expose the app. ENTRYPOINT ["/opt/{{ cookiecutter.__project_name_kebab_case }}-env/bin/poe"] CMD ["api"] +{%- endif %} diff --git a/{{ cookiecutter.__project_name_kebab_case }}/README.md b/{{ cookiecutter.__project_name_kebab_case }}/README.md index 5e04b227..3fdb0afd 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/README.md +++ b/{{ cookiecutter.__project_name_kebab_case }}/README.md @@ -101,4 +101,4 @@ The project is structured as follows: - `docs/`: Contains the ADRs and configuration guides. At the root, you will find the `Dockerfile` and a `docker-compose.yml` file to facilitate the deployment of the application. There is also a `pyproject.toml` file that contains the Poetry project configuration, as well as READMEs for project documentation. -Pour contribuer au projet, lire le fichier [CONTRIBUTING.md](CONTRIBUTING.md). +To contribute to this project, read the [CONTRIBUTING.md](CONTRIBUTING.md) file. diff --git a/{{ cookiecutter.__project_name_kebab_case }}/docker-compose.yml b/{{ cookiecutter.__project_name_kebab_case }}/docker-compose.yml index 16c56d77..98f9a98f 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/docker-compose.yml +++ b/{{ cookiecutter.__project_name_kebab_case }}/docker-compose.yml @@ -36,15 +36,17 @@ services: profiles: - dev +{%- if cookiecutter.with_fastapi_api|int %} api: build: context: . - target: api + target: app tty: true ports: - "8000:8000" profiles: - api +{%- endif %} volumes: command-history-volume: diff --git a/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml b/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml index ae64a349..2f36b9c0 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml +++ b/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml @@ -27,13 +27,16 @@ version_provider = "poetry" {%- if cookiecutter.with_fastapi_api|int %} fastapi = { extras = ["all"], version = ">=0.110.1" } loguru = "^0.7.3" -python-decouple = "^3.8" gunicorn = ">=21.2.0" {%- endif %} {%- if cookiecutter.project_type == "app" %} poethepoet = ">=0.25.0" {%- endif %} +pydantic-settings = ">=2.2.0" python = ">={{ cookiecutter.python_version }},<4.0" +{%- if cookiecutter.with_sentry|int %} +sentry-sdk = { extras = ["fastapi"], version = ">=1.40.0" } +{%- endif %} {%- if cookiecutter.with_typer_cli|int %} typer = { extras = ["all"], version = ">=0.12.0" } {%- endif %} @@ -49,8 +52,13 @@ coverage = { extras = ["toml"], version = ">=7.4.4" } mypy = ">=1.9.0" pre-commit = ">=3.7.0" pytest = ">=8.1.1" +pytest-bdd = ">=7.0.0" pytest-mock = ">=3.14.0" pytest-xdist = ">=3.5.0" +{%- if cookiecutter.with_fastapi_api|int %} +pytest-asyncio = ">=0.23.0" +{%- endif %} +httpx = ">=0.27.0" ruff = ">=0.5.7" {%- if cookiecutter.development_environment == "strict" %} safety = ">=3.1.0" @@ -82,13 +90,11 @@ source = ["src"] directory = "reports/htmlcov" [tool.coverage.xml] # https://coverage.readthedocs.io/en/latest/cmd.html#cmd-xml -directory = "reports/coverage.xml" +output = "reports/coverage.xml" [tool.mypy] # https://mypy.readthedocs.io/en/latest/config_file.html junit_xml = "reports/mypy.xml" -{%- if cookiecutter.with_fastapi_api|int %} plugins = "pydantic.mypy" -{%- endif %} {%- if cookiecutter.development_environment == "strict" %} strict = true disallow_subclassing_any = false @@ -109,7 +115,7 @@ disable_error_code = [ "attr-defined", # Mocked attributes are dynamically defined ] -{%- if cookiecutter.development_environment == "strict" and cookiecutter.with_fastapi_api|int %} +{%- if cookiecutter.development_environment == "strict" %} [tool.pydantic-mypy] # https://pydantic-docs.helpmanual.io/mypy_plugin/#configuring-the-plugin init_forbid_extra = true @@ -119,16 +125,25 @@ warn_untyped_fields = true {%- endif %} [tool.pytest.ini_options] # https://docs.pytest.org/en/latest/reference/reference.html#ini-options-ref -addopts = "--color=yes --doctest-modules --exitfirst --failed-first{% if cookiecutter.development_environment == 'strict' %} --strict-config --strict-markers --typeguard-packages={{ cookiecutter.__project_name_snake_case }}{% endif %} --verbosity=2 --junitxml=reports/pytest.xml" +# Development mode: {{ cookiecutter.development_environment }} {%- if cookiecutter.development_environment == "strict" %} -filterwarnings = ["error", "ignore::DeprecationWarning"] +addopts = "--color=yes --doctest-modules --exitfirst --failed-first --strict-config --strict-markers --typeguard-packages={{ cookiecutter.__project_name_snake_case }} --verbosity=2 --junitxml=reports/pytest.xml" +filterwarnings = ["error", "ignore::DeprecationWarning", "ignore::pytest.PytestUnraisableExceptionWarning"] +{%- else %} +addopts = "--color=yes --doctest-modules --exitfirst --failed-first --verbosity=2 --junitxml=reports/pytest.xml" +{%- endif %} +{%- if cookiecutter.with_fastapi_api|int %} +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" {%- endif %} testpaths = ["tests"] xfail_strict = true +bdd_features_base_dir = "tests/features/" -[tool.ruff] # https://github.com/charliermarsh/ruff +[tool.ruff] # https://docs.astral.sh/ruff/ fix = true line-length = 99 +preview = true src = ["src", "tests"] target-version = "py{{ cookiecutter.python_version.split('.')[:2]|join }}" @@ -138,23 +153,22 @@ select = ["ALL"] ignore = [ "ANN002", # Missing type annotation for args. Complicates the code too much with the current annotation system. "ANN003", # Missing type annotation for kwargs. Complicates the code too much with the current annotation system. - "ANN101", # Missing type annotation for self in method. Useless info as it's always the same and clutters the code. - "ANN102", # Missing type annotation for cls in classmethod. Useless info as it's always the same and clutters the code. "COM812", # Missing trailing comma in a single-line list. Already handled by ruff formater "CPY001", # Copyright notice missing. Not always needed. "E501", # Line too long. Already handled by ruff formater "E731", # Do not assign a lambda expression. Do not agree with this rule, it improves readability as it is self-documenting the lambda function. - "PD901", # Do not use the variable name "df". This is a common name for dataframes and is not a problem. "RET504", # Unnecessary assign before return. This is not bad, it helps debugging. "S311", # Standard pseudo-random generators are not suitable for security/cryptographic purposes. Our use case is not security/cryptographic. "TRY003", # Raise exception with vanilla arguments. Creating exception classes should be done only when really needed. "W505", # Doc line too long. Editor autowraps doc, and it's too much work to fix it. "ISC001", # single-line-implicit-string-concatenation, `z = "The quick " "brown fox."` becomes `z = "The quick brown fox."` This rule may cause conflicts when used with the formatter. + "DOC201", # `return` is not documented in docstring. Too verbose for most projects. + "DOC501", # Raised exception missing from docstring. Too verbose for most projects. ] unfixable = ["ERA001", "F401", "F841", "T201", "T203"] {%- else %} select = ["A", "ASYNC", "B", "C4", "C90", "D", "DTZ", "E", "F", "FLY", "I", "ISC", "LOG", "N", "NPY", "PERF", "PGH", "PIE", "PL", "PT", "Q", "RET", "RUF", "RSE", "SIM", "TID", "UP", "W", "YTT"] -ignore = ["D203", "D213", "E501", "G010", "PGH003", "RET504", "S101", "S307"] +ignore = ["D203", "D213", "E501", "PGH003", "RET504"] unfixable = ["F401", "F841"] {%- endif %} @@ -176,10 +190,12 @@ max-doc-length = 99 [tool.ruff.lint.pydocstyle] convention = "{{ cookiecutter.__docstring_style|lower }}" -[tool.ruff.lint.pylint] +[tool.ruff.lint.pylint] # https://docs.astral.sh/ruff/settings/#lint_pylint max-args = 6 +max-public-methods = 30 [tool.poe.tasks] # https://github.com/nat-n/poethepoet +{%- if cookiecutter.with_fastapi_api|int %} [tool.poe.tasks.api] help = "Serve the REST API" @@ -223,6 +239,7 @@ max-args = 6 type = "boolean" name = "dev" options = ["--dev"] +{%- endif %} [tool.poe.tasks.docs] help = "Generate this {{ cookiecutter.project_type }}'s docs" diff --git a/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/api.py b/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/api.py index db8d60b8..99cc4174 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/api.py +++ b/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/api.py @@ -1,39 +1,136 @@ """{{ cookiecutter.project_name }} REST API.""" -import asyncio import sys +import time from collections.abc import AsyncGenerator from contextlib import asynccontextmanager +from typing import Annotated -from decouple import config as env_vars -from fastapi import FastAPI +from fastapi import Depends, FastAPI, HTTPException, Request, Response +from fastapi.responses import JSONResponse from loguru import logger +from starlette.exceptions import HTTPException as StarletteHTTPException +from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint + +from {{ cookiecutter.__project_name_snake_case }}.models import HealthResponse, Item, ItemCreate +from {{ cookiecutter.__project_name_snake_case }}.services import ItemService +from {{ cookiecutter.__project_name_snake_case }}.settings import settings @asynccontextmanager -async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: # noqa: ARG001 +async def lifespan(app: FastAPI) -> AsyncGenerator[None]: # noqa: ARG001, RUF029 """Handle FastAPI startup and shutdown events.""" logger.remove() - logger.add(sys.stderr, level=env_vars("LOG_LEVEL", default="INFO")) + logger.add(sys.stderr, level=settings.log_level) +{%- if cookiecutter.with_sentry|int %} + + if settings.sentry_dsn: + import sentry_sdk + sentry_sdk.init( + dsn=settings.sentry_dsn, + environment=settings.sentry_environment, + traces_sample_rate=settings.sentry_traces_sample_rate, + ) + logger.info("Sentry initialized for environment '{}'", settings.sentry_environment) +{%- endif %} yield -app = FastAPI(lifespan=lifespan) +app = FastAPI(title=settings.app_name, lifespan=lifespan) + + +# --- Dependency injection -------------------------------------------------------- + + +def get_item_service() -> ItemService: + """Provide the shared ItemService instance.""" + return _item_service + + +_item_service = ItemService() + +ItemServiceDep = Annotated[ItemService, Depends(get_item_service)] + + +# --- Middleware ------------------------------------------------------------------ + + +class RequestLoggingMiddleware(BaseHTTPMiddleware): + """Log method, path, status code, and duration for every request.""" + + async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response: # noqa: PLR6301 + """Process request and log timing information.""" + start = time.perf_counter() + response: Response = await call_next(request) + duration_ms = (time.perf_counter() - start) * 1000 + logger.info( + "{method} {path} {status} {duration:.1f}ms", + method=request.method, + path=request.url.path, + status=response.status_code, + duration=duration_ms, + ) + return response + + +app.add_middleware(RequestLoggingMiddleware) + + +# --- Exception handlers ---------------------------------------------------------- + + +@app.exception_handler(StarletteHTTPException) +def http_exception_handler(request: Request, exc: StarletteHTTPException) -> JSONResponse: # noqa: ARG001 + """Return a structured JSON response for HTTP exceptions.""" + logger.warning("HTTP {status}: {detail}", status=exc.status_code, detail=exc.detail) + return JSONResponse( + status_code=exc.status_code, + content={"detail": exc.detail}, + ) + + +@app.exception_handler(Exception) +def unhandled_exception_handler(request: Request, exc: Exception) -> JSONResponse: # noqa: ARG001 + """Return a 500 JSON response for unhandled exceptions.""" + logger.exception("Unhandled exception: {exc}", exc=exc) + return JSONResponse( + status_code=500, + content={"detail": "Internal server error"}, + ) + + +# --- Routes ---------------------------------------------------------------------- + + +@app.get("/health") +async def health() -> HealthResponse: + """Health check endpoint.""" + return HealthResponse() + + +@app.post("/items", status_code=201) +async def create_item(data: ItemCreate, service: ItemServiceDep) -> Item: + """Create a new item.""" + return service.create(data) -@app.get("/compute") -async def compute(n: int = 42) -> int: - """Compute the result of a CPU-bound function.""" +@app.get("/items") +async def list_items(service: ItemServiceDep) -> list[Item]: + """List all items.""" + return service.list_all() - def fibonacci(n: int) -> int: - return n if n <= 1 else fibonacci(n - 1) + fibonacci(n - 2) - result = await asyncio.to_thread(fibonacci, n) - return result +@app.get("/items/{item_id}") +async def get_item(item_id: int, service: ItemServiceDep) -> Item: + """Get a single item by id.""" + item = service.get(item_id) + if item is None: + raise HTTPException(status_code=404, detail=f"Item {item_id} not found") + return item if __name__ == "__main__": import uvicorn - uvicorn.run(app, port=8000, log_level="info") + uvicorn.run(app, host=settings.api_host, port=settings.api_port, log_level="info") diff --git a/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/cli.py b/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/cli.py index 10cf3386..efc9df46 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/cli.py +++ b/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/cli.py @@ -1,13 +1,49 @@ """{{ cookiecutter.project_name }} CLI.""" +from typing import Annotated + import typer from rich import print # noqa: A004 +from rich.table import Table + +from {{ cookiecutter.__project_name_snake_case }}.settings import settings + + +app = typer.Typer(help="{{ cookiecutter.project_name }} command-line interface.") +_verbose: bool = False -app = typer.Typer() + +@app.callback() +def main( + verbose: Annotated[ # noqa: FBT002 + bool, typer.Option("--verbose", "-v", help="Enable verbose output.") + ] = False, +) -> None: + """{{ cookiecutter.project_name }} CLI.""" + global _verbose # noqa: PLW0603 + _verbose = verbose + + +@app.command() +def info() -> None: + """Display project metadata.""" + table = Table(title="{{ cookiecutter.project_name }}") + table.add_column("Key", style="cyan") + table.add_column("Value", style="green") + table.add_row("App name", settings.app_name) + table.add_row("Log level", settings.log_level) + table.add_row("Debug", str(settings.debug)) + if _verbose: + table.add_row("Settings class", type(settings).__name__) + print(table) @app.command() -def fire(name: str = "Chell") -> None: - """Fire portal gun.""" - print(f"[bold red]Alert![/bold red] {name} fired [green]portal gun[/green] :boom:") +def greet( + name: Annotated[str, typer.Argument(help="Name to greet.")], +) -> None: + """Greet someone by name.""" + if _verbose: + print(f"[dim]Greeting {name}...[/dim]") + print(f"[bold green]Hello, {name}![/bold green]") diff --git a/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/models.py b/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/models.py new file mode 100644 index 00000000..b7f3dc06 --- /dev/null +++ b/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/models.py @@ -0,0 +1,23 @@ +"""{{ cookiecutter.project_name }} data models.""" + +from pydantic import BaseModel, Field + + +class HealthResponse(BaseModel): + """Health check response.""" + + status: str = "ok" + + +class ItemCreate(BaseModel): + """Schema for creating a new item.""" + + name: str = Field(min_length=1, max_length=100, description="Item name") + description: str = Field(default="", max_length=500, description="Item description") + price: float = Field(gt=0, description="Item price (must be positive)") + + +class Item(ItemCreate): + """Schema for a stored item.""" + + id: int = Field(description="Unique item identifier") diff --git a/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/services.py b/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/services.py new file mode 100644 index 00000000..c7e78192 --- /dev/null +++ b/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/services.py @@ -0,0 +1,27 @@ +"""{{ cookiecutter.project_name }} service layer.""" + +from {{ cookiecutter.__project_name_snake_case }}.models import Item, ItemCreate + + +class ItemService: + """In-memory item service demonstrating the service layer pattern.""" + + def __init__(self) -> None: + """Initialize the service with an empty item store.""" + self._items: dict[int, Item] = {} + self._next_id: int = 1 + + def create(self, data: ItemCreate) -> Item: + """Create a new item and return it with an assigned id.""" + item = Item(id=self._next_id, **data.model_dump()) + self._items[self._next_id] = item + self._next_id += 1 + return item + + def get(self, item_id: int) -> Item | None: + """Return an item by id, or None if not found.""" + return self._items.get(item_id) + + def list_all(self) -> list[Item]: + """Return all items.""" + return list(self._items.values()) diff --git a/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/settings.py b/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/settings.py new file mode 100644 index 00000000..e676d075 --- /dev/null +++ b/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/settings.py @@ -0,0 +1,25 @@ +"""{{ cookiecutter.project_name }} settings.""" + +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + """Application settings loaded from environment variables and .env file.""" + + model_config = SettingsConfigDict(env_file=".env", extra="ignore") + + app_name: str = "{{ cookiecutter.project_name }}" + log_level: str = "INFO" + debug: bool = False +{%- if cookiecutter.with_fastapi_api|int %} + api_host: str = "0.0.0.0" # noqa: S104 + api_port: int = 8000 +{%- endif %} +{%- if cookiecutter.with_sentry|int %} + sentry_dsn: str = "" + sentry_environment: str = "development" + sentry_traces_sample_rate: float = 0.1 +{%- endif %} + + +settings = Settings() diff --git a/{{ cookiecutter.__project_name_kebab_case }}/tests/api_test.py b/{{ cookiecutter.__project_name_kebab_case }}/tests/api_test.py deleted file mode 100644 index cfc5a8ff..00000000 --- a/{{ cookiecutter.__project_name_kebab_case }}/tests/api_test.py +++ /dev/null @@ -1,15 +0,0 @@ -"""Test {{ cookiecutter.project_name }} REST API.""" - -import httpx -from fastapi.testclient import TestClient - -from {{cookiecutter.__project_name_snake_case}}.api import app - - -client = TestClient(app) - - -def test_read_root() -> None: - """Test that reading the root is successful.""" - response = client.get("/compute", params={"n": 7}) - assert httpx.codes.is_success(response.status_code) diff --git a/{{ cookiecutter.__project_name_kebab_case }}/tests/cli_test.py b/{{ cookiecutter.__project_name_kebab_case }}/tests/cli_test.py deleted file mode 100644 index c9bd159f..00000000 --- a/{{ cookiecutter.__project_name_kebab_case }}/tests/cli_test.py +++ /dev/null @@ -1,16 +0,0 @@ -"""Test {{ cookiecutter.project_name }} CLI.""" - -from typer.testing import CliRunner - -from {{cookiecutter.__project_name_snake_case}}.cli import app - - -runner = CliRunner() - - -def test_fire() -> None: - """Test that the fire command works as expected.""" - name = "GLaDOS" - result = runner.invoke(app, ["--name", name]) - assert result.exit_code == 0 - assert name in result.stdout diff --git a/{{ cookiecutter.__project_name_kebab_case }}/tests/conftest.py b/{{ cookiecutter.__project_name_kebab_case }}/tests/conftest.py new file mode 100644 index 00000000..507b9fd5 --- /dev/null +++ b/{{ cookiecutter.__project_name_kebab_case }}/tests/conftest.py @@ -0,0 +1,14 @@ +"""Shared fixtures for the test suite.""" +{%- if cookiecutter.with_fastapi_api|int %} + +import pytest +from fastapi.testclient import TestClient + +from {{ cookiecutter.__project_name_snake_case }}.api import app + + +@pytest.fixture +def api_client() -> TestClient: + """Provide a FastAPI test client.""" + return TestClient(app) +{%- endif %} diff --git a/{{ cookiecutter.__project_name_kebab_case }}/tests/features/api.feature b/{{ cookiecutter.__project_name_kebab_case }}/tests/features/api.feature new file mode 100644 index 00000000..1be6f76c --- /dev/null +++ b/{{ cookiecutter.__project_name_kebab_case }}/tests/features/api.feature @@ -0,0 +1,19 @@ +Feature: REST API + The API should handle requests correctly. + + Scenario: Health endpoint returns ok + Given the API test client + When I request GET /health + Then the response status code should be 200 + And the response JSON should contain "status" = "ok" + + Scenario: Create an item + Given the API test client + When I create an item with name "Widget" and price 9.99 + Then the response status code should be 201 + And the response JSON should contain "name" = "Widget" + + Scenario: Get a non-existent item returns 404 + Given the API test client + When I request GET /items/999 + Then the response status code should be 404 diff --git a/{{ cookiecutter.__project_name_kebab_case }}/tests/features/cli.feature b/{{ cookiecutter.__project_name_kebab_case }}/tests/features/cli.feature new file mode 100644 index 00000000..9c0bcc64 --- /dev/null +++ b/{{ cookiecutter.__project_name_kebab_case }}/tests/features/cli.feature @@ -0,0 +1,20 @@ +Feature: CLI + The CLI should execute commands correctly. + + Scenario: Info command displays metadata + Given a CLI runner + When I run the info command + Then the exit code should be 0 + And the output should contain "{{ cookiecutter.project_name }}" + + Scenario: Greet command outputs name + Given a CLI runner + When I run the greet command with name "Alice" + Then the exit code should be 0 + And the output should contain "Alice" + + Scenario: Verbose flag is accepted + Given a CLI runner + When I run the greet command with verbose and name "Bob" + Then the exit code should be 0 + And the output should contain "Bob" diff --git a/{{ cookiecutter.__project_name_kebab_case }}/tests/features/import.feature b/{{ cookiecutter.__project_name_kebab_case }}/tests/features/import.feature new file mode 100644 index 00000000..75392a31 --- /dev/null +++ b/{{ cookiecutter.__project_name_kebab_case }}/tests/features/import.feature @@ -0,0 +1,6 @@ +Feature: Package import + The package should be importable and correctly configured. + + Scenario: Import the package + Given the package is installed + Then the package name should be a string diff --git a/{{ cookiecutter.__project_name_kebab_case }}/tests/import_test.py b/{{ cookiecutter.__project_name_kebab_case }}/tests/import_test.py deleted file mode 100644 index 9cdcbcc0..00000000 --- a/{{ cookiecutter.__project_name_kebab_case }}/tests/import_test.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Test {{ cookiecutter.project_name }}.""" - -import {{ cookiecutter.__project_name_snake_case }} - - -def test_import() -> None: - """Test that the {{ cookiecutter.project_type }} can be imported.""" - assert isinstance({{ cookiecutter.__project_name_snake_case }}.__name__, str) diff --git a/{{ cookiecutter.__project_name_kebab_case }}/tests/test_api.py b/{{ cookiecutter.__project_name_kebab_case }}/tests/test_api.py new file mode 100644 index 00000000..47bf2700 --- /dev/null +++ b/{{ cookiecutter.__project_name_kebab_case }}/tests/test_api.py @@ -0,0 +1,46 @@ +{%- raw %}"""BDD step definitions for API tests."""{% endraw %} + +from fastapi.testclient import TestClient +from httpx import Response +from pytest_bdd import given, parsers, scenarios, then, when + +from {{ cookiecutter.__project_name_snake_case }}.api import app + + +scenarios("api.feature") + + +@given("the API test client", target_fixture="client") +def api_client() -> TestClient: + """Provide a test client for the API.""" + return TestClient(app) + + +@when( + parsers.cfparse("I request GET {path}"), + target_fixture="response", +) +def request_get(client: TestClient, path: str) -> Response: + """Send a GET request to the given path.""" + return client.get(path) + + +@when( + parsers.cfparse('I create an item with name "{name}" and price {price:f}'), + target_fixture="response", +) +def create_item(client: TestClient, name: str, price: float) -> Response: + """Send a POST request to create an item.""" + return client.post("/items", json={"name": name, "price": price}) + + +@then(parsers.cfparse("the response status code should be {code:d}")) +def check_status_code(response: Response, code: int) -> None: + """Verify the response status code.""" + assert response.status_code == code + + +@then(parsers.cfparse('the response JSON should contain "{key}" = "{value}"')) +def check_json_field(response: Response, key: str, value: str) -> None: + """Verify a field in the response JSON.""" + assert response.json()[key] == value diff --git a/{{ cookiecutter.__project_name_kebab_case }}/tests/test_cli.py b/{{ cookiecutter.__project_name_kebab_case }}/tests/test_cli.py new file mode 100644 index 00000000..9f9c844b --- /dev/null +++ b/{{ cookiecutter.__project_name_kebab_case }}/tests/test_cli.py @@ -0,0 +1,51 @@ +"""BDD step definitions for CLI tests.""" + +from pytest_bdd import given, parsers, scenarios, then, when +from typer.testing import CliRunner, Result + +from {{ cookiecutter.__project_name_snake_case }}.cli import app + + +scenarios("cli.feature") + + +@given("a CLI runner", target_fixture="runner") +def cli_runner() -> CliRunner: + """Provide a CLI test runner.""" + return CliRunner() + + +@when("I run the info command", target_fixture="result") +def run_info(runner: CliRunner) -> Result: + """Execute the info command.""" + return runner.invoke(app, ["info"]) + + +@when( + parsers.cfparse('I run the greet command with name "{name}"'), + target_fixture="result", +) +def run_greet(runner: CliRunner, name: str) -> Result: + """Execute the greet command with the given name.""" + return runner.invoke(app, ["greet", name]) + + +@when( + parsers.cfparse('I run the greet command with verbose and name "{name}"'), + target_fixture="result", +) +def run_greet_verbose(runner: CliRunner, name: str) -> Result: + """Execute the greet command with verbose flag.""" + return runner.invoke(app, ["--verbose", "greet", name]) + + +@then(parsers.cfparse("the exit code should be {code:d}")) +def check_exit_code(result: Result, code: int) -> None: + """Verify the CLI exit code.""" + assert result.exit_code == code + + +@then(parsers.cfparse('the output should contain "{text}"')) +def check_output_contains(result: Result, text: str) -> None: + """Verify the CLI output contains expected text.""" + assert text in result.stdout diff --git a/{{ cookiecutter.__project_name_kebab_case }}/tests/test_import.py b/{{ cookiecutter.__project_name_kebab_case }}/tests/test_import.py new file mode 100644 index 00000000..8a27549a --- /dev/null +++ b/{{ cookiecutter.__project_name_kebab_case }}/tests/test_import.py @@ -0,0 +1,22 @@ +"""BDD step definitions for package import tests.""" + +from types import ModuleType + +from pytest_bdd import given, scenarios, then + +import {{ cookiecutter.__project_name_snake_case }} + + +scenarios("import.feature") + + +@given("the package is installed", target_fixture="package") +def installed_package() -> ModuleType: + """Return the installed package module.""" + return {{ cookiecutter.__project_name_snake_case }} + + +@then("the package name should be a string") +def check_package_name(package: ModuleType) -> None: + """Verify the package has a valid name.""" + assert isinstance(package.__name__, str) From 3d4367e2d4bb658acd7fac4fe1524847bc5ab948 Mon Sep 17 00:00:00 2001 From: David Beauchemin Date: Sat, 21 Feb 2026 13:13:16 -0500 Subject: [PATCH 63/68] docs: remove emojis from README and add how-to section (#20) Co-authored-by: davebulaval Co-authored-by: Claude Opus 4.6 --- README.md | 86 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 67 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 5ec92c6c..cb753bd9 100644 --- a/README.md +++ b/README.md @@ -5,22 +5,22 @@ A modern [Cookiecutter](https://github.com/cookiecutter/cookiecutter) template for scaffolding Python packages and apps. -## 🎁 Features - -- πŸ§‘β€πŸ’» Quick and reproducible development environments with VS Code's [Dev Containers](https://code.visualstudio.com/docs/devcontainers/containers), PyCharm's [Docker Compose interpreter](https://www.jetbrains.com/help/pycharm/using-docker-compose-as-a-remote-interpreter.html#docker-compose-remote), and [GitHub Codespaces](https://github.com/features/codespaces) -- 🌈 Cross-platform support for Linux, macOS (Apple silicon and Intel), and Windows -- 🐚 Modern shell prompt with [Starship](https://github.com/starship/starship) -- πŸ“¦ Packaging and dependency management with [Poetry](https://github.com/python-poetry/poetry) -- ⚑️ Task running with [Poe the Poet](https://github.com/nat-n/poethepoet) -- ✍️ Code formatting with [Ruff](https://github.com/charliermarsh/ruff) -- βœ… Code linting with [Pre-commit](https://pre-commit.com/), [Mypy](https://github.com/python/mypy), and [Ruff](https://github.com/charliermarsh/ruff) -- 🏷 Optionally follows the [Conventional Commits](https://www.conventionalcommits.org/) standard to automate [Semantic Versioning](https://semver.org/) and [Keep A Changelog](https://keepachangelog.com/) with [Commitizen](https://github.com/commitizen-tools/commitizen) -- ♻️ Continuous integration with [GitHub Actions](https://docs.github.com/en/actions) -- πŸ§ͺ Test coverage with [Coverage.py](https://github.com/nedbat/coveragepy) -- πŸ— Scaffolding updates with [Cookiecutter](https://github.com/cookiecutter/cookiecutter) and [Cruft](https://github.com/cruft/cruft) -- 🧰 Dependency updates with [Dependabot](https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/about-dependabot-version-updates) - -## ✨ Using +## Features + +- Quick and reproducible development environments with VS Code's [Dev Containers](https://code.visualstudio.com/docs/devcontainers/containers), PyCharm's [Docker Compose interpreter](https://www.jetbrains.com/help/pycharm/using-docker-compose-as-a-remote-interpreter.html#docker-compose-remote), and [GitHub Codespaces](https://github.com/features/codespaces) +- Cross-platform support for Linux, macOS (Apple silicon and Intel), and Windows +- Modern shell prompt with [Starship](https://github.com/starship/starship) +- Packaging and dependency management with [Poetry](https://github.com/python-poetry/poetry) +- Task running with [Poe the Poet](https://github.com/nat-n/poethepoet) +- Code formatting with [Ruff](https://github.com/charliermarsh/ruff) +- Code linting with [Pre-commit](https://pre-commit.com/), [Mypy](https://github.com/python/mypy), and [Ruff](https://github.com/charliermarsh/ruff) +- Optionally follows the [Conventional Commits](https://www.conventionalcommits.org/) standard to automate [Semantic Versioning](https://semver.org/) and [Keep A Changelog](https://keepachangelog.com/) with [Commitizen](https://github.com/commitizen-tools/commitizen) +- Continuous integration with [GitHub Actions](https://docs.github.com/en/actions) +- Test coverage with [Coverage.py](https://github.com/nedbat/coveragepy) +- Scaffolding updates with [Cookiecutter](https://github.com/cookiecutter/cookiecutter) and [Cruft](https://github.com/cruft/cruft) +- Dependency updates with [Dependabot](https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/about-dependabot-version-updates) + +## Using ### Creating a new Python project @@ -42,9 +42,9 @@ To create a new Python project with this template:
- ⚠️ If your repository name β‰  the project's slugified name + If your repository name β‰  the project's slugified name - If your repository name differs from your project's slugified name (see `project_name` in the [Template parameters](https://github.com/Baseline-quebec/baseline-app-cookiecutter#-template-parameters) below), you will need to copy the scaffolded project into the repository with: + If your repository name differs from your project's slugified name (see `project_name` in the [Template parameters](https://github.com/Baseline-quebec/baseline-app-cookiecutter#template-parameters) below), you will need to copy the scaffolded project into the repository with: ```sh cp -r {project-name}/ {repository-name}/ @@ -66,7 +66,55 @@ To update your Python project to the latest template version: 2. If any of the file updates failed, resolve them by inspecting the corresponding `.rej` files. -## πŸ€“ Template parameters +## How-to + +### Run the development server (FastAPI) + +```sh +poetry run poe api +``` + +### Run the CLI + +```sh +poetry run my-app info +poetry run my-app greet "World" +``` + +### Run tests + +```sh +poetry run poe test +``` + +### Run linting + +```sh +poetry run poe lint +``` + +### Add a new dependency + +```sh +poetry add # runtime dependency +poetry add --group dev # development dependency +``` + +### Configure environment variables + +Copy the example file and fill in the values: + +```sh +cp .env.example .env +``` + +### Build and run with Docker + +```sh +docker compose up --build +``` + +## Template parameters | Parameter | Description | From c112ec84cc28decaea9d5dbdb5d4c197058ef36e Mon Sep 17 00:00:00 2001 From: David Beauchemin Date: Sat, 21 Feb 2026 14:16:17 -0500 Subject: [PATCH 64/68] =?UTF-8?q?feat:=20sprint=203=20=E2=80=94=20template?= =?UTF-8?q?=20improvements=20(10=20items)=20(#21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: sprint 3 β€” template improvements (10 items) - Add license parameter (MIT, Apache-2.0, Proprietary) with LICENSE template - Add github_org parameter for repository URL construction - Add with_pytest_bdd parameter (default off) with conditional BDD/plain tests - Bump all dependency versions (ruff, fastapi, pytest, mypy, sentry-sdk, etc.) - Update pre-commit-hooks v5β†’v6, Poetry 2.2.1β†’2.3.2 - Add Docker layer caching + Buildx to CI workflow - Simplify Dockerfile from 4 stages to 3 (merge base+poetry) - Replace CLI greet command with config + health commands - Add poe update task for cruft template sync - Add first ADR (0001-record-architecture-decisions) and Confluence link - Add dependabot.yml at repo root - Add 46 template generation tests (tests/test_cookiecutter.py) Co-Authored-By: Claude Opus 4.6 * feat: sprint 4 β€” CI/CD, codespell, mkdocs, lint fixes Template improvements: - Replace pdoc with MkDocs Material for documentation - Add codespell linter to pre-commit and pyproject.toml - Add PR title check workflow (conventional commits) - Bump actions/checkout to v6 - Fix ruff lint errors (FURB171, PLC0415, PLR2004, PLR6201) Root CI/CD: - Add fast CI workflow (pytest, Python 3.12/3.13) - Add PR title check workflow - Modernize integration test (matrix full/minimal, cruft check, pip cache) - Enrich pre-commit (pre-commit-hooks, actionlint, ruff) - Add CODEOWNERS (@davebulaval, @dpothier) Co-Authored-By: Claude Opus 4.6 * fix: resolve ruff and codespell lint errors in generated template - cli.py: iterate dict keys directly instead of .items() (B007, PERF102) - CONTRIBUTING.md: fix typos (developpement, environnement, developpers) - pyproject.toml: fix "formater" β†’ "formatter", add Jupyter to codespell ignore Co-Authored-By: Claude Opus 4.6 * fix: resolve remaining ruff and coverage errors in generated template - cli.py: remove unused `import sys`, remove stale noqa comments (S310, BLE001) - test_import.py: add settings test to ensure coverage > 50% in minimal config Co-Authored-By: Claude Opus 4.6 * fix: correct Jinja whitespace in cli.py imports Fix blank line between stdlib imports causing ruff I001 (unsorted imports) and ruff format failure. Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: davebulaval Co-authored-by: Claude Opus 4.6 --- .github/CODEOWNERS | 1 + .github/dependabot.yml | 42 ++ .github/workflows/ci.yml | 30 + .github/workflows/pr.yml | 19 + .github/workflows/test.yml | 21 +- .pre-commit-config.yaml | 27 + README.md | 20 +- cookiecutter.json | 5 +- hooks/post_gen_project.py | 21 +- tests/test_cookiecutter.py | 637 ++++++++++++++++++ .../.github/workflows/pr.yml | 22 + .../.github/workflows/test.yml | 17 +- .../.pre-commit-config.yaml | 8 +- .../CONTRIBUTING.md | 4 +- .../Dockerfile | 43 +- .../LICENSE | 46 ++ .../README.md | 10 +- .../0001-record-architecture-decisions.md | 23 + .../docs/index.md | 11 + .../mkdocs.yml | 12 + .../pyproject.toml | 88 +-- .../cli.py | 37 +- .../tests/features/cli.feature | 16 +- .../tests/test_api.py | 33 + .../tests/test_cli.py | 70 +- .../tests/test_import.py | 18 + 26 files changed, 1170 insertions(+), 111 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/pr.yml create mode 100644 tests/test_cookiecutter.py create mode 100644 {{ cookiecutter.__project_name_kebab_case }}/.github/workflows/pr.yml create mode 100644 {{ cookiecutter.__project_name_kebab_case }}/LICENSE create mode 100644 {{ cookiecutter.__project_name_kebab_case }}/docs/decisions/0001-record-architecture-decisions.md create mode 100644 {{ cookiecutter.__project_name_kebab_case }}/docs/index.md create mode 100644 {{ cookiecutter.__project_name_kebab_case }}/mkdocs.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..a6696ddf --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @davebulaval @dpothier diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..302a98db --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,42 @@ +version: 2 + +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + commit-message: + prefix: "ci" + include: scope + groups: + ci-dependencies: + patterns: + - "*" + update-types: + - "minor" + - "patch" + ci-major-updates: + patterns: + - "*" + update-types: + - "major" + - package-ecosystem: pip + directory: / + schedule: + interval: monthly + commit-message: + prefix: "chore" + prefix-development: "build" + include: scope + groups: + dependencies: + patterns: + - "*" + update-types: + - "minor" + - "patch" + major-updates: + patterns: + - "*" + update-types: + - "major" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..03beb0f9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.12", "3.13"] + + name: Unit tests β€” Python ${{ matrix.python-version }} + + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + + - name: Install dependencies + run: pip install cookiecutter pytest pyyaml + + - name: Run tests + run: pytest tests/ -v diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 00000000..e1779dfa --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,19 @@ +name: PR + +on: + pull_request: + types: [edited, opened, reopened, synchronize] + +jobs: + title: + runs-on: ubuntu-latest + name: Check PR title + steps: + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Check PR title + run: | + pip install commitizen + cz check --message "${{ github.event.pull_request.title }}" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 04f16c8d..797ee6d4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -name: Test +name: Integration on: push: @@ -14,13 +14,19 @@ jobs: fail-fast: false matrix: python-version: ["3.12", "3.13"] - project-type: ["app"] + include: + - python-version: "3.12" + variant: full + extra-context: '{"project_type": "app", "project_name": "My Project", "python_version": "3.12", "with_fastapi_api": "1", "with_typer_cli": "1", "with_sentry": "0"}' + - python-version: "3.13" + variant: minimal + extra-context: '{"project_type": "app", "project_name": "My Project", "python_version": "3.13", "with_fastapi_api": "0", "with_typer_cli": "0", "with_sentry": "0"}' - name: Python ${{ matrix.python-version }} app + name: Python ${{ matrix.python-version }} β€” ${{ matrix.variant }} steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: path: template @@ -28,11 +34,16 @@ jobs: uses: actions/setup-python@v5 with: python-version: "3.12" + cache: pip - name: Scaffold Python project run: | pip install --no-input cruft - cruft create --no-input --extra-context '{"project_type": "app", "project_name": "My Project", "python_version": "${{ matrix.python-version }}", "with_fastapi_api": "1", "with_typer_cli": "1", "with_sentry": "0"}' ./template/ + cruft create --no-input --extra-context '${{ matrix.extra-context }}' ./template/ + + - name: Verify cruft link + run: cruft check + working-directory: ./my-project/ - name: Set up Node.js uses: actions/setup-node@v4 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 45ed1878..d440b9e5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,6 +3,19 @@ default_install_hook_types: [commit-msg, pre-commit] default_stages: [commit, manual] fail_fast: true repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-yaml + args: [--allow-multiple-documents, --unsafe] + - id: check-toml + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/rhysd/actionlint + rev: v1.7.11 + hooks: + - id: actionlint + files: ^\.github/workflows/ - repo: local hooks: - id: commitizen @@ -12,3 +25,17 @@ repos: require_serial: true language: system stages: [commit-msg] + - id: ruff-check + name: ruff check + entry: ruff check + args: ["--force-exclude"] + language: system + types_or: [python, pyi] + pass_filenames: false + - id: ruff-format + name: ruff format + entry: ruff format + args: [--force-exclude, --check] + language: system + types_or: [python, pyi] + pass_filenames: false diff --git a/README.md b/README.md index cb753bd9..4d485f9f 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,10 @@ A modern [Cookiecutter](https://github.com/cookiecutter/cookiecutter) template f - Optionally follows the [Conventional Commits](https://www.conventionalcommits.org/) standard to automate [Semantic Versioning](https://semver.org/) and [Keep A Changelog](https://keepachangelog.com/) with [Commitizen](https://github.com/commitizen-tools/commitizen) - Continuous integration with [GitHub Actions](https://docs.github.com/en/actions) - Test coverage with [Coverage.py](https://github.com/nedbat/coveragepy) -- Scaffolding updates with [Cookiecutter](https://github.com/cookiecutter/cookiecutter) and [Cruft](https://github.com/cruft/cruft) +- Scaffolding updates with [Cookiecutter](https://github.com/cookiecutter/cookiecutter) and [Cruft](https://github.com/cruft/cruft) (with `poe update` task) - Dependency updates with [Dependabot](https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/about-dependabot-version-updates) +- [Architecture Decision Records](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions) (ADR) template in `docs/decisions/` +- Confluence documentation link placeholder in generated README ## Using @@ -78,7 +80,8 @@ poetry run poe api ```sh poetry run my-app info -poetry run my-app greet "World" +poetry run my-app config +poetry run my-app health # checks API health endpoint ``` ### Run tests @@ -114,6 +117,12 @@ cp .env.example .env docker compose up --build ``` +## Upstream sync + +This template is a fork of [superlinear-ai/substrate](https://github.com/superlinear-ai/substrate). The upstream has since migrated to [uv](https://github.com/astral-sh/uv) (replacing Poetry), [Copier](https://copier.readthedocs.io/) (replacing Cookiecutter), and [ty](https://github.com/astral-sh/ty) (replacing Mypy). These are major structural changes that would require reworking the entire Baseline toolchain and CI/CD pipelines. + +We intentionally stay on **Poetry + Cookiecutter + Mypy** to maintain compatibility with existing Baseline projects and internal workflows. Instead of a full upstream merge, we cherry-pick individual improvements that are independent of the build system migration (e.g., new pre-commit hooks, GitHub Actions version bumps, documentation tooling). + ## Template parameters @@ -121,12 +130,15 @@ docker compose up --build | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `project_name`
"Spline Reticulator" | The name of the project. Will be slugified to `snake_case` for importing and `kebab-case` for installing. For example, `My Package` will be `my_package` for importing and `my-package` for installing. | | `project_description`
"A Python package that reticulates splines." | A single-line description of the project. | -| `project_url`
"" | The URL to the project's repository. | +| `github_org`
"Baseline-quebec" | The GitHub organization or user that owns the repository. Used to construct the project URL and other references. | +| `project_url`
"" | The URL to the project's repository. Automatically constructed from `github_org` and `project_name`. | | `author_name`
"John Smith" | The full name of the primary author of the project. | | `author_email`
"" | The email address of the primary author of the project. | +| `license`
["Proprietary", "MIT", "Apache-2.0"] | The license for the project. Generates a LICENSE file for MIT and Apache-2.0. Proprietary projects get no LICENSE file. | | `python_version`
"3.12" | The minimum Python version that the project requires. | | `development_environment`
["simple", "strict"] | Whether to configure the development environment with a focus on simplicity or with a focus on strictness. In strict mode, additional [Ruff rules](https://docs.astral.sh/ruff/rules/) are added, and tools such as [Mypy](https://github.com/python/mypy) and [Pytest](https://github.com/pytest-dev/pytest) are set to strict mode. | | `with_fastapi_api`
["0", "1"] | If "1", [FastAPI](https://github.com/tiangolo/fastapi) is added as a run time dependency, FastAPI API stubs and tests are added, a `poe api` command for serving the API is added. | | `with_conventional_commits`
["0", "1"] | If "1", [Commitizen](https://github.com/commitizen-tools/commitizen) is added for conventional commits and semantic versioning. Automatically set to "1" in strict mode. | | `with_typer_cli`
["0", "1"] | If "1", [Typer](https://github.com/tiangolo/typer) is added as a run time dependency, Typer CLI stubs and tests are added, the package itself is registered as a CLI. | -| `with_sentry`
["0", "1"] | If "1", [Sentry SDK](https://docs.sentry.io/platforms/python/) is added with FastAPI integration. Requires `with_fastapi_api=1`. Adds `sentry_dsn`, `sentry_environment`, and `sentry_traces_sample_rate` settings. | +| `with_sentry`
["0", "1"] | If "1", [Sentry SDK](https://docs.sentry.io/platforms/python/) is added with FastAPI integration. Requires `with_fastapi_api=1`. Adds `sentry_dsn`, `sentry_environment`, and `sentry_traces_sample_rate` settings. | +| `with_pytest_bdd`
["0", "1"] | If "1", [pytest-bdd](https://github.com/pytest-dev/pytest-bdd) is added with BDD-style tests using Gherkin feature files. Default is "0" (plain pytest). | diff --git a/cookiecutter.json b/cookiecutter.json index 8edc1fa0..c10869fa 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -2,9 +2,11 @@ "project_type": "app", "project_name": "my-app", "project_description": "A Python {{ cookiecutter.project_type }} that reticulates splines.", - "project_url": "https://github.com/user/my-{{ cookiecutter.project_type }}", + "github_org": "Baseline-quebec", + "project_url": "https://github.com/{{ cookiecutter.github_org }}/{{ cookiecutter.project_name|slugify }}", "author_name": "John Smith", "author_email": "john@example.com", + "license": ["Proprietary", "MIT", "Apache-2.0"], "python_version": "3.12", "development_environment": [ "strict", @@ -13,6 +15,7 @@ "with_conventional_commits": "{% if cookiecutter.development_environment == 'simple' %}0{% else %}1{% endif %}", "with_fastapi_api": "1", "with_typer_cli": "1", + "with_pytest_bdd": "0", "with_sentry": "0", "__docker_image": "python:$PYTHON_VERSION-slim", "__docstring_style": "Google", diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py index 40314443..b56ba016 100644 --- a/hooks/post_gen_project.py +++ b/hooks/post_gen_project.py @@ -6,8 +6,15 @@ # Read Cookiecutter configuration. project_name = "{{ cookiecutter.__project_name_snake_case }}" development_environment = "{{ cookiecutter.development_environment }}" +with_conventional_commits = int("{{ cookiecutter.with_conventional_commits }}") with_fastapi_api = int("{{ cookiecutter.with_fastapi_api }}") with_typer_cli = int("{{ cookiecutter.with_typer_cli }}") +with_pytest_bdd = int("{{ cookiecutter.with_pytest_bdd }}") +license_choice = "{{ cookiecutter.license }}" + +# Remove PR title check workflow if conventional commits is disabled. +if not with_conventional_commits: + os.remove(".github/workflows/pr.yml") # Remove py.typed and Dependabot if not in strict mode. if development_environment != "strict": @@ -20,14 +27,24 @@ os.remove(f"src/{project_name}/models.py") os.remove(f"src/{project_name}/services.py") os.remove("tests/test_api.py") - os.remove("tests/features/api.feature") + if with_pytest_bdd: + os.remove("tests/features/api.feature") # Remove Typer if not selected. if not with_typer_cli: os.remove(f"src/{project_name}/cli.py") os.remove("tests/test_cli.py") - os.remove("tests/features/cli.feature") + if with_pytest_bdd: + os.remove("tests/features/cli.feature") # Remove .vscode/ directory if not using FastAPI (no launch.json needed). if not with_fastapi_api and not with_typer_cli: shutil.rmtree(".vscode", ignore_errors=True) + +# Remove BDD test infrastructure when pytest-bdd is not selected. +if not with_pytest_bdd: + shutil.rmtree("tests/features", ignore_errors=True) + +# Remove LICENSE file for proprietary projects. +if license_choice == "Proprietary": + os.remove("LICENSE") diff --git a/tests/test_cookiecutter.py b/tests/test_cookiecutter.py new file mode 100644 index 00000000..1c4d9980 --- /dev/null +++ b/tests/test_cookiecutter.py @@ -0,0 +1,637 @@ +"""Tests for the cookiecutter template generation. + +These tests validate that the template generates correctly with various +combinations of parameters and that post-generation hooks work as expected. +""" + +import os +import subprocess +from pathlib import Path + +import pytest +from cookiecutter.main import cookiecutter + + +TEMPLATE_DIR = str(Path(__file__).parent.parent) + + +@pytest.fixture +def output_dir(tmp_path: Path) -> Path: + """Provide a temporary output directory.""" + return tmp_path + + +def bake(output_dir: Path, **extra_context: str) -> Path: + """Run cookiecutter and return the generated project path.""" + defaults = { + "project_name": "test-project", + "github_org": "TestOrg", + "license": "MIT", + "python_version": "3.12", + "development_environment": "strict", + "with_fastapi_api": "1", + "with_typer_cli": "1", + "with_pytest_bdd": "0", + "with_sentry": "0", + } + defaults.update(extra_context) + cookiecutter( + TEMPLATE_DIR, + no_input=True, + output_dir=str(output_dir), + extra_context=defaults, + ) + return output_dir / "test-project" + + +# --------------------------------------------------------------------------- +# Basic generation tests +# --------------------------------------------------------------------------- + + +class TestBasicGeneration: + """Verify that the template generates without errors.""" + + def test_default_options(self, output_dir: Path) -> None: + """Template generates with default options.""" + project = bake(output_dir) + assert project.is_dir() + assert (project / "pyproject.toml").is_file() + assert (project / "src" / "test_project" / "__init__.py").is_file() + + def test_project_name_slugified(self, output_dir: Path) -> None: + """Project name is correctly slugified.""" + project = bake(output_dir, project_name="My Cool App") + expected = output_dir / "my-cool-app" + assert expected.is_dir() + assert (expected / "src" / "my_cool_app" / "__init__.py").is_file() + + +# --------------------------------------------------------------------------- +# License tests +# --------------------------------------------------------------------------- + + +class TestLicense: + """Verify license parameter behavior.""" + + def test_mit_license(self, output_dir: Path) -> None: + """MIT license generates a LICENSE file with MIT text.""" + project = bake(output_dir, license="MIT") + license_file = project / "LICENSE" + assert license_file.is_file() + content = license_file.read_text() + assert "MIT License" in content + assert "John Smith" in content + + def test_apache_license(self, output_dir: Path) -> None: + """Apache-2.0 license generates a LICENSE file with Apache text.""" + project = bake(output_dir, license="Apache-2.0") + license_file = project / "LICENSE" + assert license_file.is_file() + content = license_file.read_text() + assert "Apache License" in content + + def test_proprietary_no_license_file(self, output_dir: Path) -> None: + """Proprietary license removes the LICENSE file.""" + project = bake(output_dir, license="Proprietary") + assert not (project / "LICENSE").exists() + + def test_license_in_pyproject(self, output_dir: Path) -> None: + """License value is set in pyproject.toml.""" + project = bake(output_dir, license="MIT") + content = (project / "pyproject.toml").read_text() + assert 'license = "MIT"' in content + + +# --------------------------------------------------------------------------- +# github_org tests +# --------------------------------------------------------------------------- + + +class TestGithubOrg: + """Verify github_org parameter behavior.""" + + def test_github_org_in_repository_url(self, output_dir: Path) -> None: + """github_org is used in the repository URL.""" + project = bake(output_dir, github_org="MyOrg") + content = (project / "pyproject.toml").read_text() + assert "github.com/MyOrg/test-project" in content + + +# --------------------------------------------------------------------------- +# pytest-bdd tests +# --------------------------------------------------------------------------- + + +class TestPytestBdd: + """Verify pytest-bdd optional behavior.""" + + def test_bdd_off_no_features_dir(self, output_dir: Path) -> None: + """When pytest-bdd is off, tests/features/ does not exist.""" + project = bake(output_dir, with_pytest_bdd="0") + assert not (project / "tests" / "features").exists() + + def test_bdd_off_no_dep(self, output_dir: Path) -> None: + """When pytest-bdd is off, pytest-bdd is not in dependencies.""" + project = bake(output_dir, with_pytest_bdd="0") + content = (project / "pyproject.toml").read_text() + assert "pytest-bdd" not in content + + def test_bdd_off_plain_tests(self, output_dir: Path) -> None: + """When pytest-bdd is off, test files use plain pytest.""" + project = bake(output_dir, with_pytest_bdd="0") + test_import = (project / "tests" / "test_import.py").read_text() + assert "def test_import" in test_import + assert "pytest_bdd" not in test_import + + def test_bdd_on_features_dir(self, output_dir: Path) -> None: + """When pytest-bdd is on, tests/features/ exists.""" + project = bake(output_dir, with_pytest_bdd="1") + assert (project / "tests" / "features").is_dir() + assert (project / "tests" / "features" / "import.feature").is_file() + + def test_bdd_on_dep_present(self, output_dir: Path) -> None: + """When pytest-bdd is on, pytest-bdd is in dependencies.""" + project = bake(output_dir, with_pytest_bdd="1") + content = (project / "pyproject.toml").read_text() + assert "pytest-bdd" in content + + def test_bdd_on_bdd_tests(self, output_dir: Path) -> None: + """When pytest-bdd is on, test files use BDD style.""" + project = bake(output_dir, with_pytest_bdd="1") + test_import = (project / "tests" / "test_import.py").read_text() + assert "from pytest_bdd import" in test_import + assert "scenarios(" in test_import + + +# --------------------------------------------------------------------------- +# FastAPI / Typer toggle tests +# --------------------------------------------------------------------------- + + +class TestFastapiToggle: + """Verify with_fastapi_api parameter behavior.""" + + def test_fastapi_on(self, output_dir: Path) -> None: + """FastAPI files are present when enabled.""" + project = bake(output_dir, with_fastapi_api="1") + assert (project / "src" / "test_project" / "api.py").is_file() + assert (project / "src" / "test_project" / "models.py").is_file() + assert (project / "src" / "test_project" / "services.py").is_file() + assert (project / "tests" / "test_api.py").is_file() + + def test_fastapi_off(self, output_dir: Path) -> None: + """FastAPI files are absent when disabled.""" + project = bake(output_dir, with_fastapi_api="0") + assert not (project / "src" / "test_project" / "api.py").exists() + assert not (project / "src" / "test_project" / "models.py").exists() + assert not (project / "tests" / "test_api.py").exists() + + def test_fastapi_deps(self, output_dir: Path) -> None: + """FastAPI dependency is in pyproject.toml when enabled.""" + project = bake(output_dir, with_fastapi_api="1") + content = (project / "pyproject.toml").read_text() + assert "fastapi" in content + assert "uvicorn" in content + assert "gunicorn" in content + + +class TestTyperToggle: + """Verify with_typer_cli parameter behavior.""" + + def test_typer_on(self, output_dir: Path) -> None: + """Typer CLI files are present when enabled.""" + project = bake(output_dir, with_typer_cli="1") + assert (project / "src" / "test_project" / "cli.py").is_file() + assert (project / "tests" / "test_cli.py").is_file() + + def test_typer_off(self, output_dir: Path) -> None: + """Typer CLI files are absent when disabled.""" + project = bake(output_dir, with_typer_cli="0") + assert not (project / "src" / "test_project" / "cli.py").exists() + assert not (project / "tests" / "test_cli.py").exists() + + +# --------------------------------------------------------------------------- +# Sentry tests +# --------------------------------------------------------------------------- + + +class TestSentry: + """Verify with_sentry parameter behavior.""" + + def test_sentry_on(self, output_dir: Path) -> None: + """Sentry dependency is present when enabled.""" + project = bake(output_dir, with_sentry="1", with_fastapi_api="1") + content = (project / "pyproject.toml").read_text() + assert "sentry-sdk" in content + + def test_sentry_off(self, output_dir: Path) -> None: + """Sentry dependency is absent when disabled.""" + project = bake(output_dir, with_sentry="0") + content = (project / "pyproject.toml").read_text() + assert "sentry-sdk" not in content + + +# --------------------------------------------------------------------------- +# Development environment tests +# --------------------------------------------------------------------------- + + +class TestDevelopmentEnvironment: + """Verify strict vs simple mode.""" + + def test_strict_has_dependabot(self, output_dir: Path) -> None: + """Strict mode includes dependabot.yml.""" + project = bake(output_dir, development_environment="strict") + assert (project / ".github" / "dependabot.yml").is_file() + + def test_simple_no_dependabot(self, output_dir: Path) -> None: + """Simple mode removes dependabot.yml.""" + project = bake(output_dir, development_environment="simple") + assert not (project / ".github" / "dependabot.yml").exists() + + def test_strict_has_safety(self, output_dir: Path) -> None: + """Strict mode includes safety in dependencies.""" + project = bake(output_dir, development_environment="strict") + content = (project / "pyproject.toml").read_text() + assert "safety" in content + + def test_simple_no_safety(self, output_dir: Path) -> None: + """Simple mode does not include safety.""" + project = bake(output_dir, development_environment="simple") + content = (project / "pyproject.toml").read_text() + assert "safety" not in content + + +# --------------------------------------------------------------------------- +# Dockerfile tests +# --------------------------------------------------------------------------- + + +class TestDockerfile: + """Verify Dockerfile structure.""" + + def test_three_stages(self, output_dir: Path) -> None: + """Dockerfile has exactly 3 stages: base, dev, app.""" + project = bake(output_dir) + content = (project / "Dockerfile").read_text() + from_lines = [l.strip() for l in content.splitlines() if l.startswith("FROM")] + assert len(from_lines) == 3 + assert "AS base" in from_lines[0] + assert "AS dev" in from_lines[1] + assert "AS app" in from_lines[2] + + def test_poetry_version(self, output_dir: Path) -> None: + """Dockerfile uses Poetry 2.3.x.""" + project = bake(output_dir) + content = (project / "Dockerfile").read_text() + assert "POETRY_VERSION=2.3." in content + + def test_healthcheck_with_fastapi(self, output_dir: Path) -> None: + """Dockerfile has HEALTHCHECK when FastAPI is enabled.""" + project = bake(output_dir, with_fastapi_api="1") + content = (project / "Dockerfile").read_text() + assert "HEALTHCHECK" in content + + def test_no_healthcheck_without_fastapi(self, output_dir: Path) -> None: + """Dockerfile has no HEALTHCHECK when FastAPI is disabled.""" + project = bake(output_dir, with_fastapi_api="0") + content = (project / "Dockerfile").read_text() + assert "HEALTHCHECK" not in content + + +# --------------------------------------------------------------------------- +# CI workflow tests +# --------------------------------------------------------------------------- + + +class TestCIWorkflow: + """Verify CI workflow structure.""" + + def test_workflow_valid_yaml(self, output_dir: Path) -> None: + """CI workflow is valid YAML.""" + import yaml + + project = bake(output_dir) + content = (project / ".github" / "workflows" / "test.yml").read_text() + parsed = yaml.safe_load(content) + assert parsed["name"] == "Test" + assert "test" in parsed["jobs"] + + def test_docker_cache_step(self, output_dir: Path) -> None: + """CI workflow has Docker layer caching.""" + project = bake(output_dir) + content = (project / ".github" / "workflows" / "test.yml").read_text() + assert "actions/cache@v4" in content + assert "setup-buildx-action" in content + + +# --------------------------------------------------------------------------- +# CLI tests +# --------------------------------------------------------------------------- + + +class TestCLI: + """Verify CLI stub content.""" + + def test_cli_has_info_command(self, output_dir: Path) -> None: + """CLI has info command.""" + project = bake(output_dir, with_typer_cli="1") + content = (project / "src" / "test_project" / "cli.py").read_text() + assert "def info(" in content + + def test_cli_has_config_command(self, output_dir: Path) -> None: + """CLI has config command.""" + project = bake(output_dir, with_typer_cli="1") + content = (project / "src" / "test_project" / "cli.py").read_text() + assert "def config(" in content + + def test_cli_has_health_with_fastapi(self, output_dir: Path) -> None: + """CLI has health command when FastAPI is enabled.""" + project = bake(output_dir, with_typer_cli="1", with_fastapi_api="1") + content = (project / "src" / "test_project" / "cli.py").read_text() + assert "def health(" in content + + def test_cli_no_health_without_fastapi(self, output_dir: Path) -> None: + """CLI has no health command when FastAPI is disabled.""" + project = bake(output_dir, with_typer_cli="1", with_fastapi_api="0") + content = (project / "src" / "test_project" / "cli.py").read_text() + assert "def health(" not in content + + def test_cli_no_greet_command(self, output_dir: Path) -> None: + """CLI does not have the old greet command.""" + project = bake(output_dir, with_typer_cli="1") + content = (project / "src" / "test_project" / "cli.py").read_text() + assert "def greet(" not in content + + +# --------------------------------------------------------------------------- +# ADR / docs tests +# --------------------------------------------------------------------------- + + +class TestDocs: + """Verify documentation files.""" + + def test_adr_template_exists(self, output_dir: Path) -> None: + """ADR template exists.""" + project = bake(output_dir) + assert (project / "docs" / "decisions" / "adr_template.md").is_file() + + def test_first_adr_exists(self, output_dir: Path) -> None: + """First ADR (0001) exists.""" + project = bake(output_dir) + adr = project / "docs" / "decisions" / "0001-record-architecture-decisions.md" + assert adr.is_file() + assert "Record architecture decisions" in adr.read_text() + + def test_readme_has_docs_section(self, output_dir: Path) -> None: + """Generated README has Documentation section.""" + project = bake(output_dir) + content = (project / "README.md").read_text() + assert "## Documentation" in content + assert "Architecture Decision Records" in content + + +# --------------------------------------------------------------------------- +# Poe tasks tests +# --------------------------------------------------------------------------- + + +class TestPoeTasks: + """Verify poe tasks in pyproject.toml.""" + + def test_poe_update_task(self, output_dir: Path) -> None: + """poe update task is present.""" + project = bake(output_dir) + content = (project / "pyproject.toml").read_text() + assert "[tool.poe.tasks.update]" in content + assert "cruft update" in content + + def test_poe_lint_task(self, output_dir: Path) -> None: + """poe lint task is present.""" + project = bake(output_dir) + content = (project / "pyproject.toml").read_text() + assert "[tool.poe.tasks.lint]" in content + + def test_poe_test_task(self, output_dir: Path) -> None: + """poe test task is present.""" + project = bake(output_dir) + content = (project / "pyproject.toml").read_text() + assert "[tool.poe.tasks.test]" in content + + +# --------------------------------------------------------------------------- +# Full combination matrix tests +# --------------------------------------------------------------------------- + + +class TestCombinations: + """Test specific combinations that are likely to cause issues.""" + + def test_minimal_no_api_no_cli_no_bdd(self, output_dir: Path) -> None: + """Minimal project: no FastAPI, no Typer, no BDD.""" + project = bake( + output_dir, + with_fastapi_api="0", + with_typer_cli="0", + with_pytest_bdd="0", + with_sentry="0", + development_environment="simple", + ) + assert project.is_dir() + assert (project / "pyproject.toml").is_file() + assert (project / "tests" / "test_import.py").is_file() + # No feature files, no API/CLI tests + assert not (project / "tests" / "features").exists() + assert not (project / "tests" / "test_api.py").exists() + assert not (project / "tests" / "test_cli.py").exists() + + def test_full_everything_enabled(self, output_dir: Path) -> None: + """Full project: all options enabled.""" + project = bake( + output_dir, + license="MIT", + with_fastapi_api="1", + with_typer_cli="1", + with_pytest_bdd="1", + with_sentry="1", + development_environment="strict", + ) + assert project.is_dir() + assert (project / "LICENSE").is_file() + assert (project / "tests" / "features").is_dir() + assert (project / "tests" / "test_api.py").is_file() + assert (project / "tests" / "test_cli.py").is_file() + content = (project / "pyproject.toml").read_text() + assert "pytest-bdd" in content + assert "sentry-sdk" in content + assert "commitizen" in content + + def test_bdd_on_but_no_fastapi(self, output_dir: Path) -> None: + """BDD on but no FastAPI: api.feature should not exist.""" + project = bake( + output_dir, + with_fastapi_api="0", + with_typer_cli="1", + with_pytest_bdd="1", + ) + assert (project / "tests" / "features" / "import.feature").is_file() + assert (project / "tests" / "features" / "cli.feature").is_file() + assert not (project / "tests" / "features" / "api.feature").exists() + + def test_bdd_on_but_no_typer(self, output_dir: Path) -> None: + """BDD on but no Typer: cli.feature should not exist.""" + project = bake( + output_dir, + with_fastapi_api="1", + with_typer_cli="0", + with_pytest_bdd="1", + ) + assert (project / "tests" / "features" / "import.feature").is_file() + assert (project / "tests" / "features" / "api.feature").is_file() + assert not (project / "tests" / "features" / "cli.feature").exists() + + def test_pyproject_toml_valid_toml(self, output_dir: Path) -> None: + """Generated pyproject.toml is valid TOML.""" + import tomllib + + project = bake(output_dir) + content = (project / "pyproject.toml").read_bytes() + parsed = tomllib.loads(content.decode()) + assert "tool" in parsed + assert "poetry" in parsed["tool"] + + +# --------------------------------------------------------------------------- +# Sprint 4: codespell tests +# --------------------------------------------------------------------------- + + +class TestCodespell: + """Verify codespell hook and configuration.""" + + def test_codespell_in_pre_commit(self, output_dir: Path) -> None: + """codespell hook is present in pre-commit config.""" + project = bake(output_dir) + content = (project / ".pre-commit-config.yaml").read_text() + assert "id: codespell" in content + assert "entry: codespell" in content + + def test_codespell_dep_in_pyproject(self, output_dir: Path) -> None: + """codespell dependency is in test dependencies.""" + project = bake(output_dir) + content = (project / "pyproject.toml").read_text() + assert 'codespell = ">=2.4.0"' in content + + def test_codespell_config_in_pyproject(self, output_dir: Path) -> None: + """codespell configuration section exists in pyproject.toml.""" + import tomllib + + project = bake(output_dir) + parsed = tomllib.loads((project / "pyproject.toml").read_bytes().decode()) + assert "codespell" in parsed["tool"] + assert parsed["tool"]["codespell"]["check-filenames"] is True + + +# --------------------------------------------------------------------------- +# Sprint 4: PR title check workflow tests +# --------------------------------------------------------------------------- + + +class TestPRWorkflow: + """Verify PR title conventional commit check workflow.""" + + def test_pr_yml_exists_with_conventional_commits(self, output_dir: Path) -> None: + """pr.yml exists when conventional commits is enabled.""" + project = bake(output_dir, development_environment="strict") + assert (project / ".github" / "workflows" / "pr.yml").is_file() + + def test_pr_yml_absent_without_conventional_commits(self, output_dir: Path) -> None: + """pr.yml does not exist when conventional commits is disabled.""" + project = bake( + output_dir, + development_environment="simple", + with_conventional_commits="0", + ) + assert not (project / ".github" / "workflows" / "pr.yml").exists() + + def test_pr_yml_has_commitizen(self, output_dir: Path) -> None: + """pr.yml uses commitizen to check PR title.""" + project = bake(output_dir, development_environment="strict") + content = (project / ".github" / "workflows" / "pr.yml").read_text() + assert "commitizen" in content + assert "cz check" in content + + def test_pr_yml_valid_yaml(self, output_dir: Path) -> None: + """pr.yml is valid YAML.""" + import yaml + + project = bake(output_dir, development_environment="strict") + content = (project / ".github" / "workflows" / "pr.yml").read_text() + parsed = yaml.safe_load(content) + assert parsed["name"] == "PR" + assert "title" in parsed["jobs"] + + +# --------------------------------------------------------------------------- +# Sprint 4: actions/checkout v6 tests +# --------------------------------------------------------------------------- + + +class TestCheckoutVersion: + """Verify actions/checkout version bump.""" + + def test_checkout_v6(self, output_dir: Path) -> None: + """test.yml uses actions/checkout@v6.""" + project = bake(output_dir) + content = (project / ".github" / "workflows" / "test.yml").read_text() + assert "actions/checkout@v6" in content + assert "actions/checkout@v5" not in content + + +# --------------------------------------------------------------------------- +# Sprint 4: MkDocs Material tests +# --------------------------------------------------------------------------- + + +class TestMkDocs: + """Verify MkDocs Material replaces pdoc.""" + + def test_mkdocs_yml_exists(self, output_dir: Path) -> None: + """mkdocs.yml is generated.""" + project = bake(output_dir) + assert (project / "mkdocs.yml").is_file() + + def test_mkdocs_yml_has_project_name(self, output_dir: Path) -> None: + """mkdocs.yml contains the project name.""" + project = bake(output_dir) + content = (project / "mkdocs.yml").read_text() + assert "test-project" in content + + def test_mkdocs_material_dep(self, output_dir: Path) -> None: + """mkdocs-material is in dev dependencies.""" + project = bake(output_dir) + content = (project / "pyproject.toml").read_text() + assert "mkdocs-material" in content + + def test_pdoc_absent(self, output_dir: Path) -> None: + """pdoc is not in dependencies.""" + project = bake(output_dir) + content = (project / "pyproject.toml").read_text() + assert "pdoc" not in content + + def test_poe_docs_uses_mkdocs(self, output_dir: Path) -> None: + """poe docs task uses mkdocs.""" + project = bake(output_dir) + content = (project / "pyproject.toml").read_text() + assert "mkdocs" in content + assert "[tool.poe.tasks.docs]" in content + assert "--serve" in content + + def test_docs_index_md_exists(self, output_dir: Path) -> None: + """docs/index.md is generated.""" + project = bake(output_dir) + assert (project / "docs" / "index.md").is_file() diff --git a/{{ cookiecutter.__project_name_kebab_case }}/.github/workflows/pr.yml b/{{ cookiecutter.__project_name_kebab_case }}/.github/workflows/pr.yml new file mode 100644 index 00000000..ae833c2e --- /dev/null +++ b/{{ cookiecutter.__project_name_kebab_case }}/.github/workflows/pr.yml @@ -0,0 +1,22 @@ +{%- if cookiecutter.with_conventional_commits|int %} +name: PR + +on: + pull_request: + types: [edited, opened, reopened, synchronize] + +jobs: + title: + runs-on: ubuntu-latest + name: Check PR title + steps: + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "{{ cookiecutter.python_version }}" + + - name: Check PR title + run: | + pip install commitizen + cz check --message "{% raw %}${{ github.event.pull_request.title }}{% endraw %}" +{%- endif %} diff --git a/{{ cookiecutter.__project_name_kebab_case }}/.github/workflows/test.yml b/{{ cookiecutter.__project_name_kebab_case }}/.github/workflows/test.yml index abde7519..d7e9ee36 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/.github/workflows/test.yml +++ b/{{ cookiecutter.__project_name_kebab_case }}/.github/workflows/test.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Set up Node.js uses: actions/setup-node@v4 @@ -31,10 +31,21 @@ jobs: - name: Install @devcontainers/cli run: npm install --location=global @devcontainers/cli@latest + - name: Cache Docker layers + uses: actions/cache@v4 + with: + path: /tmp/.buildx-cache + key: {% raw %}${{{% endraw %} runner.os }}-docker-{% raw %}${{{% endraw %} hashFiles('**/poetry.lock') }} + restore-keys: | + {% raw %}${{{% endraw %} runner.os }}-docker- + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Start Dev Container run: | git config --global init.defaultBranch main - PYTHON_VERSION={% raw %}${{{% endraw %} matrix.python-version }} devcontainer up --workspace-folder . + PYTHON_VERSION={% raw %}${{{% endraw %} matrix.python-version }} BUILDKIT_INLINE_CACHE=1 devcontainer up --workspace-folder . - name: Lint {{ cookiecutter.project_type }} run: devcontainer exec --workspace-folder . poe lint @@ -46,5 +57,5 @@ jobs: uses: actions/upload-artifact@v4 with: path: reports/htmlcov/ - name: coverage-report + name: coverage-report-{% raw %}${{{% endraw %} matrix.python-version }} retention-days: 7 diff --git a/{{ cookiecutter.__project_name_kebab_case }}/.pre-commit-config.yaml b/{{ cookiecutter.__project_name_kebab_case }}/.pre-commit-config.yaml index 6f51efd1..9789197d 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/.pre-commit-config.yaml +++ b/{{ cookiecutter.__project_name_kebab_case }}/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: - id: rst-inline-touching-normal - id: text-unicode-replacement-char - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: check-added-large-files args: ['--maxkb=50000'] @@ -100,3 +100,9 @@ repos: language: system types: [python] pass_filenames: false + - id: codespell + name: codespell + entry: codespell + require_serial: true + language: system + types_or: [markdown, python, pyi, toml, yaml] diff --git a/{{ cookiecutter.__project_name_kebab_case }}/CONTRIBUTING.md b/{{ cookiecutter.__project_name_kebab_case }}/CONTRIBUTING.md index d67c6f57..dcc7a60d 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/CONTRIBUTING.md +++ b/{{ cookiecutter.__project_name_kebab_case }}/CONTRIBUTING.md @@ -93,7 +93,7 @@ and then install the dependencies and the project with
Extra steps -After setting up the developpement environnement, you'll have to create an .env file at the root of the project, copy the contents of .env.example into this new file and fill all of the variables with the appropriate values. +After setting up the development environment, you'll have to create an .env file at the root of the project, copy the contents of .env.example into this new file and fill all of the variables with the appropriate values.
@@ -107,7 +107,7 @@ The following tools will be automatically installed by poetry to support develop - _pytest_: This project uses the `pytest` framework for unit testing. -- _ruff_: This project uses `ruff` to lint and automatically format code in order to maintain consistent code across all project and developpers. +- _ruff_: This project uses `ruff` to lint and automatically format code in order to maintain consistent code across all project and developers. - _mypy_: This project uses the static type checker `mypy` to enforce type annotation and spot bugs before they can happen. diff --git a/{{ cookiecutter.__project_name_kebab_case }}/Dockerfile b/{{ cookiecutter.__project_name_kebab_case }}/Dockerfile index 8053ca90..61ade24e 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/Dockerfile +++ b/{{ cookiecutter.__project_name_kebab_case }}/Dockerfile @@ -11,6 +11,20 @@ RUN rm /etc/apt/apt.conf.d/docker-clean ENV PYTHONFAULTHANDLER=1 ENV PYTHONUNBUFFERED=1 +# Install Poetry in a separate venv so it doesn't pollute the main venv. +ENV POETRY_VERSION=2.3.2 +ENV POETRY_VIRTUAL_ENV=/opt/poetry-env +RUN --mount=type=cache,target=/root/.cache/pip/ \ + python -m venv $POETRY_VIRTUAL_ENV && \ + $POETRY_VIRTUAL_ENV/bin/pip install poetry~=$POETRY_VERSION && \ + ln -s $POETRY_VIRTUAL_ENV/bin/poetry /usr/local/bin/poetry + +# Install compilers that may be required for certain packages or platforms. +RUN --mount=type=cache,target=/var/cache/apt/ \ + --mount=type=cache,target=/var/lib/apt/ \ + apt-get update && \ + apt-get install --no-install-recommends --yes build-essential + # Create a non-root user and switch to it [1]. # [1] https://code.visualstudio.com/remote/advancedcontainers/add-nonroot-user ARG UID=1000 @@ -28,28 +42,6 @@ RUN python -m venv $VIRTUAL_ENV # Set the working directory. WORKDIR /workspaces/{{ cookiecutter.__project_name_kebab_case }}/ - - -FROM base AS poetry - -USER root - -# Install Poetry in separate venv so it doesn't pollute the main venv. -ENV POETRY_VERSION=2.2.1 -ENV POETRY_VIRTUAL_ENV=/opt/poetry-env -RUN --mount=type=cache,target=/root/.cache/pip/ \ - python -m venv $POETRY_VIRTUAL_ENV && \ - $POETRY_VIRTUAL_ENV/bin/pip install poetry~=$POETRY_VERSION && \ - ln -s $POETRY_VIRTUAL_ENV/bin/poetry /usr/local/bin/poetry - -# Install compilers that may be required for certain packages or platforms. -RUN --mount=type=cache,target=/var/cache/apt/ \ - --mount=type=cache,target=/var/lib/apt/ \ - apt-get update && \ - apt-get install --no-install-recommends --yes build-essential - -USER user - # Install the run time Python dependencies in the virtual environment. COPY --chown=user:user poetry.lock* pyproject.toml /workspaces/{{ cookiecutter.__project_name_kebab_case }}/ RUN mkdir -p /home/user/.cache/pypoetry/ && mkdir -p /home/user/.config/pypoetry/ && \ @@ -59,7 +51,7 @@ RUN --mount=type=cache,uid=$UID,gid=$GID,target=/home/user/.cache/pypoetry/ \ -FROM poetry AS dev +FROM base AS dev # Install development tools: curl, git, gpg, ssh, starship, sudo, vim, and zsh. USER root @@ -100,10 +92,7 @@ RUN git clone --branch v$ANTIDOTE_VERSION --depth=1 https://github.com/mattmc3/a mkdir ~/.history/ && \ zsh -c 'source ~/.zshrc' -FROM poetry AS app - -# Copy the virtual environment from the poetry stage. -COPY --from=poetry $VIRTUAL_ENV $VIRTUAL_ENV +FROM base AS app # Copy the {{ cookiecutter.project_type }} source code to the working directory. COPY --chown=user:user ./src ./src diff --git a/{{ cookiecutter.__project_name_kebab_case }}/LICENSE b/{{ cookiecutter.__project_name_kebab_case }}/LICENSE new file mode 100644 index 00000000..6b358420 --- /dev/null +++ b/{{ cookiecutter.__project_name_kebab_case }}/LICENSE @@ -0,0 +1,46 @@ +{% if cookiecutter.license == "MIT" -%} +MIT License + +Copyright (c) {{ cookiecutter.author_name }} + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +{% elif cookiecutter.license == "Apache-2.0" -%} + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Copyright {{ cookiecutter.author_name }} +{% elif cookiecutter.license == "Proprietary" -%} +Copyright {{ cookiecutter.author_name }}. All rights reserved. + +This software is proprietary and confidential. Unauthorized copying, distribution, +or use of this software, via any medium, is strictly prohibited. +{% endif -%} diff --git a/{{ cookiecutter.__project_name_kebab_case }}/README.md b/{{ cookiecutter.__project_name_kebab_case }}/README.md index 3fdb0afd..ef94fb90 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/README.md +++ b/{{ cookiecutter.__project_name_kebab_case }}/README.md @@ -4,10 +4,16 @@ {{ cookiecutter.project_description }} +## Documentation + +- [Architecture Decision Records](docs/decisions/) +- [Confluence β€” Guide pratique](https://confluence.example.com/{{ cookiecutter.__project_name_kebab_case }}) + ## Table of contents -1. [Setup](#setup) -2. [Usage](#usage) +1. [Documentation](#documentation) +2. [Setup](#setup) +3. [Usage](#usage) ## Setup diff --git a/{{ cookiecutter.__project_name_kebab_case }}/docs/decisions/0001-record-architecture-decisions.md b/{{ cookiecutter.__project_name_kebab_case }}/docs/decisions/0001-record-architecture-decisions.md new file mode 100644 index 00000000..b81a3958 --- /dev/null +++ b/{{ cookiecutter.__project_name_kebab_case }}/docs/decisions/0001-record-architecture-decisions.md @@ -0,0 +1,23 @@ +--- +status: accepted +date: 2024-01-01 +--- +# 1. Record architecture decisions + +## Context and Problem Statement + +We need to record the architectural decisions made on this project. +When new team members join, they need to understand the reasoning behind past decisions. + +## Decision Outcome + +We will use Architecture Decision Records (ADRs), as described by Michael Nygard +in [Documenting Architecture Decisions](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions). + +ADRs are stored in `docs/decisions/` and follow the template in `adr_template.md`. + +### Consequences + +- Decisions are documented and easily discoverable. +- New team members can read the decision log to understand the project's history. +- Each ADR is small and focused on a single decision. diff --git a/{{ cookiecutter.__project_name_kebab_case }}/docs/index.md b/{{ cookiecutter.__project_name_kebab_case }}/docs/index.md new file mode 100644 index 00000000..907ffd34 --- /dev/null +++ b/{{ cookiecutter.__project_name_kebab_case }}/docs/index.md @@ -0,0 +1,11 @@ +# {{ cookiecutter.project_name }} + +{{ cookiecutter.project_description }} + +## Getting started + +See the [README](https://github.com/{{ cookiecutter.github_org }}/{{ cookiecutter.__project_name_kebab_case }}#readme) for installation and usage instructions. + +## Architecture decisions + +Architecture Decision Records (ADRs) are documented in [docs/decisions/](decisions/). diff --git a/{{ cookiecutter.__project_name_kebab_case }}/mkdocs.yml b/{{ cookiecutter.__project_name_kebab_case }}/mkdocs.yml new file mode 100644 index 00000000..c74504fa --- /dev/null +++ b/{{ cookiecutter.__project_name_kebab_case }}/mkdocs.yml @@ -0,0 +1,12 @@ +site_name: {{ cookiecutter.project_name }} +site_description: {{ cookiecutter.project_description }} +site_url: {{ cookiecutter.project_url }} + +theme: + name: material + +nav: + - Home: index.md + +plugins: + - search diff --git a/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml b/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml index 2f36b9c0..34723382 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml +++ b/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml @@ -9,6 +9,7 @@ description = "{{ cookiecutter.project_description }}" authors = ["{{ cookiecutter.author_name }} <{{ cookiecutter.author_email }}>"] readme = "README.md" repository = "{{ cookiecutter.project_url }}" +license = "{{ cookiecutter.license }}" {%- if cookiecutter.with_conventional_commits|int %} [tool.commitizen] # https://commitizen-tools.github.io/commitizen/config/ @@ -25,52 +26,55 @@ version_provider = "poetry" [tool.poetry.dependencies] # https://python-poetry.org/docs/dependency-specification/ {%- if cookiecutter.with_fastapi_api|int %} -fastapi = { extras = ["all"], version = ">=0.110.1" } -loguru = "^0.7.3" -gunicorn = ">=21.2.0" +fastapi = { extras = ["all"], version = ">=0.115.0" } +loguru = ">=0.7.3" +gunicorn = ">=23.0.0" {%- endif %} {%- if cookiecutter.project_type == "app" %} -poethepoet = ">=0.25.0" +poethepoet = ">=0.32.0" {%- endif %} -pydantic-settings = ">=2.2.0" +pydantic-settings = ">=2.7.0" python = ">={{ cookiecutter.python_version }},<4.0" {%- if cookiecutter.with_sentry|int %} -sentry-sdk = { extras = ["fastapi"], version = ">=1.40.0" } +sentry-sdk = { extras = ["fastapi"], version = ">=2.19.0" } {%- endif %} {%- if cookiecutter.with_typer_cli|int %} -typer = { extras = ["all"], version = ">=0.12.0" } +typer = { extras = ["all"], version = ">=0.15.0" } {%- endif %} {%- if cookiecutter.with_fastapi_api|int %} -uvicorn = { extras = ["standard"], version = ">=0.29.0" } +uvicorn = { extras = ["standard"], version = ">=0.34.0" } {%- endif %} [tool.poetry.group.test.dependencies] # https://python-poetry.org/docs/master/managing-dependencies/ {%- if cookiecutter.with_conventional_commits|int %} -commitizen = ">=3.21.3" +commitizen = ">=4.1.0" +{%- endif %} +coverage = { extras = ["toml"], version = ">=7.6.0" } +mypy = ">=1.14.0" +pre-commit = ">=4.0.0" +pytest = ">=8.3.0" +{%- if cookiecutter.with_pytest_bdd|int %} +pytest-bdd = ">=8.1.0" {%- endif %} -coverage = { extras = ["toml"], version = ">=7.4.4" } -mypy = ">=1.9.0" -pre-commit = ">=3.7.0" -pytest = ">=8.1.1" -pytest-bdd = ">=7.0.0" pytest-mock = ">=3.14.0" pytest-xdist = ">=3.5.0" {%- if cookiecutter.with_fastapi_api|int %} -pytest-asyncio = ">=0.23.0" +pytest-asyncio = ">=0.25.0" {%- endif %} -httpx = ">=0.27.0" -ruff = ">=0.5.7" +httpx = ">=0.28.0" +codespell = ">=2.4.0" +ruff = ">=0.11.0" {%- if cookiecutter.development_environment == "strict" %} -safety = ">=3.1.0" +safety = ">=3.2.0" shellcheck-py = ">=0.10.0.1" -typeguard = ">=4.2.1" +typeguard = ">=4.4.0" {%- endif %} [tool.poetry.group.dev.dependencies] # https://python-poetry.org/docs/master/managing-dependencies/ cruft = ">=2.15.0" ipykernel = ">=6.29.4" ipywidgets = ">=8.1.2" -pdoc = ">=14.4.0" +mkdocs-material = ">=9.5.0" [tool.coverage.report] # https://coverage.readthedocs.io/en/latest/config.html#report {%- if cookiecutter.development_environment == "strict" %} @@ -138,7 +142,9 @@ asyncio_default_fixture_loop_scope = "function" {%- endif %} testpaths = ["tests"] xfail_strict = true +{%- if cookiecutter.with_pytest_bdd|int %} bdd_features_base_dir = "tests/features/" +{%- endif %} [tool.ruff] # https://docs.astral.sh/ruff/ fix = true @@ -153,9 +159,9 @@ select = ["ALL"] ignore = [ "ANN002", # Missing type annotation for args. Complicates the code too much with the current annotation system. "ANN003", # Missing type annotation for kwargs. Complicates the code too much with the current annotation system. - "COM812", # Missing trailing comma in a single-line list. Already handled by ruff formater + "COM812", # Missing trailing comma in a single-line list. Already handled by ruff formatter "CPY001", # Copyright notice missing. Not always needed. - "E501", # Line too long. Already handled by ruff formater + "E501", # Line too long. Already handled by ruff formatter "E731", # Do not assign a lambda expression. Do not agree with this rule, it improves readability as it is self-documenting the lambda function. "RET504", # Unnecessary assign before return. This is not bad, it helps debugging. "S311", # Standard pseudo-random generators are not suitable for security/cryptographic purposes. Our use case is not security/cryptographic. @@ -194,6 +200,12 @@ convention = "{{ cookiecutter.__docstring_style|lower }}" max-args = 6 max-public-methods = 30 +[tool.codespell] # https://github.com/codespell-project/codespell +builtin = "en-GB_to_en-US,clear,code,rare" +check-filenames = true +ignore-words-list = "jupyter" +skip = "./*_cache/,./.venv,./reports" + [tool.poe.tasks] # https://github.com/nat-n/poethepoet {%- if cookiecutter.with_fastapi_api|int %} @@ -242,25 +254,21 @@ max-public-methods = 30 {%- endif %} [tool.poe.tasks.docs] - help = "Generate this {{ cookiecutter.project_type }}'s docs" - cmd = """ - pdoc - --docformat $docformat - --output-directory $outputdirectory - {{ cookiecutter.__project_name_snake_case }} + help = "Build or serve the documentation" + shell = """ + if [ $serve ] + then { + mkdocs serve + } else { + mkdocs build + } fi """ [[tool.poe.tasks.docs.args]] - help = "The docstring style (default: {{ cookiecutter.__docstring_style|lower }})" - name = "docformat" - options = ["--docformat"] - default = "{{ cookiecutter.__docstring_style|lower }}" - - [[tool.poe.tasks.docs.args]] - help = "The output directory (default: docs)" - name = "outputdirectory" - options = ["--output-directory"] - default = "docs" + help = "Serve the documentation locally with live reload" + type = "boolean" + name = "serve" + options = ["--serve"] [tool.poe.tasks.lint] help = "Lint this {{ cookiecutter.project_type }}" @@ -291,3 +299,7 @@ max-public-methods = 30 [[tool.poe.tasks.test.sequence]] cmd = "coverage xml" + + [tool.poe.tasks.update] + help = "Update the project from the cookiecutter template" + cmd = "cruft update --cookiecutter-input" diff --git a/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/cli.py b/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/cli.py index efc9df46..6130ea59 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/cli.py +++ b/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/cli.py @@ -1,12 +1,15 @@ """{{ cookiecutter.project_name }} CLI.""" +{% if cookiecutter.with_fastapi_api|int -%} +import urllib.request +{% endif -%} from typing import Annotated import typer from rich import print # noqa: A004 from rich.table import Table -from {{ cookiecutter.__project_name_snake_case }}.settings import settings +from {{ cookiecutter.__project_name_snake_case }}.settings import Settings, settings app = typer.Typer(help="{{ cookiecutter.project_name }} command-line interface.") @@ -40,10 +43,30 @@ def info() -> None: @app.command() -def greet( - name: Annotated[str, typer.Argument(help="Name to greet.")], -) -> None: - """Greet someone by name.""" +def config() -> None: + """Print current settings from environment and .env file.""" + table = Table(title="Settings") + table.add_column("Key", style="cyan") + table.add_column("Value", style="green") + for field_name in Settings.model_fields: + value = getattr(settings, field_name) + if _verbose or field_name != "sentry_dsn": + table.add_row(field_name, str(value)) + print(table) +{%- if cookiecutter.with_fastapi_api|int %} + + +@app.command() +def health() -> None: + """Check the API health endpoint.""" + url = f"http://{settings.api_host}:{settings.api_port}/health" if _verbose: - print(f"[dim]Greeting {name}...[/dim]") - print(f"[bold green]Hello, {name}![/bold green]") + print(f"[dim]Checking {url}...[/dim]") + try: + with urllib.request.urlopen(url, timeout=5) as response: + data = response.read().decode() + print(f"[bold green]API is healthy:[/bold green] {data}") + except Exception as exc: + print(f"[bold red]API is unreachable:[/bold red] {exc}") + raise typer.Exit(code=1) from exc +{%- endif %} diff --git a/{{ cookiecutter.__project_name_kebab_case }}/tests/features/cli.feature b/{{ cookiecutter.__project_name_kebab_case }}/tests/features/cli.feature index 9c0bcc64..5163e9dd 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/tests/features/cli.feature +++ b/{{ cookiecutter.__project_name_kebab_case }}/tests/features/cli.feature @@ -7,14 +7,20 @@ Feature: CLI Then the exit code should be 0 And the output should contain "{{ cookiecutter.project_name }}" - Scenario: Greet command outputs name + Scenario: Config command displays settings Given a CLI runner - When I run the greet command with name "Alice" + When I run the config command Then the exit code should be 0 - And the output should contain "Alice" + And the output should contain "app_name" +{%- if cookiecutter.with_fastapi_api|int %} + + Scenario: Health command executes + Given a CLI runner + When I run the health command + Then the exit code should be 0 +{%- endif %} Scenario: Verbose flag is accepted Given a CLI runner - When I run the greet command with verbose and name "Bob" + When I run the info command with verbose Then the exit code should be 0 - And the output should contain "Bob" diff --git a/{{ cookiecutter.__project_name_kebab_case }}/tests/test_api.py b/{{ cookiecutter.__project_name_kebab_case }}/tests/test_api.py index 47bf2700..4382cdfa 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/tests/test_api.py +++ b/{{ cookiecutter.__project_name_kebab_case }}/tests/test_api.py @@ -1,3 +1,4 @@ +{%- if cookiecutter.with_pytest_bdd|int -%} {%- raw %}"""BDD step definitions for API tests."""{% endraw %} from fastapi.testclient import TestClient @@ -44,3 +45,35 @@ def check_status_code(response: Response, code: int) -> None: def check_json_field(response: Response, key: str, value: str) -> None: """Verify a field in the response JSON.""" assert response.json()[key] == value +{%- else -%} +"""Tests for the REST API.""" + +from http import HTTPStatus + +from fastapi.testclient import TestClient + +from {{ cookiecutter.__project_name_snake_case }}.api import app + + +client = TestClient(app) + + +def test_health_endpoint() -> None: + """Health endpoint returns ok status.""" + response = client.get("/health") + assert response.status_code == HTTPStatus.OK + assert response.json()["status"] == "ok" + + +def test_create_item() -> None: + """Create an item via POST.""" + response = client.post("/items", json={"name": "Widget", "price": 9.99}) + assert response.status_code == HTTPStatus.CREATED + assert response.json()["name"] == "Widget" + + +def test_get_nonexistent_item() -> None: + """GET a non-existent item returns 404.""" + response = client.get("/items/999") + assert response.status_code == HTTPStatus.NOT_FOUND +{%- endif %} diff --git a/{{ cookiecutter.__project_name_kebab_case }}/tests/test_cli.py b/{{ cookiecutter.__project_name_kebab_case }}/tests/test_cli.py index 9f9c844b..ea6e0e70 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/tests/test_cli.py +++ b/{{ cookiecutter.__project_name_kebab_case }}/tests/test_cli.py @@ -1,3 +1,4 @@ +{%- if cookiecutter.with_pytest_bdd|int -%} """BDD step definitions for CLI tests.""" from pytest_bdd import given, parsers, scenarios, then, when @@ -21,22 +22,24 @@ def run_info(runner: CliRunner) -> Result: return runner.invoke(app, ["info"]) -@when( - parsers.cfparse('I run the greet command with name "{name}"'), - target_fixture="result", -) -def run_greet(runner: CliRunner, name: str) -> Result: - """Execute the greet command with the given name.""" - return runner.invoke(app, ["greet", name]) +@when("I run the config command", target_fixture="result") +def run_config(runner: CliRunner) -> Result: + """Execute the config command.""" + return runner.invoke(app, ["config"]) +{%- if cookiecutter.with_fastapi_api|int %} -@when( - parsers.cfparse('I run the greet command with verbose and name "{name}"'), - target_fixture="result", -) -def run_greet_verbose(runner: CliRunner, name: str) -> Result: - """Execute the greet command with verbose flag.""" - return runner.invoke(app, ["--verbose", "greet", name]) +@when("I run the health command", target_fixture="result") +def run_health(runner: CliRunner) -> Result: + """Execute the health command.""" + return runner.invoke(app, ["health"]) +{%- endif %} + + +@when("I run the info command with verbose", target_fixture="result") +def run_info_verbose(runner: CliRunner) -> Result: + """Execute the info command with verbose flag.""" + return runner.invoke(app, ["--verbose", "info"]) @then(parsers.cfparse("the exit code should be {code:d}")) @@ -49,3 +52,42 @@ def check_exit_code(result: Result, code: int) -> None: def check_output_contains(result: Result, text: str) -> None: """Verify the CLI output contains expected text.""" assert text in result.stdout +{%- else -%} +"""Tests for the CLI.""" + +from typer.testing import CliRunner + +from {{ cookiecutter.__project_name_snake_case }}.cli import app + + +runner = CliRunner() + + +def test_info_command() -> None: + """Info command displays project metadata.""" + result = runner.invoke(app, ["info"]) + assert result.exit_code == 0 + assert "{{ cookiecutter.project_name }}" in result.stdout + + +def test_config_command() -> None: + """Config command displays current settings.""" + result = runner.invoke(app, ["config"]) + assert result.exit_code == 0 + assert "app_name" in result.stdout +{%- if cookiecutter.with_fastapi_api|int %} + + +def test_health_command() -> None: + """Health command runs without error (no server to check).""" + result = runner.invoke(app, ["health"]) + # Exit code 1 is expected when no server is running. + assert result.exit_code in {0, 1} +{%- endif %} + + +def test_verbose_flag() -> None: + """Verbose flag is accepted.""" + result = runner.invoke(app, ["--verbose", "info"]) + assert result.exit_code == 0 +{%- endif %} diff --git a/{{ cookiecutter.__project_name_kebab_case }}/tests/test_import.py b/{{ cookiecutter.__project_name_kebab_case }}/tests/test_import.py index 8a27549a..b7ac90a1 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/tests/test_import.py +++ b/{{ cookiecutter.__project_name_kebab_case }}/tests/test_import.py @@ -1,3 +1,4 @@ +{%- if cookiecutter.with_pytest_bdd|int -%} """BDD step definitions for package import tests.""" from types import ModuleType @@ -20,3 +21,20 @@ def installed_package() -> ModuleType: def check_package_name(package: ModuleType) -> None: """Verify the package has a valid name.""" assert isinstance(package.__name__, str) +{%- else -%} +"""Tests for package import.""" + +import {{ cookiecutter.__project_name_snake_case }} +from {{ cookiecutter.__project_name_snake_case }}.settings import Settings, settings + + +def test_import() -> None: + """Verify the package can be imported and has a valid name.""" + assert isinstance({{ cookiecutter.__project_name_snake_case }}.__name__, str) + + +def test_settings_defaults() -> None: + """Verify settings can be instantiated with defaults.""" + assert isinstance(settings, Settings) + assert isinstance(settings.app_name, str) +{%- endif %} From 37cd145a0771979ecf036e70fa5b94c1c206d158 Mon Sep 17 00:00:00 2001 From: David Beauchemin Date: Sat, 21 Feb 2026 14:37:05 -0500 Subject: [PATCH 65/68] docs: improve README, CONTRIBUTING, and add CLAUDE.md template (#22) - Rewrite root README with CI badges, project structure, and developer guide - Conditionalize generated README sections (API, CLI, Docker) with Jinja - Fix CONTRIBUTING.md typos and conditionalize FastAPI section - Add CLAUDE.md template for AI-assisted development in generated projects - Add pull_request_template.md for the cookiecutter repo itself - Add 11 unit tests for CLAUDE.md and conditional README (71 total) - Update CHANGELOG.md with full sprint history Co-authored-by: davebulaval Co-authored-by: Claude Opus 4.6 --- .github/pull_request_template.md | 14 ++ CHANGELOG.md | 38 ++++ README.md | 165 +++++++++--------- tests/test_cookiecutter.py | 85 +++++++++ .../CLAUDE.md | 64 +++++++ .../CONTRIBUTING.md | 20 ++- .../README.md | 117 ++++++------- 7 files changed, 346 insertions(+), 157 deletions(-) create mode 100644 .github/pull_request_template.md create mode 100644 {{ cookiecutter.__project_name_kebab_case }}/CLAUDE.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..d884a3b4 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,14 @@ +## Summary + +- + +## Test plan + +- [ ] `pytest tests/ -v` passes locally +- [ ] YAML files are valid +- [ ] Generated project passes `ruff check` and `ruff format --check` + +## Checklist + +- [ ] I have performed a self-review of my changes +- [ ] PR title follows [Conventional Commits](https://www.conventionalcommits.org/) diff --git a/CHANGELOG.md b/CHANGELOG.md index c71f24c5..a50a49a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ### Added +- `CLAUDE.md` template for AI-assisted development in generated projects +- `pull_request_template.md` for the cookiecutter repo itself +- `CODEOWNERS` file (@davebulaval, @dpothier) +- Fast CI workflow (`ci.yml`) β€” unit tests on Python 3.12 + 3.13 (~20s) +- PR title check workflow (`pr.yml`) β€” conventional commits validation +- Integration test matrix β€” full (FastAPI + Typer) and minimal (bare) variants +- Cruft link verification step in integration tests +- codespell linter in pre-commit and pyproject.toml +- MkDocs Material for documentation (replaces pdoc) +- PR title check workflow for generated projects (`pr.yml`) +- `actionlint` pre-commit hook for GitHub Actions validation +- `ruff-check` and `ruff-format` pre-commit hooks for template code +- `pre-commit-hooks` (check-yaml, check-toml, end-of-file-fixer, trailing-whitespace) +- 71 unit tests for template generation (up from 0) + +### Changed + +- Conditionalized generated README sections (API, CLI, Docker) with Jinja +- Rewrote root README with CI badges, project structure, and developer guide +- Rewrote generated README: concise, dynamic, references MkDocs +- Modernized integration workflow: checkout v6, pip cache, renamed to "Integration" +- Fixed generated CONTRIBUTING.md typos and added codespell to tools list +- Fixed `.env.sample` reference to `.env.example` in generated README + +### Fixed + +- ruff lint errors in generated code (FURB171, PLC0415, PLR2004, PLR6201, B007, PERF102) +- codespell false positives (Jupyter) and real typos (developpement, developpers, formater) +- Jinja whitespace in cli.py imports causing ruff format failure +- Unused `import sys` and stale noqa comments (S310, BLE001) in cli.py +- Coverage failure in minimal config (added settings test) + +--- + +## Sprint 2 β€” Cookiecutter Enhancements + +### Added + - pydantic-settings integration (replaces python-decouple) with `.env.example` - Pydantic models (`models.py`) and service layer (`services.py`) stubs - Rich API stubs: health endpoint, CRUD items, exception handlers, request logging middleware diff --git a/README.md b/README.md index 4d485f9f..4f41e3af 100644 --- a/README.md +++ b/README.md @@ -1,144 +1,135 @@ -[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/Baseline-quebec/baseline-app-cookiecutter) [![Open in GitHub Codespaces](https://img.shields.io/static/v1?label=GitHub%20Codespaces&message=Open&color=blue&logo=github)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=Baseline-quebec/baseline-app-cookiecutter) +[![CI](https://github.com/Baseline-quebec/baseline-app-cookiecutter/actions/workflows/ci.yml/badge.svg)](https://github.com/Baseline-quebec/baseline-app-cookiecutter/actions/workflows/ci.yml) [![Integration](https://github.com/Baseline-quebec/baseline-app-cookiecutter/actions/workflows/test.yml/badge.svg)](https://github.com/Baseline-quebec/baseline-app-cookiecutter/actions/workflows/test.yml) [![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/Baseline-quebec/baseline-app-cookiecutter) [![Open in GitHub Codespaces](https://img.shields.io/static/v1?label=GitHub%20Codespaces&message=Open&color=blue&logo=github)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=Baseline-quebec/baseline-app-cookiecutter) -# Baseline app Cookiecutter - -A modern [Cookiecutter](https://github.com/cookiecutter/cookiecutter) template for scaffolding Python packages and apps. +# Baseline App Cookiecutter +A modern [Cookiecutter](https://github.com/cookiecutter/cookiecutter) template for scaffolding Python packages and apps at [Baseline](https://github.com/Baseline-quebec). ## Features - Quick and reproducible development environments with VS Code's [Dev Containers](https://code.visualstudio.com/docs/devcontainers/containers), PyCharm's [Docker Compose interpreter](https://www.jetbrains.com/help/pycharm/using-docker-compose-as-a-remote-interpreter.html#docker-compose-remote), and [GitHub Codespaces](https://github.com/features/codespaces) - Cross-platform support for Linux, macOS (Apple silicon and Intel), and Windows -- Modern shell prompt with [Starship](https://github.com/starship/starship) - Packaging and dependency management with [Poetry](https://github.com/python-poetry/poetry) - Task running with [Poe the Poet](https://github.com/nat-n/poethepoet) -- Code formatting with [Ruff](https://github.com/charliermarsh/ruff) -- Code linting with [Pre-commit](https://pre-commit.com/), [Mypy](https://github.com/python/mypy), and [Ruff](https://github.com/charliermarsh/ruff) -- Optionally follows the [Conventional Commits](https://www.conventionalcommits.org/) standard to automate [Semantic Versioning](https://semver.org/) and [Keep A Changelog](https://keepachangelog.com/) with [Commitizen](https://github.com/commitizen-tools/commitizen) +- Code formatting and linting with [Ruff](https://github.com/charliermarsh/ruff), [Mypy](https://github.com/python/mypy), and [Pre-commit](https://pre-commit.com/) +- Spell checking with [codespell](https://github.com/codespell-project/codespell) +- Documentation with [MkDocs Material](https://squidfunk.github.io/mkdocs-material/) +- Optional [Conventional Commits](https://www.conventionalcommits.org/) with [Commitizen](https://github.com/commitizen-tools/commitizen) +- Optional [FastAPI](https://github.com/tiangolo/fastapi) REST API with health check, CRUD stubs, and Sentry integration +- Optional [Typer](https://github.com/tiangolo/typer) CLI with Rich output +- Optional [pytest-bdd](https://github.com/pytest-dev/pytest-bdd) for BDD-style tests with Gherkin feature files - Continuous integration with [GitHub Actions](https://docs.github.com/en/actions) - Test coverage with [Coverage.py](https://github.com/nedbat/coveragepy) -- Scaffolding updates with [Cookiecutter](https://github.com/cookiecutter/cookiecutter) and [Cruft](https://github.com/cruft/cruft) (with `poe update` task) +- Scaffolding updates with [Cruft](https://github.com/cruft/cruft) - Dependency updates with [Dependabot](https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/about-dependabot-version-updates) -- [Architecture Decision Records](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions) (ADR) template in `docs/decisions/` -- Confluence documentation link placeholder in generated README +- [Architecture Decision Records](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions) (ADR) template +- Claude Code instructions (`CLAUDE.md`) for AI-assisted development ## Using ### Creating a new Python project -To create a new Python project with this template: - -1. Install the latest [Cruft](https://github.com/cruft/cruft) and [Cookiecutter](https://github.com/cookiecutter/cookiecutter) in your [Python environment](https://github.com/pyenv/pyenv-virtualenv) with: +1. Install [Cruft](https://github.com/cruft/cruft) and [Cookiecutter](https://github.com/cookiecutter/cookiecutter): ```sh pip install --upgrade "cruft>=2.12.0" "cookiecutter>=2.1.1" ``` +2. [Create a new repository](https://github.com/new) and clone it locally. -2. [Create a new repository](https://github.com/new) for your Python project, then clone it locally. -3. Run the following command in the parent directory of the cloned repository to apply the Poetry Cookiecutter template: +3. Run the following command in the **parent directory** of the cloned repository: ```sh cruft create -f https://github.com/Baseline-quebec/baseline-app-cookiecutter ```
+ If your repository name differs from the project's slugified name - If your repository name β‰  the project's slugified name - - If your repository name differs from your project's slugified name (see `project_name` in the [Template parameters](https://github.com/Baseline-quebec/baseline-app-cookiecutter#template-parameters) below), you will need to copy the scaffolded project into the repository with: - - ```sh - cp -r {project-name}/ {repository-name}/ - ``` - -
- -4. Add the remote origin to your local package. - -### Updating your Python project - -To update your Python project to the latest template version: - -1. Update the project while verifying the existing template parameters and setting any new parameters, if there are any: + Copy the scaffolded project into the repository: ```sh - cruft update --cookiecutter-input + cp -r {project-name}/ {repository-name}/ ``` -2. If any of the file updates failed, resolve them by inspecting the corresponding `.rej` files. - -## How-to - -### Run the development server (FastAPI) +
-```sh -poetry run poe api -``` +4. Add the remote origin and push. -### Run the CLI +### Updating an existing project ```sh -poetry run my-app info -poetry run my-app config -poetry run my-app health # checks API health endpoint +cruft update --cookiecutter-input ``` -### Run tests +If any file updates failed, resolve conflicts by inspecting the `.rej` files. -```sh -poetry run poe test -``` +## Developing this template -### Run linting +### Quick reference -```sh -poetry run poe lint -``` +| Command | Description | +|---------|-------------| +| `pip install cookiecutter pytest pyyaml` | Install test dependencies | +| `pytest tests/ -v` | Run unit tests (~60 tests, ~7s) | +| `pre-commit run --all-files` | Run linting on template code | -### Add a new dependency +### CI/CD -```sh -poetry add # runtime dependency -poetry add --group dev # development dependency -``` +This repository has three CI workflows: -### Configure environment variables +| Workflow | Trigger | What it does | +|----------|---------|-------------| +| **CI** (`ci.yml`) | Push / PR | Runs unit tests on Python 3.12 + 3.13 (~20s) | +| **PR** (`pr.yml`) | PR | Validates PR title follows conventional commits | +| **Integration** (`test.yml`) | Push / PR | Scaffolds a project, starts a devcontainer, runs `poe lint` + `poe test` (~3 min) | -Copy the example file and fill in the values: +### Project structure -```sh -cp .env.example .env ``` - -### Build and run with Docker - -```sh -docker compose up --build +baseline-app-cookiecutter/ +β”œβ”€β”€ cookiecutter.json # Template parameters +β”œβ”€β”€ hooks/ +β”‚ β”œβ”€β”€ pre_gen_project.py # Input validation +β”‚ └── post_gen_project.py # Conditional file removal +β”œβ”€β”€ tests/ +β”‚ └── test_cookiecutter.py # Unit tests for the template +β”œβ”€β”€ {{ cookiecutter.__project_name_kebab_case }}/ +β”‚ β”œβ”€β”€ .devcontainer/ # Dev Container config +β”‚ β”œβ”€β”€ .github/workflows/ # CI for generated projects +β”‚ β”œβ”€β”€ src/{{ ... }}/ # Source code stubs +β”‚ β”œβ”€β”€ tests/ # Test stubs +β”‚ β”œβ”€β”€ pyproject.toml # Poetry config +β”‚ └── ... +β”œβ”€β”€ .github/ +β”‚ β”œβ”€β”€ workflows/ci.yml # Unit tests +β”‚ β”œβ”€β”€ workflows/pr.yml # PR title check +β”‚ β”œβ”€β”€ workflows/test.yml # Integration tests +β”‚ β”œβ”€β”€ dependabot.yml # Dependency updates +β”‚ └── CODEOWNERS # Code owners +└── .pre-commit-config.yaml # Linting for template code ``` ## Upstream sync -This template is a fork of [superlinear-ai/substrate](https://github.com/superlinear-ai/substrate). The upstream has since migrated to [uv](https://github.com/astral-sh/uv) (replacing Poetry), [Copier](https://copier.readthedocs.io/) (replacing Cookiecutter), and [ty](https://github.com/astral-sh/ty) (replacing Mypy). These are major structural changes that would require reworking the entire Baseline toolchain and CI/CD pipelines. +This template is a fork of [superlinear-ai/substrate](https://github.com/superlinear-ai/substrate). The upstream has since migrated to [uv](https://github.com/astral-sh/uv) (replacing Poetry), [Copier](https://copier.readthedocs.io/) (replacing Cookiecutter), and [ty](https://github.com/astral-sh/ty) (replacing Mypy). These are major structural changes that would require reworking the entire Baseline toolchain. -We intentionally stay on **Poetry + Cookiecutter + Mypy** to maintain compatibility with existing Baseline projects and internal workflows. Instead of a full upstream merge, we cherry-pick individual improvements that are independent of the build system migration (e.g., new pre-commit hooks, GitHub Actions version bumps, documentation tooling). +We intentionally stay on **Poetry + Cookiecutter + Mypy** to maintain compatibility with existing Baseline projects. Instead of a full upstream merge, we cherry-pick individual improvements that are independent of the build system migration. ## Template parameters - -| Parameter | Description | -| ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `project_name`
"Spline Reticulator" | The name of the project. Will be slugified to `snake_case` for importing and `kebab-case` for installing. For example, `My Package` will be `my_package` for importing and `my-package` for installing. | -| `project_description`
"A Python package that reticulates splines." | A single-line description of the project. | -| `github_org`
"Baseline-quebec" | The GitHub organization or user that owns the repository. Used to construct the project URL and other references. | -| `project_url`
"" | The URL to the project's repository. Automatically constructed from `github_org` and `project_name`. | -| `author_name`
"John Smith" | The full name of the primary author of the project. | -| `author_email`
"" | The email address of the primary author of the project. | -| `license`
["Proprietary", "MIT", "Apache-2.0"] | The license for the project. Generates a LICENSE file for MIT and Apache-2.0. Proprietary projects get no LICENSE file. | -| `python_version`
"3.12" | The minimum Python version that the project requires. | -| `development_environment`
["simple", "strict"] | Whether to configure the development environment with a focus on simplicity or with a focus on strictness. In strict mode, additional [Ruff rules](https://docs.astral.sh/ruff/rules/) are added, and tools such as [Mypy](https://github.com/python/mypy) and [Pytest](https://github.com/pytest-dev/pytest) are set to strict mode. | -| `with_fastapi_api`
["0", "1"] | If "1", [FastAPI](https://github.com/tiangolo/fastapi) is added as a run time dependency, FastAPI API stubs and tests are added, a `poe api` command for serving the API is added. | -| `with_conventional_commits`
["0", "1"] | If "1", [Commitizen](https://github.com/commitizen-tools/commitizen) is added for conventional commits and semantic versioning. Automatically set to "1" in strict mode. | -| `with_typer_cli`
["0", "1"] | If "1", [Typer](https://github.com/tiangolo/typer) is added as a run time dependency, Typer CLI stubs and tests are added, the package itself is registered as a CLI. | -| `with_sentry`
["0", "1"] | If "1", [Sentry SDK](https://docs.sentry.io/platforms/python/) is added with FastAPI integration. Requires `with_fastapi_api=1`. Adds `sentry_dsn`, `sentry_environment`, and `sentry_traces_sample_rate` settings. | -| `with_pytest_bdd`
["0", "1"] | If "1", [pytest-bdd](https://github.com/pytest-dev/pytest-bdd) is added with BDD-style tests using Gherkin feature files. Default is "0" (plain pytest). | +| Parameter | Description | +|-----------|-------------| +| `project_name`
"my-app" | The name of the project. Slugified to `snake_case` for importing and `kebab-case` for installing. | +| `project_description`
"A Python app that..." | A single-line description of the project. | +| `github_org`
"Baseline-quebec" | The GitHub organization or user that owns the repository. | +| `project_url`
auto | Automatically constructed from `github_org` and `project_name`. | +| `author_name`
"John Smith" | The full name of the primary author. | +| `author_email`
"john@example.com" | The email address of the primary author. | +| `license`
["Proprietary", "MIT", "Apache-2.0"] | The license. Generates a LICENSE file for MIT and Apache-2.0. | +| `python_version`
"3.12" | The minimum Python version. | +| `development_environment`
["strict", "simple"] | Strict mode enables additional Ruff rules, strict Mypy, and strict Pytest. | +| `with_conventional_commits`
["0", "1"] | Adds Commitizen for conventional commits. Auto-enabled in strict mode. | +| `with_fastapi_api`
["0", "1"] | Adds FastAPI with health endpoint, CRUD stubs, Pydantic models, and `poe api`. | +| `with_typer_cli`
["0", "1"] | Adds Typer CLI with `info`, `config`, and `health` commands. | +| `with_pytest_bdd`
["0", "1"] | Adds pytest-bdd with Gherkin feature files. Default: plain pytest. | +| `with_sentry`
["0", "1"] | Adds Sentry SDK with FastAPI integration. Requires `with_fastapi_api=1`. | diff --git a/tests/test_cookiecutter.py b/tests/test_cookiecutter.py index 1c4d9980..d44ee8f4 100644 --- a/tests/test_cookiecutter.py +++ b/tests/test_cookiecutter.py @@ -635,3 +635,88 @@ def test_docs_index_md_exists(self, output_dir: Path) -> None: """docs/index.md is generated.""" project = bake(output_dir) assert (project / "docs" / "index.md").is_file() + + +# --------------------------------------------------------------------------- +# CLAUDE.md tests +# --------------------------------------------------------------------------- + + +class TestClaudeMd: + """Verify CLAUDE.md is generated with correct content.""" + + def test_claude_md_exists(self, output_dir: Path) -> None: + """CLAUDE.md is generated.""" + project = bake(output_dir) + assert (project / "CLAUDE.md").is_file() + + def test_claude_md_has_project_name(self, output_dir: Path) -> None: + """CLAUDE.md contains the project name.""" + project = bake(output_dir) + content = (project / "CLAUDE.md").read_text() + assert "test-project" in content + + def test_claude_md_has_snake_case_import(self, output_dir: Path) -> None: + """CLAUDE.md references the correct import path.""" + project = bake(output_dir) + content = (project / "CLAUDE.md").read_text() + assert "test_project" in content + + def test_claude_md_has_fastapi_when_enabled(self, output_dir: Path) -> None: + """CLAUDE.md mentions FastAPI when enabled.""" + project = bake(output_dir, with_fastapi_api="1") + content = (project / "CLAUDE.md").read_text() + assert "FastAPI" in content + assert "poe api" in content + + def test_claude_md_no_fastapi_when_disabled(self, output_dir: Path) -> None: + """CLAUDE.md does not mention poe api when FastAPI is disabled.""" + project = bake(output_dir, with_fastapi_api="0") + content = (project / "CLAUDE.md").read_text() + assert "poe api" not in content + + +# --------------------------------------------------------------------------- +# README conditionalization tests +# --------------------------------------------------------------------------- + + +class TestReadmeConditional: + """Verify README sections are conditionalized correctly.""" + + def test_readme_has_api_section_with_fastapi(self, output_dir: Path) -> None: + """README has API section when FastAPI is enabled.""" + project = bake(output_dir, with_fastapi_api="1") + content = (project / "README.md").read_text() + assert "poe api" in content + + def test_readme_no_api_section_without_fastapi(self, output_dir: Path) -> None: + """README has no API section when FastAPI is disabled.""" + project = bake(output_dir, with_fastapi_api="0") + content = (project / "README.md").read_text() + assert "poe api" not in content + + def test_readme_has_cli_section_with_typer(self, output_dir: Path) -> None: + """README has CLI section when Typer is enabled.""" + project = bake(output_dir, with_typer_cli="1") + content = (project / "README.md").read_text() + assert "### CLI" in content + + def test_readme_no_cli_section_without_typer(self, output_dir: Path) -> None: + """README has no CLI section when Typer is disabled.""" + project = bake(output_dir, with_typer_cli="0") + content = (project / "README.md").read_text() + assert "### CLI" not in content + + def test_readme_has_env_example(self, output_dir: Path) -> None: + """README references .env.example (not .env.sample).""" + project = bake(output_dir) + content = (project / "README.md").read_text() + assert ".env.example" in content + assert ".env.sample" not in content + + def test_readme_has_mkdocs(self, output_dir: Path) -> None: + """README references MkDocs.""" + project = bake(output_dir) + content = (project / "README.md").read_text() + assert "poe docs" in content diff --git a/{{ cookiecutter.__project_name_kebab_case }}/CLAUDE.md b/{{ cookiecutter.__project_name_kebab_case }}/CLAUDE.md new file mode 100644 index 00000000..115a50e9 --- /dev/null +++ b/{{ cookiecutter.__project_name_kebab_case }}/CLAUDE.md @@ -0,0 +1,64 @@ +# {{ cookiecutter.project_name }} + +{{ cookiecutter.project_description }} + +## Conventions + +This project follows the [Baseline development conventions](https://github.com/Baseline-quebec/agents/blob/main/programmation/knowledge/conventions-baseline.md). + +### Key rules + +- **Language**: Code, commits, branches, and PRs in English. User-facing docs in French. +- **Naming**: `snake_case` (code), `kebab-case` (repos), `PascalCase` (classes), `UPPER_SNAKE_CASE` (constants) +- **Imports**: Absolute only (`from {{ cookiecutter.__project_name_snake_case }}.module import X`) +- **Type hints**: Required on all function signatures +- **Docstrings**: Google convention +- **Line length**: 99 characters max +- **Config**: Everything in `pyproject.toml` (no separate `.mypy.ini`, `.pylintrc`, etc.) + +## Project layout + +``` +src/{{ cookiecutter.__project_name_snake_case }}/ # source code (src layout) +tests/ # test suite +docs/ # MkDocs documentation + ADRs +pyproject.toml # Poetry config, tool settings +``` + +## Commands + +Always use `poetry run` β€” never `poetry shell`. + +```bash +poetry install # install dependencies +poetry run poe lint # ruff + mypy + pre-commit +poetry run poe test # pytest with coverage +poetry run poe docs --serve # serve MkDocs locally +{%- if cookiecutter.with_fastapi_api|int %} +poetry run poe api --dev # start FastAPI dev server +{%- endif %} +``` + +## Git workflow + +- **Branches**: `feat/`, `fix/`, `refactor/`, `docs/` + kebab-case description +- **Commits**: [Conventional Commits](https://www.conventionalcommits.org/) β€” `feat(scope): description` +- **PRs**: Squash merge only. PR title = final commit message on `main`. +- Never commit directly to `main`. + +## Tech stack + +- **Package manager**: [Poetry](https://python-poetry.org/) +- **Task runner**: [Poe the Poet](https://github.com/nat-n/poethepoet) +- **Linting**: [Ruff](https://docs.astral.sh/ruff/) (linting + formatting) +- **Type checking**: [Mypy](https://mypy.readthedocs.io/) (strict mode) +- **Testing**: [pytest](https://docs.pytest.org/){% if cookiecutter.with_pytest_bdd|int %} + [pytest-bdd](https://pytest-bdd.readthedocs.io/){% endif %} + +- **CI**: GitHub Actions in devcontainer +{%- if cookiecutter.with_fastapi_api|int %} +- **API**: [FastAPI](https://fastapi.tiangolo.com/) + [Pydantic](https://docs.pydantic.dev/) +{%- endif %} +{%- if cookiecutter.with_typer_cli|int %} +- **CLI**: [Typer](https://typer.tiangolo.com/) + [Rich](https://rich.readthedocs.io/) +{%- endif %} +- **Config**: [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) with `.env` file diff --git a/{{ cookiecutter.__project_name_kebab_case }}/CONTRIBUTING.md b/{{ cookiecutter.__project_name_kebab_case }}/CONTRIBUTING.md index dcc7a60d..253c9cef 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/CONTRIBUTING.md +++ b/{{ cookiecutter.__project_name_kebab_case }}/CONTRIBUTING.md @@ -1,7 +1,7 @@ # Contributing to {{ cookiecutter.project_name }} project ## Using - +{% if cookiecutter.with_fastapi_api|int -%} To serve this app, run: ```sh @@ -15,6 +15,14 @@ Within the Dev Container this is equivalent to: ```sh poe api ``` +{%- else -%} +To install and use this project: + +```sh +poetry install +poe test +``` +{%- endif %} ## Contributing @@ -107,13 +115,15 @@ The following tools will be automatically installed by poetry to support develop - _pytest_: This project uses the `pytest` framework for unit testing. -- _ruff_: This project uses `ruff` to lint and automatically format code in order to maintain consistent code across all project and developers. +- _ruff_: This project uses `ruff` to lint and automatically format code in order to maintain consistent code across all projects and developers. - _mypy_: This project uses the static type checker `mypy` to enforce type annotation and spot bugs before they can happen. +- _codespell_: This project uses `codespell` to detect common spelling mistakes in code and documentation. + We use the following integration tools: -- _github actions_: This project uses GitHub actions to execute verifications in the cloud before allowing to merge with the main branch. +- _GitHub Actions_: This project uses GitHub Actions to execute verifications in the cloud before allowing to merge with the main branch. ### Development workflow @@ -127,7 +137,7 @@ We use the following integration tools: 1. Push commits with `git push`. 1. When branch is fully functional, open a pull requests on GitHub and ask for a review. 1. When approved, bump the versions with `cz bump`. - 1. Build the doc with `poe doc`. + 1. Build the docs with `poe docs`. 1. Push to GitHub one last time before merging. 1. Repeat. @@ -141,7 +151,7 @@ We use the following integration tools: - Commits should be atomic. -- Dependency injection should be favorized, and objects instantiation should be made at the latest possible moment. +- Dependency injection should be favored, and objects instantiation should be made at the latest possible moment. - Always keep Separation of Concerns in mind. diff --git a/{{ cookiecutter.__project_name_kebab_case }}/README.md b/{{ cookiecutter.__project_name_kebab_case }}/README.md index ef94fb90..90b29bee 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/README.md +++ b/{{ cookiecutter.__project_name_kebab_case }}/README.md @@ -1,110 +1,97 @@ # {{ cookiecutter.project_name }} -## Description - {{ cookiecutter.project_description }} ## Documentation - [Architecture Decision Records](docs/decisions/) -- [Confluence β€” Guide pratique](https://confluence.example.com/{{ cookiecutter.__project_name_kebab_case }}) - -## Table of contents - -1. [Documentation](#documentation) -2. [Setup](#setup) -3. [Usage](#usage) +- [MkDocs](https://{{ cookiecutter.github_org }}.github.io/{{ cookiecutter.__project_name_kebab_case }}/) (run `poe docs --serve` locally) ## Setup ### Environment variables -To use this project, you need to set the environment variables. To do so, create a copy of the `.env.sample` file and name it `.env`. Then, fill in the values of the variables. - -### Requirements - -The set of requirements is dependent on how you want to run the application: locally or in a container. - -#### Local setup - -To use the application source code, you must have the following tools installed: - -- [Python 3.12](https://www.python.org/downloads/) (the exact version is important) -- [Poetry](https://python-poetry.org/docs/#installation) (the exact version is not important, but there are some differences between major versions) - -Check that the tools have been correctly installed by executing the following commands in your terminal: +Create a copy of `.env.example` and fill in the values: ```bash -python --version +cp .env.example .env ``` -```bash -poetry --version -``` +### Local setup + +Requirements: -To install the project dependencies, run the following command at the project's root: +- [Python {{ cookiecutter.python_version }}](https://www.python.org/downloads/) +- [Poetry](https://python-poetry.org/docs/#installation) ```bash poetry install ``` -This will create a virtual environment and install the dependencies in it at the root of the project in a folder named `.venv`. +### Container setup -You can then activate the virtual environment created by Poetry (version less than 2.0) by running the following command: +Requirements: [Docker](https://docs.docker.com/get-docker/) ```bash -poetry shell +docker compose up --build ``` -If you are using Poetry version 2.0 or higher, you need to install the [plugin](https://python-poetry.org/docs/plugins/#using-plugins) `poetry-plugin-shell` before running the previous command. You can do so by running the following command: - -```bash -poetry self add poetry-plugin-shell -``` - -#### Container setup - -To use the application in a docker container, you must have Docker Desktop installed: - -- [Docker](https://docs.docker.com/get-docker/) - ## Usage -### Local usage - -The project uses `poe-the-poet` as a CLI tool to manage the application. The commands are defined in the `pyproject.toml` file. To see available commands, you can run the following command at the project's root: - -```bash -poe -``` - -To run the application locally, you need to execute the following command at the project's root: +Run `poe` to see all available tasks. +{% if cookiecutter.with_fastapi_api|int %} +### API +{%- if cookiecutter.with_fastapi_api|int %} ```bash poe api --dev ``` -This will start the application in development mode. You can then access the API at the following URL: [`http://localhost:8000`](http://localhost:8000). +Access the API at [localhost:8000](http://localhost:8000) and the docs at [localhost:8000/docs](http://localhost:8000/docs). +{%- endif %} +{% endif %} +{%- if cookiecutter.with_typer_cli|int %} -The API documentation is available at the following URL: [`http://localhost:8000/docs`](http://localhost:8000/docs). You can use it to test the different endpoints of the API. +### CLI -### Container usage +```bash +poetry run {{ cookiecutter.__project_name_kebab_case }} info +poetry run {{ cookiecutter.__project_name_kebab_case }} config +{%- if cookiecutter.with_fastapi_api|int %} +poetry run {{ cookiecutter.__project_name_kebab_case }} health +{%- endif %} +``` +{%- endif %} -To run the application in a container, you need to execute the following command at the project's root: +### Common tasks ```bash -docker compose up app +poe test # run tests +poe lint # run linting +poe docs --serve # serve documentation locally ``` -This will start the application in a container. You can then access the API at the following URL: [`http://localhost:8000`](http://localhost:8000). - ## Project structure -The project is structured as follows: +``` +{{ cookiecutter.__project_name_kebab_case }}/ +β”œβ”€β”€ src/{{ cookiecutter.__project_name_snake_case }}/ # source code +β”‚ β”œβ”€β”€ settings.py # pydantic-settings config +{%- if cookiecutter.with_fastapi_api|int %} +β”‚ β”œβ”€β”€ api.py # FastAPI application +{%- endif %} +{%- if cookiecutter.with_typer_cli|int %} +β”‚ β”œβ”€β”€ cli.py # Typer CLI +{%- endif %} +β”‚ β”œβ”€β”€ models.py # Pydantic models +β”‚ └── services.py # business logic +β”œβ”€β”€ tests/ # test suite +β”œβ”€β”€ docs/ # MkDocs + ADRs +β”œβ”€β”€ pyproject.toml # Poetry config +β”œβ”€β”€ Dockerfile # production image +└── docker-compose.yml # local development +``` -- `src/`: Contains the source code of the application. -- `tests/`: Contains the unit and integration tests of the application. -- `docs/`: Contains the ADRs and configuration guides. +## Contributing -At the root, you will find the `Dockerfile` and a `docker-compose.yml` file to facilitate the deployment of the application. There is also a `pyproject.toml` file that contains the Poetry project configuration, as well as READMEs for project documentation. -To contribute to this project, read the [CONTRIBUTING.md](CONTRIBUTING.md) file. +See [CONTRIBUTING.md](CONTRIBUTING.md). From 4a0fbc1ac7d171d1e418b49e2e45a28af9f8f8a2 Mon Sep 17 00:00:00 2001 From: David Beauchemin Date: Tue, 24 Feb 2026 10:30:12 -0500 Subject: [PATCH 66/68] feat: ajouter detect-secrets au pre-commit pour bloquer les credentials (#24) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ajouter le hook Yelp/detect-secrets dans .pre-commit-config.yaml - Ajouter detect-secrets>=1.5.0 dans les dΓ©pendances de test - GΓ©nΓ©rer le fichier .secrets.baseline initial (vide) Co-authored-by: davebulaval Co-authored-by: Claude Opus 4.6 --- .../.pre-commit-config.yaml | 5 + .../.secrets.baseline | 127 ++++++++++++++++++ .../pyproject.toml | 1 + 3 files changed, 133 insertions(+) create mode 100644 {{ cookiecutter.__project_name_kebab_case }}/.secrets.baseline diff --git a/{{ cookiecutter.__project_name_kebab_case }}/.pre-commit-config.yaml b/{{ cookiecutter.__project_name_kebab_case }}/.pre-commit-config.yaml index 9789197d..a1c5cefd 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/.pre-commit-config.yaml +++ b/{{ cookiecutter.__project_name_kebab_case }}/.pre-commit-config.yaml @@ -35,6 +35,11 @@ repos: - id: debug-statements - id: destroyed-symlinks - id: detect-private-key + - repo: https://github.com/Yelp/detect-secrets + rev: v1.5.0 + hooks: + - id: detect-secrets + args: ['--baseline', '.secrets.baseline'] - id: end-of-file-fixer - id: fix-byte-order-marker - id: mixed-line-ending diff --git a/{{ cookiecutter.__project_name_kebab_case }}/.secrets.baseline b/{{ cookiecutter.__project_name_kebab_case }}/.secrets.baseline new file mode 100644 index 00000000..ec6b4306 --- /dev/null +++ b/{{ cookiecutter.__project_name_kebab_case }}/.secrets.baseline @@ -0,0 +1,127 @@ +{ + "version": "1.5.0", + "plugins_used": [ + { + "name": "ArtifactoryDetector" + }, + { + "name": "AWSKeyDetector" + }, + { + "name": "AzureStorageKeyDetector" + }, + { + "name": "Base64HighEntropyString", + "limit": 4.5 + }, + { + "name": "BasicAuthDetector" + }, + { + "name": "CloudantDetector" + }, + { + "name": "DiscordBotTokenDetector" + }, + { + "name": "GitHubTokenDetector" + }, + { + "name": "GitLabTokenDetector" + }, + { + "name": "HexHighEntropyString", + "limit": 3.0 + }, + { + "name": "IbmCloudIamDetector" + }, + { + "name": "IbmCosHmacDetector" + }, + { + "name": "IPPublicDetector" + }, + { + "name": "JwtTokenDetector" + }, + { + "name": "KeywordDetector", + "keyword_exclude": "" + }, + { + "name": "MailchimpDetector" + }, + { + "name": "NpmDetector" + }, + { + "name": "OpenAIDetector" + }, + { + "name": "PrivateKeyDetector" + }, + { + "name": "PypiTokenDetector" + }, + { + "name": "SendGridDetector" + }, + { + "name": "SlackDetector" + }, + { + "name": "SoftlayerDetector" + }, + { + "name": "SquareOAuthDetector" + }, + { + "name": "StripeDetector" + }, + { + "name": "TelegramBotTokenDetector" + }, + { + "name": "TwilioKeyDetector" + } + ], + "filters_used": [ + { + "path": "detect_secrets.filters.allowlist.is_line_allowlisted" + }, + { + "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", + "min_level": 2 + }, + { + "path": "detect_secrets.filters.heuristic.is_indirect_reference" + }, + { + "path": "detect_secrets.filters.heuristic.is_likely_id_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_lock_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_potential_uuid" + }, + { + "path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign" + }, + { + "path": "detect_secrets.filters.heuristic.is_sequential_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_swagger_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_templated_secret" + } + ], + "results": {}, + "generated_at": "2026-02-24T15:29:44Z" +} diff --git a/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml b/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml index 34723382..3d035fd8 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml +++ b/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml @@ -50,6 +50,7 @@ uvicorn = { extras = ["standard"], version = ">=0.34.0" } commitizen = ">=4.1.0" {%- endif %} coverage = { extras = ["toml"], version = ">=7.6.0" } +detect-secrets = ">=1.5.0" mypy = ">=1.14.0" pre-commit = ">=4.0.0" pytest = ">=8.3.0" From 7900573dfcd5abff9ca9051de06c4b51b3c89fbc Mon Sep 17 00:00:00 2001 From: David Beauchemin Date: Tue, 24 Feb 2026 10:41:14 -0500 Subject: [PATCH 67/68] docs: documenter detect-secrets dans CONTRIBUTING et CHANGELOG (#25) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ajouter description de detect-secrets dans la section pre-commit du CONTRIBUTING.md - Ajouter l'entrΓ©e detect-secrets dans le CHANGELOG (Unreleased) Co-authored-by: davebulaval Co-authored-by: Claude Opus 4.6 --- CHANGELOG.md | 1 + {{ cookiecutter.__project_name_kebab_case }}/CONTRIBUTING.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a50a49a4..85bb0174 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/). - codespell linter in pre-commit and pyproject.toml - MkDocs Material for documentation (replaces pdoc) - PR title check workflow for generated projects (`pr.yml`) +- `detect-secrets` (Yelp) pre-commit hook to block accidental credential commits - `actionlint` pre-commit hook for GitHub Actions validation - `ruff-check` and `ruff-format` pre-commit hooks for template code - `pre-commit-hooks` (check-yaml, check-toml, end-of-file-fixer, trailing-whitespace) diff --git a/{{ cookiecutter.__project_name_kebab_case }}/CONTRIBUTING.md b/{{ cookiecutter.__project_name_kebab_case }}/CONTRIBUTING.md index 253c9cef..e7b7ea6e 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/CONTRIBUTING.md +++ b/{{ cookiecutter.__project_name_kebab_case }}/CONTRIBUTING.md @@ -111,7 +111,7 @@ The following tools will be automatically installed by poetry to support develop - _commitizen_: This project follows the [Conventional Commits](https://www.conventionalcommits.org/) standard to automate [Semantic Versioning](https://semver.org/) and [Keep A Changelog](https://keepachangelog.com/) with [Commitizen](https://github.com/commitizen-tools/commitizen). -- _pre-commit_: This project uses pre-commit hooks that enforces the submitted code to respect conventions and high quality standards. +- _pre-commit_: This project uses pre-commit hooks that enforces the submitted code to respect conventions and high quality standards. This includes [detect-secrets](https://github.com/Yelp/detect-secrets) which prevents accidentally committing credentials (API keys, tokens, passwords). If a secret is detected, the commit is blocked. To update the baseline after a false positive: `detect-secrets scan --baseline .secrets.baseline`. - _pytest_: This project uses the `pytest` framework for unit testing. From 1e9237ed3ce61d7d0e92f4701ea9329e9c5083af Mon Sep 17 00:00:00 2001 From: davebulaval Date: Tue, 24 Feb 2026 19:24:41 -0500 Subject: [PATCH 68/68] feat: enhance .editorconfig and add .gitattributes - Add max_line_length = 99 to .editorconfig (Baseline convention) - Add web file types (css, html, js, jsx, ts, tsx) to indent rules - Add .sh (LF) and .bat (CRLF) line ending rules - Add .gitattributes for consistent line ending normalization Closes #327 Co-Authored-By: Claude Opus 4.6 --- .../.editorconfig | 9 +++++- .../.gitattributes | 28 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 {{ cookiecutter.__project_name_kebab_case }}/.gitattributes diff --git a/{{ cookiecutter.__project_name_kebab_case }}/.editorconfig b/{{ cookiecutter.__project_name_kebab_case }}/.editorconfig index 15ff9da6..f9eca780 100644 --- a/{{ cookiecutter.__project_name_kebab_case }}/.editorconfig +++ b/{{ cookiecutter.__project_name_kebab_case }}/.editorconfig @@ -10,8 +10,9 @@ trim_trailing_whitespace = true [*.py] indent_style = space indent_size = 4 +max_line_length = 99 -[*.{yml,yaml,json,toml}] +[*.{yml,yaml,json,toml,css,html,js,jsx,ts,tsx}] indent_style = space indent_size = 2 @@ -20,3 +21,9 @@ indent_style = tab [*.md] trim_trailing_whitespace = false + +[*.sh] +end_of_line = lf + +[*.bat] +end_of_line = crlf diff --git a/{{ cookiecutter.__project_name_kebab_case }}/.gitattributes b/{{ cookiecutter.__project_name_kebab_case }}/.gitattributes new file mode 100644 index 00000000..219e1432 --- /dev/null +++ b/{{ cookiecutter.__project_name_kebab_case }}/.gitattributes @@ -0,0 +1,28 @@ +# Auto detect text files and normalize line endings +* text=auto + +# Force LF for text files +*.py text eol=lf +*.sh text eol=lf +*.yml text eol=lf +*.yaml text eol=lf +*.md text eol=lf +*.txt text eol=lf +*.json text eol=lf +*.toml text eol=lf +Dockerfile text eol=lf + +# Windows scripts keep CRLF +*.ps1 text eol=crlf +*.bat text eol=crlf + +# Binary files +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.zip binary +*.gz binary +*.tar binary +*.whl binary