diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b56f8b43..70be46da 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,10 +1,14 @@ { "name": "substrate", - "image": "ghcr.io/astral-sh/uv:python3.10-bookworm", + "image": "ghcr.io/astral-sh/uv:python3.10-trixie", "features": { - "ghcr.io/devcontainers/features/docker-in-docker:2": {}, + "ghcr.io/devcontainers/features/docker-in-docker:2": { + "moby": false + }, "ghcr.io/devcontainers-extra/features/devcontainers-cli:1": {}, - "ghcr.io/devcontainers-extra/features/starship:1": {} + "ghcr.io/devcontainers-extra/features/apt-get-packages:1": { + "packages": "starship" + } }, "remoteEnv": { "VIRTUAL_ENV": "/opt/venv", diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index d91ed5ae..3e58bd3b 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Set up uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 - name: Check PR title run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2c49d533..0e50ff01 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,7 +7,7 @@ on: pull_request: jobs: - test-copier: + test: runs-on: ubuntu-latest strategy: @@ -17,16 +17,16 @@ jobs: project-type: ["app", "package"] typing: ["optional", "strict"] - name: "Copier: Python ${{ matrix.python-version }} ${{ matrix.project-type }} with ${{ matrix.typing }} typing" + name: "Python ${{ matrix.python-version }} ${{ matrix.project-type }} with ${{ matrix.typing }} typing" steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: path: substrate - name: Set up uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: enable-cache: true cache-dependency-glob: "**/uv.lock" @@ -66,50 +66,3 @@ jobs: with: context: ./my-project/ target: app - - test-cookiecutter: - runs-on: ubuntu-latest - - strategy: - fail-fast: true - matrix: - python-version: ["3.10", "3.13"] - project-type: ["app", "package"] - - name: "Cookiecutter: Python ${{ matrix.python-version }} ${{ matrix.project-type }}" - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - path: poetry-cookiecutter - - - name: Set up uv - uses: astral-sh/setup-uv@v6 - with: - enable-cache: true - cache-dependency-glob: "**/uv.lock" - - - name: Scaffold Python project - run: | - uvx cruft create --no-input --extra-context '{"project_type": "${{ matrix.project-type }}", "project_name": "My Project", "python_version": "3.10", "__docker_image":"superlinear/python-gpu:$PYTHON_VERSION-cuda11.8", "with_fastapi_api": "1", "with_typer_cli": "1"}' ./poetry-cookiecutter/ - cd my-project - git config --global init.defaultBranch main - git init - git checkout -b test - git add . - - - name: Lint and test project - uses: devcontainers/ci@v0.3 - with: - subFolder: ./my-project/ - runCmd: | - poe lint - poe test - - - name: Build app Docker image - if: ${{ matrix.project-type == 'app' }} - uses: docker/build-push-action@v6 - with: - context: ./my-project/ - target: app diff --git a/README.md b/README.md index e64a2e32..593075b7 100644 --- a/README.md +++ b/README.md @@ -68,43 +68,6 @@ If your project is on GitHub, your docs will be published automatically to GitHu > [!TIP] > Make sure to [configure the source of your project's GitHub Pages as GitHub Actions in your project settings](https://docs.github.com/en/pages/getting-started-with-github-pages/configuring-a-publishing-source-for-your-github-pages-site#publishing-with-a-custom-github-actions-workflow) to allow the GitHub Actions workflow to publish your docs. -## 🍪 Migrating from Cookiecutter - -> [!IMPORTANT] -> This project was formerly known as `Poetry Cookiecutter` and was based on [Poetry](https://github.com/python-poetry/poetry) and [Cookiecutter](https://github.com/cookiecutter/cookiecutter). We will continue to support the original Cookiecutter-based template side by side with the new Copier-based template. However, we do encourage users to upgrade to the new Copier-based template by following the instructions below. - -To migrate a project from Cookiecutter to Copier, follow these steps: - -1. In your project repository, run: - - ```sh - # Create a new branch - git checkout -b rescaffold - - # Remove unnecessary files - rm -f .cruft.json poetry.lock - - # Rescaffold the project without changing src/ and tests/ - uvx copier copy --overwrite --exclude src/ --exclude tests/ gh:superlinear-ai/substrate . - ``` - -2. Review the changes to `pyproject.toml` and reinsert your project's dependencies. -3. Review the changes to `README.md` and reinsert your project's documentation. -4. Commit and push all changes with: - - ```sh - # Stage all changes - git add . - - # Commit the staged changes - git commit -m "build: upgrade scaffolding" - - # Push the committed changes - git push origin rescaffold - ``` - -5. Create a PR from your branch, review it, and merge it! - ## Contributing
@@ -150,8 +113,8 @@ The following development environments are supported: pre-commit install --install-hooks ``` -3. _VS Code Dev Container_: clone this repository, open it with VS Code, and run Ctrl/⌘ + + P → _Dev Containers: Reopen in Container_. -4. _PyCharm Dev Container_: clone this repository, open it with PyCharm, [create a Dev Container with Mount Sources](https://www.jetbrains.com/help/pycharm/start-dev-container-inside-ide.html), and [configure an existing Python interpreter](https://www.jetbrains.com/help/pycharm/configuring-python-interpreter.html#widget) at `/opt/venv/bin/python`. +4. _VS Code Dev Container_: clone this repository, open it with VS Code, and run Ctrl/⌘ + + P → _Dev Containers: Reopen in Container_. +5. _PyCharm Dev Container_: clone this repository, open it with PyCharm, [create a Dev Container with Mount Sources](https://www.jetbrains.com/help/pycharm/start-dev-container-inside-ide.html), and [configure an existing Python interpreter](https://www.jetbrains.com/help/pycharm/configuring-python-interpreter.html#widget) at `/opt/venv/bin/python`.
diff --git a/cookiecutter.json b/cookiecutter.json deleted file mode 100644 index f548f9a9..00000000 --- a/cookiecutter.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "project_type": [ - "package", - "app" - ], - "project_name": "{% if cookiecutter.project_type == 'app' %}My App{% else %}My Package{% endif %}", - "project_description": "A Python {{ cookiecutter.project_type }} that reticulates splines.", - "project_url": "https://github.com/user/my-{{ cookiecutter.project_type }}", - "author_name": "John Smith", - "author_email": "john@example.com", - "python_version": "{% if cookiecutter.project_type == 'app' %}3.12{% else %}3.10{% endif %}", - "development_environment": [ - "simple", - "strict" - ], - "with_conventional_commits": "{% if cookiecutter.development_environment == 'simple' %}0{% else %}1{% endif %}", - "with_fastapi_api": "0", - "with_typer_cli": "0", - "continuous_integration": [ - "GitHub", - "GitLab" - ], - "private_package_repository_name": "", - "private_package_repository_url": "", - "__docker_image": "python:$PYTHON_VERSION-slim", - "__docstring_style": "NumPy", - "__project_name_kebab_case": "{{ cookiecutter.project_name|slugify }}", - "__project_name_snake_case": "{{ cookiecutter.project_name|slugify(separator='_') }}" -} \ No newline at end of file diff --git a/copier.yml b/copier.yml index 44d7c933..b1f92e8b 100644 --- a/copier.yml +++ b/copier.yml @@ -102,4 +102,3 @@ project_name_snake_case: documentation_url: when: false default: "{% if ci == 'github' %}https://{{ project_url.rstrip('/').split('/')[-2] }}.github.io/{{ project_url.rstrip('/').split('/')[-1] }}{% else %}{{ project_url }}{% endif %}" - diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py deleted file mode 100644 index 7523c3b5..00000000 --- a/hooks/post_gen_project.py +++ /dev/null @@ -1,41 +0,0 @@ -import os -import shutil - -# 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 }}") -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": - os.remove(f"src/{project_name}/py.typed") - os.remove(".github/dependabot.yml") - -# Remove FastAPI if not selected. -if not with_fastapi_api: - os.remove(f"src/{project_name}/api.py") - os.remove("tests/test_api.py") - -# Remove Typer if not selected. -if not with_typer_cli: - os.remove(f"src/{project_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 is_application: - os.remove(".github/workflows/publish.yml") - else: - os.remove(".github/workflows/deploy.yml") - if not with_conventional_commits: - os.remove(".github/workflows/pr.yml") diff --git a/template/.devcontainer/devcontainer.json.jinja b/template/.devcontainer/devcontainer.json.jinja index aa0d9787..2def936b 100644 --- a/template/.devcontainer/devcontainer.json.jinja +++ b/template/.devcontainer/devcontainer.json.jinja @@ -5,9 +5,13 @@ "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/", "features": { {%- if project_type == 'app' %} - "ghcr.io/devcontainers/features/docker-in-docker:2": {}, + "ghcr.io/devcontainers/features/docker-in-docker:2": { + "moby": false + }, {%- endif %} - "ghcr.io/devcontainers-extra/features/starship:1": {} + "ghcr.io/devcontainers-extra/features/apt-get-packages:1": { + "packages": "starship" + } }, "overrideCommand": true, "remoteUser": "user", diff --git a/template/Dockerfile.jinja b/template/Dockerfile.jinja index 946c9226..e1caecce 100644 --- a/template/Dockerfile.jinja +++ b/template/Dockerfile.jinja @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:1 -FROM ghcr.io/astral-sh/uv:python{{ python_version }}-bookworm AS dev +FROM ghcr.io/astral-sh/uv:python{{ python_version }}-trixie AS dev # Create and activate a virtual environment [1]. # [1] https://docs.astral.sh/uv/concepts/projects/config/#project-environment-path diff --git a/template/{% if ci == 'github' %}.github{% endif %}/workflows/docs.yml.jinja b/template/{% if ci == 'github' %}.github{% endif %}/workflows/docs.yml.jinja index 07e118c7..30dd10b3 100644 --- a/template/{% if ci == 'github' %}.github{% endif %}/workflows/docs.yml.jinja +++ b/template/{% if ci == 'github' %}.github{% endif %}/workflows/docs.yml.jinja @@ -26,7 +26,7 @@ jobs: uses: actions/checkout@v5 - name: Install uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 - name: Build docs run: uv run mkdocs build diff --git a/template/{% if ci == 'github' %}.github{% endif %}/workflows/test.yml.jinja b/template/{% if ci == 'github' %}.github{% endif %}/workflows/test.yml.jinja index 1be58c15..e64fac25 100644 --- a/template/{% if ci == 'github' %}.github{% endif %}/workflows/test.yml.jinja +++ b/template/{% if ci == 'github' %}.github{% endif %}/workflows/test.yml.jinja @@ -23,7 +23,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Node.js uses: actions/setup-node@v4 diff --git a/template/{% if ci == 'github' %}.github{% endif %}/workflows/{% if project_type == 'app' %}deploy.yml{% endif %}.jinja b/template/{% if ci == 'github' %}.github{% endif %}/workflows/{% if project_type == 'app' %}deploy.yml{% endif %}.jinja index 48a0200c..2b63fa97 100644 --- a/template/{% if ci == 'github' %}.github{% endif %}/workflows/{% if project_type == 'app' %}deploy.yml{% endif %}.jinja +++ b/template/{% if ci == 'github' %}.github{% endif %}/workflows/{% if project_type == 'app' %}deploy.yml{% endif %}.jinja @@ -30,7 +30,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Log in to the Docker registry uses: docker/login-action@v3 diff --git a/template/{% if ci == 'github' %}.github{% endif %}/workflows/{% if project_type == 'package' %}publish.yml{% endif %}.jinja b/template/{% if ci == 'github' %}.github{% endif %}/workflows/{% if project_type == 'package' %}publish.yml{% endif %}.jinja index c7c7859d..4431f14c 100644 --- a/template/{% if ci == 'github' %}.github{% endif %}/workflows/{% if project_type == 'package' %}publish.yml{% endif %}.jinja +++ b/template/{% if ci == 'github' %}.github{% endif %}/workflows/{% if project_type == 'package' %}publish.yml{% endif %}.jinja @@ -14,10 +14,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 - name: Publish package run: | diff --git a/template/{% if ci == 'github' %}.github{% endif %}/workflows/{% if with_conventional_commits %}pr.yml{% endif %}.jinja b/template/{% if ci == 'github' %}.github{% endif %}/workflows/{% if with_conventional_commits %}pr.yml{% endif %}.jinja index 119fe789..6905270b 100644 --- a/template/{% if ci == 'github' %}.github{% endif %}/workflows/{% if with_conventional_commits %}pr.yml{% endif %}.jinja +++ b/template/{% if ci == 'github' %}.github{% endif %}/workflows/{% if with_conventional_commits %}pr.yml{% endif %}.jinja @@ -12,7 +12,7 @@ jobs: steps: - name: Set up uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 - name: Check PR title run: | diff --git a/{{ cookiecutter.__project_name_kebab_case }}/.devcontainer/devcontainer.json b/{{ cookiecutter.__project_name_kebab_case }}/.devcontainer/devcontainer.json deleted file mode 100644 index a59cd5c8..00000000 --- a/{{ cookiecutter.__project_name_kebab_case }}/.devcontainer/devcontainer.json +++ /dev/null @@ -1,85 +0,0 @@ -{ - "name": "{{ cookiecutter.__project_name_kebab_case }}", - "dockerComposeFile": "../docker-compose.yml", - "service": "devcontainer", - "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/", - "remoteUser": "user", - "overrideCommand": true, - "postStartCommand": "cp --update /opt/build/poetry/poetry.lock /workspaces/${localWorkspaceFolderBasename}/ && mkdir -p /workspaces/${localWorkspaceFolderBasename}/.git/hooks/ && cp --update /opt/build/git/* /workspaces/${localWorkspaceFolderBasename}/.git/hooks/", - "customizations": { - "jetbrains": { - "backend": "PyCharm", - "plugins": [ - "com.github.copilot" - ], - "settings": { - "org.jetbrains.plugins.terminal:app:TerminalOptionsProvider.myShellPath": "/usr/bin/zsh" - } - }, - "vscode": { - "extensions": [ - "charliermarsh.ruff", - "GitHub.copilot", - "GitHub.copilot-chat", - {%- if cookiecutter.continuous_integration == "GitHub" %} - "GitHub.vscode-github-actions", - "GitHub.vscode-pull-request-github", - {%- elif cookiecutter.continuous_integration == "GitLab" %} - "GitLab.gitlab-workflow", - {%- endif %} - "ms-azuretools.vscode-docker", - "ms-python.mypy-type-checker", - "ms-python.python", - "ms-toolsai.jupyter", - "ryanluker.vscode-coverage-gutters", - "tamasfe.even-better-toml", - "visualstudioexptteam.vscodeintellicode" - ], - "settings": { - "coverage-gutters.coverageFileNames": [ - "reports/coverage.xml" - ], - "editor.codeActionsOnSave": { - "source.fixAll": "explicit", - "source.organizeImports": "explicit" - }, - "editor.formatOnSave": true, - "[python]": { - "editor.defaultFormatter": "charliermarsh.ruff" - }, - "[toml]": { - "editor.formatOnSave": false - }, - "editor.rulers": [ - 100 - ], - "files.autoSave": "onFocusChange", - "github.copilot.chat.agent.enabled": true, - "github.copilot.chat.codesearch.enabled": true, - "github.copilot.chat.edits.enabled": true, - "github.copilot.nextEditSuggestions.enabled": true, - "jupyter.kernels.excludePythonEnvironments": ["/usr/local/bin/python"], - "mypy-type-checker.importStrategy": "fromEnvironment", - "mypy-type-checker.preferDaemon": true, - "notebook.codeActionsOnSave": { - "notebook.source.fixAll": "explicit", - "notebook.source.organizeImports": "explicit" - }, - "notebook.formatOnSave.enabled": true, - "python.defaultInterpreterPath": "/opt/{{ cookiecutter.__project_name_kebab_case }}-env/bin/python", - "python.terminal.activateEnvironment": false, - "python.testing.pytestEnabled": true, - "ruff.importStrategy": "fromEnvironment", - {%- if cookiecutter.development_environment == "strict" %} - "ruff.logLevel": "warning", - {%- endif %} - "terminal.integrated.defaultProfile.linux": "zsh", - "terminal.integrated.profiles.linux": { - "zsh": { - "path": "/usr/bin/zsh" - } - } - } - } - } -} diff --git a/{{ cookiecutter.__project_name_kebab_case }}/.dockerignore b/{{ cookiecutter.__project_name_kebab_case }}/.dockerignore deleted file mode 100644 index c17c79a6..00000000 --- a/{{ cookiecutter.__project_name_kebab_case }}/.dockerignore +++ /dev/null @@ -1,5 +0,0 @@ -# Caches -.*_cache/ - -# Git -.git/ diff --git a/{{ cookiecutter.__project_name_kebab_case }}/.github/dependabot.yml b/{{ cookiecutter.__project_name_kebab_case }}/.github/dependabot.yml deleted file mode 100644 index b402007e..00000000 --- a/{{ cookiecutter.__project_name_kebab_case }}/.github/dependabot.yml +++ /dev/null @@ -1,36 +0,0 @@ -version: 2 - -updates: - - package-ecosystem: github-actions - directory: / - schedule: - interval: monthly - commit-message: - prefix: "ci" - prefix-development: "ci" - include: scope - groups: - ci-dependencies: - patterns: - - "*" - - package-ecosystem: pip - directory: / - schedule: - interval: monthly - commit-message: - prefix: "chore" - prefix-development: "build" - include: scope - allow: - {%- if cookiecutter.project_type == "app" %} - - dependency-type: production - {%- endif %} - - dependency-type: development - versioning-strategy: increase - groups: - {%- if cookiecutter.project_type == "app" %} - runtime-dependencies: - dependency-type: production - {%- endif %} - development-dependencies: - dependency-type: development 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 8771a41c..00000000 --- a/{{ cookiecutter.__project_name_kebab_case }}/.github/workflows/deploy.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: Deploy - -on: - push: - tags: - - "v*.*.*" - 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 }}/.github/workflows/pr.yml b/{{ cookiecutter.__project_name_kebab_case }}/.github/workflows/pr.yml deleted file mode 100644 index 119fe789..00000000 --- a/{{ cookiecutter.__project_name_kebab_case }}/.github/workflows/pr.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: PR - -on: - pull_request: - types: [edited, opened, reopened, synchronize] - -jobs: - title: - runs-on: ubuntu-latest - - name: Check PR title - - steps: - - name: Set up uv - uses: astral-sh/setup-uv@v6 - - - name: Check PR title - run: | - uvx --from=commitizen cz check --message "{% raw %}${{ github.event.pull_request.title }}{% endraw %}" 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 %} diff --git a/{{ cookiecutter.__project_name_kebab_case }}/.github/workflows/test.yml b/{{ cookiecutter.__project_name_kebab_case }}/.github/workflows/test.yml deleted file mode 100644 index ce94c5b8..00000000 --- a/{{ cookiecutter.__project_name_kebab_case }}/.github/workflows/test.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Test - -on: - push: - branches: - - main - - master - pull_request: - -jobs: - test: - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - python-version: ["{{ cookiecutter.python_version }}"] - - name: Python {% raw %}${{{% endraw %} matrix.python-version }} - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: 23 - - - name: Install @devcontainers/cli - run: npm install --location=global @devcontainers/cli@0.76.0 - - - name: Start Dev Container - run: | - git config --global init.defaultBranch main - devcontainer up --workspace-folder . - env: - PYTHON_VERSION: {% raw %}${{{% endraw %} matrix.python-version }} - - - name: Lint {{ cookiecutter.project_type }} - run: devcontainer exec --workspace-folder . poe lint - - - name: Test {{ cookiecutter.project_type }} - run: devcontainer exec --workspace-folder . poe test diff --git a/{{ cookiecutter.__project_name_kebab_case }}/.gitignore b/{{ cookiecutter.__project_name_kebab_case }}/.gitignore deleted file mode 100644 index 2ef3a6b2..00000000 --- a/{{ cookiecutter.__project_name_kebab_case }}/.gitignore +++ /dev/null @@ -1,68 +0,0 @@ -# Coverage.py -htmlcov/ -reports/ - -# cruft -*.rej - -# Data -*.csv* -*.dat* -*.pickle* -*.xls* -*.zip* -data/ - -# direnv -.envrc - -# dotenv -.env - -# Hypothesis -.hypothesis/ - -# Jupyter -*.ipynb -.ipynb_checkpoints/ -notebooks/ - -# macOS -.DS_Store - -# mise -mise.local.toml - -# mypy -.dmypy.json -.mypy_cache/ - -# Node.js -node_modules/ - -# Poetry -.venv/ -dist/ - -# PyCharm -.idea/ - -# pyenv -.python-version - -# pytest -.pytest_cache/ - -# Python -__pycache__/ -*.egg-info/ -*.py[cdo] - -# Ruff -.ruff_cache/ - -# Terraform -.terraform/ - -# VS Code -.vscode/ diff --git a/{{ cookiecutter.__project_name_kebab_case }}/.gitlab-ci.yml b/{{ cookiecutter.__project_name_kebab_case }}/.gitlab-ci.yml deleted file mode 100644 index 5264202a..00000000 --- a/{{ cookiecutter.__project_name_kebab_case }}/.gitlab-ci.yml +++ /dev/null @@ -1,148 +0,0 @@ -stages: - - build - - test - - {% if cookiecutter.project_type == "package" %}publish{% else %}deploy{% endif %} - -variables: - DOCKER_TLS_CERTDIR: "/certs" - -.python_matrix: - parallel: - matrix: - - PYTHON_VERSION: ["{{ cookiecutter.python_version }}"] - -.install_devcontainers_cli: - cache: - paths: - - .apk_cache - - .npm_cache - before_script: - - mkdir -p .apk_cache && apk add --cache-dir .apk_cache npm - - npm install --cache .npm_cache --global --prefer-offline @devcontainers/cli@0.73.0 - -# Build the Dev Container. -Build: - extends: - - .python_matrix - - .install_devcontainers_cli - stage: build - image: docker:latest - services: - - docker:dind - script: - - | - # Log in to the Docker registry. - echo "$CI_REGISTRY_PASSWORD" | docker login --username "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY" - - # Compute a hash for the Dev Container image. - export CI_IMAGE_SHA="$(sha1sum Dockerfile poetry.lock pyproject.toml | sha1sum | cut -c 1-8)" - echo "CI_IMAGE_SHA=$CI_IMAGE_SHA" >> .env - - # Build and push the Dev Container image, unless it already exists. - IMAGE_NAME="$CI_REGISTRY_IMAGE/devcontainer:$PYTHON_VERSION-$CI_IMAGE_SHA" - IMAGE_EXISTS=${IMAGE_EXISTS:-$(timeout 2s docker pull "$IMAGE_NAME" >/dev/null 2>&1 && echo $? || echo $?)} - if [ "$IMAGE_EXISTS" -ne 1 ]; then - echo "$IMAGE_NAME exists, skipping this job..." - else - {%- if cookiecutter.private_package_repository_name %} - echo "[http-basic.{{ cookiecutter.private_package_repository_name|slugify }}]" >> auth.toml - echo "username = \"gitlab-ci-token\"" >> auth.toml - echo "password = \"$CI_JOB_TOKEN\"" >> auth.toml - export $POETRY_AUTH_TOML_PATH=$(pwd)/auth.toml - {%- endif %} - devcontainer build --image-name "$IMAGE_NAME" --workspace-folder . - docker push "$IMAGE_NAME" - fi - artifacts: - reports: - dotenv: .env - -# Lint and test the {{ cookiecutter.project_type }}. -Test: - extends: - - .python_matrix - - .install_devcontainers_cli - stage: test - image: docker:latest - services: - - docker:dind - script: - - | - devcontainer up --cache-from "type=registry,ref=$CI_REGISTRY_IMAGE/devcontainer:$PYTHON_VERSION-$CI_IMAGE_SHA" --workspace-folder . - devcontainer exec --workspace-folder . git config --global --add safe.directory /workspaces/{{ cookiecutter.__project_name_kebab_case }} - devcontainer exec --workspace-folder . poe lint - devcontainer exec --workspace-folder . poe test - coverage: '/^TOTAL.*\s+(\d+(?:\.\d+)?)%/' - artifacts: - reports: - coverage_report: - coverage_format: cobertura - path: reports/coverage.xml - junit: - - reports/mypy.xml - - reports/pytest.xml - untracked: true - when: always - -{% if cookiecutter.project_type == "package" -%} -# Publish this package version to {% if cookiecutter.private_package_repository_name %}a private package repository{% else %}PyPI{% endif %}. -Publish: - stage: publish - image: $CI_REGISTRY_IMAGE/devcontainer:{{ cookiecutter.python_version }}-$CI_IMAGE_SHA - script: - {%- if cookiecutter.private_package_repository_name %} - - poetry config repositories.private "{{ cookiecutter.private_package_repository_url.replace('simple/', '').replace('simple', '') }}" - - poetry config http-basic.private "gitlab-ci-token" "$CI_JOB_TOKEN" - - poetry publish --build --repository private - {%- else %} - - poetry config pypi-token.pypi "$POETRY_PYPI_TOKEN_PYPI" - - poetry publish --build - {%- endif %} - only: - - tags -{%- else -%} -# Deploy the app to the Docker registry. -Deploy: - stage: deploy - image: docker:latest - services: - - docker:dind - script: - - | - # Log in to the Docker registry. - echo "$CI_REGISTRY_PASSWORD" | docker login --username "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY" - - # Compile a list of tags for the image. - DOCKER_TAGS="" - if [ "$CI_COMMIT_BRANCH" = "$CI_DEFAULT_BRANCH" ]; then DOCKER_TAGS="$DOCKER_TAGS latest"; fi - if [ -n "$CI_COMMIT_TAG" ]; then DOCKER_TAGS="$DOCKER_TAGS $CI_COMMIT_TAG"; fi - if [ -n "$CI_ENVIRONMENT_NAME" ]; then DOCKER_TAGS="$DOCKER_TAGS $CI_ENVIRONMENT_NAME"; fi - DOCKER_TAGS_JOINED="" - for DOCKER_TAG in $DOCKER_TAGS; do - DOCKER_TAGS_JOINED="$DOCKER_TAGS_JOINED --tag $CI_REGISTRY_IMAGE:$DOCKER_TAG" - done - - # Build the app image. - {%- if cookiecutter.private_package_repository_name %} - echo "[http-basic.{{ cookiecutter.private_package_repository_name|slugify }}]" >> auth.toml - echo "username = \"gitlab-ci-token\"" >> auth.toml - echo "password = \"$CI_JOB_TOKEN\"" >> auth.toml - {%- endif %} - docker build \ - --cache-from "type=registry,ref=$CI_REGISTRY_IMAGE/devcontainer:{{ cookiecutter.python_version }}-$CI_IMAGE_SHA" \ - --pull \ - {%- if cookiecutter.private_package_repository_name %} - --secret id=poetry-auth,src=auth.toml \ - {%- endif %} - --target app \ - $DOCKER_TAGS_JOINED \ - . - - # Push the tags to the Docker registry. - for DOCKER_TAG in $DOCKER_TAGS; do - docker push "$CI_REGISTRY_IMAGE:$DOCKER_TAG" - done - only: - - tags - when: manual -{%- endif %} \ No newline at end of file diff --git a/{{ cookiecutter.__project_name_kebab_case }}/.pre-commit-config.yaml b/{{ cookiecutter.__project_name_kebab_case }}/.pre-commit-config.yaml deleted file mode 100644 index 7bf220d0..00000000 --- a/{{ cookiecutter.__project_name_kebab_case }}/.pre-commit-config.yaml +++ /dev/null @@ -1,88 +0,0 @@ -# https://pre-commit.com -default_install_hook_types: [commit-msg, pre-commit] -default_stages: [pre-commit, manual] -fail_fast: true -repos: - - repo: meta - hooks: - - id: check-useless-excludes - - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.10.0 - hooks: - - id: python-check-mock-methods - - id: python-use-type-annotations - - id: rst-backticks - - id: rst-directive-colons - - id: rst-inline-touching-normal - - id: text-unicode-replacement-char - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 - hooks: - - id: check-added-large-files - - id: check-ast - - id: check-builtin-literals - - id: check-case-conflict - - id: check-docstring-first - - id: check-illegal-windows-names - - id: check-json - - id: check-merge-conflict - - id: check-shebang-scripts-are-executable - - id: check-symlinks - - id: check-toml - - id: check-vcs-permalinks - - id: check-xml - - id: check-yaml - - id: debug-statements - - 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-test-first] - - id: trailing-whitespace - types: [python] - - repo: local - hooks: - {%- if cookiecutter.with_conventional_commits|int %} - - id: commitizen - name: commitizen - entry: cz check - args: [--commit-msg-file] - require_serial: true - language: system - stages: [commit-msg] - {%- endif %} - - id: ruff-check - name: ruff check - entry: ruff check - args: ["--force-exclude", "--extend-fixable=ERA001,F401,F841,T201,T203"{% if cookiecutter.development_environment == "simple" %}, "--fix-only"{% endif %}] - require_serial: true - language: system - types_or: [python, pyi] - - id: ruff-format - name: ruff format - entry: ruff format - args: [--force-exclude] - require_serial: true - language: system - types_or: [python, pyi] - {%- if cookiecutter.development_environment == "strict" %} - - id: shellcheck - name: shellcheck - entry: shellcheck - args: [--check-sourced] - language: system - types: [shell] - {%- endif %} - - id: poetry-check - name: poetry check - entry: poetry check - language: system - pass_filenames: false - - id: mypy - name: mypy - entry: mypy - language: system - types: [python] diff --git a/{{ cookiecutter.__project_name_kebab_case }}/Dockerfile b/{{ cookiecutter.__project_name_kebab_case }}/Dockerfile deleted file mode 100644 index 7e9c9a25..00000000 --- a/{{ cookiecutter.__project_name_kebab_case }}/Dockerfile +++ /dev/null @@ -1,136 +0,0 @@ -# syntax=docker/dockerfile:1 -ARG PYTHON_VERSION={{ cookiecutter.python_version }} -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 -ARG UID=1000 -ARG GID=$UID -RUN groupadd --gid $GID user && \ - useradd --create-home --gid $GID --uid $UID user --no-log-init && \ - chown user /opt/ -USER user - -# Create and activate a virtual environment. -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. -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.0.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/ && \ - 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 --without test,dev --all-extras --no-interaction - - - -FROM poetry AS dev - -# Install development tools: curl, git, gpg, nodejs, ssh, starship, sudo, vim, and zsh. -USER root -RUN --mount=type=cache,target=/var/cache/apt/ \ - --mount=type=cache,target=/var/lib/apt/ \ - curl -fsSL https://deb.nodesource.com/setup_current.x | bash - && \ - apt-get update && \ - apt-get install --no-install-recommends --yes curl git gnupg nodejs ssh sudo vim zsh && \ - 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. -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. -COPY --chown=user:user .pre-commit-config.yaml /workspaces/{{ cookiecutter.__project_name_kebab_case }}/ -RUN mkdir -p /opt/build/poetry/ && cp poetry.lock /opt/build/poetry/ && \ - git init && pre-commit install --install-hooks && \ - 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.9.7 -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 && \ - echo 'source ~/.antidote/antidote.zsh' >> ~/.zshrc && \ - echo 'antidote load' >> ~/.zshrc && \ - echo 'eval "$(starship init zsh)"' >> ~/.zshrc && \ - echo 'HISTFILE=~/.history/.zsh_history' >> ~/.zshrc && \ - echo 'HISTSIZE=1000' >> ~/.zshrc && \ - echo 'SAVEHIST=1000' >> ~/.zshrc && \ - echo 'setopt share_history' >> ~/.zshrc && \ - echo 'bindkey "^[[A" history-beginning-search-backward' >> ~/.zshrc && \ - 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 - -# Copy the virtual environment from the poetry stage. -COPY --from=poetry $VIRTUAL_ENV $VIRTUAL_ENV - -# Copy the {{ cookiecutter.project_type }} source code to the working directory. -COPY --chown=user:user . . - -# 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 %} diff --git a/{{ cookiecutter.__project_name_kebab_case }}/README.md b/{{ cookiecutter.__project_name_kebab_case }}/README.md deleted file mode 100644 index 2f14f8ba..00000000 --- a/{{ cookiecutter.__project_name_kebab_case }}/README.md +++ /dev/null @@ -1,171 +0,0 @@ -[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE3IDE2VjdsLTYgNU0yIDlWOGwxLTFoMWw0IDMgOC04aDFsNCAyIDEgMXYxNGwtMSAxLTQgMmgtMWwtOC04LTQgM0gzbC0xLTF2LTFsMy0zIi8+PC9zdmc+)](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 }}){% 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 }} - -{{ cookiecutter.project_description }} -{%- if cookiecutter.project_type == "package" or cookiecutter.with_typer_cli|int %} - -## Installing - -To install this package, run: - -```sh -{% if cookiecutter.private_package_repository_name %}poetry add{% else %}pip install{% endif %} {{ cookiecutter.__project_name_kebab_case }} -``` -{%- endif %} - -## Using -{%- if cookiecutter.with_typer_cli|int %} - -To view the CLI help information, run: - -```sh -{{ cookiecutter.__project_name_kebab_case }} --help -``` -{%- elif cookiecutter.project_type == "app" %} - -To serve this app, run: - -```sh -docker compose up app -``` -{%- if cookiecutter.with_fastapi_api|int %} - -and open [localhost:8000](http://localhost:8000) in your browser. -{%- endif %} - -Within the Dev Container this is equivalent to: - -```sh -poe {% if cookiecutter.with_fastapi_api|int %}api{% else %}app{% endif %} -``` -{%- else %} - -Example usage: - -```python -import {{ cookiecutter.__project_name_snake_case }} - -... -``` -{%- 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) - {%- 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 %} - -
- -
-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. - -
-{%- 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 %} - -
- -
-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. ⭐️ _VS Code 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. _VS Code Dev Container_: clone this repository, open it with VS Code, and run Ctrl/⌘ + + P → _Dev Containers: Reopen in Container_. -1. _PyCharm Dev Container_: clone this repository, open it with PyCharm, [create a Dev Container with Mount Sources](https://www.jetbrains.com/help/pycharm/start-dev-container-inside-ide.html), and [configure an existing Python interpreter](https://www.jetbrains.com/help/pycharm/configuring-python-interpreter.html#widget) at `/opt/{{ cookiecutter.__project_name_kebab_case }}-env/bin/python`. -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 %} - -
diff --git a/{{ cookiecutter.__project_name_kebab_case }}/docker-compose.yml b/{{ cookiecutter.__project_name_kebab_case }}/docker-compose.yml deleted file mode 100644 index 07c1e58d..00000000 --- a/{{ cookiecutter.__project_name_kebab_case }}/docker-compose.yml +++ /dev/null @@ -1,80 +0,0 @@ -version: "3.9" - -services: - - devcontainer: - 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/ - - 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: . - target: app - {%- if cookiecutter.private_package_repository_name %} - secrets: - - poetry-auth - {%- endif %} - tty: true - {%- if cookiecutter.with_fastapi_api|int %} - ports: - - "8000:8000" - {%- endif %} - 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 deleted file mode 100644 index 8e602038..00000000 --- a/{{ cookiecutter.__project_name_kebab_case }}/pyproject.toml +++ /dev/null @@ -1,273 +0,0 @@ -[build-system] # https://python-poetry.org/docs/pyproject/#poetry-and-pep-517 -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" - -[project] # https://packaging.python.org/en/latest/specifications/pyproject-toml/ -name = "{{ cookiecutter.__project_name_kebab_case }}" -version = "0.0.0" -description = "{{ cookiecutter.project_description }}" -readme = "README.md" -license-files = ["LICENSE*"] -authors = [ - { name = "{{ cookiecutter.author_name }}", email = "{{ cookiecutter.author_email }}" }, -] -requires-python = ">={{ cookiecutter.python_version }},<4.0" -dependencies = [ - {%- if cookiecutter.with_fastapi_api|int %} - "fastapi[all] (>=0.115.6)", - "gunicorn (>=23.0.0)", - {%- endif %} - {%- if cookiecutter.project_type == "app" %} - "poethepoet (>=0.32.1)", - {%- endif %} - {%- if cookiecutter.with_typer_cli|int %} - "typer[all] (>=0.15.1)", - {%- endif %} - {%- if cookiecutter.with_fastapi_api|int %} - "uvicorn[standard] (>=0.34.0)", - {%- endif %} -] -{%- if cookiecutter.with_typer_cli|int %} - -[project.scripts] # https://python-poetry.org/docs/pyproject/#scripts -{{ cookiecutter.__project_name_kebab_case }} = "{{ cookiecutter.__project_name_snake_case }}.cli:app" -{%- endif %} - -[project.urls] # https://packaging.python.org/en/latest/specifications/well-known-project-urls/#well-known-labels -homepage = "{{ cookiecutter.project_url }}" -source = "{{ cookiecutter.project_url }}" -{%- if cookiecutter.with_conventional_commits|int %} -changelog = "{{ cookiecutter.project_url }}/{% if cookiecutter.continuous_integration == "GitLab" %}-/{% endif %}blob/main/CHANGELOG.md" -{%- endif %} -releasenotes = "{{ cookiecutter.project_url }}/{% if cookiecutter.continuous_integration == "GitLab" %}-/{% endif %}releases" -documentation = "{{ cookiecutter.project_url }}" -issues = "{{ cookiecutter.project_url }}/{% if cookiecutter.continuous_integration == "GitLab" %}-/{% endif %}issues" - -[tool.poetry.group.test.dependencies] # https://python-poetry.org/docs/managing-dependencies#dependency-groups -{%- if cookiecutter.with_conventional_commits|int %} -commitizen = ">=4.1.0" -{%- endif %} -coverage = { extras = ["toml"], version = ">=7.6.10" } -mypy = ">=1.14.1" -{%- if cookiecutter.project_type == "package" %} -poethepoet = ">=0.32.1" -{%- endif %} -pre-commit = ">=4.0.1" -pytest = ">=8.3.4" -pytest-mock = ">=3.14.0" -pytest-xdist = ">=3.6.1" -ruff = ">=0.9.2" -{%- if cookiecutter.development_environment == "strict" %} -shellcheck-py = ">=0.10.0.1" -typeguard = ">=4.4.1" -{%- endif %} - -[tool.poetry.group.dev.dependencies] # https://python-poetry.org/docs/managing-dependencies#dependency-groups -cruft = ">=2.16.0" -ipykernel = ">=6.29.4" -ipython = ">=8.18.0" -ipywidgets = ">=8.1.2" -pdoc = ">=15.0.1" -{%- 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 %} -{%- if cookiecutter.with_conventional_commits|int %} - -[tool.commitizen] # https://commitizen-tools.github.io/commitizen/config/ -bump_message = "bump: v$current_version → v$new_version" -tag_format = "v$version" -update_changelog_on_bump = true -version_provider = "pep621" -{%- endif %} - -[tool.coverage.report] # https://coverage.readthedocs.io/en/latest/config.html#report -{%- if cookiecutter.development_environment == "strict" %} -fail_under = 50 -{%- endif %} -precision = 1 -show_missing = true -skip_covered = true - -[tool.coverage.run] # https://coverage.readthedocs.io/en/latest/config.html#run -branch = true -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.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 -disallow_untyped_decorators = false -{%- endif %} -ignore_missing_imports = true -pretty = true -show_column_numbers = true -show_error_codes = true -show_error_context = true -warn_unreachable = true -{%- if cookiecutter.development_environment == "strict" and cookiecutter.with_fastapi_api|int %} - -[tool.pydantic-mypy] # https://pydantic-docs.helpmanual.io/mypy_plugin/#configuring-the-plugin -init_forbid_extra = true -init_typed = true -warn_required_dynamic_aliases = true -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" -{%- if cookiecutter.development_environment == "strict" %} -filterwarnings = ["error", "ignore::DeprecationWarning"] -{%- endif %} -testpaths = ["src", "tests"] -xfail_strict = true - -[tool.ruff] # https://docs.astral.sh/ruff/settings/ -fix = true -line-length = 100 -src = ["src", "tests"] -target-version = "py{{ cookiecutter.python_version.split('.')[:2]|join }}" - -[tool.ruff.format] -docstring-code-format = true -skip-magic-trailing-comma = true - -[tool.ruff.lint] -select = ["ALL"] -{%- if cookiecutter.development_environment == "strict" %} -ignore = ["CPY", "FIX", "ARG001", "COM812", "D203", "D213", "E501", "PD008", "PD009", "RET504", "S101", "TD003"] -{%- else %} -ignore = ["CPY", "FIX", "T20", "ARG001", "COM812", "D203", "D213", "E501", "PD008", "PD009", "PGH003", "RET504", "S101", "TD003"] -{%- endif %} -unfixable = ["ERA001", "F401", "F841", "T201", "T203"] - -[tool.ruff.lint.flake8-annotations] -allow-star-arg-any = true -{%- if cookiecutter.development_environment == "simple" %} -ignore-fully-untyped = true -{%- endif %} - -[tool.ruff.lint.flake8-tidy-imports] -ban-relative-imports = "all" - -[tool.ruff.lint.isort] -split-on-trailing-comma = false -{%- if cookiecutter.development_environment == "strict" %} - -[tool.ruff.lint.pycodestyle] -max-doc-length = 100 -{%- endif %} - -[tool.ruff.lint.pydocstyle] -convention = "{{ cookiecutter.__docstring_style|lower }}" - -[tool.poe.tasks] # https://github.com/nat-n/poethepoet -{%- if cookiecutter.with_fastapi_api|int %} - - [tool.poe.tasks.api] - help = "Serve the REST API" - shell = """ - if [ $dev ] - then { - uvicorn \ - --host $host \ - --port $port \ - --reload \ - {{ cookiecutter.__project_name_snake_case }}.api:app - } else { - gunicorn \ - --access-logfile - \ - --bind $host:$port \ - --graceful-timeout 10 \ - --keep-alive 10 \ - --log-file - \ - --timeout 30 \ - --worker-class uvicorn.workers.UvicornWorker \ - --worker-tmp-dir /dev/shm \ - --workers 2 \ - {{ cookiecutter.__project_name_snake_case }}.api:app - } fi - """ - - [[tool.poe.tasks.api.args]] - help = "Bind socket to this host (default: 0.0.0.0)" - name = "host" - options = ["--host"] - default = "0.0.0.0" - - [[tool.poe.tasks.api.args]] - help = "Bind socket to this port (default: 8000)" - name = "port" - options = ["--port"] - default = "8000" - - [[tool.poe.tasks.api.args]] - help = "Enable development mode" - 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" - cmd = """ - pdoc - --docformat $docformat - --output-directory $outputdirectory - {{ cookiecutter.__project_name_snake_case }} - """ - - [[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" - - [tool.poe.tasks.lint] - help = "Lint this {{ cookiecutter.project_type }}" - cmd = """ - pre-commit run - --all-files - --color always - """ - - [tool.poe.tasks.test] - help = "Test this {{ cookiecutter.project_type }}" - - [[tool.poe.tasks.test.sequence]] - cmd = "coverage run" - - [[tool.poe.tasks.test.sequence]] - cmd = "coverage report" - - [[tool.poe.tasks.test.sequence]] - cmd = "coverage xml" diff --git a/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/__init__.py b/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/__init__.py deleted file mode 100644 index 092b9828..00000000 --- a/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""{{ cookiecutter.project_name }}.""" 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 deleted file mode 100644 index c624edaf..00000000 --- a/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/api.py +++ /dev/null @@ -1,32 +0,0 @@ -"""{{ cookiecutter.project_name }} REST API.""" - -import asyncio -import logging -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager - -from fastapi import FastAPI - - -@asynccontextmanager -async def lifespan(app: FastAPI) -> AsyncGenerator[None]: - """Handle FastAPI startup and shutdown events.""" - # Startup events. - for handler in logging.root.handlers: - logging.root.removeHandler(handler) - yield - # Shutdown events. - - -app = FastAPI(lifespan=lifespan) - - -@app.get("/compute") -async def compute(n: int = 42) -> int: - """Compute the result of a CPU-bound function.""" - - 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 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 deleted file mode 100644 index 142b1860..00000000 --- a/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/cli.py +++ /dev/null @@ -1,12 +0,0 @@ -"""{{ cookiecutter.project_name }} CLI.""" - -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:") diff --git a/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/py.typed b/{{ cookiecutter.__project_name_kebab_case }}/src/{{ cookiecutter.__project_name_snake_case }}/py.typed deleted file mode 100644 index e69de29b..00000000 diff --git a/{{ cookiecutter.__project_name_kebab_case }}/tests/__init__.py b/{{ cookiecutter.__project_name_kebab_case }}/tests/__init__.py deleted file mode 100644 index 2c168973..00000000 --- a/{{ cookiecutter.__project_name_kebab_case }}/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""{{ cookiecutter.project_name }} test suite.""" diff --git a/{{ cookiecutter.__project_name_kebab_case }}/tests/test_api.py b/{{ cookiecutter.__project_name_kebab_case }}/tests/test_api.py deleted file mode 100644 index 34d0b5a9..00000000 --- a/{{ cookiecutter.__project_name_kebab_case }}/tests/test_api.py +++ /dev/null @@ -1,14 +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/test_cli.py b/{{ cookiecutter.__project_name_kebab_case }}/tests/test_cli.py deleted file mode 100644 index 7ff89f4f..00000000 --- a/{{ cookiecutter.__project_name_kebab_case }}/tests/test_cli.py +++ /dev/null @@ -1,15 +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/test_import.py b/{{ cookiecutter.__project_name_kebab_case }}/tests/test_import.py deleted file mode 100644 index 9cdcbcc0..00000000 --- a/{{ cookiecutter.__project_name_kebab_case }}/tests/test_import.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)